Skip to content

Re-populate tx-level GL code (4000 / 4001) on income txs at the V1 adapter — reverse the read-side projection part of T-075

Why (owner, 2026-06-23)

On a tx-diagnosis session for tx 9zVOyZ6AggDJaSpKqD25 (HKD 9,500 OCBC cheque deposit matched to two ERL invoices summing to 9,500), the owner observed that the GL Account column / tag is blank on every income tx in the UI — even though the journal still posts to GL 4000 correctly. Tracing back:

  • A pre-existing constant GL_REVENUE_RESTRICTED = ['4000','4001'] blocks direct manual assignment of those codes via the assign-account API (pages/api/accounting/transactions/[id].ts:768"Account 4000 cannot be directly assigned. It is populated via invoice matching only."). That part is accounting-sound and stays.
  • A separate read-side choice — a "skip when inner values are all arrays" rule inside getAccountCodeFromGL (lib/accounting/types.ts:1572) — leaves the in-app tx field accountCode blank for income txs even when the underlying doc has invoice allocations at gl["4000"]. Result: the UI's GL column, tag, drawer, linking modal, title builder, and Excel export all render blank for income txs.
  • That second choice was preserved (not introduced) by T-075. The owner does not recall agreeing to it, considers the accountCode field significant on the tx doc for both income and expense txs, and wants it re-populated.

Direction (owner-confirmed)

Re-populate tx.accountCode for income txs at the read-side adapter only. The write-side block on direct manual assignment of 4000 / 4001 stays. Coaching's gl["4001"] write + the backfill from T-075 also stay — those are the structurally-clean parts of T-075 and the owner specifically asked for them.

Plan

  1. Adapter fix (one place — lib/accounting/transactionAdapter.ts, in toV1). After the existing getAccountCodeFromGL call, fall back to GL_PRODUCTION_REVENUE when gl["4000"] is present (income matched to project invoices), then to GL_COACHING_REVENUE when gl["4001"] is present (income matched to coaching). Skip rule on the shared helper is left untouched — keeps the "direct assignment only" semantic for code that calls the helper on raw Firestore docs (journal-line builder, backfill scripts).
  2. No data migration. Adapter is read-side only.
  3. Verify the existing accounting test suite still passes (npx vitest run __tests__/lib/accounting/). Owner runs the app to spot- check the GL tag / column on income txs (I can't drive the UI from here).
  4. Cross-link to T-075 (log entry pointing here) and close T-097's spillover (T-097 was originally numbered T-092; renumbered at merge time after a UID collision with Records' font/color task).

Why A (adapter-local) over B (remove the helper's skip rule)

Research summary: 9 callers of getAccountCodeFromGL audited. None hard- depend on the skip rule — every consumer that needs to distinguish "matched income" from "direct expense" already has its own explicit matchedInvoices / matchedCoaching* guard. The skip rule plays a semantic-clarity role at the helper level (multiple call-site comments lean on "undefined means not directly assigned"), but no business logic would break if it were removed. Choosing A anyway because: - Same user-visible result (UI + Excel export both read tx.accountCode off the V1 adapter, so the adapter fix flows through to both). - Smaller blast radius — one file vs a shared helper used in 9 places. - Preserves "direct assignment only" signal where server-side code still calls the helper directly on raw Firestore docs. - Incidental upside: the receipt auto-categoriser (lib/accounting/matching/hooks.server.ts:204if (tx.accountCode) return null) now short-circuits correctly on income txs at the first guard instead of leaning on the matchedInvoices fallback below.

Out of scope

  • The orphan-invoice side finding (the two invoice numbers on tx 9zVOyZ6AggDJaSpKqD25ERL-2025-010-0423 / ERL-2025-019-0624 — didn't resolve at the obvious Firestore path during the diagnosis). Separate ticket if the owner wants it pursued.

Log

  • 2026-06-23 created. Adapter change + cross-links land in the same commit. Source: Accounting (Diagnostics) · https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
  • 2026-06-23 DONE — shipped in PR #792 (merged to main, SHA bc11e80). · Adapter fix: lib/accounting/transactionAdapter.ts toV1 falls back to GL_PRODUCTION_REVENUE / GL_COACHING_REVENUE when no direct assignment exists (commit f4ba299). · Codex P1 write-guards (commit c0a90da): uncategorizeTransaction + updateTransaction wopcRef skip gl[code] mutations for revenue-restricted codes; TransactionLinkingModal hides Unassign/Change-Account for them — so surfacing 4000/4001 as accountCode can't trigger a match-deleting Unassign. Tests 114/114. · Verified live this session: tx 9zVOyZ6AggDJaSpKqD25 now resolves accountCode = "4000" through the merged adapter (was blank). · Blast radius: read-side projection — every UI surface + the Excel export that reads tx.accountCode now shows the GL on income txs; write-side block on direct manual assignment of 4000/4001 unchanged. Status was left on doing after the merge; flipped to done here (audit, 2026-06-23). Source: Accounting (Diagnostics) · https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea