Skip to content

Conversation

@luckyPipewrench
Copy link
Collaborator

@luckyPipewrench luckyPipewrench commented Dec 3, 2025

  • Introduces SimplefinAccount::Liabilities::OverpaymentAnalyzer that classifies a SimpleFIN liability balance as:
    • :credit → overpayment/credit state → stored as negative
    • :debt → owe money → stored as positive
    • :unknown → ambiguous → fall back to existing sign-normalization
  • Integrates the heuristic in both balance write paths:
    • SimplefinAccount::Processor#process_account! (regular syncs)
    • SimplefinItem::Importer#import_account_minimal_and_balance (balances-only discovery)
  • Enables the heuristic by default. It can be explicitly disabled via:
    • Setting["simplefin_cc_overpayment_detection"] = "false"
    • SIMPLEFIN_CC_OVERPAYMENT_HEURISTIC=false
  • Adds INFO logs for all outcomes (credit/debt) and also unknown before fallback; includes compact metrics for diagnosis.

Why

SimpleFIN reports all liabilities as negative, regardless of whether it’s normal debt or an overpayment/credit. That breaks sign semantics in the app (we show debt as positive, credit as negative). Transaction history provides the signal needed to disambiguate reliably.

Heuristic details (production-safe)

  • Inputs and window
    • Prefers imported Entry records within a window (default 120 days)
    • If none/sparse, falls back to provider raw transactions (negates amounts to app convention)
  • Metrics and decision
    • charges_total = sum(amount for amount > 0)
    • payments_total = sum(-amount for amount < 0)
    • If payments exceed charges by about |observed| (with epsilon slack), classify :credit
    • Else if charges exceed payments, classify :debt
    • Else :unknown
  • Guards and stability
    • Near-zero observed balance guard (epsilon base)
    • Statement-boundary guard window
    • Sticky cache (days) to avoid flapping; only re-evaluate when necessary
  • Fallback
    • On :unknown or any analyzer error, reuses the existing sign-only normalization rules

Edge cases and how they’re handled

  • No/very sparse transactions in the window → returns :unknown → fallback logic applies (unchanged behavior)
  • Near zero balances that can flutter around statement dates → guarded by epsilon; returns :unknown to avoid churn
  • Mixed-sign SimpleFIN fields (balance vs available-balance) → if analyzer returns :unknown, legacy mixed-sign logic still resolves consistently
  • First-run/balances-only discovery (no entries yet) → importer uses raw provider payload fallback with sign conversion
  • Mislinked account type (asset linked but inferred as liability) → mapper inference still forces liability normalization path (existing behavior preserved)
  • Analyzer exceptions (parsing, bad payload) → caught and fallback applied; no user-visible errors
  • Sticky classification could lag a genuine flip → bounded by short sticky TTL; strong contrary evidence reclassifies

Observability

  • Regular processor path: INFO log on credit/debt classifications with {charges_total, payments_total, tx_count}
  • On unknown: INFO log with {reason, tx_count, charges_total, payments_total, observed} before falling back
  • Same unknown log in balances-only importer path for parity

Backward compatibility and safety

  • Default behavior is improved and on by default; explicit toggle exists for fast rollback
  • When disabled or insufficient evidence, legacy normalization behavior is preserved
  • No schema changes, no migrations, no external dependencies

Known limitations (acceptable trade-offs)

  • Extremely atypical accounts where transfers/payments are misclassified in source data can confuse the metric; sticky TTL plus fallback minimize user impact
  • Multi-currency edge cases rely on app-wide currency normalization already in place; analyzer follows existing conventions
  • Heuristic relies on a finite window; very old overpayments without recent evidence will be treated via fallback

Rollback/Toggle

  • Runtime: Setting["simplefin_cc_overpayment_detection"] = "false"
  • Environment: SIMPLEFIN_CC_OVERPAYMENT_HEURISTIC=false (restart processes)
image

Summary by CodeRabbit

  • Bug Fixes
    • Improved liability balance normalization: sign-aware handling, preferring posted over available amounts, and resilient fallbacks when heuristics or analyzers fail.
  • New Features
    • Added an overpayment analyzer to classify liabilities as credit/debt/unknown and adjust normalized balances.
  • Tests
    • New unit tests covering liability normalization, overpayment classification, and fallback paths; fixtures updated for test isolation.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Walkthrough

Adds liability-aware balance normalization across SimpleFIN importer and processor, introduces a new OverpaymentAnalyzer that classifies liability balances using recent transactions with caching and fallbacks, extracts numeric helpers into a concern, and adds tests plus empty fixtures for the analyzer and processor scenarios.

Changes

Cohort / File(s) Change Summary
SimpleFIN Account Processor
app/models/simplefin_account/processor.rb
Reworked balance normalization: decimalizes bal/avail, computes observed, infers liability via linked account or mapper (with warning), integrates OverpaymentAnalyzer (with resilient fallbacks), preserves investment handling, and adds normalize_liability_balance plus numeric helpers.
SimpleFIN Item Importer
app/models/simplefin_item/importer.rb
Added liability-aware normalization in import_account_minimal_and_balance: converts balances to decimals, infers liability, optionally calls OverpaymentAnalyzer (feature-flagged), falls back to sign-based normalization, and sets normalized balance and cash_balance; includes to_decimal and same_sign? helpers.
Overpayment Analyzer (new)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb
New class SimplefinAccount::Liabilities::OverpaymentAnalyzer that gathers recent transactions (Entries or provider raw payload), computes metrics (charges/payments/net/counts, recency), classifies as :credit, :debt, or :unknown using configurable heuristics, and uses a sticky cache to persist positive classification hints.
Numeric Helpers (new concern)
app/models/concerns/simplefin_numeric_helpers.rb
New concern exposing to_decimal and same_sign? for robust numeric conversion and sign checks; included where needed.
Tests & Fixtures
test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb, test/models/simplefin_account_processor_test.rb, test/fixtures/simplefin_accounts.yml, test/fixtures/simplefin_items.yml
Added analyzer tests (credit/debt/unknown and raw-payload fallback), processor tests for multiple liability sign scenarios and mapper-inferred behavior, and empty fixtures to ensure table truncation during tests.

Sequence Diagram(s)

sequenceDiagram
  participant Import as Importer/Processor
  participant Analyzer as OverpaymentAnalyzer
  participant DB as Entries DB
  participant Payload as Provider Payload
  participant Cache as Rails.cache

  Import->>Analyzer: call(observed_balance)
  alt analyzer enabled
    Analyzer->>DB: query recent Entries (amounts, dates)
    alt entries present
      DB-->>Analyzer: entries
    else no entries
      Analyzer->>Payload: read raw_transactions_payload
      Payload-->>Analyzer: provider transactions
    end
    Analyzer->>Analyzer: compute metrics & classification
    Analyzer->>Cache: read/write sticky classification
    Analyzer-->>Import: Result(classification, reason, metrics)
  else disabled or error
    Analyzer-->>Import: Result(:unknown)
  end
  Import->>Import: apply normalization (liability/investment/fallback)
  Import->>DB: adapter.update_balance / persist normalized values
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect OverpaymentAnalyzer heuristics, thresholds, window and sticky-cache behavior.
  • Verify numeric conversions and sign logic in to_decimal, same_sign?, and normalize_liability_balance.
  • Confirm error/rescue paths, feature-flag behavior, and deterministic test timing/config.

Possibly related PRs

Suggested reviewers

  • jjmata

Poem

🐇 I nibbled through ledgers, flipped curious signs,
Sniffed payments and charges, traced faint little lines.
I cached a bright hint, nudged balances just right,
Hopped past edge-cases into soft evening light.
Carrots for tests — tidy books and moonlit bites. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding overpayment detection for SimpleFIN liabilities with heuristic-based classification and fallbacks, which aligns with all major features introduced in the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@luckyPipewrench luckyPipewrench marked this pull request as ready for review December 3, 2025 18:33
@luckyPipewrench luckyPipewrench marked this pull request as draft December 3, 2025 18:33
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/models/simplefin_account/processor.rb (1)

40-62: Fix nil balance usage and align process_account! with SimpleFIN liability heuristic

process_account! currently uses balance without ever initializing it, which directly explains both pipeline failures:

  • Line 49: balance = -balance raises NoMethodError: undefined method '-@' for nil.
  • Line 60: account.update!(balance: balance, ...) hits Validation failed: Balance can't be blank.

On top of that, this method still uses the old “always invert for liabilities” rule, while the importer now has a dual-field heuristic using both balance and available-balance. That means overpayment edge cases remain incorrect on the full processing path.

You can fix all three issues (nil bug, validation error, and heuristic mismatch) by computing balance locally with the same dual-field logic already used in SimplefinItem::Importer#import_account_minimal_and_balance, leveraging the new helpers:

-      # Update account balance and cash balance from latest SimpleFin data
-      account = simplefin_account.current_account
-
-      # Normalize balances for liabilities (SimpleFIN typically uses opposite sign)
-      # App convention:
-      # - Liabilities: positive => you owe; negative => provider owes you (overpayment/credit)
-      # Since providers often send the opposite sign, ALWAYS invert for liabilities so
-      # that both debt and overpayment cases are represented correctly.
-      if [ "CreditCard", "Loan" ].include?(account.accountable_type)
-        balance = -balance
-      end
+      # Update account balance and cash balance from latest SimpleFin data
+      account = simplefin_account.current_account
+
+      # Normalize balances for liabilities using both current_balance and available_balance.
+      # App convention:
+      # - Liabilities: positive => you owe; negative => provider owes you (overpayment/credit)
+      # Many SimpleFIN institutions send opposite or inconsistent signs across these fields,
+      # so use a dual-field heuristic to map both debt and overpayment correctly.
+      bal   = to_decimal(simplefin_account.current_balance)
+      avail = to_decimal(simplefin_account.available_balance)
+      observed = bal.nonzero? ? bal : avail
+
+      balance = observed
+      if [ "CreditCard", "Loan" ].include?(account.accountable_type)
+        both_present = bal.nonzero? && avail.nonzero?
+        if both_present && same_sign?(bal, avail)
+          if bal.positive? && avail.positive?
+            # Both positive → upstream overpayment/credit → represent as negative
+            balance = -observed.abs
+          elsif bal.negative? && avail.negative?
+            # Both negative → upstream debt already negative → represent as positive
+            balance = observed.abs
+          end
+        else
+          # Mixed signs or only one side present → simple invert to respect liability convention
+          balance = -observed
+        end
+      end
@@
-      # Calculate cash balance correctly for investment accounts
-      cash_balance = if account.accountable_type == "Investment"
+      # Calculate cash balance correctly for investment accounts
+      cash_balance = if account.accountable_type == "Investment"
         calculator = SimplefinAccount::Investments::BalanceCalculator.new(simplefin_account)
         calculator.cash_balance
       else
         balance
       end

This ensures balance is always a concrete BigDecimal (never nil), matches the importer’s normalization behavior for SimpleFIN liabilities, and keeps investment cash_balance behavior unchanged.

🧹 Nitpick comments (3)
app/models/simplefin_account/processor.rb (1)

101-115: Wire up or centralize numeric helpers to avoid dead code and drift

The new helpers:

def to_decimal(value)
  ...
end

def same_sign?(a, b)
  ...
end

are currently unused in this class, while identical definitions exist (and are used) in SimplefinItem::Importer. That creates two problems:

  • They don’t help fix the current process_account! bug unless actually used.
  • Duplication across classes risks subtle drift in normalization behavior over time.

Once you wire these helpers into process_account! (as suggested above), consider extracting them into a small shared SimpleFIN-focused module (e.g., Simplefin::NumericHelpers) and including it in both SimplefinAccount::Processor and SimplefinItem::Importer, so the normalization logic stays in sync.

app/models/simplefin_item/importer.rb (2)

109-151: Heuristic looks solid; consider adding mismatch logging and simplifying cash

The liability normalization here is well-structured:

  • Converts both balance fields to BigDecimal with to_decimal.
  • Uses linked acct.accountable_type OR Simplefin::AccountTypeMapper.infer to decide liability status.
  • Applies a dual-field sign heuristic so:
    • Mixed-sign balances (e.g., negative balance, positive available-balance) invert to positive debt.
    • Both-positive cases invert to negative (overpayment/credit).
    • Both-negative cases become positive (debt).

Two small follow-ups you might want to consider:

  1. Surface mapper vs linked type disagreements

    Right now, is_liability is simply is_linked_liability || is_mapper_liability. When the mapper infers CreditCard/Loan but acct.accountable_type is something else (or vice versa), that’s useful observability signal for debugging mislinked accounts.

    You could optionally log or register a hint when they disagree, e.g.:

  •    is_mapper_liability = inferred && [ "CreditCard", "Loan" ].include?(inferred.accountable_type)
    
  •    is_liability = is_linked_liability || is_mapper_liability
    
  •    mapper_type = inferred&.accountable_type
    
  •    is_mapper_liability = [ "CreditCard", "Loan" ].include?(mapper_type)
    
  •    is_liability = is_linked_liability || is_mapper_liability
    
  •    if is_liability && is_linked_liability != is_mapper_liability
    
  •      Rails.logger.info(
    
  •        "SimpleFin liability type mismatch for acct=#{acct.id}: " \
    
  •        "linked=#{acct.accountable_type.inspect} inferred=#{mapper_type.inspect}"
    
  •      )
    
  •    end
    
    
    (Or feed this into `register_error`/stats if you prefer UI surfacing.)
    
    
  1. Simplify cash assignment

    The cash branch currently returns normalized in both cases:

    cash = if acct.accountable_type == "Investment"
      # Leave investment cash to investment calculators in full run
      normalized
    else
      normalized
    end

    Since both branches are identical, this can safely collapse to:

    cash = normalized

    and, if the comment is still relevant for future behavior, move it above that line.


768-781: Deduplicate numeric helpers shared with SimplefinAccount::Processor

The helpers here:

def to_decimal(value)
  ...
end

def same_sign?(a, b)
  ...
end

are identical to the ones defined in SimplefinAccount::Processor. Keeping two separate copies increases the chance of subtle divergence in SimpleFIN normalization behavior later.

Since both importer and processor now rely on the same balance/sign semantics, consider extracting these into a small shared module under the SimpleFIN namespace (e.g., Simplefin::NumericHelpers or Simplefin::BalanceUtils) and including it in both classes, rather than redefining them.

Based on learnings, cross-integration balance handling (e.g., Plaid and EnableBanking processors) is kept consistent via shared patterns, so centralizing these SimpleFIN helpers would follow the same spirit without touching other providers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a91a439 and 8c586e0.

📒 Files selected for processing (2)
  • app/models/simplefin_account/processor.rb (1 hunks)
  • app/models/simplefin_item/importer.rb (2 hunks)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/models/**/*.rb: Keep business logic in models using skinny controllers and fat models pattern
Store all monetary values in base currency (user's primary currency) and use Money objects for conversion
Use ActiveRecord validations for form convenience and complex business logic validations
Use Rails concerns and POROs for organizing business logic instead of services folder

app/models/**/*.rb: All monetary values stored in base currency (user's primary currency). Use Money objects for currency conversion and formatting.
Place business logic in app/models/ folder. Avoid creating app/services/ folder. Use Rails concerns and POROs for organization.
Models should answer questions about themselves using instance methods: use account.balance_series not AccountSeries.new(account).call.

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
{app/models/**/*.rb,db/migrate/**/*.rb}

📄 CodeRabbit inference engine (CLAUDE.md)

Simple validations (null checks, unique indexes) should be in the database. ActiveRecord validations for form convenience (prefer client-side when possible). Complex validations and business logic in ActiveRecord.

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
🧠 Learnings (9)
📓 Common learnings
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: db/migrate/20250825224717_add_enable_banking_domain.rb:3-34
Timestamp: 2025-09-13T12:23:31.324Z
Learning: When suggesting database schema improvements for banking integrations, consider consistency across all similar integrations (Plaid, SimpleFin, Enable Banking) and propose comprehensive changes in a separate PR rather than making changes to individual integrations in isolation.
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account sync must perform transfer auto-matching, calculate daily balance records from start_date to Date.current, calculate holdings, and optionally enrich transaction data

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-09-13T11:33:54.159Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account.rb:9-20
Timestamp: 2025-09-13T11:33:54.159Z
Learning: The EnableBankingAccount model uses a `has_balance` validation method that ensures at least one balance value (current_balance or available_balance) is present, similar to the Plaid integration pattern. This means balance values should not be nil and the validation expects actual balance data from the API.

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-04T16:21:39.347Z
Learnt from: sokie
Repo: we-promise/sure PR: 276
File: app/controllers/reports_controller.rb:80-84
Timestamp: 2025-11-04T16:21:39.347Z
Learning: The we-promise/sure codebase uses a custom Money class (lib/money.rb) that treats numeric inputs as dollar amounts, not fractional units (cents). When Money.new receives a numeric value, it converts it directly to BigDecimal via BigDecimal(obj.to_s) without multiplication. This differs from the standard ruby-money gem which expects cents/fractional units.

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to app/models/**/*.rb : All monetary values stored in base currency (user's primary currency). Use `Money` objects for currency conversion and formatting.

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to app/models/**/*.rb : Store all monetary values in base currency (user's primary currency) and use Money objects for conversion

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to app/models/**/*.rb : Models should answer questions about themselves using instance methods: use `account.balance_series` not `AccountSeries.new(account).call`.

Applied to files:

  • app/models/simplefin_account/processor.rb
🧬 Code graph analysis (2)
app/models/simplefin_item/importer.rb (3)
app/models/simplefin_account/processor.rb (2)
  • to_decimal (102-111)
  • same_sign? (113-115)
app/models/simplefin/account_type_mapper.rb (1)
  • infer (37-88)
app/models/account/provider_import_adapter.rb (1)
  • update_balance (110-115)
app/models/simplefin_account/processor.rb (3)
app/models/simplefin_item/importer.rb (2)
  • to_decimal (768-777)
  • same_sign? (779-781)
app/models/provider/configurable.rb (1)
  • value (209-223)
lib/money/arithmetic.rb (2)
  • positive? (53-55)
  • negative? (49-51)
🪛 GitHub Actions: Pull Request
app/models/simplefin_account/processor.rb

[error] 49-49: NoMethodError: undefined method '-@' for nil in SimplefinAccount::Processor#process_account!


[error] 60-60: ActiveRecord::RecordInvalid: Validation failed: Balance can't be blank in SimplefinAccount::Processor#process_account!

@luckyPipewrench luckyPipewrench marked this pull request as ready for review December 3, 2025 18:45
@jjmata jjmata self-requested a review December 4, 2025 14:20
@jjmata
Copy link
Collaborator

jjmata commented Dec 13, 2025

How do you feel about this one @luckyPipewrench? Do we still need it? I never heard back from SimpleFIN support about my BofA overpayment issue.

@luckyPipewrench
Copy link
Collaborator Author

Working on this one today. Should have it done soon, I've finally kept a overpaid balance long enough for it show on the simplefin sync.

Josh Waldrep added 2 commits December 13, 2025 14:25
- Updated `SimplefinAccount::Processor` to normalize liability balances based on observed values, ensuring correct handling of debts and overpayments.
- Enhanced `SimplefinItem::Importer` to apply similar normalization rules during imports, improving consistency.
- Added multiple test cases in `SimplefinAccountProcessorTest` to validate edge cases for liabilities and mixed-sign scenarios.
- Introduced helper methods (`to_decimal`, `same_sign?`) to simplify numeric operations in normalization logic.
…fication

- Introduced `SimplefinAccount::Liabilities::OverpaymentAnalyzer` to classify liability balances as credit, debt, or unknown using transaction history.
- Updated `SimplefinAccount::Processor` and `SimplefinItem::Importer` to integrate heuristic-based balance normalization with fallback logic for ambiguous cases.
- Added comprehensive unit tests in `OverpaymentAnalyzerTest` to validate classification logic and edge cases.
- Enhanced logging and observability around classification results and fallback scenarios.
@luckyPipewrench luckyPipewrench force-pushed the simplefin-liabilities-normalization branch from 4a5e032 to a1a6d4f Compare December 14, 2025 01:34
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
app/models/simplefin_item/importer.rb (2)

108-189: Redundant cash branch (no-op) — simplify or implement intended investment behavior.
cash returns normalized in both branches, so the Investment conditional currently adds no value.

-        cash = if acct.accountable_type == "Investment"
-          # Leave investment cash to investment calculators in full run
-          normalized
-        else
-          normalized
-        end
+        cash = normalized

860-874: De-duplicate numeric helpers across importer/processor/analyzer.
to_decimal/same_sign? are now repeated in multiple classes; consider extracting to a small shared module (e.g., Simplefin::DecimalHelpers) to keep behavior consistent.

test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb (1)

27-34: Prefer deleting the exact cache key vs delete_matched.
More portable across cache stores and less work.

-    Rails.cache.delete_matched("simplefin:sfa:#{@sfa.id}:liability_sign_hint") rescue nil
+    Rails.cache.delete("simplefin:sfa:#{@sfa.id}:liability_sign_hint") rescue nil
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a5e032 and a1a6d4f.

📒 Files selected for processing (5)
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb (1 hunks)
  • app/models/simplefin_account/processor.rb (2 hunks)
  • app/models/simplefin_item/importer.rb (2 hunks)
  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb (1 hunks)
  • test/models/simplefin_account_processor_test.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (12)
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • test/models/simplefin_account_processor_test.rb
test/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

test/**/*.rb: Use Minitest with fixtures (never RSpec or FactoryBot) for testing
Use mocha gem for mocks and VCR for external API tests
Use fixtures for base cases in tests (2-3 per model) and create edge cases on-the-fly within test context
Test critical code paths only: test Commands for correct params, Queries for output

test/**/*.rb: Use Rails' built-in Minitest framework with fixtures for test data. Avoid RSpec and FactoryBot.
Use VCR for external API testing.
Use mocha gem for stubs and mocks. Prefer OpenStruct for mock instances. Only mock what's necessary.

Write tests using Minitest and fixtures as the testing framework

test/**/*.rb: Always use Minitest and fixtures for testing, NEVER rspec or factories
For tests that require a large number of fixture records, use Rails helpers to help create the records needed for the test, then inline the creation instead of defining them in fixtures
Distinguish between commands and query methods. Test output of query methods; test that commands were called with the correct params.
Never test the implementation details of one class in another class's test suite
Use the mocha gem for stubs and mocks
Always prefer OpenStruct when creating mock instances, or in complex cases, a mock class
Only mock what's necessary. If you're not testing return values, don't mock a return value.

Files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • test/models/simplefin_account_processor_test.rb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • test/models/simplefin_account_processor_test.rb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • test/models/simplefin_account_processor_test.rb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • test/models/simplefin_account_processor_test.rb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • test/models/simplefin_account_processor_test.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • test/models/simplefin_account_processor_test.rb
test/**/*_test.rb

📄 CodeRabbit inference engine (AGENTS.md)

test/**/*_test.rb: Test files should use *_test.rb naming convention and mirror app/ structure (e.g., test/models/*_test.rb)
Use Minitest framework for tests with names *_test.rb, run via bin/rails test, and use test/fixtures and VCR cassettes for HTTP testing

Files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • test/models/simplefin_account_processor_test.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/models/**/*.rb: Keep business logic in models using skinny controllers and fat models pattern
Store all monetary values in base currency (user's primary currency) and use Money objects for conversion
Use ActiveRecord validations for form convenience and complex business logic validations
Use Rails concerns and POROs for organizing business logic instead of services folder

app/models/**/*.rb: All monetary values stored in base currency (user's primary currency). Use Money objects for currency conversion and formatting.
Place business logic in app/models/ folder. Avoid creating app/services/ folder. Use Rails concerns and POROs for organization.
Models should answer questions about themselves using instance methods: use account.balance_series not AccountSeries.new(account).call.

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
{app/models/**/*.rb,db/migrate/**/*.rb}

📄 CodeRabbit inference engine (CLAUDE.md)

Simple validations (null checks, unique indexes) should be in the database. ActiveRecord validations for form convenience (prefer client-side when possible). Complex validations and business logic in ActiveRecord.

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
🧠 Learnings (11)
📓 Common learnings
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: db/migrate/20250825224717_add_enable_banking_domain.rb:3-34
Timestamp: 2025-09-13T12:23:31.324Z
Learning: When suggesting database schema improvements for banking integrations, consider consistency across all similar integrations (Plaid, SimpleFin, Enable Banking) and propose comprehensive changes in a separate PR rather than making changes to individual integrations in isolation.
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to test/**/*.rb : Use fixtures for base cases in tests (2-3 per model) and create edge cases on-the-fly within test context

Applied to files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account model is a Rails delegated type with subtypes: Depository, Investment, Crypto, Property, Vehicle, OtherAsset (assets) and CreditCard, Loan, OtherLiability (liabilities)

Applied to files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • app/models/simplefin_account/processor.rb
  • test/models/simplefin_account_processor_test.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.

Applied to files:

  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • test/models/simplefin_account_processor_test.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • test/models/simplefin_account_processor_test.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account sync must perform transfer auto-matching, calculate daily balance records from start_date to Date.current, calculate holdings, and optionally enrich transaction data

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • test/models/simplefin_account_processor_test.rb
📚 Learning: 2025-09-13T11:33:54.159Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account.rb:9-20
Timestamp: 2025-09-13T11:33:54.159Z
Learning: The EnableBankingAccount model uses a `has_balance` validation method that ensures at least one balance value (current_balance or available_balance) is present, similar to the Plaid integration pattern. This means balance values should not be nil and the validation expects actual balance data from the API.

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
  • test/models/simplefin_account_processor_test.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to app/models/**/*.rb : All monetary values stored in base currency (user's primary currency). Use `Money` objects for currency conversion and formatting.

Applied to files:

  • app/models/simplefin_item/importer.rb
📚 Learning: 2025-11-04T16:21:39.347Z
Learnt from: sokie
Repo: we-promise/sure PR: 276
File: app/controllers/reports_controller.rb:80-84
Timestamp: 2025-11-04T16:21:39.347Z
Learning: The we-promise/sure codebase uses a custom Money class (lib/money.rb) that treats numeric inputs as dollar amounts, not fractional units (cents). When Money.new receives a numeric value, it converts it directly to BigDecimal via BigDecimal(obj.to_s) without multiplication. This differs from the standard ruby-money gem which expects cents/fractional units.

Applied to files:

  • app/models/simplefin_item/importer.rb
  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to app/models/**/*.rb : Store all monetary values in base currency (user's primary currency) and use Money objects for conversion

Applied to files:

  • app/models/simplefin_item/importer.rb
📚 Learning: 2025-11-04T15:16:23.341Z
Learnt from: sokie
Repo: we-promise/sure PR: 276
File: app/controllers/reports_controller.rb:159-169
Timestamp: 2025-11-04T15:16:23.341Z
Learning: In the Budget model, the methods `actual_spending` and `allocated_spending` return BigDecimal values (not Money objects), so division operations like `budget.actual_spending / budget.allocated_spending` work correctly without needing conversion to fractional units.

Applied to files:

  • app/models/simplefin_account/processor.rb
🧬 Code graph analysis (3)
test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb (2)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (2)
  • new (13-232)
  • call (31-58)
app/models/account/current_balance_manager.rb (1)
  • current_balance (16-23)
app/models/simplefin_item/importer.rb (3)
app/models/simplefin_account/processor.rb (1)
  • same_sign? (187-189)
app/models/simplefin/account_type_mapper.rb (2)
  • infer (37-88)
  • new (7-89)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (2)
  • new (13-232)
  • call (31-58)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (4)
lib/money/arithmetic.rb (3)
  • abs (41-43)
  • positive? (53-55)
  • negative? (49-51)
app/models/provider/configurable.rb (1)
  • present? (226-228)
app/models/account.rb (1)
  • start_date (222-225)
lib/simplefin/date_utils.rb (1)
  • parse_provider_date (9-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (4)
app/models/simplefin_item/importer.rb (1)

114-181: Verify whether OtherLiability should also be normalized.
Right now only CreditCard/Loan are treated as liabilities in balances-only mode; if SimpleFIN can link to OtherLiability, it will keep legacy signs.

test/models/simplefin_account_processor_test.rb (1)

85-180: Nice coverage for liability sign edge cases; verify “mislinked depository becomes negative” doesn’t break downstream assumptions.
The last test asserts a depository account ends up with a negative balance after mapper inference; that may affect any code that assumes “asset balances are non-negative”.

test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb (1)

36-78: Verify entryable: Transaction.new is valid for Entry in this codebase.
If Entry requires a persisted Transaction, these tests will error at create!.

app/models/simplefin_account/processor.rb (1)

70-123: Normalization + analyzer fallback flow looks solid; keep an eye on operational blast radius.
Given this can flip signs and affect net worth, I’d strongly suggest validating with a small sample of known-problem accounts (esp. loans) before broad rollout.

Comment on lines 62 to 81
def enabled?
env = ENV["SIMPLEFIN_CC_OVERPAYMENT_HEURISTIC"].to_s
env_specified = env.present?
env_enabled = env_specified ? (%w[1 true yes on].include?(env.downcase)) : false
# Allow dynamic Setting override; Setting[] returns value or nil
setting_val = Setting["simplefin_cc_overpayment_detection"]
setting_enabled = case setting_val
when true then true
when false then false
when String then %w[1 true yes on].include?(setting_val.downcase)
else
nil
end
# Default behavior: ENABLED unless explicitly disabled via Setting or ENV
if setting_enabled.nil?
env_specified ? env_enabled : true
else
setting_enabled
end
end
Copy link

@coderabbitai coderabbitai bot Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the file context and method to understand intent
cat -n app/models/simplefin_account/liabilities/overpayment_analyzer.rb | head -100

Repository: we-promise/sure

Length of output: 4105


🏁 Script executed:

# Search for any documentation or comments about the feature rollout
rg -i "overpayment" --type rb -B2 -A2 | head -50

Repository: we-promise/sure

Length of output: 85


🏁 Script executed:

# Look for other feature flag patterns in the codebase
rg "ENV\[" --type rb | grep -i "feature\|flag\|heuristic" | head -20

Repository: we-promise/sure

Length of output: 85


🏁 Script executed:

# Check for related settings or configuration
rg "simplefin_cc_overpayment" --type rb -B2 -A2

Repository: we-promise/sure

Length of output: 85


🏁 Script executed:

# Look for any PR or issue documentation
find . -type f \( -name "*.md" -o -name "*.txt" \) -exec rg -l "overpayment" {} \;

Repository: we-promise/sure

Length of output: 85


🏁 Script executed:

# Search for overpayment references without file type restrictions
grep -r "overpayment" . --include="*.md" --include="*.rb" --include="*.txt" 2>/dev/null | head -30

Repository: we-promise/sure

Length of output: 3552


🏁 Script executed:

# Check the documentation file that was found
cat docs/onboarding/guide.md | grep -i "overpayment\|simplefin" -B2 -A2 | head -40

Repository: we-promise/sure

Length of output: 572


🏁 Script executed:

# Search for Setting keys related to overpayment to understand operational control
grep -r "simplefin_cc_overpayment" . 2>/dev/null

Repository: we-promise/sure

Length of output: 2022


🏁 Script executed:

# Look for any git history or comments about feature rollout
grep -r "feature.*flag\|gradual.*rollout\|beta\|experimental" . --include="*.rb" --include="*.md" 2>/dev/null | grep -i "overpayment\|heuristic" | head -20

Repository: we-promise/sure

Length of output: 163


🏁 Script executed:

# Check if there are any specs or tests for the enabled? method
grep -r "enabled\?" . --include="*spec.rb" --include="*test.rb" 2>/dev/null | grep -i "overpayment" -B2 -A2

Repository: we-promise/sure

Length of output: 41


🏁 Script executed:

cat test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb

Repository: we-promise/sure

Length of output: 3919


🏁 Script executed:

# Check the importer.rb context around the "feature-flagged" comment
grep -B5 -A10 "feature-flagged" ./app/models/simplefin_item/importer.rb

Repository: we-promise/sure

Length of output: 676


Default behavior should be DISABLED for a feature-flagged heuristic.

The test setup explicitly enables this with Setting["simplefin_cc_overpayment_detection"] = "true" and disables it in teardown to prevent test bleeding. This pattern confirms the feature should default to off, not on. The importer.rb comment also labels it "feature-flagged," which typically implies opt-in behavior. Change the default at line 77 from true to false:

-      if setting_enabled.nil?
-        env_specified ? env_enabled : true
+      if setting_enabled.nil?
+        env_specified ? env_enabled : false
🤖 Prompt for AI Agents
In app/models/simplefin_account/liabilities/overpayment_analyzer.rb around lines
62–81, the method enabled? currently defaults to true when no ENV or Setting is
specified; change the default to disabled by replacing the final branch so that
when setting_enabled.nil? it returns env_specified ? env_enabled : false (i.e.
default false), ensuring feature-flagged heuristic is opt-in and matches tests
that explicitly enable/disable it.

Copy link
Collaborator Author

@luckyPipewrench luckyPipewrench Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping default ON is intentional. If SimpleFin fixes how they send the balances, we can later change the default globally, it’s a one‑line change.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@luckyPipewrench, understood! That makes sense — keeping it enabled by default allows you to quickly disable the normalization globally if SimpleFin changes their sign semantics upstream. The single-toggle approach is pragmatic for this scenario.


🧠 Learnings used
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.

Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.

Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account sync must perform transfer auto-matching, calculate daily balance records from start_date to Date.current, calculate holdings, and optionally enrich transaction data

Learnt from: Himmelschmidt
Repo: we-promise/sure PR: 104
File: app/models/simplefin_item/importer.rb:34-45
Timestamp: 2025-09-03T01:15:59.638Z
Learning: In SimpleFin integration, the default initial sync period is intentionally set to 7 days (not 3 years) to provide quick setup, with users able to optionally extend to 3 years via sync_start_date selection. This design balances setup speed with comprehensive data access options.

Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: db/migrate/20250825224717_add_enable_banking_domain.rb:3-34
Timestamp: 2025-09-13T12:23:31.324Z
Learning: When suggesting database schema improvements for banking integrations, consider consistency across all similar integrations (Plaid, SimpleFin, Enable Banking) and propose comprehensive changes in a separate PR rather than making changes to individual integrations in isolation.

- Updated `sticky_key` method in `OverpaymentAnalyzer` to handle missing `@sfa.id` with a default value.
- Enhanced `SimplefinAccount::Processor` to use `with_indifferent_access` for `raw_payload` and `org_data`, improving robustness in liability type inference.
@luckyPipewrench
Copy link
Collaborator Author

Wow, this was more complicated than you'd think it should be! But it works! @jjmata

@luckyPipewrench luckyPipewrench changed the title Fix SimpleFIN Liability Overpayment Edge Cases with Heuristic Normalization Add overpayment detection for SimpleFIN liabilities (default ON) with heuristic-based classification and robust fallbacks Dec 14, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (2)

62-81: Consider simplifying feature flag logic for readability.

The logic is functionally correct and handles ENV/Setting precedence appropriately, with default-enabled behavior as intentionally designed. However, the nested conditionals make it harder to follow.

Consider this more readable structure:

 def enabled?
-  env = ENV["SIMPLEFIN_CC_OVERPAYMENT_HEURISTIC"].to_s
-  env_specified = env.present?
-  env_enabled = env_specified ? (%w[1 true yes on].include?(env.downcase)) : false
-  # Allow dynamic Setting override; Setting[] returns value or nil
-  setting_val = Setting["simplefin_cc_overpayment_detection"]
-  setting_enabled = case setting_val
-  when true then true
-  when false then false
-  when String then %w[1 true yes on].include?(setting_val.downcase)
-  else
-    nil
-  end
-  # Default behavior: ENABLED unless explicitly disabled via Setting or ENV
-  if setting_enabled.nil?
-    env_specified ? env_enabled : true
-  else
-    setting_enabled
-  end
+  # Setting override takes precedence, then ENV, then default enabled
+  setting_val = Setting["simplefin_cc_overpayment_detection"]
+  return parse_bool(setting_val) unless setting_val.nil?
+  
+  env_val = ENV["SIMPLEFIN_CC_OVERPAYMENT_HEURISTIC"]
+  return parse_bool(env_val) if env_val.present?
+  
+  true # Default enabled
+end
+
+def parse_bool(value)
+  case value
+  when true, false then value
+  when String then %w[1 true yes on].include?(value.downcase)
+  else false
+  end
 end

119-143: Add logging for transaction gathering errors.

The fallback logic from Entry records to raw payload is well-designed. However, Line 141 swallows all exceptions silently without logging, which could hide important errors during transaction gathering (e.g., data format issues, parsing failures).

Add minimal logging before returning the empty array:

   rescue => e
-    []
+    Rails.logger.debug("SimpleFIN transaction gathering failed for sfa=#{@sfa.id}: #{e.class} - #{e.message}")
+    []
   end

This maintains the graceful degradation while providing visibility into gathering failures.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1a6d4f and f4b0f27.

📒 Files selected for processing (2)
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb (1 hunks)
  • app/models/simplefin_account/processor.rb (2 hunks)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/models/**/*.rb: Keep business logic in models using skinny controllers and fat models pattern
Store all monetary values in base currency (user's primary currency) and use Money objects for conversion
Use ActiveRecord validations for form convenience and complex business logic validations
Use Rails concerns and POROs for organizing business logic instead of services folder

app/models/**/*.rb: All monetary values stored in base currency (user's primary currency). Use Money objects for currency conversion and formatting.
Place business logic in app/models/ folder. Avoid creating app/services/ folder. Use Rails concerns and POROs for organization.
Models should answer questions about themselves using instance methods: use account.balance_series not AccountSeries.new(account).call.

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
{app/models/**/*.rb,db/migrate/**/*.rb}

📄 CodeRabbit inference engine (CLAUDE.md)

Simple validations (null checks, unique indexes) should be in the database. ActiveRecord validations for form convenience (prefer client-side when possible). Complex validations and business logic in ActiveRecord.

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
🧠 Learnings (12)
📓 Common learnings
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: db/migrate/20250825224717_add_enable_banking_domain.rb:3-34
Timestamp: 2025-09-13T12:23:31.324Z
Learning: When suggesting database schema improvements for banking integrations, consider consistency across all similar integrations (Plaid, SimpleFin, Enable Banking) and propose comprehensive changes in a separate PR rather than making changes to individual integrations in isolation.
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account sync must perform transfer auto-matching, calculate daily balance records from start_date to Date.current, calculate holdings, and optionally enrich transaction data

Applied to files:

  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-09-13T11:33:54.159Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account.rb:9-20
Timestamp: 2025-09-13T11:33:54.159Z
Learning: The EnableBankingAccount model uses a `has_balance` validation method that ensures at least one balance value (current_balance or available_balance) is present, similar to the Plaid integration pattern. This means balance values should not be nil and the validation expects actual balance data from the API.

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-11-04T15:16:23.341Z
Learnt from: sokie
Repo: we-promise/sure PR: 276
File: app/controllers/reports_controller.rb:159-169
Timestamp: 2025-11-04T15:16:23.341Z
Learning: In the Budget model, the methods `actual_spending` and `allocated_spending` return BigDecimal values (not Money objects), so division operations like `budget.actual_spending / budget.allocated_spending` work correctly without needing conversion to fractional units.

Applied to files:

  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-04T16:21:39.347Z
Learnt from: sokie
Repo: we-promise/sure PR: 276
File: app/controllers/reports_controller.rb:80-84
Timestamp: 2025-11-04T16:21:39.347Z
Learning: The we-promise/sure codebase uses a custom Money class (lib/money.rb) that treats numeric inputs as dollar amounts, not fractional units (cents). When Money.new receives a numeric value, it converts it directly to BigDecimal via BigDecimal(obj.to_s) without multiplication. This differs from the standard ruby-money gem which expects cents/fractional units.

Applied to files:

  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account model is a Rails delegated type with subtypes: Depository, Investment, Crypto, Property, Vehicle, OtherAsset (assets) and CreditCard, Loan, OtherLiability (liabilities)

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-03T01:15:59.638Z
Learnt from: Himmelschmidt
Repo: we-promise/sure PR: 104
File: app/models/simplefin_item/importer.rb:34-45
Timestamp: 2025-09-03T01:15:59.638Z
Learning: In SimpleFin integration, the default initial sync period is intentionally set to 7 days (not 3 years) to provide quick setup, with users able to optionally extend to 3 years via sync_start_date selection. This design balances setup speed with comprehensive data access options.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T12:23:31.324Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: db/migrate/20250825224717_add_enable_banking_domain.rb:3-34
Timestamp: 2025-09-13T12:23:31.324Z
Learning: When suggesting database schema improvements for banking integrations, consider consistency across all similar integrations (Plaid, SimpleFin, Enable Banking) and propose comprehensive changes in a separate PR rather than making changes to individual integrations in isolation.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:40:26.913Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_entry/processor.rb:10-12
Timestamp: 2025-09-13T11:40:26.913Z
Learning: The plaid_id field is used consistently across multiple banking integrations (Plaid, SimpleFin, and Enable Banking) as a common external identifier field. A comprehensive refactor to use a proper external_id field should be done across all integrations in a separate PR to maintain consistency and avoid scope creep.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:27:55.271Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/controllers/enable_banking_items_controller.rb:48-62
Timestamp: 2025-09-13T11:27:55.271Z
Learning: The `create_enable_banking_item!` method in `Family::EnableBankingConnectable` uses `find_or_create_by(id: enable_banking_id)` to either find an existing EnableBankingItem or create a new one, so it automatically handles updates to existing items without creating duplicates.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
🧬 Code graph analysis (1)
app/models/simplefin_account/processor.rb (1)
app/models/simplefin_item/importer.rb (2)
  • to_decimal (861-870)
  • same_sign? (872-874)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (7)
app/models/simplefin_account/processor.rb (3)

42-48: LGTM! Balance extraction with safe fallback.

The extraction logic correctly prioritizes posted balance over available balance when non-zero, with safe BigDecimal conversion via the helper method.


50-70: LGTM! Liability determination with proper hash access.

The indifferent access handling correctly addresses the past review concern about string-only hash keys. The warning log for mismatched liability types is valuable for identifying accounts that may need relinking.


72-126: LGTM! Robust overpayment analysis with comprehensive fallbacks.

The integration provides excellent resilience with multiple error handling layers (NameError for missing analyzer, general exceptions, and Sentry failures). The INFO logging with metrics provides good observability for both positive classifications and unknown cases.

app/models/simplefin_account/liabilities/overpayment_analyzer.rb (4)

1-29: LGTM! Clean structure with Result struct and sensible defaults.

The Result struct with keyword arguments and DEFAULTS hash provide a clean, configurable foundation. Constructor signature is well-designed with keyword arguments.

Minor note: to_d (line 27) vs to_decimal naming inconsistency with processor.rb, though both serve the same purpose.


31-58: LGTM! Well-structured main logic with appropriate guards.

The guard clauses follow Ruby best practices, and the sticky cache mechanism effectively prevents classification flapping. The flow from transaction gathering → metrics computation → classification → cache writing is logical and maintainable.


83-117: LGTM! Consistent configuration getters with proper validation.

The configuration methods follow a consistent pattern with Setting overrides, fallbacks to DEFAULTS, and appropriate validation. The validations ensure safe values (e.g., > 0 for most, >= 0 for statement_guard_days).


145-195: LGTM! Sound heuristic with statement guard and dynamic epsilon.

The metrics computation and classification logic are well-designed:

  • Statement guard prevents false positives from recent large payments
  • Dynamic epsilon (max of base or 0.5% of observed) scales appropriately
  • Credit/debt checks use sensible thresholds
  • Minimum payment count requirement reduces false positives from sparse data

… and apply across models

- Moved `to_decimal` and `same_sign?` methods into a new `SimplefinNumericHelpers` concern for reuse.
- Updated `OverpaymentAnalyzer`, `Processor`, and `Importer` to include the concern and remove redundant method definitions.
- Added empty fixtures for `simplefin_accounts` and `simplefin_items` to ensure test isolation.
- Refactored `OverpaymentAnalyzerTest` to reduce fixture dependencies and ensure cleanup of created records.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (1)

208-226: sticky_key still risks collisions for non-persisted records.
Current fallback "new" will collide if analyzer is ever invoked on multiple non-persisted instances within sticky_days. If that can’t happen, add an assertion/comment; otherwise include object_id in the fallback.

     def sticky_key
-      id = @sfa.id || "new"
+      id = @sfa.id || "tmp:#{@sfa.object_id}"
       "simplefin:sfa:#{id}:liability_sign_hint"
     end
🧹 Nitpick comments (6)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (4)

13-60: Good separation + safe fallbacks, but sticky payload should be schema-stable across cache stores.
read_sticky assumes sticky[:expires_at] round-trips as a Time and is comparable to @now. With some cache stores/serializers, expires_at may deserialize as a String, which can raise and force callers into rescue/fallback (Processor rescues, but analyzer should be robust itself).

-    sticky = read_sticky
-    if sticky && sticky[:expires_at] > @now
+    sticky = read_sticky
+    expires_at =
+      case sticky&.dig(:expires_at)
+      when Time then sticky[:expires_at]
+      when String then Time.parse(sticky[:expires_at]) rescue nil
+      else nil
+      end
+    if sticky && expires_at && expires_at > @now
       return Result.new(classification: sticky[:value].to_sym, reason: "sticky_hint", metrics: {})
     end

(Also consider writing expires_at as an ISO8601 string to begin with.)


84-119: Config readers: minor ergonomics—centralize common “Setting-or-default” parsing.
These methods repeat the same pattern; not urgent, but a small helper would reduce drift (especially for integer bounds).


120-145: Prefer pluck to avoid instantiating AR objects; don’t silently swallow gather errors without any trace.
select + map builds AR instances; pluck avoids that. Also, rescue => _e; [] makes “unknown → fallback” indistinguishable from “bug parsing provider payload”. Consider at least a debug log (rate-limited if needed).

-      entries = account.entries.where("date >= ?", start_date).select(:amount, :date)
-      txns = entries.map { |e| { amount: to_decimal(e.amount), date: e.date } }
+      rows = account.entries.where("date >= ?", start_date).pluck(:amount, :date)
+      txns = rows.map { |amount, date| { amount: to_decimal(amount), date: date } }
       return txns if txns.size >= min_txns
@@
-    rescue => _e
+    rescue => e
+      Rails.logger.debug("SimpleFIN overpayment gather_transactions failed for sfa=#{@sfa.id}: #{e.class} - #{e.message}") rescue nil
       []
     end

198-206: convert_provider_amount duplicates to_decimal; simplify to reduce divergence risk.

     def convert_provider_amount(val)
-      amt = case val
-      when String then BigDecimal(val) rescue BigDecimal("0")
-      when Numeric then BigDecimal(val.to_s)
-      else BigDecimal("0")
-      end
       # Negate to convert banking convention (expenses negative) -> Maybe convention
-      -amt
+      -to_decimal(val)
     end
app/models/simplefin_account/processor.rb (2)

33-127: Overpayment flow is robust; avoid rescue for control-flow and ensure observed selection matches desired precedence.
Two small nits:

  • rescue NameError for “Analyzer not loaded” is a bit broad; consider checking defined?(SimplefinAccount::Liabilities::OverpaymentAnalyzer) instead.
  • observed = bal.nonzero? ? bal : avail makes “0 balance” fall through to avail. If SimpleFIN can legitimately report 0 posted with meaningful avail, that’s fine; otherwise it may create unexpected flips.
-        rescue NameError
-          # Analyzer not loaded; keep legacy behavior
-          balance = normalize_liability_balance(observed, bal, avail)
+        rescue NameError
+          balance = normalize_liability_balance(observed, bal, avail)
         rescue => e
@@
       end

(Optional alternative to avoid the NameError path entirely:)

if defined?(SimplefinAccount::Liabilities::OverpaymentAnalyzer)
  # call analyzer
else
  balance = normalize_liability_balance(observed, bal, avail)
end

181-191: normalize_liability_balance: both_present isn’t a boolean (harmless), but clarity helps.
bal.nonzero? && avail.nonzero? returns a BigDecimal or nil; works, but reads oddly.

-      both_present = bal.nonzero? && avail.nonzero?
+      both_present = bal.nonzero? != nil && avail.nonzero? != nil
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4b0f27 and 3ce6899.

📒 Files selected for processing (7)
  • app/models/concerns/simplefin_numeric_helpers.rb (1 hunks)
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb (1 hunks)
  • app/models/simplefin_account/processor.rb (3 hunks)
  • app/models/simplefin_item/importer.rb (3 hunks)
  • test/fixtures/simplefin_accounts.yml (1 hunks)
  • test/fixtures/simplefin_items.yml (1 hunks)
  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • test/fixtures/simplefin_items.yml
  • test/fixtures/simplefin_accounts.yml
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/models/simplefin_account/liabilities/overpayment_analyzer_test.rb
  • app/models/simplefin_item/importer.rb
🧰 Additional context used
📓 Path-based instructions (10)
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/models/**/*.rb: Keep business logic in models using skinny controllers and fat models pattern
Store all monetary values in base currency (user's primary currency) and use Money objects for conversion
Use ActiveRecord validations for form convenience and complex business logic validations
Use Rails concerns and POROs for organizing business logic instead of services folder

app/models/**/*.rb: All monetary values stored in base currency (user's primary currency). Use Money objects for currency conversion and formatting.
Place business logic in app/models/ folder. Avoid creating app/services/ folder. Use Rails concerns and POROs for organization.
Models should answer questions about themselves using instance methods: use account.balance_series not AccountSeries.new(account).call.

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
{app/models/**/*.rb,db/migrate/**/*.rb}

📄 CodeRabbit inference engine (CLAUDE.md)

Simple validations (null checks, unique indexes) should be in the database. ActiveRecord validations for form convenience (prefer client-side when possible). Complex validations and business logic in ActiveRecord.

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
🧠 Learnings (19)
📓 Common learnings
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-11-24T16:56:13.406Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-conventions.mdc:0-0
Timestamp: 2025-11-24T16:56:13.406Z
Learning: Use Rails concerns for code organization around model 'traits' and shared functionality, not just for moving code to another location. Prefer models answering questions about themselves with methods like account.balance_series over service-like patterns

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to app/models/**/*.rb : Use Rails concerns and POROs for organizing business logic instead of services folder

Applied to files:

  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-09-13T12:23:31.324Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: db/migrate/20250825224717_add_enable_banking_domain.rb:3-34
Timestamp: 2025-09-13T12:23:31.324Z
Learning: When suggesting database schema improvements for banking integrations, consider consistency across all similar integrations (Plaid, SimpleFin, Enable Banking) and propose comprehensive changes in a separate PR rather than making changes to individual integrations in isolation.

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to app/models/**/*.rb : Place business logic in `app/models/` folder. Avoid creating `app/services/` folder. Use Rails concerns and POROs for organization.

Applied to files:

  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-24T16:56:13.406Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-conventions.mdc:0-0
Timestamp: 2025-11-24T16:56:13.406Z
Learning: Organize large pieces of business logic into Rails concerns and POROs rather than separate service folders. Place business logic directly in app/models/ folder

Applied to files:

  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to app/models/**/*.rb : Keep business logic in models using skinny controllers and fat models pattern

Applied to files:

  • app/models/simplefin_account/processor.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account sync must perform transfer auto-matching, calculate daily balance records from start_date to Date.current, calculate holdings, and optionally enrich transaction data

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:33:54.159Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account.rb:9-20
Timestamp: 2025-09-13T11:33:54.159Z
Learning: The EnableBankingAccount model uses a `has_balance` validation method that ensures at least one balance value (current_balance or available_balance) is present, similar to the Plaid integration pattern. This means balance values should not be nil and the validation expects actual balance data from the API.

Applied to files:

  • app/models/simplefin_account/processor.rb
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account model is a Rails delegated type with subtypes: Depository, Investment, Crypto, Property, Vehicle, OtherAsset (assets) and CreditCard, Loan, OtherLiability (liabilities)

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-03T01:15:59.638Z
Learnt from: Himmelschmidt
Repo: we-promise/sure PR: 104
File: app/models/simplefin_item/importer.rb:34-45
Timestamp: 2025-09-03T01:15:59.638Z
Learning: In SimpleFin integration, the default initial sync period is intentionally set to 7 days (not 3 years) to provide quick setup, with users able to optionally extend to 3 years via sync_start_date selection. This design balances setup speed with comprehensive data access options.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:40:26.913Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_entry/processor.rb:10-12
Timestamp: 2025-09-13T11:40:26.913Z
Learning: The plaid_id field is used consistently across multiple banking integrations (Plaid, SimpleFin, and Enable Banking) as a common external identifier field. A comprehensive refactor to use a proper external_id field should be done across all integrations in a separate PR to maintain consistency and avoid scope creep.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:27:55.271Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/controllers/enable_banking_items_controller.rb:48-62
Timestamp: 2025-09-13T11:27:55.271Z
Learning: The `create_enable_banking_item!` method in `Family::EnableBankingConnectable` uses `find_or_create_by(id: enable_banking_id)` to either find an existing EnableBankingItem or create a new one, so it automatically handles updates to existing items without creating duplicates.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-11-04T16:21:39.347Z
Learnt from: sokie
Repo: we-promise/sure PR: 276
File: app/controllers/reports_controller.rb:80-84
Timestamp: 2025-11-04T16:21:39.347Z
Learning: The we-promise/sure codebase uses a custom Money class (lib/money.rb) that treats numeric inputs as dollar amounts, not fractional units (cents). When Money.new receives a numeric value, it converts it directly to BigDecimal via BigDecimal(obj.to_s) without multiplication. This differs from the standard ruby-money gem which expects cents/fractional units.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
  • app/models/concerns/simplefin_numeric_helpers.rb
📚 Learning: 2025-11-24T16:55:43.069Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T16:55:43.069Z
Learning: Applies to app/models/**/*.rb : All monetary values stored in base currency (user's primary currency). Use `Money` objects for currency conversion and formatting.

Applied to files:

  • app/models/concerns/simplefin_numeric_helpers.rb
📚 Learning: 2025-11-24T16:54:59.198Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T16:54:59.198Z
Learning: Applies to app/models/**/*.rb : Store all monetary values in base currency (user's primary currency) and use Money objects for conversion

Applied to files:

  • app/models/concerns/simplefin_numeric_helpers.rb
📚 Learning: 2025-11-24T16:56:13.406Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-conventions.mdc:0-0
Timestamp: 2025-11-24T16:56:13.406Z
Learning: Applies to **/*.{rb,js,erb} : Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Applied to files:

  • app/models/concerns/simplefin_numeric_helpers.rb
🧬 Code graph analysis (2)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (2)
app/models/concerns/simplefin_numeric_helpers.rb (1)
  • to_decimal (8-17)
lib/simplefin/date_utils.rb (1)
  • parse_provider_date (9-27)
app/models/concerns/simplefin_numeric_helpers.rb (2)
app/models/simplefin_item/importer.rb (2)
  • to_decimal (862-871)
  • same_sign? (873-875)
lib/money/arithmetic.rb (2)
  • positive? (53-55)
  • negative? (49-51)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (4)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (2)

63-82: Enablement toggle logic matches “default ON” intent; consider making precedence explicit in comment/tests.
Right now, Setting["simplefin_cc_overpayment_detection"] overrides ENV["SIMPLEFIN_CC_OVERPAYMENT_HEURISTIC"], and default is ON when neither is set. That’s fine given PR objectives, but it’s easy to misread later.


146-197: Classification logic is coherent; one edge-case to sanity-check: debt classification gating on min_payments.
For accounts with mostly charges inside the window (few/no payments yet), :debt will stay :unknown and fall back to sign-only. If that’s intentional (avoid flipping on sparse evidence), OK; otherwise consider allowing :debt when charges dominance is strong even with payments_count < min_payments.

app/models/concerns/simplefin_numeric_helpers.rb (1)

1-22: Nice extraction; same_sign? is intentionally “false if either is zero” (good for your use).
to_decimal’s “invalid string → 0” behavior is consistent with “robust fallback” goals; just be aware it can mask upstream data issues (which is probably OK here). As per coding guidelines, using a shared concern for DRY model logic is the right direction.

app/models/simplefin_account/processor.rb (1)

1-29: include SimplefinNumericHelpers is the right reuse point.
No concerns with this integration point.

…ency

- Simplified `enabled?` method in `OverpaymentAnalyzer` for clearer precedence order (Setting > ENV > default).
- Added `parse_bool` helper to streamline boolean parsing.
- Enhanced error handling with detailed logging for transaction gathering failures.
- Improved `sticky_key` method to use a temporary object ID fallback when `@sfa.id` is missing.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (2)

82-116: Consider extracting repetitive setting getter pattern.

All six configuration methods follow the same pattern: fetch Setting with presence check, coerce to type, validate, fallback to default. While explicit and clear, this could be DRYed with a helper:

def setting_with_default(key, default, type: :integer, &validator)
  val = Setting["simplefin_cc_overpayment_#{key}"]
  v = case type
      when :integer then (val.presence || default).to_i
      when :decimal then to_decimal(val.presence || default)
      end
  validator ? (validator.call(v) ? v : default) : (v > 0 ? v : default)
end

However, the current explicit approach is also maintainable, so this is optional.


176-195: Classification logic is sound; consider extracting magic number.

The epsilon-based bounds and statement guard provide robust classification. The 0.5% adaptive epsilon at line 182 is a magic number that could be extracted:

EPSILON_PERCENTAGE = BigDecimal("0.005") # 0.5% of observed balance

def classify(m)
  # ...
  eps = [ epsilon_base, (@observed.abs * EPSILON_PERCENTAGE) ].max
  # ...
end

However, this is minor and the current code is clear.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ce6899 and d97e887.

📒 Files selected for processing (1)
  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rb: Application supports two modes: 'managed' and 'self_hosted' via Rails.application.config.app_mode
Use Current.user and Current.family instead of current_user / current_family for authentication context
Optimize database queries with proper indexes to prevent N+1 queries using includes/joins

**/*.rb: Use Rails pluralization in i18n: t("transactions.count", count: @transactions.count).
Run bin/rubocop -f github -a for Ruby linting with auto-correct before pull requests.

**/*.rb: Ruby code should use 2-space indent, snake_case for methods and variables, and CamelCase for classes and modules, following Rails conventions for folders and file names
Prefer environment variables over hard-coded values for configuration

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
app/models/**/*.rb

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/models/**/*.rb: Keep business logic in models using skinny controllers and fat models pattern
Store all monetary values in base currency (user's primary currency) and use Money objects for conversion
Use ActiveRecord validations for form convenience and complex business logic validations
Use Rails concerns and POROs for organizing business logic instead of services folder

app/models/**/*.rb: All monetary values stored in base currency (user's primary currency). Use Money objects for currency conversion and formatting.
Place business logic in app/models/ folder. Avoid creating app/services/ folder. Use Rails concerns and POROs for organization.
Models should answer questions about themselves using instance methods: use account.balance_series not AccountSeries.new(account).call.

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{rb,erb,html.erb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{rb,erb,html.erb}: Use Current.user for the current user context in controllers and views. Do NOT use current_user.
Use Current.family for the current family context. Do NOT use current_family.

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{erb,html.erb,rb}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{erb,html.erb,rb}: All user-facing strings must use localization (i18n) via the t() helper. Update locale files for each new or changed element.
Use server-side formatting for currencies, numbers, and dates. Do not format in JavaScript.

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
{app/models/**/*.rb,db/migrate/**/*.rb}

📄 CodeRabbit inference engine (CLAUDE.md)

Simple validations (null checks, unique indexes) should be in the database. ActiveRecord validations for form convenience (prefer client-side when possible). Complex validations and business logic in ActiveRecord.

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{rb,erb}

📄 CodeRabbit inference engine (.cursor/rules/general-rules.mdc)

**/*.{rb,erb}: Use Current.user for accessing the current user. Do NOT use current_user
Use Current.family for accessing the current family. Do NOT use current_family

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{rb,js,erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Format currencies, numbers, dates, and other values server-side, then pass to Stimulus controllers for display only

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
**/*.{rb,html.erb}

📄 CodeRabbit inference engine (.cursor/rules/project-conventions.mdc)

Use ActiveRecord validations for complex validations and business logic. Simple validations may be mirrored in ActiveRecord for form error handling convenience, but prioritize client-side form validation when possible

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
app/**/*.{rb,js,ts,jsx,tsx,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Code should be organized in app/ directory (Rails MVC, services, jobs, mailers, components), with JS in app/javascript/, and styles/assets in app/assets/ (Tailwind, images, fonts)

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
app/**/*.rb

📄 CodeRabbit inference engine (AGENTS.md)

Run bin/brakeman security scan before major PRs to check for static analysis of common Rails issues

Files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
🧠 Learnings (11)
📓 Common learnings
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account sync must perform transfer auto-matching, calculate daily balance records from start_date to Date.current, calculate holdings, and optionally enrich transaction data
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method in EnableBankingAccount::Processor follows the same pattern as the Plaid integration, using OpenStruct without explicit requires and returning balance values as-is without BigDecimal coercion for consistency across banking integrations.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account model is a Rails delegated type with subtypes: Depository, Investment, Crypto, Property, Vehicle, OtherAsset (assets) and CreditCard, Loan, OtherLiability (liabilities)

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:36:45.479Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account/processor.rb:72-79
Timestamp: 2025-09-13T11:36:45.479Z
Learning: The balance_calculator method pattern is consistent across banking integrations in the codebase. Both PlaidAccount::Processor and EnableBankingAccount::Processor use the same approach: `balance = account.current_balance || account.available_balance || 0` followed by `OpenStruct.new(balance: balance, cash_balance: balance)` without explicit requires, and include identical comments about not distinguishing cash vs non-cash balances for non-investment accounts.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-03T01:15:59.638Z
Learnt from: Himmelschmidt
Repo: we-promise/sure PR: 104
File: app/models/simplefin_item/importer.rb:34-45
Timestamp: 2025-09-03T01:15:59.638Z
Learning: In SimpleFin integration, the default initial sync period is intentionally set to 7 days (not 3 years) to provide quick setup, with users able to optionally extend to 3 years via sync_start_date selection. This design balances setup speed with comprehensive data access options.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T12:23:31.324Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: db/migrate/20250825224717_add_enable_banking_domain.rb:3-34
Timestamp: 2025-09-13T12:23:31.324Z
Learning: When suggesting database schema improvements for banking integrations, consider consistency across all similar integrations (Plaid, SimpleFin, Enable Banking) and propose comprehensive changes in a separate PR rather than making changes to individual integrations in isolation.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:33:54.159Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_account.rb:9-20
Timestamp: 2025-09-13T11:33:54.159Z
Learning: The EnableBankingAccount model uses a `has_balance` validation method that ensures at least one balance value (current_balance or available_balance) is present, similar to the Plaid integration pattern. This means balance values should not be nil and the validation expects actual balance data from the API.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:40:26.913Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/models/enable_banking_entry/processor.rb:10-12
Timestamp: 2025-09-13T11:40:26.913Z
Learning: The plaid_id field is used consistently across multiple banking integrations (Plaid, SimpleFin, and Enable Banking) as a common external identifier field. A comprehensive refactor to use a proper external_id field should be done across all integrations in a separate PR to maintain consistency and avoid scope creep.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-09-13T11:27:55.271Z
Learnt from: alessiocappa
Repo: we-promise/sure PR: 141
File: app/controllers/enable_banking_items_controller.rb:48-62
Timestamp: 2025-09-13T11:27:55.271Z
Learning: The `create_enable_banking_item!` method in `Family::EnableBankingConnectable` uses `find_or_create_by(id: enable_banking_id)` to either find an existing EnableBankingItem or create a new one, so it automatically handles updates to existing items without creating duplicates.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-11-04T16:21:39.347Z
Learnt from: sokie
Repo: we-promise/sure PR: 276
File: app/controllers/reports_controller.rb:80-84
Timestamp: 2025-11-04T16:21:39.347Z
Learning: The we-promise/sure codebase uses a custom Money class (lib/money.rb) that treats numeric inputs as dollar amounts, not fractional units (cents). When Money.new receives a numeric value, it converts it directly to BigDecimal via BigDecimal(obj.to_s) without multiplication. This differs from the standard ruby-money gem which expects cents/fractional units.

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
📚 Learning: 2025-11-24T16:56:30.669Z
Learnt from: CR
Repo: we-promise/sure PR: 0
File: .cursor/rules/project-design.mdc:0-0
Timestamp: 2025-11-24T16:56:30.669Z
Learning: Account sync must perform transfer auto-matching, calculate daily balance records from start_date to Date.current, calculate holdings, and optionally enrich transaction data

Applied to files:

  • app/models/simplefin_account/liabilities/overpayment_analyzer.rb
🧬 Code graph analysis (1)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (3)
app/models/concerns/simplefin_numeric_helpers.rb (1)
  • to_decimal (8-17)
lib/money/arithmetic.rb (4)
  • abs (41-43)
  • class (22-24)
  • positive? (53-55)
  • negative? (49-51)
lib/simplefin/date_utils.rb (1)
  • parse_provider_date (9-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci / test
🔇 Additional comments (7)
app/models/simplefin_account/liabilities/overpayment_analyzer.rb (7)

1-24: LGTM! Clear documentation and well-structured constants.

The class documentation clearly explains the classification approach, fallback strategy, and conventions. The DEFAULTS hash provides sensible thresholds for the heuristic.


26-30: LGTM! Clean initialization with testable time injection.

The now: parameter enables deterministic testing of time-dependent behavior (sticky cache expiry, statement guards).


63-80: LGTM! Flexible configuration with clear precedence.

The three-tier fallback (Setting → ENV → default) provides good operational control, and parse_bool handles various formats robustly.


118-143: LGTM! Robust transaction gathering with appropriate fallbacks.

The two-tier approach (entries → raw payload) provides resilience, and the exception handler ensures the analyzer degrades gracefully rather than failing hard.


145-174: LGTM! Accurate metrics computation with BigDecimal precision.

The separation of charges (positive) and payments (negative) correctly handles Maybe's transaction convention, and the recent_payment tracking enables the statement boundary guard.


197-224: LGTM! Clean helper methods with proper convention conversion.

The negation in convert_provider_amount correctly converts SimpleFIN's banking convention to Maybe's expense convention, and the sticky cache key properly handles unpersisted records.


37-37: Verify exclusion of OtherLiability type.

The check only includes "CreditCard" and "Loan", but the Account model includes OtherLiability as a liability type according to project learnings. If OtherLiability accounts can experience overpayments, they should be included.

#!/bin/bash
# Description: Check if OtherLiability is used in the codebase and whether it should support overpayment detection

# Find OtherLiability references
rg -n "OtherLiability" --type rb -C 3

# Check Account model for liability types
ast-grep --pattern 'class OtherLiability'

@jjmata jjmata requested a review from sokie December 14, 2025 19:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants