mirror of
https://github.com/we-promise/sure.git
synced 2026-04-08 14:54:49 +00:00
* Implement providers factory * Multiple providers sync support - Proper Multi-Provider Syncing: When you click sync on an account with multiple providers (e.g., both Plaid and SimpleFin), all provider items are synced - Better API: The existing account.providers method already returns all providers, and account.provider returns the first one for backward compatibility - Correct Holdings Deletion Logic: Holdings can only be deleted if ALL providers allow it, preventing accidental deletions that would be recreated on next sync TODO: validate this is the way we want to go? We would need to check holdings belong to which account, and then check provider allows deletion. More complex - Database Constraints: The existing validations ensure an account can have at most one provider of each type (one PlaidAccount, one SimplefinAccount, etc.) * Add generic provider_import_adapter * Finish unified import strategy * Update app/models/plaid_account.rb Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: soky srm <sokysrm@gmail.com> * Update app/models/provider/factory.rb Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: soky srm <sokysrm@gmail.com> * Fix account linked by plaid_id instead of external_id * Parse numerics to BigDecimal Parse numerics to BigDecimal before computing amount; guard nils. Avoid String * String and float drift; also normalize date. * Fix incorrect usage of assert_raises. * Fix linter * Fix processor test. * Update current_balance_manager.rb * Test fixes * Fix plaid linked account test * Add support for holding per account_provider * Fix proper account access Also fix account deletion for simpefin too * FIX match tests for consistency * Some more factory updates * Fix account schema for multipe providers Can do: - Account #1 → PlaidAccount + SimplefinAccount (multiple different providers) - Account #2 → PlaidAccount only - Account #3 → SimplefinAccount only Cannot do: - Account #1 → PlaidAccount + PlaidAccount (duplicate provider type) - PlaidAccount #123 → Account #1 + Account #2 (provider linked to multiple accounts) * Fix account setup - An account CAN have multiple providers (the schema shows account_providers with unique index on [account_id, provider_type]) - Each provider should maintain its own separate entries - We should NOT update one provider's entry when another provider syncs * Fix linter and guard migration * FIX linter issues. * Fixes - Remove duplicated index - Pass account_provider_id - Guard holdings call to avoid NoMethodError * Update schema and provider import fix * Plaid doesn't allow holdings deletion * Use ClimateControl for proper env setup * No need for this in .git --------- Signed-off-by: soky srm <sokysrm@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
146 lines
5.4 KiB
Ruby
146 lines
5.4 KiB
Ruby
class Account::CurrentBalanceManager
|
|
InvalidOperation = Class.new(StandardError)
|
|
|
|
Result = Struct.new(:success?, :changes_made?, :error, keyword_init: true)
|
|
|
|
def initialize(account)
|
|
@account = account
|
|
end
|
|
|
|
def has_current_anchor?
|
|
current_anchor_valuation.present?
|
|
end
|
|
|
|
# Our system should always make sure there is a current anchor, and that it is up to date.
|
|
# The fallback is provided for backwards compatibility, but should not be relied on since account.balance is a "cached/derived" value.
|
|
def current_balance
|
|
if current_anchor_valuation
|
|
current_anchor_valuation.entry.amount
|
|
else
|
|
Rails.logger.warn "No current balance anchor found for account #{account.id}. Using cached balance instead, which may be out of date."
|
|
account.balance
|
|
end
|
|
end
|
|
|
|
def current_date
|
|
if current_anchor_valuation
|
|
current_anchor_valuation.entry.date
|
|
else
|
|
Date.current
|
|
end
|
|
end
|
|
|
|
def set_current_balance(balance)
|
|
if account.linked?
|
|
result = set_current_balance_for_linked_account(balance)
|
|
else
|
|
result = set_current_balance_for_manual_account(balance)
|
|
end
|
|
|
|
# Update cache field so changes appear immediately to the user
|
|
account.update!(balance: balance)
|
|
|
|
result
|
|
rescue => e
|
|
Result.new(success?: false, changes_made?: false, error: e.message)
|
|
end
|
|
|
|
private
|
|
attr_reader :account
|
|
|
|
def opening_balance_manager
|
|
@opening_balance_manager ||= Account::OpeningBalanceManager.new(account)
|
|
end
|
|
|
|
def reconciliation_manager
|
|
@reconciliation_manager ||= Account::ReconciliationManager.new(account)
|
|
end
|
|
|
|
# Manual accounts do not manage the `current_anchor` valuation (otherwise, user would need to continually update it, which is bad UX)
|
|
# Instead, we use a combination of "auto-update strategies" to set the current balance according to the user's intent.
|
|
#
|
|
# The "auto-update strategies" are:
|
|
# 1. Value tracking - If the account has a reconciliation already, we assume they are tracking the account value primarily with reconciliations, so we append a new one
|
|
# 2. Transaction adjustment - If the account doesn't have recons, we assume user is tracking with transactions, so we adjust the opening balance with a delta until it
|
|
# gets us to the desired balance. This ensures we don't append unnecessary reconciliations to the account, which "reset" the value from that
|
|
# date forward (not user's intent).
|
|
#
|
|
# For more documentation on these auto-update strategies, see the test cases.
|
|
def set_current_balance_for_manual_account(balance)
|
|
# If we're dealing with a cash account that has no reconciliations, use "Transaction adjustment" strategy (update opening balance to "back in" to the desired current balance)
|
|
if account.balance_type == :cash && account.valuations.reconciliation.empty?
|
|
adjust_opening_balance_with_delta(new_balance: balance, old_balance: account.balance)
|
|
else
|
|
existing_reconciliation = account.entries.valuations.find_by(date: Date.current)
|
|
|
|
result = reconciliation_manager.reconcile_balance(balance: balance, date: Date.current, existing_valuation_entry: existing_reconciliation)
|
|
|
|
# Normalize to expected result format
|
|
Result.new(success?: result.success?, changes_made?: true, error: result.error_message)
|
|
end
|
|
end
|
|
|
|
def adjust_opening_balance_with_delta(new_balance:, old_balance:)
|
|
delta = new_balance - old_balance
|
|
|
|
result = opening_balance_manager.set_opening_balance(balance: account.opening_anchor_balance + delta)
|
|
|
|
# Normalize to expected result format
|
|
Result.new(success?: result.success?, changes_made?: true, error: result.error)
|
|
end
|
|
|
|
# Linked accounts manage "current balance" via the special `current_anchor` valuation.
|
|
# This is NOT a user-facing feature, and is primarily used in "processors" while syncing
|
|
# linked account data (e.g. via Plaid)
|
|
def set_current_balance_for_linked_account(balance)
|
|
if current_anchor_valuation
|
|
changes_made = update_current_anchor(balance)
|
|
Result.new(success?: true, changes_made?: changes_made, error: nil)
|
|
else
|
|
create_current_anchor(balance)
|
|
Result.new(success?: true, changes_made?: true, error: nil)
|
|
end
|
|
end
|
|
|
|
def current_anchor_valuation
|
|
@current_anchor_valuation ||= account.valuations.current_anchor.includes(:entry).first
|
|
end
|
|
|
|
def create_current_anchor(balance)
|
|
entry = account.entries.create!(
|
|
date: Date.current,
|
|
name: Valuation.build_current_anchor_name(account.accountable_type),
|
|
amount: balance,
|
|
currency: account.currency,
|
|
entryable: Valuation.new(kind: "current_anchor")
|
|
)
|
|
|
|
# Reload associations and clear memoized value so it gets the new anchor
|
|
account.valuations.reload
|
|
@current_anchor_valuation = nil
|
|
end
|
|
|
|
def update_current_anchor(balance)
|
|
changes_made = false
|
|
|
|
ActiveRecord::Base.transaction do
|
|
# Update associated entry attributes
|
|
entry = current_anchor_valuation.entry
|
|
|
|
if entry.amount != balance
|
|
entry.amount = balance
|
|
changes_made = true
|
|
end
|
|
|
|
if entry.date != Date.current
|
|
entry.date = Date.current
|
|
changes_made = true
|
|
end
|
|
|
|
entry.save! if entry.changed?
|
|
end
|
|
|
|
changes_made
|
|
end
|
|
end
|