Files
accounted/lib/invariants
Mattsson 0c3864cae5 fix(payroll): normalize KU10 organisation numbers (#1583)
Normalize KU10 employer identities to Skatteverket's 12-digit schema format, validate the structural XSD contract, correct FK201's XML element name, and add focused sourced tests. Resolves #1410.
2026-08-13 15:39:49 +02:00
..

lib/invariants

Shared product contracts: the formats and bounds that more than one consumer must agree on, with the reason for each rule recorded next to it.

What belongs here

A rule belongs here only when consumers outside the owning module need the identical invariant to validate input or to generate compatible output.

Our consumers are the web app, the public /api/v1 surface, the MCP server, the SIE importer and exporter, and the Skatteverket-bound generators (AGI, KU10, SRU, iXBRL). When two of those disagree about what a valid value looks like, the disagreement is invisible until a customer's filing fails.

What does not belong here

  • Rules specific to one module. They stay in that module.
  • General helpers. lib/utils.ts, lib/money.ts.
  • Database questions. "Is this account in the company's chart?" is lib/bookkeeping/account-validation.ts, not a format rule.
  • Business rules. "Is this fiscal year open?" is a period question.

Naming

The name must make the owning concept explicit. isAccountNumber, not isValidNumber. Two rules in here share a byte-identical regex (account-number and fiscal-year are both /^\d{4}$/) and mean entirely different things: the names are the only thing keeping a call site honest, so they carry the weight.

Layout

File Owns
org-number.ts Swedish organisationsnummer / personnummer: canonical 10-digit form, Luhn, Skatteverket 12-digit conversion
account-number.ts BAS account number format (4 digits, always a string)
iso-date.ts YYYY-MM-DD shape, plus a real-calendar-date check
fiscal-year.ts Four-digit räkenskapsår key
zod.ts Zod primitives built from the rules above, for API schemas

zod.ts is separate so that consumers which do not use Zod (the MCP server, report generators) can import a rule without pulling the dependency in.

Adding a rule

  1. Write the rule and the reason in the docblock. A rule without a recorded reason gets re-litigated or worked around within a quarter.
  2. Export a pure validator (primitives in, boolean or null out).
  3. Add the Zod primitive to zod.ts if an API surface needs it.
  4. Replace the hand-rolled copies. npm run check:guards tracks how many remain and fails if the count goes up.