Accounting Inspect/Close modal shares ONE live source of truth with the Bank tab
Goal (owner, 2026-06-14)¶
"Make it so that they share one single source of truth." Reproduced live: the GCP
linker matched tx mjaAuF1u9moedF1NTmCB (2025-02-09 HK$0.75 → invoice 202501); the
Bank Transactions tab showed it matched, but the Month-end Checklist inspect-period
modal still showed it unmatched. Two different client data paths; only one was live.
Root cause¶
- Bank tab → React Query (
useTransactions), invalidated live by the FirestoreonSnapshotinlib/firestore/useAccountingLiveSync.ts→ updated. ✓ - Inspect/Close modal → its OWN one-shot
fetch()into local state (inspectReadiness,periodTxs) incomponents/accounting/periods/PeriodsTab.tsx. Only refreshed on open or on the'periods:changed'window event — which fires ONLY on periodCloses changes, not on transaction matches. So the modal kept a frozen snapshot. ✗ (stale, not wrong data)
Fix (single source of truth = the shared live React Query layer)¶
The modal no longer fetches on its own. It now reads the SAME caches everything else does:
1. lib/accounting/hooks/useTransactions.ts — added optional { enabled } (backward-compat;
only caller BankTransactionsTab passes one arg).
2. lib/accounting/hooks/useReports.ts — new usePeriodReadiness() + reportKeys.periodReadiness,
keyed UNDER the ['accounting','reports'] prefix so the existing live-sync invalidation
(fired by useAccountingLiveSync on every tx / GL change) refreshes readiness automatically.
3. components/accounting/periods/PeriodsTab.tsx — deleted the manual loadPeriodTransactions
+ inspectReadiness/periodTxs/periodTxsLoading/periodTxsError state + the lazy-load
effect. Readiness now = usePeriodReadiness({candidateMonth: inspectMonth}); the period's
tx list = useTransactions({startDate,endDate,subsidiaryId,limit:0}, {enabled: inspect && !close}).
inspectMonth is the only local UI state left. Alert mark-read now patches the shared
readiness cache via queryClient.setQueryData(reportKeys.periodReadiness(...)). Also removed
2 pre-existing unused antd imports (Col, Row).
Result: a match landing anywhere (auto-linker, another tab, another device) flips the row in the modal in real time — same invalidation, same cache, no second fetch path to drift.
Verification¶
tsc --noEmitclean (exit 0, project-wide).eslintclean on the 3 touched files.- NOT yet verified in a live browser — the Accounting page is behind NextAuth/Google SSO, so a headless preview can't reach the modal. Now SHIPPED to prod, so verify ON PROD: open Inspect for 2025-02, confirm the GCP rows show matched (and watch one flip live if re-matched).
- SHIPPED 2026-06-14: commit b39e6adb → nightly → main (merge 3cb2a75d). Production deploying.
Remaining / follow-ups¶
- Live-verify the exact repro (open Inspect for 2025-02, confirm the GCP txs show matched).
- Commit (bundle with the GCP lookback fix in pages/api/gcp-billing/auto-link.ts, still uncommitted).
- Optional "every corner" sweep: Reports/Transactions/COA/Journals were ALREADY live (React Query);
audit Receipts/Reimbursements tabs (different collection
records/receipts— may be a manual path like this one was). Reuse the same hook pattern if so. Relates to T-037 live initiative.
Log¶
- 2026-06-14 created + implemented (owner repro of modal-vs-bank-tab disagreement). Static checks green; live-verify + commit pending.
- 2026-06-14 dead-code audit of touched files (clean: no commented-out code, no unused symbols, new exports consumed). Committed as 2 commits (13701a7a GCP lookback · b39e6adb single-source-of-truth); promoted main→nightly (b39e6adb) then nightly→main (merge 3cb2a75d, preserves PR #724/#725). Prod deploying. REMAINING: live-verify on prod → then done.
- 2026-06-14 REOPENED — owner reported mjaAuF1u9moedF1NTmCB STILL unmatched in the modal on
BOTH prod & preview after the single-source change. The single-source fix was necessary but
NOT the root cause. REAL ROOT CAUSE:
linkTransactionToBilling(and addBillingLink, unlinkTransactionFromBilling) write theglbilling allocation but NEVER settransaction.status— so a GCP-linked charge keepsstatus:'unmatched'. Bank tab reads the derivedlinkedBilling(→ shows matched); readiness/modal read storedstatus(→ unmatched).statusMUST be written (it's a Firestore-queried field — listTransactions .where transaction.status), so it can't be derive-only. Fix (owner approved 1+2+3):- computeTransactionStatus() canonical helper in transactionAdapter.ts (income match → matched/ partial; else accountCode/billing → categorized; else unmatched). Receipts NOT counted (receipt path doesn't set status). 2. persistRecomputedStatus() choke-point; all 3 billing paths now route through it. 3. Backfilled the 5 GCP txs (unmatched→categorized) — ran a one-off script (dry-first, scoped to billing-linked drift) against prod tebs-erl; verified 0 drift across all 176 txns (helper agreed with stored status on 171, fixed the 5). DATA fix is LIVE in prod Firestore now (the 5 read 'categorized' = resolved → modal stops flagging them). CODE fix (prevent recurrence on future links) is tsc+lint clean, UNCOMMITTED. REMAINING: commit + promote (nightly→main) the code fix; live-confirm modal on prod.
- 2026-06-14 SHIPPED the code fix: commit bea88e3d → nightly → main (merge d723a858, same local-merge flow as 3cb2a75d, preserves history; gh auth still broken so no PR API). Prod deploying. Data fix (5 GCP txs → categorized) already live from the backfill. DONE pending owner's live-confirm of the Inspect modal on prod.