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.
This commit is contained in:
Mattsson
2026-08-13 15:39:49 +02:00
committed by GitHub
parent a82f126031
commit 0c3864cae5
4 changed files with 92 additions and 20 deletions
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest'
import { generateKU10Xml } from '@/lib/salary/ku/ku10-generator'
function companyFixture(orgNumber: string) {
return {
orgNumber,
companyName: 'Testbolaget AB',
year: 2026,
contactName: 'Test Testsson',
contactPhone: '0700000000',
contactEmail: 'test@example.com',
}
}
/**
* Skatteverket KU schema 12.0 defines OrganisationsnummerTYPE as a 12-digit
* value with prefix `16`. UppgiftslamnarId is the exact FK201 element name.
* The XSD intentionally does not validate the identity's check digit.
* Source: https://www.skatteverket.se/foretag/skatterochavdrag/kontrolluppgifter/testtjanstochtekniskbeskrivning.4.233f91f71260075abe8800073614.html
*/
describe('generateKU10Xml: Skatteverket KU 12.0 identity contract', () => {
it.each(['5560125790', '556012-5790', '556012 5790', '165560125790'])(
'emits %s as a 12-digit organisation identity',
(orgNumber) => {
const xml = generateKU10Xml(companyFixture(orgNumber), [])
expect(xml).toContain('<Organisationsnummer>165560125790</Organisationsnummer>')
expect(xml).toContain('<UppgiftslamnarId>165560125790</UppgiftslamnarId>')
}
)
it('uses the schema element name instead of the misspelled legacy tag', () => {
const xml = generateKU10Xml(companyFixture('5560125790'), [])
expect(xml).not.toContain('UppgijftslamnareId')
expect(xml).not.toContain('UppgiftslamnareId')
expect(xml).toContain('UppgiftslamnarId')
})
it.each([
['', 'missing'],
['55601', 'too short'],
['5560125790x', 'contains a stray character'],
['195560125790', 'has a non-organisation century prefix'],
['1100125790', 'has an invalid organisation-number group digit'],
])('rejects %s when it is %s', (orgNumber) => {
expect(() => generateKU10Xml(companyFixture(orgNumber), [])).toThrow(
/organisationsnumret måste innehålla 10 siffror eller 12 siffror med prefixet 16/
)
})
it('does not add check-digit validation that the KU schema does not perform', () => {
const xml = generateKU10Xml(companyFixture('5560125791'), [])
expect(xml).toContain('<Organisationsnummer>165560125791</Organisationsnummer>')
})
})
+25 -5
View File
@@ -40,6 +40,29 @@ export interface KU10CompanyData {
contactEmail: string
}
const KU_ORG_NUMBER_PATTERN = /^16\d{2}[2-9]\d{7}$/
/**
* Normalize the employer identity to the 12-digit format required by the KU
* schema. Skatteverket's KU 12.0 XSD defines OrganisationsnummerTYPE with the
* `16` prefix and does not validate the check digit.
*
* Source: https://www.skatteverket.se/foretag/skatterochavdrag/kontrolluppgifter/testtjanstochtekniskbeskrivning.4.233f91f71260075abe8800073614.html
*/
function normalizeKUOrgNumber(raw: string): string {
const cleaned = stripOrgNumberFormatting(raw)
const normalized = /^\d{10}$/.test(cleaned) ? `16${cleaned}` : cleaned
if (!KU_ORG_NUMBER_PATTERN.test(normalized)) {
throw new Error(
'KU10 kan inte genereras: organisationsnumret måste innehålla 10 siffror ' +
'eller 12 siffror med prefixet 16.'
)
}
return normalized
}
/**
* Generate KU10 XML for all employees for a calendar year.
*
@@ -50,10 +73,7 @@ export function generateKU10Xml(
employees: KU10EmployeeData[]
): string {
const lines: string[] = []
// Shared rule (lib/invariants/org-number.ts). The previous
// `replace('-', '')` removed only the FIRST hyphen and left spaces intact, so
// an org number entered as "556012 5790" reached Skatteverket with a space in it.
const orgNr = stripOrgNumberFormatting(company.orgNumber)
const orgNr = normalizeKUOrgNumber(company.orgNumber)
lines.push('<?xml version="1.0" encoding="UTF-8"?>')
lines.push('<Skatteverket xmlns="http://xmls.skatteverket.se/se/skatteverket/ai/instans/infoForBeskworksgivku/1.0"')
@@ -73,7 +93,7 @@ export function generateKU10Xml(
// Blankettgemensamt
lines.push(' <Blankettgemensamt>')
lines.push(` <Uppgiftslamnare>`)
lines.push(` <UppgijftslamnareId>${orgNr}</UppgijftslamnareId>`)
lines.push(` <UppgiftslamnarId>${orgNr}</UppgiftslamnarId>`)
lines.push(` <NamnUppgiftslamnare>${escapeXml(company.companyName)}</NamnUppgiftslamnare>`)
lines.push(` </Uppgiftslamnare>`)
lines.push(' </Blankettgemensamt>')