fix(bill-modal): SimpleFIN import refreshes payments + Tracker live (BM4)

The Sync button and merchant-rule historical import both CREATE payments but
only reloaded linked transactions, so the modal's Payment History stayed stale
and the Tracker row behind the modal didn't update (kept showing due/overdue)
until close+reopen. Both now await Promise.all([loadPayments(),
loadLinkedTransactions()]) then onSave?.(), matching the unmatch handlers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 18:11:28 -05:00
parent 4a38cc8614
commit ad1f5bebf6
2 changed files with 12 additions and 3 deletions

View File

@ -3,6 +3,7 @@
### 🐛 Tracker & bill-modal hardening
- **[Bill modal/SimpleFIN] Importing bank payments didn't refresh the payment list or the Tracker** — the two flows in the bill modal that *create* payments — **Sync** (`syncBillSimplefinPayments`) and a **merchant-rule historical import** (`onRulesChanged` → `importHistoricalPayments`) — only reloaded the linked-transactions list, unlike the unmatch handlers which correctly reload payments *and* linked transactions *and* call `onSave`. So after importing, say, 3 payments from bank history, the modal's Payment History stayed stale and the Tracker row behind it kept showing "due/overdue" even though the bill was now covered — until you closed and reopened. Both paths now `await Promise.all([loadPayments(), loadLinkedTransactions()])` then `onSave?.()`, matching the unmatch pattern, so imported payments appear immediately and the Tracker updates live. (The SimpleFIN *search/preview/candidate* flow was already correct.) (Tracker BM4)
- **[Tracker/SimpleFIN] Bank card's "unpaid this month" and "remaining" over-counted off-month bills** — `buildBankTracking` (`services/trackerService.js`) summed `expected_amount` for *all* active unpaid bills via SQL with no occurrence gate, so an annual or off-month quarterly bill inflated `unpaid_this_month` (and therefore the bank `remaining`) even though the Tracker rows beside it correctly excluded it — the same class of bug as QA-B5-02, still live on the bank path. `getTracker` now derives the unpaid total from the already-gated rows (via `resolveDueDate`), netting partial payments, and passes it into `buildBankTracking`. Also made `summary.remaining` / `total_remaining` use the bank card's own remaining when bank tracking is on (they previously used manual starting-amount math even in bank mode, disagreeing with safe-to-spend), and switched a stray `balance / 100` to `fromCents`. New test file `tests/trackerService.test.js` covers the gating fix, summary totals, the bank-mode remaining agreement, cents↔dollars integrity, and `getOverdueCount` gating — the dense Tracker aggregation had no dedicated tests before. (Tracker T1)
- **[Payments] Quick-pay could create duplicate payments and double-drop the balance** — `POST /api/payments/quick` (the one-click "pay" behind every Tracker row) had **no duplicate guard** and its INSERT + balance update weren't atomic, unlike `POST /api/payments/bulk`. A double-click, a retry, or two open tabs made a *second* payment for the same bill/date/amount and applied the balance drop twice; a failure between the INSERT and the balance write left a payment with no balance adjustment. Quick-pay now checks the same `bill_id + paid_date + amount` composite key (returning the existing payment idempotently, HTTP 200) and wraps the INSERT + `applyBalanceDelta` in a single `db.transaction`. A different amount on the same day is still a legitimate new payment. Test: `tests/paymentsQuickRoute.test.js`. (Tracker X1)

View File

@ -1211,7 +1211,11 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
const result = await api.syncBillSimplefinPayments(sourceBill.id);
if (result.added > 0) {
toast.success(`${result.added} payment${result.added !== 1 ? 's' : ''} imported from bank history.`);
loadLinkedTransactions?.();
// Imported payments must refresh the payment list AND the
// Tracker behind the modal (the row may now be covered)
// same as the unmatch handlers.
await Promise.all([loadPayments(), loadLinkedTransactions()]);
onSave?.();
} else {
toast.info('No new matching transactions found.');
}
@ -1238,9 +1242,13 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa
<BillMerchantRules
billId={sourceBill?.id}
billName={sourceBill?.name}
onRulesChanged={() => {
onRulesChanged={async () => {
setLocalHasRules(true);
loadLinkedTransactions?.();
// A historical import (fired after adding a rule) creates
// payments, so refresh the payment list AND the Tracker too
// not just the linked transactions.
await Promise.all([loadPayments(), loadLinkedTransactions()]);
onSave?.();
}}
/>
</div>