diff --git a/app/models/enable_banking_account/processor.rb b/app/models/enable_banking_account/processor.rb index a6b546f0a..1a7f96539 100644 --- a/app/models/enable_banking_account/processor.rb +++ b/app/models/enable_banking_account/processor.rb @@ -41,29 +41,32 @@ class EnableBankingAccount::Processor available_credit = nil # For liability accounts, ensure balance sign is correct. - # DELIBERATE UX DECISION: For CreditCards, we display the available credit (credit_limit - outstanding debt) - # rather than the raw outstanding debt. Do not revert this behavior, as future maintainers should understand - # users expect to see how much credit they have left rather than their debt balance. - # The 'available_credit' calculation overrides the 'balance' variable. - if account.accountable_type == "Loan" - balance = balance.abs - elsif account.accountable_type == "CreditCard" - if enable_banking_account.credit_limit.present? - available = enable_banking_account.credit_limit - balance.abs - available_credit = [ available, 0 ].max - balance = available_credit - unless account.accountable.present? - Rails.logger.warn "EnableBankingAccount::Processor - CreditCard accountable missing for account #{account.id}" + # For CreditCards, we expect the main balance to reflect the absolute outstanding debt + # rather than available credit, to ensure net worth calculations handle the liability accurately. + # Any available credit metrics (from limits) are instead stored safely as metadata on the Accountable. + # Loans and CreditCards must always represent their outstanding balance as an absolute + # positive debt amount, regardless of the API's reported sign, to ensure the BalanceSheet + # calculates net worth accurately. + if account.accountable_type == "Loan" || account.accountable_type == "CreditCard" + # Standardize the raw balance to an absolute positive debt + outstanding_debt = balance.abs + + # Override the top-level balance variable intended for the account + balance = outstanding_debt + + if account.accountable_type == "CreditCard" + if enable_banking_account.credit_limit.present? + # Compute available credit based on the strictly positive outstanding debt + available = enable_banking_account.credit_limit - outstanding_debt + available_credit = [ available, 0 ].max + unless account.accountable.present? + Rails.logger.warn "EnableBankingAccount::Processor - CreditCard accountable missing for account #{account.id}" + end + elsif account.accountable&.available_credit.present? + # Fallback: no credit_limit from API — compute it using available_credit defined at account level + Rails.logger.info "Using stored available_credit fallback for account #{account.id}" + available_credit = account.accountable.available_credit end - elsif account.accountable&.available_credit.present? - # Fallback: no credit_limit from API — compute it using available_credit defined at account level - Rails.logger.info "Using stored available_credit fallback for account #{account.id}" - available_credit = account.accountable.available_credit - outstanding = balance.abs - balance = [ available_credit - outstanding, 0 ].max - else - # Fallback: no credit_limit from API — display raw outstanding balance - # We cannot derive available credit without knowing the limit; leave balance unchanged. end end diff --git a/test/models/enable_banking_account/processor_test.rb b/test/models/enable_banking_account/processor_test.rb index 6bdbebda1..ab4c41b1b 100644 --- a/test/models/enable_banking_account/processor_test.rb +++ b/test/models/enable_banking_account/processor_test.rb @@ -42,7 +42,7 @@ class EnableBankingAccount::ProcessorTest < ActiveSupport::TestCase assert_nil result end - test "sets CC balance to available_credit when credit_limit is present" do + test "sets CC balance as absolute debt and tracks available_credit when limit is present" do cc_account = accounts(:credit_card) @enable_banking_account.update!( current_balance: 450.00, @@ -53,13 +53,13 @@ class EnableBankingAccount::ProcessorTest < ActiveSupport::TestCase EnableBankingAccount::Processor.new(@enable_banking_account).process - assert_equal 550.0, cc_account.reload.cash_balance + assert_equal 450.0, cc_account.reload.cash_balance if cc_account.accountable.respond_to?(:available_credit) assert_equal 550.0, cc_account.accountable.reload.available_credit end end - test "falls back to stored available_credit when credit_limit is absent" do + test "sets CC balance as absolute debt and keeps stored available_credit when limit absent" do cc_account = accounts(:credit_card) cc_account.accountable.update!(available_credit: 1000.0) @@ -70,10 +70,11 @@ class EnableBankingAccount::ProcessorTest < ActiveSupport::TestCase EnableBankingAccount::Processor.new(@enable_banking_account).process - assert_equal 700.0, cc_account.reload.cash_balance + assert_equal 300.0, cc_account.reload.cash_balance + assert_equal 1000.0, cc_account.accountable.reload.available_credit end - test "sets CC balance to raw outstanding when credit_limit is absent" do + test "sets CC balance to absolute debt when both limit and stored available_credit are absent" do cc_account = accounts(:credit_card) cc_account.accountable.update!(available_credit: nil)