Skip to content

Feat/loans overview new insights#1775

Open
HugoleDino wants to merge 56 commits into
we-promise:mainfrom
HugoleDino:feat/loans-overwiew-new-insights
Open

Feat/loans overview new insights#1775
HugoleDino wants to merge 56 commits into
we-promise:mainfrom
HugoleDino:feat/loans-overwiew-new-insights

Conversation

@HugoleDino

@HugoleDino HugoleDino commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Add new editable loan parameters and enhance the overview with additional financial insights.
This PR replaces #1497

Changes

Editable fields added

  • Down Payment
  • Insurance Rate
  • Insurance Rate Type
  • Start Date

Overview enhancements

  • Display of remaining loan amount
  • Calculation of initial leverage (Debt / Down Payment)
  • Updated calculations based on editable settings:
    • Total cost of the loan
    • Monthly payment breakdown:
      • Principal
      • Interest
      • Insurance

These changes improve flexibility for users and provide clearer visibility into loan dynamics and costs.

Screenshots

image image

Summary by CodeRabbit

  • New Features

    • Added down payment, loan start date, insurance rate and insurance type.
    • Amortization schedules, monthly payment breakdowns (principal, interest, insurance), totals (interest, insurance, paid, cost), repayment progress (elapsed/remaining months + visual indicator), and cached schedule generation.
    • Enhanced loan form and overview UI, with English/French translations.
  • Bug Fixes / Validation

    • Added validations and defaults for start date, rates, term and down payment.
  • Tests

    • Expanded model and system tests covering schedules, ratios, edge cases.
  • Chores

    • Database schema updated with new columns and constraints.

Review Change Stack

@superagent-security superagent-security Bot added contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis. labels May 12, 2026
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds down_payment, start_date, insurance_rate, and insurance_rate_type; implements amortization schedule, timeline and cost calculations on Loan; updates controller permits, form and overview UI, locales, migrations, schema, fixtures, and tests.

Changes

Loan Amortization and Cost Tracking System

Layer / File(s) Summary
Database schema and migrations
db/migrate/20260512155544_add_down_payment_to_loans.rb, db/migrate/20260512155616_add_start_date_to_loans.rb, db/migrate/20260512155636_add_insurance_rate_type_to_loans.rb, db/migrate/20260512155654_add_insurance_rate_to_loans.rb, db/migrate/20260512191028_enforce_start_date_not_null_on_loans.rb, db/schema.rb
Adds down_payment, start_date, insurance_rate_type, and insurance_rate to loans with check constraints; backfills/enforces start_date; regenerates schema dump with updated constraints and reordered declarations.
Loan model validation and amortization engine
app/models/loan.rb
Defaults start_date before validation, expands numeric/inclusion validations, refactors monthly_payment to use BigDecimal/original_balance, and implements months_elapsed, remaining_months, finished?, cached amortization_schedule (with generate_amortization_schedule), remaining_balance_at, payment_breakdown, totals (total_insurance, total_paid, total_interest, total_cost), and ratio helpers (balance_paid_ratio, initial_leverage_ratio).
Form input and controller permitting
app/controllers/loans_controller.rb, app/views/loans/_form.html.erb
Controller permits new loan attributes. Form partial renders localized subtype select, down_payment money field, insurance_rate and insurance_rate_type inputs with translated labels/options, and a required start_date date field beside term_months.
Overview display and presentation
app/views/loans/tabs/_overview.html.erb
Rewrites overview tab to compute and display repayment progress via balance_paid_ratio, shows monthly payment breakdown from payment_breakdown, elapsed/remaining months, term split (years/months), rate type, initial leverage styling, and conditional total cost card.
Localization for form and overview UI
config/locales/views/loans/en.yml, config/locales/views/loans/fr.yml
Adds/enhances English and French translations for loan subtypes, form fields (down_payment, insurance_rate, insurance_rate_type, start_date), overview labels, rate_types, and pluralized year/month strings.
Testing and fixtures
test/fixtures/loans.yml, test/models/loan_test.rb, test/system/accounts_test.rb
Fixture adds start_date. Tests expanded with create_loan helper and many cases covering timeline methods, amortization schedule generation/caching and edge cases, totals, payment_breakdown ratios and nil/zero handling, balance_paid_ratio clamping, and initial_leverage_ratio. System test fills Start date in account creation.

Sequence Diagram

sequenceDiagram
  participant User as User/Form
  participant Controller as LoansController
  participant Loan as Loan Model
  participant Cache as Rails.cache
  participant Generator as AmortizationGenerator
  participant View as Overview View

  User->>Controller: submit loan form with down_payment, start_date, insurance_rate, insurance_rate_type
  Controller->>Loan: create/update Loan with new attributes
  Loan->>Loan: before_validation sets default start_date
  Loan->>Loan: validate numeric and enum fields
  View->>Loan: request amortization_schedule
  Loan->>Cache: fetch by cache_key_with_version + original_balance_cache_key
  Cache-->>Loan: return cached schedule or miss
  Loan->>Generator: generate_amortization_schedule (month-by-month)
  Generator->>Generator: compute interest, insurance (insurance_rate_type logic), principal, remaining_balance
  Generator-->>Loan: return schedule array
  Loan->>View: return cached schedule, payment_breakdown, totals, ratios
  View->>User: render progress circle, cost cards, payment detail
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jjmata
  • sokie

Poem

🐰 I hop through months of principal and interest,
Tallying insurance and down-payment cheer,
Schedules unfold as balances fall small,
Ratios and totals — I track them all,
Tests keep my sums honest, month after month.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title 'Feat/loans overview new insights' is directly related to the PR's main objective of enhancing the loan overview with new financial insights, though it is somewhat generic and doesn't convey the full scope of changes (new editable parameters, calculations, UI updates).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@jjmata

jjmata commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Not sure how much of this you had a chance to address, but reposting here for continuity:


Independent review of the full diff. I verified which earlier comments were addressed, then read the code fresh. Several earlier raised issues are genuinely fixed; a few open threads remain, and I found additional problems.


Still-open review thread issues

1. db/schema.rbdown_payment missing precision / scale (unresolved, not outdated)

The migration defines decimal(15, 2) but the schema dump records an unconstrained decimal:

# schema.rb (current)
t.decimal "down_payment"

# should be
t.decimal "down_payment", precision: 15, scale: 2

Databases loaded from db/schema.rb will diverge from migration-driven ones. Fix by re-running bin/rails db:schema:dump after all migrations have run.


2. db/schema.rb — Unrelated columns included (unresolved, not outdated)

The schema diff contains columns and indexes that have no corresponding migration in this PR:

  • accounts.holdings_snapshot_data (jsonb)
  • accounts.holdings_snapshot_at (datetime)
  • snaptrade_accounts.account_id (string + unique index)
  • trades.realized_gain, cost_basis_amount, cost_basis_currency, holding_period_days, realized_gain_confidence, realized_gain_currency, plus the realized_gain partial index

db:schema:load will install all of these while running only this PR's migrations will not. Please regenerate schema.rb against a clean migration history so only the intended loan columns appear in this diff.


New findings

3. app/models/loan.rb — Callback and internal helpers are public

set_default_start_date, generate_amortization_schedule, and payment_date_for are all public. The callback helper should be private (Rails convention). generate_amortization_schedule is an internal step of amortization_schedule — callers bypassing it skip the caching layer entirely, and the method is named like a command, not a query anyone should call directly. Suggest placing all three under private.


4. app/views/loans/tabs/_overview.html.erb — Actual current balance no longer shown (functional regression)

The old overview displayed account.balance_money — the real synced balance from the institution. The new overview replaces this with an elapsed-time progress ring (loan.elapsed_ratio). These are fundamentally different:

  • A user who made extra payments will see "25% complete" based on time even though they've paid down 40% of the balance.
  • A user who missed payments will see the same ring as someone current.

The loans.overview.remaining_principal locale key still exists but is now unused. I'd expect both pieces of information (actual balance + amortization progress) to be visible, not one replacing the other.


5. app/views/loans/tabs/_overview.html.erb — "Edit loan details" link removed with no replacement

The original overview had an explicit DS::Link to edit_loan_path. The new overview drops it entirely. If the edit path isn't accessible from elsewhere in the loan UI, users won't be able to correct their loan settings after saving.


6. config/locales/views/loans/en.yml — Wrong word in already_paid_html

already_paid_html: "You have already refunded <span ...>%{percentage}</span> of your loan"

"Refunded" means money came back to you. The correct word for paying back a loan is "repaid" or "paid back". The French translation correctly uses "remboursé" (repaid) — the English should match.


7. config/locales/views/loans/en.yml + fr.yml — YAML indentation inconsistency + missing trailing newline

The years pluralization block uses 4-space child indentation while months uses 2-space:

      years:
          one: "%{count} year"    # 10 spaces from root — inconsistent
          other: "%{count} years"
      months:
        one: "%{count} month"     # 8 spaces from root
        other: "%{count} months"

YAML parses both correctly (relative indentation is what counts), but the inconsistency is a maintenance hazard and the same pattern appears in fr.yml. Neither file ends with a trailing newline.


8. app/models/loan.rb — Cache key does not cover original_balance source

amortization_schedule caches by cache_key_with_version, which changes when the Loan record itself is saved. But original_balance is derived from account.first_valuation_amount — a value that lives on the Account / its valuations, not on the Loan. If the initial balance entry is edited without touching any Loan attribute, the cached schedule will be stale and show incorrect payment breakdowns.


What was confirmed addressed ✅

For completeness — these earlier-raised concerns are genuinely fixed in the current diff:

  • insurance_rate || 0 correctly handles nil insurance in schedule generation
  • start_date validated on: :create only; migration backfills and enforces NOT NULL
  • months_elapsed uses start + months.months > as_of — end-of-month correctness fixed
  • Schedule dates use start_date >> i instead of the drifting next_month loop
  • remaining_balance_at guards month_number&.positive?
  • total_cost correctly sums original_balance + total_interest + total_insurance
  • DB check constraints added for down_payment ≥ 0, insurance_rate ≥ 0, and insurance_rate_type IN (…)
  • start_date migration uses up/down with backfill before enforcing NOT NULL
  • Insurance rate type options and rate type labels are i18n'd
  • Term display uses divmod(12) to render years + remaining months correctly
  • Invalid "declining" enum in test fixtures corrected to "decreasing_life"

Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
app/views/loans/tabs/_overview.html.erb (1)

171-171: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the CTA text instead of hardcoding English.

Line 171 should use t(...) for user-facing copy to keep locale parity.

As per coding guidelines, user-facing strings in views/components must use t().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/loans/tabs/_overview.html.erb` at line 171, Replace the hardcoded
CTA string "Edit loan details" in the _overview.html.erb view with a translation
helper call (t(...)) so the user-facing copy is localized; update the view to
call t with a descriptive key (e.g. t('loans.tabs.overview.edit_loan_details')
or a scoped key like t('.edit_loan_details')) and add the corresponding entry to
your locale YAML (e.g. en.yml) under that key; ensure the same key is used
wherever this CTA appears so tests and locales remain consistent.
🧹 Nitpick comments (2)
app/views/loans/_form.html.erb (1)

7-9: ⚡ Quick win

Use t() helper instead of I18n.t in ERB user-facing copy.

Switch the option label lookup to t(...) for consistency with project i18n conventions in views.

As per coding guidelines, user-facing strings in views/components must use t() for translation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/loans/_form.html.erb` around lines 7 - 9, The view uses I18n.t
inside the loan_form.select options mapping (Loan::SUBTYPES.map { |k, v|
[I18n.t("loans.subtypes.#{k}.long", default: v[:long]), k] }) which violates the
project convention for views; replace I18n.t with the view helper t(...) so the
option labels use t("loans.subtypes.#{k}.long", default: v[:long]) while keeping
the rest of the select call (loan_form.select, Loan::SUBTYPES, and the
prompt/include_blank options) unchanged.
app/views/loans/tabs/_overview.html.erb (1)

29-29: ⚡ Quick win

Replace raw Tailwind palette classes with design-system tokens.

These SVG stroke classes bypass the design token system. Please switch to functional tokens defined by the project design system.

As per coding guidelines, ERB/CSS should use functional tokens from app/assets/tailwind/sure-design-system.css instead of raw Tailwind palette classes.

Also applies to: 38-38

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/views/loans/tabs/_overview.html.erb` at line 29, SVG elements currently
use raw Tailwind palette classes like "stroke-zinc-700 fill-none"; replace those
with the project's functional design-system token classes defined in
app/assets/tailwind/sure-design-system.css (e.g., swap "stroke-zinc-700" for the
equivalent token class from the design system and keep "fill-none" if
appropriate). Locate the SVGs in app/views/loans/tabs/_overview.html.erb (the
class attributes at the shown lines and the other occurrence around line 38) and
update both occurrences to use the corresponding design-system stroke token
class names so the code uses the project's tokens rather than raw palette
classes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/models/loan.rb`:
- Line 39: The model currently only validates start_date on create via the
validates :start_date, presence: true, on: :create line in Loan, which lets
DB-level NOT NULL errors surface on update; remove the on: :create option (or
change to on: :save) so start_date presence is validated on updates as well,
updating the Loan validation to run for both create and update.

In `@config/locales/views/loans/fr.yml`:
- Line 51: The FR locale entry for the key rate_types.adjustable is left in
English ("Adjustable"); update the value to the proper French translation (e.g.,
"Ajustable" or "Variable") so the rate_types.adjustable key is consistent with
surrounding French labels.

In `@db/migrate/20260512155616_add_start_date_to_loans.rb`:
- Line 10: This migration currently enforces NOT NULL with change_column_null
:loans, :start_date, false which can break running older app instances; instead
make this migration only add the start_date column as nullable (or remove the
change_column_null line), add a safe backfill step that populates start_date for
all existing loans (e.g., an UPDATE that sets a sensible default or derived
value), deploy the app so all writers include start_date, and then create a
separate follow-up migration that runs change_column_null :loans, :start_date,
false to enforce NOT NULL once rollout is complete.

---

Outside diff comments:
In `@app/views/loans/tabs/_overview.html.erb`:
- Line 171: Replace the hardcoded CTA string "Edit loan details" in the
_overview.html.erb view with a translation helper call (t(...)) so the
user-facing copy is localized; update the view to call t with a descriptive key
(e.g. t('loans.tabs.overview.edit_loan_details') or a scoped key like
t('.edit_loan_details')) and add the corresponding entry to your locale YAML
(e.g. en.yml) under that key; ensure the same key is used wherever this CTA
appears so tests and locales remain consistent.

---

Nitpick comments:
In `@app/views/loans/_form.html.erb`:
- Around line 7-9: The view uses I18n.t inside the loan_form.select options
mapping (Loan::SUBTYPES.map { |k, v| [I18n.t("loans.subtypes.#{k}.long",
default: v[:long]), k] }) which violates the project convention for views;
replace I18n.t with the view helper t(...) so the option labels use
t("loans.subtypes.#{k}.long", default: v[:long]) while keeping the rest of the
select call (loan_form.select, Loan::SUBTYPES, and the prompt/include_blank
options) unchanged.

In `@app/views/loans/tabs/_overview.html.erb`:
- Line 29: SVG elements currently use raw Tailwind palette classes like
"stroke-zinc-700 fill-none"; replace those with the project's functional
design-system token classes defined in
app/assets/tailwind/sure-design-system.css (e.g., swap "stroke-zinc-700" for the
equivalent token class from the design system and keep "fill-none" if
appropriate). Locate the SVGs in app/views/loans/tabs/_overview.html.erb (the
class attributes at the shown lines and the other occurrence around line 38) and
update both occurrences to use the corresponding design-system stroke token
class names so the code uses the project's tokens rather than raw palette
classes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 608e2518-c62b-4628-98fc-5f113d5992f9

📥 Commits

Reviewing files that changed from the base of the PR and between 18f0718 and e9e16c9.

📒 Files selected for processing (14)
  • app/controllers/loans_controller.rb
  • app/models/loan.rb
  • app/views/loans/_form.html.erb
  • app/views/loans/tabs/_overview.html.erb
  • config/locales/views/loans/en.yml
  • config/locales/views/loans/fr.yml
  • db/migrate/20260512155544_add_down_payment_to_loans.rb
  • db/migrate/20260512155616_add_start_date_to_loans.rb
  • db/migrate/20260512155636_add_insurance_rate_type_to_loans.rb
  • db/migrate/20260512155654_add_insurance_rate_to_loans.rb
  • db/schema.rb
  • test/fixtures/loans.yml
  • test/models/loan_test.rb
  • test/system/accounts_test.rb

Comment thread app/models/loan.rb Outdated
Comment thread config/locales/views/loans/fr.yml Outdated
Comment thread db/migrate/20260512155616_add_start_date_to_loans.rb Outdated
@jjmata
jjmata self-requested a review May 12, 2026 18:42
Signed-off-by: HugoleDino <135261771+HugoleDino@users.noreply.github.com>
@HugoleDino

Copy link
Copy Markdown
Contributor Author

Hey @jjmata !
I integrated in this PR the fixes of the problems you raised. I will check the new coderabbit reviews.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@db/schema.rb`:
- Line 250: The schema.rb currently contains unrelated changes (e.g.
columns/categories.lucide_icon, families.vector_store_id,
import_rows.exchange_operating_mic, imports.rows_to_skip,
snaptrade_accounts.activities_fetch_pending,
sophtron_accounts.account_number_mask, sophtron_items timestamp reorder,
users.ui_layout) that must be removed by regenerating the schema from a clean DB
state; revert the current db/schema.rb to the upstream/base version, create a
fresh empty database, run all migrations from scratch so only the loan-related
migrations are applied, dump the schema (regenerate db/schema.rb) and verify
that the only diffs left reference the loan migration artifacts before
committing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2eca72e2-bb04-4dd8-aede-3233f728581a

📥 Commits

Reviewing files that changed from the base of the PR and between e9e16c9 and 71dfb46.

📒 Files selected for processing (1)
  • db/schema.rb

Comment thread db/schema.rb

Copy link
Copy Markdown
Collaborator

A few blockers to address before this is reviewable:

  1. CI failingci / test fails on the current head commit. Please fix before requesting review.
  2. Dirty db/schema.rb — the schema file contains column noise from other branches. Regenerate with bin/rails db:migrate on a clean main checkout.
  3. Unsafe migrationadd_start_date_to_loans adds a NOT NULL column without a default or backfill. This will fail on any existing database with rows. Add a default or a two-step (add nullable → backfill → add constraint) migration.
  4. i18n — several view strings are hardcoded English rather than using t().
  5. Design tokens — raw Tailwind palette classes (stroke-zinc-700, etc.) should use the functional tokens from sure-design-system.css.

Generated by Claude Code

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
@superagent-security superagent-security Bot removed the pr:verified PR passed security analysis. label May 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
db/schema.rb (1)

255-257: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Regenerate schema.rb from a clean migration state.

This dump still carries non-loan schema drift: Line 255 loosens binance_items nullability, Line 1341 adds an unrelated unique index on recurring_transactions, and the other touched lines are unrelated table changes outside this feature. Since db:schema:load uses this file, merging it would ship schema changes that are not part of the loan work.

Also applies to: 344-344, 688-688, 849-849, 893-893, 1341-1341, 1540-1540, 1591-1591, 1615-1616, 1808-1808

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@db/schema.rb` around lines 255 - 257, The schema dump contains unrelated
schema drift (e.g., loosened nullability on binance_items, a new unique index on
recurring_transactions and other unrelated table changes); to fix this, reset to
a clean DB state, run the full migrations for the current branch, then
regenerate schema.rb so it only reflects intended loan-related migrations: run
the Rails tasks to drop/create the test/dev DB, run migrations from scratch and
then run rails db:schema:dump (or rails db:migrate followed by db:schema:dump)
to overwrite db/schema.rb; verify that the resulting file no longer contains the
unrelated changes (look for binance_items nullability, the
recurring_transactions unique index, and the other listed line artifacts) before
committing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@db/schema.rb`:
- Around line 255-257: The schema dump contains unrelated schema drift (e.g.,
loosened nullability on binance_items, a new unique index on
recurring_transactions and other unrelated table changes); to fix this, reset to
a clean DB state, run the full migrations for the current branch, then
regenerate schema.rb so it only reflects intended loan-related migrations: run
the Rails tasks to drop/create the test/dev DB, run migrations from scratch and
then run rails db:schema:dump (or rails db:migrate followed by db:schema:dump)
to overwrite db/schema.rb; verify that the resulting file no longer contains the
unrelated changes (look for binance_items nullability, the
recurring_transactions unique index, and the other listed line artifacts) before
committing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 721e7be2-093b-428b-8ff5-ded3205e8604

📥 Commits

Reviewing files that changed from the base of the PR and between 87778ab and fab6b5b.

📒 Files selected for processing (1)
  • db/schema.rb

@jjmata jjmata left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please fix broken tests and merge conflicts.

jjmata and others added 4 commits May 19, 2026 21:51
* feat: add Cloudflare Containers PR preview deployments

Add GitHub workflows to automatically deploy PRs to Cloudflare
Containers after tests pass, with automatic cleanup after 24 hours.

Components:
- workers/preview/: Cloudflare Worker entry point that routes
  traffic to the Rails container
- preview-deploy.yml: Deploys PRs after CI passes, comments
  preview URL on PR
- preview-cleanup.yml: Cleans up previews on PR close or after
  24 hours via scheduled job

The container sleeps after 30 minutes of inactivity and wakes
automatically on the next request.

Required secrets:
- CLOUDFLARE_API_TOKEN
- CLOUDFLARE_ACCOUNT_ID
- CLOUDFLARE_WORKERS_SUBDOMAIN

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: use development environment with embedded PostgreSQL for previews

- Add preview-specific Dockerfile with PostgreSQL server included
- Add docker-entrypoint.sh to start PostgreSQL and run migrations
- Change RAILS_ENV from production to development
- Auto-generate SECRET_KEY_BASE and DATABASE_URL for self-contained previews

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* feat: add Redis to preview container

- Install redis-server in the preview Dockerfile
- Start Redis in the entrypoint before PostgreSQL
- Auto-configure REDIS_URL for Sidekiq background jobs

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: mark GitHub deployment inactive on manual PR cleanup

When using workflow_dispatch with a specific pr_number, the workflow
now also marks the associated GitHub deployment as inactive, mirroring
the behavior of the batch cleanup path.

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: remove npm cache config that requires missing lockfile

The setup-node action's cache feature requires a package-lock.json
which doesn't exist in workers/preview/. Remove the cache configuration
to fix the workflow.

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: only update deployment status when deployment ID exists

Add condition to check steps.deployment.outputs.result exists before
attempting to update deployment status. This prevents a JavaScript
syntax error when the deployment step fails and no ID is available.

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: quote shell variables to fix SC2086 shellcheck warning

Quote the --var argument and GITHUB_OUTPUT redirection to prevent
word splitting issues.

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: add permissions for deployment status operations

Add deployments: write permission to the cleanup workflow so the
GITHUB_TOKEN can list and update deployment statuses.

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: specify build context for Dockerfile in wrangler config

Use object syntax for image config to set build context to repository
root, allowing the Dockerfile to reference files from both the root
(Gemfile, .ruby-version) and workers/preview/ (docker-entrypoint.sh).

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: run wrangler from repo root for correct build context

- Update workflow to run wrangler with --config flag from repo root
- Update wrangler.toml paths (main, image) to be relative to repo root
- Embed entrypoint script directly in Dockerfile using heredoc
- Remove separate docker-entrypoint.sh file

This ensures the Docker build context includes Gemfile, .ruby-version,
and other files at the repo root.

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: move preview Dockerfile to repo root for correct build context

Wrangler resolves paths relative to the config file, not the current
directory. Moving Dockerfile.preview to repo root ensures:
- Build context is the repo root (where Gemfile, .ruby-version are)
- Path in wrangler.toml is ../../Dockerfile.preview (relative to config)
- Worker runs from workers/preview/ directory again

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: use find to locate pg_hba.conf instead of glob in redirection

Shell glob patterns don't work with redirection operators. Use find
to locate the actual pg_hba.conf path before writing to it.

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix: enable workers_dev for preview deployments

Add workers_dev = true to make the preview worker accessible via
the workers.dev subdomain.

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* feat: enable observability for container logs

https://claude.ai/code/session_013EZuzBxWPEEYp3TQptXWdP

* fix preview container boot path

* fix: set preview container startup command explicitly

* fix: update preview worker compatibility date

* chore: expose preview container diagnostics

* fix: recover from stale preview container state

* fix: harden preview container startup paths

* chore: report preview startup stages

* fix: bypass stale container helper state during recovery

* fix: allow longer preview container startup

* fix: upgrade preview container runtime

* fix: use supported node version for preview deploy

* fix: use public container startup flow

* fix: simplify preview container startup

* chore: retain preview container diagnostic history

* fix: bypass systemctl redirect for postgres startup

* chore: probe rails readiness from inside preview container

* chore: capture rails process and port diagnostics

* chore: capture rails startup logs on preview timeout

* fix: align preview bind behavior with ipv6 startup model

* chore: capture preview socket state on rails timeout

* chore: capture rails wait state and child processes

* fix: launch preview with puma directly

* fix: run preview in production mode

* chore: probe preview app boot before puma

* fix: disable lookbook routes in production preview

* chore: capture ruby backtrace from hung boot probe

* fix: disable bootsnap in preview runtime

* fix: disable sidekiq web routes in production preview

* chore: trace hung preview boot probe with strace

* fix: json-escape preview telemetry payloads

* fix: pass preview telemetry env vars correctly

* chore: signal ruby child for preview boot backtrace

* fix: allow longer preview cold-start budget

* fix: skip sidekiq web requires in production preview

* chore: deploy hello world preview container

* fix(preview): restore rails image without redundant warmup

* feat(preview): seed demo dataset on boot

* ci(preview): require preview-cf label

* ci(preview): reuse pr workflow checks

* fix(preview): avoid clearing demo data in production boot

* fix(preview): tolerate already-running postgres on boot

* fix(preview): check demo user via psql during boot

* fix(preview): defer heavy demo seed until after boot

* fix(preview): move demo-user creation after rails boot

* fix(preview): fail fast on container lifecycle errors

* fix(preview): validate manual cleanup pr input

* fix(preview): parameterize preview pr number

* ci(preview): use setup-node v6

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>
* fix(preview): use worker list metadata for cleanup

* fix(preview): handle cleanup edge cases

* fix(preview): harden scheduled cleanup errors

* feat(preview): add warmup screen and readiness gate

* fix(preview): report success after image deploy

* fix(preview): stop blocking healthy previews on stale status
…mise#1806)

* Extract hardcoded strings to i18n

Replace numerous hardcoded English strings with I18n lookups (t / I18n.t) across controllers, views, helpers, and components, and convert model validation error messages to symbol keys. Added multiple locale files under config/locales for models and views. This centralizes user-facing notices/alerts, UI text, import/validation messages, and prepares the app for localization and easier translation maintenance.

* Update en.yml

* Update preview-cleanup.yml

* Revert "Update preview-cleanup.yml"

This reverts commit 1ba6d3c.

* test: align i18n assertions with translated messages

* Standardize balance error key and tweak locales

Replace SophtronAccount's :requires_balance error key with :no_balance and update related locale strings for sophtron, plaid, and simplefin accounts to use the new key and clearer copy. Also switch the QIF upload redirect notice to use a relative translation key (t('.qif_uploaded')), remove an unused SSO providers help line, and fix a trailing-newline/whitespace issue in the subscriptions locale. These changes standardize validation keys and improve translation consistency and messaging.

---------

Co-authored-by: KiloClaw <kiloclaw@openclaw.ai>

Copy link
Copy Markdown
Collaborator

DS Drift Patrol — 1 finding

Rule 2 — Non-functional Tailwind tokens (app/views/loans/tabs/_overview.html.erb)

An SVG <circle> track element uses raw gray classes with a manual dark-mode variant:

class="text-gray-200 dark:text-gray-700"

Replace with class="text-subdued". DS functional tokens adapt to light/dark automatically via CSS variables — the explicit dark: override is not needed and will diverge from the DS theme in future.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

DS Drift Patrol — 2 additional findings

Supplementing the earlier scan from today which flagged text-gray-200 dark:text-gray-700 on the progress ring track. The filled-arc tokens below were not covered in that report.

Rule 2 — Non-functional Tailwind color literals (app/views/loans/tabs/_overview.html.erb)

The progress arc SVG uses raw amber palette tokens for both its stroke and label text:

class="stroke-amber-500 transition-all duration-500"
class: "font-medium text-amber-500"

stroke-amber-500 and text-amber-500 are hardcoded palette values. Align with the existing text-warning / stroke-warning semantic tokens (which map to amber in the current palette) so the arc inherits any future theme changes automatically:

class="stroke-warning transition-all duration-500"
class: "font-medium text-warning"

Generated by Claude Code

@HugoleDino

HugoleDino commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

DS Drift Patrol — 1 finding

Rule 2 — Non-functional Tailwind tokens (app/views/loans/tabs/_overview.html.erb)

An SVG <circle> track element uses raw gray classes with a manual dark-mode variant:

class="text-gray-200 dark:text-gray-700"

Replace with class="text-subdued". DS functional tokens adapt to light/dark automatically via CSS variables — the explicit dark: override is not needed and will diverge from the DS theme in future.

Generated by Claude Code

@sure-design This suggestion renders too light in dark mode and too strong in light mode for the empty circle track. It gives too much visual weight to the design compared to the actual data represented by the text-warning filled circle — though this is subjective. No existing DS token matches these visual requirements, so the current raw classes remain the most accurate implementation for now.

@superagent-security superagent-security Bot removed contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis. labels May 28, 2026
@jjmata

jjmata commented May 29, 2026

Copy link
Copy Markdown
Collaborator

@sure-design This suggestion renders too light in dark mode and too strong in light mode for the empty circle track. It gives too much visual weight to the design compared to the actual data represented by the text-warning filled circle — though this is subjective. No existing DS token matches these visual requirements, so the current raw classes remain the most accurate implementation for now.

Any recommendations @gariasf?

Replace the hand-rolled SVG progress ring with the shared `donut-chart`
Stimulus controller (the same one Budgets and Goals use). The empty track
now uses the `--budget-unused-fill` DS token (gray-200 light / gray-500
dark) instead of hardcoded `text-gray-200 dark:text-gray-700`, fixing the
track rendering too light in dark mode and too strong in light mode.

The filled arc keeps `var(--color-warning)` by marking the "paid" segment
as the controller's overage segment, so its var() color passes straight to
the SVG fill instead of being parsed by d3.color() (which returns null for
var(...)). Adds Loan#to_donut_segments to build the segment data.
@gariasf

gariasf commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Rather than hand-tuning the track color, I wired the ring onto the shared donut-chart controller that Budgets and Goals already use — so the empty track now pulls --budget-unused-fill (gray-200 light / gray-500 dark) and themes itself. Turns out a fitting token does exist, it's just the ring-track one we use elsewhere. Nice side effect: the loan ring now matches the other two.

Pushed in 7e0d9c1.

Loan repayment ring — light & dark

@gariasf

gariasf commented May 30, 2026

Copy link
Copy Markdown
Collaborator

@HugoleDino small thing on the Overview tab — every money figure renders with cents, but they're all computed as whole-dollar integers, so the .00 is structurally always zero. monthly_payment ends in payment.round(0).to_i, and generate_amortization_schedule rounds every row (interest / principal / insurance / remaining_balance are all .round(0).to_i). There's no input path where these show real cents — so it's just noise on an already number-dense grid.

They're also projections (30-year total cost/interest, estimated monthly payment), so the cents imply a precision the numbers don't actually carry.

I'd drop them with precision: 0, which is already how Sure formats its summary/overview surfaces — the dashboard balance sheet (incl. account balances), the budget donut, and the investment value tooltips all pass precision: 0. No new plumbing needed; format_money already forwards the option:

<%= format_money(loan.total_cost, precision: 0) %>   # → $652,320

Worth applying to original principal, total cost + breakdown, and monthly payment + breakdown. The real synced balance up in the account header can keep its cents — this is just about the projection grid.

@gariasf

gariasf commented May 30, 2026

Copy link
Copy Markdown
Collaborator

Did a design-system pass on the Overview tab — compared it against properties/tabs/_overview, vehicles/tabs/_overview, and accounts/_summary_card.

First, two things that look like they could be off but aren't — they match the peers exactly, so I'd leave them as-is:

  • Edit at the bottom (ghost DS::Link, modal frame) and no icons on the stat cards are byte-for-byte what property & vehicle do. (FWIW the header ⋯ menu already has an Edit too, but that double-up exists on every account type, not just loans.)
  • It's also correctly on summary_card, semantic color tokens, and the privacy blur. 👍

Where I think there's room — ran a quick experiment, three versions:

Current / A / B

A — drop the cents (precision: 0). Quick win, ties to my earlier note: these figures are always whole-dollar, and it matches how the dashboard balance sheet and budgets already format summary numbers.

B — composition bars. The breakdown cards hold proportional data but show it as a flat list, so the reader has to do the math. A small stacked bar tells the story instantly: interest is ~53% of total cost and ~78% of the current monthly payment. Reuses existing tokens (--color-blue-500 / --color-warning / --color-violet-500), no new DS, and it folds months elapsed/remaining into 36 / 360.

One tiny DS nit while you're in there: the Initial leverage value is text-2xl font-semibold — louder than every other card's text-xl font-medium. Worth normalizing.

@HugoleDino

Copy link
Copy Markdown
Contributor Author

Rather than hand-tuning the track color, I wired the ring onto the shared donut-chart controller that Budgets and Goals already use — so the empty track now pulls --budget-unused-fill (gray-200 light / gray-500 dark) and themes itself. Turns out a fitting token does exist, it's just the ring-track one we use elsewhere. Nice side effect: the loan ring now matches the other two.

Pushed in 7e0d9c1.

Loan repayment ring — light & dark

Nice, I like the light theme render! Dark theme feels slightly too light to me but overall it's cool, I like it.

Did a design-system pass on the Overview tab — compared it against properties/tabs/_overview, vehicles/tabs/_overview, and accounts/_summary_card.

First, two things that look like they could be off but aren't — they match the peers exactly, so I'd leave them as-is:

  • Edit at the bottom (ghost DS::Link, modal frame) and no icons on the stat cards are byte-for-byte what property & vehicle do. (FWIW the header ⋯ menu already has an Edit too, but that double-up exists on every account type, not just loans.)
  • It's also correctly on summary_card, semantic color tokens, and the privacy blur. 👍

Where I think there's room — ran a quick experiment, three versions:

Current / A / B

A — drop the cents (precision: 0). Quick win, ties to my earlier note: these figures are always whole-dollar, and it matches how the dashboard balance sheet and budgets already format summary numbers.

B — composition bars. The breakdown cards hold proportional data but show it as a flat list, so the reader has to do the math. A small stacked bar tells the story instantly: interest is ~53% of total cost and ~78% of the current monthly payment. Reuses existing tokens (--color-blue-500 / --color-warning / --color-violet-500), no new DS, and it folds months elapsed/remaining into 36 / 360.

One tiny DS nit while you're in there: the Initial leverage value is text-2xl font-semibold — louder than every other card's text-xl font-medium. Worth normalizing.

I really love the result of proposal B! The stacked bar is a great idea !!

@gariasf gariasf left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Took it for a spin again on a clean DB and drove the Overview tab in the browser — the feature is nice, but there are blockers around the loan-balance sign convention.

1. Negative-balance handling is broken. Loan accounts carry a negative balance/valuation (the account header itself shows "Remaining principal balance −$11,100.00"). The new analytics derive from account.first_valuation_amount / account.balance and never take .abs, so the Overview renders:

  • Total cost: −$12,900.00, Monthly payment: −$215.00 (negative)
  • Repayment progress: "100.0% repaid" + a full ring, while the loan still owes −$11,100. balance_paid_ratio mixes the positive initial_balance column with the negative account.balance → clamps to 1.0.
  • Initial leverage: "Unknown"initial_leverage_ratio = original_balance(−11,100) / down_payment(5,000) = −2.22 → no band matches.

monthly_payment, total_cost, total_interest, balance_paid_ratio, and initial_leverage_ratio all need to normalize the balance sign.

2. Still conflicting with main — needs a rebase.

Medium — dual source of truth for the starting balance: the "Original principal" card + balance_paid_ratio read the initial_balance column ($25,000), while amortization + total_cost principal use original_balance = first_valuation_amount (−$11,100). They disagree on the same screen — pick one canonical source.

Nits:

  • monthly_payment builds BigDecimal(interest_rate) / BigDecimal(balance_amount) without .to_s (a Float would raise); generate_amortization_schedule correctly uses .to_s — make it consistent.
  • LEVERAGE_BANDS ranges overlap at boundaries (0..4, 4..8, 8..) — use exclusive ranges.
  • remove_obsolete_recurring_transactions_unique_index migration + demo/generator.rb changes are unrelated to the loans feature (scope creep).

For coverage: loan_test.rb passes locally, but none of the model tests use a realistic negative-balance loan — that's exactly where the bugs live. A test with a negative-balance loan would catch all of the above.

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Comment thread app/models/loan.rb
months = term_months
date = start_date

months.times do |i|

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Unbounded term_months allows DoS via amortization schedule generation

generate_amortization_schedule loops term_months times with no upper bound.

Add a maximum validation for term_months to prevent DoS from huge schedules.

AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.

<file name="app/models/loan.rb">
<violation number="1" location="app/models/loan.rb:271">
<priority>P1</priority>
<title>Unbounded term_months allows DoS via amortization schedule generation</title>
<evidence>The generate_amortization_schedule method performs `months.times do |i|` where `months` is `term_months`, but `term_months` only has a validation for `greater_than: 0` with no upper bound. An authenticated attacker can create a loan with an extremely large term_months value (e.g., 999999999) and viewing the loan overview will trigger a long-running loop that consumes CPU and memory, potentially causing a denial of service.</evidence>
<recommendation>Add a maximum value validation for `term_months` in the Loan model, such as `less_than_or_equal_to: 600` (50 years), to cap the amortization schedule. Additionally, add a guard clause in `generate_amortization_schedule` to short-circuit or raise when `term_months` exceeds the permitted maximum.</recommendation>
</violation>
</file>

@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label Jun 14, 2026
@sure-admin sure-admin modified the milestones: v0.7.2, v0.7.3 Jul 2, 2026

Copy link
Copy Markdown
Collaborator

DS Drift Patrol — resolved

7 of 7 previously-flagged issue(s) addressed. No new findings on this scan.

(The Overview tab's progress visualization was rewritten to use the shared donut-chart Stimulus controller, removing the raw SVG hex strokes, text-gray-200 dark:text-gray-700, and stroke-amber-500/text-amber-500 literals flagged in earlier scans.)


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

DS Drift Patrol — 1 finding

Automated scan of added + lines only. Pre-existing/previously-flagged violations in this PR are out of scope (see the prior DS Drift history on this PR — 7/7 resolved as of 2026-07-07; this is a new, separate finding).

Rule 5 — i18n bypass via hardcoded default: string

app/views/loans/_form.html.erb:8

Loan::SUBTYPES.map { |k, v| [I18n.t("loans.form.subtypes.#{k}.long", default: v[:long]), k] },

config/locales/views/loans/en.yml (also added in this PR) already defines loans.form.subtypes.<mortgage|student|auto|other>.long for every subtype, so the default: v[:long] fallback is redundant. Per CLAUDE.md, missing translations should raise in development — a hardcoded default: silently masks a missing/renamed key instead. Suggested fix: drop the default: argument now that the locale entries exist — I18n.t("loans.form.subtypes.#{k}.long").


Generated by DS Drift Patrol


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Flagging that @gariasf's June 1 review found blocking sign-convention bugs (negative loan balances producing negative total cost/monthly payment, and a bogus 100% "repaid" state) — these look unaddressed, since the branch hasn't had a substantive commit since May 30 (only a stale merge from main on June 14). Given it's now ~6 weeks stale relative to main, worth confirming whether this is still being actively worked or should be deprioritized.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:flagged PR flagged for review by security analysis.

Projects

Development

Successfully merging this pull request may close these issues.

7 participants