Skip to content
Last updated: Sep 25, 2026

Materiality Tiers — One Config, Many Domains ​

OwnerClassificationVersionEffectiveNext reviewStatus
Platform EngineeringInternal1.02026-09-152026-12-15Draft

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.

TableSchemaStatusEvidence
materialityconfigfinbase✅ Canonical — all engines read thispackages/finance/finbase/finbased/prisma/models/finbase.prisma (model materialityconfig)
materialityconfiginvoice⚠️ Deprecated fork — narrower, no domain axis; kept only for the legacy invoice CRUD controllerpackages/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:

ColumnMeaning
applicationcodeDomain scope — CASH / INVOICE / JOURNAL / RECON (null = global default)
tiernameCritical / High / Medium / Low (invoice also uses CFO in its inline fallback)
minamount / maxamountAbsolute band (maxamount NULL = open-ended top tier)
percentRelative threshold (e.g. % of portfolio/period total); NULL = no relative test
basisPeriodTotal / AccountBalance / EntityTotal / Portfolio — what percent is relative to
andorOr (default) / And — how the absolute and relative tests combine
currency, businessentityid, isactiveScope + 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)ServesBasis conventionSeed
CASH✅ cashapp AND collections (one domain, two modules)Portfolio (% of total past-due AR) + absolute floorsscripts/collections/seed-cash-materialityconfig.sql
INVOICEInvoice classificationabsolute bands (account currency)seeded rows read by classifier; inline fallback in the razor
JOURNALJournal-entry assessmentEntityTotal, absolute spine (percent NULL in v1)scripts/journal/seed-journal-materialityconfig.sql
RECONPeriod / account reconciliationAccountBalance + % 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 exposuretotal open past-due portfoliopackages/finance/cash/casha/services/cash/collectionsfinni.service.ts
RECONabs(glbalance) per accountperiod totalzhttp/reconciliation/assessperiodintelligence/assessperiodintelligence.ts
JOURNALentry amountentity/period total (percent NULL in v1)zhttp/journal/jvvalidations/assessjournalintelligence/assessjournalintelligence.ts
INVOICEinvoice 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 collectionsfinni query so the worklist segmentation chips reflect current exposure.
  • Freeze-time (assess workflows): the assess*intelligence workflows export finbase.materialityconfig into DuckDB, compute materialitytier in 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):

#ConsumerDomainWhat it doesFile
1Invoice materiality classifierINVOICEWrites materialitytier onto invoiceprocessingmetrics; feeds approver-tier routing (#170) + detail-page badge (#171/#172) + auto-clear gatezhttp/invoice/processtransformations/materialityclassification/materialityclassification.ts
2Collections assess / Finni worklistCASHSegments the collections worklist ("which customers matter")packages/finance/cash/casha/services/cash/collectionsfinni.service.ts; packages/finance/cash/cashwf/src/workflows/assesscollectionintelligence/workflow.ts
3Recon period intelligenceRECONTags each account/period with a tier alongside anomaly scoringzhttp/reconciliation/assessperiodintelligence/assessperiodintelligence.ts; packages/finance/reconciliation/reconciliationwf/src/workflows/assessperiodintelligence/workflow.ts
4Journal entry intelligenceJOURNALTags each JV with a materiality tier for topside-entry reviewzhttp/journal/jvvalidations/assessjournalintelligence/assessjournalintelligence.ts; packages/finance/journal/journalwf/src/workflows/assessjournalintelligence/workflow.ts
5Invoice approval routingINVOICEApproval 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:

SurfacePathRepoNotes
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 componentsettings/materialityblitz-ui (other repo)MaterialityConfigList.vue — shared across modules
Per-module entryadmin/materialityblitz-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 validationThe same as a validation check
An amount-band policyAn anomaly score (that is a separate signal on the same rows)

7. Facts flagged for verification ​

  • ◯ No checked-in RECON seed script. The CASH and JOURNAL seeds live under scripts/, and both reference RECON as "already seeded" / "mirrors the RECON rows," but no seed-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 uses Low/Medium/High/CFO. Seeded INVOICE rows should define the authoritative bands; the CFO label only appears in the fallback.

Revision history ​

VersionDateAuthorChange
1.02026-09-15Platform EngineeringInitial draft — canonical table, domain axis (CASH serves cashapp+collections), tier computation, consumers, admin UI.

Finaisse Internal — Confidential. Access-restricted; not for external distribution.