1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 13:19:39 +02:00

Plaid sync domain improvements (#2267)
Some checks are pending
Publish Docker image / ci (push) Waiting to run
Publish Docker image / Build docker image (push) Blocked by required conditions

Breaks our Plaid sync process out into more manageable classes. Notably, this moves the sync process to a distinct, 2-step flow:

1. Import stage - we first make API calls and import Plaid data to "mirror" tables
2. Processing stage - read the raw data, apply business rules, build internal domain models and sync balances

This provides several benefits:

- Plaid syncs can now be "replayed" without fetching API data again
- Mirror tables provide better audit and debugging capabilities
- Eliminates the "all or nothing" sync behavior that is currently in place, which is brittle
This commit is contained in:
Zach Gollwitzer 2025-05-23 18:58:22 -04:00 committed by GitHub
parent 5c82af0e8c
commit 03a146222d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 3763 additions and 706 deletions

View file

@ -0,0 +1,99 @@
class PlaidAccount::Processor
include PlaidAccount::TypeMappable
attr_reader :plaid_account
def initialize(plaid_account)
@plaid_account = plaid_account
end
# Each step represents a different Plaid API endpoint / "product"
#
# Processing the account is the first step and if it fails, we halt the entire processor
# Each subsequent step can fail independently, but we continue processing the rest of the steps
def process
process_account!
process_transactions
process_investments
process_liabilities
end
private
def family
plaid_account.plaid_item.family
end
# Shared securities reader and resolver
def security_resolver
@security_resolver ||= PlaidAccount::Investments::SecurityResolver.new(plaid_account)
end
def process_account!
PlaidAccount.transaction do
account = family.accounts.find_or_initialize_by(
plaid_account_id: plaid_account.id
)
# Name and subtype are the only attributes a user can override for Plaid accounts
account.enrich_attributes(
{
name: plaid_account.name,
subtype: map_subtype(plaid_account.plaid_type, plaid_account.plaid_subtype)
},
source: "plaid"
)
account.assign_attributes(
accountable: map_accountable(plaid_account.plaid_type),
balance: balance_calculator.balance,
currency: plaid_account.currency,
cash_balance: balance_calculator.cash_balance
)
account.save!
end
end
def process_transactions
PlaidAccount::Transactions::Processor.new(plaid_account).process
rescue => e
report_exception(e)
end
def process_investments
PlaidAccount::Investments::TransactionsProcessor.new(plaid_account, security_resolver: security_resolver).process
PlaidAccount::Investments::HoldingsProcessor.new(plaid_account, security_resolver: security_resolver).process
rescue => e
report_exception(e)
end
def process_liabilities
case [ plaid_account.plaid_type, plaid_account.plaid_subtype ]
when [ "credit", "credit card" ]
PlaidAccount::Liabilities::CreditProcessor.new(plaid_account).process
when [ "loan", "mortgage" ]
PlaidAccount::Liabilities::MortgageProcessor.new(plaid_account).process
when [ "loan", "student" ]
PlaidAccount::Liabilities::StudentLoanProcessor.new(plaid_account).process
end
rescue => e
report_exception(e)
end
def balance_calculator
if plaid_account.plaid_type == "investment"
@balance_calculator ||= PlaidAccount::Investments::BalanceCalculator.new(plaid_account, security_resolver: security_resolver)
else
OpenStruct.new(
balance: plaid_account.current_balance || plaid_account.available_balance,
cash_balance: plaid_account.available_balance || 0
)
end
end
def report_exception(error)
Sentry.capture_exception(error) do |scope|
scope.set_tags(plaid_account_id: plaid_account.id)
end
end
end