The per-module Gl*Code appsettings keys pinned GL's account *codes* in
ERPCore's config, while GL generates codes positionally and refuses a
requested one. Nothing connected the two, so "1100 means Inventory" held
only because someone once created accounts in a particular order - and on
2026-10-07 eleven of the twelve configured codes turned out never to have
existed in GL. Every GRN, adjustment, day-end, invoice and FIFO posting had
been failing with "Account <code> not found".
The mapping is now master data in gl_account_config, keyed by accounting
role rather than by module, and GlAccountConfigService validates every
submitted code against GL's live chart of accounts before storing it. A
code GL does not have, or cannot post to, is refused with the role named -
so the mismatch is unrepresentable rather than merely detected. Correcting
a mapping no longer needs a redeploy.
Notes:
- Fails closed on save (503 if GL is unreachable), deliberately opposite to
the startup validator, which must still let the app serve.
- Non-postable accounts are rejected too: GL rejects a journal line against
one, so storing it only defers the failure. The auto-created 1000
Bank/Cash header is exactly that trap.
- The settings endpoint also serves the selectable accounts, because
GeneralLedgerController is gated on `ledgers` while this is on `settings`
- and it keeps the picker and the validation list the same data.
- Reading an unmapped role throws naming the role, instead of posting ""
and letting GL return an opaque 404 from inside a transaction.
- The appsettings keys are now seed-only: DataSeeder copies them in once so
existing deployments keep their current mapping. No runtime fallback, or
there would be two sources of truth again.
- The 32 read sites across 11 services collapsed to 12 roles, since every
module carried the same value for a given role.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>