Skip to content

Add Interactive Brokers Provider#1712

Closed
gian-reto wants to merge 0 commit intowe-promise:mainfrom
gian-reto:gta/ibkr-provider
Closed

Add Interactive Brokers Provider#1712
gian-reto wants to merge 0 commit intowe-promise:mainfrom
gian-reto:gta/ibkr-provider

Conversation

@gian-reto
Copy link
Copy Markdown

@gian-reto gian-reto commented May 9, 2026

The Lunch Flow and SnapTrade integrations unfortunately don't support syncing trade history and other activity from Interactive Brokers. Additionally, holdings and cost basis values might be incomplete, see #898.

Therefore, I decided to implement a separate provider for IBKR, based on Flex Queries. Setting it up requires a bit of manual configuration in the IBKR dashboard, but once the Flex Query has been created, syncing happens automatically.

Features:

  • Supports multi-currency holdings and cash.
  • Syncs buys, sells, commissions, dividends, deposits, and withdrawals into the activity history.
  • Syncs current total account balance (holdings + cash).
  • Syncs historical daily account balances using IBKR’s daily equity summary, so end-of-day balances and charts match IBKR's data accurately.
  • Syncs historic FX conversion info for accurate currency conversions (in line with the data in the IBKR dashboard).
  • Displays logos for activity items that have a relation to a security.

Currently has some restrictions:

  • Only syncs up to 365 days of IBKR history (max. possible range for Flex Queries).
  • Only stock trades (and other trades of type "STK", like ETFs) are supported, others are filtered out.
  • Only long positions are supported, others are filtered out.

Misc.

  • Marks some additional values as privacy-sensitive to fix privacy view in the account detail views (activity & holdings list).
  • This PR also adds a Nix devshell. If this is considered out of scope or unwanted, I'll gladly remove it, though.

Summary by CodeRabbit

Release Notes

  • New Features

    • Connect and import Interactive Brokers accounts with automatic discovery and setup workflows.
    • Link existing manual investment accounts to Interactive Brokers holdings.
    • Improved multi-currency holdings calculations using exchange rates.
  • Improvements

    • Enhanced privacy controls marking sensitive financial data across the interface.
    • Better support for trade exchange rate tracking.

Review Change Stack

@brin-security-scanner brin-security-scanner Bot added the contributor:verified Contributor passed trust analysis. label May 9, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

📝 Walkthrough

Walkthrough

This PR implements comprehensive Interactive Brokers (IBKR) integration via Flex Query API, enabling account discovery, trade/activity imports, historical balance syncs, and account linking workflows. It adds IBKR models, a provider adapter, controller actions, account setup UI, exchange rate support on trades, and extensive test coverage, alongside privacy-sensitive CSS updates to existing balance/holdings displays.

Changes

IBKR Integration

Layer / File(s) Summary
Database Schema
db/migrate/20260508110000_create_ibkr_items_and_accounts.rb, db/migrate/20260509153000_simplify_ibkr_account_payloads.rb, db/migrate/20260509170000_add_extra_to_trades.rb, db/migrate/20260509171000_add_raw_equity_summary_payload_to_ibkr_accounts.rb, db/schema.rb
New ibkr_items and ibkr_accounts tables with UUID PKs, JSONB payloads, and indexes; trades.extra JSONB column for exchange rate storage; schema version bump to 2026_05_09_171000.
Core IBKR Models
app/models/ibkr_item.rb, app/models/ibkr_account.rb, app/models/family/ibkr_connectable.rb
IbkrItem with status enum, credentials, import/process/sync methods; IbkrAccount with account linking and statement upsertion; Family::IbkrConnectable concern enabling IBKR connectivity.
IBKR API Client
app/models/provider/ibkr_flex.rb
HTTParty-based Flex Query client with credentials validation, two-step request/polling workflow, error handling, and retry logic with exponential backoff.
XML Parsing & Data Helpers
app/models/ibkr_item/report_parser.rb, app/models/ibkr_account/data_helpers.rb
Flex statement XML parser extracting sections, balances, and dates; numeric/date/datetime parsers, security resolution, and currency extraction helpers.
Data Processing Pipeline
app/models/ibkr_account/activities_processor.rb, app/models/ibkr_account/holdings_processor.rb, app/models/ibkr_account/historical_balances_sync.rb, app/models/ibkr_account/processor.rb
Trade/cash transaction import with FX rates and commission handling; holdings grouping and aggregation; historical equity summary bulk sync; orchestrated account balance repair and opening anchor.
Provider Integration
app/models/provider/ibkr_adapter.rb, app/models/ibkr_item/importer.rb, app/models/ibkr_item/provided.rb, app/models/ibkr_item/syncer.rb, app/models/ibkr_item/unlinking.rb, app/models/data_enrichment.rb
Adapter registers with Provider::Factory for IbkrAccount type; Importer coordinates statement download and account upsert; Provided concern exposes ibkr_provider; Syncer manages credential/setup checks and processing; Unlinking gathers and destroys provider links; DataEnrichment.source enum includes ibkr.
Exchange Rate & Trade Support
app/models/trade.rb, app/models/transaction.rb, app/models/account/provider_import_adapter.rb, app/models/balance/sync_cache.rb
Trade#exchange_rate getter/setter backed by extra JSON; Transaction#activity_security metadata accessors; import_trade accepts optional exchange_rate; Balance::SyncCache uses trade exchange rates during conversion.
Account & Holdings Updates
app/models/account.rb, app/models/account/syncer.rb, app/models/holding.rb, app/models/holding/materializer.rb, app/models/holding/portfolio_cache.rb
Account.create_from_ibkr_account factory; Account::Syncer applies provider balance overrides post-sync; Holding#weight uses currency-converted amounts; Materializer cleans up provider snapshot overwrites; PortfolioCache applies date-specific exchange rates.
SnapTrade Multi-Currency
app/models/snaptrade_account/processor.rb
Conditional balance calculation for multi-currency holdings using API totals when currencies differ from account currency.
Controller & Routing
app/controllers/ibkr_items_controller.rb, config/routes.rb
CRUD scaffolding, sync trigger, account preload/select/link flows, account setup creation with auto-provider linking; resource routes with collection/member actions.
Views & UI
app/views/settings/providers/_ibkr_panel.html.erb, app/views/settings/providers/show.html.erb, app/views/ibkr_items/setup_accounts.html.erb, app/views/ibkr_items/select_existing_account.html.erb, app/views/ibkr_items/_ibkr_item.html.erb, app/views/accounts/index.html.erb
Settings panel with Flex Query instructions, credential form, and status indicator; account setup wizard with loading/empty states and account/link selection; expandable IBKR item card with admin controls; accounts index renders IBKR items list.
Privacy & Display
app/views/holdings/_cash.html.erb, app/views/holdings/_cost_basis_cell.html.erb, app/views/holdings/_holding.html.erb, app/javascript/controllers/time_series_chart_controller.js, app/views/trades/_trade.html.erb, app/views/transactions/_transaction.html.erb
Balances, holdings quantity, cost basis, and tooltips marked with privacy-sensitive CSS class; conditional logo rendering for trades and transactions with fallback icons.
Tests & Fixtures
test/fixtures/files/ibkr/flex_statement.xml, test/fixtures/ibkr_items.yml, test/fixtures/ibkr_accounts.yml, test/controllers/ibkr_items_controller_test.rb, test/models/ibkr_item_*.rb, test/models/ibkr_account_*.rb, test/models/account_*.rb, test/models/balance/sync_cache_test.rb, test/models/holding/*.rb, test/models/trade_test.rb, test/models/transaction_test.rb, test/models/snaptrade_account_processor_test.rb, flake.nix
Flex statement fixture with two accounts and nested sections; IBKR item/account fixtures; controller tests for preload/select/complete/link flows; model tests for processor, importer, parser, creation, exchange rate, and multi-currency behavior; Nix dev shell.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • we-promise/sure#933: Parallel provider integration pattern (Indexa Capital) adding connectable, adapter, and account item list rendering.
  • we-promise/sure#544: Modifies Account::ProviderImportAdapter to extend trade metadata handling.
  • we-promise/sure#1099: FX override feature implementation using per-entry exchange rate and balance conversion integration.

Suggested labels

enhancement

Suggested reviewers

  • jjmata

Poem

🐰 A rabbit hops through broker's woods,
With Flex Queries held tight,
Holdings and activities dance with glee,
Exchange rates glimmer so bright,
And families link their accounts with care,
In privacy's gentle light! 🌙✨

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

@brin-security-scanner brin-security-scanner Bot added the pr:verified PR passed security analysis. label May 9, 2026
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: 3056b2a642

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return
end

provider = ibkr_account.ensure_account_provider!(account)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject already-linked IBKR accounts before relinking

When an admin submits a stale/crafted link_existing_account request with an ibkr_account_id that is already linked, ensure_account_provider!(account) updates the existing AccountProvider to the new account instead of failing. That silently detaches the IBKR feed from the original account and starts importing future data into another manual account, leaving the old account with orphaned imported rows; guard here that ibkr_account.account_provider is blank (as complete_account_setup does via current_account.present?) before creating the link.

Useful? React with 👍 / 👎.

Comment on lines +89 to +92
<%= button_to sync_ibkr_item_path(ibkr_item),
method: :post,
class: "inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-secondary hover:text-primary border border-secondary rounded-lg hover:border-primary",
disabled: ibkr_item.syncing? do %>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Move button_to controls out of the edit form

For an already-configured IBKR item, this button_to is emitted inside the surrounding styled_form_with credentials form, which produces nested <form> elements (the delete button below has the same issue). Browsers do not handle nested forms consistently, so the Sync/Delete controls can either be ignored or disrupt the Update Configuration form; render these buttons outside the edit form like the other provider panels do.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
app/models/account/provider_import_adapter.rb (1)

556-596: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t clear persisted FX rates when the provider omits the field.

Because exchange_rate now defaults to nil and is always assigned, a re-import that does not send this keyword will wipe any existing trade-level rate. That can silently change historical conversions on later syncs. Please only write exchange_rate when the caller explicitly provided it, and add a regression test for the “re-import without exchange_rate preserves the stored value” case.

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

In `@app/models/account/provider_import_adapter.rb` around lines 556 - 596, The
import_trade method currently always assigns exchange_rate (via
entry.entryable.assign_attributes), which overwrites persisted FX rates when
callers omit that keyword; change the method to use a sentinel default for
exchange_rate (e.g. UNSET_EXCHANGE_RATE = Object.new) in the import_trade
signature and only include exchange_rate in the attributes hash when
exchange_rate != UNSET_EXCHANGE_RATE, leaving existing DB values intact; update
the assign_attributes usage in import_trade and add a regression test that
re-imports a Trade without passing exchange_rate to verify the stored
exchange_rate is preserved.
🟡 Minor comments (9)
test/models/transaction_test.rb-143-154 (1)

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

Add a test for nested extra["security"]["id"] fallback.

The new coverage only exercises top-level security_id and empty metadata. The fallback branch in Line [148] (extra["security"]["id"]) is still untested; add one case to lock that behavior.

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

In `@test/models/transaction_test.rb` around lines 143 - 154, Add a unit test in
test/models/transaction_test.rb to exercise the fallback branch of
Transaction#activity_security by creating a Transaction with extra: { "security"
=> { "id" => security.id } } (use the same securities fixture like
securities(:aapl)) and assert that transaction.activity_security returns that
security; this mirrors the existing top-level security_id test but targets the
nested extra["security"]["id"] path to cover the untested branch.
db/migrate/20260508110000_create_ibkr_items_and_accounts.rb-9-11 (1)

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

Use t.text for credential columns to match other provider migrations.

The IBKR migration uses t.string for token and query_id, but other provider credentials in this codebase use t.text: mercury (token), lunchflow (api_key), coinbase (api_key, api_secret), indexa_capital (api_token), and binance (api_key, api_secret). Both column types work with ActiveRecord's encrypts (which is properly configured for these fields in IbkrItem), but standardize on t.text for consistency.

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

In `@db/migrate/20260508110000_create_ibkr_items_and_accounts.rb` around lines 9 -
11, The migration defines credential columns using t.string for token and
query_id; change both to t.text to match other provider migrations and the
project's convention (these columns are used with ActiveRecord encryption in
IbkrItem). Update the migration's column definitions for token and query_id from
t.string to t.text, run/prepare a new migration or rollback+recreate this
migration if already applied, and ensure the schema.rb is updated so the
database column types and encrypted fields remain consistent with other
providers.
app/views/settings/providers/show.html.erb-82-86 (1)

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

Localize the new provider title.

Line 82 introduces a user-facing hard-coded string; this should use t() and a locale key.

Suggested fix
-    <%= settings_section title: "Interactive Brokers Flex Query (beta)", collapsible: true, open: false do %>
+    <%= settings_section title: t("settings.providers.ibkr.title"), collapsible: true, open: false do %>

Also add settings.providers.ibkr.title in config/locales/en.yml.

As per coding guidelines: **/*.{erb,rb} “Always use t() helper for user-facing strings… Update config/locales/en.yml for new strings.”

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

In `@app/views/settings/providers/show.html.erb` around lines 82 - 86, Replace the
hard-coded title in the settings_section call (the title: "Interactive Brokers
Flex Query (beta)" in show.html.erb) with a translation helper call (e.g.
t('settings.providers.ibkr.title')), and add the corresponding key
settings.providers.ibkr.title to config/locales/en.yml with the English text
"Interactive Brokers Flex Query (beta)"; ensure the view uses t(...) and the
locale file contains the new key.
app/views/settings/providers/_ibkr_panel.html.erb-11-53 (1)

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

Fix invalid list nesting in setup instructions

Several <ul> blocks are direct children of <ol> (e.g., Line 15, Line 45), which is invalid semantic HTML and hurts accessibility. Nest each sub-list inside its parent <li>.

As per coding guidelines, "Always generate semantic HTML."

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

In `@app/views/settings/providers/_ibkr_panel.html.erb` around lines 11 - 53, The
ordered list has direct child <ul> elements which is invalid; for each sub-list
(e.g., the <ul> blocks under the "Cash Report:", "Cash Transactions:", "Net
Asset Value (NAV) in Base:", "Open Positions:", "Trades:" and the configuration
options) move the <ul> so it is nested inside the corresponding parent <li>
(wrap each sub-<ul> inside the <li> that contains the header text like "Cash
Report:", "Cash Transactions:", "Open Positions:", etc.), ensure all <li>
elements enclose their child <ul> and that tags are properly opened/closed to
produce semantic HTML.
app/models/family/ibkr_connectable.rb-14-14 (1)

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

Handle blank item_name when applying default provider name

Line 14 uses item_name || ..., so an empty string won’t fall back to "Interactive Brokers". Use item_name.presence || ... instead.

Proposed fix
-      name: item_name || "Interactive Brokers",
+      name: item_name.presence || "Interactive Brokers",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/family/ibkr_connectable.rb` at line 14, The name assignment in
ibkr_connectable.rb currently uses `item_name || "Interactive Brokers"`, which
treats an empty string as truthy; update the fallback to use presence so blank
strings fall back: change the expression to use `item_name.presence ||
"Interactive Brokers"` where the `name` is set (refer to the `name: item_name ||
"Interactive Brokers"` assignment) so empty or whitespace-only item_name values
will correctly default to "Interactive Brokers".
test/models/account/syncer_test.rb-25-29 (1)

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

Assert execution order explicitly to match the test intent

Line 25-Line 27 only checks call counts. The test title asserts ordering (“after materialization”), but this can still pass if order regresses.

Proposed fix
-    Account::MarketDataImporter.any_instance.expects(:import_all).once
-    Balance::Materializer.any_instance.expects(:materialize_balances).once
-    IbkrAccount::HistoricalBalancesSync.any_instance.expects(:sync!).once
+    sequence = sequence("ibkr_sync_order")
+    Account::MarketDataImporter.any_instance.expects(:import_all).once.in_sequence(sequence)
+    Balance::Materializer.any_instance.expects(:materialize_balances).once.in_sequence(sequence)
+    IbkrAccount::HistoricalBalancesSync.any_instance.expects(:sync!).once.in_sequence(sequence)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/models/account/syncer_test.rb` around lines 25 - 29, The test currently
only asserts call counts for Account::MarketDataImporter.import_all,
Balance::Materializer.materialize_balances, and
IbkrAccount::HistoricalBalancesSync.sync! but not their order; modify the test
around Account::Syncer.new(account).perform_sync to enforce ordering by creating
a Mocha sequence (or equivalent) and attach each expectation (.expects(...)) to
that sequence in the desired order: import_all first, materialize_balances
second, and sync! third so the calls are asserted to occur in that exact
sequence when Account::Syncer#perform_sync is invoked.
app/controllers/ibkr_items_controller.rb-53-86 (1)

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

update always forces status: :good regardless of validation outcome.

Line 58 merges status: :good into the update payload even when the user only edited name. This unconditionally clears any requires_update state on save (e.g., when credentials remain unchanged but were previously broken), masking a real failure mode until the next sync runs. Consider only resetting status when query_id or token actually change.

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

In `@app/controllers/ibkr_items_controller.rb` around lines 53 - 86, The update
action currently forces status: :good by merging status into attrs
unconditionally; change it so status is only reset when the credentials actually
changed: compute attrs = ibkr_item_params.to_h, set attrs["query_id"] =
`@ibkr_item.query_id` if blank and attrs["token"] = `@ibkr_item.token` if blank as
before, then detect if attrs["query_id"] != `@ibkr_item.query_id` ||
attrs["token"] != `@ibkr_item.token` and only then merge status: :good into the
payload passed to `@ibkr_item.update`; keep the existing sync_later call around
`@ibkr_item.sync_later` unless `@ibkr_item.syncing`? but only trigger it when you
have reset status (i.e., when you merged status: :good).
app/controllers/ibkr_items_controller.rb-167-178 (1)

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

Misleading success message when processing fails after link.

After IbkrAccount::Processor.new(...).process is rescued silently (only logged), control still falls through to redirect_to account_path(account), notice: "Successfully linked...". The user is told linking succeeded even when the subsequent processing raised. Either let the rescue alter the redirect (e.g., a different alert/notice indicating the linkage succeeded but the initial sync failed and will be retried), or schedule processing asynchronously so failure doesn't influence this synchronous user-facing flow.

Additionally, the outer rescue => e at Line 175 inserts e.message directly into the flash alert returned to the browser. Internal exception messages can leak implementation/PII details — prefer logging the full error and showing a generic alert to the user.

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

In `@app/controllers/ibkr_items_controller.rb` around lines 167 - 178, The current
flow rescues errors from IbkrAccount::Processor.new(ibkr_account.reload).process
but always continues to redirect with the success notice, and the outer rescue
exposes e.message to users; fix by making processing failures change the
user-facing response: in the inner rescue (around
IbkrAccount::Processor.process) set a local status flag (e.g., processing_failed
= true) or enqueue the work asynchronously instead of letting the failure fall
through, then branch before redirect_to account_path(account) to show a clear
notice like "Linked, initial sync failed — will retry" (or schedule async
processing via ibkr_account.ibkr_item.sync_later) when processing_failed is
true; also change the outer rescue to log the full error (Rails.logger.error
including e.class and backtrace) but present a generic flash alert (e.g.,
"Failed to link Interactive Brokers account") rather than injecting e.message
into the alert.
app/models/ibkr_account/activities_processor.rb-161-162 (1)

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

Remove commission field requirements from supported_trade?.

Trades without commission data are being rejected entirely, but IBKR Flex Query reports have commission as an optional field—it's not guaranteed for every trade (e.g., commission-free promotions, certain ETF trades). The trade itself can be processed without these fields, and import_commission_transaction already handles missing commission gracefully by returning early (line 117). These checks prevent valid trades from being imported.

♻️ Suggested change
-        row[:trade_price].present? &&
-        row[:transaction_id].present? &&
-        row[:ib_commission].present? &&
-        row[:ib_commission_currency].present? &&
-        fx_rate_available?(row)
+        row[:trade_price].present? &&
+        row[:transaction_id].present? &&
+        fx_rate_available?(row)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/ibkr_account/activities_processor.rb` around lines 161 - 162,
supported_trade? is currently rejecting trades when row[:ib_commission] or
row[:ib_commission_currency] are missing; remove the commission presence checks
so trades without commission are allowed (delete the lines checking
row[:ib_commission].present? && row[:ib_commission_currency].present?). Keep
import_commission_transaction as-is (it already returns early when commission is
absent), so only modify supported_trade? to stop requiring commission fields.
🧹 Nitpick comments (11)
app/models/ibkr_item/unlinking.rb (1)

9-10: 💤 Low value

Use the polymorphic association instead of a hardcoded provider_type string.

AccountProvider.where(provider: provider_account) sets both provider_type/provider_id from the polymorphic mapping, avoiding the hardcoded "IbkrAccount" literal that would silently break under any rename/namespacing.

♻️ Proposed change
-      links = AccountProvider.where(provider_type: "IbkrAccount", provider_id: provider_account.id).to_a
+      links = AccountProvider.where(provider: provider_account).to_a
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/ibkr_item/unlinking.rb` around lines 9 - 10, Replace the hardcoded
polymorphic type lookup with the polymorphic association: in the block where
ibkr_accounts.find_each yields provider_account and you currently call
AccountProvider.where(provider_type: "IbkrAccount", provider_id:
provider_account.id), change the query to use AccountProvider.where(provider:
provider_account) so Rails sets provider_type and provider_id via the
polymorphic mapping (update any variable names like provider_account and the
surrounding ibkr_accounts.find_each loop accordingly).
app/views/ibkr_items/select_existing_account.html.erb (1)

3-33: ⚡ Quick win

Localize the IBKR linking copy with t() keys

User-facing strings in this modal are hard-coded (e.g., header, empty-state guidance, balance label, action labels). Please move them to locale keys and render with t(...).

As per coding guidelines, "Always use t() helper for user-facing strings" and "Update config/locales/en.yml for new strings."

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

In `@app/views/ibkr_items/select_existing_account.html.erb` around lines 3 - 33,
Replace all hard-coded user-facing strings in this view with i18n keys and calls
to t(...): change the dialog header (dialog.with_header), empty-state paragraphs
and list items, the balance label inside the span that uses number_to_currency,
and the DS::Button/DS::Link text props to use t("...") keys; keep the dynamic
interpolations (like ibkr_account.name, currency and number_to_currency) intact.
Add corresponding entries to config/locales/en.yml under a sensible namespace
(e.g. ibkr_items.select_existing_account.header, .empty_state.line1,
.empty_state.line2, .balance_label, .actions.link, .actions.cancel). Ensure you
reference the same keys in the view via
t("ibkr_items.select_existing_account.<key>") to locate and replace each
hard-coded string.
app/views/ibkr_items/_ibkr_item.html.erb (2)

11-12: ⚡ Quick win

Replace raw IB badge colors with design-system tokens

Line 11-Line 12 uses hard-coded hex color classes. Please use design-system token classes so the badge follows theme/system constraints.

As per coding guidelines, "Use Tailwind CSS design tokens ... instead of raw Tailwind classes."

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

In `@app/views/ibkr_items/_ibkr_item.html.erb` around lines 11 - 12, The IB badge
uses hard-coded hex Tailwind classes; replace the raw color classes on the badge
container and the span (the div with class "bg-[`#D32F2F`]/10" and the span with
"text-[`#D32F2F`]") with the project's Tailwind design-system token classes (e.g.,
use the design token for the danger/background token on the container and the
corresponding text color token on the span) so the badge follows theme tokens
instead of raw hex values.

19-107: ⚡ Quick win

Localize status and action copy via t()

This partial introduces many hard-coded user-facing strings (status text, empty-state text, action labels). Move them to locale keys and render via t(...).

As per coding guidelines, "Always use t() helper for user-facing strings."

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

In `@app/views/ibkr_items/_ibkr_item.html.erb` around lines 19 - 107, The partial
contains hard-coded user-facing strings (e.g., "Deletion in progress", "Flex Web
Service", "Syncing", "Credentials need attention", "Error", "Synced ... ago.",
"Never synced.", "Set up accounts", "Delete", "Accounts need setup", "No IBKR
accounts discovered yet.", and link/button labels used with DS::Link,
DS::Tooltip, menu.with_item, icon/tag.span, and ProviderSyncSummary rendering);
replace each literal with a call to t(...) using descriptive locale keys (e.g.,
ibkr_items.status.deletion_in_progress, ibkr_items.service.flex_web,
ibkr_items.status.syncing, ibkr_items.actions.setup_accounts,
ibkr_items.empty.no_accounts, etc.), update calls in the partial (tag.span,
render DS::Tooltip, DS::Link, menu.with_item) to use the t(...) results, and add
corresponding keys to the locale YAML so all user-facing strings are localized.
app/views/settings/providers/_ibkr_panel.html.erb (2)

10-118: ⚡ Quick win

Move panel copy to i18n keys

This partial introduces substantial user-visible text as hard-coded literals. Please move these strings to locale keys and use t(...) consistently.

As per coding guidelines, "Always use t() helper for user-facing strings."

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

In `@app/views/settings/providers/_ibkr_panel.html.erb` around lines 10 - 118, The
partial contains many hard-coded user-facing strings; extract them into i18n
keys (e.g., under a new locale namespace like ibkr_panel.*) and replace literals
with t('ibkr_panel.<key>') usages throughout the partial: move the "Setup
instructions:" heading, every list item and note paragraph, the placeholders for
form.text_field :query_id and :token, button labels used with button_to (Sync,
Disconnect Interactive Brokers?), the form.submit text, the status messages
("Not configured.", the sync summary label if any) and link text into locale
keys; keep view logic and helpers (styled_form_with, form.text_field, button_to
sync_ibkr_item_path, form.submit, ibkr_item.sync_status_summary) intact and
reference the newly created keys in those calls so all user-visible strings use
the t(...) helper.

116-116: ⚡ Quick win

Use a design token class instead of raw bg-gray-400

Line 116 uses a raw Tailwind color utility. Prefer a design-system token class to keep theme consistency.

As per coding guidelines, "Always prefer using functional tokens defined in sure-design-system.css rather than raw Tailwind utilities."

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

In `@app/views/settings/providers/_ibkr_panel.html.erb` at line 116, Replace the
raw Tailwind color utility on the small status dot (the div with classes "w-2
h-2 bg-gray-400 rounded-full") with the corresponding design-system token class
from sure-design-system.css; locate that div in _ibkr_panel.html.erb and swap
out "bg-gray-400" for the project's functional token class that maps to the
intended neutral color (the named token used across sure-design-system.css) so
the component uses the design token instead of a raw Tailwind utility.
app/models/ibkr_item/report_parser.rb (1)

78-100: 💤 Low value

Optional: simplify section_rows — consider extracting the fallback path.

The dual-mode logic (children-of-container vs container-as-row vs statement-direct fallback) is correct but dense. A short comment describing the three input shapes IBKR can produce here would help future readers without changing behavior.

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

In `@app/models/ibkr_item/report_parser.rb` around lines 78 - 100, The
section_rows method handles three IBKR XML shapes (rows as children of container
elements, container elements themselves as rows, or rows directly under
statement) but the logic is dense; extract the fallback "statement-direct" path
into a small helper (e.g., a private method like
statement_direct_rows(statement, row_names)) and add a short comment above
section_rows describing the three input shapes and the chosen resolution order;
update section_rows to call the new helper when rows.empty? to keep behavior
identical while making the fallback path explicit and easier to read.
app/models/ibkr_item.rb (1)

35-43: 💤 Low value

Prefer a custom error class over raw StandardError.

Raising StandardError, "IBKR provider is not configured" makes catch sites unable to discriminate this configuration failure from any unrelated runtime error. Consider an explicit error class (e.g., IbkrItem::ProviderNotConfiguredError) so callers and the surrounding rescue => e (which re-raises) can be acted upon by tests and upstream handlers without string matching.

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

In `@app/models/ibkr_item.rb` around lines 35 - 43, Replace the generic
StandardError with a dedicated error class so callers can distinguish
provider-configuration failures: define a new error class (e.g.,
IbkrItem::ProviderNotConfiguredError) and raise that from
import_latest_ibkr_data instead of StandardError, update the rescue/logging
behavior if necessary to reference the new class (method import_latest_ibkr_data
and the IbkrItem::Importer call are the relevant symbols), and ensure any tests
or upstream handlers catch IbkrItem::ProviderNotConfiguredError rather than
matching the error message.
app/controllers/ibkr_items_controller.rb (1)

134-142: 💤 Low value

Possible N+1 / large in-memory filter on select_existing_account.

Current.family.ibkr_items.includes(...).flat_map(&:ibkr_accounts).select { |a| a.account_provider.nil? } materializes every ibkr_account and account_provider for the family before filtering in Ruby. Consider scoping at the DB level, e.g., Current.family.ibkr_items.joins(:ibkr_accounts).left_joins(ibkr_accounts: :account_provider).where(account_providers: { id: nil })..., returning only the needed ibkr_account records ordered by updated_at/created_at desc.

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

In `@app/controllers/ibkr_items_controller.rb` around lines 134 - 142, The current
code materializes all ibkr_accounts in Ruby via
Current.family.ibkr_items.flat_map(&:ibkr_accounts).select and filters
account_provider there; instead perform the filtering and ordering at the DB
level: start from Current.family.ibkr_items, join to :ibkr_accounts and
left_join ibkr_accounts: :account_provider, add where(account_providers: { id:
nil }) to only return ibkr_accounts without providers, select the ibkr_accounts
records (not the parent ibkr_items), and order by
COALESCE(ibkr_accounts.updated_at, ibkr_accounts.created_at) DESC so
`@available_ibkr_accounts` is a DB-scoped collection for the
select_existing_account render.
app/models/ibkr_account/activities_processor.rb (2)

195-213: 💤 Low value

security_symbol_for_conid does redundant with_indifferent_access work per call.

For each cash transaction the helper iterates raw_holdings_payload and (potentially) raw_activities_payload[:trades], calling with_indifferent_access per row. When there are many cash transactions this is O(N×M). Consider memoizing a conid → symbol map per processor instance.

♻️ Suggested change
+    def conid_symbol_map
+      `@conid_symbol_map` ||= begin
+        map = {}
+        Array(`@ibkr_account.raw_holdings_payload`).each do |holding|
+          h = holding.with_indifferent_access
+          map[h[:conid].to_s] ||= h[:symbol] if h[:conid].present?
+        end
+        Array(`@ibkr_account.raw_activities_payload`&.dig("trades") || `@ibkr_account.raw_activities_payload`&.dig(:trades)).each do |trade|
+          t = trade.with_indifferent_access
+          map[t[:conid].to_s] ||= t[:symbol] if t[:conid].present?
+        end
+        map
+      end
+    end
+
     def security_symbol_for_conid(conid)
       return nil if conid.blank?
-
-      holding_symbol = Array(`@ibkr_account.raw_holdings_payload`).find do |holding|
-        holding.with_indifferent_access[:conid].to_s == conid.to_s
-      end&.with_indifferent_access&.dig(:symbol)
-      return holding_symbol if holding_symbol.present?
-
-      Array(`@ibkr_account.raw_activities_payload`&.dig("trades") || `@ibkr_account.raw_activities_payload`&.dig(:trades)).find do |trade|
-        trade.with_indifferent_access[:conid].to_s == conid.to_s
-      end&.with_indifferent_access&.dig(:symbol)
+      conid_symbol_map[conid.to_s]
     end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/ibkr_account/activities_processor.rb` around lines 195 - 213,
security_symbol_for_conid is doing repeated with_indifferent_access conversions
over `@ibkr_account.raw_holdings_payload` and raw_activities_payload[:trades],
causing O(N×M) work; instead build and memoize a conid→symbol lookup on the
processor instance (e.g. `@conid_symbol_map`) the first time it’s needed. Populate
that map once by iterating Array(`@ibkr_account.raw_holdings_payload`) and
Array(`@ibkr_account.raw_activities_payload`&.dig("trades") ||
`@ibkr_account.raw_activities_payload`&.dig(:trades)), calling
with_indifferent_access only while creating the map, then change
security_symbol_for_conid to return `@conid_symbol_map`[conid.to_s] (or nil) so
resolve_security_for_cash_transaction and resolve_security remain unchanged but
much faster.

150-164: 💤 Low value

Filter out zero-quantity trades.

supported_trade? only checks present? for quantity, so a row with quantity: "0" passes validation and proceeds to import a no-op trade with signed_quantity = 0 and native_amount = 0. Worth rejecting these explicitly to avoid creating empty trade entries.

♻️ Suggested change
     def supported_trade?(row)
       row[:asset_category].to_s == "STK" &&
         row[:buy_sell].present? &&
         row[:conid].present? &&
         row[:currency].present? &&
         row[:quantity].present? &&
+        parse_decimal(row[:quantity])&.nonzero? &&
         row[:symbol].present? &&
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/ibkr_account/activities_processor.rb` around lines 150 - 164, The
supported_trade? predicate currently allows rows with quantity "0" because it
only checks presence; update supported_trade? to explicitly reject zero
quantities by converting row[:quantity] to a numeric (e.g., using to_d or to_f)
and returning false if the numeric value is zero or effectively zero, ensuring
any zero or zero-equivalent quantity rows are filtered out before further
processing (leave other checks like fx_rate_available?(row) intact).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/controllers/ibkr_items_controller.rb`:
- Around line 27-83: Replace all hard-coded user-facing strings in this
controller with i18n calls using the t() helper: update flash notices/alerts in
the create and update flows (e.g., the strings "Successfully configured
Interactive Brokers.", "Successfully updated Interactive Brokers
configuration.", and any redirect alert messages), and the `@error_message`
construction that joins `@ibkr_item.errors.full_messages` so you pass localized
wrapper keys; use hierarchical keys such as ibkr_items.flash.created,
ibkr_items.flash.updated, and ibkr_items.errors.not_configured (or similar)
referenced from the create and update methods and from any places using
attrs["query_id"]/attrs["token"] fallbacks, and add corresponding entries to
config/locales/en.yml. Ensure you call t(...) where the current literal strings
are used (and keep the original messages as the default locale values in
en.yml).

In `@app/models/account/syncer.rb`:
- Around line 39-46: The IBKR-specific call in apply_provider_balance_overrides
(the IbkrAccount::HistoricalBalancesSync.new(ibkr_account).sync! invocation) can
raise and currently bubbles up to abort perform_sync; wrap that sync! call in a
rescue that captures StandardError (or Exception subclass you prefer) so
provider failures are swallowed and do not stop the overall account sync, and
log the exception (including message and backtrace) using the existing logger or
process logger; keep the early guards (account.linked_to? and
find_by(provider_type: "IbkrAccount")) but ensure any exception from
HistoricalBalancesSync.sync! is rescued and handled gracefully.

In `@app/models/ibkr_account/activities_processor.rb`:
- Line 65: The extra["exchange_rate"] is stored as BigDecimal for trades
(exchange_rate: parse_decimal(row[:fx_rate_to_base]) in ActivitiesProcessor) but
as Float for cash and commission paths; make exchange_rate consistently a
BigDecimal across all activity types by replacing uses of &.to_f with the same
parse_decimal(...) conversion (or a single helper that returns BigDecimal) in
the cash transaction and commission branches referenced in this file so all
extras store BigDecimal for exchange_rate.

In `@app/models/ibkr_item.rb`:
- Around line 107-123: Update sync_status_summary and institution_display_name
to use Rails I18n helpers instead of hard-coded strings: replace the literal "No
IBKR accounts discovered yet" with a t(...) lookup, use t(..., count:
linked_count) or Rails pluralization via t with count for the linked account
text (e.g., key that supports pluralization) and similarly for the
"#{linked_count} linked, #{unlinked_count} need setup" message use a translation
key that accepts both counts (or compose using two t(...) calls with count:
linked_count and count: unlinked_count), and replace institution_display_name's
"Interactive Brokers" with a t(...) lookup; reference the existing methods
total_accounts_count, linked_accounts_count, and unlinked_accounts_count to pass
counts into the translation calls and add appropriate keys to the locale files.

In `@app/models/ibkr_item/importer.rb`:
- Around line 13-24: The snapshot write (ibkr_item.upsert_ibkr_snapshot!) can
succeed while subsequent account upserts
(ibkr_item.ibkr_accounts.find_or_initialize_by /
ibkr_account.upsert_from_ibkr_statement!) fail, leaving partial state; wrap the
snapshot, the per-account upserts, and the final status update
(ibkr_item.update!(status: :good)) inside a single DB transaction (e.g.,
ActiveRecord::Base.transaction or ibkr_item.transaction) so any exception rolls
back all writes and preserves atomicity.

In `@app/models/provider/ibkr_adapter.rb`:
- Around line 14-26: The provider definition in ibkr_adapter.rb currently uses
hard-coded user-facing strings for the "name" and "description" keys (and also
the institution_name elsewhere in this file), so replace those string literals
with i18n lookups (e.g., use t("providers.ibkr.name"),
t("providers.ibkr.connection_description") and
t("providers.ibkr.institution_name") respectively) and add corresponding entries
under config/locales/en.yml (providers: ibkr: name:, connection_description:,
institution_name:) so the UI strings are localized; ensure you call Rails' t()
helper in the same context where the provider hash is built (preserving the
existing lambdas/new_account_path/existing_account_path entries).

In `@app/models/provider/ibkr_flex.rb`:
- Around line 1-17: Change Provider::IbkrFlex to inherit from Provider (class
Provider::IbkrFlex < Provider) and stop exposing a separate provider-specific
error stack for public consumption; instead wrap the class’s public data
retrieval entrypoint (the fetch / public fetch path method) with
with_provider_response so errors are normalized by the shared Provider behavior.
Update the public fetch method to call with_provider_response { ...actual IBKR
HTTP/data logic... } and remove or keep the internal
ApiError/AuthenticationError only for internal mapping (do not let callers
bypass with_provider_response).

In `@app/models/snaptrade_account/processor.rb`:
- Around line 74-76: The info log in SnaptradeAccount::Processor exposes a
sensitive monetary value; remove the balance amount from the info-level message
(or demote the log to debug) and log only non-sensitive context/identifiers
(e.g., snaptrade_account.id or account external_id) instead; update the log call
in the block guarded by use_api_total_balance? that currently references
snaptrade_account.current_balance so it no longer prints the amount but
preserves the return of snaptrade_account.current_balance || 0.

In `@app/models/trade.rb`:
- Around line 21-29: The setter exchange_rate= currently accepts
"NaN"/"Infinity" because Float(value) doesn't raise; after converting
(normalized_value = Float(value)) add a check using normalized_value.finite? (or
normalized_value.nan?/infinite?) and if not finite treat it as invalid (set
"exchange_rate_invalid" => true and store the original value or nil as
appropriate) instead of marking valid; likewise update the validator that
performs the numeric <= 0 check (the method that validates exchange_rate) to
first reject non-finite values by checking value.finite? (or
value.nan?/value.infinite?) and mark them invalid before doing the <= 0
comparison so NaN/Infinity can't pass validation.

In `@app/models/transaction.rb`:
- Around line 151-156: The activity_security method currently does a
per-instance lookup via Security.find_by(id: activity_security_id), causing N+1
queries; change callers to preload all required Security records (e.g., fetch
Security.where(id: transactions.map(&:activity_security_id)).index_by(&:id)) and
replace the per-row lookup with a map lookup (use the
preloaded_map[activity_security_id]) or convert activity_security to read from
an association and use ActiveRecord preload/includes in the query that builds
the transactions list; remove or short-circuit the Security.find_by usage in the
activity_security method (or make it use a provided preloaded map) so rendering
uses the bulk-loaded securities instead of issuing individual queries.
- Around line 147-149: The accessor activity_security_id currently reads
top-level keys from Transaction#extra which breaks provider namespacing; update
activity_security_id to be provider-aware by reading under the provider
namespace in extra (e.g., extra&.dig(provider_key, "security_id") ||
extra&.dig(provider_key, "security", "id")), where provider_key is derived from
the transaction (use an existing provider attribute/method such as provider or
provider_name or add an optional provider argument to the method), and
remove/avoid reading top-level "security_id" or "security" keys.

In `@app/views/ibkr_items/setup_accounts.html.erb`:
- Around line 1-173: Replace all hard-coded user-facing strings in this view
with i18n calls using the t() helper and add corresponding keys under a
hierarchical namespace like ibkr_items.setup_accounts in config/locales/en.yml;
specifically update content_for :title, the DS::Dialog header title, the info
box headings and bullets, status messages
(`@waiting_for_sync/`@no_accounts_found), form labels and checkbox label text
(e.g., account_<%= ibkr_account.id %> label), button and link texts ("Create
selected accounts", "Cancel", "Refresh", "Back to Settings", "Link", "Done"),
section headers ("Available accounts", "Already linked"), the select prompt
("Select an account...") and any interpolated strings that include
currency/amounts so they use t() with interpolation where needed; ensure keys
follow the suggested hierarchy (e.g., ibkr_items.setup_accounts.title,
.info_box.title, .info_box.bullets.item1, .buttons.create_selected,
.status.fetching_accounts, .select.prompt, .linked.already_linked) and replace
the inline literals with t('ibkr_items.setup_accounts.xxx').

---

Outside diff comments:
In `@app/models/account/provider_import_adapter.rb`:
- Around line 556-596: The import_trade method currently always assigns
exchange_rate (via entry.entryable.assign_attributes), which overwrites
persisted FX rates when callers omit that keyword; change the method to use a
sentinel default for exchange_rate (e.g. UNSET_EXCHANGE_RATE = Object.new) in
the import_trade signature and only include exchange_rate in the attributes hash
when exchange_rate != UNSET_EXCHANGE_RATE, leaving existing DB values intact;
update the assign_attributes usage in import_trade and add a regression test
that re-imports a Trade without passing exchange_rate to verify the stored
exchange_rate is preserved.

---

Minor comments:
In `@app/controllers/ibkr_items_controller.rb`:
- Around line 53-86: The update action currently forces status: :good by merging
status into attrs unconditionally; change it so status is only reset when the
credentials actually changed: compute attrs = ibkr_item_params.to_h, set
attrs["query_id"] = `@ibkr_item.query_id` if blank and attrs["token"] =
`@ibkr_item.token` if blank as before, then detect if attrs["query_id"] !=
`@ibkr_item.query_id` || attrs["token"] != `@ibkr_item.token` and only then merge
status: :good into the payload passed to `@ibkr_item.update`; keep the existing
sync_later call around `@ibkr_item.sync_later` unless `@ibkr_item.syncing`? but only
trigger it when you have reset status (i.e., when you merged status: :good).
- Around line 167-178: The current flow rescues errors from
IbkrAccount::Processor.new(ibkr_account.reload).process but always continues to
redirect with the success notice, and the outer rescue exposes e.message to
users; fix by making processing failures change the user-facing response: in the
inner rescue (around IbkrAccount::Processor.process) set a local status flag
(e.g., processing_failed = true) or enqueue the work asynchronously instead of
letting the failure fall through, then branch before redirect_to
account_path(account) to show a clear notice like "Linked, initial sync failed —
will retry" (or schedule async processing via ibkr_account.ibkr_item.sync_later)
when processing_failed is true; also change the outer rescue to log the full
error (Rails.logger.error including e.class and backtrace) but present a generic
flash alert (e.g., "Failed to link Interactive Brokers account") rather than
injecting e.message into the alert.

In `@app/models/family/ibkr_connectable.rb`:
- Line 14: The name assignment in ibkr_connectable.rb currently uses `item_name
|| "Interactive Brokers"`, which treats an empty string as truthy; update the
fallback to use presence so blank strings fall back: change the expression to
use `item_name.presence || "Interactive Brokers"` where the `name` is set (refer
to the `name: item_name || "Interactive Brokers"` assignment) so empty or
whitespace-only item_name values will correctly default to "Interactive
Brokers".

In `@app/models/ibkr_account/activities_processor.rb`:
- Around line 161-162: supported_trade? is currently rejecting trades when
row[:ib_commission] or row[:ib_commission_currency] are missing; remove the
commission presence checks so trades without commission are allowed (delete the
lines checking row[:ib_commission].present? &&
row[:ib_commission_currency].present?). Keep import_commission_transaction as-is
(it already returns early when commission is absent), so only modify
supported_trade? to stop requiring commission fields.

In `@app/views/settings/providers/_ibkr_panel.html.erb`:
- Around line 11-53: The ordered list has direct child <ul> elements which is
invalid; for each sub-list (e.g., the <ul> blocks under the "Cash Report:",
"Cash Transactions:", "Net Asset Value (NAV) in Base:", "Open Positions:",
"Trades:" and the configuration options) move the <ul> so it is nested inside
the corresponding parent <li> (wrap each sub-<ul> inside the <li> that contains
the header text like "Cash Report:", "Cash Transactions:", "Open Positions:",
etc.), ensure all <li> elements enclose their child <ul> and that tags are
properly opened/closed to produce semantic HTML.

In `@app/views/settings/providers/show.html.erb`:
- Around line 82-86: Replace the hard-coded title in the settings_section call
(the title: "Interactive Brokers Flex Query (beta)" in show.html.erb) with a
translation helper call (e.g. t('settings.providers.ibkr.title')), and add the
corresponding key settings.providers.ibkr.title to config/locales/en.yml with
the English text "Interactive Brokers Flex Query (beta)"; ensure the view uses
t(...) and the locale file contains the new key.

In `@db/migrate/20260508110000_create_ibkr_items_and_accounts.rb`:
- Around line 9-11: The migration defines credential columns using t.string for
token and query_id; change both to t.text to match other provider migrations and
the project's convention (these columns are used with ActiveRecord encryption in
IbkrItem). Update the migration's column definitions for token and query_id from
t.string to t.text, run/prepare a new migration or rollback+recreate this
migration if already applied, and ensure the schema.rb is updated so the
database column types and encrypted fields remain consistent with other
providers.

In `@test/models/account/syncer_test.rb`:
- Around line 25-29: The test currently only asserts call counts for
Account::MarketDataImporter.import_all,
Balance::Materializer.materialize_balances, and
IbkrAccount::HistoricalBalancesSync.sync! but not their order; modify the test
around Account::Syncer.new(account).perform_sync to enforce ordering by creating
a Mocha sequence (or equivalent) and attach each expectation (.expects(...)) to
that sequence in the desired order: import_all first, materialize_balances
second, and sync! third so the calls are asserted to occur in that exact
sequence when Account::Syncer#perform_sync is invoked.

In `@test/models/transaction_test.rb`:
- Around line 143-154: Add a unit test in test/models/transaction_test.rb to
exercise the fallback branch of Transaction#activity_security by creating a
Transaction with extra: { "security" => { "id" => security.id } } (use the same
securities fixture like securities(:aapl)) and assert that
transaction.activity_security returns that security; this mirrors the existing
top-level security_id test but targets the nested extra["security"]["id"] path
to cover the untested branch.

---

Nitpick comments:
In `@app/controllers/ibkr_items_controller.rb`:
- Around line 134-142: The current code materializes all ibkr_accounts in Ruby
via Current.family.ibkr_items.flat_map(&:ibkr_accounts).select and filters
account_provider there; instead perform the filtering and ordering at the DB
level: start from Current.family.ibkr_items, join to :ibkr_accounts and
left_join ibkr_accounts: :account_provider, add where(account_providers: { id:
nil }) to only return ibkr_accounts without providers, select the ibkr_accounts
records (not the parent ibkr_items), and order by
COALESCE(ibkr_accounts.updated_at, ibkr_accounts.created_at) DESC so
`@available_ibkr_accounts` is a DB-scoped collection for the
select_existing_account render.

In `@app/models/ibkr_account/activities_processor.rb`:
- Around line 195-213: security_symbol_for_conid is doing repeated
with_indifferent_access conversions over `@ibkr_account.raw_holdings_payload` and
raw_activities_payload[:trades], causing O(N×M) work; instead build and memoize
a conid→symbol lookup on the processor instance (e.g. `@conid_symbol_map`) the
first time it’s needed. Populate that map once by iterating
Array(`@ibkr_account.raw_holdings_payload`) and
Array(`@ibkr_account.raw_activities_payload`&.dig("trades") ||
`@ibkr_account.raw_activities_payload`&.dig(:trades)), calling
with_indifferent_access only while creating the map, then change
security_symbol_for_conid to return `@conid_symbol_map`[conid.to_s] (or nil) so
resolve_security_for_cash_transaction and resolve_security remain unchanged but
much faster.
- Around line 150-164: The supported_trade? predicate currently allows rows with
quantity "0" because it only checks presence; update supported_trade? to
explicitly reject zero quantities by converting row[:quantity] to a numeric
(e.g., using to_d or to_f) and returning false if the numeric value is zero or
effectively zero, ensuring any zero or zero-equivalent quantity rows are
filtered out before further processing (leave other checks like
fx_rate_available?(row) intact).

In `@app/models/ibkr_item.rb`:
- Around line 35-43: Replace the generic StandardError with a dedicated error
class so callers can distinguish provider-configuration failures: define a new
error class (e.g., IbkrItem::ProviderNotConfiguredError) and raise that from
import_latest_ibkr_data instead of StandardError, update the rescue/logging
behavior if necessary to reference the new class (method import_latest_ibkr_data
and the IbkrItem::Importer call are the relevant symbols), and ensure any tests
or upstream handlers catch IbkrItem::ProviderNotConfiguredError rather than
matching the error message.

In `@app/models/ibkr_item/report_parser.rb`:
- Around line 78-100: The section_rows method handles three IBKR XML shapes
(rows as children of container elements, container elements themselves as rows,
or rows directly under statement) but the logic is dense; extract the fallback
"statement-direct" path into a small helper (e.g., a private method like
statement_direct_rows(statement, row_names)) and add a short comment above
section_rows describing the three input shapes and the chosen resolution order;
update section_rows to call the new helper when rows.empty? to keep behavior
identical while making the fallback path explicit and easier to read.

In `@app/models/ibkr_item/unlinking.rb`:
- Around line 9-10: Replace the hardcoded polymorphic type lookup with the
polymorphic association: in the block where ibkr_accounts.find_each yields
provider_account and you currently call AccountProvider.where(provider_type:
"IbkrAccount", provider_id: provider_account.id), change the query to use
AccountProvider.where(provider: provider_account) so Rails sets provider_type
and provider_id via the polymorphic mapping (update any variable names like
provider_account and the surrounding ibkr_accounts.find_each loop accordingly).

In `@app/views/ibkr_items/_ibkr_item.html.erb`:
- Around line 11-12: The IB badge uses hard-coded hex Tailwind classes; replace
the raw color classes on the badge container and the span (the div with class
"bg-[`#D32F2F`]/10" and the span with "text-[`#D32F2F`]") with the project's
Tailwind design-system token classes (e.g., use the design token for the
danger/background token on the container and the corresponding text color token
on the span) so the badge follows theme tokens instead of raw hex values.
- Around line 19-107: The partial contains hard-coded user-facing strings (e.g.,
"Deletion in progress", "Flex Web Service", "Syncing", "Credentials need
attention", "Error", "Synced ... ago.", "Never synced.", "Set up accounts",
"Delete", "Accounts need setup", "No IBKR accounts discovered yet.", and
link/button labels used with DS::Link, DS::Tooltip, menu.with_item,
icon/tag.span, and ProviderSyncSummary rendering); replace each literal with a
call to t(...) using descriptive locale keys (e.g.,
ibkr_items.status.deletion_in_progress, ibkr_items.service.flex_web,
ibkr_items.status.syncing, ibkr_items.actions.setup_accounts,
ibkr_items.empty.no_accounts, etc.), update calls in the partial (tag.span,
render DS::Tooltip, DS::Link, menu.with_item) to use the t(...) results, and add
corresponding keys to the locale YAML so all user-facing strings are localized.

In `@app/views/ibkr_items/select_existing_account.html.erb`:
- Around line 3-33: Replace all hard-coded user-facing strings in this view with
i18n keys and calls to t(...): change the dialog header (dialog.with_header),
empty-state paragraphs and list items, the balance label inside the span that
uses number_to_currency, and the DS::Button/DS::Link text props to use t("...")
keys; keep the dynamic interpolations (like ibkr_account.name, currency and
number_to_currency) intact. Add corresponding entries to config/locales/en.yml
under a sensible namespace (e.g. ibkr_items.select_existing_account.header,
.empty_state.line1, .empty_state.line2, .balance_label, .actions.link,
.actions.cancel). Ensure you reference the same keys in the view via
t("ibkr_items.select_existing_account.<key>") to locate and replace each
hard-coded string.

In `@app/views/settings/providers/_ibkr_panel.html.erb`:
- Around line 10-118: The partial contains many hard-coded user-facing strings;
extract them into i18n keys (e.g., under a new locale namespace like
ibkr_panel.*) and replace literals with t('ibkr_panel.<key>') usages throughout
the partial: move the "Setup instructions:" heading, every list item and note
paragraph, the placeholders for form.text_field :query_id and :token, button
labels used with button_to (Sync, Disconnect Interactive Brokers?), the
form.submit text, the status messages ("Not configured.", the sync summary label
if any) and link text into locale keys; keep view logic and helpers
(styled_form_with, form.text_field, button_to sync_ibkr_item_path, form.submit,
ibkr_item.sync_status_summary) intact and reference the newly created keys in
those calls so all user-visible strings use the t(...) helper.
- Line 116: Replace the raw Tailwind color utility on the small status dot (the
div with classes "w-2 h-2 bg-gray-400 rounded-full") with the corresponding
design-system token class from sure-design-system.css; locate that div in
_ibkr_panel.html.erb and swap out "bg-gray-400" for the project's functional
token class that maps to the intended neutral color (the named token used across
sure-design-system.css) so the component uses the design token instead of a raw
Tailwind utility.
🪄 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: b04e6f85-d173-429f-9aa7-b36a033fd7f5

📥 Commits

Reviewing files that changed from the base of the PR and between b74014a and 3056b2a.

⛔ Files ignored due to path filters (1)
  • flake.lock is excluded by !**/*.lock
📒 Files selected for processing (68)
  • app/components/UI/account/activity_date.html.erb
  • app/controllers/accounts_controller.rb
  • app/controllers/ibkr_items_controller.rb
  • app/javascript/controllers/time_series_chart_controller.js
  • app/models/account.rb
  • app/models/account/provider_import_adapter.rb
  • app/models/account/syncer.rb
  • app/models/balance/sync_cache.rb
  • app/models/data_enrichment.rb
  • app/models/family.rb
  • app/models/family/ibkr_connectable.rb
  • app/models/holding.rb
  • app/models/holding/materializer.rb
  • app/models/holding/portfolio_cache.rb
  • app/models/ibkr_account.rb
  • app/models/ibkr_account/activities_processor.rb
  • app/models/ibkr_account/data_helpers.rb
  • app/models/ibkr_account/historical_balances_sync.rb
  • app/models/ibkr_account/holdings_processor.rb
  • app/models/ibkr_account/processor.rb
  • app/models/ibkr_item.rb
  • app/models/ibkr_item/importer.rb
  • app/models/ibkr_item/provided.rb
  • app/models/ibkr_item/report_parser.rb
  • app/models/ibkr_item/syncer.rb
  • app/models/ibkr_item/unlinking.rb
  • app/models/provider/ibkr_adapter.rb
  • app/models/provider/ibkr_flex.rb
  • app/models/snaptrade_account/processor.rb
  • app/models/trade.rb
  • app/models/transaction.rb
  • app/views/accounts/index.html.erb
  • app/views/holdings/_cash.html.erb
  • app/views/holdings/_cost_basis_cell.html.erb
  • app/views/holdings/_holding.html.erb
  • app/views/ibkr_items/_ibkr_item.html.erb
  • app/views/ibkr_items/select_existing_account.html.erb
  • app/views/ibkr_items/setup_accounts.html.erb
  • app/views/settings/providers/_ibkr_panel.html.erb
  • app/views/settings/providers/show.html.erb
  • app/views/trades/_trade.html.erb
  • app/views/transactions/_transaction.html.erb
  • config/routes.rb
  • db/migrate/20260508110000_create_ibkr_items_and_accounts.rb
  • db/migrate/20260509153000_simplify_ibkr_account_payloads.rb
  • db/migrate/20260509170000_add_extra_to_trades.rb
  • db/migrate/20260509171000_add_raw_equity_summary_payload_to_ibkr_accounts.rb
  • db/schema.rb
  • flake.nix
  • test/controllers/ibkr_items_controller_test.rb
  • test/fixtures/files/ibkr/flex_statement.xml
  • test/fixtures/ibkr_accounts.yml
  • test/fixtures/ibkr_items.yml
  • test/models/account/provider_import_adapter_test.rb
  • test/models/account/syncer_test.rb
  • test/models/account_ibkr_creation_test.rb
  • test/models/account_test.rb
  • test/models/balance/sync_cache_test.rb
  • test/models/holding/materializer_test.rb
  • test/models/holding/portfolio_cache_test.rb
  • test/models/holding_test.rb
  • test/models/ibkr_account/historical_balances_sync_test.rb
  • test/models/ibkr_account_processor_test.rb
  • test/models/ibkr_item_importer_test.rb
  • test/models/ibkr_item_report_parser_test.rb
  • test/models/snaptrade_account_processor_test.rb
  • test/models/trade_test.rb
  • test/models/transaction_test.rb

Comment on lines +27 to +83
flash.now[:notice] = "Successfully configured Interactive Brokers."
render turbo_stream: [
turbo_stream.replace(
"ibkr-providers-panel",
partial: "settings/providers/ibkr_panel"
),
*flash_notification_stream_items
]
else
redirect_to settings_providers_path, notice: "Successfully configured Interactive Brokers.", status: :see_other
end
else
@error_message = @ibkr_item.errors.full_messages.join(", ")

if turbo_frame_request?
render turbo_stream: turbo_stream.replace(
"ibkr-providers-panel",
partial: "settings/providers/ibkr_panel",
locals: { error_message: @error_message }
), status: :unprocessable_entity
else
redirect_to settings_providers_path, alert: @error_message, status: :unprocessable_entity
end
end
end

def update
attrs = ibkr_item_params.to_h
attrs["query_id"] = @ibkr_item.query_id if attrs["query_id"].blank?
attrs["token"] = @ibkr_item.token if attrs["token"].blank?

if @ibkr_item.update(attrs.merge(status: :good))
@ibkr_item.sync_later unless @ibkr_item.syncing?

if turbo_frame_request?
flash.now[:notice] = "Successfully updated Interactive Brokers configuration."
render turbo_stream: [
turbo_stream.replace(
"ibkr-providers-panel",
partial: "settings/providers/ibkr_panel"
),
*flash_notification_stream_items
]
else
redirect_to settings_providers_path, notice: "Successfully updated Interactive Brokers configuration.", status: :see_other
end
else
@error_message = @ibkr_item.errors.full_messages.join(", ")

if turbo_frame_request?
render turbo_stream: turbo_stream.replace(
"ibkr-providers-panel",
partial: "settings/providers/ibkr_panel",
locals: { error_message: @error_message }
), status: :unprocessable_entity
else
redirect_to settings_providers_path, alert: @error_message, status: :unprocessable_entity
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

i18n: extract flash/notice copy via t().

All flash notices and alerts in this controller (and the inline attrs["query_id"]/token fallback messages) are hard-coded English strings. As per coding guidelines, every user-facing string must go through t(), with keys organized hierarchically (e.g., ibkr_items.flash.created, ibkr_items.flash.updated, ibkr_items.errors.not_configured, etc.) and entries added to config/locales/en.yml. The same applies to lines 96, 111, 122, 130, 155, 160, 174, 177, and 232–236.

As per coding guidelines: "Always use t() helper for user-facing strings."

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

In `@app/controllers/ibkr_items_controller.rb` around lines 27 - 83, Replace all
hard-coded user-facing strings in this controller with i18n calls using the t()
helper: update flash notices/alerts in the create and update flows (e.g., the
strings "Successfully configured Interactive Brokers.", "Successfully updated
Interactive Brokers configuration.", and any redirect alert messages), and the
`@error_message` construction that joins `@ibkr_item.errors.full_messages` so you
pass localized wrapper keys; use hierarchical keys such as
ibkr_items.flash.created, ibkr_items.flash.updated, and
ibkr_items.errors.not_configured (or similar) referenced from the create and
update methods and from any places using attrs["query_id"]/attrs["token"]
fallbacks, and add corresponding entries to config/locales/en.yml. Ensure you
call t(...) where the current literal strings are used (and keep the original
messages as the default locale values in en.yml).

Comment on lines +39 to +46
def apply_provider_balance_overrides
return unless account.linked_to?("IbkrAccount")

ibkr_account = account.account_providers.find_by(provider_type: "IbkrAccount")&.provider
return unless ibkr_account

IbkrAccount::HistoricalBalancesSync.new(ibkr_account).sync!
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent IBKR override failures from failing the entire account sync.

Line 45 can raise and currently aborts perform_sync; this makes a provider-specific enrichment step capable of failing the whole sync path.

Suggested fix
     def apply_provider_balance_overrides
       return unless account.linked_to?("IbkrAccount")

       ibkr_account = account.account_providers.find_by(provider_type: "IbkrAccount")&.provider
       return unless ibkr_account

       IbkrAccount::HistoricalBalancesSync.new(ibkr_account).sync!
+    rescue => e
+      Rails.logger.error("Error syncing IBKR historical balances for account #{account.id}: #{e.message}")
+      Sentry.capture_exception(e)
     end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/account/syncer.rb` around lines 39 - 46, The IBKR-specific call in
apply_provider_balance_overrides (the
IbkrAccount::HistoricalBalancesSync.new(ibkr_account).sync! invocation) can
raise and currently bubbles up to abort perform_sync; wrap that sync! call in a
rescue that captures StandardError (or Exception subclass you prefer) so
provider failures are swallowed and do not stop the overall account sync, and
log the exception (including message and backtrace) using the existing logger or
process logger; keep the early guards (account.linked_to? and
find_by(provider_type: "IbkrAccount")) but ensure any exception from
HistoricalBalancesSync.sync! is rescued and handled gracefully.

name: build_trade_name(security.ticker, signed_quantity),
source: "ibkr",
activity_label: buy_sell == "SELL" ? "Sell" : "Buy",
exchange_rate: parse_decimal(row[:fx_rate_to_base])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Inconsistent exchange_rate value types stored in extra.

For trades the FX rate is passed as a BigDecimal (Line 65: exchange_rate: parse_decimal(row[:fx_rate_to_base])), but in cash transactions and commissions it is converted with &.to_f to a Float (Lines 95 and 130). This mixes precision/representation across activity types persisted under the same key, which can lead to subtle discrepancies in equality checks (e.g., BigDecimal("0.9") == 0.9 is false) and JSON round-trip differences. Pick one (BigDecimal preferred for monetary FX) and apply uniformly.

Also applies to: 95-95, 130-130

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

In `@app/models/ibkr_account/activities_processor.rb` at line 65, The
extra["exchange_rate"] is stored as BigDecimal for trades (exchange_rate:
parse_decimal(row[:fx_rate_to_base]) in ActivitiesProcessor) but as Float for
cash and commission paths; make exchange_rate consistently a BigDecimal across
all activity types by replacing uses of &.to_f with the same parse_decimal(...)
conversion (or a single helper that returns BigDecimal) in the cash transaction
and commission branches referenced in this file so all extras store BigDecimal
for exchange_rate.

Comment on lines +48 to +69
def resolve_security(row)
data = row.with_indifferent_access
ticker = data[:symbol].to_s.strip.upcase
return nil if ticker.blank?

Security.find_by(ticker: ticker) || create_security_from_row(ticker)
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
Security.find_by(ticker: ticker)
end

def trade_date_for(row)
parse_date(row.with_indifferent_access[:trade_date]) || Date.current
end

def extract_currency(row, fallback: nil)
value = row.with_indifferent_access[:currency]
value.present? ? value.to_s.upcase : fallback
end

def create_security_from_row(ticker)
Security.create!(ticker: ticker, name: ticker)
end
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 9, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect parsing/normalization in the IBKR provider/import paths to find which keys are populated alongside :symbol.
fd -t f -e rb -E node_modules . | xargs rg -n -C2 ":symbol" 2>/dev/null
rg -nP --type=rb -C3 '\b(listing_exchange|listingExchange|exchange|exch)\b'

Repository: we-promise/sure

Length of output: 35581


🏁 Script executed:

cat -n app/models/ibkr_account/data_helpers.rb

Repository: we-promise/sure

Length of output: 2327


🏁 Script executed:

# Find where IBKR data is imported and what fields are available
rg -A5 -B5 "def process" app/models/ibkr_account/ | head -100

Repository: we-promise/sure

Length of output: 4773


🏁 Script executed:

# Look for IBKR test fixtures or sample data
fd -t f -e rb app/models/ibkr_account | xargs ls -la

Repository: we-promise/sure

Length of output: 3906


🏁 Script executed:

# Search for where rows are passed to resolve_security in IBKR context
rg "resolve_security" app/models/ibkr_account/ -B3 -A1

Repository: we-promise/sure

Length of output: 2245


🏁 Script executed:

# Find test fixtures with IBKR data to see what fields are available
find test -name "*ibkr*" -type f | head -20

Repository: we-promise/sure

Length of output: 335


🏁 Script executed:

# Look at actual IBKR test data
find . -path ./node_modules -prune -o -type f -name "*.yml" -o -name "*.json" | xargs grep -l "ibkr" 2>/dev/null | head -10

Repository: we-promise/sure

Length of output: 93


🏁 Script executed:

# Search for sample IBKR payloads in test files
rg "raw_holdings_payload|raw_activities_payload" test/ -A10 | head -100

Repository: we-promise/sure

Length of output: 7203


🏁 Script executed:

# Look for IBKR field names like listing_exchange, exchange in the codebase
rg "listing.*exchange|listingExchange" . --type=rb | head -20

Repository: we-promise/sure

Length of output: 86


🏁 Script executed:

cat -n test/models/ibkr_item_importer_test.rb | head -150

Repository: we-promise/sure

Length of output: 1463


🏁 Script executed:

cat -n test/fixtures/ibkr_accounts.yml

Repository: we-promise/sure

Length of output: 912


🏁 Script executed:

cat -n test/fixtures/ibkr_items.yml | head -200

Repository: we-promise/sure

Length of output: 462


🏁 Script executed:

# Find the IBKR flex statement fixture to see actual data structure
find test -name "*flex*" -type f

Repository: we-promise/sure

Length of output: 103


🏁 Script executed:

# Look at the IBKR parser to understand how data is extracted from the XML
rg "class.*Parser" app/models/ibkr_item/ -A5

Repository: we-promise/sure

Length of output: 549


🏁 Script executed:

# Check for any mention of exchange or listing in IBKR related files
rg -i "exchange|listing" app/models/ibkr_account/ app/models/ibkr_item/ | head -30

Repository: we-promise/sure

Length of output: 401


🏁 Script executed:

cat test/fixtures/files/ibkr/flex_statement.xml | head -300

Repository: we-promise/sure

Length of output: 3050


🏁 Script executed:

# Look at the ReportParser to see what fields it extracts from the XML
cat -n app/models/ibkr_item/report_parser.rb | head -200

Repository: we-promise/sure

Length of output: 6047


resolve_security does not disambiguate by exchange — a valid concern, but the suggested fix assumes unavailable data.

Security has a unique index on upper(ticker), COALESCE(upper(exchange_operating_mic), ''), so multiple rows can exist for the same ticker across different exchanges. Security.find_by(ticker: ticker) returns whichever row matches first, and create_security_from_row creates a row with no exchange info, potentially conflating exchange-distinct securities.

However, IBKR Flex Query data does not expose exchange information. The available fields in parsed rows include symbol, conid, currency, assetCategory, securityID, securityIDType, etc., but no exchange or listing exchange identifier. The suggested approach of passing exchange data through resolve_security cannot be implemented without a data source for the exchange value.

Consider whether:

  1. Exchange information can be inferred from securityID (e.g., ISIN → exchange lookup via a provider)
  2. conid (IBKR contract ID) already uniquely identifies a security across exchanges and could be used as the primary key instead of ticker
  3. This is a limitation of the IBKR data format that needs an alternative approach

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.

I don't want to make changes in the core Security logic, because that's used throughout the application and also affects other providers and features. Unfortunately, IBKR's exchange identifiers are proprietary, and it doesn't provide clean MIC identifiers... I'll keep this as-is for now, as incorrect matches can be manually fixed by the user and I have not experienced any weird matches in my testing (with a handful of global stocks).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@gian-reto, understood — that's a reasonable trade-off given the data limitations. Using conid as a lookup hint (via the holdings-based fallback already in activities_processor.rb) partially mitigates the risk, and for most mainstream stocks a ticker-only match will be correct. I'll note this constraint for future reference.

(´・ω・`)


✏️ Learnings added
Learnt from: gian-reto
Repo: we-promise/sure PR: 1712
File: app/models/ibkr_account/data_helpers.rb:48-69
Timestamp: 2026-05-09T20:14:21.086Z
Learning: In app/models/ibkr_account/data_helpers.rb, `resolve_security` intentionally resolves securities by ticker only (no exchange). IBKR Flex Query data does not expose clean MIC exchange identifiers — IBKR exchange codes are proprietary. Multi-exchange ticker disambiguation is therefore not feasible without an external lookup. This is an accepted limitation: incorrect matches are rare in practice and can be manually corrected. Do not flag the absence of exchange-based disambiguation in IBKR-related security resolution code.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: jjmata
Repo: we-promise/sure PR: 593
File: app/controllers/pages_controller.rb:200-203
Timestamp: 2026-01-09T12:25:59.839Z
Learning: Do not compare hard-coded strings to identify the 'Uncategorized Investments' category in Ruby/Rails code (e.g., ct.category.name != "Uncategorized Investments"). Localization/internationalization can change strings. Prefer: (1) a model-level predicate such as ct.category.uncategorized_investment? or (2) compare by identity with a canonical category, e.g., ct.category != Category.uncategorized_investments. This improves maintainability and correctness across translations. Apply this pattern to Ruby files that perform category checks (controllers, models, or elsewhere).

Learnt from: dgilperez
Repo: we-promise/sure PR: 934
File: app/models/family/syncer.rb:47-57
Timestamp: 2026-02-09T21:30:32.649Z
Learning: In Rails model reviews, for files under app/models, also inspect included concerns (e.g., Family::PlaidConnectable). Check the associations, scopes, and methods defined in the included concern modules. Do not flag a behavior as missing on the main model if it is implemented in a concern; instead verify whether the concern provides the expected behavior and is properly included. This pattern applies to other model files that use concerns.

Learnt from: UberDudePL
Repo: we-promise/sure PR: 1330
File: app/models/bond_lot.rb:399-401
Timestamp: 2026-04-08T06:24:33.376Z
Learning: In financial event code (e.g., settlement/maturity flows in app/models/**/*.rb), do not log sensitive monetary amounts (such as gross, tax, net) at info level. Instead, log only non-sensitive identifiers (e.g., record IDs, event names/status) or lower the log level to debug if amounts are required for troubleshooting. Avoid introducing new info-level log lines that include monetary fields.

Learnt from: EdeAbreu23
Repo: we-promise/sure PR: 1692
File: app/models/holding.rb:53-57
Timestamp: 2026-05-06T21:57:46.943Z
Learning: Guideline: store cost_basis on Holding as a per-share value (not total position cost). Templates should render total cost as cost_basis * qty. remap_security! should continue computing weighted-average per-share cost from per-share inputs. When provider data (e.g., Schwab via SimpleFIN or Plaid) supplies total position cost basis, normalize by dividing by qty before persisting to cost_basis. Implement this normalization in the provider/import/materialization path, not in Holding#avg_cost. This applies to app/models/holding.rb and related code paths, including app/views/holdings/_cost_basis_cell.html.erb and any importer/remap logic.

Comment thread app/models/ibkr_item.rb
Comment on lines +107 to +123
def sync_status_summary
total_accounts = total_accounts_count
linked_count = linked_accounts_count
unlinked_count = unlinked_accounts_count

if total_accounts.zero?
"No IBKR accounts discovered yet"
elsif unlinked_count.zero?
"#{linked_count} account#{'s' unless linked_count == 1} linked"
else
"#{linked_count} linked, #{unlinked_count} need setup"
end
end

def institution_display_name
"Interactive Brokers"
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

i18n: replace hard-coded strings and inline pluralization with t() helpers.

sync_status_summary and institution_display_name return user-facing English strings. As per coding guidelines, use t() for user-facing strings and Rails pluralization for count-aware text.

♻️ Suggested change
   def sync_status_summary
     total_accounts = total_accounts_count
     linked_count = linked_accounts_count
     unlinked_count = unlinked_accounts_count

     if total_accounts.zero?
-      "No IBKR accounts discovered yet"
+      t("ibkr_items.sync_status.none_discovered")
     elsif unlinked_count.zero?
-      "#{linked_count} account#{'s' unless linked_count == 1} linked"
+      t("ibkr_items.sync_status.linked", count: linked_count)
     else
-      "#{linked_count} linked, #{unlinked_count} need setup"
+      t("ibkr_items.sync_status.partial", linked: linked_count, unlinked: unlinked_count)
     end
   end

   def institution_display_name
-    "Interactive Brokers"
+    t("ibkr_items.institution_display_name")
   end

As per coding guidelines: "Always use t() helper for user-facing strings... Use Rails pluralization: t("transactions.count", count: @transactions.count)."

📝 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 sync_status_summary
total_accounts = total_accounts_count
linked_count = linked_accounts_count
unlinked_count = unlinked_accounts_count
if total_accounts.zero?
"No IBKR accounts discovered yet"
elsif unlinked_count.zero?
"#{linked_count} account#{'s' unless linked_count == 1} linked"
else
"#{linked_count} linked, #{unlinked_count} need setup"
end
end
def institution_display_name
"Interactive Brokers"
end
def sync_status_summary
total_accounts = total_accounts_count
linked_count = linked_accounts_count
unlinked_count = unlinked_accounts_count
if total_accounts.zero?
t("ibkr_items.sync_status.none_discovered")
elsif unlinked_count.zero?
t("ibkr_items.sync_status.linked", count: linked_count)
else
t("ibkr_items.sync_status.partial", linked: linked_count, unlinked: unlinked_count)
end
end
def institution_display_name
t("ibkr_items.institution_display_name")
end
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/models/ibkr_item.rb` around lines 107 - 123, Update sync_status_summary
and institution_display_name to use Rails I18n helpers instead of hard-coded
strings: replace the literal "No IBKR accounts discovered yet" with a t(...)
lookup, use t(..., count: linked_count) or Rails pluralization via t with count
for the linked account text (e.g., key that supports pluralization) and
similarly for the "#{linked_count} linked, #{unlinked_count} need setup" message
use a translation key that accepts both counts (or compose using two t(...)
calls with count: linked_count and count: unlinked_count), and replace
institution_display_name's "Interactive Brokers" with a t(...) lookup; reference
the existing methods total_accounts_count, linked_accounts_count, and
unlinked_accounts_count to pass counts into the translation calls and add
appropriate keys to the locale files.

Comment on lines +74 to +76
if use_api_total_balance?
Rails.logger.info "SnaptradeAccount::Processor - Using API total for multi-currency holdings: #{snaptrade_account.current_balance}"
return snaptrade_account.current_balance || 0
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid logging the balance amount at info level here.

This adds a new info log with current_balance, which is privacy-sensitive financial data. Please drop the amount from the message or move it to debug with account/context identifiers only.

Suggested change
-        Rails.logger.info "SnaptradeAccount::Processor - Using API total for multi-currency holdings: #{snaptrade_account.current_balance}"
+        Rails.logger.debug "SnaptradeAccount::Processor - Using API total for multi-currency holdings for snaptrade_account=#{snaptrade_account.id}"

Based on learnings: “In financial event code (e.g., settlement/maturity flows in app/models/**/*.rb), do not log sensitive monetary amounts (such as gross, tax, net) at info level.”

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

In `@app/models/snaptrade_account/processor.rb` around lines 74 - 76, The info log
in SnaptradeAccount::Processor exposes a sensitive monetary value; remove the
balance amount from the info-level message (or demote the log to debug) and log
only non-sensitive context/identifiers (e.g., snaptrade_account.id or account
external_id) instead; update the log call in the block guarded by
use_api_total_balance? that currently references
snaptrade_account.current_balance so it no longer prints the amount but
preserves the return of snaptrade_account.current_balance || 0.

Comment thread app/models/trade.rb
Comment on lines +21 to +29
def exchange_rate=(value)
if value.blank?
self.extra = (extra || {}).merge("exchange_rate" => nil, "exchange_rate_invalid" => false)
else
begin
normalized_value = Float(value)
self.extra = (extra || {}).merge("exchange_rate" => normalized_value, "exchange_rate_invalid" => false)
rescue ArgumentError, TypeError
self.extra = (extra || {}).merge("exchange_rate" => value, "exchange_rate_invalid" => true)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Ruby, does Float("NaN") or Float("Infinity") raise an exception, and how do BigDecimal NaN/Infinity values behave in comparisons like <= 0?

💡 Result:

Float("NaN") and Float("Infinity") do not raise an exception; they produce non-finite Float values (NaN / +Infinity or -Infinity) as valid results [1]. For BigDecimal, parsing/creating NaN and infinities via strings like "NaN" and "Infinity" is supported, and NaN/infinity are represented as special BigDecimal values [2]. Comparison behavior: 1) Float - Ruby documents that the result of comparisons involving NaN is “undefined” / implementation-dependent (e.g., NaN < NaN) [3]. 2) BigDecimal - BigDecimal’s NaN is not considered equal to any value, even itself [2]. - Ruby’s BigDecimal comparison operators are expected to follow that “NaN is incomparable / no meaningful ordering” idea; in practice, comparisons involving BigDecimal NaN do not behave like ordinary total ordering (e.g., <=> returns nil; and using <= can raise an ArgumentError for some operand type combinations) [4][5]. So specifically for the question “how do BigDecimal NaN/Infinity values behave in comparisons like <= 0?”: - NaN: comparisons with <= are not a meaningful numeric ordering; NaN is not equal to itself [2] and comparisons can yield non-boolean outcomes / nil ordering (and in some cases raise ArgumentError) [4][5]. - Infinity: BigDecimal supports Infinity and -Infinity as distinct values [2], so comparisons against finite numbers like 0 follow the expected infinite ordering (Infinity should satisfy <= only in the negative-infinity case; +Infinity should not satisfy <= 0). (The doc confirms Infinity/−Infinity are valid BigDecimal values, but the exact truth table for each operator is not explicitly shown in the excerpts retrieved.) [2]. If you want robust handling, check nan? / infinite? before comparing: BigDecimal provides #nan? and #infinite? predicates [2].

Citations:


Reject non-finite exchange rates in the validator.

Float(value) accepts "NaN" and "Infinity" without raising exceptions—they produce valid Float objects. The setter's rescue clause only catches ArgumentError and TypeError, so these non-finite values pass through and are marked as valid. The validator's <= 0 check then fails to detect them because NaN comparisons have undefined behavior in Ruby. Explicitly reject non-finite values using nan? or infinite? predicates to prevent poisoning downstream currency conversions.

Applies to lines 21-29 (setter) and 79-87 (validator).

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

In `@app/models/trade.rb` around lines 21 - 29, The setter exchange_rate=
currently accepts "NaN"/"Infinity" because Float(value) doesn't raise; after
converting (normalized_value = Float(value)) add a check using
normalized_value.finite? (or normalized_value.nan?/infinite?) and if not finite
treat it as invalid (set "exchange_rate_invalid" => true and store the original
value or nil as appropriate) instead of marking valid; likewise update the
validator that performs the numeric <= 0 check (the method that validates
exchange_rate) to first reject non-finite values by checking value.finite? (or
value.nan?/value.infinite?) and mark them invalid before doing the <= 0
comparison so NaN/Infinity can't pass validation.

Comment thread app/models/transaction.rb
Comment thread app/models/transaction.rb
Comment on lines +151 to +156
def activity_security
return @activity_security if defined?(@activity_security)
return @activity_security = nil if activity_security_id.blank?

@activity_security = Security.find_by(id: activity_security_id)
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

activity_security adds an N+1 query path for transaction rows.

Line [155] performs a Security.find_by per transaction instance. In list rendering, this becomes one extra query per row. Please preload referenced securities in bulk and resolve from a map in the render context.

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

In `@app/models/transaction.rb` around lines 151 - 156, The activity_security
method currently does a per-instance lookup via Security.find_by(id:
activity_security_id), causing N+1 queries; change callers to preload all
required Security records (e.g., fetch Security.where(id:
transactions.map(&:activity_security_id)).index_by(&:id)) and replace the
per-row lookup with a map lookup (use the preloaded_map[activity_security_id])
or convert activity_security to read from an association and use ActiveRecord
preload/includes in the query that builds the transactions list; remove or
short-circuit the Security.find_by usage in the activity_security method (or
make it use a provided preloaded map) so rendering uses the bulk-loaded
securities instead of issuing individual queries.

Comment on lines +1 to +173
<% content_for :title, "Set Up Interactive Brokers Accounts" %>

<%= render DS::Dialog.new(disable_click_outside: true) do |dialog| %>
<% dialog.with_header(title: "Set Up Your Interactive Brokers Accounts") do %>
<div class="flex items-center gap-2">
<%= icon "chart-line", class: "text-primary" %>
<span class="text-primary">Select which IBKR brokerage accounts to link.</span>
</div>
<% end %>

<% dialog.with_body do %>
<div class="space-y-6">
<div class="bg-surface border border-primary p-4 rounded-lg">
<div class="flex items-start gap-3">
<%= icon "info", size: "sm", class: "text-primary mt-0.5 flex-shrink-0" %>
<div>
<p class="text-sm text-primary mb-2"><strong>IBKR Flex Query Import</strong></p>
<ul class="text-xs text-secondary space-y-1 list-disc list-inside">
<li>Holdings with current prices and quantities</li>
<li>Cost basis per position</li>
<li>Trades, dividends, commissions, and cash deposits or withdrawals</li>
</ul>
<p class="text-xs text-warning mt-2">
<%= icon "alert-triangle", size: "xs", class: "inline-block mr-1" %>
Historical activity is limited to the report window of the Flex Query
</p>
</div>
</div>
</div>

<% if @waiting_for_sync %>
<div id="ibkr-sync-spinner" class="flex flex-col items-center justify-center py-6 space-y-3">
<div class="animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
<p class="text-secondary text-center">Fetching accounts from Interactive Brokers...</p>
</div>
<div class="flex gap-3 justify-center">
<%= render DS::Link.new(
text: "Refresh",
variant: "secondary",
icon: "refresh-cw",
href: setup_accounts_ibkr_item_path(@ibkr_item),
frame: "_top"
) %>
<%= render DS::Link.new(
text: "Cancel",
variant: "ghost",
href: accounts_path,
frame: "_top"
) %>
</div>
<% elsif @no_accounts_found %>
<div class="no-accounts-found flex flex-col items-center justify-center py-6 space-y-3">
<%= icon "alert-circle", size: "lg", class: "text-warning" %>
<p class="text-primary text-center font-medium">No accounts found.</p>
<p class="text-secondary text-center text-sm">Sure could not find any IBKR accounts in the latest Flex report.</p>
</div>
<div class="flex gap-3 justify-center">
<%= render DS::Link.new(
text: "Back to Settings",
variant: "secondary",
href: settings_providers_path,
frame: "_top"
) %>
</div>
<% else %>
<%= form_with url: complete_account_setup_ibkr_item_path(@ibkr_item), method: :post, data: { turbo_frame: "_top" } do %>
<% if @unlinked_accounts.any? %>
<div class="space-y-4">
<h3 class="font-medium text-primary">Available accounts</h3>

<% @unlinked_accounts.each do |ibkr_account| %>
<div class="border border-primary rounded-lg p-4 hover:bg-surface transition-colors">
<div class="flex items-center gap-3">
<input type="checkbox"
id="account_<%= ibkr_account.id %>"
name="account_ids[]"
value="<%= ibkr_account.id %>"
checked
class="cursor-pointer">
<label for="account_<%= ibkr_account.id %>" class="flex-1 cursor-pointer">
<h4 class="font-medium text-primary"><%= ibkr_account.name %></h4>
<p class="text-sm text-secondary">
Investment •
Balance: <%= number_to_currency(ibkr_account.current_balance || 0, unit: Money::Currency.new(ibkr_account.currency || "USD").symbol) %>
</p>
<% if ibkr_account.ibkr_account_id.present? %>
<p class="text-xs text-secondary">Account ID: <%= ibkr_account.ibkr_account_id %></p>
<% end %>
</label>
</div>
</div>
<% end %>
</div>

<div class="flex gap-3 mt-6">
<%= render DS::Button.new(
text: "Create selected accounts",
variant: "primary",
icon: "plus",
type: "submit",
class: "flex-1"
) %>
<%= render DS::Link.new(
text: "Cancel",
variant: "secondary",
href: accounts_path,
frame: "_top"
) %>
</div>
<% end %>
<% end %>

<% if @unlinked_accounts.any? && @linkable_accounts.any? %>
<div class="border-t border-secondary pt-4 mt-2">
<p class="text-xs text-secondary mb-3">Or link a discovered IBKR account to an existing manual investment account:</p>
<div class="space-y-2">
<% @unlinked_accounts.each do |ibkr_account| %>
<%= form_with url: link_existing_account_ibkr_items_path, method: :post, data: { turbo_frame: "_top" } do |link_form| %>
<%= link_form.hidden_field :ibkr_account_id, value: ibkr_account.id %>
<p class="text-xs text-primary mb-1"><%= ibkr_account.name %></p>
<div class="flex items-center gap-2">
<%= link_form.select :account_id,
options_for_select(@linkable_accounts.map { |account| ["#{account.name} (#{number_to_currency(account.balance, unit: Money::Currency.new(account.currency || 'USD').symbol)})", account.id] }),
{ prompt: "Select an account..." },
class: "bg-container border border-primary rounded px-2 py-1 text-sm text-primary flex-1 min-w-0" %>
<%= render DS::Button.new(
text: "Link",
variant: "secondary",
size: "sm",
type: "submit"
) %>
</div>
<% end %>
<% end %>
</div>
</div>
<% end %>

<% if @linked_accounts.any? %>
<div class="<%= 'border-t border-primary pt-6 mt-4' if @unlinked_accounts.any? %>">
<h3 class="font-medium text-primary mb-4">Already linked</h3>
<% @linked_accounts.each do |ibkr_account| %>
<div class="border border-success/20 bg-success/5 rounded-lg p-4 mb-2">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<%= icon "check-circle", class: "text-success" %>
<div>
<h4 class="font-medium text-primary"><%= ibkr_account.name %></h4>
<p class="text-sm text-secondary">
Linked to: <%= link_to ibkr_account.current_account.name, account_path(ibkr_account.current_account), class: "link" %>
</p>
</div>
</div>
</div>
</div>
<% end %>
</div>

<% if @unlinked_accounts.blank? %>
<div class="flex justify-end mt-4">
<%= render DS::Link.new(
text: "Done",
variant: "primary",
href: accounts_path,
frame: "_top"
) %>
</div>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
<% end %>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add i18n support for all user-facing strings.

The view contains numerous hard-coded English strings (e.g., "Set Up Your Interactive Brokers Accounts", "Available accounts", "Create selected accounts", etc.) that should use the t() helper for proper internationalization. All user-facing text should be moved to config/locales/en.yml with appropriate hierarchical keys.

As per coding guidelines: Always use t() helper for user-facing strings. Organize i18n keys hierarchically by feature: ibkr_items.setup_accounts.title, ibkr_items.setup_accounts.info_box.title, etc.

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

In `@app/views/ibkr_items/setup_accounts.html.erb` around lines 1 - 173, Replace
all hard-coded user-facing strings in this view with i18n calls using the t()
helper and add corresponding keys under a hierarchical namespace like
ibkr_items.setup_accounts in config/locales/en.yml; specifically update
content_for :title, the DS::Dialog header title, the info box headings and
bullets, status messages (`@waiting_for_sync/`@no_accounts_found), form labels and
checkbox label text (e.g., account_<%= ibkr_account.id %> label), button and
link texts ("Create selected accounts", "Cancel", "Refresh", "Back to Settings",
"Link", "Done"), section headers ("Available accounts", "Already linked"), the
select prompt ("Select an account...") and any interpolated strings that include
currency/amounts so they use t() with interpolation where needed; ensure keys
follow the suggested hierarchy (e.g., ibkr_items.setup_accounts.title,
.info_box.title, .info_box.bullets.item1, .buttons.create_selected,
.status.fetching_accounts, .select.prompt, .linked.already_linked) and replace
the inline literals with t('ibkr_items.setup_accounts.xxx').

@jjmata
Copy link
Copy Markdown
Collaborator

jjmata commented May 9, 2026

Nice! If you can get the PR comments resolved I can merge quickly before this other work lands/creates conflicts.

EDIT: and the CI tests passing. :-)

@jjmata jjmata self-requested a review May 9, 2026 09:29
@gian-reto
Copy link
Copy Markdown
Author

@jjmata very cool, thanks! I'll probably have time later today to go through all the comments, implement the suggestions and make tests pass 👍

@gian-reto
Copy link
Copy Markdown
Author

Sorry, my bad... I didn't want to close this. Working on it!

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

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Development

Successfully merging this pull request may close these issues.

2 participants