Skip to content

Add comprehensive bond accounts and API integration#1330

Draft
UberDudePL wants to merge 75 commits intowe-promise:mainfrom
UberDudePL:feature/bond
Draft

Add comprehensive bond accounts and API integration#1330
UberDudePL wants to merge 75 commits intowe-promise:mainfrom
UberDudePL:feature/bond

Conversation

@UberDudePL
Copy link
Copy Markdown

@UberDudePL UberDudePL commented Apr 1, 2026

This pull request introduces comprehensive support for bond accounts, including new UI components, controller logic, automation for bond lot settlement, and infrastructure for handling inflation-linked bonds via GUS CPI data. It also adds several environment and settings options for configuring GUS inflation import. The most important changes are grouped below:

Bond Account and Bond Lot Support:

  • Introduced BondLotsController and BondsController to handle CRUD operations for bond lots and accounts, including creation, editing, updating, and deletion, with appropriate permission checks and transaction management. (app/controllers/bond_lots_controller.rb [1] app/controllers/bonds_controller.rb [2]
  • Updated account and dashboard UI to support bond-specific features, such as new activity menu item for bonds, bond-specific chart views, and new dashboard sections for bond summaries. (app/components/UI/account/activity_feed.html.erb [1] app/components/UI/account/chart.html.erb [2] [3] app/components/UI/account/chart.rb [4] app/components/UI/account_page.rb [5] [6] app/controllers/pages_controller.rb [7] [8]
  • Added logic to automatically settle matured bond lots and update their status, both on account view and via a scheduled job. (app/controllers/accounts_controller.rb [1] app/jobs/settle_matured_bond_lots_job.rb [2]

Inflation-Linked Bond and GUS CPI Integration:

  • Added support for importing GUS inflation rates, including a background job, settings controls, and environment variables to enable/disable and configure the feature. (app/jobs/import_gus_inflation_rates_job.rb [1] app/controllers/settings/hostings_controller.rb [2] [3] [4] .env.example [5] .env.local.example [6] .env.test.example [7]
  • Added frontend Stimulus controllers for bond account and bond lot forms to dynamically show/hide fields based on subtype and tax wrapper selections, supporting inflation-linked bonds and manual CPI entry. (app/javascript/controllers/bond_account_form_controller.js [1] app/javascript/controllers/bond_lot_form_controller.js [2]

User Experience and Automation:

  • Implemented a dashboard notice prompting users to review bond lots that require rate updates, improving user awareness of pending actions. (app/controllers/pages_controller.rb app/controllers/pages_controller.rbR355-R365)
  • Enhanced account model to display bond subtype labels appropriately, including wrapper labels for bonds. (app/models/account.rb app/models/account.rbR341-R349)

These changes significantly expand the platform's support for bonds, especially inflation-linked government bonds, and automate important workflows for users and admins.

Validation
I ran focused regression tests for:

  • Bond and BondLot models
  • Bond, BondLot, Accounts, Pages, Settings controllers
  • CPI import jobs and settlement jobs

All executed tests are green locally.

This PR is intentionally English-only for Bond features.
Polish localization have been merged in a separate PR #1356 - any changes to pl.yml files now are related to bonds and should be part of this PR

Summary by CodeRabbit

  • New Features

    • Bond support: Bond accounts with positions/closed/activity tabs, full bond‑lot create/read/update/delete flows, dashboard bond summary, automatic settlement of matured lots, and CPI/inflation import with provider support and manual import UI.
  • Client-side

    • Enhanced form behaviors: dynamic bond and bond‑lot form syncing, inflation controls, and auto-fetch coordination.
  • Tests

    • Extensive model, controller, job, and integration tests for bonds, bond lots, providers, imports, and settlement.
  • Documentation

    • Docker self‑hosting guide: optional inflation import notes.
  • Chores

    • Updated example envs/compose/CI, new DB migrations, and scheduled jobs.

Copilot AI review requested due to automatic review settings April 1, 2026 00:09
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b76f1478-2b17-4950-b324-9fb67c91d84d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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 Bond account support with Bond and BondLot models, CPI/inflation provider integrations and importers, new provider adapters, jobs/schedules, controllers/routes, Stimulus form controllers, view components/partials, DB migrations/schema updates, balance/ledger integrations, settings/env vars, presenters, and extensive tests/locales/docs.

Changes

Cohort / File(s) Summary
Env / Compose / CI
/.env.example, .env.local.example, .env.test.example, compose.example.yml, compose.example.ai.yml, .github/workflows/ci.yml
Added inflation-import env vars (INFLATION_IMPORT_ENABLED, GUS_SDP_API_KEY) and optional CPI provider overrides; CI env creds updated.
Routes & Controllers
config/routes.rb, app/controllers/bonds_controller.rb, app/controllers/bond_lots_controller.rb, app/controllers/pages_controller.rb, app/controllers/settings/hostings_controller.rb
New bond/bond_lot routes and controllers; dashboard aggregates bond metrics and shows rate-review notice; hosting settings gain import action with conditional persistence and import scheduling.
Domain: Bond & BondLot
app/models/bond.rb, app/models/bond_lot.rb, app/models/purchase_holding_presenter.rb
New Bond and BondLot domain models with lifecycle, valuation, settlement, reinvestment and extensive public APIs.
Inflation: Models & Providers
app/models/gus_inflation_rate.rb, app/models/inflation_rate.rb, app/models/inflation_rate_importer.rb, app/models/provider/gus_sdp.rb, app/models/provider/us_bls_cpi.rb, app/models/provider/es_ine_cpi.rb, app/models/bond/inflation_provider.rb
New inflation-rate persistence, import orchestration, and provider adapters with lookup/import logic and provider-selection helper.
Settings & Hostings UI
app/models/setting.rb, app/components/settings/hostings/*, app/views/settings/hostings/show.html.erb
Persisted settings for inflation import and GUS API key; hostings UI/components for global import control, provider config, import metadata and manual import action.
Balance / Ledger Integration
app/models/balance/base_calculator.rb, app/models/balance/sync_cache.rb, app/models/account.rb
Holdings/flows account calculations updated to account for bond lots; sync cache duplication tweak; account classification and wrapper helper added.
Jobs & Schedule
app/jobs/import_inflation_rates_job.rb, app/jobs/settle_matured_bond_lots_job.rb, config/schedule.yml
Added import and settle jobs, cron entries, and import metadata persistence/error handling.
Views / Components / UI
app/views/bonds/**, app/views/bond_lots/**, app/views/pages/dashboard/_bond_summary.html.erb, app/components/UI/*
Added bond account forms, bond-lot CRUD views/partials, dashboard bond summary, dashboard row component, and settings UI components.
Stimulus / Frontend
app/javascript/controllers/bond_account_form_controller.js, app/javascript/controllers/bond_lot_form_controller.js, app/javascript/controllers/bond_lot_inflation_controller.js
New Stimulus controllers for bond account form behavior, bond-lot product presets syncing, and inflation/auto-fetch UI state.
Migrations & Schema
db/migrate/*, db/schema.rb
Migrations to create bonds, bond_lots, gus_inflation_rates, inflation_rates, add indexes/constraints, change FK cascade, add holdings snapshot columns, and month-range constraints; schema updated.
Presenters / Components
app/models/purchase_holding_presenter.rb, app/components/UI/dashboard/bond_summary_row.*
Presentation helper and ViewComponent for dashboard bond rows and holding displays.
Tests / Fixtures
test/**/*, test/fixtures/*
Extensive unit/integration tests for models, providers, controllers, jobs, presenters; fixtures for bonds and bond_lots.
Locales / Docs / Misc
config/locales/**, docs/*, .github/copilot-instructions.md, .gitignore, config/database.yml
Added EN/PL locales for bonds and settings, hosting docs for GUS import, copilot guidance, minor .gitignore and DB config tweaks.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client
    participant BondLotsCtrl as BondLotsController
    participant BondLotModel as BondLot
    participant Entry as Entry/Transaction
    participant Account
    participant Sync as SyncJob

    Client->>BondLotsCtrl: POST /bond_lots (params)
    BondLotsCtrl->>BondLotModel: build & validate
    BondLotsCtrl->>BondLotModel: save_with_purchase_entry!
    BondLotModel->>Entry: create purchase Entry & Transaction (embed bond_lot_id)
    BondLotModel-->>BondLotsCtrl: persisted
    BondLotsCtrl->>Account: schedule sync_later(window_start_date)
    Account->>Sync: enqueue
    BondLotsCtrl-->>Client: redirect with notice
Loading
sequenceDiagram
    actor Scheduler
    participant ImportJob as ImportInflationRatesJob
    participant Importer as InflationRateImporter
    participant Gus as GusInflationRate
    participant Intl as InflationRate
    participant Setting

    Scheduler->>ImportJob: perform(start_year,end_year,providers)
    ImportJob->>Importer: new(...) / import_all
    Importer->>Gus: import_range!(...)
    Importer->>Intl: import_year! (per-year, per-provider)
    Gus-->>Importer: counts
    Intl-->>Importer: counts
    Importer-->>ImportJob: results hash
    ImportJob->>Setting: update last_import_at/count/range/details
Loading
sequenceDiagram
    actor Scheduler
    participant SettleJob as SettleMaturedBondLotsJob
    participant BondLot as BondLot
    participant Provider as InflationProvider
    participant InflationRateModel as InflationRate
    participant Entry

    Scheduler->>SettleJob: perform(on: Date.current)
    SettleJob->>BondLot: find auto-close matured lots
    loop per lot
      BondLot->>Provider: request rates (may trigger import)
      Provider->>InflationRateModel: for_date/import as needed
      InflationRateModel-->>Provider: record
      BondLot->>BondLot: settle_if_matured! (compute gain/tax/net)
      BondLot->>Entry: create settlement entry/transaction
      BondLot->>BondLot: mark closed, persist settlement_amount
    end
    SettleJob-->>Scheduler: complete or raise first error
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • jjmata

"🐇
I hopped through rails and stitched a lot,
CPI winds and coupons tied in a knot.
Lots settle, imports hum at dawn,
Syncs enqueue and docs are drawn.
Hooray — a rabbit's tiny bond-song!"

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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92a8fad207

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/models/bond_lot.rb Outdated
Comment thread app/jobs/import_gus_inflation_rates_job.rb Outdated
Comment thread app/controllers/bond_lots_controller.rb Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class Bond accounts and BondLot purchases with automated maturity settlement and optional inflation-linked (EOD/ROD) support backed by GUS SDP CPI imports, plus UI/dashboard surfaces and self-hosting settings to configure the integration.

Changes:

  • Introduces Bond / BondLot models, controllers, views, routes, and fixtures/tests to support bond accounts and purchase lots (including settlement automation and replacement lots).
  • Adds GUS SDP provider + GusInflationRate model and scheduled/manual import job, with self-hosting settings UI and env-based configuration.
  • Extends account UI (tabs, chart tooltip, dashboard section) and balance calculations to account for bond holdings and cash.

Reviewed changes

Copilot reviewed 74 out of 74 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
test/models/gus_inflation_rate_test.rb Adds unit tests for CPI lag lookup and import error handling.
test/models/bond_test.rb Adds tests for bond defaults and derived maturity date/original balance.
test/models/bond_lot_test.rb Adds extensive tests for lot valuation, inflation logic, and settlement automation.
test/jobs/settle_matured_bond_lots_job_test.rb Tests scheduled settlement job behavior.
test/jobs/import_gus_inflation_rates_job_test.rb Tests CPI import job gating + status persistence.
test/fixtures/bonds.yml Adds bond fixture for tests.
test/fixtures/bond_lots.yml Adds bond lot fixtures for tests.
test/fixtures/accounts.yml Adds bond account fixture for tests.
test/controllers/transactions/bulk_deletions_controller_test.rb Ensures bulk deletion removes linked bond lots via entry association.
test/controllers/settings/hostings_controller_test.rb Tests manual CPI import enqueueing and disabled gating.
test/controllers/pages_controller_test.rb Verifies dashboard bond summary + notice behavior.
test/controllers/bonds_controller_test.rb Tests bond account CRUD via AccountableResource.
test/controllers/bond_lots_controller_test.rb Tests bond lot CRUD and entry linkage.
test/controllers/accounts_controller_test.rb Tests bond tabs and auto-settlement on account open.
docs/hosting/docker.md Documents optional GUS CPI env vars and settings location.
db/schema.rb Adds bonds/bond_lots/gus_inflation_rates schema and FKs.
db/migrate/20260331211000_add_requires_rate_review_to_bond_lots.rb Adds requires_rate_review flag + index.
db/migrate/20260331210000_add_tax_wrapper_and_auto_buy_to_bonds.rb Adds bond tax wrapper + auto-buy setting.
db/migrate/20260331193000_add_maturity_settlement_fields_to_bond_lots.rb Adds settlement/tax fields and indexes.
db/migrate/20260331170000_add_gus_inflation_rates_and_auto_fetch_to_bond_lots.rb Adds CPI table + per-lot auto-fetch flag.
db/migrate/20260331153000_reconfigure_bond_products_for_eod_rod.rb Adds inflation-linked fields and normalizes subtype/defaults.
db/migrate/20260331133000_add_purchase_fields_to_bond_lots.rb Adds subtype/rate fields and entry reference.
db/migrate/20260331120000_create_bond_lots.rb Creates bond lots table + indexes.
db/migrate/20260330233052_create_bonds.rb Creates bonds table.
config/schedule.yml Schedules CPI import and bond lot settlement jobs.
config/routes.rb Adds bonds/bond_lots resources and settings import endpoint.
config/locales/views/settings/hostings/en.yml Adds hostings copy/alerts for GUS settings and import.
config/locales/views/pages/en.yml Adds dashboard notice string and bond summary strings.
config/locales/views/bonds/en.yml Adds bond/bond lot UI text, labels, and activity messages.
config/locales/views/accounts/en.yml Adds “bond” account type label.
compose.example.yml Adds env wiring for GUS CPI imports.
compose.example.ai.yml Adds env wiring for GUS CPI imports (AI compose).
app/views/settings/hostings/show.html.erb Renders new GUS SDP settings panel.
app/views/settings/hostings/_gus_sdp_settings.html.erb Adds UI for enabling imports, setting API key, and triggering manual import.
app/views/pages/dashboard/_bond_summary.html.erb Adds dashboard “Bonds” section summary + top lots table.
app/views/bonds/tabs/_positions.html.erb Adds bond “positions” tab UI listing open lots and cash.
app/views/bonds/tabs/_closed.html.erb Adds bond “closed lots” tab UI.
app/views/bonds/new.html.erb Adds bond account creation modal.
app/views/bonds/edit.html.erb Adds bond account edit modal.
app/views/bonds/_purchase_holding.html.erb Adds open lot row partial for positions tab.
app/views/bonds/_form.html.erb Adds bond account fields (initial balance, wrapper, auto-buy).
app/views/bonds/_closed_purchase_holding.html.erb Adds closed lot row partial with settlement/interest/tax meta.
app/views/bonds/_cash_holding.html.erb Adds bond cash position row partial.
app/views/bond_lots/show.html.erb Adds drawer view with edit form (open) and history section.
app/views/bond_lots/new.html.erb Adds bond lot creation modal.
app/views/bond_lots/edit.html.erb Adds bond lot edit modal.
app/views/bond_lots/_form.html.erb Adds bond lot form with inflation-linked conditional fields + tax/settlement controls.
app/views/accounts/show/_activity.html.erb Adjusts “New activity” link to create bond lots for bond accounts.
app/views/accounts/new.html.erb Adds bond as an account type option.
app/models/setting.rb Adds GUS settings fields + env override helper.
app/models/provider/gus_sdp.rb Implements GUS SDP CPI YoY fetch client via Faraday.
app/models/gus_inflation_rate.rb Adds CPI record model with import + lookup logic.
app/models/entry.rb Adds has_one :bond_lot, dependent: :destroy for cascading deletes.
app/models/concerns/accountable.rb Adds Bond to Accountable types list.
app/models/bond.rb Adds bond account domain model and wrapper behavior.
app/models/bond_lot.rb Adds lot valuation, inflation logic, settlement automation, and backfill enqueueing.
app/models/balance/base_calculator.rb Extends holdings value derivation for bond lots.
app/models/account.rb Adds bond wrapper label behavior and balance_type mapping.
app/jobs/settle_matured_bond_lots_job.rb Adds scheduled bond lot settlement job.
app/jobs/import_gus_inflation_rates_job.rb Adds scheduled/manual CPI import job + status tracking.
app/javascript/controllers/bond_lot_form_controller.js Adds Stimulus controller to toggle lot form fields by subtype + global CPI toggle.
app/javascript/controllers/bond_account_form_controller.js Adds Stimulus controller to show/hide wrapper-only fields.
app/controllers/settings/hostings_controller.rb Adds GUS settings persistence + manual import endpoint.
app/controllers/pages_controller.rb Adds dashboard bond section locals + session-scoped rate review notice.
app/controllers/bonds_controller.rb Adds Bond controller via AccountableResource.
app/controllers/bond_lots_controller.rb Adds bond lot CRUD controller with entry linkage.
app/controllers/accounts_controller.rb Auto-settles matured lots when viewing bond accounts.
app/components/UI/account/chart.rb Treats Bond like investment accounts for chart title behavior.
app/components/UI/account/chart.html.erb Enables holdings/cash view and tooltip for bond accounts.
app/components/UI/account/activity_feed.html.erb Adds “New activity” menu item for bond accounts.
app/components/UI/account_page.rb Adds bond-specific tabs and tab fallback behavior.
.env.test.example Documents GUS env vars for tests.
.env.local.example Documents GUS env vars for local dev.
.env.example Documents GUS env vars for general setup.

Comment thread app/controllers/bond_lots_controller.rb Outdated
Comment thread app/controllers/bond_lots_controller.rb Outdated
Comment thread app/controllers/accounts_controller.rb Outdated
Comment thread app/models/bond_lot.rb Outdated
Comment thread app/models/bond_lot.rb Outdated
Comment thread app/views/pages/dashboard/_bond_summary.html.erb Outdated
Comment thread app/views/settings/hostings/_gus_sdp_settings.html.erb Outdated
Comment thread app/jobs/import_gus_inflation_rates_job.rb Outdated
Comment thread app/controllers/settings/hostings_controller.rb Outdated
Comment thread app/views/bond_lots/_form.html.erb Outdated
Copy link
Copy Markdown

@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: 11

🧹 Nitpick comments (28)
.env.example (1)

58-62: Consider relocating this block for better organization.

The GUS SDP variables are inserted between the "Provider selection for exchange rates" section (lines 53-57) and the "Alternative: Use Yahoo Finance" section (lines 64-66), which disrupts the logical grouping of data provider settings. Consider moving this block either before the provider selection section or after the Yahoo Finance alternative.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 58 - 62, The GUS SDP env vars
(GUS_INFLATION_IMPORT_ENABLED and GUS_SDP_API_KEY) are placed between the
"Provider selection for exchange rates" and the "Alternative: Use Yahoo Finance"
sections, breaking the provider settings group; relocate the two lines so they
sit either before the provider selection block or immediately after the Yahoo
Finance alternative block to keep all data-provider related variables grouped
together and update surrounding comments if needed to preserve readability.
test/controllers/pages_controller_test.rb (1)

35-35: Consider using i18n lookup for flash message assertion.

The hard-coded English string "1 bond lot(s) need updated issue rates: US T-Bill." is fragile—if the translation changes, this test will fail even though the functionality is correct. Consider using the i18n helper to build the expected message:

expected = I18n.t("pages.dashboard.bond_rate_review_notice", count: 1, accounts: "US T-Bill")
assert_equal expected, flash[:notice]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/controllers/pages_controller_test.rb` at line 35, Replace the hard-coded
assertion string in the test's assert_equal against flash[:notice] with an i18n
lookup; build the expected value via
I18n.t("pages.dashboard.bond_rate_review_notice", count: 1, accounts: "US
T-Bill") and assert_equal that expected value to flash[:notice] instead of the
literal "1 bond lot(s) need updated issue rates: US T-Bill."; update the
assert_equal call that currently references flash[:notice] to use the expected
variable derived from I18n.t.
app/models/balance/base_calculator.rb (1)

19-26: Use account.bond? predicate for consistency.

The chart.html.erb component uses account.bond? to check the account type. Using the predicate here instead of the string comparison would be more consistent with the codebase style and more resilient to refactoring:

-      `@holdings_value_for_date`[date] ||= if account.accountable_type == "Bond"
+      `@holdings_value_for_date`[date] ||= if account.bond?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/balance/base_calculator.rb` around lines 19 - 26, Replace the
string type check in the memoized holdings calculation with the model predicate:
in the block that assigns `@holdings_value_for_date`[date] (the conditional
currently using account.accountable_type == "Bond"), use account.bond? instead
and keep the existing branches (use account.bond.bond_lots... for bonds, else
use sync_cache.get_holdings). This keeps behavior the same but uses the
predicate used elsewhere (e.g., chart.html.erb) and is resilient to refactors.
db/migrate/20260330233052_create_bonds.rb (1)

1-15: Consider adding indexes on frequently queried columns.

The migration creates the base bonds table correctly with appropriate decimal precisions for financial data. However, if subtype or rate_type will be used for filtering/grouping bonds in queries, consider adding indexes:

       t.jsonb :locked_attributes, default: {}, null: false
       t.timestamps
     end
+
+    add_index :bonds, :subtype
+    add_index :bonds, :rate_type
   end

This can be deferred if query patterns are not yet established.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/migrate/20260330233052_create_bonds.rb` around lines 1 - 15, Add indexes
on frequently filtered columns in the CreateBonds migration: inside the change
method where create_table :bonds is declared (or immediately after it) add
indexes for the subtype and rate_type columns (e.g., add_index :bonds, :subtype
and add_index :bonds, :rate_type) so queries that filter or group by those
fields are fast; include names or options (using: :btree) if you need explicit
index naming or a specific index type, or use t.index within the create_table
block to define them inline.
test/jobs/settle_matured_bond_lots_job_test.rb (1)

4-24: Consider adding one boundary case for the job scope.

Please add a companion assertion that a lot with auto_close_on_maturity: false (or not yet matured) remains open after perform_now, so regressions in the query filter are caught early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/jobs/settle_matured_bond_lots_job_test.rb` around lines 4 - 24, Add a
boundary case to the test by creating a second BondLot via BondLot.create! with
the same matured criteria but auto_close_on_maturity: false (or create a lot
with purchased_on such that it's not yet matured), call
SettleMaturedBondLotsJob.perform_now as before, then assert that the second
lot.reload.closed_on is still nil to verify non-auto-close lots (or
not-yet-matured lots) are not closed by SettleMaturedBondLotsJob.
app/jobs/settle_matured_bond_lots_job.rb (1)

5-8: Add/confirm an index for the settlement query pattern.

This scheduled path should be backed by an index matching open + auto_close_on_maturity + maturity_date filtering to avoid full scans on large datasets.

As per coding guidelines, “Optimize database queries with proper indexes to prevent N+1 queries using includes/joins.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/jobs/settle_matured_bond_lots_job.rb` around lines 5 - 8, The scheduled
query over BondLot.open.where(auto_close_on_maturity: true).where("maturity_date
<= ?", on) needs a composite index to avoid full table scans: add a migration
that creates an index on the column(s) used by the BondLot.open scope (e.g. the
state/status column), plus auto_close_on_maturity and maturity_date (e.g.
[state, auto_close_on_maturity, maturity_date] or [status,
auto_close_on_maturity, maturity_date] depending on your scope implementation),
update schema, and run migrations; ensure the index name is descriptive and
include a test or note in the model/README that the index supports the
BondLot.open + auto_close_on_maturity + maturity_date query pattern.
test/controllers/settings/hostings_controller_test.rb (1)

251-277: Add an explicit non-admin authorization test for this new endpoint.

Given this action can trigger background imports, please mirror the existing admin-gate tests (clear_cache, disconnect_external_assistant) for import_gus_inflation_rates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/controllers/settings/hostings_controller_test.rb` around lines 251 -
277, Add a new authorization test that mirrors the existing admin-gate tests
(e.g. clear_cache, disconnect_external_assistant): create a test like "non-admin
cannot enqueue gus inflation import" that uses with_self_hosting but does not
sign in as an admin, then POST to
import_gus_inflation_rates_settings_hosting_url with the same params and assert
that no ImportGusInflationRatesJob is enqueued (assert_no_enqueued_jobs only:
ImportGusInflationRatesJob) and that the request is rejected per your app policy
(match the existing admin-gate pattern: assert_redirected_to and flash alert or
assert_response :forbidden depending on how
clear_cache/disconnect_external_assistant assert authorization).
db/migrate/20260331170000_add_gus_inflation_rates_and_auto_fetch_to_bond_lots.rb (1)

14-14: Consider default value for existing bond lots.

The auto_fetch_inflation column defaults to true, meaning all existing bond lots will automatically opt-in to GUS inflation fetching after migration. If there are existing bond lots that were manually managed, this could cause unexpected behavior changes.

If this is intentional (making the new feature opt-out rather than opt-in), this is fine. Otherwise, consider defaulting to false to preserve existing behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@db/migrate/20260331170000_add_gus_inflation_rates_and_auto_fetch_to_bond_lots.rb`
at line 14, The migration currently adds auto_fetch_inflation to :bond_lots with
default: true which opts all existing records into automatic GUS inflation
fetching; change the migration to use default: false (add_column :bond_lots,
:auto_fetch_inflation, :boolean, null: false, default: false) so existing bond
lots preserve prior behavior, and if you need selective opt-in, add an explicit
data backfill/update step after the column add to set true only for records that
should auto-fetch. Ensure you update any tests or seed logic that assume the new
default.
app/jobs/import_gus_inflation_rates_job.rb (1)

5-5: Consider using gus_inflation_import_enabled_effective for consistency.

The Setting model defines gus_inflation_import_enabled_effective (lines 69-73 in setting.rb) which properly handles the ENV override. Using the base field here could cause inconsistent behavior if the ENV var is set differently than the persisted setting.

Proposed fix
  def perform(opts = {})
-   return unless Setting.gus_inflation_import_enabled
+   return unless Setting.gus_inflation_import_enabled_effective
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/jobs/import_gus_inflation_rates_job.rb` at line 5, Replace the direct
Setting.gus_inflation_import_enabled check in ImportGusInflationRatesJob with
the effective accessor Setting.gus_inflation_import_enabled_effective so the job
honors the ENV override; update the guard (the line currently using
gus_inflation_import_enabled) to call gus_inflation_import_enabled_effective
instead.
app/views/bonds/_purchase_holding.html.erb (1)

35-35: Nested ternary reduces readability.

The triple-nested ternary on line 35 is difficult to parse at a glance.

Proposed refactor using explicit conditionals
-       <% rate_text = lot.requires_rate_review? ? t(".update_needed") : current_rate.present? ? number_to_percentage(current_rate, precision: 3) : t(".unknown") %>
+       <% rate_text = if lot.requires_rate_review?
+                        t(".update_needed")
+                      elsif current_rate.present?
+                        number_to_percentage(current_rate, precision: 3)
+                      else
+                        t(".unknown")
+                      end %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/bonds/_purchase_holding.html.erb` at line 35, Replace the nested
ternary that assigns rate_text with a clear conditional block: first check
lot.requires_rate_review? and set rate_text to t(".update_needed"); elsif check
current_rate.present? and set rate_text to number_to_percentage(current_rate,
precision: 3); else set rate_text to t(".unknown"). Update the expression around
the variable rate_text (referencing lot.requires_rate_review?, current_rate,
number_to_percentage, t(".update_needed"), and t(".unknown")) to use
if/elsif/else for readability.
app/views/bond_lots/_form.html.erb (2)

9-9: Consider using gus_inflation_import_enabled_effective for consistency.

The form uses Setting.gus_inflation_import_enabled in multiple places (lines 9, 100, 109, 123), but the model provides gus_inflation_import_enabled_effective which properly handles ENV override. If the ENV var is set differently than the persisted value, this could cause inconsistent behavior.

Proposed fix (example for line 9)
-                        bond_lot_form_global_auto_fetch_enabled_value: Setting.gus_inflation_import_enabled
+                        bond_lot_form_global_auto_fetch_enabled_value: Setting.gus_inflation_import_enabled_effective

Apply similar changes to lines 100, 109, and 123.

Also applies to: 100-100, 109-109, 123-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/bond_lots/_form.html.erb` at line 9, Replace direct calls to
Setting.gus_inflation_import_enabled with the ENV-aware accessor
Setting.gus_inflation_import_enabled_effective in the bond_lot form partial;
specifically update the occurrences referenced (the
bond_lot_form_global_auto_fetch_enabled_value and the other usages at lines
noted) so the form uses gus_inflation_import_enabled_effective to reflect ENV
overrides and avoid inconsistent behavior between persisted and env-provided
values.

74-84: Inconsistent indentation in rate_type/coupon_frequency section.

The <div> wrapper on line 74 and closing on line 84 use different indentation than surrounding elements (extra 8 spaces).

Proposed fix
-            <div data-bond-lot-form-target="otherFields">
+      <div data-bond-lot-form-target="otherFields">
        <%= form.select :rate_type,
          Bond::RATE_TYPES.map { |value| [ value.titleize, value ] },
          { label: t("bond_lots.form.rate_type"), required: true },
          { data: { bond_lot_form_target: "otherRequiredInput" } } %>

        <%= form.select :coupon_frequency,
          Bond::COUPON_FREQUENCIES.map { |value| [ value.humanize, value ] },
          { label: t("bond_lots.form.coupon_frequency"), required: true },
          { data: { bond_lot_form_target: "otherRequiredInput" } } %>
-            </div>
+      </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/bond_lots/_form.html.erb` around lines 74 - 84, The indentation for
the div wrapping the rate_type and coupon_frequency selects is inconsistent with
surrounding markup; adjust the leading spaces so the <div
data-bond-lot-form-target="otherFields"> and its closing tag align with
neighbouring elements, and ensure the nested lines for form.select calls (for
:rate_type using Bond::RATE_TYPES and for :coupon_frequency using
Bond::COUPON_FREQUENCIES) are indented one level deeper to match the file's
indentation style.
app/views/bond_lots/show.html.erb (2)

90-90: Use design token instead of hardcoded color.

text-red-500 should be replaced with the functional token text-destructive per project design system conventions.

As per coding guidelines: "Use Tailwind design tokens (e.g., text-primary, bg-container, border-secondary) instead of raw color names."

Proposed fix
-                 class: "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border border-secondary",
+                 class: "rounded-lg px-3 py-2 text-destructive text-sm font-medium border border-secondary",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/bond_lots/show.html.erb` at line 90, In the view template change
the hardcoded Tailwind color class on the element that currently has the class
string "rounded-lg px-3 py-2 text-red-500 text-sm font-medium border
border-secondary" by replacing text-red-500 with the design token
text-destructive so the class becomes "rounded-lg px-3 py-2 text-destructive
text-sm font-medium border border-secondary"; ensure no other hardcoded color
variants remain in the same element.

46-47: Use format_money helper for consistency.

Lines 46-47 use Money.new(...).format directly, while line 20-21 use format_money(). The helper should be used consistently throughout for uniform formatting behavior.

Proposed fix
-                 <p class="text-primary"><%= t(".history_balance", opening: Money.new(event[:opening_balance], `@account.currency`).format, closing: Money.new(event[:closing_balance], `@account.currency`).format) %></p>
-                 <p class="text-secondary"><%= t(".history_interest", interest: Money.new(event[:interest_earned], `@account.currency`).format, rate: number_to_percentage(event[:annual_rate_percent], precision: 3)) %></p>
+                 <p class="text-primary"><%= t(".history_balance", opening: format_money(Money.new(event[:opening_balance], `@account.currency`)), closing: format_money(Money.new(event[:closing_balance], `@account.currency`))) %></p>
+                 <p class="text-secondary"><%= t(".history_interest", interest: format_money(Money.new(event[:interest_earned], `@account.currency`)), rate: number_to_percentage(event[:annual_rate_percent], precision: 3)) %></p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/bond_lots/show.html.erb` around lines 46 - 47, Replace the direct
Money.new(...).format calls with the existing format_money helper for
consistency: update the arguments passed to the translation helpers
t(".history_balance", ...) and t(".history_interest", ...) so opening: and
closing: use format_money(event[:opening_balance], `@account.currency`) and
format_money(event[:closing_balance], `@account.currency`) respectively, and
interest: uses format_money(event[:interest_earned], `@account.currency`); keep
number_to_percentage(event[:annual_rate_percent], precision: 3) unchanged.
app/controllers/bond_lots_controller.rb (2)

18-26: Inconsistent early return pattern in edit and show.

Lines 20 and 25 use nil unless require_account_permission! which is unconventional. The new action (line 6) and other actions use return unless, which is clearer and consistent with Rails conventions.

   def edit
     `@account` = `@bond_lot.account`
-    nil unless require_account_permission!(`@account`)
+    return unless require_account_permission!(`@account`)
   end

   def show
     `@account` = `@bond_lot.account`
-    nil unless require_account_permission!(`@account`)
+    return unless require_account_permission!(`@account`)
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/bond_lots_controller.rb` around lines 18 - 26, The edit and
show actions use the unconventional "nil unless
require_account_permission!(`@account`)" pattern; replace those lines with
explicit early returns matching the existing convention by calling
require_account_permission!(`@account`) and returning immediately on failure
(e.g., "return unless require_account_permission!(`@account`)") after assigning
`@account` = `@bond_lot.account` so edit and show mirror the new action and other
controllers and maintain consistent control flow.

34-38: Consider combining save and update into a single operation.

The @bond_lot.save! followed immediately by @bond_lot.update!(entry: ...) within the same transaction results in two database writes. This could be simplified.

♻️ Proposed refactor
     if `@bond_lot.valid`?
       ActiveRecord::Base.transaction do
+        entry = create_purchase_entry!(`@account`, `@bond_lot`)
+        `@bond_lot.entry` = entry
         `@bond_lot.save`!
-        `@bond_lot.update`!(entry: create_purchase_entry!(`@account`, `@bond_lot`))
       end

Note: This requires adjusting create_purchase_entry! to not reference bond_lot.id if it's not yet persisted, or using a different approach for the extra data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/bond_lots_controller.rb` around lines 34 - 38, The two writes
(save! then update!) on `@bond_lot` should be combined into a single persistence
operation: call create_purchase_entry! in a way that does not require
bond_lot.id (or change it to accept the in-memory `@bond_lot`), assign the
resulting entry to `@bond_lot.entry` (or build the association) and then call
`@bond_lot.save`! inside the ActiveRecord::Base.transaction so only one DB write
occurs; update the create_purchase_entry! signature/implementation to use the
bond_lot object (or its attributes) instead of relying on a persisted id if
necessary.
db/schema.rb (1)

1602-1603: Consider adding on_delete behavior for bond_lots.entry_id foreign key.

The foreign key from bond_lots to entries doesn't specify on_delete behavior. If an entry is deleted independently (entries have on_delete: :cascade from accounts), associated bond_lots would have dangling entry_id references.

Consider whether on_delete: :nullify (allow orphan lots) or on_delete: :cascade (delete lot with entry) better matches the domain model. Based on the controller logic where entries and lots are deleted together in a transaction, nullify may be appropriate as a safety net.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/schema.rb` around lines 1602 - 1603, The foreign key from bond_lots to
entries currently lacks on_delete behavior; update the constraint for
bond_lots.entry_id (the add_foreign_key call referencing "entries") to include
an appropriate on_delete option—preferably on_delete: :nullify (or :cascade if
you decide deletion semantics require removing lots with their entry)—so that
deleting an Entry won't leave dangling entry_id references; locate the
add_foreign_key "bond_lots", "entries" invocation and change it to include the
chosen on_delete behavior (and add a migration to alter the foreign key if this
is already in production schema).
app/views/pages/dashboard/_bond_summary.html.erb (2)

3-5: Potential N+1 query when collecting open lots.

The flat_map iterates over each account and calls account.bond.bond_lots.open, which may trigger separate queries per account. Consider eager loading or refactoring to a single query.

-<% open_lots = bond_accounts.flat_map { |account| account.bond.bond_lots.open.map { |lot| [ account, lot ] } } %>
+<% open_lots = bond_accounts.includes(bond: :bond_lots).flat_map { |account| account.bond.bond_lots.open.map { |lot| [ account, lot ] } } %>

Alternatively, ensure the controller preloads bond: { bond_lots: ... } when fetching @bond_accounts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/pages/dashboard/_bond_summary.html.erb` around lines 3 - 5,
open_lots is built with bond_accounts.flat_map calling
account.bond.bond_lots.open which may trigger an N+1; fix it by loading
bond_lots in a single query or eager-loading them when fetching bond_accounts.
Update the controller where `@bond_accounts` is set to include/ preload the
associations (bond and bond_lots with the open scope) so that view code using
open_lots, bond_accounts, bond.bond_lots.open does not fire per-account queries;
alternatively, replace the view aggregation with a single query (e.g., query
BondLot.open joined to bonds/accounts) and assign that to open_lots so the view
only iterates over already-loaded records.

40-66: Heavy logic in ERB template violates coding guidelines.

Lines 40–66 contain substantial conditional logic for determining label, total_return_amount, total_return_label, rate_text, and rate_meta. As per coding guidelines, avoid heavy logic in ERB view files and prefer helpers or presenters. Consider extracting this into a helper method or a decorator/presenter object.

♻️ Example refactor to helper
# app/helpers/bond_lots_helper.rb
module BondLotsHelper
  def bond_lot_display_data(lot)
    {
      label: Bond.long_subtype_label_for(lot.subtype) || t("bonds.purchase_holding.unknown"),
      rate_text: compute_rate_text(lot),
      rate_meta: compute_rate_meta(lot),
      total_return: compute_total_return_display(lot)
    }
  end
  
  private
  
  def compute_rate_text(lot)
    # move lines 49-59 logic here
  end
  
  # ...
end

Then in the partial:

<% display = bond_lot_display_data(lot) %>
<%= display[:label] %>

As per coding guidelines: "Avoid heavy logic in ERB view files; prefer helpers and components instead."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/pages/dashboard/_bond_summary.html.erb` around lines 40 - 66, The
ERB partial contains heavy presentation logic (in the top_lots loop using
Bond.long_subtype_label_for and computing
total_return_amount/total_return_label, rate_text, and rate_meta) that should be
moved into a helper or presenter; create a helper method (e.g.
bond_lot_display_data(lot)) that returns a hash with keys label, rate_text,
rate_meta, and total_return (or separate private methods like
compute_rate_text(lot), compute_rate_meta(lot),
compute_total_return_display(lot)), move the conditional logic from the template
into those helper methods, and then replace the in-template logic with a single
call to bond_lot_display_data(lot) inside the partial while keeping references
to lot.inflation_linked?, lot.requires_rate_review?, lot.current_rate_percent,
lot.interest_rate, lot.current_inflation_component_percent,
lot.current_margin_percent, lot.rate_type, and lot.coupon_frequency in the
helper.
app/models/bond.rb (1)

45-53: Consider combining existence check and sum into a single query.

original_balance first checks bond_lots.exists? then calls bond_lots.sum(:amount), resulting in two database queries. sum returns 0 for empty relations, so the existence check may be unnecessary unless the fallback to first_valuation_amount is specifically for accounts without any lots.

If the intent is "use lots sum if any lots exist, otherwise fallback", consider:

   def original_balance
-    principal_amount = if bond_lots.exists?
-      bond_lots.sum(:amount)
-    else
-      account.first_valuation_amount
-    end
+    lots_sum = bond_lots.sum(:amount)
+    principal_amount = lots_sum.positive? ? lots_sum : account.first_valuation_amount

     Money.new(principal_amount, account.currency)
   end

However, if a lot with amount: 0 is valid, the current logic is more correct. Verify the intended behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/bond.rb` around lines 45 - 53, The method original_balance
currently does two DB queries (bond_lots.exists? and bond_lots.sum(:amount));
replace them by calling total = bond_lots.sum(:amount) once and use that as
principal_amount, falling back to account.first_valuation_amount only when total
== 0 if that matches intended behavior (i.e., change logic in original_balance
to compute principal_amount = bond_lots.sum(:amount); principal_amount =
account.first_valuation_amount if principal_amount.zero?); if a lot with amount:
0 is a valid case and should prefer the lots presence, keep the existing exists?
check instead—verify which behavior is desired.
app/models/bond_lot.rb (5)

403-403: Redundant ternary expression.

inflation_linked? ? nil : nil always evaluates to nil. This appears to be a copy-paste artifact.

♻️ Proposed fix
-        interest_rate: inflation_linked? ? nil : nil,
+        interest_rate: nil,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/bond_lot.rb` at line 403, The line setting interest_rate uses a
redundant ternary `inflation_linked? ? nil : nil`; update the BondLot model to
assign interest_rate directly to nil and remove the ternary. Locate the
attribute/hash key `interest_rate:` in app/models/bond_lot.rb (and the
`inflation_linked?` predicate nearby) and replace the ternary expression with a
simple `nil` value to eliminate the dead code.

474-476: Side-effect-heavy return is confusing.

Line 475 combines assignment and return in a way that obscures intent. The return value is unused (this is a callback), and the assignment is the actual side effect.

♻️ Proposed clarity improvement
    def normalize_tax_settings
-     return self.tax_rate = 0 if apply_tax_exempt_wrapper!
+     if apply_tax_exempt_wrapper!
+       self.tax_rate = 0
+       return
+     end

      self.tax_strategy = "standard" if tax_strategy.blank?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/bond_lot.rb` around lines 474 - 476, In normalize_tax_settings,
avoid the side-effect-heavy inline return/assignment; instead, perform the
assignment as a clear statement when apply_tax_exempt_wrapper! is true (e.g.,
set self.tax_rate = 0 in an if block) and do not rely on returning that
assignment value since the callback ignores the return—update
normalize_tax_settings to separate the predicate check
(apply_tax_exempt_wrapper!) from the side-effect (setting self.tax_rate) for
clarity.

187-240: Consider extracting shared loop logic.

The while loop in capitalization_history closely mirrors estimated_current_value. While the outputs differ, the period iteration logic could potentially be extracted into a shared iterator method to reduce duplication. This is a minor observation for future maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/bond_lot.rb` around lines 187 - 240, The capitalization_history
method duplicates period iteration logic that's also used in
estimated_current_value; extract that shared loop into a single iterator method
(e.g., iterate_periods or with_capitalization_periods) that accepts start
(purchased_on), end (history_end or maturity/now), and yields per-period data
(period_number, start_on, end_on, days_in_step, days_in_year,
full_year_capitalization, rate_context, opening_balance) so both
capitalization_history and estimated_current_value can call it and build their
respective events/results using the yielded values; update
capitalization_history to call the new iterator and compute
interest_earned/closing_balance from the yielded values and remove duplicated
loop code.

523-530: Auto-buy is restricted to tax-exempt inflation-linked bonds.

The should_auto_buy_new_issue? method requires both tax_exempt_wrapper? and inflation_linked?. If this is intentional business logic (e.g., specific to Polish IKE/IKZE accounts with EOD/ROD bonds), consider adding a brief comment explaining the restriction for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/bond_lot.rb` around lines 523 - 530, The method
should_auto_buy_new_issue? currently gates auto-buy behind
bond&.auto_buy_new_issues?, bond&.tax_exempt_wrapper?, and inflation_linked?
which is a deliberate business restriction; add a brief explanatory comment
above should_auto_buy_new_issue? stating that auto-buy is limited to tax-exempt,
inflation-linked bonds (e.g., Polish IKE/IKZE with EOD/ROD bonds) so future
maintainers understand why bond&.tax_exempt_wrapper? and inflation_linked? are
both required, and keep the existing checks (referencing nominal_per_unit and
the floor calculation) unchanged.

94-94: Simplify days-in-year calculation.

The expression (cursor + 1.year - cursor).to_i is a convoluted way to determine leap year days. Consider using a clearer approach.

♻️ Proposed simplification
-      days_in_year = [ (cursor + 1.year - cursor).to_i, 1 ].max
+      days_in_year = Date.leap?(cursor.year) ? 366 : 365
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/bond_lot.rb` at line 94, Replace the convoluted days_in_year
calculation with a clear leap-year check: use Date.leap?(cursor.year) to set
days_in_year to 366 for leap years and 365 otherwise (i.e. days_in_year =
Date.leap?(cursor.year) ? 366 : 365), updating the expression around the
days_in_year variable that currently uses (cursor + 1.year - cursor).to_i.
app/models/gus_inflation_rate.rb (3)

81-84: Redundant .presence call.

indicator_id is already assigned with .presence on line 82, so the second .presence on line 83 is unnecessary.

♻️ Proposed simplification
      def cpi_indicator_id
-       indicator_id = ENV["GUS_SDP_CPI_INDICATOR_ID"].presence
-       indicator_id.presence || Provider::GusSdp::DEFAULT_CPI_INDICATOR_ID
+       ENV["GUS_SDP_CPI_INDICATOR_ID"].presence || Provider::GusSdp::DEFAULT_CPI_INDICATOR_ID
      end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/gus_inflation_rate.rb` around lines 81 - 84, The cpi_indicator_id
method uses .presence twice; remove the redundant call by assigning indicator_id
= ENV["GUS_SDP_CPI_INDICATOR_ID"].presence and then returning indicator_id ||
Provider::GusSdp::DEFAULT_CPI_INDICATOR_ID (i.e., drop the second .presence).
Update the cpi_indicator_id method to return either the present ENV value or
Provider::GusSdp::DEFAULT_CPI_INDICATOR_ID directly.

104-108: PostgreSQL-specific make_date function.

The query uses PostgreSQL's make_date function, which ties this model to PostgreSQL. This is acceptable if PostgreSQL is the only supported database, but worth noting for portability awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/gus_inflation_rate.rb` around lines 104 - 108, The method
latest_before_or_on uses PostgreSQL's make_date(year, month, 1) making the query
DB-specific; change latest_before_or_on to avoid make_date by comparing year and
month numerically: compute target_year = target_date.year and target_month =
target_date.month and replace the where clause with a portable condition like
"year < ? OR (year = ? AND month <= ?)" (binding target_year, target_year,
target_month), keep the same order(year: :desc, month: :desc). This removes the
make_date dependency while preserving behavior.

86-98: Refactor error detection to use structured error types instead of string matching.

The error detection methods in not_found_error? and rate_limited_error? rely on substring matching against error.message, which is fragile. The project already uses a more robust pattern elsewhere—see Provider::YahooFinance which defines error subclasses (RateLimitError, AuthenticationError) and overrides default_error_transformer to detect specific Faraday error types.

Apply the same pattern here: Have Provider::GusSdp define NotFoundError and RateLimitError subclasses, override default_error_transformer to detect Faraday::NotFoundError and Faraday::TooManyRequestsError, then update GusInflationRate to check error.is_a?(Provider::GusSdp::RateLimitError) instead of parsing error messages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/gus_inflation_rate.rb` around lines 86 - 98, Replace fragile
string-matching in GusInflationRate#not_found_error? and `#rate_limited_error`?
with structured error type checks: add Provider::GusSdp::NotFoundError and
Provider::GusSdp::RateLimitError classes, implement
Provider::GusSdp.default_error_transformer to map Faraday::NotFoundError and
Faraday::TooManyRequestsError to those subclasses, then update GusInflationRate
to detect errors via error.is_a?(Provider::GusSdp::NotFoundError) and
error.is_a?(Provider::GusSdp::RateLimitError) instead of checking error.message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/accounts_controller.rb`:
- Line 45: Wrap the call to `@account.bond`&.settle_matured_lots! (guarded by
`@account.bond`?) in a rescue block so any exception during settlement is caught
and does not bubble up to the show action; log the exception with context (e.g.
logger.error or Rails.logger.error including `@account.id` and the
exception.message/backtrace) and optionally report to the app's error-tracking
(Sentry/Raven) instead of re-raising, then continue rendering the page.

In `@app/models/bond.rb`:
- Around line 96-98: The inflation_linked? method calls
subtype.in?(INFLATION_LINKED_SUBTYPES) but subtype can be nil, causing a
NoMethodError; update inflation_linked? to guard against nil (e.g., use safe
navigation or an explicit presence check) before calling in? or switch to
calling INFLATION_LINKED_SUBTYPES.include?(subtype) so the method safely returns
false when subtype is nil; ensure the change is applied in the inflation_linked?
method and references the existing INFLATION_LINKED_SUBTYPES constant.

In `@app/models/gus_inflation_rate.rb`:
- Around line 73-74: The provider method currently memoizes `@provider` and can
hold stale credentials when Setting.gus_sdp_api_key or cpi_indicator_id change;
update the provider method (provider) to avoid class-level memoization by either
always instantiating a new Provider::GusSdp with current api_key and
cpi_indicator_id or implement a cache keyed by "#{api_key}-#{cpi_indicator_id}"
(e.g., store in a hash like provider_cache[cache_key]) so the
Provider::GusSdp.new(api_key: api_key, cpi_indicator_id: cpi_indicator_id) is
recreated when credentials change; remove the use of `@provider` as a long-lived
cache unless tied to those keys.

In `@app/models/provider/gus_sdp.rb`:
- Around line 22-30: The JSON parsing block that builds rows from
JSON.parse(response.body) is brittle: replace the current logic around parsed
and rows so it defensively handles nil, non-hash/non-array types, and hashes
missing "data"; specifically, after parsed = JSON.parse(response.body) ensure
parsed is an Array (use it), or if it's a Hash fetch "data" only when present
and an Array, otherwise fall back to an empty Array before calling rows.map;
update the code around JSON.parse(response.body), rows, and the rows.map block
to use this safe fallback.
- Around line 37-49: The Faraday connection in the client method lacks request
timeouts; update the Faraday.new block in method client to set reasonable open
and read/request timeouts (e.g. open_timeout and timeout) so requests to
base_url cannot hang indefinitely. Locate the Faraday.new initialization inside
client and add the request timeout options (or use faraday.request :timeout
settings) alongside the existing retry and raise_error configuration, ensuring
api_key header behavior remains unchanged.

In `@app/views/bonds/_form.html.erb`:
- Line 28: The toggle helper is calling account.accountable.auto_buy_new_issues?
which can raise NoMethodError when account.accountable is nil; update the call
in the bond_form toggle (the :auto_buy_new_issues field) to safely handle nil
(e.g., use safe navigation or a default boolean) so the helper receives a
boolean even for new accounts—ensure the expression around
account.accountable.auto_buy_new_issues? is replaced with a nil-safe
alternative.

In `@config/locales/views/pages/en.yml`:
- Line 18: The translation key bond_rate_review_notice currently uses "bond
lot(s)"—replace it with Rails i18n pluralization by changing
bond_rate_review_notice into a hash with count variants (one and other) and
interpolate %{count} and %{accounts} (e.g., bond_rate_review_notice.one and
bond_rate_review_notice.other) so the UI shows "bond lot" for count 1 and "bond
lots" otherwise; update any callers that pass a count to ensure they use
I18n.t('bond_rate_review_notice', count: n, accounts: accounts).

In `@db/migrate/20260331120000_create_bond_lots.rb`:
- Around line 3-12: Add explicit PostgreSQL CHECK constraints to the bond_lots
table to enforce domain invariants: ensure amount > 0, term_months > 0,
maturity_date >= purchased_on, and interest_rate IS NULL OR interest_rate >= 0
(or a sensible upper bound if desired). Update the migration that defines
create_table :bond_lots (the block that sets t.references :bond, t.date
:purchased_on, t.decimal :amount, t.integer :term_months, t.date :maturity_date,
t.decimal :interest_rate) to add database-level checks (using
add_check_constraint or SQL CHECK clauses) for these conditions so invalid rows
cannot be inserted even if application validations are bypassed.

In `@db/migrate/20260331133000_add_purchase_fields_to_bond_lots.rb`:
- Around line 3-5: The three new columns on bond_lots (subtype, rate_type,
coupon_frequency) must be non-null at the DB level; modify the migration to add
them with null: false and a safe default (e.g., add_column :bond_lots, :subtype,
:string, null: false, default: '' and likewise for :rate_type and
:coupon_frequency), or implement the two-step pattern (add nullable columns,
backfill existing rows, then change_column_null to enforce null: false) so
existing data isn't broken. Ensure you reference the bond_lots table and the
three column names when making the change.

In `@db/migrate/20260331153000_reconfigure_bond_products_for_eod_rod.rb`:
- Around line 75-85: The down method currently overwrites all subtype values via
the two execute blocks (updating bonds and bond_lots), which irreversibly loses
original values; to fix, remove those destructive UPDATEs in the down method and
instead raise ActiveRecord::IrreversibleMigration (i.e., replace the execute
<<~SQL blocks in the down method with raise ActiveRecord::IrreversibleMigration)
so rollbacks are explicitly prevented and signal the migration is not
reversible; alternatively, if you prefer reversible behavior implement a backup
in up (add original_subtype columns and copy subtype from bonds and bond_lots)
and then restore those values and drop the backup columns in down.

In `@test/controllers/settings/hostings_controller_test.rb`:
- Line 253: The tests set the global flag Setting.gus_inflation_import_enabled
and do not restore it, causing state bleed; update the test(s) in
hostings_controller_test.rb to capture the original value before mutating
Setting.gus_inflation_import_enabled and ensure it is restored after the test
(use an ensure block, a teardown hook, or an around wrapper) so the original
value is always reassigned regardless of test outcome; apply the same restore
pattern for every place where Setting.gus_inflation_import_enabled is mutated
(the occurrences around the current test and the other mutation noted).

---

Nitpick comments:
In @.env.example:
- Around line 58-62: The GUS SDP env vars (GUS_INFLATION_IMPORT_ENABLED and
GUS_SDP_API_KEY) are placed between the "Provider selection for exchange rates"
and the "Alternative: Use Yahoo Finance" sections, breaking the provider
settings group; relocate the two lines so they sit either before the provider
selection block or immediately after the Yahoo Finance alternative block to keep
all data-provider related variables grouped together and update surrounding
comments if needed to preserve readability.

In `@app/controllers/bond_lots_controller.rb`:
- Around line 18-26: The edit and show actions use the unconventional "nil
unless require_account_permission!(`@account`)" pattern; replace those lines with
explicit early returns matching the existing convention by calling
require_account_permission!(`@account`) and returning immediately on failure
(e.g., "return unless require_account_permission!(`@account`)") after assigning
`@account` = `@bond_lot.account` so edit and show mirror the new action and other
controllers and maintain consistent control flow.
- Around line 34-38: The two writes (save! then update!) on `@bond_lot` should be
combined into a single persistence operation: call create_purchase_entry! in a
way that does not require bond_lot.id (or change it to accept the in-memory
`@bond_lot`), assign the resulting entry to `@bond_lot.entry` (or build the
association) and then call `@bond_lot.save`! inside the
ActiveRecord::Base.transaction so only one DB write occurs; update the
create_purchase_entry! signature/implementation to use the bond_lot object (or
its attributes) instead of relying on a persisted id if necessary.

In `@app/jobs/import_gus_inflation_rates_job.rb`:
- Line 5: Replace the direct Setting.gus_inflation_import_enabled check in
ImportGusInflationRatesJob with the effective accessor
Setting.gus_inflation_import_enabled_effective so the job honors the ENV
override; update the guard (the line currently using
gus_inflation_import_enabled) to call gus_inflation_import_enabled_effective
instead.

In `@app/jobs/settle_matured_bond_lots_job.rb`:
- Around line 5-8: The scheduled query over
BondLot.open.where(auto_close_on_maturity: true).where("maturity_date <= ?", on)
needs a composite index to avoid full table scans: add a migration that creates
an index on the column(s) used by the BondLot.open scope (e.g. the state/status
column), plus auto_close_on_maturity and maturity_date (e.g. [state,
auto_close_on_maturity, maturity_date] or [status, auto_close_on_maturity,
maturity_date] depending on your scope implementation), update schema, and run
migrations; ensure the index name is descriptive and include a test or note in
the model/README that the index supports the BondLot.open +
auto_close_on_maturity + maturity_date query pattern.

In `@app/models/balance/base_calculator.rb`:
- Around line 19-26: Replace the string type check in the memoized holdings
calculation with the model predicate: in the block that assigns
`@holdings_value_for_date`[date] (the conditional currently using
account.accountable_type == "Bond"), use account.bond? instead and keep the
existing branches (use account.bond.bond_lots... for bonds, else use
sync_cache.get_holdings). This keeps behavior the same but uses the predicate
used elsewhere (e.g., chart.html.erb) and is resilient to refactors.

In `@app/models/bond_lot.rb`:
- Line 403: The line setting interest_rate uses a redundant ternary
`inflation_linked? ? nil : nil`; update the BondLot model to assign
interest_rate directly to nil and remove the ternary. Locate the attribute/hash
key `interest_rate:` in app/models/bond_lot.rb (and the `inflation_linked?`
predicate nearby) and replace the ternary expression with a simple `nil` value
to eliminate the dead code.
- Around line 474-476: In normalize_tax_settings, avoid the side-effect-heavy
inline return/assignment; instead, perform the assignment as a clear statement
when apply_tax_exempt_wrapper! is true (e.g., set self.tax_rate = 0 in an if
block) and do not rely on returning that assignment value since the callback
ignores the return—update normalize_tax_settings to separate the predicate check
(apply_tax_exempt_wrapper!) from the side-effect (setting self.tax_rate) for
clarity.
- Around line 187-240: The capitalization_history method duplicates period
iteration logic that's also used in estimated_current_value; extract that shared
loop into a single iterator method (e.g., iterate_periods or
with_capitalization_periods) that accepts start (purchased_on), end (history_end
or maturity/now), and yields per-period data (period_number, start_on, end_on,
days_in_step, days_in_year, full_year_capitalization, rate_context,
opening_balance) so both capitalization_history and estimated_current_value can
call it and build their respective events/results using the yielded values;
update capitalization_history to call the new iterator and compute
interest_earned/closing_balance from the yielded values and remove duplicated
loop code.
- Around line 523-530: The method should_auto_buy_new_issue? currently gates
auto-buy behind bond&.auto_buy_new_issues?, bond&.tax_exempt_wrapper?, and
inflation_linked? which is a deliberate business restriction; add a brief
explanatory comment above should_auto_buy_new_issue? stating that auto-buy is
limited to tax-exempt, inflation-linked bonds (e.g., Polish IKE/IKZE with
EOD/ROD bonds) so future maintainers understand why bond&.tax_exempt_wrapper?
and inflation_linked? are both required, and keep the existing checks
(referencing nominal_per_unit and the floor calculation) unchanged.
- Line 94: Replace the convoluted days_in_year calculation with a clear
leap-year check: use Date.leap?(cursor.year) to set days_in_year to 366 for leap
years and 365 otherwise (i.e. days_in_year = Date.leap?(cursor.year) ? 366 :
365), updating the expression around the days_in_year variable that currently
uses (cursor + 1.year - cursor).to_i.

In `@app/models/bond.rb`:
- Around line 45-53: The method original_balance currently does two DB queries
(bond_lots.exists? and bond_lots.sum(:amount)); replace them by calling total =
bond_lots.sum(:amount) once and use that as principal_amount, falling back to
account.first_valuation_amount only when total == 0 if that matches intended
behavior (i.e., change logic in original_balance to compute principal_amount =
bond_lots.sum(:amount); principal_amount = account.first_valuation_amount if
principal_amount.zero?); if a lot with amount: 0 is a valid case and should
prefer the lots presence, keep the existing exists? check instead—verify which
behavior is desired.

In `@app/models/gus_inflation_rate.rb`:
- Around line 81-84: The cpi_indicator_id method uses .presence twice; remove
the redundant call by assigning indicator_id =
ENV["GUS_SDP_CPI_INDICATOR_ID"].presence and then returning indicator_id ||
Provider::GusSdp::DEFAULT_CPI_INDICATOR_ID (i.e., drop the second .presence).
Update the cpi_indicator_id method to return either the present ENV value or
Provider::GusSdp::DEFAULT_CPI_INDICATOR_ID directly.
- Around line 104-108: The method latest_before_or_on uses PostgreSQL's
make_date(year, month, 1) making the query DB-specific; change
latest_before_or_on to avoid make_date by comparing year and month numerically:
compute target_year = target_date.year and target_month = target_date.month and
replace the where clause with a portable condition like "year < ? OR (year = ?
AND month <= ?)" (binding target_year, target_year, target_month), keep the same
order(year: :desc, month: :desc). This removes the make_date dependency while
preserving behavior.
- Around line 86-98: Replace fragile string-matching in
GusInflationRate#not_found_error? and `#rate_limited_error`? with structured error
type checks: add Provider::GusSdp::NotFoundError and
Provider::GusSdp::RateLimitError classes, implement
Provider::GusSdp.default_error_transformer to map Faraday::NotFoundError and
Faraday::TooManyRequestsError to those subclasses, then update GusInflationRate
to detect errors via error.is_a?(Provider::GusSdp::NotFoundError) and
error.is_a?(Provider::GusSdp::RateLimitError) instead of checking error.message.

In `@app/views/bond_lots/_form.html.erb`:
- Line 9: Replace direct calls to Setting.gus_inflation_import_enabled with the
ENV-aware accessor Setting.gus_inflation_import_enabled_effective in the
bond_lot form partial; specifically update the occurrences referenced (the
bond_lot_form_global_auto_fetch_enabled_value and the other usages at lines
noted) so the form uses gus_inflation_import_enabled_effective to reflect ENV
overrides and avoid inconsistent behavior between persisted and env-provided
values.
- Around line 74-84: The indentation for the div wrapping the rate_type and
coupon_frequency selects is inconsistent with surrounding markup; adjust the
leading spaces so the <div data-bond-lot-form-target="otherFields"> and its
closing tag align with neighbouring elements, and ensure the nested lines for
form.select calls (for :rate_type using Bond::RATE_TYPES and for
:coupon_frequency using Bond::COUPON_FREQUENCIES) are indented one level deeper
to match the file's indentation style.

In `@app/views/bond_lots/show.html.erb`:
- Line 90: In the view template change the hardcoded Tailwind color class on the
element that currently has the class string "rounded-lg px-3 py-2 text-red-500
text-sm font-medium border border-secondary" by replacing text-red-500 with the
design token text-destructive so the class becomes "rounded-lg px-3 py-2
text-destructive text-sm font-medium border border-secondary"; ensure no other
hardcoded color variants remain in the same element.
- Around line 46-47: Replace the direct Money.new(...).format calls with the
existing format_money helper for consistency: update the arguments passed to the
translation helpers t(".history_balance", ...) and t(".history_interest", ...)
so opening: and closing: use format_money(event[:opening_balance],
`@account.currency`) and format_money(event[:closing_balance], `@account.currency`)
respectively, and interest: uses format_money(event[:interest_earned],
`@account.currency`); keep number_to_percentage(event[:annual_rate_percent],
precision: 3) unchanged.

In `@app/views/bonds/_purchase_holding.html.erb`:
- Line 35: Replace the nested ternary that assigns rate_text with a clear
conditional block: first check lot.requires_rate_review? and set rate_text to
t(".update_needed"); elsif check current_rate.present? and set rate_text to
number_to_percentage(current_rate, precision: 3); else set rate_text to
t(".unknown"). Update the expression around the variable rate_text (referencing
lot.requires_rate_review?, current_rate, number_to_percentage,
t(".update_needed"), and t(".unknown")) to use if/elsif/else for readability.

In `@app/views/pages/dashboard/_bond_summary.html.erb`:
- Around line 3-5: open_lots is built with bond_accounts.flat_map calling
account.bond.bond_lots.open which may trigger an N+1; fix it by loading
bond_lots in a single query or eager-loading them when fetching bond_accounts.
Update the controller where `@bond_accounts` is set to include/ preload the
associations (bond and bond_lots with the open scope) so that view code using
open_lots, bond_accounts, bond.bond_lots.open does not fire per-account queries;
alternatively, replace the view aggregation with a single query (e.g., query
BondLot.open joined to bonds/accounts) and assign that to open_lots so the view
only iterates over already-loaded records.
- Around line 40-66: The ERB partial contains heavy presentation logic (in the
top_lots loop using Bond.long_subtype_label_for and computing
total_return_amount/total_return_label, rate_text, and rate_meta) that should be
moved into a helper or presenter; create a helper method (e.g.
bond_lot_display_data(lot)) that returns a hash with keys label, rate_text,
rate_meta, and total_return (or separate private methods like
compute_rate_text(lot), compute_rate_meta(lot),
compute_total_return_display(lot)), move the conditional logic from the template
into those helper methods, and then replace the in-template logic with a single
call to bond_lot_display_data(lot) inside the partial while keeping references
to lot.inflation_linked?, lot.requires_rate_review?, lot.current_rate_percent,
lot.interest_rate, lot.current_inflation_component_percent,
lot.current_margin_percent, lot.rate_type, and lot.coupon_frequency in the
helper.

In `@db/migrate/20260330233052_create_bonds.rb`:
- Around line 1-15: Add indexes on frequently filtered columns in the
CreateBonds migration: inside the change method where create_table :bonds is
declared (or immediately after it) add indexes for the subtype and rate_type
columns (e.g., add_index :bonds, :subtype and add_index :bonds, :rate_type) so
queries that filter or group by those fields are fast; include names or options
(using: :btree) if you need explicit index naming or a specific index type, or
use t.index within the create_table block to define them inline.

In
`@db/migrate/20260331170000_add_gus_inflation_rates_and_auto_fetch_to_bond_lots.rb`:
- Line 14: The migration currently adds auto_fetch_inflation to :bond_lots with
default: true which opts all existing records into automatic GUS inflation
fetching; change the migration to use default: false (add_column :bond_lots,
:auto_fetch_inflation, :boolean, null: false, default: false) so existing bond
lots preserve prior behavior, and if you need selective opt-in, add an explicit
data backfill/update step after the column add to set true only for records that
should auto-fetch. Ensure you update any tests or seed logic that assume the new
default.

In `@db/schema.rb`:
- Around line 1602-1603: The foreign key from bond_lots to entries currently
lacks on_delete behavior; update the constraint for bond_lots.entry_id (the
add_foreign_key call referencing "entries") to include an appropriate on_delete
option—preferably on_delete: :nullify (or :cascade if you decide deletion
semantics require removing lots with their entry)—so that deleting an Entry
won't leave dangling entry_id references; locate the add_foreign_key
"bond_lots", "entries" invocation and change it to include the chosen on_delete
behavior (and add a migration to alter the foreign key if this is already in
production schema).

In `@test/controllers/pages_controller_test.rb`:
- Line 35: Replace the hard-coded assertion string in the test's assert_equal
against flash[:notice] with an i18n lookup; build the expected value via
I18n.t("pages.dashboard.bond_rate_review_notice", count: 1, accounts: "US
T-Bill") and assert_equal that expected value to flash[:notice] instead of the
literal "1 bond lot(s) need updated issue rates: US T-Bill."; update the
assert_equal call that currently references flash[:notice] to use the expected
variable derived from I18n.t.

In `@test/controllers/settings/hostings_controller_test.rb`:
- Around line 251-277: Add a new authorization test that mirrors the existing
admin-gate tests (e.g. clear_cache, disconnect_external_assistant): create a
test like "non-admin cannot enqueue gus inflation import" that uses
with_self_hosting but does not sign in as an admin, then POST to
import_gus_inflation_rates_settings_hosting_url with the same params and assert
that no ImportGusInflationRatesJob is enqueued (assert_no_enqueued_jobs only:
ImportGusInflationRatesJob) and that the request is rejected per your app policy
(match the existing admin-gate pattern: assert_redirected_to and flash alert or
assert_response :forbidden depending on how
clear_cache/disconnect_external_assistant assert authorization).

In `@test/jobs/settle_matured_bond_lots_job_test.rb`:
- Around line 4-24: Add a boundary case to the test by creating a second BondLot
via BondLot.create! with the same matured criteria but auto_close_on_maturity:
false (or create a lot with purchased_on such that it's not yet matured), call
SettleMaturedBondLotsJob.perform_now as before, then assert that the second
lot.reload.closed_on is still nil to verify non-auto-close lots (or
not-yet-matured lots) are not closed by SettleMaturedBondLotsJob.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

Comment thread app/controllers/accounts_controller.rb Outdated
Comment thread app/models/bond.rb
Comment thread app/models/gus_inflation_rate.rb Outdated
Comment thread app/models/provider/gus_sdp.rb
Comment thread app/models/provider/gus_sdp.rb
Comment thread config/locales/views/pages/en.yml Outdated
Comment thread db/migrate/20260331120000_create_bond_lots.rb
Comment thread db/migrate/20260331133000_add_purchase_fields_to_bond_lots.rb Outdated
Comment thread db/migrate/20260331153000_reconfigure_bond_products_for_eod_rod.rb Outdated
Comment thread test/controllers/settings/hostings_controller_test.rb Outdated
Copy link
Copy Markdown

@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 (4)
app/controllers/bond_lots_controller.rb (3)

18-26: Inconsistent permission check patterns.

Lines 20 and 25 use nil unless require_account_permission! while lines 6, 30, 51, and 68 use return unless require_account_permission!. The nil unless pattern is unusual and inconsistent with the rest of the controller.

♻️ Proposed fix for consistency
   def edit
     `@account` = `@bond_lot.account`
-    nil unless require_account_permission!(`@account`)
+    return unless require_account_permission!(`@account`)
   end
 
   def show
     `@account` = `@bond_lot.account`
-    nil unless require_account_permission!(`@account`)
+    return unless require_account_permission!(`@account`)
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/bond_lots_controller.rb` around lines 18 - 26, The edit and
show actions use the unusual pattern "nil unless require_account_permission!"
which is inconsistent with other actions; update both methods (edit and show) to
use "return unless require_account_permission!" after assigning `@account` =
`@bond_lot.account` so the permission check behavior matches other controller
actions that call require_account_permission!.

36-40: Consider atomic bond lot creation with entry.

The current flow saves the bond lot, then immediately updates it to set the entry. This requires two database writes and could leave orphaned records if the entry creation fails (though the transaction should rollback).

♻️ Alternative: Create entry first, then create lot with entry reference
     if `@bond_lot.valid`?
       ActiveRecord::Base.transaction do
-        `@bond_lot.save`!
-        `@bond_lot.update`!(entry: create_purchase_entry!(`@account`, `@bond_lot`))
+        `@bond_lot.save`!
+        entry = create_purchase_entry!(`@account`, `@bond_lot`)
+        `@bond_lot.update_column`(:entry_id, entry.id)
       end

Or consider having create_purchase_entry! accept the bond_lot and set the association directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/bond_lots_controller.rb` around lines 36 - 40, The current
transaction saves `@bond_lot` then immediately calls `@bond_lot.update`!(entry:
create_purchase_entry!(`@account`, `@bond_lot`)), causing two writes and potential
ordering issues; change the flow so the purchase entry is created (or prepared)
before persisting the bond lot and then set the entry association on `@bond_lot`
prior to save!, or refactor create_purchase_entry! to accept the bond lot and
assign the association directly; keep the whole operation inside
ActiveRecord::Base.transaction and replace the separate save! + update! with a
single save! that persists the bond lot with its entry association.

117-134: Entry update logic could be extracted to the model.

Per coding guidelines, controllers should be lightweight with domain logic in models. The update_purchase_entry! method contains business logic about how bond lot changes should propagate to entries.

Consider moving this to BondLot#sync_purchase_entry! or similar.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/bond_lots_controller.rb` around lines 117 - 134, The
controller method update_purchase_entry! contains domain logic and should be
moved to the model: implement a BondLot#sync_purchase_entry! (or
BondLot#update_purchase_entry!) instance method that performs the same work
(guarding on entry presence, computing subtype_label via
Bond.long_subtype_label_for(subtype) || Bond.display_name.singularize, and
calling entry.update! with date, name (use I18n.t same key), amount and
entryable_attributes merging bond_subtype, bond_term_months and
bond_interest_rate into entry.entryable.extra). Replace the controller’s
update_purchase_entry! call with a single call to the new BondLot instance
method so the controller remains thin and behavior is unchanged.
app/models/bond_lot.rb (1)

162-188: Indentation inconsistency in settle_if_matured! method.

The with_lock block (line 164) and nested transaction block (line 175) have inconsistent indentation making the code structure hard to follow. While syntactically valid, the indentation suggests incorrect nesting.

♻️ Proposed fix for consistent indentation
   def settle_if_matured!(on: Date.current)
     # Lock the row to prevent concurrent settlements.
     with_lock do
-    return false unless auto_close_on_maturity?
-    return false unless open?
-    return false unless matured?(on:)
+      return false unless auto_close_on_maturity?
+      return false unless open?
+      return false unless matured?(on:)
 
-    settlement_date = [ on, maturity_date ].compact.min
-    gross_value = estimated_current_value(on: settlement_date)
-    gain = [ gross_value - amount.to_d, 0.to_d ].max
-    tax_withheld_amount = (gain * settlement_tax_rate_percent / 100).round(4)
-    net_value = (gross_value - tax_withheld_amount).round(4)
+      settlement_date = [ on, maturity_date ].compact.min
+      gross_value = estimated_current_value(on: settlement_date)
+      gain = [ gross_value - amount.to_d, 0.to_d ].max
+      tax_withheld_amount = (gain * settlement_tax_rate_percent / 100).round(4)
+      net_value = (gross_value - tax_withheld_amount).round(4)
 
-    ActiveRecord::Base.transaction do
-    create_settlement_entry!(settlement_date:, net_value:, tax_withheld_amount:, gross_value:)
-    update!(
-      closed_on: settlement_date,
-      settlement_amount: net_value,
-      tax_withheld: tax_withheld_amount
-    )
-    create_reinvestment_lot!(settlement_date:, net_value:) if should_auto_buy_new_issue?(net_value:)
-  end
+      ActiveRecord::Base.transaction do
+        create_settlement_entry!(settlement_date:, net_value:, tax_withheld_amount:, gross_value:)
+        update!(
+          closed_on: settlement_date,
+          settlement_amount: net_value,
+          tax_withheld: tax_withheld_amount
+        )
+        create_reinvestment_lot!(settlement_date:, net_value:) if should_auto_buy_new_issue?(net_value:)
+      end
 
-    account.sync_later(window_start_date: settlement_date)
-    true
-  end
+      account.sync_later(window_start_date: settlement_date)
+      true
+    end
+  end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/bond_lot.rb` around lines 162 - 188, The with_lock/do and
ActiveRecord::Base.transaction blocks in settle_if_matured! are misindented,
which obscures nesting; reformat the method so with_lock do begins a block that
clearly contains the transaction block and the subsequent account.sync_later and
true return, and indent the ActiveRecord::Base.transaction do block and its
inner calls (create_settlement_entry!, update!, create_reinvestment_lot!)
consistently so the end keywords line up with their corresponding do keywords
and the method’s control flow is visually correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/models/bond_lot.rb`:
- Around line 313-331: The orphaned block after inflation_linked_rate_context
must be wrapped into the missing method definition inflation_snapshot_for(on:);
locate the loose code that queries GusInflationRate and returns the hash
(currently sitting after inflation_linked_rate_context) and move or wrap it
inside def inflation_snapshot_for(on:) ... end, ensuring to remove the stray
extra end and keep references to GusInflationRate.for_date,
auto_fetch_inflation?, cpi_lag_months, inflation_rate_assumption, and
current_inflation_indicator_id so the method returns either the "gus" hash or
the "manual" hash as originally intended.

In `@db/migrate/20260331153000_reconfigure_bond_products_for_eod_rod.rb`:
- Around line 63-79: The down method currently defines a nested def down (a
syntax bug) so the irreversible raise is never executed; fix by removing the
inner method definition and making the outer down immediately raise
ActiveRecord::IrreversibleMigration (or move the raise to the top of the outer
down) instead of performing the column removals; update the method named down in
this migration (remove the nested def down block and any destructive
remove_column/remove_index calls) so the migration is correctly marked
irreversible.

In `@test/controllers/settings/hostings_controller_test.rb`:
- Around line 253-254: The test captures old_val after mutating
Setting.gus_inflation_import_enabled, so restore logic is ineffective; update
both occurrences to read the original value into old_val before assigning
Setting.gus_inflation_import_enabled = true (i.e., call old_val =
Setting.gus_inflation_import_enabled prior to the mutation) in the
hostings_controller_test.rb tests (the blocks that reference
Setting.gus_inflation_import_enabled and old_val) and apply the same change to
the second test block around lines 270-287 so the original setting is preserved
and restored correctly.

---

Nitpick comments:
In `@app/controllers/bond_lots_controller.rb`:
- Around line 18-26: The edit and show actions use the unusual pattern "nil
unless require_account_permission!" which is inconsistent with other actions;
update both methods (edit and show) to use "return unless
require_account_permission!" after assigning `@account` = `@bond_lot.account` so the
permission check behavior matches other controller actions that call
require_account_permission!.
- Around line 36-40: The current transaction saves `@bond_lot` then immediately
calls `@bond_lot.update`!(entry: create_purchase_entry!(`@account`, `@bond_lot`)),
causing two writes and potential ordering issues; change the flow so the
purchase entry is created (or prepared) before persisting the bond lot and then
set the entry association on `@bond_lot` prior to save!, or refactor
create_purchase_entry! to accept the bond lot and assign the association
directly; keep the whole operation inside ActiveRecord::Base.transaction and
replace the separate save! + update! with a single save! that persists the bond
lot with its entry association.
- Around line 117-134: The controller method update_purchase_entry! contains
domain logic and should be moved to the model: implement a
BondLot#sync_purchase_entry! (or BondLot#update_purchase_entry!) instance method
that performs the same work (guarding on entry presence, computing subtype_label
via Bond.long_subtype_label_for(subtype) || Bond.display_name.singularize, and
calling entry.update! with date, name (use I18n.t same key), amount and
entryable_attributes merging bond_subtype, bond_term_months and
bond_interest_rate into entry.entryable.extra). Replace the controller’s
update_purchase_entry! call with a single call to the new BondLot instance
method so the controller remains thin and behavior is unchanged.

In `@app/models/bond_lot.rb`:
- Around line 162-188: The with_lock/do and ActiveRecord::Base.transaction
blocks in settle_if_matured! are misindented, which obscures nesting; reformat
the method so with_lock do begins a block that clearly contains the transaction
block and the subsequent account.sync_later and true return, and indent the
ActiveRecord::Base.transaction do block and its inner calls
(create_settlement_entry!, update!, create_reinvestment_lot!) consistently so
the end keywords line up with their corresponding do keywords and the method’s
control flow is visually correct.
🪄 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: c61f03c4-f5d4-4d06-8f8f-cf7b5cd00c76

📥 Commits

Reviewing files that changed from the base of the PR and between 92a8fad and 75eb8b1.

📒 Files selected for processing (20)
  • app/controllers/accounts_controller.rb
  • app/controllers/bond_lots_controller.rb
  • app/controllers/pages_controller.rb
  • app/controllers/settings/hostings_controller.rb
  • app/jobs/import_gus_inflation_rates_job.rb
  • app/models/bond.rb
  • app/models/bond_lot.rb
  • app/models/gus_inflation_rate.rb
  • app/models/provider/gus_sdp.rb
  • app/views/bond_lots/_form.html.erb
  • app/views/bonds/_form.html.erb
  • app/views/pages/dashboard/_bond_summary.html.erb
  • app/views/settings/hostings/_gus_sdp_settings.html.erb
  • config/locales/views/pages/en.yml
  • config/locales/views/settings/hostings/en.yml
  • db/migrate/20260331120000_create_bond_lots.rb
  • db/migrate/20260331133000_add_purchase_fields_to_bond_lots.rb
  • db/migrate/20260331153000_reconfigure_bond_products_for_eod_rod.rb
  • docs/localization/pl-localization-prep.md
  • test/controllers/settings/hostings_controller_test.rb
✅ Files skipped from review due to trivial changes (1)
  • docs/localization/pl-localization-prep.md
🚧 Files skipped from review as they are similar to previous changes (13)
  • app/controllers/accounts_controller.rb
  • db/migrate/20260331120000_create_bond_lots.rb
  • db/migrate/20260331133000_add_purchase_fields_to_bond_lots.rb
  • app/controllers/pages_controller.rb
  • app/jobs/import_gus_inflation_rates_job.rb
  • config/locales/views/settings/hostings/en.yml
  • app/controllers/settings/hostings_controller.rb
  • app/views/settings/hostings/_gus_sdp_settings.html.erb
  • app/views/bond_lots/_form.html.erb
  • app/views/pages/dashboard/_bond_summary.html.erb
  • app/views/bonds/_form.html.erb
  • app/models/gus_inflation_rate.rb
  • config/locales/views/pages/en.yml

Comment thread app/models/bond_lot.rb Outdated
Comment thread db/migrate/20260331153000_reconfigure_bond_products_for_eod_rod.rb Outdated
Comment thread test/controllers/settings/hostings_controller_test.rb Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 75 out of 75 changed files in this pull request and generated 11 comments.

Comment thread app/models/bond_lot.rb Outdated
Comment thread app/models/bond_lot.rb Outdated
Comment thread app/models/bond_lot.rb Outdated
Comment thread db/migrate/20260331120000_create_bond_lots.rb
Comment thread db/migrate/20260331153000_reconfigure_bond_products_for_eod_rod.rb Outdated
Comment thread app/controllers/pages_controller.rb Outdated
Comment thread config/locales/views/pages/en.yml Outdated
Comment thread app/controllers/settings/hostings_controller.rb Outdated
Comment thread app/views/bond_lots/_form.html.erb Outdated
Comment thread app/controllers/accounts_controller.rb Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 75 out of 75 changed files in this pull request and generated 4 comments.

Comment thread app/controllers/bond_lots_controller.rb Outdated
Comment thread app/controllers/bond_lots_controller.rb
Comment thread app/controllers/pages_controller.rb Outdated
Comment thread app/models/bond_lot.rb Outdated
Copy link
Copy Markdown

@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: 6

🧹 Nitpick comments (2)
app/views/settings/hostings/_gus_sdp_settings.html.erb (2)

93-98: Consider consolidating database queries.

This section makes 4 separate queries to GusInflationRate (count, exists?, minimum, maximum). For a settings page with low traffic this is acceptable, but could be consolidated into a single query if desired.

Optional optimization

You could precompute the stats in the controller or use a single aggregation:

# In controller
`@gus_stats` = GusInflationRate.pick(Arel.sql('COUNT(*), MIN(year), MAX(year)'))
# Returns [count, min_year, max_year] or nil if no records

Then in the view:

<% count, min_year, max_year = `@gus_stats` || [0, nil, nil] %>
<p><strong><%= t(".stored_records") %>:</strong> <%= count %></p>
<% if count > 0 %>
  <p><strong><%= t(".stored_range") %>:</strong> <%= min_year %>-<%= max_year %></p>
<% end %>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/settings/hostings/_gus_sdp_settings.html.erb` around lines 93 - 98,
Consolidate the four GusInflationRate queries by computing the stats once
(count, min year, max year) in the controller and passing them to the view (e.g.
`@gus_stats`), then destructure those values in the view and use count to decide
whether to render the stored_range; update references to GusInflationRate.count,
.exists?, .minimum(:year), and .maximum(:year) to use the precomputed values
(count, min_year, max_year) instead.

50-52: Use functional design token instead of raw Tailwind color variant.

text-secondary-600 should be replaced with the design system token text-secondary for consistency with the rest of the file and the project's styling conventions.

Proposed fix
-      <div class="text-secondary-600 text-sm">
+      <div class="text-secondary text-sm">

As per coding guidelines: "Use Tailwind design tokens (e.g., text-primary, bg-container, border-secondary) instead of raw color names."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/settings/hostings/_gus_sdp_settings.html.erb` around lines 50 - 52,
Replace the raw Tailwind color class "text-secondary-600" with the design token
"text-secondary" in the ERB block that renders the configured_via_env message
(the div containing <%= t(".configured_via_env") %>), so the div's class list
uses the project design token for text color instead of the raw color variant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/bond_lots_controller.rb`:
- Around line 29-33: The create action renders :new for non-bond accounts before
`@bond_lot` is initialized; change create to use the exact non-bond rejection path
used by new (rather than render :new) — either call the same redirect/alert
helper or move the non-bond check into a shared before_action used by both new
and create; alternatively, if you must render :new, ensure `@bond_lot` is
initialized the same way new does before rendering. Update the create method
(and related before_action if added) to reference create, new and `@bond_lot` so
the behavior and instance setup match.

In `@app/models/bond_lot.rb`:
- Around line 86-92: The loop in the compound/settlement logic stops iterating
when annual_rate_for(on: cursor) returns blank but later code still sets
closed_on and settlement_amount, allowing auto-settlement with incomplete rates;
update the logic in the methods that use cursor/period_end (the loop that
computes days_in_step and calls annual_rate_for) so that if annual_rate_for
returns nil/blank you do not persist closed_on or settlement_amount and instead
mark the lot as needing review (e.g., set requires_rate_review or return/raise
to abort settlement); ensure the same guard is applied in the other affected
block (around the code that sets closed_on/settlement_amount at lines ~168-189)
so no matured lot is auto-closed when rates are unresolved.
- Around line 303-312: The code is currently coercing a missing CPI
(inflation_component from inflation_snapshot_for(on:)) to 0 and computing an
annual_rate_decimal, which hides a missing CPI; change the logic in the method
that calls inflation_snapshot_for(on:) (and touches inflation_component,
inflation_margin and inflation_rate_assumption) so that when
inflation_snapshot[:inflation_component] is nil and there is no
inflation_rate_assumption provided you return annual_rate_decimal: nil (do not
treat nil as 0); only perform the numeric sum/division when inflation_component
is present or an explicit inflation_rate_assumption exists (i.e. use the
explicit assumption or the snapshot value, otherwise bail out), and remove the
fallback that coerces nil to 0 in the calculation currently using
((inflation_component || 0.to_d) + margin_component) / 100.
- Around line 80-100: The loop uses cursor + 1.year as the anniversary anchor
but should use the bond's issue_date to compute annual anniversaries (while
still starting accrual at purchased_on). Fix by computing an anniversary
boundary for the current cursor using issue_date (e.g. compute years_since_issue
= ((cursor - issue_date) / 1.year).floor and next_anniversary = issue_date +
(years_since_issue + 1).years), then replace uses of "cursor + 1.year" (and
related calculations for days_in_year and the full-year branch) with
next_anniversary and the corresponding anniversary_start; keep value accrual
starting from purchased_on and keep calling annual_rate_for(on: cursor) for the
rate value but use the issue_date-derived next_anniversary for
rate-reset/maturity boundary logic in the loop (apply same change to the other
similar blocks referenced).

In `@app/views/bond_lots/_form.html.erb`:
- Around line 31-36: The form's number_field for :units currently allows 0 but
the BondLot model validation rejects values <= 0; update the form.number_field
(the field with data-bond-lot-form-target "inflationInput" inside the
inflationFields block) to use a min value greater than 0 (e.g., min: 0.01 or
whatever smallest valid unit your model expects) and keep the step consistent so
the client-side constraint matches the BondLot validation.

In `@app/views/pages/dashboard/_bond_summary.html.erb`:
- Line 33: top_lots is being selected by comparing raw estimated_current_value
across mixed currencies which yields incorrect ordering; convert each lot's
estimated_current_value into the family currency before sorting (the same
normalization used for summary totals) by using the existing exchange conversion
path (e.g., use Current.family.currency / ExchangeRate lookup or the helper used
elsewhere) when computing the sort key for open_lots so the .sort_by block
compares values all in Current.family.currency, then take .first(5).

---

Nitpick comments:
In `@app/views/settings/hostings/_gus_sdp_settings.html.erb`:
- Around line 93-98: Consolidate the four GusInflationRate queries by computing
the stats once (count, min year, max year) in the controller and passing them to
the view (e.g. `@gus_stats`), then destructure those values in the view and use
count to decide whether to render the stored_range; update references to
GusInflationRate.count, .exists?, .minimum(:year), and .maximum(:year) to use
the precomputed values (count, min_year, max_year) instead.
- Around line 50-52: Replace the raw Tailwind color class "text-secondary-600"
with the design token "text-secondary" in the ERB block that renders the
configured_via_env message (the div containing <%= t(".configured_via_env") %>),
so the div's class list uses the project design token for text color instead of
the raw color variant.
🪄 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: 9ec3a114-38f4-4a96-a5f9-64ce4a9bc832

📥 Commits

Reviewing files that changed from the base of the PR and between 75eb8b1 and 159e9c8.

📒 Files selected for processing (14)
  • app/controllers/accounts_controller.rb
  • app/controllers/bond_lots_controller.rb
  • app/controllers/settings/hostings_controller.rb
  • app/models/bond_lot.rb
  • app/views/bond_lots/_form.html.erb
  • app/views/pages/dashboard/_bond_summary.html.erb
  • app/views/settings/hostings/_gus_sdp_settings.html.erb
  • config/locales/views/bonds/en.yml
  • db/migrate/20260331120000_create_bond_lots.rb
  • db/migrate/20260331133000_add_purchase_fields_to_bond_lots.rb
  • db/migrate/20260331153000_reconfigure_bond_products_for_eod_rod.rb
  • test/controllers/accounts_controller_test.rb
  • test/controllers/pages_controller_test.rb
  • test/controllers/settings/hostings_controller_test.rb
✅ Files skipped from review due to trivial changes (2)
  • test/controllers/accounts_controller_test.rb
  • db/migrate/20260331120000_create_bond_lots.rb
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/controllers/settings/hostings_controller_test.rb
  • db/migrate/20260331133000_add_purchase_fields_to_bond_lots.rb
  • config/locales/views/bonds/en.yml

Comment thread app/controllers/bond_lots_controller.rb Outdated
Comment thread app/models/bond_lot.rb
Comment thread app/models/bond_lot.rb
Comment thread app/models/bond_lot.rb Outdated
Comment thread app/views/bond_lots/_form.html.erb Outdated
Comment thread app/views/pages/dashboard/_bond_summary.html.erb Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 75 out of 75 changed files in this pull request and generated 7 comments.

Comment thread app/controllers/accounts_controller.rb Outdated
Comment thread app/jobs/settle_matured_bond_lots_job.rb
Comment thread app/views/pages/dashboard/_bond_summary.html.erb Outdated
Comment thread app/views/pages/dashboard/_bond_summary.html.erb Outdated
Comment thread app/models/gus_inflation_rate.rb Outdated
Comment thread app/models/bond_lot.rb Outdated
Comment thread app/views/pages/dashboard/_bond_summary.html.erb Outdated
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
app/controllers/pages_controller.rb (1)

359-363: Consolidate query loads to avoid duplicate database queries.

Line 362-363 triggers two separate queries on pending_lots: a COUNT query via .count and a SELECT query via .includes(...).map(...). Load the records once with eager loading and reuse:

♻️ Suggested optimization
 def show_bond_rate_review_notice!
   return if session[:bond_rate_review_prompted]

-  pending_lots = BondLot.needs_rate_review.joins(bond: :account).merge(Account.accessible_by(Current.user))
-  return if pending_lots.empty?
+  lots = BondLot.needs_rate_review
+           .joins(bond: :account)
+           .merge(Account.accessible_by(Current.user))
+           .includes(bond: :account)
+           .to_a
+  return if lots.empty?

-  account_names = pending_lots.includes(bond: :account).map { |lot| lot.account.name }.uniq.first(3).join(", ")
-  flash.now[:notice] = t("pages.dashboard.bond_rate_review_notice", count: pending_lots.count, accounts: account_names)
+  account_names = lots.map { |lot| lot.account.name }.uniq.first(3).join(", ")
+  flash.now[:notice] = t("pages.dashboard.bond_rate_review_notice", count: lots.size, accounts: account_names)
   session[:bond_rate_review_prompted] = true
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/pages_controller.rb` around lines 359 - 363, The code
triggers two DB queries by enumerating pending_lots twice; change the logic to
eager-load and materialize the relation once (e.g., call load or to_a on the
relation returned by BondLot.needs_rate_review.joins(bond:
:account).merge(Account.accessible_by(Current.user))) and then reuse that in
calculating count and account_names (use the loaded collection for .count and
.includes(...).map(...) logic) so the records are fetched only once before
building flash.now[:notice] in pages_controller (referencing pending_lots,
account_names, and the flash assignment).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/controllers/pages_controller.rb`:
- Around line 359-363: The code triggers two DB queries by enumerating
pending_lots twice; change the logic to eager-load and materialize the relation
once (e.g., call load or to_a on the relation returned by
BondLot.needs_rate_review.joins(bond:
:account).merge(Account.accessible_by(Current.user))) and then reuse that in
calculating count and account_names (use the loaded collection for .count and
.includes(...).map(...) logic) so the records are fetched only once before
building flash.now[:notice] in pages_controller (referencing pending_lots,
account_names, and the flash assignment).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0b2a19b-8885-490c-ad96-c32fe05e657d

📥 Commits

Reviewing files that changed from the base of the PR and between 159e9c8 and 5e270af.

📒 Files selected for processing (3)
  • app/controllers/pages_controller.rb
  • app/models/bond_lot.rb
  • config/locales/views/bonds/en.yml
✅ Files skipped from review due to trivial changes (1)
  • config/locales/views/bonds/en.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/models/bond_lot.rb

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (4)
app/controllers/settings/hostings_controller.rb (2)

175-190: Consider server-side year bounds validation.

The view enforces min: 1990 and max: Date.current.year on the form fields, but these bounds aren't validated server-side. An admin could bypass the form and submit extreme values. Since this is admin-only and the external API would return empty data for invalid years, this is low risk—but adding server-side validation would make the API more defensive.

Optional: Add server-side year bounds
     start_year = import_params[:gus_inflation_start_year].presence&.to_i || (Date.current.year - 20)
     end_year = import_params[:gus_inflation_end_year].presence&.to_i || (Date.current.year - 1)
+    
+    min_year = 1990
+    max_year = Date.current.year
+    start_year = start_year.clamp(min_year, max_year)
+    end_year = end_year.clamp(min_year, max_year)

     if start_year > end_year
       return redirect_to settings_hosting_path, alert: t(".invalid_import_range")
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/settings/hostings_controller.rb` around lines 175 - 190, Add
server-side validation in import_gus_inflation_rates to enforce the same year
bounds as the form: parse import_params[:gus_inflation_start_year] and
:gus_inflation_end_year to integers (fall back to defaults as currently done),
then ensure start_year and end_year are within 1990..Date.current.year (and
start_year <= end_year); if validation fails, redirect_to settings_hosting_path
with an appropriate alert (reuse t(".invalid_import_range") or add a new i18n
key) instead of enqueuing ImportGusInflationRatesJob. Use the existing
import_params and ImportGusInflationRatesJob identifiers to locate the code.

210-217: Optional: Consolidate into a single query.

This makes up to 3 database queries. Since it's on an admin-only page, performance is acceptable, but could be simplified.

Optional: Single query approach
     def set_gus_stats
-      gus_count = GusInflationRate.count
-      `@gus_stats` = {
-        count: gus_count,
-        min_year: gus_count > 0 ? GusInflationRate.minimum(:year) : nil,
-        max_year: gus_count > 0 ? GusInflationRate.maximum(:year) : nil
-      }
+      stats = GusInflationRate.pick(Arel.sql("COUNT(*), MIN(year), MAX(year)"))
+      count, min_year, max_year = stats || [0, nil, nil]
+      `@gus_stats` = { count: count, min_year: min_year, max_year: max_year }
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/settings/hostings_controller.rb` around lines 210 - 217, The
method set_gus_stats currently issues up to three DB queries; replace these with
a single aggregate query on GusInflationRate that selects COUNT(*), MIN(year),
and MAX(year) (e.g., using GusInflationRate.select('COUNT(*) AS count, MIN(year)
AS min_year, MAX(year) AS max_year').take), then build `@gus_stats` from the
returned aggregate row (convert count to integer and set min_year/max_year to
nil when count is zero). Update set_gus_stats to use that single query and map
the aggregate fields into the same keys: count, min_year, max_year.
app/models/bond_lot.rb (1)

429-465: Consider inheriting fixed-rate params for reinvestment lots.

Lines 448-450 set interest_rate, rate_type, and coupon_frequency to nil for non-inflation-linked reinvestments. Since these are fixed-rate bonds, the replacement lot should likely inherit the parent's rates to enable accurate valuation until the user reviews.

💡 Suggested change
           subtype: subtype,
-          interest_rate: inflation_linked? ? nil : interest_rate,
-          rate_type: inflation_linked? ? nil : rate_type,
-          coupon_frequency: inflation_linked? ? nil : coupon_frequency,
+          interest_rate: interest_rate,  # Inherit for fixed-rate continuity
+          rate_type: rate_type,
+          coupon_frequency: coupon_frequency,

This allows fixed-rate reinvestments to have valid rates immediately while still being flagged for review via requires_rate_review: true.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/bond_lot.rb` around lines 429 - 465, In create_reinvestment_lot!
ensure non-inflation-linked replacement lots inherit the parent's fixed-rate
parameters instead of nil: when building replacement_lot (method
create_reinvestment_lot! in BondLot/bond_lot.rb) set interest_rate, rate_type
and coupon_frequency to the current lot's values for non-inflation-linked
reinvestments (i.e. mirror interest_rate, rate_type, coupon_frequency from
self), while keeping requires_rate_review: true so users can still review/update
the inherited rates.
app/controllers/bond_lots_controller.rb (1)

74-79: Simplify destroy: let Entry cascade handle BondLot deletion.

Per the Entry model (has_one :bond_lot, dependent: :destroy), destroying the entry will automatically destroy the associated bond_lot. The current order destroys the bond_lot first, then the entry—but since the bond_lot is already gone, the cascade is a no-op. This works but is confusing and could break if the association changes.

♻️ Suggested simplification
     entry = `@bond_lot.entry`

     ActiveRecord::Base.transaction do
-      `@bond_lot.destroy`!
-      entry&.destroy!
+      entry&.destroy! || `@bond_lot.destroy`!
     end

Or more explicitly, destroy only the entry when it exists (it cascades), otherwise destroy the orphan lot:

     entry = `@bond_lot.entry`

     ActiveRecord::Base.transaction do
+      # Entry has `dependent: :destroy` on bond_lot, so destroying entry cascades.
+      # If no entry exists, destroy the lot directly.
-      `@bond_lot.destroy`!
-      entry&.destroy!
+      if entry
+        entry.destroy!
+      else
+        `@bond_lot.destroy`!
+      end
     end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/bond_lots_controller.rb` around lines 74 - 79, The controller
currently destroys `@bond_lot` before its Entry, which is confusing given Entry
has_one :bond_lot, dependent: :destroy; change the transaction to destroy the
Entry first when present so the cascade removes the BondLot (and only destroy
`@bond_lot` directly if there is no associated entry). Update the logic in
bond_lots_controller (the block using `@bond_lot` and entry) to: within
ActiveRecord::Base.transaction, if entry.present? call entry.destroy! else call
`@bond_lot.destroy`! so the dependent: :destroy cascade is relied upon.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/models/bond_lot.rb`:
- Around line 375-386: The method apply_product_defaults unconditionally assigns
self.term_months = defaults[:term_months], overwriting user-provided values;
change this to only set the default when term_months is nil/unset (e.g. use ||=
or a nil-check) so apply_product_defaults behaves like
Bond#apply_subtype_defaults and preserves existing term_months; update the
apply_product_defaults method (referenced by the method name and
Bond::PRODUCT_DEFAULTS/subtype and the term_months attribute) accordingly.

---

Nitpick comments:
In `@app/controllers/bond_lots_controller.rb`:
- Around line 74-79: The controller currently destroys `@bond_lot` before its
Entry, which is confusing given Entry has_one :bond_lot, dependent: :destroy;
change the transaction to destroy the Entry first when present so the cascade
removes the BondLot (and only destroy `@bond_lot` directly if there is no
associated entry). Update the logic in bond_lots_controller (the block using
`@bond_lot` and entry) to: within ActiveRecord::Base.transaction, if
entry.present? call entry.destroy! else call `@bond_lot.destroy`! so the
dependent: :destroy cascade is relied upon.

In `@app/controllers/settings/hostings_controller.rb`:
- Around line 175-190: Add server-side validation in import_gus_inflation_rates
to enforce the same year bounds as the form: parse
import_params[:gus_inflation_start_year] and :gus_inflation_end_year to integers
(fall back to defaults as currently done), then ensure start_year and end_year
are within 1990..Date.current.year (and start_year <= end_year); if validation
fails, redirect_to settings_hosting_path with an appropriate alert (reuse
t(".invalid_import_range") or add a new i18n key) instead of enqueuing
ImportGusInflationRatesJob. Use the existing import_params and
ImportGusInflationRatesJob identifiers to locate the code.
- Around line 210-217: The method set_gus_stats currently issues up to three DB
queries; replace these with a single aggregate query on GusInflationRate that
selects COUNT(*), MIN(year), and MAX(year) (e.g., using
GusInflationRate.select('COUNT(*) AS count, MIN(year) AS min_year, MAX(year) AS
max_year').take), then build `@gus_stats` from the returned aggregate row (convert
count to integer and set min_year/max_year to nil when count is zero). Update
set_gus_stats to use that single query and map the aggregate fields into the
same keys: count, min_year, max_year.

In `@app/models/bond_lot.rb`:
- Around line 429-465: In create_reinvestment_lot! ensure non-inflation-linked
replacement lots inherit the parent's fixed-rate parameters instead of nil: when
building replacement_lot (method create_reinvestment_lot! in
BondLot/bond_lot.rb) set interest_rate, rate_type and coupon_frequency to the
current lot's values for non-inflation-linked reinvestments (i.e. mirror
interest_rate, rate_type, coupon_frequency from self), while keeping
requires_rate_review: true so users can still review/update the inherited rates.
🪄 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: 05c1d905-adb7-4381-b5ec-32ed422d2a41

📥 Commits

Reviewing files that changed from the base of the PR and between 5e270af and da8f593.

📒 Files selected for processing (19)
  • app/controllers/bond_lots_controller.rb
  • app/controllers/pages_controller.rb
  • app/controllers/settings/hostings_controller.rb
  • app/jobs/import_gus_inflation_rates_job.rb
  • app/jobs/settle_matured_bond_lots_job.rb
  • app/models/bond.rb
  • app/models/bond_lot.rb
  • app/models/gus_inflation_rate.rb
  • app/views/bond_lots/_form.html.erb
  • app/views/bond_lots/new.html.erb
  • app/views/bonds/_form.html.erb
  • app/views/bonds/edit.html.erb
  • app/views/bonds/new.html.erb
  • app/views/pages/dashboard/_bond_summary.html.erb
  • app/views/settings/hostings/_gus_sdp_settings.html.erb
  • config/locales/views/pages/en.yml
  • db/migrate/20260401120000_add_settlement_composite_index_to_bond_lots.rb
  • db/schema.rb
  • test/controllers/pages_controller_test.rb
✅ Files skipped from review due to trivial changes (5)
  • app/views/bond_lots/new.html.erb
  • db/migrate/20260401120000_add_settlement_composite_index_to_bond_lots.rb
  • config/locales/views/pages/en.yml
  • app/views/bonds/_form.html.erb
  • app/views/bond_lots/_form.html.erb
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/views/bonds/edit.html.erb
  • app/views/bonds/new.html.erb
  • test/controllers/pages_controller_test.rb
  • app/jobs/import_gus_inflation_rates_job.rb
  • app/jobs/settle_matured_bond_lots_job.rb
  • app/models/gus_inflation_rate.rb
  • db/schema.rb

Comment thread app/models/bond_lot.rb Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 75 out of 75 changed files in this pull request and generated 4 comments.

Comment thread app/views/settings/hostings/_gus_sdp_settings.html.erb Outdated
Comment thread app/views/bond_lots/_form.html.erb Outdated
Comment thread app/controllers/pages_controller.rb Outdated
Comment thread app/controllers/pages_controller.rb Outdated
@UberDudePL UberDudePL requested a review from Copilot April 1, 2026 08:06
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 75 out of 75 changed files in this pull request and generated 3 comments.

Comment thread app/models/gus_inflation_rate.rb Outdated
Comment thread db/migrate/20260331133000_add_purchase_fields_to_bond_lots.rb Outdated
Comment thread app/models/bond_lot.rb Outdated
@UberDudePL
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@UberDudePL
Copy link
Copy Markdown
Author

@coderabbitai resume

@UberDudePL UberDudePL requested a review from Copilot April 1, 2026 08:27
Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (5)
db/schema.rb (1)

1603-1604: Consider adding ON DELETE behavior for referential integrity.

While Rails handles deletion via dependent: :destroy, adding database-level constraints ensures integrity if records are ever deleted outside Rails (admin tools, raw SQL, data migrations):

♻️ Suggested improvement
-  add_foreign_key "bond_lots", "bonds"
-  add_foreign_key "bond_lots", "entries"
+  add_foreign_key "bond_lots", "bonds", on_delete: :cascade
+  add_foreign_key "bond_lots", "entries", on_delete: :nullify

This aligns with similar patterns in this schema (e.g., balances -> accounts uses on_delete: :cascade, accounts -> users uses on_delete: :nullify).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@db/schema.rb` around lines 1603 - 1604, The schema adds foreign keys for
bond_lots -> bonds and bond_lots -> entries without specifying ON DELETE
behavior; update the two add_foreign_key calls for "bond_lots", "bonds" and
"bond_lots", "entries" to include an appropriate on_delete option (e.g.,
on_delete: :cascade or on_delete: :nullify) to enforce referential integrity at
the DB level and match the project's existing patterns. Ensure you choose the
correct behavior for each relationship and run migrations/schema dump to persist
the change.
app/jobs/settle_matured_bond_lots_job.rb (1)

5-14: Consider eager loading to avoid N+1 in error logging.

The query chain accesses lot.account.id in the rescue block, but account is delegated through bond. If multiple lots fail, each failure triggers a separate query. While this only affects the error path, adding .includes(bond: :account) would be more robust.

♻️ Suggested optimization
   def perform(on: Date.current)
     BondLot.open
       .where(auto_close_on_maturity: true)
       .where("maturity_date <= ?", on)
+      .includes(bond: :account)
       .find_each do |lot|
         lot.settle_if_matured!(on:)
       rescue StandardError => e
         Rails.logger.error(
           "SettleMaturedBondLotsJob failed for lot_id=#{lot.id} account_id=#{lot.account.id}: #{e.class}: #{e.message}"
         )
       end
   end

As per coding guidelines: Optimize database queries with proper indexes to prevent N+1 queries using includes/joins.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/jobs/settle_matured_bond_lots_job.rb` around lines 5 - 14, The rescue
block in the BondLot.open ... .find_each loop dereferences lot.account (via
bond) causing an N+1 on the error path; update the query that builds the
iterator (the chain starting at BondLot.open.where(...).where("maturity_date <=
?", on).find_each) to eager-load the associated bond and account (e.g. add
includes(bond: :account)) so lot.settle_if_matured!(on:) and the rescue logging
can access lot.account.id without triggering extra queries.
app/controllers/bond_lots_controller.rb (1)

74-82: Clarify destroy dependency chain.

The logic destroys entry when present (relying on Entry has_one :bond_lot, dependent: :destroy to cascade), or destroys the lot directly if no entry exists. This is correct but subtle—a brief comment would help future maintainers understand the cascade.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/bond_lots_controller.rb` around lines 74 - 82, Add a brief
in-line comment above the transaction explaining the destroy dependency: note
that when entry is present we call entry.destroy! and rely on Entry's has_one
:bond_lot, dependent: :destroy association to cascade and remove the associated
`@bond_lot`, otherwise we explicitly destroy `@bond_lot` when no entry exists; place
this comment near the entry / `@bond_lot` destruction logic so future maintainers
understand the cascade behavior.
app/views/pages/dashboard/_bond_summary.html.erb (1)

44-65: Consider extracting rate display logic to a helper or presenter.

The nested conditionals for rate_text and rate_meta are quite dense. While functionally correct, this logic could be extracted to a view helper or presenter for better testability and readability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/views/pages/dashboard/_bond_summary.html.erb` around lines 44 - 65,
Extract the nested display logic for rate_text and rate_meta into a view helper
or presenter (e.g., a BondRatePresenter or helper methods rate_text_for(lot) and
rate_meta_for(lot)) so the template only calls those methods; move the
conditional branches that reference lot.inflation_linked?,
lot.requires_rate_review?, lot.current_rate_percent, lot.interest_rate,
lot.current_inflation_source, lot.current_inflation_component_percent,
lot.current_margin_percent, lot.current_inflation_indicator_id, lot.rate_type
and lot.coupon_frequency into the new presenter/helper, implement tests for the
presenter methods, and replace the complex ERB block with simple calls like
rate_text_for(lot) and rate_meta_for(lot).
app/models/bond_lot.rb (1)

84-109: Consider extracting duplicate anniversary calculation logic.

The anniversary boundary calculation (lines 88-91 and 230-233) is duplicated between estimated_current_value and capitalization_history. Consider extracting to a private helper method for DRY compliance.

♻️ Suggested extraction
def anniversary_boundaries_for(cursor:, issue_base:)
  years_since_issue = 0
  years_since_issue += 1 while issue_base + years_since_issue.years <= cursor
  next_anniversary = issue_base + years_since_issue.years
  anniversary_start = issue_base + (years_since_issue - 1).years
  [next_anniversary, anniversary_start]
end

Also applies to: 226-272

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/bond_lot.rb` around lines 84 - 109, Duplicate anniversary boundary
calculation appears in estimated_current_value and capitalization_history;
extract it into a private helper (e.g., anniversary_boundaries_for(cursor:,
issue_base:)) that returns [next_anniversary, anniversary_start], replace the
repeated block in both methods with a call to that helper, and ensure callers
still compute days_in_step, days_in_year and use
next_anniversary/anniversary_start as before; keep the helper private in the
BondLot class and update any tests if they reference the original logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/bond_lots_controller.rb`:
- Around line 129-137: The code calls entryable.extra.merge(...) which will
raise if entryable.extra is nil; in bond_lots_controller (around the
entryable_attributes block) guard against nil by coercing entryable.extra to a
hash before merging (e.g., use entry.entryable.extra.to_h or
entry.entryable.extra || {}), then merge the bond_subtype, bond_term_months and
bond_interest_rate into that safe hash so bond_lot.entry.entryable.extra cannot
cause a NoMethodError.

---

Nitpick comments:
In `@app/controllers/bond_lots_controller.rb`:
- Around line 74-82: Add a brief in-line comment above the transaction
explaining the destroy dependency: note that when entry is present we call
entry.destroy! and rely on Entry's has_one :bond_lot, dependent: :destroy
association to cascade and remove the associated `@bond_lot`, otherwise we
explicitly destroy `@bond_lot` when no entry exists; place this comment near the
entry / `@bond_lot` destruction logic so future maintainers understand the cascade
behavior.

In `@app/jobs/settle_matured_bond_lots_job.rb`:
- Around line 5-14: The rescue block in the BondLot.open ... .find_each loop
dereferences lot.account (via bond) causing an N+1 on the error path; update the
query that builds the iterator (the chain starting at
BondLot.open.where(...).where("maturity_date <= ?", on).find_each) to eager-load
the associated bond and account (e.g. add includes(bond: :account)) so
lot.settle_if_matured!(on:) and the rescue logging can access lot.account.id
without triggering extra queries.

In `@app/models/bond_lot.rb`:
- Around line 84-109: Duplicate anniversary boundary calculation appears in
estimated_current_value and capitalization_history; extract it into a private
helper (e.g., anniversary_boundaries_for(cursor:, issue_base:)) that returns
[next_anniversary, anniversary_start], replace the repeated block in both
methods with a call to that helper, and ensure callers still compute
days_in_step, days_in_year and use next_anniversary/anniversary_start as before;
keep the helper private in the BondLot class and update any tests if they
reference the original logic.

In `@app/views/pages/dashboard/_bond_summary.html.erb`:
- Around line 44-65: Extract the nested display logic for rate_text and
rate_meta into a view helper or presenter (e.g., a BondRatePresenter or helper
methods rate_text_for(lot) and rate_meta_for(lot)) so the template only calls
those methods; move the conditional branches that reference
lot.inflation_linked?, lot.requires_rate_review?, lot.current_rate_percent,
lot.interest_rate, lot.current_inflation_source,
lot.current_inflation_component_percent, lot.current_margin_percent,
lot.current_inflation_indicator_id, lot.rate_type and lot.coupon_frequency into
the new presenter/helper, implement tests for the presenter methods, and replace
the complex ERB block with simple calls like rate_text_for(lot) and
rate_meta_for(lot).

In `@db/schema.rb`:
- Around line 1603-1604: The schema adds foreign keys for bond_lots -> bonds and
bond_lots -> entries without specifying ON DELETE behavior; update the two
add_foreign_key calls for "bond_lots", "bonds" and "bond_lots", "entries" to
include an appropriate on_delete option (e.g., on_delete: :cascade or on_delete:
:nullify) to enforce referential integrity at the DB level and match the
project's existing patterns. Ensure you choose the correct behavior for each
relationship and run migrations/schema dump to persist the change.
🪄 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: ded27873-f071-4aea-87ea-427f8b8e9897

📥 Commits

Reviewing files that changed from the base of the PR and between 5e270af and cb2c8e5.

📒 Files selected for processing (20)
  • app/controllers/bond_lots_controller.rb
  • app/controllers/pages_controller.rb
  • app/controllers/settings/hostings_controller.rb
  • app/javascript/controllers/bond_lot_form_controller.js
  • app/jobs/import_gus_inflation_rates_job.rb
  • app/jobs/settle_matured_bond_lots_job.rb
  • app/models/bond.rb
  • app/models/bond_lot.rb
  • app/models/gus_inflation_rate.rb
  • app/views/bond_lots/_form.html.erb
  • app/views/bond_lots/new.html.erb
  • app/views/bonds/_form.html.erb
  • app/views/bonds/edit.html.erb
  • app/views/bonds/new.html.erb
  • app/views/pages/dashboard/_bond_summary.html.erb
  • app/views/settings/hostings/_gus_sdp_settings.html.erb
  • config/locales/views/pages/en.yml
  • db/migrate/20260401120000_add_settlement_composite_index_to_bond_lots.rb
  • db/schema.rb
  • test/controllers/pages_controller_test.rb
✅ Files skipped from review due to trivial changes (8)
  • app/views/bonds/edit.html.erb
  • app/views/bonds/new.html.erb
  • app/views/bond_lots/new.html.erb
  • config/locales/views/pages/en.yml
  • app/views/bond_lots/_form.html.erb
  • app/views/bonds/_form.html.erb
  • app/jobs/import_gus_inflation_rates_job.rb
  • app/models/gus_inflation_rate.rb
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/controllers/pages_controller_test.rb
  • db/migrate/20260401120000_add_settlement_composite_index_to_bond_lots.rb
  • app/javascript/controllers/bond_lot_form_controller.js
  • app/controllers/pages_controller.rb

Comment thread app/controllers/bond_lots_controller.rb Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 1, 2026

✅ Actions performed

Reviews resumed.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 102 out of 103 changed files in this pull request and generated 2 comments.

Comment thread app/components/UI/dashboard/bond_summary_row.rb
Comment thread app/components/UI/dashboard/bond_summary_row.rb Outdated
Copy link
Copy Markdown

@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 (1)
test/models/bond_lot_test.rb (1)

598-614: Lock the non-par pricing path in with an output assertion.

This test only proves the 950 purchase price survives validation. It still passes if coupon or maturity math keeps using amount instead of the 1000 face value, so the new non-par path is not covered end-to-end.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/models/bond_lot_test.rb` around lines 598 - 614, The test only checks
the stored amount, so lock the non-par pricing path by asserting the derived
face-value used for coupon/maturity calculations: call
BondLot#derive_amount_from_units (or invoke the public method that computes the
lot's derived face value) and assert it equals units * nominal_per_unit (10 *
100 = 1000), and/or assert any coupon/maturity-calculation helper on BondLot
that uses the derived amount returns values based on 1000 rather than 950;
update the test to include this additional assertion using the lot instance's
units, nominal_per_unit, and derive_amount_from_units (or the relevant
coupon/maturity method).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/models/bond_lot.rb`:
- Around line 174-209: The method estimated_current_value (and other places that
use amount as the coupon/redemption base) wrongly uses amount as the cash-flow
principal; add a private helper cashflow_principal (suggested implementation:
units.present? && nominal_per_unit.present? ? units.to_d * nominal_per_unit.to_d
: amount.to_d) and replace uses of amount.to_d when calculating coupon
accruals/redemption/coupon reinvestment (e.g., in estimated_current_value and
the other similar blocks referenced) with cashflow_principal, while keeping
amount for cost/return/tax basis logic unchanged.
- Around line 46-47: The two conversions that set converted_value and
converted_return are forcing a 1:1 FX by passing custom_rate: 1; remove the
custom_rate override so Money.new(...,
account.currency).exchange_to(family_currency) uses the real rate lookup
(ExchangeRate.find_or_fetch_rate) and returns correct normalized amounts; update
the calls that construct converted_value and converted_return to drop the
custom_rate parameter and keep the rest of the logic the same.

In `@app/models/bond/inflation_provider.rb`:
- Around line 3-9: The current DEFAULT_PROVIDER_BY_LOCALE maps generic "en" to
"us_bls", which causes blank product_code flows to silently default to US CPI;
remove the "en" => "us_bls" entry (keep explicit "us" and "en-us" mappings only)
and update the code that selects the provider when product_code is blank to
treat a missing mapping as nil (no default) rather than falling back to US;
ensure the lookup that uses DEFAULT_PROVIDER_BY_LOCALE and the product_code
branch return nil/unset for generic "en" so manual/custom lots outside the US do
not get assigned "us_bls".

---

Nitpick comments:
In `@test/models/bond_lot_test.rb`:
- Around line 598-614: The test only checks the stored amount, so lock the
non-par pricing path by asserting the derived face-value used for
coupon/maturity calculations: call BondLot#derive_amount_from_units (or invoke
the public method that computes the lot's derived face value) and assert it
equals units * nominal_per_unit (10 * 100 = 1000), and/or assert any
coupon/maturity-calculation helper on BondLot that uses the derived amount
returns values based on 1000 rather than 950; update the test to include this
additional assertion using the lot instance's units, nominal_per_unit, and
derive_amount_from_units (or the relevant coupon/maturity method).
🪄 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: afdfb9a6-1264-4610-bb14-2cfcbec9a7a2

📥 Commits

Reviewing files that changed from the base of the PR and between 6e1ca73 and 32e306d.

📒 Files selected for processing (15)
  • app/components/UI/account/chart.rb
  • app/models/account.rb
  • app/models/balance/sync_cache.rb
  • app/models/binance_account/usd_converter.rb
  • app/models/bond/inflation_provider.rb
  • app/models/bond_lot.rb
  • app/models/gus_inflation_rate.rb
  • app/models/purchase_holding_presenter.rb
  • compose.example.yml
  • config/locales/views/accounts/en.yml
  • config/routes.rb
  • db/migrate/20260407170000_fix_bond_lots_non_inflation_check_constraint.rb
  • test/models/bond/inflation_provider_test.rb
  • test/models/bond_lot_test.rb
  • test/models/gus_inflation_rate_test.rb
✅ Files skipped from review due to trivial changes (2)
  • config/locales/views/accounts/en.yml
  • compose.example.yml
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/components/UI/account/chart.rb
  • app/models/account.rb
  • config/routes.rb
  • app/models/balance/sync_cache.rb

Comment thread app/models/bond_lot.rb Outdated
Comment thread app/models/bond_lot.rb
Comment thread app/models/bond/inflation_provider.rb
- Add cashflow_principal helper (units * nominal_per_unit fallback to amount)
  and use it in estimated_current_value, coupon_amount_per_period,
  capitalization_history — keeps purchase price as cost basis only
- Remove custom_rate: 1 override in dashboard_summary; use real FX lookup
- Remove 'en' => 'us_bls' from DEFAULT_PROVIDER_BY_LOCALE; generic English
  no longer silently defaults to US BLS
- Memoize BondSummaryRow#projected_total_return? with defined? guard
- Add nil fallback for CPI reference date in inflation_data_unavailable
- Add cashflow_principal assertion to non-par pricing test
@UberDudePL
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 102 out of 103 changed files in this pull request and generated 1 comment.

Comment on lines +30 to +34
<%= render Settings::Hostings::InflationImportSettingsComponent.new(
inflation_import_enabled_effective: Setting.inflation_import_enabled_effective,
env_inflation_import_enabled: ENV["INFLATION_IMPORT_ENABLED"].presence || ENV["GUS_INFLATION_IMPORT_ENABLED"],
last_import_at: Setting.inflation_last_import_at,
last_import_range: Setting.inflation_last_import_range,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

env_inflation_import_enabled uses ENV["GUS_INFLATION_IMPORT_ENABLED"] without .presence. If the legacy env var is defined but empty (common with dotenv-style GUS_INFLATION_IMPORT_ENABLED=), this will still be treated as “present” and will incorrectly lock the UI toggle. Use .presence on the legacy var as well so the lock behavior matches Setting.inflation_import_enabled_effective.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Corrected in commit: (d21122e)

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
app/components/UI/dashboard/bond_summary_row.rb (1)

90-121: Memoize and reuse inflation source to avoid repeated source resolution per row.

Line 90 and Line 97-121 can resolve inflation source multiple times in the same render path. Cache once and branch on that local value for cheaper dashboard rendering with many lots.

♻️ Proposed refactor
     def inflation_linked_rate_meta
       return t("bonds.purchase_holding.pending_review") if lot.requires_rate_review?
@@
       inflation = helpers.number_to_percentage(inflation_component.to_d, precision: 3)
       margin = helpers.number_to_percentage(margin_component.to_d, precision: 3)
+      source = current_inflation_source_key
 
-      if lot.gus_inflation_source?(allow_import: false)
+      if source == "gus_sdp"
         t(
           "bonds.purchase_holding.inflation_meta_gus",
           inflation: inflation,
           margin: margin,
           indicator: lot.current_inflation_indicator_id
         )
-      elsif current_inflation_source_key.blank? || current_inflation_source_key == "manual"
+      elsif source.blank? || source == "manual"
         t(
           "bonds.purchase_holding.inflation_meta_manual",
           inflation: inflation,
           margin: margin
         )
@@
     def current_inflation_source_key
-      lot.current_inflation_source(allow_import: false).to_s.presence
+      `@current_inflation_source_key` ||= lot.current_inflation_source(allow_import: false).to_s.presence
     end

Based on learnings: GUS provider detection must match "gus_sdp" when branching on current_inflation_source.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/UI/dashboard/bond_summary_row.rb` around lines 90 - 121,
Compute and cache the inflation source once per row instead of calling
lot.current_inflation_source(allow_import: false) multiple times: introduce a
memoized/local variable (e.g., `@inflation_source` or inflation_source) used by
current_inflation_source_key and localized_inflation_provider so the source is
resolved only once; update the branching that currently checks
gus_inflation_source? to compare against the actual provider key "gus_sdp" when
deciding the GUS branch; ensure current_inflation_source_key uses the memoized
value and localized_inflation_provider returns the translated provider or
fallback based on that same cached value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/components/UI/dashboard/bond_summary_row.rb`:
- Around line 90-121: Compute and cache the inflation source once per row
instead of calling lot.current_inflation_source(allow_import: false) multiple
times: introduce a memoized/local variable (e.g., `@inflation_source` or
inflation_source) used by current_inflation_source_key and
localized_inflation_provider so the source is resolved only once; update the
branching that currently checks gus_inflation_source? to compare against the
actual provider key "gus_sdp" when deciding the GUS branch; ensure
current_inflation_source_key uses the memoized value and
localized_inflation_provider returns the translated provider or fallback based
on that same cached value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb2a9eec-0479-41c0-86fa-198f83a37b89

📥 Commits

Reviewing files that changed from the base of the PR and between 32e306d and 4ef188f.

📒 Files selected for processing (4)
  • app/components/UI/dashboard/bond_summary_row.rb
  • app/models/bond/inflation_provider.rb
  • app/models/bond_lot.rb
  • test/models/bond_lot_test.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/models/bond_lot.rb

- Use .presence for GUS_INFLATION_IMPORT_ENABLED fallback in hostings view\n- Memoize and reuse inflation source key in BondSummaryRow\n- Branch GUS path by explicit provider key (gus_sdp)
@UberDudePL
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
app/components/UI/dashboard/bond_summary_row.rb (1)

114-116: Memoization does not cache nil results.

Line 115 uses ||=, so when source is nil this recomputes on every call. Prefer defined?-based memoization to cache nullable values.

♻️ Proposed fix
 def current_inflation_source_key
-  `@current_inflation_source_key` ||= lot.current_inflation_source(allow_import: false).to_s.presence
+  return `@current_inflation_source_key` if defined?(`@current_inflation_source_key`)
+
+  `@current_inflation_source_key` = lot.current_inflation_source(allow_import: false).to_s.presence
 end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/UI/dashboard/bond_summary_row.rb` around lines 114 - 116,
current_inflation_source_key uses ||= which won't cache nil, causing repeated
calls to lot.current_inflation_source; replace the memoization with a
defined?-style check (e.g. use defined?(`@current_inflation_source_key`) or
instance_variable_defined? to detect prior assignment) so
`@current_inflation_source_key` is set once to the string or nil and returned on
subsequent calls; keep the existing call to
lot.current_inflation_source(allow_import: false).to_s.presence but assign it
unconditionally on first access and return the cached value thereafter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/components/UI/dashboard/bond_summary_row.rb`:
- Around line 114-116: current_inflation_source_key uses ||= which won't cache
nil, causing repeated calls to lot.current_inflation_source; replace the
memoization with a defined?-style check (e.g. use
defined?(`@current_inflation_source_key`) or instance_variable_defined? to detect
prior assignment) so `@current_inflation_source_key` is set once to the string or
nil and returned on subsequent calls; keep the existing call to
lot.current_inflation_source(allow_import: false).to_s.presence but assign it
unconditionally on first access and return the cached value thereafter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e98b588a-996f-4bbe-b8cc-5ffb29ba26a6

📥 Commits

Reviewing files that changed from the base of the PR and between 4ef188f and d21122e.

📒 Files selected for processing (2)
  • app/components/UI/dashboard/bond_summary_row.rb
  • app/views/settings/hostings/show.html.erb
✅ Files skipped from review due to trivial changes (1)
  • app/views/settings/hostings/show.html.erb

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 102 out of 103 changed files in this pull request and generated 1 comment.

Comment thread db/migrate/20260407183000_add_bond_lot_enum_check_constraints.rb
…n no-op

- Use defined? guard in BondSummaryRow#current_inflation_source_key
  to cache nil values (not just presence)
- Make 20260407183000 migration down a no-op since constraints
  already exist in CreateBondLots baseline
@UberDudePL
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 102 out of 103 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (1)
app/components/UI/dashboard/bond_summary_row.rb (1)

14-20: Avoid recomputing return amounts across selector + value methods.

lot.total_return_amount / lot.projected_total_return_amount can be evaluated more than once per row render. Consider memoizing raw values once and reusing in both methods.

♻️ Suggested refactor
 def total_return_amount
-  `@total_return_amount` ||= if projected_total_return?
-    lot.projected_total_return_amount(allow_import: false)
-  else
-    lot.total_return_amount(allow_import: false)
-  end
+  `@total_return_amount` ||= projected_total_return? ? projected_return_amount : current_return_amount
 end

 private
+  def current_return_amount
+    `@current_return_amount` ||= lot.total_return_amount(allow_import: false)
+  end
+
+  def projected_return_amount
+    `@projected_return_amount` ||= lot.projected_total_return_amount(allow_import: false)
+  end
+
   def projected_total_return?
     return `@projected_total_return` if defined?(`@projected_total_return`)

-    `@projected_total_return` = lot.total_return_amount(allow_import: false).abs < 0.01.to_d &&
-      lot.projected_total_return_amount(allow_import: false).positive?
+    `@projected_total_return` = current_return_amount.abs < 0.01.to_d &&
+      projected_return_amount.positive?
   end

Also applies to: 66-71

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/UI/dashboard/bond_summary_row.rb` around lines 14 - 20,
Memoize the raw return values once and reuse them instead of calling
lot.total_return_amount or lot.projected_total_return_amount multiple times: add
a single memoized helper (e.g., fetch_return_amounts or `@cached_return_amounts`)
that computes both the projected and actual raw amounts from lot (calling
lot.projected_total_return_amount(allow_import: false) and
lot.total_return_amount(allow_import: false) once), then update
total_return_amount and the other methods referenced around lines 66-71 to read
from that cache rather than invoking lot methods again.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/UI/dashboard/bond_summary_row.rb`:
- Line 43: The inflation-linked branch double-formats the rate:
current_rate_percent() already returns a percent (e.g., 2.5) and passing that
into helpers.number_to_percentage causes a ×100 again; update the conditional
that uses current_rate_percent() so you either render current_rate_percent()
directly (no number_to_percentage) or divide current_rate_percent() by 100 and
then call helpers.number_to_percentage to convert back to a fractional value;
adjust where lot.interest_rate and current_rate_percent() are used so inflation
and non-inflation branches produce consistent percent formatting.

---

Nitpick comments:
In `@app/components/UI/dashboard/bond_summary_row.rb`:
- Around line 14-20: Memoize the raw return values once and reuse them instead
of calling lot.total_return_amount or lot.projected_total_return_amount multiple
times: add a single memoized helper (e.g., fetch_return_amounts or
`@cached_return_amounts`) that computes both the projected and actual raw amounts
from lot (calling lot.projected_total_return_amount(allow_import: false) and
lot.total_return_amount(allow_import: false) once), then update
total_return_amount and the other methods referenced around lines 66-71 to read
from that cache rather than invoking lot methods again.
🪄 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: f565e65f-d8ee-4f34-89a0-c7c3a89b4c15

📥 Commits

Reviewing files that changed from the base of the PR and between 4ef188f and 435e013.

📒 Files selected for processing (3)
  • app/components/UI/dashboard/bond_summary_row.rb
  • app/views/settings/hostings/show.html.erb
  • db/migrate/20260407183000_add_bond_lot_enum_check_constraints.rb
✅ Files skipped from review due to trivial changes (1)
  • app/views/settings/hostings/show.html.erb
🚧 Files skipped from review as they are similar to previous changes (1)
  • db/migrate/20260407183000_add_bond_lot_enum_check_constraints.rb

Comment thread app/components/UI/dashboard/bond_summary_row.rb Outdated
- Extract current_return_amount and projected_return_amount helpers
  to avoid repeated evaluation of lot.total_return_amount() and
  lot.projected_total_return_amount(allow_import: false) per row render.
  Use single memoized values in both total_return_amount and projected_total_return?

- Fix double-percent formatting in rate_text:
  current_rate_percent() already returns percent (e.g., 2.5)
  so format directly as string instead of calling number_to_percentage
  which would multiply by 100 again.
  For fixed-rate bonds, multiply interest_rate by 100 before formatting.

All 60 bond tests passing, RuboCop clean.
@UberDudePL
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 102 out of 103 changed files in this pull request and generated 2 comments.

t("bonds.purchase_holding.unknown")
else
rate = lot.interest_rate
rate.present? ? "#{(rate * 100).round(3)}%" : t("bonds.purchase_holding.unknown")
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

rate_text multiplies lot.interest_rate by 100 before formatting. Elsewhere in the bond UI/tests interest_rate is already stored as a percent value (e.g., 4.25 meaning 4.25%), so this will display 425% instead of 4.25%. Format the stored percent directly (e.g., via number_to_percentage(rate, precision: 3) or "#{rate.round(3)}%").

Suggested change
rate.present? ? "#{(rate * 100).round(3)}%" : t("bonds.purchase_holding.unknown")
rate.present? ? "#{rate.round(3)}%" : t("bonds.purchase_holding.unknown")

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,83 @@
class Settings::Hostings::InflationImportSettingsComponent < ApplicationComponent
PROVIDERS = %w[gus_sdp us_bls es_ine].freeze
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

PROVIDERS is a hard-coded list that duplicates Bond::InflationProvider::PROVIDERS.keys. This can drift if a provider is added/removed elsewhere (UI would silently omit it). Consider deriving this list from the canonical provider registry so stats rendering stays in sync.

Suggested change
PROVIDERS = %w[gus_sdp us_bls es_ine].freeze
PROVIDERS = Bond::InflationProvider::PROVIDERS.keys.map(&:to_s).freeze

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/components/UI/dashboard/bond_summary_row.rb`:
- Around line 71-76: The predicate method projected_total_return? should guard
against nils on current_return_amount and projected_return_amount to avoid
NoMethodError; update projected_total_return? (and its memoization
`@projected_total_return`) to return false if either current_return_amount or
projected_return_amount is nil, and only call numeric methods (.abs, .positive?,
.to_d) when those values are present (e.g., use explicit nil checks or safe
navigation) so missing data yields false instead of raising.
- Around line 26-28: The helper total_return_class currently calls
total_return_amount.negative? which can raise NoMethodError when
total_return_amount is nil; update total_return_class to guard against nil
(e.g., return a neutral class like "" or "text-muted" if
total_return_amount.nil?) or coerce safely (e.g., use
total_return_amount.to_f.negative?) and ensure you reference total_return_amount
(and the model callers lot.total_return_amount /
lot.projected_total_return_amount) so the view also handles nil by rendering a
placeholder instead of the numeric value.
🪄 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: 2c46de9e-a9f5-4ca8-b34d-f9fedc9abc10

📥 Commits

Reviewing files that changed from the base of the PR and between 435e013 and 5dd3b78.

📒 Files selected for processing (1)
  • app/components/UI/dashboard/bond_summary_row.rb

Comment on lines +26 to +28
def total_return_class
total_return_amount.negative? ? "text-destructive" : "text-success"
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential NoMethodError if total_return_amount is nil.

Based on the model code, both lot.total_return_amount and lot.projected_total_return_amount can return nil (e.g., when estimated_current_value is nil or interest_rate/annual_rate is missing). Calling .negative? on nil will raise NoMethodError.

Suggested fix
  def total_return_class
-   total_return_amount.negative? ? "text-destructive" : "text-success"
+   return "text-secondary" if total_return_amount.nil?
+
+   total_return_amount.negative? ? "text-destructive" : "text-success"
  end

You may also want to handle the nil case in the view template to avoid rendering invalid return amounts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/UI/dashboard/bond_summary_row.rb` around lines 26 - 28, The
helper total_return_class currently calls total_return_amount.negative? which
can raise NoMethodError when total_return_amount is nil; update
total_return_class to guard against nil (e.g., return a neutral class like "" or
"text-muted" if total_return_amount.nil?) or coerce safely (e.g., use
total_return_amount.to_f.negative?) and ensure you reference total_return_amount
(and the model callers lot.total_return_amount /
lot.projected_total_return_amount) so the view also handles nil by rendering a
placeholder instead of the numeric value.

Comment on lines +71 to +76
def projected_total_return?
return @projected_total_return if defined?(@projected_total_return)

@projected_total_return = current_return_amount.abs < 0.01.to_d &&
projected_return_amount.positive?
end
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Nil values will cause NoMethodError in predicate logic.

Both current_return_amount and projected_return_amount can be nil (per context snippets showing the underlying model methods return nil when data is missing). Calling .abs or .positive? on nil raises NoMethodError.

Suggested fix with nil guards
  def projected_total_return?
    return `@projected_total_return` if defined?(`@projected_total_return`)

-   `@projected_total_return` = current_return_amount.abs < 0.01.to_d &&
-     projected_return_amount.positive?
+   current = current_return_amount
+   projected = projected_return_amount
+
+   `@projected_total_return` = current.present? &&
+     projected.present? &&
+     current.abs < 0.01.to_d &&
+     projected.positive?
  end

This ensures the method returns false when data is unavailable rather than raising an exception.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def projected_total_return?
return @projected_total_return if defined?(@projected_total_return)
@projected_total_return = current_return_amount.abs < 0.01.to_d &&
projected_return_amount.positive?
end
def projected_total_return?
return `@projected_total_return` if defined?(`@projected_total_return`)
current = current_return_amount
projected = projected_return_amount
`@projected_total_return` = current.present? &&
projected.present? &&
current.abs < 0.01.to_d &&
projected.positive?
end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/UI/dashboard/bond_summary_row.rb` around lines 71 - 76, The
predicate method projected_total_return? should guard against nils on
current_return_amount and projected_return_amount to avoid NoMethodError; update
projected_total_return? (and its memoization `@projected_total_return`) to return
false if either current_return_amount or projected_return_amount is nil, and
only call numeric methods (.abs, .positive?, .to_d) when those values are
present (e.g., use explicit nil checks or safe navigation) so missing data
yields false instead of raising.

@sokie
Copy link
Copy Markdown
Collaborator

sokie commented Apr 9, 2026

@UberDudePL this is a massive undertaking, so first of all want to take time to thank you for the work.
@jjmata here is my honest opinion on the feature.
I always look at features from my POV where Sure is meant to offer simplicity and ease of use while being powerful enough that it covers what people need to have a good overview of their financial situation.
Bonds IS a good feature to have.

Product Fit & Complexity Assessment

The Good

  • Bonds as inverse of Loans makes intuitive sense conceptually, both are fixed-income instruments, one you owe, one you're owed.
  • Lot-level tracking is the right call. Unlike loans (one obligation per account), people hold multiple bond lots with different maturities, rates, and purchase dates.
  • Product presets (US T-Bills, TIPS, I-Bonds, Spanish Letras, Polish EOD/ROD) dramatically lower the barrier to entry for common bond types.
  • Auto-maturity settlement with tax calculation is genuinely useful for a personal finance app.

The Concern: Inflation-Linked + GUS CPI
This is the elephant in the room. The inflation subsystem is disproportionately complex relative to the rest of the app.
The inflation code is bigger the bonds implementation, plus the three CPI providers (GUS SDP, US BLS, ES INE) each have their own:

  • API client with retry logic
  • Two separate DB tables (gus_inflation_rates vs inflation_rates — more on this below)
  • Settings page configuration
  • Scheduled import job

My honest assessment: The inflation subsystem is over-engineered for v1. If the goal is "simple, easy to understand, good flexibility for bond portfolio oversight," then:

  1. Fixed-rate bonds, zero-coupon bills, and simple savings bonds cover the vast majority of personal bond holdings. These work great in this PR.
  2. Inflation-linked bonds (TIPS, I-Bonds, Polish EOD/ROD) are niche. They're important to some users but are the long tail.
  3. Building full auto-fetch CPI integration with 3 country-specific government API providers is building for the power user before shipping for the common user.

Suggestion: Consider shipping bonds with inflation-linked support using manual rate entry only (the inflation_rate_assumption field already handles this). The auto-fetch CPI infrastructure could be a fast-follow PR. This would cut ~40% of the PR's net-new complexity.

Some concerns worth addressing anyway:

  • 9 migrations for what's conceptually one feature. Some seem additive as the features were being added, since this is the first implementation, they can be all moved to 1 file.
  • gus_inflation_rates and inflation_rates can be 1 table.
  • BondLot Model is 1000 lines alone, can be split into modules.
  • PurchaseHoldingPresenter Duplicates BondSummaryRow
  • BondLot has 12 before_validation callbacks that run in sequence. This is fragile and hard to reason about.

@jjmata
Copy link
Copy Markdown
Collaborator

jjmata commented Apr 15, 2026

Starting to see codebase drift here, what do you think of @sokie's suggestion @UberDudePL?

@UberDudePL
Copy link
Copy Markdown
Author

UberDudePL commented Apr 20, 2026

Hey @jjmata and @sokie, Sorry, I was a little out last couple of days. I fully agree with the approach suggested. I will remove CPIs and any automatic calculations from the PR and split it into two, Bonds and UI

# Conflicts:
#	app/controllers/settings/hostings_controller.rb
#	db/schema.rb
#	test/controllers/accounts_controller_test.rb
#	test/controllers/settings/hostings_controller_test.rb
@UberDudePL
Copy link
Copy Markdown
Author

Hey @jjmata & @sokie, I've create new PR. I will close this one after the new one is Merged:
#1532

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Extract bond lot entry creation logic from BondLotsController into model/service

5 participants