Skip to content

Display-name audit follow-up — vendor template clean-up + live-sync templates to transactions

Renumbered T-115 → T-118 (2026-06-25, renumber-on-merge). While this task was in flight on claude/busy-dirac-QmUdM (PR #808), a parallel agent landed T-115 (Records — Receipt Register XLSX Substantiating Notes column) on main first. Per the AGENTS.md "whoever merges second renumbers" rule, this task moved to T-118 at merge time; the in-code T-115 comments were updated to T-118 to match. Commit messages stay historical T-115. Sibling T-114 → T-117.

Why (owner, 2026-06-25)

Continuation of the display-name audit that started in T-117. After the receipt-token loader closed the gap on receipt-matched txs, the owner asked for the same audit pass across every doc-type template AND for template edits in Settings → Routing to reflect immediately on a transaction (no page refresh).

Two pieces ship together:

  1. Vendor-invoice template clean-up. The configured vendor_invoice doc-type template was: [{{accountCode}} - {{accountName}}] {{merchantCategory}} | {{merchant}} // {{invoiceNumber}} {{merchantCategory}} had no populator — vendor invoices don't carry a granular AI category like receipts do (the receipt token loader exists because receipts have parsedReceipt.category; vendor docs have nothing equivalent). Token rendered blank, leading to a | floating with nothing left of it. Owner picked option A — drop the token from the template — rather than build a vendor-side category source.

  2. Live-sync templates → transactions invalidation. Display names are computed at READ time (never cached on the tx doc), so a template edit only needs to trigger a transactions refetch on the client for the new name to surface immediately. The existing useAccountingLiveSync already invalidates transactions on COA changes (GL-level fallback templates are stored on each accounting/accounts/gl/{code} doc) — but doc-type templates live under accounting/settings/docTypes/{recordType}, a separate path nothing was listening on. Edits sat unrendered until the next organic refetch.

What shipped

  • accounting/settings/docTypes/vendor_invoice (Firestore one-off): displayNameTemplate set to [{{accountCode}} - {{accountName}}] {{merchant}} // {{invoiceNumber}}. Audit-log marker on the updatedBy field references T-118.
  • lib/firestore/useAccountingLiveSync.ts — added a third useEffect that subscribes to accounting/settings/docTypes (collection snapshot via the shared firestoreSubscriptionManager). On any change: invalidate accountingKeys.transactions.all, accountingKeys.journalsAll(), and ['accounting', 'reports'] — the same cascade the COA listener fires. Same isWithinLocalMutationCooldown gate so an admin's own template save doesn't kick a redundant fetch mid-optimistic-update.

Audit summary (other doc-type / GL templates surveyed, no changes needed)

Cross-checked every configured template against what enrichTransactionDisplayName actually populates per linkedRecordType:

Doc-type template Tokens it needs Populator Verdict
receipt merchant, merchantCategory getReceiptDisplayTokens (T-117)
vendor_invoice merchant, invoiceNumber expenseMetadata.* from vendor matcher ✅ (after T-118 drop of {{merchantCategory}})
workspace_invoice merchant, billingMonth, invoiceNumber getBillingDisplayTokens
gcp_invoice gcpServices, gcpBillingMonth, gcpInvoiceNumber getBillingDisplayTokens
cross_border_fee merchant, billingMonth, invoiceNumber getCrossBorderFeeTokens
project_invoice clientCompany, workType, projectTitle, invoiceNumber matched-invoice branch
coaching_invoice studentName, sessionOrdinal, sessionDate matched-coaching branch
wopc_project workType, projectTitle, invoiceNumber, payee, wopcRef matched-invoice + wopcReferenceNumber likely ✅ (no live sample to render against)
wopc_coaching studentName, sessionOrdinal, invoiceNumber, payee, wopcRef coaching match + WOPC ref likely ✅ (no live sample)

15/43 GL fallback templates were also surveyed; the {{subscription}} token referenced by some (5000 / 6000 / 6006) has no current populator either — but those templates only fire when no record is linked (extremely rare for software/IT GLs which almost always have a vendor invoice match), so the surfaced behaviour is acceptable until that becomes a real issue.

Out of scope

  • Vendor-side category populator. Could mirror T-117's receipt loader by reading the vendor's category field from accounting/vendors/{id} — but vendors don't currently carry an AI-derived category, so the field would need to be human-curated. Worth opening if granular vendor P&L slicing becomes a desired UX; today the GL itself is the granular unit.
  • {{subscription}} token populator for 5000/6000/6006 GL fallbacks. Same reasoning — the fallback path is rare (matched txs go through doc-type templates) and the token has no obvious source today.

Decision log

2026-06-25 — T-118 opened + shipped

  • Attestation (Accounting (Diagnostics)): read AGENTS.md (tip 26330aac); board scanned by scope — T-117 just closed (receipt token loader); no overlap with this audit follow-up.
  • Source: Accounting (Diagnostics) · https://claude.ai/code/session_01G58Y71noihrYCDEDMexmea
  • Owner direction (verbatim, 2026-06-25):

    "Can you help me run thru other GL accounts too (Especially those that you've touched/ has to do with the receipts/invoice auto-matching)?" "Okay, A. wire the live-sync for templates → transactions invalidation"

Owner picked option A (drop {{merchantCategory}} from the vendor template) over option B (build a vendor-side category source) after I laid out the trade-off — option A is the proportional fix; the granular vendor category UX can come later if it becomes a real ask. - Tests + tsc: npx tsc --noEmit clean. - Live verification: vendor template Firestore doc rewritten and re-read shows the new value; no live vendor-matched tx in prod today to render against (audit found 0 across all non-receipt linked types). - Blast radius: the new live-sync listener fires per snapshot on the docTypes subcollection — at most 9 docs currently, edits are rare (admin-only). Invalidation cascade is identical to the COA listener already in place, so any cooldown / refetch behaviour is consistent. Vendor template change is a single-field merge on one Firestore doc; next read picks up the new template (server cache TTL 60s).