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 fieldaccountCodeblank for income txs even when the underlying doc has invoice allocations atgl["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
accountCodefield 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¶
- Adapter fix (one place —
lib/accounting/transactionAdapter.ts, intoV1). After the existinggetAccountCodeFromGLcall, fall back toGL_PRODUCTION_REVENUEwhengl["4000"]is present (income matched to project invoices), then toGL_COACHING_REVENUEwhengl["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). - No data migration. Adapter is read-side only.
- 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). - 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:204 — if (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
9zVOyZ6AggDJaSpKqD25—ERL-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, SHAbc11e80). · Adapter fix:lib/accounting/transactionAdapter.tstoV1falls back toGL_PRODUCTION_REVENUE/GL_COACHING_REVENUEwhen no direct assignment exists (commitf4ba299). · Codex P1 write-guards (commitc0a90da):uncategorizeTransaction+updateTransactionwopcRef skipgl[code]mutations for revenue-restricted codes;TransactionLinkingModalhides Unassign/Change-Account for them — so surfacing 4000/4001 asaccountCodecan't trigger a match-deleting Unassign. Tests 114/114. · Verified live this session: tx9zVOyZ6AggDJaSpKqD25now resolvesaccountCode = "4000"through the merged adapter (was blank). · Blast radius: read-side projection — every UI surface + the Excel export that readstx.accountCodenow shows the GL on income txs; write-side block on direct manual assignment of 4000/4001 unchanged. Status was left ondoingafter the merge; flipped todonehere (audit, 2026-06-23). Source: Accounting (Diagnostics) · https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea