Commit Graph

953 Commits

Author SHA1 Message Date
Shibu M
d058bcbdcc Merge branch 'we-promise:main' into Transfer-charges 2026-06-21 01:29:31 +05:30
Artem Danilov
c65f4828dd fix(tinkoff): resolve the tradeable listing and price bonds via BondBy (#2413)
* fix(tinkoff): resolve the tradeable listing and price bonds via BondBy

Validated against the live T-Invest API and fixed three correctness issues in
Provider::TinkoffInvest:

- FindInstrument returns several listings per ticker (e.g. SBER on TQBR plus
  dark/non-API boards 37M/SPEQ); only the apiTradeAvailableFlag listing has live
  prices. resolve_short now ranks tradeable-first (then requested-MIC, then exact
  ticker/ISIN) instead of taking the first match, so GetLastPrices/GetCandles
  return data. Search now surfaces only tradeable instruments.
- find_instruments no longer hard-filters on apiTradeAvailableFlag, so a
  qualified-investor instrument Tinkoff lists but can't API-trade still resolves
  (for logos) and otherwise falls back to another price provider.
- Bond nominal comes from BondBy (the generic GetInstrumentBy omits it),
  returning the current amortized nominal so percent-of-par converts correctly.

Verified end-to-end: SBER 313.65, T 282.76, LQDT 2.019, and the SFO Split bond
(amortized nominal 417.71) all price; logos resolve to real CDN PNGs.

* fix(tinkoff): honor requested MIC first, guard amortizing bonds, strip suffix

Address review feedback on the resolution/pricing path:

- resolve_short now ranks a requested-MIC match BEFORE tradeability, so a
  security is never priced off another exchange's listing (e.g. a MISX security
  no longer picks up a tradeable XSPX board's price); tradeability and exact
  ticker/ISIN remain secondary tie-breaks.
- Amortizing bonds: BondBy returns only the current nominal, so applying it to
  historical percent-of-par closes would underprice them. For amortizing bonds
  we now return only the live price and skip the candle history; fixed-par bonds
  and equities keep full history. bond_info exposes nominal + amortizationFlag.
- Strip exchange suffixes (.ME/.MOEX/.MISX/.MCX) before querying T-Invest, which
  only knows the bare SECID — so a stored "T.MOEX" ticker resolves to "T".

* fix(tinkoff): don't cache nil resolution results (skip_nil) so a transient empty response isn't locked in for the 24h TTL
2026-06-20 21:46:01 +02:00
ghost
56000d7834 feat(mobile): add SureCard primitive and migrate account cards (#2370)
* feat(mobile): add SureCard primitive and migrate account cards

SureCard mirrors the web card chrome — `bg-container` + a hairline border +
rounded corners + the subtle DS shadow (`shadowXs`, from the scale shipped in
#2349) — resolved from the active SureColors palette so it stays in lockstep with
`sure.tokens.json` in light and dark. An optional `onTap` gives a flat ink
response clipped to the card radius.

Migrates AccountCard off the Material `Card` to `SureCard`.

Refs #2235.

* fix(coinstats): deterministic wallet batch order in bulk fetches

The bulk balance/transaction fetches built the "blockchain:address" param in
linked-account query order, which isn't stable across runs — so the batched
param (and its mocked expectations) varied seed to seed, intermittently failing
CoinstatsItem::ImporterTest and red-flagging unrelated PRs. Sort the wallets
before joining so the batch order is deterministic, and align the test
expectations to the sorted order.

* fix(mobile): tokenize swipe-reveal radius + cover SureCard dark theme

Address PR review:
- The account-card swipe-reveal background still used a hardcoded radius (12)
  that mismatched SureCard's radiusLg (10) once the card was migrated — tokenize
  it so the reveal corners line up during the swipe.
- Parameterize the SureCard chrome test over light + dark, since the card is
  brightness-aware (catches palette regressions in either mode).

* test(mobile): assert SureCard onTap ink is clipped to the card radius

Address review nit: the onTap test claimed the ink is clipped to the card but
only checked the InkWell exists. Now it asserts the InkWell's borderRadius equals
SureTokens.radiusLg (tester.widget already enforces a single InkWell).
2026-06-20 21:43:20 +02:00
Jake
dc2a565b6a feat(up): add Up Bank (AU) provider integration (#2391)
* feat(up): add Up Bank (AU) provider integration

Adds Up Bank as a per-family, token-based bank sync provider, modelled on
the existing Akahu integration. Up uses a JSON:API REST API with a personal
access token (Bearer), cursor pagination via links.next, and returns both
HELD (pending) and SETTLED transactions from one endpoint.

New:
- Provider::Up client (JSON:API unwrap, links.next pagination, retries,
  typed errors, /util/ping) + Provider::UpAdapter (Factory-registered,
  Depository + Loan).
- UpItem / UpAccount models with Provided, Unlinking, Syncer,
  SyncCompleteEvent, Importer, Processor, Transactions::Processor, and
  UpEntry::Processor (amount sign flip, HELD->pending, foreignAmount FX,
  merchant from description, stale-pending pruning).
- Family::UpConnectable, UpItemsController, routes, settings panel + connect
  flow views, accounts index wiring, initializer, en locale, and model tests.

Core wiring:
- "up" added to Transaction::PENDING_PROVIDERS, the three pending-match SQL
  blocks in Account::ProviderImportAdapter, Provider::Metadata::REGISTRY,
  ProviderMerchant/DataEnrichment source enums, ProviderConnectionStatus,
  settings provider panels, and financial data reset.

Migration create_up_items_and_accounts must be run before use. No external
API endpoints added (no OpenAPI changes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(up): dump up tables to schema and make since filter TZ-safe

The feature commit added the up_items/up_accounts migration but never
re-dumped db/schema.rb, leaving the schema version and tables stale.
Add the two table definitions and foreign keys and bump the schema
version so a fresh DB load matches the migration.

Also format a bare Date `since` as UTC midnight instead of the server's
local zone, so `filter[since]` is deterministic regardless of where the
app runs (previously shifted by the local UTC offset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(up): address code review feedback

Behavior/correctness:
- Persist skipped accounts via a new up_accounts.ignored flag and a
  needs_setup scope, so skipped accounts stop resurfacing as "needs
  setup" on every sync. Linking clears the flag.
- destroy now checks unlink_all! per-account results and aborts deletion
  (alert) if any unlink failed, instead of swallowing failures.
- render_provider_panel_error redirect uses :see_other (was an invalid
  4xx redirect status).
- Up provider adapter falls back to item institution name/url when
  institution_metadata is absent (early return previously blocked it).

Resilience/security:
- fetch_all_resources guards against an API repeating the same
  links.next cursor (Set#add?), preventing infinite pagination.
- HTTP client validates absolute URLs (from links.next) against Up's
  HTTPS host before sending the bearer token, preventing credential
  leakage to untrusted hosts.

Diagnostics:
- Route provider sync/import failures through DebugLogEntry.capture
  (controller, UpItem, syncer, unlinking) with family/account context.
  Low-level HTTP client and currency-normalization warnings keep
  Rails.logger to match existing provider conventions.

Data integrity:
- up_accounts.name and currency are NOT NULL (align with model presence
  validations); account_id stays nullable (allow_nil uniqueness).

Forms:
- select_existing_account radio is required; controller guards a blank/
  unknown up_account_id with a friendly alert instead of RecordNotFound.

Tests:
- Add UpAccount needs_setup scope test, pagination loop guard test,
  untrusted-host rejection test; tighten filter[since] assertion to the
  exact UTC timestamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(up): address second-round review feedback

- Capture sync/import failures via DebugLogEntry so swallowed errors in
  account/transaction fetching and transaction processing surface in
  /settings/debug instead of only Rails.logger.
- Gate UP_DEBUG_RAW raw payload dump to local envs to avoid leaking PII
  (merchant names, amounts, account IDs) in managed/production logs.
- Collapse linked/unlinked/total account counts into one memoized query
  instead of 3 separate COUNTs per rendered item.
- Rename "Set Up Up Accounts" locale title to "Link Up Accounts".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(up): add method docstrings and align failed_result keys

Add docstrings to all Up provider source files (controller, models,
providers, concerns) to satisfy the 80% docstring coverage threshold.

Third-round review: failed_result now mirrors import's result shape
(accounts_updated/created/failed, transactions_imported/failed) instead
of the stale accounts_imported key, so failure results stay consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:33:09 +02:00
Shibu M
10b0a687e3 Merge branch 'we-promise:main' into Transfer-charges 2026-06-20 11:06:01 +05:30
Sure Admin (bot)
318c7476e1 fix(coinstats): stabilize bulk wallet ordering (#2402) 2026-06-19 17:09:35 +02:00
Artem Danilov
9de9a23ce2 feat(prices): add T-Invest (T-Bank) securities + brand-logo provider (#2408)
Adds Provider::TinkoffInvest, a token-based securities provider built on the
public T-Invest REST gateway (invest-public-api.tinkoff.ru/rest). It serves
prices for Russian instruments (shares, ETF/БПИФ, bonds) and, crucially, brand
logos via the T-Invest CDN — the authoritative logo source for MOEX
instruments, which ISS (MoexPublic) does not provide.

- Registry: register `tinkoff_invest` under the :securities concept; token via
  ENV TINKOFF_INVEST_API_KEY or encrypted Setting.tinkoff_invest_api_key.
- Logos independent of the price provider: Security#import_brand_logo consults
  T-Invest for a logo whenever a token is configured (after the price-provider
  metadata fetch, so it never short-circuits website_url backfill). Gated on
  token presence, not the securities checklist.
- display_logo_url: with no website domain, a stored provider logo (T-Invest)
  now beats the ticker-only Brandfetch lettermark; when a domain exists,
  Brandfetch still wins (unchanged).
- MoexPublic no longer reports moex.com as the issuer website — it's the
  exchange, not the issuer, and would make Brandfetch render the exchange logo
  for every instrument and shadow the real brand logo.
- Prices: GetCandles (daily, paged) + GetLastPrices; Quotation units+nano/1e9;
  bonds priced as percent-of-par x nominal (missing nominal raises, not 0).
- Settings: encrypted token field (always shown) + provider checkbox + en locale.
- Tests for search/info/logo-url/prices/bond/incomplete-candle and display logic.

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-19 17:07:09 +02:00
sentry[bot]
84df335e7c fix(trades): guard against nil security in create_trade (#2066)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-06-19 16:54:37 +02:00
Shibu M
0b037e2027 Merge branch 'we-promise:main' into Transfer-charges 2026-06-19 10:10:31 +05:30
ghost
6ae9779567 perf(reports): avoid residual category lazy loads (#2255)
* perf(reports): avoid residual category lazy loads

* test(reports): reuse SQL query capture helper

Move the reports SQL capture regression helper into test/support and document the category loading and budget reload choices called out in review.
2026-06-18 20:19:36 +02:00
ghost
894d705e83 perf(dashboard): streamline investment activity totals (#2257)
* perf(dashboard): streamline investment activity totals

* fix(dashboard): skip empty investment totals cache writes

Short-circuit empty investment account totals before cache fetch and move the new SQL query capture regression helper into test/support.

* refactor(dashboard): address review on investment totals query

- Drop defensive ArgumentError guards in InvestmentStatement::Totals#initialize.
  The sole caller (totals_query) always passes correct types, and the
  empty_result early-return already handles the zero-accounts case.
- Remove the redundant accounts JOIN and its family_id/status filters from the
  aggregation SQL. account_ids is already scoped to the family's visible
  (draft/active) investment accounts, so the join back to accounts is
  unnecessary work in the query plan. Drop the now-unused family_id param.
- Add a regression assertion that the aggregate no longer joins accounts.
2026-06-18 20:18:18 +02:00
ghost
5811a8df15 perf(recurring): batch recurring transaction identification (#2239)
* perf(recurring): batch recurring transaction identification

* fix(recurring): clamp manual month-end matching

* test(recurring): guard amount-scoped lookup cache
2026-06-18 12:28:32 +02:00
Artem Danilov
b57bb938da feat(prices): add Moscow Exchange (MOEX ISS) securities + FX provider (#2394)
* feat(prices): add Moscow Exchange (MOEX ISS) securities + FX provider

Add Provider::MoexPublic, a keyless provider built on the free MOEX ISS API
(https://iss.moex.com/iss), modeled on Provider::BinancePublic.

Securities: shares, funds/ETF/БПИФ (e.g. LQDT), and bonds (OFZ + corporate).
Bonds are priced clean — LAST% × FACEVALUE / 100 in the instrument currency,
with per-row FACEVALUE for amortizing issues; NKD/accrued coupon excluded.

Exchange rates: also implements ExchangeRateConcept for RUB↔{USD,EUR,CNY} via
selt TOM instruments (USD000UTSTOM/EUR_RUB__TOM/CNYRUB_TOM); the selt quote is
X/RUB, inverted for RUB→X, nil for non-RUB-crossed pairs.

Details:
- Board/engine resolution via the ISS primary-board flag with a hardcoded
  priority fallback (TQBR, TQTF, TQOB, TQCB, …).
- Instrument currency from CURRENCYID/FACEUNIT (handles USD/CNY eurobonds &
  FX funds), normalizing legacy SUR/RUR → RUB; default RUB.
- Full history via from/till + start= pagination; current price fallback chain
  LAST → MARKETPRICE → LCURRENTPRICE → LCLOSEPRICE → PREVPRICE → latest history
  close.
- Bare SECID identity, exchange_operating_mic=MISX, country_code=nil (wildcard
  like Binance); search accepts .ME/.MOEX/.MISX/.MCX aliases and ISIN.
- RateLimitable throttling, SslConfigurable, Faraday retry/timeouts; all public
  methods wrapped in with_provider_response.

Wired into Provider::Registry for both :securities and :exchange_rates, the
hosting provider-selection UI, locales, and config/exchanges.yml (MISX).

Docker-tested (devcontainer, Ruby 3.4.9): 29 new tests green, full provider
suite + i18n green, rubocop clean; smoke-tested against live ISS (SBER price,
OFZ clean price, USD/RUB FX).

* fix(moex): address review — FX weekend lookback, dead branch, translated hints

- fetch_exchange_rate now fetches a 10-day lookback window (not just the exact
  day) so a weekend/holiday request resolves to the prior trading day's close,
  matching Yahoo's behavior (Codex P2).
- Remove dead identical if/else branches in history_row_price (CodeRabbit).
- Translate moex_public_hint into ca/fr/hu/vi/zh-CN instead of English copy
  (CodeRabbit).
- Add a test covering the FX prior-trading-day lookback.

* fix(moex): guard ISS date parsing; doc TQTE in board priority

Address maintainer review (jjmata):
- parse_iss_date wraps Date.parse so a malformed ISS TRADEDATE skips just that
  row (with a contextual log warning) instead of failing the whole history/FX
  fetch. Used in history_row_price and fx_history.
- Add TQTE to the BOARD_PRIORITY doc comment (it was in the constant but missing
  from the comment).
- Add a test covering the unparseable-date skip.
2026-06-18 10:36:20 +02:00
maverick
25ac29fbaf Merge branch 'we-promise:main' into Transfer-charges 2026-06-17 16:48:12 +05:30
Artem Danilov
fffcaf2345 fix(prices): resolve dashed crypto tickers (BTC-USD, TRX-USD) via BinancePublic (#2372)
* fix(prices): resolve dashed crypto tickers (BTC-USD, TRX-USD) via BinancePublic

Provider::BinancePublic is the dedicated crypto price provider, but its
search_securities only matched bare base assets ("BTC") or unseparated
Binance pairs ("BTCUSDT"/"BTCUSD"). The canonical "<BASE>-USD" form that
Yahoo emits and users paste (e.g. "TRX-USD", "USDT-USD") never matched, so
those holdings fell through to an unpriced offline security whenever the
stock provider didn't also return the coin.

Collapse the base/quote separator in the search query (and strip it in
parse_ticker) so "BTC-USD" / "TRX/USDT" are treated like "BTCUSD" and
resolve to live Binance pricing. Stablecoins pasted as "USDT-USD" resolve
to the synthetic USD price via their stripped base.

* fix(prices): only synthesize stablecoin search for USD quote

Address review: gate the synthetic stablecoin result on a USD quote (bare
coin or "<coin>USD" form). A non-USD quote like "USDTEUR" / "USDT-EUR"
has a real Binance pair and now falls through to normal matching instead of
being replaced by the USD synthetic. Drops the now-unused base_asset_query
helper. Adds a USDT-EUR regression test.
2026-06-17 11:38:34 +02:00
maverick
b4be6ca30d Merge branch 'we-promise:main' into Transfer-charges 2026-06-16 21:08:59 +05:30
Augusto Xavier
844cba2fd3 fix(imports): normalize CSV upload encoding when validations are skipped (#2299)
Import::UploadsController#update persists CSV uploads with
save!(validate: false), which skips the before_validation
:ensure_utf8_encoding callback. Non-UTF-8 files (e.g. ISO-8859-1 /
Windows-1252 exports from Brazilian banks) were written to the text
column as-is and rejected by Postgres with PG::CharacterNotInRepertoire,
surfacing as a 500 during upload.

Also register ensure_utf8_encoding on before_save so the existing
normalization (rchardet detection + Latin-1/Windows fallback) still runs
when validations are skipped. The callback is idempotent and no-ops on
valid UTF-8, so the validated path is unchanged.

Fixes #2294
2026-06-16 17:13:03 +02:00
maverick
420615c755 Merge branch 'we-promise:main' into Transfer-charges 2026-06-16 20:38:18 +05:30
SenaRinka / 陶音まの
aec851cae1 Add transaction name to get_transactions MCP tool output (#2026)
* fix(assistant): include transaction name in get_transactions MCP output

get_transactions already loads `entry = txn.entry` but only emits
`merchant`, which is null unless a merchant is assigned. As a result MCP
clients receive transactions with no identifying name. Add `entry.name`
(the description shown in the UI and returned by the REST API) at zero
extra query cost.

* fix(assistant): include transaction name in get_transactions MCP output

get_transactions already loads `entry = txn.entry` but only emits
`merchant`, which is null unless a merchant is assigned. As a result MCP
clients receive transactions with no identifying name. Add `entry.name`
(the description shown in the UI and returned by the REST API) at zero
extra query cost.
2026-06-16 16:52:39 +02:00
maverick
c6f6d1d6a0 Merge branch 'we-promise:main' into Transfer-charges 2026-06-16 15:34:10 +05:30
Artem Danilov
e342ac4ad1 fix(accounts): persist subtype when creating an account (#2356)
The Account#subtype= writer delegates to accountable&.subtype=, which is a
silent no-op while the accountable is nil. On create the accountable is built
from accountable_attributes via accepts_nested_attributes_for, but the form
submits subtype as a top-level account attribute. Mass-assignment applies
subtype before accountable_attributes, so the selected subtype was dropped on
create (update worked because the accountable already exists).

Build the accountable from the delegated type inside the writer when it is not
yet present, so the value is preserved; the later accountable_attributes
assignment (update_only) updates the same record.

Add regression tests covering the create flow and the assignment ordering.
2026-06-16 10:57:35 +02:00
Jonathan Chang
b9716a0485 fix: Transaction Pagination Skipping Entries (#2179)
* Fix transaction pagination regressions and CI blockers

* Rails EOL already fixed

---------

Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-06-16 10:51:51 +02:00
maverick
7b509dd406 Merge branch 'we-promise:main' into Transfer-charges 2026-06-16 12:02:51 +05:30
ghost
6ea931f3fe feat(imports): add YNAB CSV import (#2361)
Adds YnabImport (mirroring ActualImport) for YNAB "Export budget" register CSVs:

- Amount — combines the split Outflow/Inflow columns into a single signed amount
  (inflow - |outflow|), stripping currency symbols and thousands separators. A
  single signed Amount column takes precedence when present.
- Category — resolves across export shapes: the combined "Category Group/Category"
  column, the split "Category Group" + "Category", or legacy YNAB 4
  "Master Category" + "Sub Category".
- Names — falls back from a blank Payee to the Memo, then the default row name.
- Validation — requires at least one amount source (Outflow/Inflow or Amount); a
  file exposing none leaves rows un-clean instead of importing zero-dollar entries.

Enables the previously-disabled YNAB option on the imports screen (using the YNAB
logo, like Mint) with its configuration partial, and removes the now-dead
imports.new.coming_soon locale key. Documents the type in the API import-type
enums (rswag request spec + swagger_helper + generated openapi.yaml).

Closes #1255.
2026-06-16 08:32:17 +02:00
maverick
f3c2a3122d Merge branch 'we-promise:main' into Transfer-charges 2026-06-16 11:57:07 +05:30
Augusto Xavier
09dc428136 fix(rules): make explicit re-apply override locked attributes (#2273)
When a rule is re-applied from the UI, RulesController passes
ignore_attribute_locks: true, but Enrichable#enrich_attributes still
rejected locked attributes unconditionally, so locked (manually edited
or import-locked) transactions were silently skipped and reported as
blocked.

Thread the flag through enrich_attribute/enrich_attributes as a new
ignore_locks keyword (default false, so provider syncs and AI
enrichment keep respecting locks) and pass it from the six synchronous
rule action executors.

Fixes #2051
2026-06-15 22:12:24 +02:00
Guillem Arias Fauste
c29380ce57 feat(dashboard): masonry packing + per-widget size controls (#2328)
* feat(dashboard): masonry packing + per-widget size controls

In two-column mode the dashboard used a row-based CSS grid, so cards
stretched to equal row height and left dead space (e.g. the Net Worth
chart padded out to match the tall Balance Sheet table). Replace the
row-based layout with masonry packing and add per-widget size guardrails.

- Masonry: CSS grid with computed row-spans + grid-auto-flow: dense, driven
  by a dashboard-masonry Stimulus controller (ResizeObserver + turbo:frame-load).
  The DOM stays a single flat list, so drag/keyboard reorder is unaffected.
  Active only in multi-column mode; single column falls back to normal flow.
- Internal sizing: the net worth chart height is now driven by a
  --dash-widget-h CSS var (fixes an inert flex-1) so the card no longer pads
  out below the chart.
- Guardrails: per-widget layout metadata (col_span, grow, min_height,
  width_toggle) in PagesController, with per-user overrides persisted under
  preferences["dashboard_section_layout"], deep-merged so width and height coexist.
- Size menu: a hover control on size-capable cards — Width (Half/Full) for the
  cashflow sankey and net worth chart, Height (Compact/Auto/Tall) for grow
  widgets. The sankey defaults to full width.

Adds model + controller tests for preference persistence and i18n keys.

* refactor(dashboard): redesign size menu with segmented controls

The size menu used a plain radio list and a diagonal maximize-2 trigger that
collided with the cashflow sankey's modal-expand button. Replace it with a
layout-config popover: a sliders-horizontal trigger plus two labeled axis
groups (Width, Height), each rendered as a DS::SegmentedControl with the
active option filled. Clearer, more compact, and it reads as a card-layout
control. The widget-size controller now mirrors the segmented control's
active-class + aria-pressed contract.

* feat(dashboard): expose width toggle on balance sheet and investments

Tables benefit from horizontal room, so give Balance Sheet and Investments
the same Width (Half/Full) control as the sankey and net worth chart. Height
presets stay chart-only — a table sized to a fixed height would just add
whitespace or force scrolling. The Outflows donut is intentionally left out
(full width is mostly whitespace for a donut).

* fix(dashboard): address review feedback on size controls

- Only apply the full-width col-span and show the Width control when the
  two-column layout is enabled. A full widget previously leaked
  2xl:col-span-2 into the single-column grid, creating an implicit second
  column at 2xl widths and breaking the single-column preference. This also
  keeps the Width control coherent with the Appearance two-column setting
  (it now appears only where it does something). [Codex]
- Stop size-menu keydowns from bubbling to the section reorder handler, so
  keyboard users can open the menu and pick options without entering
  grab/reorder mode. [Codex]
- Harden preferences params: ignore a malformed (non-hash)
  dashboard_section_layout / collapsed_sections instead of raising a 500,
  and require section_order to be an array. [CodeRabbit]
- Localize the dashboard sections aria-label. [CodeRabbit]

* chore(settings): mention per-widget size controls in two-column copy

Surface the new per-widget width/height controls in the Appearance
"Two-column layout" description so the capability is discoverable.

* test(dashboard): assert non-mutation for malformed layout input

Addresses review feedback: asserting assert_nil made the test depend on
the fixture happening to have no dashboard height for net_worth_chart.
Capture the pre-PATCH value and assert it is unchanged, so the test
stays valid (malformed input ignored) even if the fixture later gets a
default height.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-06-15 20:34:34 +02:00
Sure Admin (bot)
08bfd43775 fix: preserve CoinStats balances when wallet sync data is missing (#2132)
* test: cover CoinStats missing-wallet balance preservation

* fix: preserve CoinStats balances when wallet data is missing

* chore: route CoinStats sync warnings to debug log

* docs: make debug log guidance timeless

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-06-15 20:32:59 +02:00
Sure Admin (bot)
89eb441145 fix(sync): discover nightly provider items reflectively (#2334) 2026-06-15 20:09:39 +02:00
maverick
5948d79a00 Merge branch 'main' into Transfer-charges 2026-06-15 08:40:25 +05:30
Guillem Arias Fauste
5937195df0 fix(chat): clear assistant bubble on destroy so the chat doesn't hang on Thinking (#2315)
* fix(chat): clear the assistant message bubble when a turn is destroyed

When an assistant turn fails before any text streams (e.g. a provider
auth/model/network error on the first call), Assistant::Builtin#respond_to
destroys the still-pending message. Message only broadcast on create and
update, never on destroy, so the rendered 'Thinking…' bubble was never
removed — the chat appeared stuck thinking forever even though the job
had already errored (and appended an error via chat#add_error below it).

Add after_destroy_commit broadcast_remove_to so a destroyed message is
removed from the page.

* refactor(chat): trim destroy-broadcast comment to one line

Project convention asks for comments only when the why is non-obvious; the
behaviour is already covered by the commit/PR description. Per review feedback.

---------

Co-authored-by: Guillem Arias <guillem.arias@col.vueling.com>
2026-06-14 22:09:02 +02:00
ghost
88343002d1 chore(deps): upgrade Rails 7.2 → 8.1 (#2301)
* chore(deps): upgrade Rails 7.2 → 8.1

Rails 7.2 reaches end of life on 2026-08-09. Bump the framework to the
current 8.1.x line.

- Gemfile: rails "~> 8.0" (resolves 8.1.3); bundle update rails pulls the
  Rails 8 framework gems plus the bumps it requires — ViewComponent
  3.23 → 4.x (Rails 8 support), rails-i18n 7 → 8, rswag, and transitive deps.
- app/models/transfer.rb: make Transfer#date nil-safe
  (inflow_transaction&.entry&.date). Rails 8's date_field evaluates the
  field default on a new/unpersisted Transfer (the new-transfer form), where
  the association is nil; without this, TransfersController#new raises
  "undefined method 'entry' for nil". Matches the &. pattern already used in
  Transfer#sync_account_later.

Framework behavioral defaults are unchanged (config.load_defaults stays as-is).

Validated on Rails 8.1.3: zeitwerk:check passes, full suite green
(4904 runs, 0 failures, 0 errors), rubocop and brakeman clean.

* fix(rails8): style textarea + deterministic property edit system test

The Rails 8 gem bump kept config.load_defaults at 7.2, but Rails 8 renamed
two ActionView::Helpers::FormBuilder field helpers regardless of defaults:
:text_area → :textarea and :check_box → :checkbox. StyledFormBuilder builds
its styled helpers from `field_helpers`, so `form.text_area` (e.g. the
account "Notes" field) silently fell through to the unstyled base helper and
rendered without a label — failing 8 system tests with
`Unable to find field "Notes"`.

- app/helpers/styled_form_builder.rb: exclude both spellings of the
  non-text helpers (:check_box and :checkbox) and alias the legacy
  `text_area` to the Rails 8 `textarea` so existing call sites stay styled.
  Harmless on Rails 7.2 (old names present instead).

- test/system/property_test.rb: open the property edit dialog via the
  account menu with a retry. The account page issues a Turbo morph refresh
  shortly after load (turbo_refreshes_with :morph + a family-stream
  broadcast); opening the modal while that refresh is in flight let the
  morph re-render the page and wipe the just-loaded #modal turbo-frame.
  Rails 8 timing made the race deterministic. Retrying once the refresh has
  settled makes the test stable (confirmed via Turbo frame-load vs
  full-page morph event traces; 3x green in isolation).

- config/brakeman.ignore: the added comment block shifted the pre-existing
  (already-ignored, Weak) class_eval Dangerous Eval warning from line 5 -> 10,
  changing its fingerprint. Re-point the existing suppression to the new
  fingerprint/line so scan_ruby stays green.

Validated on Rails 8.1.3: full system suite green
(92 runs, 355 assertions, 0 failures, 0 errors), rubocop clean,
brakeman 0 warnings, CodeRabbit no findings.

* chore(deps): pin rails to the 8.1 minor line (~> 8.1.0)

Tighten the constraint from `~> 8.0` to `~> 8.1.0` (>= 8.1.0, < 8.2) so a
future `bundle update rails` tracks the 8.1.x line rather than silently
jumping to 8.2 when it ships. Matches the upgrade plan's stated intent
(target 8.1.x for the EOL runway) and a review note on #2301.

No resolved-version changes: bundle install keeps rails at 8.1.3 and every
other locked gem unchanged — only the Gemfile.lock DEPENDENCIES constraint
line moves. zeitwerk:check still passes; the already-green unit/system
suites ran on this exact resolved tree.

* chore(rails8): adopt Rails 8.1 framework defaults (config.load_defaults 8.1)

The gem bump above kept config.load_defaults at 7.2 so the change set could be
reasoned about in stages; this finalizes the upgrade by adopting the modern
framework defaults now that the suite is green on Rails 8.1.

Rails 8.0 added no new framework defaults (there is no new_framework_defaults_8_0
template), so 7.2 -> 8.1 is the single meaningful step. No incremental
new_framework_defaults_8_1.rb opt-in file is needed: the full suites pass with all
8.1 defaults enabled at once.

The 8.1 defaults this turns on include action_on_path_relative_redirect=:raise
(open-redirect hardening), raise_on_missing_required_finder_order_columns,
escape_json_responses=false / escape_js_separators_in_json=false (JSON perf), and
Ruby-parser template-dependency tracking.

Validated with no application code changes: bin/rails test 4904/0/0,
bin/rails test:system 92/0/0, rubocop + brakeman clean.

* chore(ci): restore brakeman CheckEOLRails now that the app is on Rails 8.1

config/brakeman.yml existed only to skip brakeman's CheckEOLRails. That check
fires on the calendar (it warns 60 days before a framework's EOL and escalates
as the date nears), so Rails 7.2's 2026-08-09 EOL turned `bin/brakeman` red
(exit 3) on every branch and on main regardless of the diff. The skip carried a
TODO to remove it once Sure upgraded off 7.2.

This PR puts the app on Rails 8.1 (EOL well in the future), so the skip is
obsolete; remove the file (its sole content was the skip) in the same change that
makes it unnecessary -- no stale-config window. brakeman auto-loads the file when
present and falls back to defaults when absent, and nothing references it
explicitly. CheckEOLRuby was already enabled and is unchanged; config/brakeman.ignore
is untouched.

Validated on Rails 8.1: bin/brakeman runs EOLRails + EOLRuby, 0 warnings,
0 errors, exit 0.
2026-06-14 21:48:14 +02:00
Shibu M
fe6b2f9872 merged with main 2026-06-12 09:13:24 +00:00
BeltaKoda
749d54dd96 Fix Plaid sync failure for loan subtypes missing from Loan::SUBTYPES (#2298)
PlaidAccount::TypeMappable maps the Plaid loan subtypes "home equity",
"line of credit", and "business" to home_equity, line_of_credit, and
business — but Loan::SUBTYPES never defined them. Linking any such
account (e.g. a HELOC reported by the institution as loan/"line of
credit") makes the item's sync fail with:

    Validation failed: Accountable subtype is not included in the list

and, because Link itself succeeded, the failure is silent in the UI
(same UX gap as #1792).

Add the three subtypes to Loan::SUBTYPES, and add a regression test
asserting every subtype emitted by TYPE_MAPPING is valid for its
accountable so the mapper and models can't drift apart again.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:26:40 +02:00
Rene Arredondo
0e1b3f8396 fix(enable-banking): tolerate any 422 on PDNG fetch (#1805) (#1889)
* fix(enable-banking): tolerate any 422 on PDNG fetch (#1805)

* fix(enable-banking): fall back to string key when reading PDNG error
2026-06-11 16:48:40 +02:00
Kelvinchen03
59edcaa986 fix: EODHD lookup for EU mutual funds with EUFUND exchange code (#2212) 2026-06-11 16:17:17 +02:00
ghost
de8cd86f2f perf(sync): scope transfer matching after account sync (#2230)
* perf(sync): scope transfer matching after account sync

* fix(sync): make transfer lookup index migration reversible
2026-06-11 14:39:36 +02:00
ghost
3ccb82ef9d fix(imports): import Actual rows with blank payee (#2282)
* fix(imports): import Actual rows with blank payee

Actual Budget exports reconciliation and starting-balance rows with a
blank Payee. ActualImport mapped the row name straight from the Payee
column with no fallback (unlike Import and MintImport), so a blank Payee
produced a blank Entry name. Entry requires a name, and import! wraps all
rows in a single transaction, so one blank-payee row failed validation
and rolled back the entire import -- surfacing only a generic "Import
failed" while the worker logged "done".

Fall back to the Notes column (which carries text like "Reconciliation
balance adjustment") and then to the default row name, matching the
blank-name handling already used by the base importer and MintImport.

Add a blank-payee row to the Actual fixture and regression tests covering
the Notes fallback, the default fallback, and an end-to-end import that
no longer fails on blank-payee rows.

* ci(security): skip calendar-based brakeman Rails EOL check

The scan_ruby job fails because brakeman's CheckEOLRails warns that Rails
7.2.3.1 reaches end of life on 2026-08-09. That check fires purely on the
calendar -- it warns 60 days before the EOL date and escalates in
confidence as the date nears (brakeman/checks/eol_check.rb) -- so it turns
`bin/brakeman` (exit code 3) red on every branch and on main regardless of
the code being scanned.

Add config/brakeman.yml (auto-loaded by `bin/brakeman`) skipping only
CheckEOLRails. CheckEOLRuby is left enabled because the current Ruby is not
near end of life, so that signal is preserved. A TODO records that the skip
should be removed when Sure upgrades off Rails 7.2.
2026-06-11 09:04:56 +02:00
maverick
17babb19ba Merge branch 'main' into Transfer-charges 2026-06-10 10:55:06 +05:30
Guillem Arias Fauste
f25fe30a41 feat(ai): honor Setting.llm_provider for batch and PDF flows (#2265)
Auto-categorization, merchant detection/enhancement, and PDF/bank-statement
extraction hard-coded Provider::Registry.get_provider(:openai), so selecting
Anthropic (or running an Anthropic-only self-hosted install) left those
operations using/missing OpenAI rather than the chosen provider.

Add Provider::Registry.preferred_llm_provider, which resolves the LLM provider
honoring Setting.llm_provider with a configured-provider fallback (mirroring how
chat picks its provider), and route all six TODO(#2113) call sites through it:

- Family::AutoCategorizer#llm_provider
- Family::AutoMerchantDetector#llm_provider
- ProviderMerchant::Enhancer#llm_provider
- PdfImport (process_pdf + extract_bank_statement)
- Assistant::Function::ImportBankStatement

Provider::Anthropic already implements auto_categorize / auto_detect_merchants /
enhance_provider_merchants (#1984) and process_pdf / extract_bank_statement
(#1985), so no provider changes are needed — only the wiring.

Closes #2113.
2026-06-09 23:02:26 +02:00
maverick
016a1acf35 Merge branch 'main' into Transfer-charges
Signed-off-by: maverick <23173570+DataEnginr@users.noreply.github.com>
2026-06-09 18:50:23 +05:30
Dan
3627c4d2d1 Respect CoinStats wallet rate limits (#2251) 2026-06-08 22:40:34 +02:00
Guillem Arias Fauste
3b4aa55516 test(goals): pledge-delta integration coverage (follow-up to #2178) (#2206)
* fix(goals): match manual_save pledges by contribution delta, not full balance

Closes #2177

* fix(goals): clarify depository-only contribution; lowercase test names

Address review: document that valuation_contribution's delta is only consumed
for goal-linked Depository accounts, so the positive-delta guard is correct and
no liability paydown sign case can arise. Lowercase NOT in two test names to
match project convention.

* test(goals): pin pledge matching through the reconciliation manager

Follow-up to #2178, which matched manual_save pledges by contribution
delta but only unit-tested the matcher with an injected delta. The
delta derivation itself (the balances-table lookup and the nil-to-0
fallback in valuation_contribution) had no coverage, and
reconciliation_manager_test never touched pledges.

Two manager-level tests pin the wiring end to end: a 2000 -> 2150
reconcile matches an open 150 pledge, and a same-balance reconcile
(zero delta) leaves it open, closing the <= 0 boundary.

Also documents the staleness window on valuation_contribution: the
prior balance reads the balances table, which recomputes async after
the valuation saves, so a second same-day reconcile racing that sync
derives a stale delta and self-heals on the next save.

* fix(goals): derive same-day re-reconcile delta from the prior valuation

The balances table recomputes asynchronously after a reconcile, so a
second same-day reconcile racing that sync derived its delta from the
pre-first-reconcile row. An overstated delta could wrongly close a
larger open pledge — and a wrong match, unlike a miss, never
self-heals.

Prefer the valuation's own pre-save amount as the prior balance when
the reconcile updates an existing valuation; fall back to the balances
row, then 0. Same-date re-reconciles are immune to the race; only the
cross-date window remains, and only in the miss direction.

---------

Co-authored-by: galuis116 <galuis116@gmail.com>
2026-06-08 21:45:35 +02:00
Guillem Arias Fauste
d845e44ff8 feat(ai): default Anthropic installs to pgvector RAG (4/5) (#1986)
* feat(ai): add Anthropic provider with chat parity (1/5)

Introduces Provider::Anthropic alongside Provider::Openai, implementing
the LlmConcept chat_response contract over the official anthropic Ruby
SDK. Batch ops, PDF, and RAG land in follow-up PRs.

- Provider::Anthropic uses Messages API for sync and streaming responses
- ChatConfig builds requests with ephemeral prompt-cache markers on the
  system prompt and the last tool definition
- MessageFormatter reconstructs multi-turn history (text + tool_use +
  tool_result blocks) from raw Message records, including the paired
  user-role tool_result turn Anthropic requires after every tool_use
- ChatParser maps Anthropic Message into the shared ChatResponse Data
- Registry, Setting, User, Chat default model wired for ANTHROPIC_*
  envs and Setting.anthropic_*; LLM_PROVIDER selects between providers
- Responder forwards raw conversation_history (Array<Message>) so
  providers without hosted conversation state can rebuild context
- OpenAI provider accepts and ignores the new kwarg (no behavior change)

Tests cover provider init, model gating, MessageFormatter for all turn
shapes, ChatConfig request building (max_tokens, system cache, tool
conversion), ChatParser for text / tool_use / mixed blocks, Registry
discovery, and mocked chat_response success / error / function_request
paths. Live VCR cassettes recorded in a follow-up with a real key.

Stacked PRs: 2/5 batch ops + cost ledger, 3/5 PDF, 4/5 pgvector RAG,
5/5 settings UI + disclosure.

* fix(ai): address PR review on Anthropic provider foundation

Surface fixes raised by Codex + CodeRabbit on PR 1/5:

- Provider::Anthropic#chat_response now accepts (and ignores) a
  `messages:` kwarg. Assistant::Responder passes both `messages:`
  (OpenAI-shape) and `conversation_history:` (raw Message records) for
  cross-provider parity, so the previous signature raised
  ArgumentError on the first chat turn through the Anthropic provider.
- Provider::Anthropic#supports_model? bypasses the `claude` prefix
  gate when a custom base_url is configured, mirroring the OpenAI
  provider. Bedrock-shaped IDs like
  `anthropic.claude-sonnet-4-5-20250929-v1:0` and
  `claude-opus-4@20250514` are otherwise rejected by
  Assistant::Provided#get_model_provider and the chat dies.
- Setting.anthropic_access_token is now in
  EncryptedSettingFields::ENCRYPTED_FIELDS so the Anthropic API key
  is encrypted at rest like every other provider secret. Previously
  plaintext while siblings (openai_access_token, twelve_data_api_key,
  external_assistant_token) were ciphertext.
- Chat.default_model falls back to whichever provider is actually
  configured. Previously, with LLM_PROVIDER=anthropic but no
  Anthropic credentials, the default model resolved to a Claude ID
  that no registered provider supported, so chats failed even when
  OpenAI was fully configured. Adds Provider::{Anthropic,Openai}#configured?
  class methods for the readable callsite.
- Provider::Anthropic.effective_model uses
  `ENV["ANTHROPIC_MODEL"].presence || Setting.anthropic_model` so the
  Setting lookup is only performed when the env var is absent — the
  previous `ENV.fetch(KEY, default)` evaluated the default arg
  eagerly on every call.
- Provider::Anthropic::ChatConfig#anthropic_input_schema strips both
  `:strict` and `"strict"` keys so JSON-decoded schemas with string
  keys cannot leak the OpenAI-only flag through to Anthropic.

Test coverage added: supports_model? bypass on custom endpoints,
chat_response messages: kwarg compatibility, default_model fallback
in the three credential combinations, configured? against ENV +
Setting, strict-flag stripping for both key types, and a
`Setting.expects(:anthropic_model).never` assertion proving the
ENV-precedence test now exercises the lazy path.

All 4365 tests pass (1 pre-existing libvips env error unrelated).

* test(chat): make default_model tests resilient to ENV model overrides

CodeRabbit flagged on PR review: the new default_model tests asserted
against Provider::*::DEFAULT_MODEL, but Chat.default_model actually
returns Provider::*.effective_model.presence (which reads
OPENAI_MODEL / ANTHROPIC_MODEL from the environment). With either env
var set, the tests would fail intermittently even though routing was
correct.

- New default_model tests now assert against the provider's
  effective_model directly, so they verify the routing decision
  (which provider's value wins) without coupling to the constant.
- Pre-existing "creates with default model" assertions had the same
  brittleness; switch them to compare against Chat.default_model so
  the chosen model is whatever the env / Setting cascade resolves to.

Verified by running `ANTHROPIC_MODEL=claude-haiku-4-5 OPENAI_MODEL=gpt-4o
bin/rails test test/models/chat_test.rb` — 16 runs, 0 failures
(previously 2 pre-existing failures + 0 from the new tests).

* fix(ai): address local review on Anthropic foundation

- Provider::Anthropic#supports_pdf_processing? bypasses prefix gate for
  custom endpoints, mirroring supports_model?
- Provider::Anthropic#initialize raises Error when custom_endpoint? AND
  model.blank?, parity with Provider::Openai
- stream_chat_response captures partial usage on mid-stream errors and
  records it via the new on_partial callback so chat_response can skip
  the duplicate error row in the outer rescue
- safe_accumulated_message swallows the secondary failure when the SDK
  cannot reconstruct a snapshot
- langfuse_client memoizes properly (||= instead of =) so repeated calls
  don't churn Langfuse instances
- MessageFormatter sorts tool_calls by created_at then id so the
  message array is deterministic across replays; skips tool_calls
  missing both provider_call_id and provider_id rather than sending
  `id: nil` and getting rejected by Anthropic
- Setting.anthropic_access_token default falls back through
  ENV["ANTHROPIC_API_KEY"].presence (was missing .presence, so an
  empty-string env value bled through)
- User#openai_configured? / #anthropic_configured? delegate to the
  Provider::* class methods — single source of truth
- Assistant::Responder renames the OpenAI-shape history builder
  conversation_history → openai_messages_payload so the kwarg name
  matches the local method name (messages: openai_messages_payload,
  conversation_history: chat_message_records)
- Assistant::Builtin stale-history comment updated to reference both
  builders

Adds a streaming chat_response test using ad-hoc subclasses of the
SDK event types so the case/when dispatch matches via is_a? without
stubbing class-level === behavior.

* test(ai): add Anthropic tool_use round-trip + multi-tool turn coverage

Addresses @jjmata's "worth confirming" note on PR #1983: tool-use turns
from prior assistant messages must round-trip correctly when retrieved
from the database.

- New `ChatParser → ToolCall::Function → MessageFormatter` test walks
  the full path: Anthropic response with a tool_use block →
  ChatFunctionRequest → ToolCall::Function.from_function_request →
  persisted on the AssistantMessage → MessageFormatter rebuild on the
  next turn. Asserts the original `tool_use.id` is preserved end-to-end
  as both `tool_use.id` and the paired `tool_result.tool_use_id`, and
  that the original `input` hash and serialized result content survive.
- New multi-tool assistant turn test confirms two tool_use blocks on a
  single assistant message render as two tool_use blocks followed by
  two paired tool_result blocks in a single user-role follow-up,
  matching Anthropic's required alternation.

Both tests exercise the existing PR1 code without behavior changes.

* test(ai): require "ostruct" explicitly in Anthropic provider tests

OpenStruct is moving out of Ruby's default load path (warning in 3.4+,
removed in 3.5+). Tests work today because ActiveSupport transitively
loads it, but that's incidental. Match the existing convention in
test/controllers/settings/hostings_controller_test.rb which explicitly
requires ostruct for the same reason.

* fix(ai): sanitize Langfuse warn logs, normalize tool_use.input, dedup history fetch

Addresses three open CodeRabbit findings on PR #1983.

- Provider::Anthropic Langfuse rescue branches no longer include
  `e.full_message` in `Rails.logger.warn`. `full_message` bundles the
  backtrace + cause chain and on some SDK error types includes the
  serialized request/response payload (prompt, model output). Logs
  now report `#{e.class}: #{e.message}` only. Three sites:
  create_langfuse_trace, log_langfuse_generation, upsert_langfuse_trace.
  Note: Provider::Openai has the same pattern (copy-pasted source) —
  harmonization deferred to a follow-up cleanup PR; this commit fixes
  only the Anthropic provider to keep PR scope tight.

- MessageFormatter#parse_arguments now coerces any non-Hash parsed
  result to `{}`. Anthropic's Messages API requires `tool_use.input`
  to be a JSON object (map); a stored ToolCall::Function record whose
  arguments parse to a scalar, bool, or array (corrupt row, legacy
  data, cross-provider bleed) would otherwise produce a payload the
  API rejects. Normal flow stores Hash arguments end-to-end so the
  fix is defensive — adds 2 tests covering scalar/array JSON strings
  and non-String non-Hash inputs.

- Assistant::Responder dedups the chat-history fetch. The previous
  layout fired two near-identical `chat.messages.where(...).includes(
  :tool_calls).ordered` queries per LLM turn (one for the OpenAI-shape
  payload, one for the raw-records kwarg). A new memoized
  `complete_chat_messages` fetches once; `chat_message_records` filters
  out the current message via `Array#reject`, `openai_messages_payload`
  iterates the cached array unchanged. One SQL query per turn instead
  of two. Memoization scope = single Responder instance (per LLM call),
  so cache invalidation is not a concern.

All 4370 tests pass (1 pre-existing libvips env error unrelated).
Rubocop + brakeman clean.

* fix(ci): replace sk-ant- prefixed test placeholders

Pipelock secret scanner pattern-matches `sk-ant-*` as a real Anthropic
API key and fails the PR security-scan check. Test stubs and
ClimateControl env values used `sk-ant-test`, `sk-ant-from-setting`,
`sk-ant-x`, `sk-ant-y` as obvious placeholders, but the scanner does
not care about value entropy.

Switched to `fake-anthropic-key-*` / `fake-token-*` strings so the
scanner stops flagging them. No production code touched, no behavior
change — Provider::Anthropic still accepts any non-blank token.

* feat(ai): add Anthropic batch ops + LLM cost ledger (2/5)

Implements auto_categorize, auto_detect_merchants, and
enhance_provider_merchants on Provider::Anthropic via forced tool calls,
plus the cost-ledger plumbing they need.

- Provider::Anthropic::AutoCategorizer, AutoMerchantDetector,
  ProviderMerchantEnhancer each define a single output tool whose
  input_schema mirrors the desired output, then force the model to call
  it via tool_choice: { type: "tool", name: ..., disable_parallel_tool_use: true }.
  Anthropic guarantees the tool_use.input matches the schema, so there
  is no JSON parsing fragility, no <think> tag stripping, and no
  json_object/json_schema fallback ladders.
- Concerns::UsageRecorder mirrors the OpenAI sibling but persists
  cache_creation_input_tokens / cache_read_input_tokens to dedicated
  columns instead of metadata.
- Migration adds cache_creation_tokens, cache_read_tokens (nullable
  integers) to llm_usages. OpenAI rows leave them null.
- LlmUsage::PRICING gains Claude 4.x rows (opus-4-7 $15/$75, sonnet-4-6
  $3/$15, haiku-4-5 $1/$5 per MTok). infer_provider returns "anthropic"
  for claude-* via the existing exact/prefix lookup.
- Provider::Anthropic#chat_response now persists cache columns directly
  rather than stashing them in metadata.
- 25-transaction batch cap mirrors the OpenAI provider so the cost
  ledger sees the same shape regardless of which provider ran a batch.

Tests cover the forced-tool-call path, null/None normalization,
case-insensitive merchant matching, the missing-tool_use error path,
and Anthropic-specific pricing + provider inference on LlmUsage.

Stacked on #1983 (PR 1/5). 3/5 PDF + vision next.

* fix(ai): attribute Bedrock model IDs to anthropic + clean nil enum

- LlmUsage.infer_provider now returns "anthropic" for Bedrock /
  Vertex shaped IDs (anthropic.* and anthropic/*), so cost-ledger
  filtering by provider stays correct even when no per-MTok rate is
  stored. Previously these IDs fell through to the "openai" default.
- AutoCategorizer drops the redundant nil sentinel from the
  category_name enum — the union type [string, null] already permits
  null, and some JSON Schema validators reject nil literals inside
  enum arrays.

* test(ai): require "ostruct" in Anthropic batch op tests

Same rationale as the PR1 ostruct fix — explicit require so the tests
don't depend on ActiveSupport's transitive load when Ruby 3.5+ removes
OpenStruct from the default load path.

* feat(ai): Anthropic native PDF processing (3/5)

Implements process_pdf and extract_bank_statement on Provider::Anthropic
using the native `document` content block — no rasterization, no text
pre-extraction.

- Provider::Anthropic::PdfProcessor classifies the document, summarizes
  it, and extracts statement metadata via a forced report_document_analysis
  tool whose input_schema mirrors the existing Provider::Openai output
  (document_type from Import::DOCUMENT_TYPES, summary, extracted_data).
- Provider::Anthropic::BankStatementExtractor returns the same
  { transactions, period, account_holder, account_number, bank_name,
  opening_balance, closing_balance } shape via report_bank_statement so
  downstream pdf_import code is provider-agnostic.
- Both attach the PDF as
  { type: "document", source: { type: "base64", media_type: "application/pdf", data: <b64> } }
  — Claude 3.5+ / 4.x accept this natively (up to 32MB / 100 pages).
  No pdf-reader, no pdftoppm, no chunking for typical statements.
- supports_pdf_processing? (introduced in PR 1) already returns true for
  claude-* models, gating process_pdf with a clear error otherwise.
- Cost ledger rows are persisted via the shared UsageRecorder concern,
  including cache_creation/cache_read tokens.

Tests verify the document block shape, tool_choice forcing, normalized
document_type for unknown classifications, transaction normalization
(date / amount / reference → notes), and the missing-tool_use error
path. Blank pdf_content raises before any client call.

Stacked on #1984 (PR 2/5). 4/5 pgvector RAG next.

* fix(ai): guard PDF size + surface bank-statement truncation

- PdfProcessor and BankStatementExtractor raise upfront when
  pdf_content.bytesize exceeds MAX_PDF_BYTES (32 MB, matching
  Anthropic's hard limit). Previously a 100 MB PDF would be
  base64-encoded (~133 MB) and packed into the JSON body before
  the API rejected it — peak heap ~270 MB per Sidekiq worker.
- BankStatementExtractor inspects response.stop_reason; when the
  model hit max_tokens it logs a warning and flags result[:truncated]
  so downstream callers know the transaction list may be incomplete.
- ISO date pattern added to statement_period_start/end schema in
  PdfProcessor so the model can't return "March 2026" — Anthropic
  enforces the regex via the tool's input_schema.

Tests cover the size guard (raises before any client.messages call),
truncated-result flagging, and the warning log path.

* test(ai): require "ostruct" in Anthropic PDF tests

Match the explicit ostruct require added in PR1/PR2 — same Ruby 3.5+
load-path reason.

* feat(ai): default Anthropic installs to pgvector RAG (4/5)

The provider-agnostic vector store stack (VectorStore::Pgvector + the
Embeddable concern) already shipped to main. This PR closes the
Anthropic loop:

- VectorStore::Registry.adapter_name now returns :pgvector when
  Setting.llm_provider == "anthropic" and no explicit
  VECTOR_STORE_PROVIDER override is set. Anthropic has no hosted vector
  store, so falling back to the local pgvector adapter is the only
  correct default. Explicit VECTOR_STORE_PROVIDER still wins.
- SearchFamilyFiles surfaces a longer message when no adapter is wired
  up — calling out pgvector + EMBEDDING_URI_BASE as the supported
  Anthropic-only path so the user is not stuck with an "OpenAI required"
  hint that is no longer accurate.

The Embeddable concern already pulls embeddings from
EMBEDDING_URI_BASE / EMBEDDING_ACCESS_TOKEN (with OpenAI as fallback),
so Anthropic installs point this at Voyage AI, a local Ollama instance,
or OpenAI embeddings — independent of the chat provider.

Tests cover the new default routing, the existing OpenAI default
staying intact, and explicit VECTOR_STORE_PROVIDER overriding the
Anthropic default.

Stacked on #1985 (PR 3/5). 5/5 settings UI + retention disclosure next.

* fix(ai): provision pgvector table when it is the default store

#1986 makes pgvector the default vector store for Anthropic installs, but
CreateVectorStoreChunks only ran when VECTOR_STORE_PROVIDER=pgvector was set
explicitly — so a fresh Anthropic-only install migrated without the
vector_store_chunks table and failed on uploads/searches.

Add VectorStore::Registry.pgvector_effective? as the single source of truth
for "is pgvector active?" (explicit env OR the Anthropic default), and a new
idempotent migration that enables the extension + creates the table whenever
pgvector is effective and the table is missing — covering fresh and
already-migrated installs without drift. Addresses Codex P1.

* fix(ai): provision pgvector table for Anthropic-default installs

Migration gated on raw VECTOR_STORE_PROVIDER==pgvector, so an
Anthropic-default install (which selects pgvector implicitly via
Setting.llm_provider without setting VECTOR_STORE_PROVIDER) skipped
table creation and failed later on a missing vector_store_chunks
relation. Route through VectorStore::Registry.pgvector_effective? —
the single source of truth already shared by the adapter selection.

Addresses Codex P1 review finding.

* fix(ai): provision pgvector chunks table on schema-load installs

The ensure-migration only helps db:migrate upgraders. Fresh installs go
through bin/docker-entrypoint's db:prepare, which loads schema.rb (the
conditional table can't be dumped there — it needs the vector extension)
and marks every migration applied without running it. An Anthropic-only
fresh install therefore selected the pgvector adapter but had no table,
failing with raw PG errors on first upload or search.

Two layers close it:

- VectorStore::Pgvector#ensure_schema! provisions the table idempotently
  on first use (mirrors CreateVectorStoreChunks; memoized; failures wrap
  in VectorStore::Error, which with_response turns into a clean failed
  response).
- VectorStore::Registry#build_pgvector now gates on
  VectorStore::Pgvector.available? (table exists, or extension present),
  so installs whose Postgres lacks pgvector entirely degrade to the
  assistant's provider_not_configured message instead of raising
  mid-chat.

Also resolves the schema.rb version conflict against main (keep the
branch's 2026_06_01_120000, on top of main's current tables).

* fix(ai): address review nitpicks on pgvector provisioning

- Registry: update the adapter doc comment to mention the
  Anthropic-to-pgvector default alongside the openai fallback.
- ensure_schema!: guard the DDL with if_not_exists instead of a Mutex.
  Adapter instances are built per call and never shared across threads,
  so the realistic race is two processes (web + Sidekiq) provisioning
  concurrently; IF NOT EXISTS makes the loser a no-op where a Mutex
  would only serialize threads inside one process.
2026-06-08 21:35:12 +02:00
Shibu M
ecf19ed1f9 Merge remote-tracking branch 'upstream/main' into Transfer-charges 2026-06-08 15:30:15 +00:00
Jonathan Chang
8a38a82c93 fix: Savings Goal Marked Reached While Still Short (#2180)
* fix: saving goal is marke while several states

* feat(enable_banking): support MFA/decoupled banks and harden session handling (#2174)

Decoupled/MFA banks (e.g. VR Bank in Holstein) were hard-blocked because the
authorize flow aborted whenever auth_methods[0] was DECOUPLED. Enable Banking's
hosted /auth page actually coordinates decoupled SCA and redirects back with a
code, so route these banks through it instead:

- Provider#start_authorization accepts and forwards an auth_method param
- EnableBankingItem#select_auth_method picks the best method
  (REDIRECT > DECOUPLED > EMBEDDED), filtering by psu_type and skipping hidden
  methods
- Shared begin_authorization! re-fetches ASPSP metadata on each authorize and
  reauthorize, so the method is always re-derived (no persistence required)
- Remove the DECOUPLED block in the controller

Also stop the integration from constantly reporting "session expired":

- Only a session-level GET /sessions 401/404 flips the connection to
  requires_update; per-account 401/404 are retried and no longer kill the
  whole connection
- Reconcile session_expires_at from the API's access.valid_until on every sync
- Treat an expired session as a graceful requires_update state instead of
  raising a bare error

No schema changes. Adds covering tests.

* fix: request change reach

---------

Co-authored-by: Tobias Rahloff <rahloff@gmail.com>
2026-06-06 17:25:27 +02:00
ghost
1448128762 perf(reports): collapse investment flow aggregates (#2190)
* perf(reports): collapse investment flow aggregates

* fix(reports): avoid shared-account flow duplication
2026-06-06 17:11:33 +02:00
Guillem Arias Fauste
a461ff97bb fix(sync-toast): morph refresh, defer behind modals, DS conformance (#2105)
* fix(sync-toast): morph refresh, defer behind modals, DS conformance

Follow-up to #1964 (addresses #2071).

- Refresh via Turbo morph visit instead of window.location.reload, so
  scroll position and data-turbo-permanent elements (the AI chat panel)
  survive and there is no white flash.
- Defer the toast while a <dialog> is open and reveal it on close. A
  refresh mid-modal closes the dialog and discards its in-progress input,
  which is the exact data loss this toast exists to prevent. Handles
  stacked modals.
- Refresh CTA and close button now use DS::Button (secondary / icon). The
  close is always visible, inside the card, focusable, and has an
  aria-label; the old hover-only corner chip was unreachable on touch and
  not keyboard-focusable.
- Add role="status" / aria-live="polite" to the toast.
- Fix icon color: "inverse" is not a key in the icon helper color map, so
  it silently rendered no color class (dark icon on bg-info). Use "white",
  which maps to the functional text-inverse token.
- Tighten copy: "New data available" / "Refresh".
- Sync the broadcast comment with the actual replace/morph behavior.

* fix(sync-toast): detach deferred dialog listener on disconnect

A toast replaced by a newer broadcast_replace_to while a <dialog> was open kept
its 'close' listener attached, so the detached controller fired #reveal()/#arm()
when the dialog closed — a spurious auto-refresh from a stale toast (and repeated
syncs could queue several). Store the dialog + handler refs and remove the
listener in disconnect(). Flagged by codex + coderabbit on #2105.

* fix(sync-toast): re-check interaction and dialogs at refresh-fire time

The interaction check ran once at arm time but the refresh fired two
seconds later. The post-dialog reveal made that window matter: the user
closes a dialog sitting on a form, resumes typing, and the timer morphs
the page — wiping non-turbo-permanent input, the exact data-loss class
this toast exists to prevent. A dialog opened during the window had the
mirror problem (the refresh would close it).

Bail inside the callback instead, leaving the toast visible for a
manual refresh, matching the mid-form behavior. Also documents the
dialog-removed-without-close edge on the deferred listener.
2026-06-06 17:02:26 +02:00
Blaž Dular
94422955f8 feat(merchants): add raw data import (csv) for merchants (#1992)
* feat(merchants): add csv import endpoint for merchants

* docs: update endpoint docs

* fix(merchant): recommended ai fixes
2026-06-06 16:33:32 +02:00
Shibu M
b923889103 removed other changes 2026-06-05 11:45:06 +00:00