mirror of
https://github.com/we-promise/sure.git
synced 2026-08-12 11:10:20 +00:00
cfdd3c476fb0c7b36ef7f805bc72c8ec3362033e
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
83c64b9e94 |
fix(goals): pledge lifecycle + connected-account detection
Behavioural fixes touching Goal, GoalPledge, the reconciler and the goals controller. No schema change. B5 — connected-account detection covered only Plaid. SimpleFIN, Brex, Enable Banking, IBKR, Kraken, SnapTrade and Lunchflow users got "manual_save" pledges by default; their auto-synced Transactions then failed to match (reconciler matches Transactions to "transfer" pledges only). Pledges sat in the yellow banner until expiry. Switch the detection to !Account#manual?, which mirrors the existing `Account.manual` scope (no account_providers, no plaid_account_id, no simplefin_account_id). Add `Account#manual?` so the per-instance and per-query checks can't drift. B7 — `extend!` widens `expires_at` but `matches?` was anchored on `created_at ± 5d`, so an extension that pushed the expiry past day 5 didn't actually buy any match runway. Widen the upper bound to `max(created_at + 5d, expires_at)`. The lower bound stays at `created_at − 5d`. B8 — `Goal#open_pledges` returned `status: open` regardless of expiry. Between a pledge timing out (day 7) and the 15-min sweep job marking it `expired`, the show page rendered a ghost yellow banner with "0 days left" that the reconciler would no longer touch. Add `expires_at >= NOW` to the scope so the visible state matches the match-eligible state. B9 — Double-click on Record pledge produced two identical open pledges, which then stacked as two yellow banners. Add a create-time validation rejecting duplicates against (goal_id, account_id, amount, status=open, expires_at >= NOW). B10 — The reconciler used `transaction.with_lock` but didn't lock the pledge. Two concurrent reconcile attempts on different transactions could both target the same pledge; one would lose to the partial unique index on `transactions.extra->'goal'->>'pledge_id'` and the RecordNotUnique was caught by the outer StandardError rescue, which silently dropped the other transaction's match attempt entirely. Lock the pledge first, re-check `status_open?` inside the lock, and catch RecordNotUnique alongside RecordInvalid/NotOpenError in the reconciler — so on a lost race we fall through to the next candidate pledge instead of exiting the loop. Extract the Valuation-match path to `GoalPledge#resolve_with_valuation!` so it goes through the same locked status-recheck. B12 — When a goal is destroyed, `dependent: :destroy` reaped pledges but left `transactions.extra["goal"]["pledge_id"]` pointing at the now-deleted UUIDs. The partial unique index on that JSON path then indexed stale references. Add a `before_destroy` on GoalPledge that clears the matching transaction's `extra` if it still points back to the pledge. B6 — `last_matched_pledge_at` used `goal_pledges.maximum(:updated_at)` on matched rows. Any backfill or sync-resync that touches a matched pledge bumped `updated_at`, so a single resync set every goal's "Last saved N days ago" header back to "today". Switch to the entry's `date` via a join through `matched_transaction_id`, which reflects the date the money actually moved. B22 — `scope :chronological` ordered DESC, the opposite of what the name promises. Rename to `:reverse_chronological` and update the one caller in `goals#show`. (Other models' `chronological` scopes are unrelated and ordered correctly.) Also: preload `account_providers` on `linked_accounts` in the index and show controllers so `Account#manual?` walks the in-memory collection instead of triggering N queries. Tests: add fixture-backed coverage for extend-widens-match-window, post-extend rejection beyond expiry, and the duplicate-pledge validation. Existing assertions still hold against the new `matches?` window math. |
||
|
|
eb7ef50eed |
fix(goals): CI green — schema, brakeman, pledge modal, error class
Regenerate schema.rb after the three v2 migrations so CI's db:schema:load picks up goal_pledges, the dropped goal_contributions, and the partial unique pledge_id index. Brakeman: - Drop :account_id and :kind from goal_pledge permit; look the account up via @goal.linked_accounts.find_by(id:) instead and set kind server-side from goal.any_connected_account?. - Rename goals.show.projection.on_track to .on_track_html so I18n marks the result html_safe automatically; drop the unconditional .html_safe call in show.html.erb. Pledge modal: rewrite app/views/goal_pledges/new.html.erb to use DS::Dialog (the Sure convention for create modals — matches categories/transfers). Error handling: replace `raise ActiveRecord::RecordInvalid, "string"` in GoalPledge#extend!/cancel! with a dedicated GoalPledge::NotOpenError; the controller rescues that specifically. Tests: rewrite the "pace is zero" test to create a fresh account with no entries (the fixture's depository accounts carry transaction history that produces a non-zero pace). All goal tests now green (73 runs, 157 assertions, 0 failures). |
||
|
|
88032ce020 |
feat(goals): v2 architecture — drop ledger, derive balance, add pledge
Reshape the goals feature to live on top of linked-account balances. A goal's balance is now the live balance of every depository account linked to it — no parallel ledger, no "log a contribution" step. The "Add contribution" affordance is replaced by a 7-day GoalPledge (kind: transfer | manual_save). GoalPledge::Reconciler matches incoming Transactions (via Account::ProviderImportAdapter) and Valuations (via Account::ReconciliationManager) against open pledges within ±5 days, ±$0.50, or ±1% — single hook covers every provider (Plaid, SimpleFIN, Lunchflow, Enable Banking, Brex, IBKR, Kraken, SnapTrade) plus manual balance edits. A 15-minute Sidekiq cron sweeps expired pledges. Goal model: balance derived from linked_accounts.sum(&:balance), new pace (90-day net non-transfer inflow), months_of_runway, last_matched_pledge_*, pledge_action_label_key (the "I just transferred…" vs "I just saved…" verb switch). UI: - Index gets a 3-card KPI strip (Contributed last 30d / Needs this month / On track) plus a pending-pledges callout. - Show page swaps the "Add contribution" CTA for the pledge modal, replaces the contribution list with a pending-pledge banner, and rebuilds the funding widget into per-account rows with a 12-bucket weekly sparkline and last-30 inflow. - Projection chart adds a required-line (dashed light from today → target) and a translucent pending-pledge bump at today's X. Schema (3 migrations): 1. goal_pledges table with PG enums (goal_pledge_kind, goal_pledge_status), open-by-expiry index, and unique-when-not-null matched_transaction_id. 2. Drop goal_contributions. 3. Partial unique index on transactions ((extra -> 'goal' ->> 'pledge_id')) built CONCURRENTLY so it doesn't block prod. After pulling: run bin/rails db:migrate, then commit the schema.rb sync separately (or let CI regenerate). Deferred to v1.1: allocation columns, contention/archived banners, "why is this behind?" diagnostic, reallocate flow, refresh-sync + Plaid throttle, unallocated-cash chip, joint-account approval, goal_activities log, polymorphic matched_entry_id/type for manual pledge audit. |