mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 15:34:58 +00:00
2620653b2abeb090f0cb5d7f6e0075dfbe453d65
743 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2620653b2a |
fix(balance): derive waypoint start from day's flows to prevent double-counting and phantom bumps (#2031)
* fix(balance): fix double-counting on reconciliation waypoints with same-day transactions Waypoint branch was setting start = end = waypoint and passing real flows to build_balance. Since end_balance is a PG generated column that recomputes from flows, transactions were double-counted on waypoint days and the prior gap day inherited a phantom jump. Fix: pin only the end to the API value, derive start from the day's own flows (same as current_anchor). Transaction attributed once, gap day correct, investment cash/holdings split correct. Adds regression test + GUI breakdown test verified against real PG columns through UI::Account::BalanceReconciliation. Fixes #2007. * test(balance): add investment waypoint regression test Covers reconciliation waypoint + same-day trade on investment accounts: end_balance must match API-reported total (not double-count trade flows), cash/non-cash flows must be preserved, and gap day total must be correct. |
||
|
|
be2d3aa3bb |
fix(plaid): surface configuration/product-access errors from the Link flow (#1792) (#1991)
* fix(plaid): surface configuration/product-access errors from Link flow (#1792) * fix(plaid): harden Plaid Link onExit guard + nil-body JSON parse (#1792 review) * fix lint check issue * fix test unit check |
||
|
|
52083d5774 |
feat(reports): add Period Return card to Investment Performance (#1962)
* feat(reports): add Period Return card to Investment Performance tab Surfaces market-only return (absolute + %) for the selected period using net_market_flows from the balances table, excluding contributions and withdrawals. Appears in both the interactive report and the print view. * docs: remove TODOS.md; fold FX fallback caveat into PR description The single V2 item (Period Return's 1:1 FX fallback on missing rates) is now documented under Known Limitations in the PR description, so a tracked file in the repo root is redundant. * fix(investment_statement): align start_value denominator scope and FX handling Add status filter to match absolute_return, and move FX conversion into SQL so pre-period balances are found even when an account's currency was changed after balances were recorded. |
||
|
|
2e55bbe294 |
fix(jobs): delegate recurring-transaction sync gate to Sync.for_family (#1975)
* fix(jobs): delegate recurring-transaction sync gate to Sync.for_family `IdentifyRecurringTransactionsJob#family_has_incomplete_syncs?` hand-rolled the list of provider `*_items` associations it polled — plaid, simplefin, lunchflow, enable_banking, sophtron — missing nine other `Syncable` provider concerns on `Family`: coinbase, binance, kraken, coinstats, snaptrade, mercury, brex, indexa_capital, ibkr. When a sync on any of those nine was in flight, the debounce gate fell through and `RecurringTransaction::Identifier` ran against a partial dataset; the follow-up re-enqueue then hit the `find_or_initialize_by` upsert path and inherited the stale `occurrence_count`. Same drift pattern that bolted sophtron on as the 5th entry (#591) was already an iteration of. The maintainers' own `Sync.for_family` (sync.rb:61) already enumerates every `*_items` association via `Family.reflect_on_all_associations(:has_many)` filtered by inclusion of `Syncable` — exactly the helper the gate should delegate to so the list cannot drift again. - Add `Sync.any_incomplete_for?(family)` class method that wraps `for_family(family).incomplete.exists?`. - Rewrite `family_has_incomplete_syncs?` to delegate. 14 lines → 1. - New test file `test/jobs/identify_recurring_transactions_job_test.rb` covers in-flight Coinbase + Mercury (gate fires), idle (identifier runs), missing family, and superseded-by-newer-schedule. - `test/models/sync_test.rb` gets 2 new tests pinning `any_incomplete_for?` against a provider `_items` sync and a family-itself sync. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(jobs): stub Rails.cache.read for supersession test (NullStore in test env) `Rails.cache` is `ActiveSupport::Cache::NullStore` in the Rails test env, so the previous test's `Rails.cache.write(cache_key, @scheduled_at + 10, ...)` was a no-op and `Rails.cache.read(cache_key)` returned `nil`. The supersession short-circuit `return if latest_scheduled && latest_scheduled > scheduled_at` then fell through, the job proceeded to invoke `RecurringTransaction::Identifier`, and the Mocha `.expects(:identify_recurring_patterns).never` failed in CI. Switch to `Rails.cache.stubs(:read).with(cache_key).returns(...)` — the same idiom `test/models/provider/twelve_data_test.rb:186-197` already uses for the cache layer. Add an `assert_nil` on the bare `perform` return so Minitest's assertion counter sees an explicit assertion (silences the "missing assertions" warning). No production-code change. Behavior under test is unchanged; only the test mechanism for simulating "newer scheduled run already in cache" is fixed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
174f7e6be6 |
feat(binance): add full account sync and transaction processing (#1822)
* feat(binance): add full account sync and transaction processing - Fixed a bug that hindered Account setup - Wire up Binance accounts, sync statistics, and unlinked account tracking in the accounts dashboard. - Support setting a sync_start_date during Binance account setup. - Set Binance accounts' opening balance to zero to ensure the ledger builds cleanly from the actual trade history. - Expand the Binance importer and processor to handle Spot, Margin, Earn, P2P, and Futures trades and assets. - Implement TransactionBuilder to parse raw Binance trades, accurately calculating fees, base/quote asset amounts, and market values for proper ledger integration. - Update Binance API timeout (`recvWindow`) to 60,000ms to prevent connection drops. These changes provide comprehensive support for tracking Binance portfolios, ensuring accurate historical ledgers and proper visibility of sync statuses in the frontend dashboard. * refactor(binance): enforce strong params, double-entry safety, and native fiat currency support - Implement strong parameters in BinanceItemsController#complete_account_setup to satisfy Rails security guidelines. - Add robust date parsing with a grace fallback to prevent controller crashes on malformed sync start dates. - Wrap P2P transaction creations inside a database transaction block to guarantee ledger integrity and prevent orphan records. - Optimize P2P deduplication queries by batching checks for both transaction and funding external IDs. - Shift P2P entry persistence from forced USD tracking to native fiat values extracted directly from the Binance API payload. - Update BinanceAccount::ProcessorTest assertions and fixtures to validate native fiat and fee calculation logic. * fix(binance): process sync trades before caching transaction payload - Reorder Binance processor execution to insert trade records into the database prior to updating the `raw_transactions_payload` cache. This guarantees that if a database insertion fails, the cache won't prematurely mark the sync as successful, ensuring the data is retried on the next run. - Move `set_opening_anchor_balance(balance: 0)` out of the generic crypto exchange account builder and apply it specifically during Binance account creation. - Refactor date parsing in BinanceItemsController to explicitly catch `ArgumentError` via a block instead of using a blanket inline `rescue`. - Clean up the `setup_accounts` view template by removing hardcoded default translation strings. * fix(binance): enhance trade sync logic and error propagation - Pass `startTime` (from `sync_start_date`) to spot and futures trade endpoints on initial sync to optimize data fetching. - Include previously synced futures pairs alongside spot pairs when resolving relevant symbols to properly recover sold-out assets. - Re-raise exceptions in processor rescue blocks to prevent silent failures and ensure errors are correctly propagated to background jobs. - Decrease Binance API `recvWindow` from 60000ms to 5000ms to align with recommended default timeout values. |
||
|
|
a3609b81d3 |
fix(enable_banking): clear stuck pending flag when ASPSP reuses same transaction_id (#1982)
* fix(enable_banking): clear stuck pending flag when ASPSP reuses same transaction_id for booked version * fix: scope pending→booked bypass to user_modified entries only * refactor: extract clear_pending_flags_from_extra helper to deduplicate pending-flag removal logic * refactor: use clear_pending_flags_from_extra in user_modified bypass path * fix(provider_import_adapter): add type check in clear_pending_flags_from_extra Add a check to ensure that the value associated with a provider key in the `extra` hash is a Hash before attempting to call `delete` on it. This prevents a `NoMethodError` when encountering malformed data where the provider key exists but does not map to a Hash. * fix(provider_import_adapter): fix indentation and ensure proper return in clear_pending_flags_from_extra * fix(provider_import_adapter): make clear_pending_flags_from_extra private * fix: guard clear_pending_flags_from_extra against non-Hash extra values |
||
|
|
b8ebb24e8b |
fix(holdings): carry provider cost_basis forward to calculated rows past snapshot date (#1818)
* fix(holdings): carry provider cost_basis forward to calculated rows Providers like IBKR Flex emit holdings on report_date and only include trades within the query window. The reverse calculator + gapfill therefore produces rows past report_date with nil cost_basis, even though the provider supplied a basis on the snapshot. That nil basis silently blanks `Trend`, the Reports "Total Return" card, the Top Holdings return column, and Gains by Tax Treatment, because every one of them gates on `holding.avg_cost`. When a calculated row would otherwise have no usable cost_basis, backfill it with the most recent provider-supplied cost_basis for the same (security, currency) on or before the holding date. Existing calculated/manual values are preserved (they outrank a provider carry-forward), and existing provider carry-forwards are refreshed when a newer snapshot supersedes them. * - Fix currency mismatch: provider snapshots were keyed by (security_id, currency) but calculated rows use account currency while IBKR provider rows use the security's native currency (e.g., USD vs EUR). Now keyed by security_id only; carry_forward_provider_cost_basis converts via Money#exchange_to at the snapshot date (same convention as ReverseCalculator for trade prices), with a ConversionError fallback. - Trim long inline comment to three lines - Fix safe-nav inconsistency: existing.cost_basis.positive? -> existing&.cost_basis&.positive? - Add test: refreshes stale carry-forward when a newer provider snapshot arrives - Add test: carry-forward is a no-op for forward-strategy accounts with no provider holdings * fix(holdings): prevent overwriting zero-valued manual cost basis Ensure that manual cost basis entries with a value of zero (e.g., for free shares) are not overwritten by provider carry-forward values during materialization. Additionally, updated the logic to allow zero-valued manual or calculated cost bases to be preserved, and added tests to verify currency conversion and error handling during cost basis carry-forward. * refactor(holdings): allow zero-valued cost basis in provider snapshots Remove the filter that restricted provider cost basis snapshots to values greater than zero. This ensures that manual cost basis entries with a value of zero (e.g., for free shares) are correctly captured and available for carry-forward logic. * perf(holdings): optimize provider cost basis snapshot lookup Filter provider cost basis snapshots by the security IDs present in the current holdings set to reduce the amount of data loaded into memory. * refactor(holdings): move PortfolioCache FX fix to dedicated branch Remove date-accurate exchange rate fix from this branch — it has been split into fix/portfolio-cache-historical-fx-rate to keep concerns separate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * revert(portfolio_cache): restore date-accurate FX in get_price |
||
|
|
ab52b2b144 |
fix(family-sharing): prevent silent data loss when rehoming or removing users (#1896)
* fix(family-sharing): prevent silent data loss when rehoming or removing users Fixes #1689. Two destructive paths could strand a pre-existing user's family and accounts: 1. Invitation#accept_for unconditionally overwrote user.family_id, orphaning the prior family + its accounts with no user able to reach them. 2. Settings::ProfilesController#destroy then called @user.destroy when an admin removed the rehomed member, destroying the only login path back to the now-orphaned data. Add hard-block guards on both paths. accept_for refuses when the invitee already belongs to a family with accounts; ProfilesController#destroy refuses when the member owns accounts in another family (legacy state from the old flow). InvitationsController#create surfaces a specific, actionable flash so the admin understands why the auto-accept was refused. No automatic recovery of already-orphaned data — that needs a separate one-shot script per dosubot's analysis on the issue. * fix(family-sharing): scope invite orphan-guard to invitee-owned accounts (#1896 review) Codex flagged (P1) and the maintainer review independently raised that would_orphan_existing_family? keyed off user.family.accounts.exists? — any account in the invitee's current family — which wrongly blocked a non-owner member from leaving a multi-user household. Rename to would_orphan_owned_accounts? and key off user.owned_accounts.where.not(family_id: family_id), making the invite guard symmetric with the destroy-path guard in Settings::ProfilesController. A member who owns no accounts now orphans nothing by moving and is free to accept the invitation; an owner is still blocked. Add a regression test for the non-owner case and update the existing tests to give the invitee explicit account ownership. * Remove extra comments per project conventions --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com> |
||
|
|
3e2990a52c |
feat(ibkr): compute net_market_flows from IBKR equity equity delta and trade flows (#1970)
* feat(ibkr): compute net_market_flows from IBKR equity delta and trade flows
Replace the hardcoded net_market_flows: 0 in HistoricalBalancesSync with an
exact derivation from IBKR's own equity summary data, eliminating any
dependency on third-party security price providers for Period Return.
Formula: nmf = Δnon_cash - net_buy_sell
- non_cash = IBKR equity total - materializer cash (exact per IBKR)
- net_buy_sell = sum of trade amounts converted to base currency using
the stored fx_rate_to_base (IBKR's own FX rate, already on Trade#exchange_rate)
Sets non_cash_adjustments = net_buy_sell so the virtual column identity
(end_non_cash_balance = start + nmf + adjustments) resolves to IBKR's
exact equity figure.
* test(ibkr): add sell-trade and no-trade nmf tests; fix memoization guard
- Add test: sell trades (negative amount) correctly isolate market loss in nmf
- Add test: no-trade scenario produces nmf = full Δnon_cash
- Fix: `return {} unless account` inside ||= exited the method without memoizing;
restructure to `if account ... else {} end` so the result is always cached
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ibkr): exclude dividend/interest trades from net_buy_sell; use historical FX date
Addresses two issues flagged in code review:
- P1: trades with qty=0 (Dividend, Interest) were included in net_buy_sell,
inflating/deflating nmf on dates with income events. Filter to qty != 0 at
the SQL level so only buy/sell trades affect the market-flow calculation.
- P2: Money#exchange_to defaulted to Date.current when no custom_rate was
stored, causing historical nmf to drift as FX rates change over time.
Pass date: entry.date so the fallback lookup uses the trade's own date.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(ibkr): cover Money::ConversionError fallback in trade_flows_by_date
Adds a test that stubs Money#exchange_to to raise ConversionError for a
cross-currency trade with no stored exchange_rate, verifying that the
rescue clause falls back to entry.amount and that nmf and
end_non_cash_balance still resolve correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ibkr): log warning when FX conversion falls back to unconverted amount
When Money::ConversionError is raised for a cross-currency trade with no
stored exchange_rate, warn with entry currency, account currency, date,
amount, and entry/account IDs so the silent fallback is visible in logs.
Same-currency ConversionErrors (unexpected but possible) stay silent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ibkr): skip unconvertible FX trades, redact log, tighten join
- On Money::ConversionError, skip the entry from net_buy_sell rather
than falling back to the raw amount (which treated e.g. EUR as CHF);
nmf now absorbs the full Δnon_cash for that date instead of silently
misstating period return
- Remove entry amount, entry ID, and account ID from the FX warning log
to avoid exposing financial data in log output
- Consolidate entryable_type guard into the JOIN condition rather than a
separate WHERE clause
- Add inline comment on the first-day zero case to distinguish intent
from a bug
- Update ConversionError test to assert skip behavior (nmf=200, not 50)
* fix(ibkr): exclude dates with unconvertible FX trades from balance upsert
* fix(ibkr): skip upsert_all when all balance rows are filtered by failed FX dates
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
8f5454ad29 |
fix(settings): preserve OpenAI form input on validation failure (#1862)
* fix(settings): preserve OpenAI form input on validation failure Fixes #1824. The OpenAI settings form auto-submits on blur, so typing the URI base before the model triggers cross-field validation. The rescue re-renders the page with values read from Setting.openai_*, which is still blank because the failed save was rejected — so the user's input disappears and they see 'OpenAI model is required' with no value to fix. Stash the submitted uri_base and model on rescue and prefer them over the saved Setting when rendering, so the user can finish typing the missing field and re-submit. * test(settings): cover openai_model preservation on validation fail (#1862) jjmata asked for symmetric coverage of the model field. Add a test where the user changes the URI base and clears the model in the same submit: the cross-field validation fails and the re-rendered model input must reflect the submitted (cleared) value rather than reverting to the saved model. Complements the existing uri_base preservation test. |
||
|
|
98ca1608f4 |
fix(enable_banking): match bank list search against BIC, not just name (#1874)
* fix(enable_banking): match bank list search against BIC, not just name Bank-search filter on the Enable Banking bank-selection modal only indexed `aspsp[:name]`, so users searching by BIC code (e.g. `INGDDEFF`) got no results even when the bank was rendered in the list. Switch the per-item data attribute to a `name + BIC` haystack and read from it in the Stimulus controller, so either token matches. Refs #1814 * style(bank_search): apply Biome formatting to forEach callback (#1874 review) |
||
|
|
eca8c6ce1f |
fix : account destroyed cascade transfer destruction then … (#1795)
* fix: cascade destroy transfers and reset transaction kind on account destruction. * Add rescue no method to transfer transaction reset --------- Co-authored-by: arumaio <aruma.pro+git@protonmail.com> |
||
|
|
ea51612ac7 |
refactor(views): migrate 6 residual inline alerts to DS::Alert (#1933)
* refactor(views): migrate 6 residual inline alerts to DS::Alert PR #1731 extended DS::Alert and migrated 9 inline alert blocks. Six hand-rolled alert blocks slipped through that sweep and stayed on raw palette tokens with no `theme-dark:` variants: - `app/views/settings/llm_usages/show.html.erb` — "About Cost Estimates" blue info block. Most visible offender: `bg-blue-50 border border-blue-200` + `text-blue-900 / text-blue-700 / text-blue-600` rendered as a bright white-blue island in dark mode (the bug spotted on the LLM usage page). - `app/views/accounts/confirm_unlink.html.erb` — yellow warning with bullet list. - `app/views/oidc_accounts/new_user.html.erb` — blue info heading. - `app/views/oidc_accounts/link.html.erb` — two blocks (yellow verify warning + blue create info). Also flips the file's pre-existing `text-gray-600` hint paragraph to `text-secondary` (caught by the `DeprecatedClasses` erb_lint rule on save). - `app/views/rules/confirm.html.erb` — AI cost notice. - `app/views/rules/confirm_all.html.erb` — AI cost notice. All six migrate to `DS::Alert.new(title:, variant:)` (with a block content slot for the rich/conditional bodies). DS::Alert resolves `bg-info/10`, `border-info/20`, etc. from the `@theme` semantic tokens, so dark mode now renders a subtle blue/yellow tint over the page surface instead of a hardcoded light-mode pill. Out of scope (left as-is, not alert-shaped): - `app/views/assistant_messages/_tool_calls.html.erb` — a tool-call display panel (not an alert; needs its own token sweep). - `app/views/import/rows/_form.html.erb` — inline cell-error tooltip (`bg-red-50 border border-red-200`) — also not alert-shaped; a future PR can swap it to `bg-destructive/10 border-destructive-subtle` once #1932 lands. Surfaced while scanning DS drift for the LLM usage page bug. Tracking issue: #1715 (closed but conceptually relevant) / #1911 (active drift patrol). * fix(oidc): keep alert description in <p>, retarget tests for DS::Alert title CI on #1933 caught three test failures introduced by migrating the two OIDC link alerts and the verify-redirect copy from hand-rolled `<h3>` / `<p>` markup to `DS::Alert`: 1. `OidcAccountsControllerTest#test_should_show_create_account_option_for_new_user` 2. `OidcAccountsControllerTest#test_does_not_show_create_account_button_when_JIT_link-only_mode` 3. `SessionsControllerTest#test_redirects_to_account_linking_when_no_OIDC_identity_exists` DS::Alert renders its `title:` slot as a `<p>` (semantically the alert heading lives on the container's `aria-labelledby`, not on a heading tag) and renders block / message content directly inside a `<div>`, not a `<p>`. The pre-migration markup used `<h3>` for the heading and `<p class="...text-blue-700">` for the description, so the tests above asserted those specific tags. Two fixes: - `app/views/oidc_accounts/link.html.erb` — wrap the html_safe description bodies in explicit `<p>` tags inside the DS::Alert block. Restores the `<p>` element the session-redirect test asserts on, and keeps the description as a semantic paragraph rather than a bare text node inside the alert container. - `test/controllers/oidc_accounts_controller_test.rb` — flip the two `assert_select "h3", text: "Create New Account"` calls to match the DS::Alert title `<p>`. The test was asserting an implementation detail of the pre-migration markup; switching to the new tag keeps the assertion meaningful (the heading text still has to render) without re-introducing an `<h3>` outside of DS::Alert. * fix(test): match Create New Account title with regex (sr-only "Info:" prefix) DS::Alert prepends `<span class="sr-only">Info:</span>` inside the title `<p>`, so the full text content is "Info: Create New Account", not "Create New Account". `assert_select "p", text: "Create New Account"` requires an exact text match and rejected the prefixed string. Switch to a regex match — keeps the heading-text assertion meaningful without coupling to the screen-reader prefix. |
||
|
|
cc8e2abf18 |
fix(design-system): DS::Menu add :icon_sm variant for dense action lists (#1930)
PR #1840 bumped DS::Button icon-only `:md` size from `w-9 h-9` (36×36) to `w-11 h-11` (44×44) for WCAG 2.5.5 enhanced touch target. DS::Menu's `:icon` variant uses DS::Button at the default `:md` size, so every row-level "..." action-list trigger grew from 36×36 to 44×44. For dense lists where each row has a trigger — most visibly the transaction category dropdown (`category/dropdowns/_row.html.erb`) — the per-row height bump (+8px) compounds: a 5-category panel that used to fit in ~220px now wants ~260px, the badges look smaller relative to the row chrome, and the overall density that made the dropdown scannable regresses visibly. Add an `:icon_sm` variant that renders the trigger as DS::Button at `size: :sm` (32×32). Meets WCAG 2.5.8 AA (24×24) — appropriate for compact in-row triggers where 44×44 isn't required. Standalone toolbar / row-action `...` triggers should keep `:icon` for AAA. Migrate `category/dropdowns/_row.html.erb` to `:icon_sm` to restore the pre-#1840 dropdown density. |
||
|
|
20844923e6 |
refactor(transactions): migrate 5 transaction badges to DS::Pill (#1751 PR B) (#1917)
Migrates the hand-rolled "Pending" / "Review recommended" / "Potential duplicate" / "Split" badges across the transaction views to the extended DS::Pill primitive from #1902. **Visual contract for badge mode** In #1902 the badge mode (`marker: false`) used `rounded-md` (chip shape) because the marker mode does. But every existing pill / status badge in the codebase uses `rounded-full` — see `settings/providers/_status_pill.html.erb`, `settings/providers/_maturity_badge.html.erb`, and the inline transaction badges this PR is migrating. To keep the visual contract consistent, this PR shifts `DS::Pill`'s badge mode to `rounded-full` (marker mode stays `rounded-md`, unchanged from #1829). The shape distinction now reads: markers are tags, badges are pills. **Callsites migrated** (5): - `app/views/transactions/_transaction.html.erb` — Pending, Review-recommended, Possible-duplicate, Split badges - `app/views/transactions/_header.html.erb` — Pending badge - `app/views/transactions/_split_parent_row.html.erb` — Split badge **Tone mapping** | Badge | Tone | Notes | |---|---|---| | Pending | `:neutral` | unchanged copy/icon, gains subtle DS-controlled bg | | Review recommended | `:neutral` | matches existing `bg-surface-inset` look | | Possible duplicate | `:warning` | DS semantic alias for the existing `text-warning` | | Split | `:neutral` | matches existing `bg-surface-inset` look | **Deferred to follow-up PRs** - `app/views/transactions/_transfer_match.html.erb` — uses two responsive-visibility variants (`hidden lg:inline-flex` for long copy, `inline-flex lg:hidden` for short). DS::Pill currently has no `class:` arg for caller-controlled wrapper classes; deferring until that lands. - `app/views/transactions/searches/filters/_badge.html.erb` — has a close button alongside the label (`button_to clear_filter_*`) and uses `rounded-3xl p-1.5` instead of a true pill. Closer to a removable filter chip — better fit for a separate `DS::FilterChip` primitive than for `DS::Pill`. Refs #1751. |
||
|
|
09058b0cc6 |
feat(design-system): extend DS::Pill with badge mode + semantic tones (#1751 PR A) (#1902)
* feat(design-system): extend DS::Pill with badge mode + semantic tones (#1751) Adds two extensions to the existing `DS::Pill` (originally landed as a stage marker primitive in #1829) so it can also serve as the shared status / category badge across the app — the use case tracked by #1751. **Badge mode (`marker: false`)** The original `DS::Pill` was intentionally sub-12px (text-[10px] / text-[11px]) + uppercase + tracking-wide so it reads as a marker (`Beta`, `Canary`, `NEW`), not a label. That shape is wrong for status badges where the surrounding context is regular UI copy and the pill needs to feel like a chip (`Pending`, `Active`, `Past due`, `Failed`). The new `marker: false` flag drops the uppercase + arbitrary sub-12px text and snaps the chrome to the DS text scale: - `marker: false, size: :sm` → `text-xs` (12px), normal case - `marker: false, size: :md` → `text-sm` (14px), normal case - `marker: true` (default) → existing #1829 behavior, unchanged **Semantic tone aliases** Status badges read more naturally with semantic tone names than with the underlying palette colors: | Alias | Resolves to | |---|---| | `:success` | `:green` | | `:warning` | `:amber` | | `:error` / `:destructive` | `:red` (new tone, added here) | | `:info` | `:indigo` | | `:neutral` | `:gray` | Visual-name tones (`:violet`, `:indigo`, `:fuchsia`, `:amber`, `:green`, `:gray`, `:red`) still work as before — semantic aliases resolve through `SEMANTIC_TONE_ALIASES` at component init time, so the callsite can pick whichever name reads better. Unknown tones still fall back to `:violet` (existing behavior). **Red palette** Adds the `:red` tone (palette already present in `design/tokens/sure.tokens.json` — `red-50/100/200/500/700/tint-10`). Needed for `:error` / `:destructive` status badges. **Icon slot** Adds an `icon:` option (already documented in the component's doc-comment as planned). When set, the Lucide glyph replaces the colored dot inside the pill — useful for status badges that read better with a glyph (`circle-check`, `triangle-alert`, `loader`, etc.) than the generic dot. **Scope** API + tests + Lookbook preview only. No callsite migrations in this PR — that's the next slice of #1751, done as separate per-bucket PRs (transaction badges, provider badges, misc) to keep diffs small. DS::Pill currently has no in-app callsites (#1829 shipped the primitive ahead of consumers), so this is a pure-additive change. Existing API is fully backwards-compatible — `marker:` defaults to `true`, so without that flag the pill renders exactly as it does today. * fix(test): use assert_no_selector for dot-suppression assertion `refute_selector ..., count: 1` only fails when there are exactly 1 matches — it would silently pass for 0 OR 2+. The intent is "no dots should render when an icon is set"; `assert_no_selector` strictly asserts zero matches. Flagged by coderabbit on #1902. |
||
|
|
655895341d |
feat(imports): verify Sure NDJSON import readback (#1869)
* feat(imports): verify Sure NDJSON readback * fix(imports): tighten Sure readback verification * fix(imports): polish Sure verification review nits |
||
|
|
e21ab9819f |
feat(dashboard): zoom into cashflow sankey categories (#1807)
* feat(dashboard): zoom into cashflow sankey categories Click a category node on the dashboard cashflow Sankey to focus on it and its descendants only; a back button restores the full view. Clicking the Cash Flow node zooms to the expense (outbound) side. - Pure utility (app/javascript/utils/sankey_zoom.js) computes the descendant subgraph from a clicked node, with direction inferred by reachability from the cash flow node (outbound for expense, inbound for income). - Stable node ids emitted from the controller so the JS can identify nodes across re-renders. - Stimulus controller adds chart + zoomOutButton targets, fade transition, and only sets a pointer cursor when a node has children. - Node:test coverage for expense, income, cash-flow, and malformed-data cases; \"type\": \"module\" added to package.json so the .js util is ESM-compatible under Node. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(dashboard): extract cashflow sankey chart partial Deduplicate sankey chart markup between inline and expanded dialog views, and reset zoom state when chart data changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(js): rename sankey_zoom util to .mjs to drop project-wide ESM flag Removes "type": "module" from package.json to avoid implicitly switching every .js file in the project to ESM (a future footgun for any .js config file added by Biome, Vite, etc.). Renames the utility to .mjs so node --test can import the ES module directly, and adds an explicit importmap pin since pin_all_from only globs .js/.jsm. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(assets): register .mjs MIME type for Propshaft Propshaft derives Content-Type from Mime::Type.lookup_by_extension, which returns nil for :mjs by default. Browsers refuse to execute ES modules served with an empty Content-Type, breaking the sankey_zoom util loaded via importmap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
12785754c8 |
feat(design-system): split DS::Menu into strict action-list + new DS::Popover (#1850)
* feat(design-system): split DS::Menu into strict action-list + new DS::Popover for mixed content Closes #1743. DS::Menu used to absorb both action-list dropdowns (row context menus, "more actions") AND mixed-content panels (user-account dropdown, filter forms, picker pop-ups). The two shapes carry incompatible a11y contracts: - **Action list**: `role="menu"` container, `role="menuitem"` children, Up/Down arrow nav per WAI-ARIA APG. - **Mixed content**: NO menu role — `role="menu"` restricts AT users to menuitem-only navigation and breaks any panel with forms, headings, or generic groupings. This PR splits the component: ## DS::Menu (tightened) Strict action-list primitive. Variants reduced to `:icon` and `:button` (no `:avatar`). `custom_content` slot removed. Bakes in: - `role="menu"` on the panel, `aria-haspopup="menu"` + `aria-expanded` + `aria-controls` on the trigger. - `role="menuitem"` + `tabindex="-1"` on every DS::MenuItem; the controller installs roving tabindex (first item gets `tabindex="0"` when the menu opens) and handles ArrowUp/Down/Home/End + Escape + Enter/Space activation. - `role="separator"` on the divider variant. - Stable per-instance `menu-<8-char hex>` id so the trigger's `aria-controls` resolves correctly. `DS::Menu.new(variant: :avatar, ...)` now raises ArgumentError pointing at DS::Popover. ## DS::Popover (new) Positioned panel for **mixed**, **non-action-list** content: account menus, picker forms, filter forms, embedded controls. Slots: `button`, `header`, `custom_content`. Variants: `:icon`, `:button`, `:avatar`. NO `role="menu"` — the panel announces as a generic dialog-popup (`aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`). Mirrors DS::Menu's floating-ui positioning + Escape/outside-click lifecycle in its own Stimulus controller (`DS--popover`). Avatar variant ships a focus ring + bumped touch target (44×44 via `w-11 h-11` per #1738). ## Migrated callsites (7 → DS::Popover) - `app/views/users/_user_menu.html.erb` — avatar trigger + profile header + nav links (items kept as DS::MenuItem inside `custom_content` for visual parity) - `app/views/categories/_menu.html.erb` — turbo-framed category picker - `app/views/budgets/_budget_header.html.erb` — budget picker - `app/views/reports/index.html.erb` — period picker - `app/views/holdings/_cost_basis_cell.html.erb` — cost-basis edit form - `app/views/transactions/searches/_form.html.erb` — filter form - `app/components/UI/account/activity_feed.html.erb:70` — status checkboxes (the row-level "new" menu on line 9 stays as DS::Menu) The other 33 DS::Menu callsites stay as-is — pure action lists. Locale: `ds.popover.avatar_default_label` + `users.user_menu.aria_label` keys added (en only; other locales handled in a separate i18n pass). * fix(test): update sidebar user-menu selector for Menu→Popover migration The user-menu now renders as `DS::Popover` (variant: :avatar) instead of `DS::Menu` after the menu split, so its trigger carries `data-DS--popover-target="button"` rather than the old `data-DS--menu-target`. Update the sidebar-driven settings test helper to match — every system test that drives Settings via the sidebar gates on this selector. * fix(review): DS::Popover/Menu trigger a11y + caller-attr preservation - popover.rb / menu.rb: button slot now merges (not overwrites) caller- provided data and aria hashes, sets aria-haspopup/expanded/controls on the :button variant, defaults type="button" on block-rendered buttons. - menu.rb / menu.html.erb: drop renders_one :header (strict-menu API shouldn't expose an arbitrary-markup escape hatch); preview updated. - menu_controller.js: handle Enter/Space activation on focused menuitem so keyboard navigation matches the ARIA menu pattern. - cost_basis_cell / transactions/searches/_menu: retarget cancel button data-action from DS--menu#close to DS--popover#close (host controller changed in the migration). * fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai> * fix(review): MenuItem roving: false for DS::Popover usage Codex P1 on #1850: \`DS::MenuItem\` hard-codes \`tabindex=\"-1\"\` and \`role=\"menuitem\"\` for both link and button variants — correct inside \`DS::Menu\` (which provides arrow-key roving and announces \`role=\"menu\"\`), but breaks every \`DS::MenuItem\` rendered inside \`DS::Popover\` (\`app/views/users/_user_menu.html.erb\`). Popover has no roving handler, so Tab skips every item — Settings, Changelog, Feedback, Contact, Log out become keyboard-unreachable. Add a \`roving:\` keyword (default \`true\`) to \`DS::MenuItem\` that gates both \`tabindex=\"-1\"\` and \`role=\"menuitem\"\`. \`DS::Menu\` callers keep the default (roving menu semantics intact). Pass \`roving: false\` from \`_user_menu.html.erb\` so user-menu items land in the normal Tab order. Existing \`menu.with_item(...)\` callers in the design system still default to \`true\`, so no behavior change for \`DS::Menu\` consumers. * fix(review): make menuitem_attrs authoritative on roving CodeRabbit Major on #1850: \`merged_opts\` was splatted AFTER \`menuitem_attrs\` in \`DS::MenuItem#wrapper\`, so a stray \`role: :button\` or \`tabindex: 0\` from a \`menu.with_item(..., role: …)\` caller could silently downgrade the \`DS::Menu\` ARIA contract that \`menuitem_attrs\` enforces. Strip \`:role\` and \`:tabindex\` from \`merged_opts\` whenever \`roving\` is enabled, then splat \`menuitem_attrs\` last. When \`roving: false\` (popover usage in \`_user_menu.html.erb\`) callers keep full control — Tab order and explicit ARIA stay tunable by the caller. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <noreply@coderabbit.ai> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
e30ccd94af |
fix(design-system): DS::Tooltip a11y — focusable trigger, keyboard parity, Esc dismiss (#1845)
* fix(design-system): DS::Tooltip a11y — focusable trigger, keyboard parity, Esc dismiss Closes #1747. Five fixes on the tooltip primitive. 1. **Tooltip anchor not in a11y tree.** The trigger was a bare Lucide icon, which Lucide renders with `aria-hidden="true"`. The tooltip target had `role="tooltip"` but nothing referenced it, so AT users had no way to discover the description. Wrap the icon in a focusable `<button type="button">` with `aria-describedby="<tooltip-id>"` so the underlying icon stays `aria-hidden` and the button picks up the description binding. 2. **Stable per-instance id.** Each DS::Tooltip now mints a `tooltip-<8-char hex>` id wired between the trigger's `aria-describedby` and the tooltip's `id`. 3. **Keyboard parity.** Hover-only triggers locked keyboard-only users out. Add `focusin` / `focusout` listeners on the controller element so Tab onto the trigger reveals the tooltip, Tab away dismisses it. 4. **Esc-to-dismiss.** Matches the WAI-ARIA tooltip pattern. `Escape` while the tooltip is open closes it without removing focus from the trigger. 5. **Resize-safe width cap.** Replace the hard-coded `max-w-[200px]` with `max-w-[20rem]` so the tooltip scales with the user's root font-size setting (large-text accessibility pref). Slightly wider visual cap (320px @ default) but no longer clips on text-zoom. Plus: docstring note that tooltip content must be non-interactive (no buttons / links / form controls inside) — `aria-describedby` exposes content as a description, not as an interactive subtree. Callers needing actions should reach for a popover/menu primitive. API unchanged. Existing 30+ DS::Tooltip callsites work without modification — they all pass `text:`-only payloads, which still render correctly under the new markup. * fix(review): as: option + alpha focus-ring on DS::Tooltip Addresses two AI review findings on #1845: 1. **Button-inside-summary spec violation.** Wrapping the icon in `<button>` regressed keyboard/AT behavior at 13 callsites where DS::Tooltip lives inside a `<summary>` (8 provider items, lunchflow disclosure, activity_date, 4 simplefin badges). HTML's content model forbids interactive content inside `<summary>`; browsers and AT can drop focus or conflate activation with the disclosure toggle. Add `as:` parameter — default `:button` preserves the standalone a11y wrap; `:span` renders a non-focusable wrapper for summary-nested usage. `focusin` bubbles up to the controller from the ancestor `<summary>`, so keyboard tooltips still appear on tab. Migrate the 13 in-summary callsites to `as: :span`. 2. **Raw palette focus ring → alpha tokens.** Swap `outline-gray-900 theme-dark:focus-visible:outline-white` to the established focus-ring pattern `focus-visible:ring-2 focus-visible:ring-alpha-black-300 theme-dark:focus-visible:ring-alpha-white-300` — matches the DS::Toggle fix landed in #1843 review and provider_card / form-field tokens. * fix(review): bind tooltip focus on ancestor <summary> Codex P2 follow-up on #1845: \`as: :span\` renders a non-focusable trigger inside the disclosure \`<summary>\`. Keyboard users hit Tab and focus lands on the summary itself; \`focusin\` fires on the summary and bubbles UP — never down to a descendant span — so the existing listener on \`this.element\` never fires and the tooltip stays hidden for keyboard-only users on every in-summary row (provider _item partials, lunchflow disclosure, activity_date, simplefin badges). My earlier reply that the focusin "bubbles up to the Stimulus controller on the outer span" was wrong about the direction; \`focusin\` only bubbles upward. In \`addEventListeners\`, resolve \`this.element.closest("summary")\` and bind \`focusin\` / \`focusout\` / \`keydown\` on it too. Track the ancestor on the controller and undo the bindings in \`removeEventListeners\` so reconnect-on-Turbo cycles don't leak. Update the template comment to reflect the actual mechanism. * docs(ds-tooltip): correct as=:span comment to match controller mechanism --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
e8ce28648d |
refactor: rename beta features gate to preview features (#1837)
* refactor: rename beta features gate to preview features Renames the opt-in gate introduced in PR #1829 from "beta" to "preview". Same shape (per-user JSONB toggle, `before_action` concern, marker pill) just retitled so the surface speaks the language Sure uses elsewhere ("preview" reads as in-progress, "beta" had baggage with provider maturity copy and external testing programs). Renames: - BetaGateable -> PreviewGateable - require_beta_features! -> require_preview_features! - beta_features_enabled? -> preview_features_enabled? - preferences["beta_features_enabled"] -> preferences["preview_features_enabled"] - DS::Pill default label "Beta" -> "Preview" - Settings -> Preferences toggle copy "beta features" -> "preview features" - config/locales/views/beta/ -> config/locales/views/preview/ - docs/llm-guides/gating-a-beta-feature.md -> gating-a-preview-feature.md Includes a data migration that copies any existing `beta_features_enabled` JSONB key into `preview_features_enabled` so early opt-ins survive the rename, then removes the old key. The migration is fully reversible. Provider maturity copy ("maturity.beta = Beta" under Settings -> Bank sync) is intentionally untouched - that's a separate concept describing a provider's integration stability, not Sure's feature gate. * review: apply CodeRabbit findings on PR #1837 - Settings::PreferencesController#update now routes the `preview_features_enabled` input through strong params and casts via ActiveModel::Type::Boolean instead of reading raw params and string- comparing to "1". Matches Sure's controller convention for permitted params and avoids stringly-typed boolean handling. - Rename migration now wraps the destination JSONB key write in COALESCE so a row that somehow ends up with both keys keeps the destination value instead of having it overwritten by the source. Up and down paths get the same defensive shape. * 📝 CodeRabbit Chat: Implement requested code changes * 📝 CodeRabbit Chat: Implement requested code changes * fix: restore all missing translation keys; rename beta→preview label * fix: restore all missing sections (appearances, debugs, llm_usages, providers, etc.); rename beta→preview * fix: restore missing keys (member_removal_failed, confirm_delete, etc.); add preview section * fix(i18n/ca): use 'està en vista prèvia' instead of 'és una vista prèvia' * fix(i18n/ca): use 'en desenvolupament'; drop article in preview title * fix(i18n/es): use 'en desarrollo' instead of 'en progreso' * fix(i18n/ca): use 'funcions experimentals' instead of 'vista prèvia' * fix(i18n/es): use 'funciones experimentales' instead of 'vista previa' * fix(i18n/ca): use 'funcions experimentals' in preferences.show.preview * fix(i18n/es): use 'funciones experimentales' in preferences.show.preview * fix(i18n/ca): use 'Experimental' pill label instead of 'Vista prèvia' * fix(i18n/es): use 'Experimental' pill label instead of 'Vista previa' --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> |
||
|
|
1ddd8bd040 |
feat(i18n): complete Catalan translations + extract residual hardcoded strings (#1836)
* feat(i18n): complete Catalan translations + extract residual hardcoded strings
CA coverage
- All view/model/breadcrumb/doorkeeper/mailer locale files for ca: 0 missing
keys (was ~3,400). Translations follow informal "tu" register, sentence case,
domain glossary (Compte/Saldo/Transacció/Posició/Operació/Pressupost/...).
- Catalan pluralization test: ca uses one/other; mirrors
test/lib/polish_pluralization_test.rb.
- 8 LanguageTool-flagged grammar fixes applied (Connexió òrfena, Secret de
l'API, comma-pero, apostrophe elisions, etc).
Hardcoded string extraction (also fixes EN parity)
- UI::Account::Chart#title + chart.html.erb view tabs -> UI.account.chart.*
- UI::Account::BalanceReconciliation labels + tooltips ->
UI.account.balance_reconciliation.{labels,tooltips}.*
- transactions/_transfer_match.html.erb (Auto-matched, A/M, Confirm/Reject
match, Payment/Transfer is confirmed) -> transactions.transfer_match.*
- AccountOrder labels (Name/Balance asc/desc) -> account_order.* keys with
fallback to existing hardcoded labels.
- Depository::SUBTYPES surface in account list -> depositories.subtypes.*.*
- User role badge -> users.roles.* (admin / member / super_admin).
- 110+ country names -> countries.* (config/locales/countries.ca.yml).
Breadcrumb locale fix
- Breadcrumbable was a before_action that ran before Localize's around_action
switched I18n.locale, so default crumbs rendered in EN even when locale=ca.
- Convert to helper_method that defers translation to render-time (when
I18n.locale is already correct). Add all missing breadcrumb keys to ca + en.
- Layouts switched from @breadcrumbs to breadcrumbs helper.
Locale-aware helpers / formatters
- ApplicationHelper#localized_ordinal: ordinalize that respects ca
(1r/2n/3r/4t/Nè). Wired into preferences month_start_day select.
- Family#moniker_label / moniker_label_plural: translate the default "Family"/
"Group" monikers via shared.family_moniker.* with fallback to the family's
custom override.
- Budget#name: use I18n.l for month_year/short/long instead of strftime("%B %Y")
so the budget header date follows the active locale.
Tooling
- script/lt_check_ca.rb: batched LanguageTool checker (premium endpoint when
LT_USERNAME/LT_API_KEY are set, free fallback otherwise), picky mode,
motherTongue=en for false-friend detection.
- lib/tasks/i18n_screenshot.rake: dev-only rake to set user.locale=ca and
role=super_admin on the demo user so the i18n surfaces can be walked.
Out of scope (pre-existing, not introduced here)
- Native browser file input "Choose Files / No file chosen" (browser locale).
- D3.js client-side chart x-axis dates (JS-side Intl.DateTimeFormat needed).
- Sankey/donut labels = seed category names (data, not i18n).
- 2 rails-i18n datetime/errors interpolation warnings inherited from
config/locales/defaults/ca.yml.
* fix(i18n): apply idiomatic Catalan review (3-agent + native review)
Three parallel review agents flagged 203 findings (31 high / 73 medium / 99 low)
across all 111 ca.yml files. This commit applies the high-severity bugs plus a
curated subset of medium-impact fixes.
Grammar / agreement
- provider_sync_summary.health.stale_pending: `(exclòs)` -> `(exclosa/excloses)`
to agree with feminine `transacció(s)`.
- accounts.confirm_unlink.warning_no_sync: added reflexive `es` -
`el compte ja no es sincronitzarà`.
- sophtron_setup_required.heading: `no configurats` -> `sense configurar`
(avoids broken agreement across "ID" masc. + "clau" fem.).
- admin.sso_providers.form.errors_title: split into one/other pluralization
keys (en + ca); singular `ha impedit` was wrong for count > 1.
Brand consistency
- IndexaCapital -> Indexa Capital (37 occurrences across one file).
- Lunchflow -> Lunch Flow in two remaining places.
Anglicisms / domain mistranslations
- kraken_items setup_accounts.instructions: `ompliments d'operacions`
(lit. dental/food fillings) -> `execucions d'operacions`.
- settings kraken_panel.read_only_title: `Sincronització d'intercanvi`
(swap/trade) -> `Sincronització només de lectura amb l'exchange`.
- transactions convert_to_trade.security_custom + security_not_listed_hint:
`cotització` (price quote) -> `ticker` (the EN field IS a ticker symbol).
- loans.form.rate_type: `Tipus d'interès` collided with sibling
interest_rate -> `Modalitat del tipus`.
- brex_items.provider_panel.sandbox_note_html: `L'staging` (broken
contraction) -> `el staging`.
Idiom traps
- coinbase/binance/kraken wait_for_sync: `acabi de sincronitzar` is
ambiguous in CA (`acabar de + inf` reads as "has just done X") ->
`acabi la sincronització`.
- chats.ai_greeting.there: `a tothom` -> `''` (the EN fallback "Hey there"
is singular; literal CA `tothom` is plural and wrong for 1:1 chat).
- transactions.split_parent_row.split_label: `Divideix` (imperative) is
wrong as a status badge -> `Divisió` (noun).
- transactions.keep_both (2 occurrences): infinitive `mantenir ambdues` ->
imperative `mantén-les totes dues` to match the sibling Yes/No buttons.
- rules.clear_ai_cache: `Reinicia` (restart) -> `Buida` (empty/clear),
which matches the success notice (`s'està netejant`).
Moniker gender breakage (cross-file)
%{moniker} is interpolated downcased from family.moniker_label and may
resolve to feminine `família`/`llar` or masculine `grup`. Strings that
hard-code a gendered article ('al teu %{moniker}', 'aquesta %{moniker}',
'aquest/a %{moniker}') broke on at least one branch. Restructured the
affected sentences to drop the gendered determiner:
- account_sharings.show.no_members
- merchants.family_empty / family_title / provider_empty
- registrations.new.join_family_title
- settings.preferences.show.currencies_subtitle / sharing_subtitle
- simplefin_items.select_existing_account.no_accounts_found
- invitations.new.subtitle
- invitation_mailer.invite_email.subject (mailers/) + body (views/)
- snaptrade_items.providers.snaptrade.free_tier_warning
Terminology consistency
- models/account_statement/ca.yml attributes aligned with view-side
forms: `Saldo d'obertura`/`Saldo de tancament` ->
`Saldo inicial`/`Saldo final`; `Suggeriment de...` -> `Pista de...`.
- account_statements.coverage.status.not_expected:
`No s'esperava` -> `No previst` (status label, not past action).
- account_statements.index.empty_unmatched: aligned with the section's
own label `Safata sense aparellar`.
- imports.create.document_provider_not_configured + document_upload_failed:
`arxiu vectorial` -> `magatzem vectorial` (correct TermCat term).
- coinstats_items blockchain gender: `els blockchains` / `un blockchain` ->
`les blockchains` / `una blockchain` (feminine per TermCat).
- accounts.account.remove_default: `Treu el predeterminat` ->
`Treu com a predeterminat` (pairs with sibling `Estableix com a
predeterminat`).
- accounts.tax_treatments.tax_deferred: `Diferit fiscalment` (lit. calque)
-> `Tributació diferida` (standard CA tax-accounting term).
- settings.payments.show.currently_on_plan: `Actualment al` ->
`Actualment al pla:` (was a fragment).
Out of scope (review flagged, not applied here)
- LOW-severity stylistic preferences (Veure vs Mostra, etc).
- `models/category/ca.yml` default category names — seeded at family
creation, not via I18n at runtime, so changes wouldn't affect existing
families.
- `models/period/ca.yml` short labels mixing EN (MTD/YTD) and CA (STD/MA)
— needs a one-convention decision separately.
* fix(i18n,ca): drop gendered article in period_activity + tighten cash-flow terms
- pages.dashboard.investment_summary.period_activity: 'Activitat del
%{period}' contracted 'del' = 'de el' (masc.sg.). %{period} resolves
to mixed forms ('Setmana en curs' fem, 'Últims 30 dies' pl., 'Any en
curs' apostrophe), so hard-coded 'del' was wrong on most labels.
Replaced with 'Activitat — %{period}' (em-dash) to skip the
contraction entirely.
- pages.dashboard.outflows_donut.title / total_outflows: switched from
bare 'Sortides' / 'Total de sortides' to 'Sortides de caixa' /
'Total de sortides de caixa' to match TermCat's precise term
('sortida de caixa' = cash outflow).
* fix(i18n,ca): rephrase transfer source/destination amount labels
'Import d'origen' / 'Import de destinació' were literal calques of
'Source amount' / 'Destination amount'. In a multi-currency transfer
form (sender/receiver in different currencies) the natural CA pair is
'Import enviat' / 'Import rebut'.
* fix(i18n,ca): 'Dades en brut' -> 'Dades sense processar'
The literal calque of 'Raw data' read as too technical for personal-
finance UI. 'Dades sense processar' is the more natural Catalan
equivalent for raw/unprocessed data files.
* fix(i18n): localize Import col_sep label + separator options
The CSV upload form rendered 'Col sep' (the auto-humanized attribute
name) plus hardcoded English 'Comma (,)' / 'Semicolon (;)' options
from Import::SEPARATORS.
- activerecord.attributes.import.col_sep added (en + ca: 'Column
separator' / 'Separador de columnes').
- Import.separator_options class method returns translated tuples;
view switched from Import::SEPARATORS to Import.separator_options.
- activerecord.attributes.import.col_seps.{comma,semicolon} added so
the option labels follow the active locale.
* fix(i18n,ca): drop moniker apposition in sharing/currencies section titles
- sharing_title 'Compartició de %{moniker}' rendered as 'Compartició
de Família' (a noun-noun apposition that's odd in CA) -> 'Compartició
de comptes'.
- sharing_subtitle replaced '%{moniker}' with 'entre els membres' so
the sentence reads naturally and doesn't depend on moniker gender.
- currencies_title 'Divises de %{moniker}' had the same apposition
-> 'Divises'. Subtitle no longer references moniker either.
* fix(i18n,ca): keep 'Self Hosting' untranslated
Reverted 'Autoallotjament' / 'autoallotjada' / 'autoallotjats' usages
to the original English 'Self Hosting' (sidebar label, breadcrumbs,
hostings page title, chat assistant settings hint, redis configuration
subheading, LLM usages cost-estimates description).
The brand-style term reads more naturally in EN for technical users
configuring their own deployment.
* fix(i18n,ca): lowercase 'self hosting' (sentence case in labels)
* fix(i18n): extract budget_categories stepper + allocation_progress strings
Hardcoded English strings on the budget category editor:
- 'Setup' / 'Categories' stepper labels in budgets/_budget_nav.html.erb
- 'X% set' / '> 100% set' / 'left to allocate' / 'Budget exceeded by ...'
in budget_categories/_allocation_progress.erb
- '/m avg' caption + 'Shared' placeholder + 'Leave empty to share
parent's budget' tooltip in budget_categories/_budget_category_form
and _uncategorized_budget_category_form
Extracted to:
- budgets.budget_nav.{setup,categories}
- budget_categories.allocation_progress.{percent_set,over_set,left_to_allocate,budget_exceeded_html}
- budget_categories.budget_category_form.{monthly_average,shared_placeholder,shared_title}
CA translations added; EN keys mirror the prior literals.
* chore(i18n): drop translation tooling from PR
These were dev-only helpers used during the Catalan translation pass:
- script/lt_check_ca.rb: LanguageTool API checker (premium/free
endpoint, picky mode, batching). Useful for ongoing locale QA but
shouldn't ship in this feature PR.
- lib/tasks/i18n_screenshot.rake: rake task that flips user.locale and
role on the demo user for walking the i18n surfaces locally.
Both stay available locally; pulled out of the PR scope.
* fix(i18n): apply PR review feedback (CodeRabbit + Codex)
- balance_reconciliation crypto_items: use :end_balance_crypto tooltip
(was :end_balance_investment). Added new UI.account.balance_reconciliation.tooltips.end_balance_crypto key in en + ca.
- doorkeeper.ca.yml confidentiality.no: was YAML boolean false, now string 'No'.
- views/categories: 'Poor contrast, choose darker color or' continued with hardcoded 'auto-adjust.' button text; extracted to categories.form.auto_adjust key (en + ca).
- imports.create.document_upload_failed: 'a l'magatzem' was broken
contraction -> 'al magatzem'.
- invitation_mailer body + mailer subject: 'unir-se' -> 'unir-te' (was
3rd person, should be 2nd to match the rest of the copy).
- 7 strings across mercury_items / sophtron_items / simplefin_items /
lunchflow_items / brex_items / indexa_capital_items / other_assets:
'se sincronitzaran' -> 'es sincronitzaran', 'se segueixen' ->
'es segueixen' (correct reflexive pronoun before consonants).
- settings.providers.status: key was 'false' (YAML-coerced), now 'off'
to match settings/en.yml status.off used in view lookups.
- sophtron_items.sophtron_setup_required.message: stripped trailing
blank line from the quoted scalar.
- settings/profiles/show.html.erb: switched 'family_moniker ==
"Group"' branch checks to 'Current.family&.moniker == "Group"'.
After Family#moniker_label started returning translated values,
callers using the display label for branching would render the
household copy for group families in ca. Compare the stored sentinel
instead.
- Did not apply CodeRabbit's webauthn 'eliminada' -> 'desada' suggestion:
the key is wired to the destroy action (verified at
settings/webauthn_credentials_controller.rb:55), so 'eliminada' is
correct.
|
||
|
|
6262b0a493 |
fix(ibkr): correct historical cash/non-cash split for linked accounts (#1813)
* fix(ibkr): resolve weekend balance oscillations and improve data processing Address issues where IBKR weekend/holiday data caused incorrect balance calculations and improve the robustness of IBKR account processing. - Fix historical balance oscillations by ignoring anomalous weekend rows and filling gaps by carrying forward the last known trading day value. - Normalize report dates to the last trading day to ensure consistency. - Improve `HoldingsProcessor` to skip individual bad lots instead of failing the entire group. - Refactor `ActivitiesProcessor` to accumulate fee counts locally via return values instead of using instance variables. - Add support for accounting parentheses notation in `DataHelpers`. - Memoize the account object in `IbkrAccount::Processor` to reduce database queries. - Update tests to reflect date normalization and improved precision assertions. * fix(ibkr): derive historical cash from materializer balances, not equity summary Real IBKR Flex exports do not include a reliable cash/stock breakdown in EquitySummaryByReportDateInBase — only the total is consistently present. The previous implementation parsed the missing cash field as zero and wrote cash_balance=0 for every historical date, causing negative and wildly incorrect cash values throughout the account history. Instead, read the materializer's already-computed cash_balance for each date (derived from holdings via the reverse calculator) and use only IBKR's total as an authoritative balance anchor. This is consistent with how present-day balances are handled and requires no weekend/holiday filtering since IBKR does not emit weekend rows and holiday totals are legitimate data points. Also accept equity summary rows without an explicit currency field (some Flex configurations omit it) and explicitly reject BASE_SUMMARY aggregate rows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: simplify boolean coercion in import_commission_transaction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ibkr): cover trailing weekend gap and align qty with valid lots HistoricalBalancesSync: extend fill_gaps to account.current_anchor_date so days after the last equity summary row (e.g. Saturday/Sunday when a sync runs over the weekend) are also overridden rather than left with the materializer's stale total=cash value. HoldingsProcessor: replace separate quantity sum + weighted_cost_basis_for with a single valid_lots method that computes both from the same set of parseable lots. Previously a lot with a valid position but unparseable cost_basis_price was excluded from the cost basis calculation but still counted in quantity, producing inconsistent qty/cost_basis values. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * review: address PR feedback on ibkr fix branch - Remove all "Fix N:" review-artifact comment labels - Add Sentry.capture_message for silenced anchor repair failure so it surfaces in production monitoring - Add Rails.logger.warn for zero/nil total rows skipped in HistoricalBalancesSync - Document normalize_to_last_trading_day holiday limitation and why gap-fill covers it - Rewrite two non-obvious comments to stand alone without the label prefix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * style: remove alignment padding in balance_rows hash Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ibkr): address two P1 review findings - Allow zero and negative equity summary totals through HistoricalBalancesSync so fully-liquidated and margin accounts are not silently skipped (which would cause fill_gaps to propagate a stale non-zero total forward). - Remove normalize_to_last_trading_day from HoldingsProcessor: shifting weekend report_dates to Friday caused Balance::SyncCache#get_holdings_value to find no holdings on Saturday/Sunday (exact-date lookup), collapsing non_cash to zero — reintroducing the very oscillation the fix was meant to prevent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(ibkr): add tests for historical balances sync and data helpers - Add test case to verify non-cash balance calculation in historical balances sync - Add test case to ensure rows with unparseable or nil totals are skipped - Add new test file for IBKR data helpers * fix(ibkr): prevent date range overflow during historical sync Adjust the calculation of `last_date` in `HistoricalBalancesSync` to ensure it does not exceed the current anchor date or today's date. This prevents the sync process from attempting to fetch or process future dates, which was causing oscillations in weekend data. Also remove the conditional check for Sentry before capturing error messages in the account processor. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
7411db5689 |
feat(i18n): add Hungarian translations for strings extracted in #1806 (#1817)
* add missing Hungarian translations for newly extracted strings Replace hard-coded UI strings with I18n lookups across controllers, models and views (breadcrumbs, dashboard, reports, settings, transactions, balance sheet, MFA status). Update models to use translations for category defaults, account/display names, classification group and period labels; remove a few hardcoded display_name methods. Add and update numerous locale files (English and extensive Hungarian translations, plus model/view/doorkeeper entries) to provide the required keys. These changes centralize copy for localization and prepare the app for Hungarian/English UI text. * Pluralize account type labels; tidy Crypto model Update English locale account type labels to use plural forms for consistency (Investment(s), Properties, Vehicles, Other Assets, Credit Cards, Loans, Other Liabilities). Also remove an extra blank line in app/models/crypto.rb to tidy up formatting. * Back to singular * fix(i18n): separate singular and group account labels * Update _accountable_group.html.erb * Use I18n plural names for account types Change Accountable#display_name to look up pluralized account type names via I18n (accounts.types_plural.<underscored_class>) with a fallback to the legacy display logic. Add legacy_display_name helper to preserve previous behavior (singular for Depository and Crypto, pluralized otherwise). Add corresponding types_plural entries in English and Hungarian locale files for various account types. --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com> Co-authored-by: sure-admin <sure-admin@splashblot.com> |
||
|
|
5249842c76 |
feat: beta features toggle + Beta pill primitive (#1829)
* feat: beta features toggle + Beta pill primitive Adds the infrastructure for self-service beta opt-in. No call sites yet: this PR is meant to land first so feature PRs (Goals, etc.) can ship behind the gate incrementally. User opts in via a single toggle at the bottom of Settings → Preferences. The flag persists in the existing `users.preferences` JSONB column under `beta_features_enabled` — same shape as `dashboard_two_column` and `show_split_grouped`, so no migration is needed. Controllers gate a beta feature by adding `before_action :require_beta_features!` from the new `BetaGateable` concern (included in ApplicationController). Views use the `beta_features_enabled?` helper to hide / show nav items, banners, etc. Logged-out callers always return false. Ships `DS::BetaPill`, a small inline marker for tagging features as Beta / Canary in nav, headers, and lists. Five tones (violet by default, indigo, fuchsia, amber, gray) map to existing Sure color tokens — no raw hex. Three styles (soft / filled / outline) and two sizes (sm / md) cover the surfaces in the design handoff. The `dot_only:` mode renders just the colored dot for use on a collapsed sidebar. * review: rename to DS::Pill, fix CR/Codex nits, add tests CodeRabbit + Codex review feedback: - Rename DS::BetaPill → DS::Pill. The component was already generic in shape (tones, styles, sizes); the name was misleading scope. "Beta" becomes the default label (still i18n-driven). Goals' StatusPill can later refactor onto this primitive without a third pill. - Localize the default pill label via i18n (`ds.pill.default_label`) instead of hard-coding English. - Add role="img" to the dot-only span so the aria-label is consistently exposed to assistive tech. - Wrap the Preferences toggle row in <label for="…"> so the title and description become an honest click target for the toggle (matches the cursor-pointer affordance). - Drop arbitrary Tailwind values (py-[3px], gap-[5px], tracking-[…]) in favor of scale tokens. text-[10/11px] stays because the pill is intentionally sub-12px (Sure's smallest scale token is text-xs / 12px) to read as a marker, not a label. - Add User#beta_features_enabled? predicate tests covering default-off, explicit-true, and non-boolean truthy values. Won't fix: - Palette refs (`--color-violet-*` etc.). Sure has no semantic Beta/ Canary tokens; introducing them in this PR would be a design-system change beyond the scope. The component centralizes palette use in one `palette` method, matching the existing pattern in Goals::StatusPillComponent. * review: consistent title fallback in full-pill branch * docs: how to gate a feature behind the beta toggle * docs: unwrap doc lines to match existing style * chore(preview): run Cloudflare PR previews on basic instances (#1831) * fix(preview): use Rails health endpoint for container ping (#1823) * fix(preview): use Rails health endpoint for container ping * fix(preview): point container ping to localhost/up --------- Co-authored-by: Sure Admin (bot) <sure-admin@splashblot.com> |
||
|
|
b73da38f49 |
fix(pwa): serve manifest for html accept headers (#1828)
* fix(pwa): serve manifest for html accept headers * style: add trailing newline to pwa controller |
||
|
|
4fd460d551 |
Add Actual Budget CSV import flow (#1830)
* Add Actual Budget CSV import flow * Address Actual import review feedback |
||
|
|
70fc52769d |
Add super_admin debug event log (#1816)
* Add super-admin debug event log * Address debug log review feedback * Whitelist debug filter params * Make debug log retention configurable |
||
|
|
2df10ca4ef |
Retry Enable Banking sync with provider-corrected date range (#1801)
* Clamp Enable Banking sync window * Pipelock noise --------- Co-authored-by: KiloClaw <kiloclaw@openclaw.ai> Co-authored-by: Juan José Mata <jjmata@jjmata.com> |
||
|
|
0c126b1674 |
feat(i18n): extract hardcoded English strings to locale files (#1806)
* Extract hardcoded strings to i18n
Replace numerous hardcoded English strings with I18n lookups (t / I18n.t) across controllers, views, helpers, and components, and convert model validation error messages to symbol keys. Added multiple locale files under config/locales for models and views. This centralizes user-facing notices/alerts, UI text, import/validation messages, and prepares the app for localization and easier translation maintenance.
* Update en.yml
* Update preview-cleanup.yml
* Revert "Update preview-cleanup.yml"
This reverts commit
|
||
|
|
04549d80bf |
fix(rules): count blocked rule transactions (#1782)
* Add blocked count to rule run summary * test(rules): cover rule run blocked counts * fix(rules): derive blocked count from modified rows Blocked rule transactions are the processed rows that were not modified. This keeps the displayed queued / processed / modified / blocked summary aligned when a run has already processed all matching rows but some were skipped by enrichment locks. * fix(rules): count processed rows for rule jobs Synchronous rule actions return the number of rows they modified, but rule-run processed counts should represent the number of matched transactions the job attempted to process. Using queued matches for processed preserves the distinction between processed and modified rows, which lets locked manual edits appear as blocked instead of making processed collapse to modified. This changes RuleJob counter semantics, so it was committed separately from the derived blocked-count display change. |
||
|
|
0ad1e59165 |
fix(a11y): add skip-link and aria-current="page" to application layout (#1781)
* fix(a11y): add skip-link and aria-current="page" to application layout * test(a11y): cover application layout skip-link and #main anchor * fix(a11y): extend skip-link and #main anchor to settings layout |
||
|
|
c106aaf10d |
fix(enable-banking): preserve claimed pending date on subsequent syncs (#1797)
After the first sync claims a pending entry (setting auto_claimed_pending_ids), subsequent syncs find the entry by booked external_id as an existing record. pending_match is never entered so pending_entry_date stays nil, causing `nil || date` to silently overwrite the preserved pending date with the booked settlement date. Fix by checking auto_claimed_pending_ids on the existing entry — its presence signals a prior auto-claim, so entry.date (the original pending date) is kept. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
81e66870d7 |
Add period navigation arrows to Reports view (#1756)
* Add period navigation arrows to reports view * Fix accessibility: render disabled next arrow as span instead of anchor * Add tests for period navigation arrows and localized strings * Refactor period navigation: move date logic to controller * Fix test assertions: tighten selectors and remove debug code * Redesign period navigation arrows to match budget screen style * custom period test assert next period * Add YTD tests and fix indentation in period navigation tests * Add period picker menu to reports navigation * Fix accessibility: use disabled button for next arrow * fix a test that was lost in the repos update * Use i18n for period navigation labels * Add accessible labels to period picker navigation links * Use i18n for quarter and YTD labels in period picker * Add accessible labels to active period navigation chevrons * Tighten custom period navigation test assertions * Add comment clarifying build_period_navigation dependency on setup_report_data * Replace link_to with DS::Link in period picker navigation Use Date#quarter instead of manual quarter calculation Remove border from month/quarter/year display in period picker |
||
|
|
ba3b20627d |
feat(balance): Preserve historical balances as waypoints for linked accounts (#1663)
* feat(balance): persist daily balance snapshots for linked accounts (SnapTrade, Plaid) When updating a linked account's balance, the previous day's current_anchor is now preserved as a reconciliation valuation before being replaced. This creates a chain of API-reported balance waypoints over time. The ReverseCalculator has been updated to treat these reconciliation valuations as reset points during reverse syncs, ensuring historical balances accurately reflect the known API-reported values even with incomplete transaction history. * fix(balance): don't treat current_anchor as reconciliation waypoint The ReverseCalculator was incorrectly treating the current_anchor valuation (on Date.current) as a reconciliation waypoint, causing it to reset the balance and ignore same-day transactions. This fix adds a check to ensure only true reconciliation entries (entryable.reconciliation?) trigger the reset behavior. Additionally, set_current_balance_for_linked_account is now wrapped in a database transaction to ensure atomicity when preserving stale anchors and creating/updating the current anchor. Logging has been improved to use debug level for amount details. A regression test was added to verify that same-day flows are correctly processed when a current_anchor exists on the current date. * test(account): ensure preserved valuations use correct historical date Add validation that valuation entries created during balance preservation are dated as of yesterday. This prevents future-dated entries and maintains temporal accuracy in financial snapshots. * refactor: remove redundant transaction block and unused method comment in current balance manager * refactor(account): remove redundant valuations reload in CurrentBalanceManager and add regression test for consecutive reconciliation waypoints * refactor: remove redundant transaction block and update anchor rotation log to include entry ID |
||
|
|
e59235fdc5 |
feat(statements): add account statement vault (#1753)
* feat(statements): add account statement vault Add web-only statement uploads, account linking, duplicate detection, and per-account coverage/reconciliation checks without mutating transactions. Extend ActiveStorage authorization and targeted tests for family/account scoping. * fix(statements): return deleted account statements to inbox Preserve linked statement records when an account is deleted by moving them back to the unmatched inbox, then expand coverage for upload validation, sanitized parser metadata, unavailable reconciliation, and missing-month coverage. * fix(statements): harden vault upload review flows Address review and security findings in the statement vault by preserving sanitized parser metadata, failing closed on orphaned statement blobs, avoiding account_id mass assignment permits, and adding regression coverage for link/delete edge cases. * fix(statements): harden vault upload and access controls * fix(statements): address vault hardening review * fix(statements): address vault review feedback Prioritize SHA-256 duplicate detection while preserving MD5 fallback for legacy rows. Remove free-form account notes from statement matching, document direct account-destroy unlinking, and add year-selectable historical coverage with muted out-of-range months. * fix(statements): harden vault review follow-ups Clarify legacy MD5 checksum use, whitelist statement balance helper dispatch, and preserve sanitized parser metadata. Hide statement management controls from read-only viewers while keeping server-side authorization unchanged. * fix(statements): repair settings system coverage Allow the changelog provider lookup in the self-hosting settings system test, include Statement Vault in settings navigation coverage, and align the feature title casing. Update the devcontainer so ActiveStorage and parallel system tests can run in the documented environment. * fix(statements): move vault beside accounts Place Statement Vault with account settings instead of between Imports and Exports. Keep settings footer ordering and system navigation coverage aligned, including the non-admin visibility guard. * fix(statements): address vault review cleanup Resolve CodeRabbit review feedback for statement upload validation, duplicate race handling, account statement matching semantics, metadata detection, ActiveStorage authorization tests, and small UI/style cleanups. * fix(statements): address vault cleanup review * fix(statements): deduplicate vault style helpers * fix(statements): close vault review follow-ups * fix(statements): refresh schema after upstream rebase * fix(statements): process vault uploads sequentially * fix(statements): close vault review follow-ups * fix(statements): scope vault index to accessible accounts * fix(statements): harden statement vault readiness Squash the statement vault migration hardening into the feature migration, tighten Active Storage authorization edge cases, bound CSV metadata detection, and add real PDF fixture coverage for stored statements. Validation: targeted statement/auth/controller/provider tests, full Rails suite, system tests, RuboCop, Biome, Brakeman, Zeitwerk, importmap audit, npm audit, ERB lint, CodeRabbit, and Codex Security all passed locally. * fix(statements): close vault review follow-ups Move statement unlinking to after account destroy commit, keep Kraken account creation on the shared crypto helper, and add statement metadata length limits with DB checks. Validation: fresh devcontainer with fresh DB via db:prepare, focused account/statement/Kraken/Binance tests, RuboCop, Brakeman, Zeitwerk, git diff --check, CodeRabbit, and Codex Security passed before commit. * fix(statements): address vault scan follow-ups Move statement tab data setup out of the ERB partial, harden reconciliation labels and coverage initialization, and tighten statement schema constraints. Validation: CodeRabbit and Codex Security reviewed the current PR diff; Rails focused tests, full Rails tests, system tests, RuboCop, Brakeman, Zeitwerk, ERB lint, npm lint, importmap audit, npm audit, and git diff --check passed. * fix(statements): defer vault tab loading --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
42e7ae677a |
fix(exports): align CSV roundtrip contracts (#1725)
* fix(exports): align CSV roundtrip contracts * fix(exports): version CSV export contract * fix(exports): stabilize CSV export values * fix(imports): preserve legacy CSV roundtrip contracts * fix(imports): escape pipe characters in CSV tags --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
834686cffd |
fix(simplefin): treat Vanguard/Fidelity cost_basis as total when needed (#1772)
* fix(simplefin): treat Vanguard/Fidelity cost_basis as total when needed PR #1692 normalized SimpleFIN holdings cost_basis under the assumption that the `cost_basis` / `basis` keys carry a per-share value (per the SimpleFIN spec) and only `total_cost` / `value` carry a total position cost. Vanguard and Fidelity violate the spec — they populate `cost_basis` with the *total* (see the payload in #1182). After PR #1692 those holdings get stored with cost_basis = total, and Holding#calculate_trend then computes previous = qty × avg_cost, so the "previous" value is inflated by a factor of qty and an entire investment account renders a phantom return of roughly -(1 − 1/qty), i.e. -97% to -99%. Fix: sanity-check raw cost_basis against the holding's market share price. Let share_price = market_value / qty; the geometric midpoint between "raw is per-share" (raw ≈ share_price) and "raw is total" (raw ≈ qty × share_price) is share_price × √qty. If raw is above the midpoint it is divided by qty; otherwise it is kept as per-share. Falls back to the pre-fix behaviour (trust the spec) when market_value or qty is unavailable, so confidently-correct readings are never made worse. Verified against the reported Vanguard payload (qty=139, cost_basis= 22004.40, market_value=22626.42): normalize_cost_basis now returns $158.31/share, matching 22004.40 / 139, and the phantom -99% return collapses to a realistic ~+2.8%. Per-share readings ($45 cost on a $50 share price) remain untouched. Closes #1718. Refs #1182, #1692. * fixup: replace cost_basis heuristic with institution allowlist Codex and @EdeAbreu23 flagged a real false-positive in the previous geometric-midpoint heuristic: a legitimate per-share `cost_basis` on a holding with a large unrealized loss (e.g. 100 shares with $100/share basis now worth $5/share) trips `share_price × √qty` and gets divided to $1/share — corrupting any standards-compliant brokerage with a big loss. Adopt @EdeAbreu23's safer shape: - total_cost / value: always divide by qty (unchanged from #1692). - cost_basis / basis: keep as-is by default. - Only divide cost_basis / basis when the holding's SimpleFIN account is connected to a known-misbehaving institution. Allowlist starts with `vanguard` and `fidelity`, matched case-insensitively against the account's stored org name and domain. Easy to extend as more brokerages turn up. Trades a small maintenance cost (curated list) for zero risk of corrupting compliant providers. Verified against five scenarios (all expected): Vanguard total in cost_basis (allowlist) → +2.83% Fidelity total in basis (allowlist) → +33.33% Big-loss per-share (Codex case) → -95.0% (preserved) Honest per-share, small loss → +11.11% (unchanged) total_cost on any institution → +11.11% (unchanged) --------- Co-authored-by: plind-junior <plind-junior@users.noreply.github.com> |
||
|
|
95f6451b39 |
feat(sync): add Brex provider connections (#1752)
* feat(sync): add Brex provider schema Adds Brex item and account tables with per-family credentials, scoped upstream account uniqueness, encrypted token storage, and sanitized provider payload columns. * feat(sync): add Brex provider core Adds Brex item/account models, provider client and adapter support, family connection helpers, and provider enum registration for read-only Brex cash and card data. * feat(sync): add Brex import pipeline Adds Brex account discovery, linked-account sync, cash/card balance processors, transaction import, sanitized metadata handling, and idempotent provider entry processing. * feat(sync): add Brex connection flows Adds Mercury-style Brex connection management, explicit item-scoped account selection and linking, settings provider UI, account index visibility, localized copy, and per-item cache handling. * test(sync): cover Brex provider workflows Adds targeted coverage for Brex provider requests, adapter config, item/account guards, importer behavior, entry processing, and Mercury-style controller flows. * fix(sync): align Brex API edge cases Tightens Brex account fetching against the official card-account response shape, sends transaction start filters as RFC3339 date-times, and keeps provider error bodies out of user-facing messages while expanding provider client guard coverage. * fix(sync): harden Brex provider integration Restrict Brex API base URLs to official hosts, tighten account-selection UI behavior, and add tests for invalid credentials, cache scoping, and provider setup edge cases. * test(sync): avoid Brex secret-shaped fixtures * refactor(sync): extract Brex account flows * fix(sync): address Brex provider review feedback * fix(sync): address Brex review follow-ups Move remaining Brex review cleanup into focused model behavior, tighten link/setup edge cases, localize summaries, and add regression coverage from CodeRabbit feedback. Also records the security-review pass as no-findings after diff-scoped inspection and Brakeman validation. * refactor(sync): split Brex account flow controllers Route Brex account selection and setup actions through small namespaced controllers while keeping existing URLs and helpers stable. Business flow remains in BrexItem::AccountFlow; the main Brex item controller now only handles connection CRUD, provider-panel rendering, destroy, and sync. * fix(sync): address Brex CodeRabbit review * fix(sync): address Brex follow-up review * fix(sync): address Brex review follow-ups * fix(sync): address Brex sync review findings * fix(sync): polish Brex review copy and errors * fix(sync): register Brex provider health * fix(sync): polish Brex bank sync presentation * fix(sync): address Brex review follow-ups * fix(sync): tighten Brex setup params * test(api): stabilize usage rate-limit window * fix(sync): polish Brex setup flow nits * fix(sync): harden Brex setup params * fix(sync): finalize Brex review cleanup --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
7b21a619ec |
fix(enable-banking): gracefully skip PDNG fetch for ASPSPs that don't support it (#1789)
* fix(enable-banking): gracefully skip PDNG fetch for ASPSPs that don't support it Some banks reject the PDNG transaction status filter with a 422 validation error, causing the entire account sync to fail including booked transactions. Wrap the pending transaction fetch in a rescue block to catch validation errors from the provider. If the ASPSP does not support the "PDNG" status, the error is logged and the process continues without pending transactions instead of failing the entire import. * fix(enable-banking): gate PDNG fallback on transactionStatus error detail Tighten the rescue added in the previous commit so it only silences 422s that explicitly mention transactionStatus in the API error body. Any other validation error (bad date_from, malformed headers, etc.) re-raises and fails the sync as before, preventing silent data loss. Tests added for both branches: ASPSP-rejects-PDNG (success) and unrelated-validation-error (failure). |
||
|
|
406e7217a1 |
fix(enable-banking): fix pending→posted auto-claim producing badge, duplicate, and wrong date (#1783)
* fix(enable-banking): clear pending flag and prevent stale re-import after auto-claim When a booked transaction claims a pending entry via the amount/date heuristic (find_pending_transaction), two bugs caused the entry to remain incorrectly pending and the old pending transaction to reappear on subsequent syncs. Bug 1: The extra["enable_banking"]["pending"] flag was never cleared on the claimed entry. For simple booked transactions with nil extra the deep-merge path is skipped entirely, so the pending badge persisted forever. Bug 2: After the claim the old pending external_id (e.g. PDNG_123) stayed in the stored raw_transactions_payload. The importer's C4 filter only removes pending entries whose transaction_id matches a BOOK id — Enable Banking issues completely different ids for pending vs booked transactions — so PDNG_123 was never pruned. On the next sync find_or_initialize_by(PDNG_123) couldn't find the claimed entry (now keyed as BOOK_456) and created a fresh pending duplicate with no category. Fix: on claim, explicitly clear all providers' pending keys from extra in-memory, and store the displaced pending external_id in extra["auto_claimed_pending_ids"]. The Processor now queries this field alongside manual_merge to build the excluded_ids set, so the stale pending data is skipped on every future sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(enable-banking): preserve pending date when claiming transactions When a pending transaction is claimed by a booked transaction, the original pending date is now preserved instead of being overwritten by the booked transaction's date. This ensures historical accuracy for transactions that were originally recorded on a different date. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
ce5d7dd736 |
Add Interactive Brokers Provider (#1722)
* Display multi-currency holdings correctly * Implement IBKR provider * Fix: Use historical exchange rate for historical prices * Add brokerage exchange rate for trades * Sync historical balances from IBKR * Add logos in activity history * Fix privacy mode blur in account view * Improve IBKR XML Flex report parser errors |
||
|
|
6402f1dd08 |
fix(sso): preserve user-edited name across OIDC logins (#1777)
OidcIdentity#sync_user_attributes! runs on every SSO sign-in and overwrote user.first_name / user.last_name with whatever the IdP sent, because the precedence was `auth.info.* || user.*` — the IdP always won when it supplied a value. A user who edited their first name to "Adam" inside Sure had it reset to the IdP value "Ben" on the next login, while the last name only "stuck" when the IdP happened not to return a last_name (#1103). Swap the precedence to `user.* || auth.info.*` so the IdP fills only when Sure has nothing on file (first link or admin-blanked field). Edits inside Sure are then authoritative for every subsequent login. The audit copy on the OidcIdentity record itself is unchanged, so the IdP-reported name is still available for debugging. Closes #1103. Co-authored-by: plind-junior <plind-junior@users.noreply.github.com> |
||
|
|
0ab3b0b698 |
feat(exports): add rule operand references (#1726)
* feat(exports): add rule operand references * fix(exports): preserve rule operand references * refactor(exports): simplify rule operand branches * refactor(validation): centralize UUID format checks * fix(imports): preserve false rule operands |
||
|
|
12d799e0b8 |
fix(binance): support CRYPTO: prefix and USD stablecoins (#1771)
* fix(binance): support CRYPTO: prefix and USD stablecoins Holdings processors (CoinStats, Coinbase, Kraken, SimpleFIN, Lunchflow, Binance) store crypto securities with a "CRYPTO:" prefix, but Provider::BinancePublic#parse_ticker only accepted Binance-search-style tickers like "BTCUSD". As a result, every fetched price for tickers like CRYPTO:USDT, CRYPTO:USDC, CRYPTO:SOL, CRYPTO:TRUMP, CRYPTO:KAITO failed with "Unsupported Binance ticker". - Strip the CRYPTO: prefix in parse_ticker. - Short-circuit USD-pegged stablecoins (USDT, USDC, BUSD, DAI, FDUSD, TUSD, USDP, PYUSD) to a synthetic flat 1.0 USD price. Binance has no self-pair (USDTUSDT is invalid), and the few stablecoin/USDT pairs that do exist hover at ~1.0 with sub-cent noise. - Default prefixed bare base assets (CRYPTO:SOL etc.) to the …USDT pair (USD). Only when prefixed, so unprefixed garbage like BTCBNB / BTCGBP still returns nil and the existing rejection tests still pass. - fetch_security_info returns links: nil for stablecoins rather than a broken /trade/ URL. Closes #1441. * fix(binance): strip CRYPTO: prefix in search_securities Security::Resolver calls search_provider with the raw holdings-processor symbol (CRYPTO:SOL, CRYPTO:USDT) before any price fetch. Without prefix handling here, first-time crypto imports never resolve to an online Binance security and the new stablecoin/prefix paths in parse_ticker were unreachable for that flow. - Strip CRYPTO: from the search query. - Short-circuit USD stablecoins to a synthetic search result (no exchangeInfo call, no Binance self-pair to find). - Teach parse_ticker the "{stablecoin}USD" form produced by the synthetic result so price fetches route to stablecoin_prices. --------- Co-authored-by: plind-junior <plind-junior@users.noreply.github.com> |
||
|
|
73b6077ac3 |
Constrain Lunchflow base URL to trusted endpoint (#1768)
* Constrain Lunchflow base URL to trusted endpoint Prevent SSRF by ignoring user-provided Lunchflow base_url values unless they match the canonical Lunchflow HTTPS endpoint. Add model tests covering invalid host/scheme and valid canonicalization behavior. * Linter |
||
|
|
5ceb55be03 |
Scope SnapTrade orphan cleanup to current family (#1769)
* Scope SnapTrade orphan cleanup to current family Restrict orphaned user listing and deletion to SnapTrade user IDs that belong to the current family namespace. Add model tests to prevent cross-family enumeration/deletion regressions. * Update test/models/snaptrade_item_test.rb Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Juan José Mata <jjmata@jjmata.com> * test: fix snaptrade orphaned users assertion * style: fix snaptrade test array spacing --------- Signed-off-by: Juan José Mata <jjmata@jjmata.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: KiloClaw <kiloclaw@openclaw.ai> |
||
|
|
d943e32b15 |
fix: correct SnapTrade cash activity signs (#1634)
* fix: correct snaptrade cash activity signs * test: update snaptrade withdrawal sign expectation --------- Co-authored-by: SureBot <sure-bot@we-promise.com> |
||
|
|
c1678181f0 |
fix(imports): import raw balance records (#1724)
* fix(imports): import raw balance records * fix(imports): preserve partial balance components |
||
|
|
7c06fe6296 |
feat(recurring): allow marking transfers as recurring (#895) (#1589)
Refs #895, discussion #1224. Adds a "Mark as recurring" entry point on the transfer detail drawer that creates a `RecurringTransaction` carrying both source and destination accounts. The recurring index, settings toggle (`recurring_transactions_disabled`), and projected upcoming feed all light up automatically once the data shape is there. Schema: * `destination_account_id` nullable FK to accounts. `on_delete: :cascade` matches #20251030172500's precedent for accounts FKs. The existing `account_id` FK is widened to cascade in the same migration so Family destruction with a recurring transfer doesn't FK-violate. * Two predicate-partitioned partial unique indexes per shape: non-transfer rows (`destination_account_id IS NULL`, original 5-column shape preserved) and transfer rows (6-column shape including the destination). Postgres treats NULLs as distinct in unique indexes, so widening would have broken non-transfer dedupe. * Two CHECK constraints enforcing transfer invariants in PostgreSQL: `chk_recurring_txns_transfer_requires_source` (destination implies source) and `chk_recurring_txns_transfer_distinct_accounts` (destination cannot equal source). Per CLAUDE.md "Enforce null checks, unique indexes, and simple validations in the database schema for PostgreSQL". * `Account` gains an `inbound_recurring_transfers` inverse so the destroy chain reaches both ends. Controller / behaviour: * `transfers#mark_as_recurring` mirrors `transactions#mark_as_recurring`: i18n flashes (4 new keys: transfer_marked_as_recurring, transfer_already_exists, transfer_creation_failed, transfer_feature_disabled), `respond_to format.html`, `redirect_back_or_to transactions_path`, server-side gate on `recurring_transactions_disabled?`, and rescue both `RecordInvalid` and `RecordNotUnique` for the race window between the dedupe `find_by` and `create_from_transfer`. The `StandardError` rescue now logs the exception (class, message, transfer/family/user ids) before surfacing the generic flash so production failures aren't context-less. * `RecurringTransaction.accessible_by(user)` now requires destination_account_id (when present) to be in the user's accessible set, so a recurring transfer never leaks to a user without access to BOTH endpoints. * Model validation gains a `destination_account.blank?` branch in `transfer_endpoints_consistent` so a dangling `destination_account_id` (referenced row destroyed) surfaces as a normal validation error instead of an FK exception on save. * `Identifier` filter for transfer-kind transactions moved into SQL. UI: * Recurring index table and projected feed render transfer rows with the existing letter-avatar and the row's `name` field ("Transfer to {destination}"). No special pill or icon -- every row in `/recurring_transactions` is recurring by definition. Amount column on transfers uses `text-secondary` (muted-but-live) instead of the income/expense colour, since transfers are zero-net for the family. Out of scope (called out in the PR body): * Auto-creation of future Transfer rows on a schedule (discussion #1224's primary ask). Behaviour change vs the current projection-only model. * Auto-identification of recurring transfer pairs in `Identifier`. * Frequency model richer than `expected_day_of_month`. * `Cleaner` for recurring transfers (issue #1590 tracks this). Tests: * `RecurringTransaction#transfer?` predicate (with / without destination). * `transfer_endpoints_consistent`: rejects same source and destination, rejects dangling destination_account_id, rejects cross-family destination. * `RecurringTransaction.create_from_transfer` happy path; multi-currency variant stores source-side currency. * `projected_entry` exposes source / destination on transfer rows. * `Identifier` skips transfer-kind transactions; creates a pattern from expense halves while ignoring co-resident transfer halves. * Destroying the destination account cascades to inbound recurring transfers (FK + AR association). * Unique partial index still de-duplicates non-transfer rows after the destination_account_id widening. * `transfers#mark_as_recurring` happy path, idempotent on second call, rejected when `recurring_transactions_disabled`. Suite: 3261 / 0 / 0 / 24 on the latest upstream/main. Lint clean. Brakeman clean. Signed-off-by: Guillem Arias Fauste <gariasf@proton.me> |