Skip to content

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 Firestore onSnapshot in lib/firestore/useAccountingLiveSync.ts → updated. ✓
  • Inspect/Close modal → its OWN one-shot fetch() into local state (inspectReadiness, periodTxs) in components/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 --noEmit clean (exit 0, project-wide). eslint clean 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 the gl billing allocation but NEVER set transaction.status — so a GCP-linked charge keeps status:'unmatched'. Bank tab reads the derived linkedBilling (→ shows matched); readiness/modal read stored status (→ unmatched). status MUST 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):
    1. 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.