Materiality Tiers — One Config, Many Domains
| Owner | Classification | Version | Effective | Next review | Status |
|---|---|---|---|---|---|
| Platform Engineering | Internal | 1.0 | 2026-09-15 | 2026-12-15 | Draft |
Purpose. Materiality tiers answer one question for every finance domain: does this thing matter enough to need attention? An invoice, a customer's past-due exposure, a journal entry, and a reconciled account balance are all classified into the same tier vocabulary (Critical / High / Medium / Low) using one shared policy table. This page documents the canonical table, the applicationcode domain axis (and the rule that CASH serves both cashapp and collections), the live consumers, how a tier is computed, and the admin UI. Materiality is standalone config — it is not part of the BPD orchestrator and is orthogonal to validation; it only classifies, it never gates.
1. One canonical table (and a deprecated fork)
There is exactly one canonical table. A second, narrower copy exists in the invoice schema and is deprecated.
| Table | Schema | Status | Evidence |
|---|---|---|---|
materialityconfig | finbase | ✅ Canonical — all engines read this | packages/finance/finbase/finbased/prisma/models/finbase.prisma (model materialityconfig) |
materialityconfig | invoice | ⚠️ Deprecated fork — narrower, no domain axis; kept only for the legacy invoice CRUD controller | packages/finance/invoice/invoiced/prisma/models/invoice.prisma |
The canonical table was added in PR #1254; the reproducible migration is packages/finance/finbase/finbased/prisma/migrations/manual/20260813_materialityconfig_table.sql (idempotent CREATE TABLE IF NOT EXISTS, added because the golden finbase.sql had the model but no manual/ migration → drift-check flagged it missing on fresh DBs).
Canonical columns:
| Column | Meaning |
|---|---|
applicationcode | Domain scope — CASH / INVOICE / JOURNAL / RECON (null = global default) |
tiername | Critical / High / Medium / Low (invoice also uses CFO in its inline fallback) |
minamount / maxamount | Absolute band (maxamount NULL = open-ended top tier) |
percent | Relative threshold (e.g. % of portfolio/period total); NULL = no relative test |
basis | PeriodTotal / AccountBalance / EntityTotal / Portfolio — what percent is relative to |
andor | Or (default) / And — how the absolute and relative tests combine |
currency, businessentityid, isactive | Scope + soft-delete |
⚠️ The deprecated invoice fork (invoice.materialityconfig) has only tiername / minamount / maxamount / currency / description / businessentityid / isactive — no applicationcode, percent, basis, or andor. It is not read by any engine; the invoice materiality classifier reads the finbase table (see §4). ◯ The one thing still bound to the fork is the invoice CRUD controller/service (§5).
2. The applicationcode axis is a DOMAIN axis — do not lowercase
applicationcode on materialityconfig holds an UPPERCASE domain value, not a module slug. This is deliberate and differs from the module-slug usage of the same column name on other tables (e.g. BPD rows use lowercase cashapp / invoicehub).
Domain (applicationcode) | Serves | Basis convention | Seed |
|---|---|---|---|
CASH | ✅ cashapp AND collections (one domain, two modules) | Portfolio (% of total past-due AR) + absolute floors | scripts/collections/seed-cash-materialityconfig.sql |
INVOICE | Invoice classification | absolute bands (account currency) | seeded rows read by classifier; inline fallback in the razor |
JOURNAL | Journal-entry assessment | EntityTotal, absolute spine (percent NULL in v1) | scripts/journal/seed-journal-materialityconfig.sql |
RECON | Period / account reconciliation | AccountBalance + % of period total | ◯ no checked-in seed script found (see §7) |
⚠️ Do NOT normalize these to lowercase. Every consumer filters with the literal UPPERCASE string — e.g. WHERE mc.applicationcode = 'CASH' in packages/finance/cash/casha/services/cash/collectionsfinni.service.ts:355, = 'RECON' in the recon assess razor, = 'JOURNAL' in the journal assess razor, and = 'INVOICE' in the invoice classifier. Lowercasing (or "helpfully" mapping CASH→cashapp) silently returns zero bands, and every engine then falls back to its inline defaults or a NULL tier. The CASH-serves-both rule is load-bearing: collections and cashapp share one set of bands by design.
3. How a tier is computed
The tier is the strongest (highest-floor) band whose test the amount satisfies. The absolute and relative tests are combined per andor (default Or). Expressed as the common SQL shape used by the assess engines:
SELECT mc.tiername
FROM finbase.materialityconfig mc[, <total CTE>]
WHERE mc.applicationcode = '<DOMAIN>' AND mc.isactive
AND ( (<amount> >= COALESCE(mc.minamount, 0)
AND (mc.maxamount IS NULL OR <amount> < mc.maxamount)) -- absolute band
OR (mc.percent IS NOT NULL
AND <amount> >= (mc.percent / 100.0) * <total>) ) -- relative test
ORDER BY mc.minamount DESC
LIMIT 1| Domain | <amount> | <total> | File |
|---|---|---|---|
| CASH (collections) | customer past-due exposure | total open past-due portfolio | packages/finance/cash/casha/services/cash/collectionsfinni.service.ts |
| RECON | abs(glbalance) per account | period total | zhttp/reconciliation/assessperiodintelligence/assessperiodintelligence.ts |
| JOURNAL | entry amount | entity/period total (percent NULL in v1) | zhttp/journal/jvvalidations/assessjournalintelligence/assessjournalintelligence.ts |
| INVOICE | invoice amount in account currency (falls back to raw) | n/a (absolute only) | zhttp/invoice/processtransformations/materialityclassification/materialityclassification.ts |
Two evaluation moments, same bands:
- Read-time (worklist chips): e.g. collections computes the tier live in the
collectionsfinniquery so the worklist segmentation chips reflect current exposure. - Freeze-time (assess workflows): the
assess*intelligenceworkflows exportfinbase.materialityconfiginto DuckDB, computematerialitytierin the razor, and freeze it onto the domain row (arcollectionstatus,periodsummary,jvprocessingdata,invoiceprocessingmetrics). ✅ Read-time and freeze-time use identical band logic.
⚠️ Every consumer keeps an inline fallback if the config table is empty/unreadable — so an un-seeded domain degrades to hardcoded defaults (invoice: <10k Low, <50k Medium, <250k High, else CFO) rather than erroring. This is zero-regression by design but means "tiers look wrong" can mean "the seed didn't run," not "the config is wrong."
4. Consumers (with file evidence)
Roughly five live consumers read the canonical table (the assess* razors dominate; the invoice path adds classification + approver routing):
| # | Consumer | Domain | What it does | File |
|---|---|---|---|---|
| 1 | Invoice materiality classifier | INVOICE | Writes materialitytier onto invoiceprocessingmetrics; feeds approver-tier routing (#170) + detail-page badge (#171/#172) + auto-clear gate | zhttp/invoice/processtransformations/materialityclassification/materialityclassification.ts |
| 2 | Collections assess / Finni worklist | CASH | Segments the collections worklist ("which customers matter") | packages/finance/cash/casha/services/cash/collectionsfinni.service.ts; packages/finance/cash/cashwf/src/workflows/assesscollectionintelligence/workflow.ts |
| 3 | Recon period intelligence | RECON | Tags each account/period with a tier alongside anomaly scoring | zhttp/reconciliation/assessperiodintelligence/assessperiodintelligence.ts; packages/finance/reconciliation/reconciliationwf/src/workflows/assessperiodintelligence/workflow.ts |
| 4 | Journal entry intelligence | JOURNAL | Tags each JV with a materiality tier for topside-entry review | zhttp/journal/jvvalidations/assessjournalintelligence/assessjournalintelligence.ts; packages/finance/journal/journalwf/src/workflows/assessjournalintelligence/workflow.ts |
| 5 | Invoice approval routing | INVOICE | Approval rules match on materialitytier (Low→auto/Senior, Medium→Manager, High→Controller) | packages/finance/finbase/finbased/prisma/seed.ts (approval-rule seed); packages/finance/invoice/invoicea/services/invoice/invoiceapproval.service.ts |
The classifier transform is registered as a service (INV-MATERIALITY) wired to both invoice BPDs at sequence 10 — see packages/finance/finbase/finbased/prisma/migrations/manual/20260604_materiality_classification_transform.sql.
5. Admin UI (frontend is in a separate repo)
◯ The materiality admin UI is frontend-only and lives in the blitz-ui repo, not in this backend, so it is not present in blitz/src. What exists here is the backend:
| Surface | Path | Repo | Notes |
|---|---|---|---|
| Canonical CRUD API | /materialityconfigs (finbase) | blitz (this repo) | packages/finance/finbase/finbasea/controllers/finbase/v1/materialityconfig.controller.ts — reads/writes the canonical finbase table |
| Legacy CRUD API | /materialityconfig (invoice) | blitz (this repo) | ⚠️ Bound to the deprecated invoice fork via @blitz/invoiced; retire in favour of the finbase endpoint |
| Shared list component | settings/materiality | blitz-ui (other repo) | MaterialityConfigList.vue — shared across modules |
| Per-module entry | admin/materiality | blitz-ui (other repo) | maps module → domain (applicationcode) |
⚠️ Branch feat/materiality-standardization is FE-only (blitz-ui) — it standardizes the admin surfaces (shared list + per-module entry, module→domain mapping); it does not change the backend table or the domain-axis contract described above.
6. What materiality is NOT
| Materiality is… | Materiality is NOT… |
|---|---|
| Standalone config (its own table + CRUD) | ✅ Part of the BPD orchestrator |
| A classifier (produces a tier label) | A gate (it does not block or fail anything) |
| Orthogonal to validation | The same as a validation check |
| An amount-band policy | An anomaly score (that is a separate signal on the same rows) |
7. Facts flagged for verification
- ◯ No checked-in
RECONseed script. TheCASHandJOURNALseeds live underscripts/, and both reference RECON as "already seeded" / "mirrors the RECON rows," but noseed-recon-materialityconfig.sql(or equivalent RECON insert) was found in the repo. RECON bands may be seeded manually/out-of-band; confirm before relying on them on a fresh DB. - ◯ "~5 consumers" is approximate. Invoice contributes two logical consumers (classifier + approval routing) that share the same domain; collections/recon/journal each contribute one assess path. Counting read-time vs freeze-time separately would raise the number.
- ⚠️ Tier vocabulary drift: canonical seeds use
Critical/High/Medium/Low, but the invoice classifier's inline fallback usesLow/Medium/High/CFO. Seeded INVOICE rows should define the authoritative bands; theCFOlabel only appears in the fallback.
Related
Revision history
| Version | Date | Author | Change |
|---|---|---|---|
| 1.0 | 2026-09-15 | Platform Engineering | Initial draft — canonical table, domain axis (CASH serves cashapp+collections), tier computation, consumers, admin UI. |