Implement SimpleFin API client and data models

- Add SimplefinItem model with sync capabilities and encryption
- Add SimplefinAccount model for account data mapping
- Implement Provider::Simplefin API client with token exchange
- Add SimpleFin protocol support with proper error handling
- Include sync jobs, importers, and processors for data flow
- Add family SimpleFin connectivity mixin
This commit is contained in:
Sholom Ber
2025-08-07 12:39:29 -04:00
parent 9561e73e17
commit 8497703518
10 changed files with 514 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
class SimplefinAccount::Processor
attr_reader :simplefin_account
def initialize(simplefin_account)
@simplefin_account = simplefin_account
end
def process
ensure_account_exists
process_transactions
end
private
def ensure_account_exists
return if simplefin_account.account.present?
account = Account.create_from_simplefin_account(simplefin_account)
simplefin_account.update!(account: account)
end
def process_transactions
return unless simplefin_account.raw_transactions_payload.present?
account = simplefin_account.account
transactions_data = simplefin_account.raw_transactions_payload
transactions_data.each do |transaction_data|
process_transaction(account, transaction_data)
end
end
def process_transaction(account, transaction_data)
# Convert SimpleFin transaction to internal Transaction format
amount_cents = parse_amount(transaction_data[:amount], account.currency)
posted_date = parse_date(transaction_data[:posted])
transaction_attributes = {
account: account,
name: transaction_data[:description] || "Unknown transaction",
amount: Money.new(amount_cents, account.currency),
date: posted_date,
currency: account.currency,
raw_data: transaction_data
}
# Use external ID to prevent duplicates
external_id = "simplefin_#{transaction_data[:id]}"
Transaction.find_or_create_by(external_id: external_id) do |transaction|
transaction.assign_attributes(transaction_attributes)
end
rescue => e
Rails.logger.error("Failed to process SimpleFin transaction #{transaction_data[:id]}: #{e.message}")
# Don't fail the entire sync for one bad transaction
end
def parse_amount(amount_value, currency)
case amount_value
when String
(BigDecimal(amount_value) * 100).to_i
when Numeric
(amount_value * 100).to_i
else
0
end
rescue ArgumentError
0
end
def parse_date(date_value)
case date_value
when String
Date.parse(date_value)
when Integer
# Unix timestamp
Time.at(date_value).to_date
else
Date.current
end
rescue ArgumentError, TypeError
Date.current
end
end