Commit Graph

2988 Commits

Author SHA1 Message Date
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
v0.7.3-alpha.1
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
soky srm
edb91cad6f Merge pull request #2183 from DataEnginr/Transfer-charges
feat (Transfer fees) : Add Bank Charges for Transfers
2026-07-02 18:11:09 +02:00
DataEnginr
5935e56a8b Merge branch 'main' into Transfer-charges 2026-07-01 05:50:51 +00:00
Jestin Palamuttam
f51b240967 Fix date-dependent flake in investment_statement_test (#2539)
test "totals aggregate directly from trade entries" builds a month-to-date
period (beginning_of_month..Date.current) but places a trade at
start_date + 1.day. On the 1st of the month the period collapses to a single
day, so that trade falls outside the range and withdrawals aggregate to 0,
failing the assertion (expected 40, got 0). The test passes every other day.

Use a fixed multi-day period so the test is date-independent.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 06:33:06 +02:00
Nicolas Lefèvre
1ff78bddf2 i18n: fully update French (fr) translations (#1922)
* i18n: fully update French (fr) translations

* Fix French translation and terminology issues

- Correct passkey terminology to 'Clés d'accès et clés de sécurité'.
- Normalize 'Enable Banking' branding and use 'Lier' as action verb.
- Fix statement literal translations ('Solde Sure', 'Mouvements de la période', 'Début de période', 'Dupliqué').
- Fix depository checking and savings translations to 'Compte courant' and 'Compte d'épargne'.
- Correct 'tax_free_bond' translation to 'Obligation non imposable'.
- Normalize SimpleFIN branding.

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-07-01 01:56:13 +02: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
Sure Admin (bot)
9f253f21df docs: explain self-hosted onboarding modes (#2533) 2026-06-30 21:09:25 +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
galuis116
67c6f9dda8 fix(goals): match manual_save pledges by contribution delta, not full balance (#2178)
* 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.
2026-06-30 08:23:14 +02:00
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
Juan José Mata
356be8ca55 Bump versions 2026-06-29 22:58:07 -07: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
0fa7ca5824 test(system): harden property edit flow against the account-menu morph race (#2421)
PropertyTest#open_account_edit_dialog already retried the menu→Edit flow
because the account page issues a Turbo morph refresh shortly after load
(turbo_refreshes_with :morph). But the naive retry had two gaps that can
still flake under a slow CI browser:

- It re-clicked the DS::Menu trigger every iteration. The trigger toggles
  (menu_controller#toggle), so a retry after a slow-but-successful modal
  load would close the open menu and hide "Edit". Now it opens the menu
  only when it is closed.
- It did not tolerate the menu node detaching mid-click ("Node with given
  id does not belong to the document"), an inspector error Capybara does
  not auto-retry. Now it rescues the transient detach/stale errors and
  retries, re-raising anything else.

Mirrors the same guard applied to AccountsTest#open_account_edit_dialog.
2026-06-30 07:46:19 +02:00
ghost
bd740f112c fix(mobile): in-progress feedback for the Clear local data action (#2439)
* fix(mobile): in-progress feedback for the Clear local data action

The reset- and delete-account tiles already disable and show a spinner
while their network calls are in flight, but "Clear local data" (an async
op that wipes offline storage and clears the category/merchant/tag
providers) had no in-progress feedback. The settings list stayed fully
interactive during the wipe, so the row could be tapped again.

Mirror the existing reset/delete pattern: add an _isClearingData flag
(set before the work, cleared in a finally with a mounted guard) and give
the tile a trailing CircularProgressIndicator plus enabled/onTap guards
while it runs. Completes the in-progress feedback across all destructive
async actions in Settings.

flutter analyze: no new issues; full suite (166) green.

* refactor(mobile): unified destructive action guard in settings

Replace three independent boolean flags (_isClearingData, _isResettingAccount,
_isDeletingAccount) with a single _activeDestructiveAction string. All three
danger-zone tiles now disable together whenever any destructive operation is
running, preventing concurrent destructive actions.

* refactor(mobile): name the destructive-action discriminator constants

Address review feedback (jjmata): replace the scattered 'clear' / 'reset' /
'delete' string literals used for _activeDestructiveAction with private
constants (_clearAction / _resetAction / _deleteAction), defined once. The
discriminator can no longer drift via a mistyped literal across the handlers and
tiles — a typo'd constant name fails to compile. No behavior change.
2026-06-30 07:37:34 +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
ghost
3a7dc3c346 design-system(mobile): SureSpacing + SureTypography scale tokens (#2438)
* feat(mobile): add SureSpacing + SureTypography scale tokens

Introduce hand-authored spacing and type-scale constants mirroring the
Tailwind defaults the web design system relies on, so widgets reference a
named step instead of a raw numeric EdgeInsets/SizedBox/fontSize.

- SureSpacing: xs..huge mapping to Tailwind space-1..space-8 (4..32px).
- SureTypography: xs..xxl mapping to Tailwind text-xs..text-2xl font sizes.

Both are hand-written rather than generated from sure.tokens.json because
spacing and the type ramp come from Tailwind's built-in scale, not the
canonical token file (consistent with the tracker's guidance).

Adopt them in the existing primitives (card padding, button metrics +
gap, chip/segmented/list-group gaps and padding, text-field padding +
label gap). All migrations are value-preserving — each token equals the
literal it replaces — so there is no layout change; off-scale one-offs
(control heights, hairlines, deliberate 14px field padding) stay literal.

flutter analyze: no new issues; full suite (166) green.

* docs(mobile): note off-scale SureChip inset; expose SureTypography line heights

Address review feedback (jjmata):

- SureChip: add an inline comment explaining the horizontal:14 content inset is
  deliberately off the SureSpacing scale (between lg=12 and xl=16) — a tuned
  FilterChip-parity dimension, not a spacing step — so it isn't "fixed" to a
  token later.
- SureTypography: expose the paired line heights (xsLineHeight … xxlLineHeight,
  logical px matching the Tailwind text-* defaults) that were previously only in
  doc comments, with a note on deriving Flutter's TextStyle.height multiplier
  (lineHeight / fontSize). No behavior change.
2026-06-30 07:23:24 +02:00
Shibu M
35f395b39f Merge branch 'we-promise:main' into Transfer-charges 2026-06-30 10:44:23 +05:30
ghost
6d3e39e588 fix(mobile): dispose API-key dialog controller on any dismissal (#2399)
* fix(mobile): own the API-key dialog controller in its own State

_showApiKeyDialog created a TextEditingController in the parent State and
only disposed it in the Cancel/Sign-In handlers, so it leaked whenever the
dialog was dismissed via a barrier tap or the system back button.

Disposing it right after `await showDialog` (an earlier attempt) instead
risks disposing the controller while the dialog's TextField is still
mounted during the route's exit transition ("TextEditingController was
used after being disposed").

Extract the dialog into a small StatefulWidget (_ApiKeyLoginDialog) that
owns the controller and disposes it in State.dispose(), tying the
controller's lifecycle to the dialog's widget tree. The dialog pops
true/false/null and the screen shows the error snackbar on a failed
attempt.

flutter analyze: no new issues; flutter test: all green.

* test(mobile): cover ApiKeyLoginDialog controller disposal

Per review: add a widget test for the lifecycle contract that is the core
of this change. Exposes the dialog as `ApiKeyLoginDialog` (@visibleForTesting)
with an injectable controller, and asserts the controller is disposed on
all three dismissal paths — Cancel button, barrier tap, and system back —
by checking that using the controller afterward throws (a disposed
ChangeNotifier throws on reuse).

119 tests pass; flutter analyze: no new issues.

* test(mobile): derive the barrier-tap point from dialog geometry

Per review: replace the hard-coded Offset(10, 10) in the barrier-dismissal
test. Tapping the ModalBarrier widget directly hits the dialog that
occludes its centre, so instead tap halfway between the screen corner and
the dialog's top-left — a point derived from the dialog's real geometry,
resilient to layout changes, and always on the dismissible barrier.

* fix(mobile): keyboard submit + disable Sign In when API key is empty

- Add onSubmitted to the API key TextField so the keyboard Done/Enter key
  submits the form (no need to tap the button).
- Wire a controller listener to rebuild the dialog state and disable the
  Sign In ElevatedButton while the field is blank, giving clear feedback
  before any network call is made.
2026-06-30 07:06:03 +02:00
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
ghost
401cd6ab08 feat(mobile): privacy mode to mask money values (#2386)
* feat(mobile): add privacy mode to mask money values

Adds an app-wide "privacy mode" so users can hide monetary amounts from
over-the-shoulder view.

- PrivacyProvider (ChangeNotifier) backed by PreferencesService, so the
  choice persists across launches and every money widget rebuilds on
  toggle.
- MoneyMasker.mask() collapses an amount's numeric portion into a short
  fixed run of bullets while keeping the currency symbol and sign
  (e.g. CA$1,234.56 -> CA$••••). A fixed run avoids leaking the value's
  magnitude and reads cleanly without stray separators. Currency- and
  locale-agnostic — it operates on already-formatted strings.
- Masking applied at every money render site: net worth + per-currency
  totals + breakdown sheet (NetWorthCard), account balances (AccountCard,
  AccountDetailHeader, transaction form account selector), and transaction
  amounts (transactions list, recent transactions, calendar).
- Two entry points: a "Hide amounts" switch in Settings -> Security, and a
  quick eye toggle in the top bar (visible on every tab).

Tests: MoneyMasker unit tests (fixed-run mask, magnitude hidden, symbol/
sign kept, passthrough, idempotent) + a widget test asserting the net
worth masks/unmasks as the provider flips; account_card_test updated to
provide the new provider. flutter analyze: no new issues; full suite
(123) green.

* fix(mobile): address privacy-mode review feedback

- Startup masking (Codex P1): read the privacy preference in main() before
  runApp and seed PrivacyProvider with it, so the first frame already has
  the correct value — money is never briefly rendered unmasked for a user
  who enabled "Hide amounts". Provider stays fail-closed otherwise: starts
  masked, SureApp's no-arg default is masked, and a failed read keeps it
  masked. A late-completing initial load no longer clobbers an explicit
  user toggle.
- setHidden() reverts the in-memory state (and logs) if persistence fails,
  keeping the UI consistent with what's actually stored.
- Mask the cash-balance detail chip in AccountDetailHeader (was leaking
  the cash position in privacy mode).
- Privacy top-bar toggle gets a "Toggle privacy" tooltip + icon semantic
  label for accessibility (kept as an InkWell to match the adjacent
  settings control).
- Tests: assert fail-closed initial state; assert the exact masked count;
  test the persistence round-trip (set -> reload); add
  PreferencesService.resetForTest() and reset between tests so the cached
  singleton can't leak state.

125 tests pass; flutter analyze: no new issues.

* refactor(mobile): thread hideAmounts through calendar tiles

Per review: the calendar tile builders read PrivacyProvider via
context.read, relying implicitly on the parent build()'s context.watch to
rebuild them — fragile if a tile is later extracted or wrapped in a
RepaintBoundary. Pass hideAmounts down explicitly instead, matching the
recent_transactions_screen pattern:

- build() (context.watch) -> _buildCalendar -> _buildDayCell
- _showTransactionsDialog reads once when the modal opens ->
  _buildTransactionTile

No more context.read inside tile methods. 125 tests pass; analyze clean.

* fix(mobile): watch PrivacyProvider inside calendar dialog builder

Moving the hideAmounts read inside the showDialog builder and switching
from context.read to context.watch ensures the dialog re-masks transaction
amounts if the user toggles privacy mode while the dialog is open.
2026-06-30 06:49:05 +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
Juan José Mata
45bbb3c00a Fix iOS build uploads 2026-06-28 13:43:15 -07:00
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
76fe10798d Update schema.rb version after migrate, preserve clean diff vs main 2026-06-28 19:53:02 +00: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
90eac92375 Merge branch 'we-promise:main' into Transfer-charges 2026-06-29 00:04:49 +05:30
dependabot[bot]
1fdec91451 chore(deps): bump yard from 0.9.42 to 0.9.44 (#2522)
Bumps [yard](https://yardoc.org) from 0.9.42 to 0.9.44.

---
updated-dependencies:
- dependency-name: yard
  dependency-version: 0.9.44
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 20:15:59 +02:00
Juan José Mata
2e91453fcc Require omniauth-rails_csrf_protection v2.0+ for Rails 8.2 compatibility (#2520)
* Bump omniauth-rails_csrf_protection to 2.0 to drop ActiveSupport::Configurable

omniauth-rails_csrf_protection 1.0.2 does `include ActiveSupport::Configurable`
in its TokenVerifier, which triggers on Rails 8.1:

  DEPRECATION WARNING: ActiveSupport::Configurable is deprecated without
  replacement, and will be removed in Rails 8.2.

v2.0 reworks TokenVerifier to delegate `config` to `ActionController::Base.config`
on Rails 8.1+, so it no longer references (or even requires) the deprecated
module. The gem's runtime deps, railtie, and public middleware integration are
unchanged, so this is a behavior-preserving bump. Pin `>= 2.0` so a fresh
install can't regress to the deprecated version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019eC5BNZmL6LPBMCUr35ev9

* Verbosity

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

---------

Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-28 20:00:28 +02:00
Shibu M
eccf050521 Merge branch 'we-promise:main' into Transfer-charges 2026-06-28 22:31:14 +05:30
soky srm
1d909a0b9e Merge pull request #2416 from DataEnginr/feature/exclude-from-reports
Adds a toggle to mark accounts as excluded from all financial reports while keeping them active and visible individually.
2026-06-28 18:59:42 +02:00
DataEnginr
2c18987a2d Merge upstream/main into feature/exclude-from-reports 2026-06-28 16:46:43 +00:00
DataEnginr
eee0ca8975 Fix migration: add missing add_column, keep schema on 7.2 with minimal diff 2026-06-28 16:46:39 +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