Files
accounted/lib/salary/agi/kontrollera-schemas.ts
T
f266c386f3 chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers

Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n
namespaces and 4 unused dependencies; fold byte-identical helper copies
into one canonical home each (lib/utils chunk/sleep/utcDateStamp,
lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format,
lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body +
v1ValidationError rolled out to ~55 v1 routes, booking-template schemas).

No behaviour change: v1 bodies and status codes, MCP tool schemas, DB
writes and money math are untouched. Naive ore rounding was deliberately
not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list
of things left alone on purpose.

tsc, lint, 19588 unit tests and check:guards green; antipattern baseline
ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(transactions): import RawTransaction from @/types after the ingest re-export removal

CI's type ratchet (check:types, full tsconfig) caught the one test file
that still imported the type through lib/transactions/ingest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 11:51:16 +02:00

102 lines
3.9 KiB
TypeScript

import { z } from 'zod'
/**
* Zod schemas for Skatteverket AGI pre-flight kontrollera endpoints.
* Matches the v1.7 §7 (HU) and §8 (IU) JSON spec exactly, with strict()
* to reject unknown properties: without this guard, a caller could inject
* fields like agRegistreradId or personnummer overrides into the payload
* we forward verbatim to SKV.
*
* Conventions used by Skatteverket:
* - IDENTITET: 12 digits (orgnr prefixed "16" + 10 digits, or personnummer YYYYMMDDXXXX)
* - redovisningsPeriod: YYYYMM
* - amount fields: integer SEK (no decimals)
* - boolean kryss fields: true/false (JSON): SKV converts to <FK>1</FK> in XML
*/
// 12-digit IDENTITET pattern. Slightly looser than the XSD regex used in
// xml-generator.ts (we don't re-validate samordningsnummer arithmetic here)
// because SKV will reject malformed values on its end with a clearer
// felmeddelande than we can surface: what matters here is that we don't
// forward an obviously bogus or oversized string.
const IDENTITET = z
.string()
.regex(/^\d{12}$/, 'Förväntat 12-siffrigt IDENTITET (orgnr eller personnummer).')
const REDOVISNINGSPERIOD = z
.string()
.regex(/^\d{6}$/, 'Förväntat YYYYMM.')
// First period the API accepts; same constraint as xml-generator.
.refine((s) => Number.parseInt(s, 10) >= 201807, {
message: 'Redovisningsperioden är tidigare än 201807 (AGI API minimum).',
})
// SEK amount as non-negative integer with a sanity cap matching SKV's
// internal BELOPP10 (10-digit) ceiling. Bigger values are rejected locally
// rather than forwarded.
const AMOUNT = z.number().int().nonnegative().max(9_999_999_999)
const SPEC_NUMBER = z.number().int().min(1).max(999_999_999)
export const AGIKontrolleraHUSchema = z
.object({
agRegistreradId: IDENTITET,
redovisningsPeriod: REDOVISNINGSPERIOD,
summaSkatteavdr: AMOUNT.optional(),
summaArbAvgSlf: AMOUNT.optional(),
totalSjuklonekostnad: AMOUNT.optional(),
})
.strict()
export const AGIKontrolleraIUSchema = z
.object({
agRegistreradId: IDENTITET,
redovisningsPeriod: REDOVISNINGSPERIOD,
betalningsmottagarId: IDENTITET,
specifikationsnummer: SPEC_NUMBER,
// Cash + tax: FK011 / FK001
kontantErsattningUlagAG: AMOUNT.optional(),
avdrPrelSkatt: AMOUNT.optional(),
// Benefit amounts (UlagAG variants)
skatteplBilformanUlagAG: AMOUNT.optional(), // FK013
drivmVidBilformanUlagAG: AMOUNT.optional(), // FK018
kostformanUlagAG: AMOUNT.optional(), // FK015
skatteplOvrigaFormanerUlagAG: AMOUNT.optional(), // FK012
// Housing benefit KRYSS flags
bostadsformanSmahusUlagAG: z.boolean().optional(), // FK041
bostadsformanEjSmahusUlagAG: z.boolean().optional(), // FK043
// F-skatt / ej UlagSA variants
kontantErsattningEjUlagSA: AMOUNT.optional(), // FK131
skatteplBilformanEjUlagSA: AMOUNT.optional(), // FK133
drivmVidBilformanEjUlagSA: AMOUNT.optional(), // FK134
kostformanEjUlagSA: AMOUNT.optional(), // FK139
skatteplOvrigaFormanerEjUlagSA: AMOUNT.optional(), // FK132
bostadsformanSmahusEjUlagSA: z.boolean().optional(), // FK137
bostadsformanEjSmahusEjUlagSA: z.boolean().optional(),// FK138
// Flags
formanHarJusterats: z.boolean().optional(), // FK048
forstaAnstalld: z.boolean().optional(), // FK062
vaxaStod: z.boolean().optional(), // FK063
borttag: z.boolean().optional(), // FK205
})
.strict()
.refine(
(iu) => !(iu.forstaAnstalld === true && iu.vaxaStod === true),
{
message: 'FK062 (ForstaAnstalld) och FK063 (VaxaStod) är ömsesidigt uteslutande.',
path: ['vaxaStod'],
},
)
/**
* Hard cap on the raw JSON body for kontrollera endpoints. Even a fully
* legal IU is < 4 KB serialised; 64 KB is a generous safety margin that
* still trivially rejects pathological inputs before they reach Zod.
*/
export const AGI_KONTROLLERA_MAX_BYTES = 64 * 1024