Files
sure/app/models/provider/simplefin.rb
LPW 664c6c2b7c Pending detection, FX metadata, Pending UI badge. (#374)
* - Add support for `SIMPLEFIN_INCLUDE_PENDING` to control pending behavior via ENV.
- Enhance debug logging for SimpleFin API requests and raw payloads.
- Refine pending flag handling in `SimplefinEntry::Processor` based on provider data and inferred conditions.
- Improve FX metadata processing for transactions with currency mismatches.
- Add new tests for pending detection, FX metadata, and edge cases involving `posted` values.
- Add pending indicator UI to transaction view.

* Document pending transaction detection, storage, and UI behavior for SimpleFIN and Plaid integrations. Add debug flags for troubleshooting.

* Add `pending?` method to `Transaction` model, refactor UI indicator, and centralize SimpleFIN configuration

- Introduced `pending?` method in `Transaction` for unified pending state detection.
- Refactored transaction pending indicator in the UI to use `pending?` method.
- Centralized SimpleFIN configuration in initializer with ENV-backed toggles.
- Updated tests for `pending?` behavior and clarified docs for pending detection logic

* Add SimpleFIN debug and runtime flags to `.env.local.example` and `.env.test.example`

- Introduced `SIMPLEFIN_INCLUDE_PENDING` and `SIMPLEFIN_DEBUG_RAW` flags for controlling pending behavior and debugging.
- Updated example environment files with descriptions for new configuration options.

* Normalize formatting for `SIMPLEFIN_INCLUDE_PENDING` and `SIMPLEFIN_DEBUG_RAW` flags in `.env.local.example` and `.env.test.example`.

---------

Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
2025-12-19 23:24:48 +01:00

101 lines
3.5 KiB
Ruby

class Provider::Simplefin
# Pending: some institutions do not return pending transactions even with `pending=1`.
# This is provider variability (not a bug). For troubleshooting, you can set
# `SIMPLEFIN_INCLUDE_PENDING=1` and/or `SIMPLEFIN_DEBUG_RAW=1` (both default-off).
# These are centralized in `Rails.configuration.x.simplefin.*` via
# `config/initializers/simplefin.rb`.
include HTTParty
headers "User-Agent" => "Sure Finance SimpleFin Client"
default_options.merge!(verify: true, ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER, timeout: 120)
def initialize
end
def claim_access_url(setup_token)
# Decode the base64 setup token to get the claim URL
claim_url = Base64.decode64(setup_token)
response = HTTParty.post(claim_url)
case response.code
when 200
# The response body contains the access URL with embedded credentials
response.body.strip
when 403
raise SimplefinError.new("Setup token may be compromised, expired, or already used", :token_compromised)
else
raise SimplefinError.new("Failed to claim access URL: #{response.code} #{response.message}", :claim_failed)
end
end
def get_accounts(access_url, start_date: nil, end_date: nil, pending: nil)
# Build query parameters
query_params = {}
# SimpleFin expects Unix timestamps for dates
if start_date
start_timestamp = start_date.to_time.to_i
query_params["start-date"] = start_timestamp.to_s
end
if end_date
end_timestamp = end_date.to_time.to_i
query_params["end-date"] = end_timestamp.to_s
end
query_params["pending"] = pending ? "1" : "0" unless pending.nil?
accounts_url = "#{access_url}/accounts"
accounts_url += "?#{URI.encode_www_form(query_params)}" unless query_params.empty?
# The access URL already contains HTTP Basic Auth credentials
begin
response = HTTParty.get(accounts_url)
rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e
Rails.logger.error "SimpleFin API: GET /accounts failed: #{e.class}: #{e.message}"
raise SimplefinError.new("Exception during GET request: #{e.message}", :request_failed)
rescue => e
Rails.logger.error "SimpleFin API: Unexpected error during GET /accounts: #{e.class}: #{e.message}"
raise SimplefinError.new("Exception during GET request: #{e.message}", :request_failed)
end
case response.code
when 200
JSON.parse(response.body, symbolize_names: true)
when 400
Rails.logger.error "SimpleFin API: Bad request - #{response.body}"
raise SimplefinError.new("Bad request to SimpleFin API: #{response.body}", :bad_request)
when 403
raise SimplefinError.new("Access URL is no longer valid", :access_forbidden)
when 402
raise SimplefinError.new("Payment required to access this account", :payment_required)
else
Rails.logger.error "SimpleFin API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
raise SimplefinError.new("Failed to fetch accounts: #{response.code} #{response.message} - #{response.body}", :fetch_failed)
end
end
def get_info(base_url)
response = HTTParty.get("#{base_url}/info")
case response.code
when 200
response.body.strip.split("\n")
else
raise SimplefinError.new("Failed to get server info: #{response.code} #{response.message}", :info_failed)
end
end
class SimplefinError < StandardError
attr_reader :error_type
def initialize(message, error_type = :unknown)
super(message)
@error_type = error_type
end
end
end