Commit Graph

1879 Commits

Author SHA1 Message Date
minion1227
dd707ec91b fix(invitations): allow re-inviting after an unaccepted invite expires (#2543)
* fix(invitations): allow re-inviting after an unaccepted invite expires

An expired, never-accepted invitation still occupies the partial unique
index `index_invitations_on_email_and_family_id_pending` (predicate
`accepted_at IS NULL`), but the `pending` scope excludes it via the
`expires_at > now` clause. The duplicate-guard validation therefore
passes and the INSERT collides with the index, raising
ActiveRecord::RecordNotUnique — surfaced to the admin as a 500 with no
way to re-invite that address through the UI.

Remove the stale expired row inside the create transaction
(before_create) so the unique slot is freed and the re-invite succeeds.
It runs only after validations pass, so a still-pending (non-expired)
invitation can never be removed here. Add a controller-level rescue of
ActiveRecord::RecordNotUnique as a safety net against a concurrent
double-submit race.

Closes #2535

* refactor(invitations): scope RecordNotUnique rescue to the save

The method-level `rescue ActiveRecord::RecordNotUnique` wrapped the whole
create action, so any future write after @invitation.save (e.g.
accept_for) would silently be reported as a generic invite failure.
Extract save_invitation to scope the rescue to the save alone, and add a
regression test that drives the raced unique-index path directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 08:59:54 +02:00
Orange🍊
94d1e954e1 fix(reports): hide tax-advantaged transactions in breakdown (#2549) 2026-07-08 08:51:22 +02:00
sentry[bot]
7e1713b831 fix(pwa): harden service worker render to explicitly use js format (#2629)
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
2026-07-08 08:31:51 +02:00
cuppabot
7fb6df71f2 Russian language support added (#2005)
* Add russian localization

* Update localization files from upstream

* Add russian to supported languages

* Add CHANGELOG.ru.md

* Changes in CHANGELOG.ru.md

* Fix some errors after PR

* Remove duplicated 'merge_duplicate' key in the same mapping in locales/views/transactions/ru.yml

* Fixed a translation error into Russian about creating a new account by invitation.

* Clarification of financial terms in Russian translation

* Add russian localization

* Update localization files from upstream

* Add russian to supported languages

* Add CHANGELOG.ru.md

* Changes in CHANGELOG.ru.md

* Fix some errors after PR

* Remove duplicated 'merge_duplicate' key in the same mapping in locales/views/transactions/ru.yml

* Fixed a translation error into Russian about creating a new account by invitation.

* Clarification of financial terms in Russian translation

* Improve Russian i18n translations

* Improve Russian i18n translations

* Improve Russian i18n translations

* Improve Russian i18n translations

* Improve Russian i18n translations

* Improve tests for Russian i18n translations

* Improve Russian i18n translations

* Improve Russian i18n translations

* Minimalistic SECURITY.md

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>

* ci(preview): render Cloudflare config from trusted template (#2207)

* Fix SSO provider settings updates (#2210)

* fix(goals): UI polish — submit validation, container-responsive cards, picker & filter fixes (#2160)

* fix(ds): disabled buttons use not-allowed cursor

Tailwind v4 preflight sets cursor:pointer on every <button>, including
disabled ones. Add disabled:cursor-not-allowed to the DS button base so
disabled buttons read as non-interactive.

* fix(goals): disable new-goal submit until required fields valid

The submit button was always enabled, and the funding-accounts checkbox
group has no native 'required', so a goal with no linked account could be
submitted and only fail server-side (422). Disable the submit until name,
a positive target amount, and >=1 account are present, and add a
submit-time guard that surfaces inline errors and focuses the first
offending field as a backstop.

* fix(goals): keep color/icon picker from shifting the form

DS::Disclosure wraps its body in an mt-2 div that sits in normal flow, so
opening the absolutely-positioned picker popover nudged the form down ~8px.
Use a raw <details> for the picker so the popover overlays without
reflowing the content beneath it.

* fix(goals): container-responsive cards, meta-line status, scrollable tabs

- Card grids + KPI strip switch from viewport breakpoints to container
  queries (@container), so columns track the actual content width when the
  account/AI sidebars are open instead of crushing three columns into a
  narrow center.
- Move the status pill from the title row to the meta line; the title now
  gets the full header width (no more 'Hou...' truncation at 3-up). Drop
  the secondary line when the pill already states it (completed / open).
- Card internals harden for narrow widths (ring shrink-0, footer wrap,
  shorter 'N days left' secondary).
- Filter tabs scroll horizontally instead of wrapping 'On track' mid-label
  or forcing the row wider than the viewport.

* fix(goals): only require a funding account on the create form

GoalsController#edit renders account checkboxes from visible accounts only,
so a goal backed solely by a now-hidden (disabled / pending_deletion)
account renders none checked. The submit-validation then wedged the edit
form — a name/notes change couldn't be saved even though #update preserves
existing links when account_ids is omitted. Gate the account requirement on
a require-account Stimulus value that is true only for the create form.

* fix(goals): give the color/icon picker trigger an accessible name

The hand-rolled <summary> is icon-only (pen), so screen readers announced an
unlabeled control. Add a localized aria-label.

* fix(goals): render form validation errors inline on server re-render

The client-side controller already surfaces name / amount / account errors
inline and disables submit, but a server 422 (JS disabled, or a race that
lets an invalid submit through) fell back to the top run-on banner
("X must be filled and Y and Z."). Make the existing inline error hints
server-aware so a failed submit shows the same per-field red text the
client path does, and drop the base-error banner — base only ever carries
the account requirement, which now renders beside the funding-accounts
list.

* fix(goals): blur projection chart under privacy mode

The projection chart's SVG axis labels and annotations (target, "$X short",
$200K/$400K ticks) rendered in cleartext while privacy mode blurred every
other number on the page. Tag the chart container `privacy-sensitive`, the
same wrapper-level treatment the net-worth and cashflow-sankey charts
already use, so it blurs with the rest.

* fix(goals): stop target-date input stretching when amount error shows

The target-amount / target-date row is a two-column grid. The amount
column carries its inline error <p> underneath, so when that error
appears the column grows and the default `items-stretch` makes the
sibling date input stretch to match — the date box visibly grew taller
than the amount box. Anchor the row with `items-start` so each input
keeps its natural height and the error just extends below its own column.

* fix(goals): make invalid submit announce errors instead of dead-ending

Review follow-ups:
- Swap the submit gate from the disabled attribute to aria-disabled: a
  truly disabled default submit also blocks Enter-key implicit
  submission, so an invalid form was a dead button with every inline
  error still hidden. The button now stays clickable and lets
  validateOnSubmit surface the errors; DS buttonish styles the
  aria-disabled state (cursor-not-allowed + opacity-50). Side effect:
  loading_button_controller submits also dim while busy, which reads as
  an upgrade.
- Focus the first funding-account checkbox when accounts are the only
  missing field (focus previously went nowhere).
- Drop the now-orphaned goals.goal_card.days_left_by locale key.

* fix(ds): canonical transaction-row — stop category pills truncating (#2147)

* fix(ds): canonical transaction-row — stop category pills truncating (#2137)

The desktop transaction grid gave the name column col-span-8 (67%, usually
half-empty) and the category column only col-span-2 (17%), so the category
pill's name area was clamped to ~44px and ellipsized nearly every value
("Misc...", "Shop...", "Rest...") despite the empty space the audit flagged.

- Rebalance the lg:grid-cols-12 row: name col-span-8 -> 7, category 2 -> 3
  (amount unchanged; still 12). Category gains 50% width from the over-wide
  name column.
- categories/_badge: the pill was `flex w-full` (stretched to fill the column);
  make it `inline-flex max-w-full` so it hugs its content and short names render
  fully, capping + truncating only when genuinely too long.

Verified on /transactions: visible-row category truncation dropped 7/7 -> 2/7
even in the compressed (AI-panel-open) view; Payment / Shopping / Restaurants
now render in full. Fixes both the interactive categories/menu and the static
transfer categories/badge (menu reuses the badge partial).

* feat(ds): lift categories/_badge onto DS::Pill

Addresses the Drift Patrol finding (and jjmata's request) properly
instead of patching the bespoke span: the badge becomes a DS::Pill in
badge mode. The pill primitive grows the three capabilities the badge
needed and the bot's one-liner glossed over:

- truncate: pills that may shrink inside min-w-0 columns drop their
  shrink-0/whitespace-nowrap and ellipsize the label instead of
  overflowing (the transaction-row category cell this PR fixes).
- label_testid: stamps data-testid on the label span; five test files
  target [data-testid='category-name'].
- icon_size: passthrough to the icon helper (badge keeps its
  established sm glyph; default stays xs).

custom_color was already the sanctioned escape hatch for user-chosen
hues. Owned visual deltas from pill standardization: px-1.5/py-1 ->
px-2/py-0.5, gap-1 -> gap-1.5, border mix 10% -> 20%; bg mix, hex text,
radius, text scale, icon size, truncation and testid are parity.

Label wrapper only renders when truncate/label_testid ask for it, so
existing pill DOM (and the find("span", text:) assertions on it) is
unchanged. Four new pill tests + a Lookbook case cover the recipe.

* 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

* chore(ci): finish Node 24 GitHub Actions migration (#2221)

* chore(ci): finish Node 24 GitHub Actions migration

* chore(ci): update preview security check for github-script v8

* fix(mobile): redact sensitive diagnostic logs (#2199)

* fix(mobile): redact sensitive diagnostic logs

* fix(mobile): tighten diagnostic log redaction

* fix(mobile): harden sanitized diagnostics

* fix(mobile): clarify offline fallback diagnostics

* fix(mobile): refine diagnostic log redaction

* fix(mobile): tighten diagnostic log redaction

* fix(mobile): harden auth diagnostic failures

* feat(ds): DS::SegmentedControl — fix invisible dark selected pill (#2145)

* feat(ds): DS::SegmentedControl; fix invisible dark selected pill (#2137)

Ships DS::SegmentedControl — a single-select pill group (filters, mode
switches) — plus a Lookbook preview, tests, and an exemplar migration of the
budget filter tabs.

The audit flagged the dark selected pill as invisible: the bespoke controls
paired a container-inset track (gray-800) with a gray-700 active pill, barely a
step apart. The primitive uses the DS tab-token values (tab-bg-group -> a
near-black alpha-black-700 track in dark), against which the gray-700 active
pill clearly reads. Values are inlined with @variant theme-dark because
@apply-ing the custom tab utilities drops their dark override.

- app/components/DS/segmented_control.rb: slot-based with_segment(label,
  active:, href:, **opts); link or button; full_width: for equal footprint;
  selected style isolated in .segmented-control__segment--active so a controller
  can toggle it as one class.
- .segmented-control recipe in components.css.
- Migrate budgets/_budget_tabs; budget_filter_controller now toggles the single
  --active class instead of five raw utility classes.

Verified in-browser: dark active pill reads (gray-700 on near-black track);
filter toggle still works. Tests + rubocop clean.

Deferred (follow-up): auth sign-in/up switch, transaction-type tabs, and other
bespoke segmented controls — same primitive, one migration each.

* fix(a11y): expose segmented-control selection + derive active from filter param

- DS::SegmentedControl: set aria-current (links) / aria-pressed (buttons) from
  the segment's active state so screen readers announce the selection.
- budget_filter_controller: mirror aria-pressed when it toggles the active class.
- _budget_tabs: compute each segment's initial active: from params[:filter] so
  a ?filter=over_budget request server-renders the correct pill (no flash before
  Stimulus runs). Addresses CodeRabbit reviews on #2145.

* fix(ds): close unterminated segmented-control CSS rule

The --active rule swallowed the table-scroll block's comment opener and
never closed, so the Tailwind build died with 'Missing closing } at
@layer components' and both test jobs failed at boot. Restore the
closing brace and the /* opener.

Also add the budgets.show.filter.aria_label locale key the budget tabs
view referenced only through its inline default.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>

* fix(ds): one height rail for icon and text buttons (#2202)

* fix(ds): put icon buttons on the text-button height rail

Icon-only DS::Button containers were 32/44/48px squares while text
buttons of the same nominal size render ~28/36/48px tall, so every
mixed header row (icon menu trigger next to text buttons — the
transactions index, account pages, the app header) sat misaligned.

- buttonish SIZES: icon containers now share the text rail
  (sm w-7, md w-9, lg w-12 unchanged).
- DS::Popover's hand-rolled w-11 trigger joins the rail at w-9.
- The layout's hand-rolled privacy toggle (mobile + desktop) matches
  the md icon-button chrome: w-9, rounded-lg, container-inset hover —
  it sat at w-8 with a different hover next to a DS icon button.
- DS::Select's panel adopts shadow-border-lg, the elevation Menu and
  Popover already use, replacing the weaker shadow-lg+border-xs combo.

Measured on the transactions header after the change: 36/38/36px.

* fix(ds): keep the 44px touch target on coarse pointers

The height rail trades icon-button size for row alignment, which is a
pointer-precision tradeoff: WCAG 2.5.5's 44x44 minimum is about
fingers, not mice. sm/md icon containers (and the two off-rail
consumers: the popover trigger and the layout privacy toggles) gain
pointer-coarse:w-11/h-11, so touch devices keep the full target while
fine-pointer layouts get the aligned 36px row.

Measured via Playwright: desktop 36x36, iPhone emulation 44x44 on both
the menu trigger and the privacy toggle.

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

* perf(transactions): preload new form options (#2189)

* perf(transactions): preload new form options

* refactor(transactions): reuse new form account scope

* perf(reports): collapse investment flow aggregates (#2190)

* perf(reports): collapse investment flow aggregates

* fix(reports): avoid shared-account flow duplication

* 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>

* Improve Russian i18n translations

* Improve Russian i18n translations

* Improve Russian i18n translations

* Improve Russian i18n translations

* No need for language-specific CHANGELOG.md file

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: tea <john.fx@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: ghost <49853598+JSONbored@users.noreply.github.com>
Co-authored-by: Guillem Arias Fauste <accounts@gariasf.com>
Co-authored-by: Blaž Dular <22869613+xBlaz3kx@users.noreply.github.com>
Co-authored-by: Sure Admin (bot) <sure-admin@splashblot.com>
Co-authored-by: Jonathan Chang <55106972+jonathanchang31@users.noreply.github.com>
Co-authored-by: Tobias Rahloff <rahloff@gmail.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-07-08 08:10:18 +02:00
Stephen Jolly
ba0e169f6b Don't persist zero balances when a provider balance fetch fails (#2617)
* fix(sync): don't persist zero balances when a provider balance fetch fails

A nil current_balance means the sync's balance fetch did not succeed
(the snapshot upsert clears it; only a successful fetch repopulates it).
The Lunchflow and Enable Banking processors coerced nil to 0 and write
it (plus a currency fallback) onto the linked account, so any transient
provider failure persists wrong data with no user-visible signal.
Instead, treat nil as no-data: skip the account update. Also stop the
Lunchflow snapshot upsert resetting an established account's currency to
USD when the accounts endpoint omits currency.

* fix(lunchflow): normalize the preserved currency in snapshot upsert

Addresses the review comment on #2617: the preserved fallback reused the
record's raw in-memory currency, so a blank value would fail the presence
validation and break import, and an invalid code would persist instead of
falling back to USD. Run it through parse_currency like the payload value.
Regression test added.

* fix(lunchflow,eb): review round 2 — failure visibility and currency parity

Addresses the three review findings on #2617:
1. Capture the Lunch Flow balance-fetch failure via DebugLogEntry (the sync
   otherwise reports success with no mention of the skip).
   This is Lunch Flow only: Enable Banking's importer already surfaces the
   failure through transactions_failed/@sync_error.
2. Apply the currency-preservation fix to Enable Banking's snapshot upsert
   (same shape as the Lunch Flow fix: parity/safety). Regression test added.
3. Label the Enable Banking processor currency assertion as a parity check —
   it also passes on main, since the reset defect was Lunch Flow-specific.
2026-07-08 08:08:10 +02:00
Orange🍊
690f1c648b fix(sso): request OIDC group claims (#2503) 2026-07-04 08:24:31 +02:00
Maxence C.
9d8b953c8e fix: only mark overlapping statement periods as duplicate (#2569)
* fix: only mark overlapping statement periods as duplicate

* fix: treat non-overlapping statement periods as covered
2026-07-04 03:27:59 +02:00
Anthony
e38d552c15 Fix two crashes in rake demo_data:default (#2586)
* Fix two crashes in `rake demo_data:default`

- Demo::Generator#generate_credit_card_cycles!: the balance-adjust step created a $0 "Balance Adjust" transfer whenever a card's balance was already under its target (negative diff), which fails Transfer's opposite-amounts validation (a transfer can't have a zero amount). Only create the adjustment transfer when the diff is actually positive. - Demo::DataCleaner#destroy_everything!: ApiKey has a before_destroy guard (prevent_demo_monitoring_key_destroy!) that throws :abort to stop the demo monitoring key being revoked from the UI. That abort silently no-ops the whole Family.destroy_all cascade below it (accounts/entries/ trades all survive undestroyed), which only then surfaces downstream as a NOT NULL violation in Security.destroy_all. Delete the demo monitoring key directly (bypassing callbacks) before destroying families; safe since this class is dev/test only. Verified end-to-end: db:drop/create/schema:load, then `rake demo_data:default` followed by `SKIP_CLEAR=0 rake demo_data:default` (exercises both the generation and the clear-and-regenerate paths) complete without error.

* Reconcile below-target card balances with a charge instead of skipping

The previous fix avoided the $0 transfer crash by skipping the balance
adjustment entirely when a card's ending balance landed more than $250
under its target, but that let demo_data:default finish with Amex/Sapphire
balances far from their documented targets. A transfer can't carry a
negative payment amount, so when the diff is negative, add a direct
"Balance Reconciliation" charge on the card instead of a transfer.

Verified across 10 seeds: Sapphire now lands exactly on its $4,200 target
instead of drifting below it, with no validation errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 03:22:53 +02:00
glorydavid03023
d606013e76 fix(trend): keep percent sign consistent with direction for negative/zero base (#2580)
Trend#percent divided the delta by the signed previous value, so a negative
base inverted the sign of the result and contradicted #direction. This showed
an up arrow next to a negative percentage (and vice versa) for anything that
can go below zero — most visibly net worth in reports and balance sparklines.

Divide by the magnitude of the base instead, and carry the sign of current
when the base is zero so an all-negative move reports -Infinity rather than
+Infinity.

Fixes #2579
2026-07-04 03:13:39 +02:00
Orange🍊
59c47c4c66 fix(balance): surface reverse opening boundary adjustments (#2502) 2026-07-04 02:47:46 +02:00
DataEnginr
5935e56a8b Merge branch 'main' into Transfer-charges 2026-07-01 05:50:51 +00:00
Neko
1f036fc526 feat: add preference to disable modal close on outside click (#2226)
* Add a user preference under Settings → Appearance that prevents modals
  from closing when clicking outside them. Useful to avoid accidentally
  losing unsaved form data.

  - Add `disable_modal_click_outside?` helper to User model (JSONB prefs)
  - DS::Dialog reads the user preference as default when not explicitly set
    (existing callers passing disable_click_outside: true/false are unaffected)
  - Wire up save in AppearancesController
  - Add toggle in the Modals section of the Appearance settings page
  - Add i18n strings

* fix(i18n): move modal keys to appearances.show namespace in 7 locales

   The modal translation keys (modals_title, modals_subtitle,
   disable_modal_click_outside_title, disable_modal_click_outside_description)
   were under settings.preferences.show but the view calls t(".modals_title")
   from settings/appearances/show.html.erb. Moved them to
   settings.appearances.show in de, es, nb, nl, ro, tr, and zh-TW.

* refactor(ds): decouple Dialog from Current.user via defaults_provider

---------

Co-authored-by: neko <neko@nixos>
2026-07-01 01:50:20 +02:00
DataEnginr
9b6a966ddb fix: derive amount_abs from inflow entry to avoid $0.00 regression on auto-matched transfers
- amount_abs now uses inflow_transaction.entry.amount_money.abs
  instead of the amount column, which is never set by the auto-matcher
- Rename transfer_has_opposite_amounts_or_fees to
  transfer_has_opposite_amounts (validation no longer checks fees)
- Remove unused calculate_rate_tab / convert_tab i18n keys
- Add fee-field assertions to API controller test and rswag spec
2026-06-30 18:00:13 +00:00
Shibu M
8ccea36517 Merge branch 'we-promise:main' into Transfer-charges 2026-06-30 11:55:59 +05:30
threatsurfer
ab32388326 feat(up): flag internal transfers and round-ups as funds_movement (#2460)
* feat(up): flag internal transfers and round-ups as funds_movement

Up populates relationships.transferAccount on transactions that move money
between the user's own accounts (including round-ups swept into a Saver), but
flatten_transaction dropped it, so these imported as ordinary income/expense
and distorted budgets and cashflow.

- Provider::Up#flatten_transaction: lift transfer_account_id from
  relationships.transferAccount.data.id.
- UpEntry::Processor: import transfers as funds_movement and persist
  transfer_account_id in extra["up"].
- Account::ProviderImportAdapter#import_transaction: optional kind: param; an
  explicit provider kind takes precedence over account-type auto-detection and
  is applied after the sync-protection check, so user re-categorisations
  survive re-sync.

Complementary to Family#auto_match_transfers!: two-sided transfers between
linked accounts are still paired into a Transfer (the matcher does not filter
on kind); one-sided movements and round-ups, which the matcher cannot pair,
are the cases this fixes.

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

* fix(up): account-type kind wins over provider transfer hint

Codex review caught that Up HOME_LOAN accounts map to a Loan account, so a
repayment carrying transferAccount would be reclassified from loan_payment to
funds_movement (budget-excluded). Make the provider kind: a fallback:
activity-label and account-type classification now take precedence, so
loan_payment and cc_payment survive. Adds a regression test (loan repayment
stays loan_payment), a depository-applies test, and a note that the up_test
stub ignores query: intentionally.

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

---------

Co-authored-by: Gavin Matthews <matthews.gav@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 08:19:25 +02:00
Anthony
a14e1a1321 refactor(imports): header-less settings_section (#2395)
Follow-up to #2279, which flattened the family_exports list onto the
canonical surface recipe but left imports on the old pattern.

- settings_section header-less (drop the duplicate title; the page h1
  + the inset count header already label it)
- drop the redundant inner space-y-4 wrapper
- table sits directly in the inset (remove the inner bg-container card)

Now: one title, one card, one inset — matching #2279.

Fixes #2393
2026-06-30 08:16:46 +02:00
sentry[bot]
24ba7ab6a0 fix(pwa): harden manifest render to explicitly use json format (#2508)
* fix(pwa): harden manifest render to explicitly use json format

* Fix RuboCop spacing in PWA manifest render

---------

Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
2026-06-30 07:51:51 +02:00
ghost
6768d03b3c feat(mercury): pending transactions, kind/counterpartyId metadata, and test coverage (#2452)
* feat(mercury): pending transactions, kind/counterpartyId metadata, and test coverage

- Transaction::PENDING_PROVIDERS: add "mercury" so the existing pending
  reconciliation pipeline (pending→posted amount matching in
  ProviderImportAdapter) activates for Mercury entries

- MercuryEntry::Processor: pass extra: to import_transaction with
    extra["mercury"]["pending"]        = true/false (status == "pending")
    extra["mercury"]["kind"]           = ACH / Wire / Card / etc.
    extra["mercury"]["counterparty_id"] = Mercury counterparty UUID
  Pending transactions are now imported with the flag set rather than
  being silently ignored; failed transactions continue to be skipped

- Tests (new files, 30 cases):
  - test/models/mercury_entry/processor_test.rb  — sign convention,
    date fallback, name priority, notes concat, pending flag, kind,
    counterpartyId, failed skip, idempotency, merchant creation,
    no-linked-account guard
  - test/models/mercury_item/importer_test.rb    — account discovery,
    no-duplicate unlinked records, balance update on linked accounts,
    transaction dedup (append-only new ids), sync window (90-day first
    sync, last_synced_at-7d subsequent), 401 marks requires_update
  - test/models/mercury_account/processor_test.rb — balance update,
    CreditCard sign negation, cash_balance parity, no-linked-account
    no-op, transaction processing delegation

* fix(mercury): address review findings — pending SQL, dedup upsert, N+1, nil assertion

- provider_import_adapter: add mercury to all three find_pending_transaction*
  SQL predicates so Mercury pending entries are found and claimed when the
  posted version arrives (exact, fuzzy, and low-confidence paths)

- mercury_item/importer: replace append-only dedup with an upsert-by-id
  that replaces the stored raw payload when status changes from pending to
  non-pending; prevents pending flag from persisting indefinitely when Mercury
  reuses the same transaction ID for the posted version

- kraken_account/ledger_processor: preload all existing kraken ledger
  external_ids into a Set before the loop; replaces per-entry exists? query
  (N+1) with an in-memory Set#include? lookup

- test/models/mercury_account/processor_test: capture return value from
  process and add assert_nil to enforce the nil contract stated in the test name

* fix(mercury): route transaction-count diagnostics through DebugLogEntry
2026-06-30 07:37:14 +02:00
ghost
d329a4f69d feat(kraken): import deposits, withdrawals, staking & fees via Ledgers API (#2451)
* feat(kraken): fetch and import Ledgers API for deposits, withdrawals, staking, fees

Closes #2450

Kraken TradesHistory only returns spot buy/sell trades. The Ledgers API
(/0/private/Ledgers) covers deposits, withdrawals, staking rewards, Earn
income, and standalone fees — everything that was missing from syncs.

Changes:
- Provider::Kraken#get_ledgers — new method forwarding start/type/offset params
- KrakenItem::Importer#fetch_ledgers — paginated fetch (up to 200 pages) with
  graceful fallback if the API key lacks Query Ledger Entries permission
- Importer#upsert_kraken_account — stores "ledgers" alongside "trades" in
  raw_transactions_payload
- KrakenAccount::LedgerProcessor — new class; maps each supported ledger type
  (deposit, withdrawal, staking, earn, fee) to a Transaction entry with the
  correct investment_activity_label, kind, and sign convention; skips trade/
  transfer/margin types to avoid double-counting with TradesHistory
- KrakenAccount::Processor#process — calls LedgerProcessor after process_trades
- Multi-currency: fiat amounts converted via ExchangeRate (non-USD fiat bridged
  through USD); crypto amounts use the spot price cached in raw_payload["assets"]
  with a price_missing flag when no price is available
- Dedup guard: external_id "kraken_ledger_<id>" + source "kraken" prevents
  re-importing on repeated syncs

* fix(kraken): correct sign convention, fee inclusion, and earn subtype filtering

- Sign convention: deposits/staking/earn → negative (inflow), withdrawals/fees
  → positive (outflow), matching Sure's global convention (inflow is negative)
- Fee inclusion: use (amount - fee).abs as abs_impact so withdrawal fees are
  counted in the total outflow rather than discarded
- Earn subtypes: skip allocation/deallocation ledger entries (internal fund
  movements); only import rewardallocation/bonusallocation as Interest income
- DebugLogEntry: replace Rails.logger.warn/error with DebugLogEntry.capture
  throughout LedgerProcessor and the Ledgers permission fallback in Importer,
  so support-relevant incidents surface in /settings/debug
- Importer test: stub get_ledgers in setup so existing tests do not error
  on the new fetch_ledgers call

* fix(kraken): route duplicate ledger-id warning through DebugLogEntry

* perf(kraken): batch ledger idempotency check; strengthen tests

Address review feedback (jjmata):

- N+1: LedgerProcessor#process_ledger_entry ran `account.entries.exists?(...)`
  per ledger entry (up to ~10k per sync). Load the existing Kraken external IDs
  once into a Set and test membership in memory (newly created IDs are added so
  the same run stays idempotent) — same pattern as #2452.
- Tests: the idempotency test now asserts the first pass actually creates the
  entry (assert_difference) before asserting the second is a no-op; add a guard
  asserting the second (all-skipped) pass issues a single bulk external_id pluck,
  not one query per entry.

No behavior change to imported entries.

* perf(kraken): scope ledger idempotency pluck to kraken_ledger_ prefix

Only load existing ledger external IDs (not trade entries) into the idempotency
Set, matching the reviewed approach. No behavior change.
2026-06-30 07:35:14 +02:00
Guillem Arias Fauste
c5ca0431c9 feat(goals): investment-backed goals (Phase 2) (#2491)
* feat(goals): earmark a portion of an account toward a goal

Goals currently count each linked account's whole balance, so an account
shared across goals double-counts and one account can't fund several goals
in distinct slices. Add a per-account earmark — the "GoalBacking" the v1
model already foreshadowed (goal.rb).

- goal_accounts.allocated_amount (nullable). NULL = "dedicate the whole
  balance" (the v1 default: no backfill, existing goals unchanged); a set
  amount reserves a fixed slice.
- Goal#current_balance is now the single chokepoint computing each account's
  backing under a family-wide shared pool: fixed earmarks take their slice,
  an unallocated link takes the remainder, and when fixed earmarks exceed the
  balance every slice is scaled down pro-rata so the goals' shares can never
  sum past the account balance (no double-counting).
- Account#free_to_earmark / #goal_earmarked_total (mirror Budget's
  available_to_allocate) back a soft, non-blocking over-allocation hint.
- GoalsController threads a goal[allocations] hash through create/update.

Phase 1 of the goals earmarking work; investment-backed goals follow.

* feat(goals): earmark UI on the goal form + backing-aware funding breakdown

- Goal form: a per-account "earmark amount" input (blank = whole balance)
  next to each funding-account checkbox, prefilled from the saved
  allocation on edit.
- Goal#account_backing exposes a single linked account's share so the
  funding-accounts breakdown shows each account's earmarked contribution
  and percent instead of its whole balance — keeping the show page
  consistent with the (now allocation-aware) progress ring.
- English strings for the earmark controls and the "earmarked of balance"
  breakdown line.

* fix(goals): address review on the earmark shared-pool math

- Overdrawn (<= 0 balance) accounts now back nothing on both the fixed and
  whole-balance paths. The fixed path previously produced negative backing and
  let a goal claim money the account doesn't hold.
- An archived goal reads its OWN earmark from its own goal_accounts instead of
  the shared pool (which excludes archived goals), so it no longer mis-reports
  the whole account balance for itself.
- goals#index injects one family-wide earmark pool into every card
  (Goal.pooled_allocations_for) instead of querying once per goal (N+1), and
  preloads goal_accounts.
- The projection chart scales its whole-account historical series by the
  backing ratio so the saved line meets current_balance at "today" rather than
  dropping off a cliff for earmarked goals.
- Honest comments: free_to_earmark no longer claims a form warning that doesn't
  exist yet; pace documents its deliberate whole-account basis.

* fix(goals): widen the earmark input so the 'Whole balance' placeholder isn't clipped

* fix(goals): address review on #2490

- autosave: true on goal_accounts so earmark edits to already-linked accounts
  persist through goal.save! (Rails only auto-saves newly built children, so
  changing/clearing an existing earmark was silently dropped). + test.
- Reset the balance/progress memos on AASM transitions, not just the status
  memos, so a same-instance render after complete!/archive! isn't stale. + test.
- backing_ratio is 0 (not 1) when the linked-account total is non-positive, so
  the projection saved series ends at 0 to match the forced-zero current_balance.
- Localize the funding-row subtype label via goals.form.subtypes.*.
- Add the earmark strings to zh-CN (the maintained second locale; goals has no
  ca locale, so Catalan keeps falling back to en like the rest of goals).

* feat(goals): investment-backed goals (Phase 2)

Goals can now be funded by investment accounts, not just depository.

- Relax linked_accounts_must_be_depository -> _must_be_fundable
  (depository || investment); the funding picker + counts include investment
  accounts.
- Add goals.progress_basis ('balance' | 'contributions', default 'balance').
  Investment-backed goals default to 'contributions' so a market swing doesn't
  move the goal: current_balance = value - cumulative market gain
  (Sum of balances.net_market_flows); depository accounts have zero
  net_market_flows, so they're unchanged. Goal#market_value_money shows what
  it's worth today next to the contributed figure on the show page.
- Pledge false-match guard: investment accounts never use manual_save /
  valuation-delta matching (a market move isn't a deposit) - they resolve on
  transfer (cash-inflow) entries only. Guarded in both
  Account#default_pledge_kind and GoalPledge#matches?.
- Add a `reopen` AASM event (completed -> active) + route/action/menu item so a
  manually-completed goal whose value later dips can be reopened.

Stacked on the earmarking branch (#2490). Full suite green; +6 goal tests.

* fix(goals): address Phase 2 review — allocation-aware contributions, N+1, basis-on-update

- Contributions basis now goes through the same earmark/shared-pool logic as
  the balance basis: backing_balance_for -> backing_share_for(account, base),
  where base is the live balance (balance basis) or net contributions
  (contributions basis). Earmarks are respected and shared accounts no longer
  double-count on contributions goals; market_value_money stays consistent.
- Batch the per-account net_market_flows sum (Goal.market_flows_for) and inject
  it on index like pooled_allocations, killing the N+1 for contributions goals.
- Default the basis on update too (not just create), so adding an investment
  account to an existing depository goal flips it to contributions instead of
  silently tracking market value.
- Fix the stale reconciliation_manager comment (renamed validation) and the
  orphaned zh-CN must_be_depository key.

* fix(goals): address review on #2491

- before_save (not before_validation) for the progress_basis default, so a goal
  can be inspected via valid? without its basis flipping as a side effect (jjmata).
- Pledge copy keys off default_pledge_kind, not manual?, so a manual investment
  account — which pledges via transfer — shows the transfer prompt instead of the
  "update your manual balance" flow (codex). pledge_action_label_key and the
  pledge modal's per-account helper flag both use it.
- Add the Phase 2 strings (reopen success/invalid_transition, show.reopen,
  ring.market_value) to zh-CN.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-06-30 07:26:23 +02:00
Shibu M
35f395b39f Merge branch 'we-promise:main' into Transfer-charges 2026-06-30 10:44:23 +05:30
Guillem Arias Fauste
1b403d64e5 feat(goals): earmark a portion of an account toward a goal (Phase 1) (#2490)
* feat(goals): earmark a portion of an account toward a goal

Goals currently count each linked account's whole balance, so an account
shared across goals double-counts and one account can't fund several goals
in distinct slices. Add a per-account earmark — the "GoalBacking" the v1
model already foreshadowed (goal.rb).

- goal_accounts.allocated_amount (nullable). NULL = "dedicate the whole
  balance" (the v1 default: no backfill, existing goals unchanged); a set
  amount reserves a fixed slice.
- Goal#current_balance is now the single chokepoint computing each account's
  backing under a family-wide shared pool: fixed earmarks take their slice,
  an unallocated link takes the remainder, and when fixed earmarks exceed the
  balance every slice is scaled down pro-rata so the goals' shares can never
  sum past the account balance (no double-counting).
- Account#free_to_earmark / #goal_earmarked_total (mirror Budget's
  available_to_allocate) back a soft, non-blocking over-allocation hint.
- GoalsController threads a goal[allocations] hash through create/update.

Phase 1 of the goals earmarking work; investment-backed goals follow.

* feat(goals): earmark UI on the goal form + backing-aware funding breakdown

- Goal form: a per-account "earmark amount" input (blank = whole balance)
  next to each funding-account checkbox, prefilled from the saved
  allocation on edit.
- Goal#account_backing exposes a single linked account's share so the
  funding-accounts breakdown shows each account's earmarked contribution
  and percent instead of its whole balance — keeping the show page
  consistent with the (now allocation-aware) progress ring.
- English strings for the earmark controls and the "earmarked of balance"
  breakdown line.

* fix(goals): address review on the earmark shared-pool math

- Overdrawn (<= 0 balance) accounts now back nothing on both the fixed and
  whole-balance paths. The fixed path previously produced negative backing and
  let a goal claim money the account doesn't hold.
- An archived goal reads its OWN earmark from its own goal_accounts instead of
  the shared pool (which excludes archived goals), so it no longer mis-reports
  the whole account balance for itself.
- goals#index injects one family-wide earmark pool into every card
  (Goal.pooled_allocations_for) instead of querying once per goal (N+1), and
  preloads goal_accounts.
- The projection chart scales its whole-account historical series by the
  backing ratio so the saved line meets current_balance at "today" rather than
  dropping off a cliff for earmarked goals.
- Honest comments: free_to_earmark no longer claims a form warning that doesn't
  exist yet; pace documents its deliberate whole-account basis.

* fix(goals): widen the earmark input so the 'Whole balance' placeholder isn't clipped

* fix(goals): address review on #2490

- autosave: true on goal_accounts so earmark edits to already-linked accounts
  persist through goal.save! (Rails only auto-saves newly built children, so
  changing/clearing an existing earmark was silently dropped). + test.
- Reset the balance/progress memos on AASM transitions, not just the status
  memos, so a same-instance render after complete!/archive! isn't stale. + test.
- backing_ratio is 0 (not 1) when the linked-account total is non-positive, so
  the projection saved series ends at 0 to match the forced-zero current_balance.
- Localize the funding-row subtype label via goals.form.subtypes.*.
- Add the earmark strings to zh-CN (the maintained second locale; goals has no
  ca locale, so Catalan keeps falling back to en like the rest of goals).

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-06-30 06:55:29 +02:00
Shibu M
9481a41aa9 Merge branch 'we-promise:main' into Transfer-charges 2026-06-29 11:45:02 +05:30
Artem Danilov
6910518e81 fix(settings): use design-system checkbox for securities providers (#2430)
The securities-provider checkboxes used raw Tailwind utilities
(rounded border-primary text-primary focus:ring-primary) instead of the
design-system .checkbox component. In dark mode text-primary resolves to
white, so a checked box rendered a white check on a white fill and the
checkmark was invisible.

Switch to the theme-aware .checkbox checkbox--light classes used by every
other checkbox in the app (settings/preferences, transaction filters,
etc.), which render a dark check on a light fill in dark mode.
2026-06-29 00:47:57 +02:00
sentry[bot]
9f93aae05f fix(sync): prevent NoMethodError when syncable is nil in after_commit (#2484)
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-06-29 00:47:09 +02:00
Artem Danilov
92456936fb fix(accounts): persist subtype when it is assigned before accountable_type (#2432)
#2356 build the accountable inside Account#subtype= so a top-level subtype
survives create. That fix assumes accountable_type is already set when
subtype= runs, but the real controller path violates it: strong-params
permit preserves filter order, and account_params lists :subtype before
:accountable_type. So on create subtype= runs while accountable_type (and
accountable_class) is still blank, the build is skipped, and the chosen
subtype is silently dropped — the account renders with the type's fallback
label (e.g. a Depository shows 'Cash' instead of 'Savings').

The existing regression test only covered the accountable_type-first order,
so it never caught this.

Make the writer order-independent: when subtype arrives before the type,
stash it and apply it from an accountable_type= override once the type is
known. Add regression tests for the permit order and for create_and_sync.
2026-06-29 00:40:34 +02:00
HairyHook
8bc20cb9a5 Fixes issue #2415 - Subcategories are not alphabetically ordered like Categories (#2429)
* Update categories_controller.rb

First change required to fix subcategories not sorted alphabetically.

Signed-off-by: HairyHook <63165721+HairyHook@users.noreply.github.com>

* Subcategory ordering fix 1 in category.rb

Signed-off-by: HairyHook <63165721+HairyHook@users.noreply.github.com>

* Update categories_controller.rb

Signed-off-by: HairyHook <63165721+HairyHook@users.noreply.github.com>

---------

Signed-off-by: HairyHook <63165721+HairyHook@users.noreply.github.com>
2026-06-29 00:37:59 +02:00
Juan José Mata
b3f70c8951 feat:Add SnapTrade OAuth device flow (#2523)
* Add SnapTrade OAuth connection flow

* Restore SnapTrade brokerage portal links

* Guard SnapTrade OAuth setup completion

* Move SnapTrade OAuth start to POST

* Use one SnapTrade item in provider panel

* Fix SnapTrade OAuth controller tests

* Fix SnapTrade OAuth drawer completion redirect

* Update SnapTrade limits message.

* Restrict SnapTrade OAuth scopes
2026-06-29 00:30:03 +02:00
sentry[bot]
174a2e19f9 fix(sync): scope after_commit to prevent nil family error on destroy (#1976)
* fix(sync): scope after_commit to prevent nil family error on destroy

* Linter noise

---------

Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
Co-authored-by: Juan José Mata <jjmata@jjmata.com>
2026-06-29 00:06:27 +02:00
Shibu M
3e32b7e3f3 Merge branch 'we-promise:main' into Transfer-charges 2026-06-29 03:00:25 +05:30
Orange🍊
30780a5961 refactor(api): scope controllers through current_resource_owner (#2414)
Follow-up to #2405. Replace remaining API controller reads of
Current.user, Current.family, and Current.session with
current_resource_owner. Add an architecture guard test to prevent
regression.

Scope is limited to the Current sweep only:
- Revert balance_sheet user-scoping (moves to account-auth PR B).
- Revert provider_connections DebugLogEntry logging (separate PR).
- Remove UsersController#destroy attempt to destroy unsaved API session.
2026-06-28 21:59:44 +02:00
DataEnginr
e75f2a0c78 Fix fee display consistency, derive fees from entries, clean schema churn
- Show principal-only transfer amounts on both sides with separate fee and total lines (fixes inconsistent gross/net convention)
- Derive displayed fee amounts from fee_transactions entries (single source of truth) instead of stored columns
- Remove stored source_fee_amount/destination_fee_amount columns from transfers table
- Add foreign key for transactions.transfer_id -> transfers.id (replaces invalid CHECK subquery)
- Move destination fee line inside destination side div for consistent layout
- Remove orphaned view_fee_transaction locale keys from 7 locale files
- Rebuild schema.rb from origin/main to eliminate unrelated column reordering churn
2026-06-28 19:47:53 +00:00
DataEnginr
75234dab19 Fix fee display, derive fees from entries, clean schema 2026-06-28 18:39:28 +00:00
DataEnginr
1b21c4dd7b Store fees as separate expense transactions with principal-only entries
Entries now hold principal only (no fee baked into amounts). Fee transactions created as standard kind with Fees category. Transfer#amount_abs returns principal from new amount column. Update handler recomputes entries and fee transactions on edit. Remove dead source_principal/destination_principal helpers. Schema regenerated cleanly with only transfer fee columns.
2026-06-28 18:39:28 +00:00
Shibu M
eccf050521 Merge branch 'we-promise:main' into Transfer-charges 2026-06-28 22:31:14 +05:30
DataEnginr
2c18987a2d Merge upstream/main into feature/exclude-from-reports 2026-06-28 16:46:43 +00:00
Shibu M
2f1fc2edee Merge branch 'we-promise:main' into Transfer-charges 2026-06-28 22:02:04 +05:30
Orange🍊
d637cd2f75 fix(imports): support QIF dd mmm yyyy date format (#2500)
* Fix QIF import for dd mmm yyyy dates (#2498)

American Express QIF exports use dd mmm yyyy D-fields (e.g. D26 Jan 2026).
Import previously failed with "Unable to detect date format" for two reasons:

1. QifParser.normalize_qif_date stripped all internal whitespace, turning
   26 Jan 2026 into the unparseable 26Jan2026. For month-name dates the
   space IS the separator, so collapse multiples to a single space instead
   of removing them. Numeric dates keep the existing strip-all behavior.

2. Family::DATE_FORMATS had no candidate mapping to dd mmm yyyy, so
   detect_date_format could not match it. Add "%d %b %Y" (DD MMM YYYY).

Extend the 2-digit-year expansion separator class to include space so
month-name dates with 2-digit years also normalize (26 Jan 26 -> 26 Jan 2026).

Covered by normalize/parse/detect and Amex-style row-generation regression
tests in qif_import_test.rb.

* test(qif): cover month-name 2-digit year normalization & parse

Addresses CodeRabbit nitpick on #2500: the production change extends
the 2-digit-year expansion regex separator class to include a space
so month-name dates like "26 Jan 26" normalize to "26 Jan 2026".
Add normalize_qif_date and parse_qif_date regressions for that branch.
2026-06-28 17:02:07 +02:00
Josh
d4b12d7f7a Fix SimpleFIN partial auth reconnect status (#2509)
* Fix SimpleFIN partial auth reconnect status

* Address SimpleFIN review feedback
2026-06-28 16:12:33 +02:00
DataEnginr
945a08b0c2 Merge remote-tracking branch 'upstream/main' into feature/exclude-from-reports
# Conflicts:
#	db/schema.rb
2026-06-27 17:44:14 +00:00
Shibu M
354ece074d Merge branch 'we-promise:main' into Transfer-charges 2026-06-27 23:09:05 +05:30
Juan José Mata
b416618558 Add SnapTrade OAuth device flow (#2494)
* Add SnapTrade OAuth device flow

* Add SnapTrade OAuth device flow

### Motivation
- Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint.
- Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling.

### Description
- Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers.
- Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly.
- Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity.
- Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses.
- Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI.
- Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence.

### Testing
- Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed.
- Ran `bin/rubocop` against modified files and no offenses were reported.

* Add SnapTrade OAuth device flow

### Motivation
- Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint.
- Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling.

### Description
- Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers.
- Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly.
- Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity.
- Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses.
- Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI.
- Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence.

### Testing
- Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed.
- Ran `bin/rubocop` against modified files and no offenses were reported.

* Add SnapTrade OAuth device flow

### Motivation
- Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint.
- Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling.

### Description
- Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers.
- Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly.
- Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity.
- Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses.
- Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI.
- Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence.

### Testing
- Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed.
- Ran `bin/rubocop` against modified files and no offenses were reported.

* Add SnapTrade OAuth device flow

### Motivation
- Integrate SnapTrade device-code OAuth so administrators can start a device authorization flow and poll for tokens using SnapTrade's well-known OAuth metadata endpoint.
- Persist and encrypt device-flow token material on `SnaptradeItem` to support long-lived API calls and future refresh handling.

### Description
- Add OAuth discovery and device-code support to the SnapTrade provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token` helper methods and shared `oauth_connection` / `parse_oauth_response` helpers.
- Persist OAuth device-flow tokens on `snaptrade_items` via a migration that adds `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at`, and update `db/schema.rb` accordingly.
- Extend `SnaptradeItem` to encrypt stored `oauth_access_token` and `oauth_refresh_token` when ActiveRecord encryption is configured and add `oauth_token_active?` to check token validity.
- Add high-level `start_oauth_device_flow` and `complete_oauth_device_flow!` helpers to the `SnaptradeItem::Provided` concern to start authorization and persist token responses.
- Expose admin-only member routes and JSON controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` on `SnaptradeItemsController` to drive the device flow from the UI.
- Add focused Minitest coverage: `test/models/provider/snaptrade_oauth_test.rb` for provider requests and error handling and `test/models/snaptrade_item_oauth_test.rb` for token persistence.

### Testing
- Ran the targeted test suite: `bin/rails test test/models/provider/snaptrade_oauth_test.rb test/models/snaptrade_item_oauth_test.rb test/controllers/snaptrade_items_controller_test.rb`, and all tests passed.
- Ran `bin/rubocop` against modified files and no offenses were reported.

* Add SnapTrade OAuth device flow (provider, model, controller, routes, migration, tests)

### Motivation

- Add support for SnapTrade OAuth 2.0 device authorization flow so users can authorize SnapTrade via device codes in addition to the existing portal flow.
- Persist OAuth token metadata on `SnaptradeItem` for subsequent polling and usage by the provider and UI.

### Description

- Implemented OAuth device flow in the provider with `oauth_authorization_server_metadata`, `start_device_authorization`, and `poll_device_token`, plus HTTP helper methods `oauth_connection`, `oauth_client_id`, and `parse_oauth_response` in `Provider::Snaptrade`.
- Added controller endpoints `start_oauth_device_flow` and `complete_oauth_device_flow` in `SnaptradeItemsController` with robust error handling and helpers `oauth_error_payload` and `parse_oauth_error_body` to surface OAuth error fields.
- Extended `SnaptradeItem` with encrypted columns for `oauth_access_token`, `oauth_refresh_token`, token metadata, `oauth_token_active?`, and model methods `start_oauth_device_flow` and `complete_oauth_device_flow!` to store token metadata.
- Added migration `AddOauthDeviceFlowToSnaptradeItems` and updated `db/schema.rb` to include the new columns, registered a new initializer `config/initializers/snaptrade.rb` to read `SNAPTRADE_OAUTH_CLIENT_ID`, and updated `.env*.example` files to document `SNAPTRADE_OAUTH_CLIENT_ID`.
- Exposed new routes `start_oauth_device_flow` and `complete_oauth_device_flow` for `snaptrade_items`.

### Testing

- Added unit tests for provider OAuth behavior in `test/models/provider/snaptrade_oauth_test.rb`, model token persistence in `test/models/snaptrade_item_oauth_test.rb`, and controller error propagation in `test/controllers/snaptrade_items_controller_test.rb`.
- Ran the test suite with `bin/rails test` and the full test run (including the new SnapTrade OAuth tests) passed.

* Add SnapTrade OAuth device flow (start/poll), store tokens, and tests

### Motivation
- Add support for SnapTrade OAuth Device Authorization flow so administrators can start device authorization and poll for tokens without exposing provider internals.
- Persist OAuth token metadata on `SnaptradeItem` so the app can reuse and surface token state for SnapTrade integrations.
- Improve error handling and sanitization for OAuth API errors returned by SnapTrade to avoid leaking upstream internals.

### Description
- Introduces new environment examples and initializer: adds `SNAPTRADE_OAUTH_CLIENT_ID` to `.env.local.example`/`.env.test.example` and configures `Rails.configuration.x.snaptrade.oauth_client_id` in `config/initializers/snaptrade.rb`.
- Extends `Provider::Snaptrade` with OAuth device flow support, adding discovery URL, device grant constant, Faraday `oauth_connection`, and methods `oauth_authorization_server_metadata`, `start_device_authorization`, `poll_device_token`, `oauth_client_id`, and `parse_oauth_response`, plus improved retry and API error wrapping.
- Adds DB migration and schema changes to store OAuth fields on `snaptrade_items` (`oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, `oauth_token_expires_at`) and marks those attributes encrypted when ActiveRecord encryption is enabled.
- Adds `SnaptradeItem` helpers `start_oauth_device_flow`, `complete_oauth_device_flow!`, and `oauth_token_active?` to start/poll and persist token metadata.
- Adds controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` with sanitized error responses and helper methods `oauth_error_payload` and `parse_oauth_error_body`, and wires new member routes in `config/routes.rb`.
- Updates `SnaptradeItemsController` before_action lists to include the new actions and adds user-facing error message helper `start_oauth_device_flow_error_message`.

### Testing
- Added unit tests `test/models/provider/snaptrade_oauth_test.rb` to validate discovery, device authorization request, token polling, missing client ID handling, and OAuth error propagation, and all assertions passed.
- Added `test/models/snaptrade_item_oauth_test.rb` to assert `complete_oauth_device_flow!` stores tokens and expiry and that `oauth_token_active?` reports correctly, and the test passed.
- Extended `test/controllers/snaptrade_items_controller_test.rb` with controller-level tests confirming sanitized OAuth error payloads and behavior for start/complete endpoints, and these controller tests passed.

* Add SnapTrade OAuth device flow support and token storage

### Motivation

- Add support for the OAuth 2.0 device authorization flow for SnapTrade so users can link brokerages via a device-code flow without traditional browser-based client redirects.
- Persist OAuth token metadata on the SnaptradeItem so tokens can be reused and expiration tracked.
- Surface and sanitize provider error payloads to callers while avoiding leakage of internal configuration details.

### Description

- Added new DB columns and a migration (`oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, `oauth_token_expires_at`) and updated `db/schema.rb` to reflect the change.
- Encrypted the new token fields on `SnaptradeItem` and added `oauth_token_active?`, `start_oauth_device_flow`, and `complete_oauth_device_flow!` helpers to the model.
- Implemented OAuth logic in `Provider::Snaptrade` including discovery (`OAUTH_DISCOVERY_URL`), `start_device_authorization`, `poll_device_token`, request parsing, retry handling, and a small Faraday connection wrapper; added configuration accessors for `oauth_client_id` via `config.x.snaptrade`.
- Added controller endpoints `start_oauth_device_flow` and `complete_oauth_device_flow` with safe error handling and helpers to format OAuth error payloads, and registered new member routes for `snaptrade_items`.
- Added an initializer to load `SNAPTRADE_OAUTH_CLIENT_ID` into `Rails.configuration.x.snaptrade`, and updated `.env.local.example` and `.env.test.example` to include the new env var.
- Added unit tests for provider OAuth behavior (`test/models/provider/snaptrade_oauth_test.rb`), SnaptradeItem token persistence (`test/models/snaptrade_item_oauth_test.rb`), and controller error handling (`test/controllers/snaptrade_items_controller_test.rb`), and updated controller tests accordingly.

### Testing

- Ran provider-level tests in `Provider::SnaptradeOauthTest` to validate discovery, device authorization, token polling, and error handling, which passed.
- Ran model tests in `SnaptradeItemOauthTest` to verify token metadata is stored and `oauth_token_active?` works, which passed.
- Ran controller tests in `SnaptradeItemsControllerTest` that exercise the start/complete endpoints and error sanitization, which passed.

* Add SnapTrade OAuth device-flow support and token storage

### Motivation

- Add support for SnapTrade OAuth device authorization so users can perform device-code based OAuth without embedding secrets in the browser.
- Persist OAuth token metadata on `SnaptradeItem` and expose programmatic start/complete endpoints while avoiding leaking provider internals on errors.
- Make OAuth client ID configurable via environment or credentials for Sure deployments.

### Description

- Introduce `SNAPTRADE_OAUTH_CLIENT_ID` to `.env` examples and add `config.x.snaptrade.oauth_client_id` initializer for configuration.
- Add migration `AddOauthDeviceFlowToSnaptradeItems` and update `schema.rb` to add `oauth_access_token`, `oauth_refresh_token`, `oauth_token_type`, `oauth_scope`, and `oauth_token_expires_at` to `snaptrade_items`.
- Persist and encrypt new token fields in `SnaptradeItem` and add helper methods `oauth_token_active?`, `start_oauth_device_flow`, and `complete_oauth_device_flow!`.
- Extend `Provider::Snaptrade` with OAuth discovery, device authorization (`start_device_authorization`), token polling (`poll_device_token`), Faraday-based `oauth_connection`, parsing and error-wrapping logic, and retry handling.
- Add controller actions `start_oauth_device_flow` and `complete_oauth_device_flow` to `SnaptradeItemsController`, plus helper methods to format OAuth error payloads and user-facing error messages.
- Wire up new routes (`post :start_oauth_device_flow`, `post :complete_oauth_device_flow`) and add `config/initializers/snaptrade.rb`.
- Add comprehensive tests for the OAuth flow and controller error handling and update a system test stub to avoid provider leakage during UI tests.

### Testing

- Added `Provider::SnaptradeOauthTest` which stubs discovery, device authorization and token endpoints and asserts request payloads and responses; tests passed.
- Added `SnaptradeItemOauthTest` which verifies token metadata persistence and `oauth_token_active?`; test passed.
- Extended `SnaptradeItemsControllerTest` with scenarios for successful and failing device-flow completion and start flow error handling; tests passed.
- Ran the affected system test changes in `TradesTest` (provider stubbing and modal behavior); the updated tests passed.
2026-06-27 06:43:12 +02:00
Markus Laaksonen
112b09f425 fix(sync): store EnableBanking credit card debt as balance instead of available credit (#2459)
* fix(sync): store EnableBanking credit card debt as balance instead of available credit

Previously, the EnableBanking processor forcibly overrode the primary account balance for credit cards to be the available credit (credit_limit - debt) rather than the actual outstanding debt. Since credit cards are modeled as Liability accounts, this caused the balance sheet (net worth) to treat available credit mathematically as a debt.

This PR aligns the EnableBanking processor with the Plaid and SimpleFIN processors by storing the absolute debt as the account balance, while tracking the available credit via accountable metadata.

Fixes #2458

* docs(sync): update EnableBanking credit card processor documentation

Updates the inline processor comments to reflect the new behavior introduced by the previous commit, clarifying that outstanding debt is stored sequentially as the primary balance rather than the UX available credit overriding it.

* refactor(sync): clarify balance and available credit calculations in EnableBanking processor

Refactors the debt polarity assignments to clarify why liability balances are strictly parsed as absolute positive numbers. Replaces the implicit ordering dependency between the '.abs' conversion and the 'available_credit' math with an explicit 'outstanding_debt' variable to prevent regression by future maintainers.

* style: remove trailing whitespace in EnableBanking processor
2026-06-27 06:20:50 +02:00
Shibu M
846ece9d01 Merge branch 'we-promise:main' into Transfer-charges 2026-06-27 06:32:41 +05:30
DataEnginr
7695ae9ba7 Replace raw title= attribute with DS::Tooltip on eye-off icon 2026-06-27 00:59:55 +00:00
DataEnginr
42a075a6db Address all PR review comments 2026-06-27 00:57:03 +00:00
DataEnginr
8d514525a6 fix RuboCop Layout/SpaceInsideArrayLiteralBrackets on included_in_reports scope 2026-06-27 00:57:03 +00:00
DataEnginr
9723dc9e98 resolved the issues raised post pr 2026-06-27 00:57:03 +00:00
panther
eb0866a678 Add exclude_from_reports option to accounts
Adds a toggle to mark accounts as excluded from all financial reports
while keeping them active and visible individually.

- Migration: add exclude_from_reports boolean column to accounts
- Model: included_in_reports scope
- BalanceSheet: filter excluded from ClassificationGroup and AccountGroup totals,
  HistoricalAccountScope, AccountRow flag
- IncomeStatement: exclude via SQL fragments in Totals, FamilyStats, CategoryStats
- InvestmentStatement/Budget: chain included_in_reports scope
- ReportsController: filter in breakdown view, export queries, trades
- AccountsController: toggle_exclude_from_reports action + route
- UI: DS::Toggle in account form, eye-off indicator in sidebar, menu toggle items
- Locale: all labels in en.yml
- Tests: model scope, controller toggle, income statement/balance sheet filtering
- 5070 tests pass (0 failures, 0 regressions)
2026-06-27 00:57:03 +00:00
Artem Danilov
9532c158dd fix(balances): materialize entries dated before the opening anchor (#2434)
Co-authored-by: Tolaria <zeolite-fairing-0k@icloud.com>
2026-06-26 06:52:09 +02:00