* fix: correct INK2R SRU sign convention and field ordering Cost fields (7511-7528) were negated to negative values, but Skatteverket expects positive values on the INK2R form. Updated the engine to keep debit-normal income statement balances positive and adjusted all result calculations accordingly. Also switched SRU field output from Object.keys() to canonical Skatteverket ordering arrays. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — deduplicate code arrays, add disposition test Remove duplicate ASSET_CODES/EQUITY_LIABILITY_CODES arrays from the engine and import the canonical arrays from types.ts. Add test coverage for bokslutsdispositioner fields (7524, 7525, 7419) to exercise the new subtraction sign path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
341 lines
12 KiB
TypeScript
341 lines
12 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
generateSRUSubmission,
|
|
validateBlanketterSru,
|
|
getZipFilename,
|
|
} from '../sru-generator'
|
|
import type { INK2Declaration } from '../types'
|
|
|
|
function makeDeclaration(overrides?: Partial<INK2Declaration>): INK2Declaration {
|
|
const defaultInk2r = {
|
|
'7201': 0, '7202': 0, '7214': 0, '7215': 50000, '7216': 0, '7217': 0,
|
|
'7230': 0, '7231': 0, '7233': 0, '7232': 0, '7234': 0, '7235': 0,
|
|
'7241': 0, '7242': 0, '7243': 0, '7244': 0, '7245': 0, '7246': 0,
|
|
'7251': 25000, '7252': 0, '7261': 0, '7262': 0, '7263': 0,
|
|
'7270': 0, '7271': 0, '7281': 100000,
|
|
'7301': 50000, '7302': 20000,
|
|
'7321': 0, '7322': 0, '7323': 0,
|
|
'7331': 0, '7332': 0, '7333': 0,
|
|
'7350': 0, '7351': 0, '7352': 0, '7353': 0, '7354': 0,
|
|
'7360': 0, '7361': 0, '7362': 0, '7363': 0, '7364': 0,
|
|
'7365': 30000, '7366': 0, '7367': 0, '7369': 70000, '7368': 0, '7370': 0,
|
|
'7410': 500000, '7411': 0, '7412': 0, '7413': 0,
|
|
'7511': 0, '7512': 0, '7513': 100000, '7514': 80000, '7515': 10000, '7516': 0, '7517': 5000,
|
|
'7414': 0, '7415': 0, '7423': 0, '7416': 0, '7417': 0,
|
|
'7521': 0, '7522': 3000,
|
|
'7524': 0, '7419': 0, '7420': 0, '7525': 0, '7421': 0, '7422': 0,
|
|
'7528': 0,
|
|
'7450': 302000, '7550': 0,
|
|
} as INK2Declaration['ink2r']
|
|
|
|
return {
|
|
fiscalYear: {
|
|
id: 'period-1',
|
|
name: 'Räkenskapsår 2025',
|
|
start: '2025-01-01',
|
|
end: '2025-12-31',
|
|
isClosed: true,
|
|
},
|
|
ink2: {
|
|
'7011': '20250101',
|
|
'7012': '20251231',
|
|
'7113': 302000,
|
|
'7114': 0,
|
|
},
|
|
ink2r: defaultInk2r,
|
|
ink2s: {
|
|
'7011': '20250101',
|
|
'7012': '20251231',
|
|
'7650': 302000,
|
|
'7750': 0,
|
|
'7651': 0,
|
|
'8020': 302000,
|
|
'8021': 0,
|
|
},
|
|
breakdown: {} as INK2Declaration['breakdown'],
|
|
totals: {
|
|
totalAssets: 175000,
|
|
totalEquityLiabilities: 175000,
|
|
operatingResult: 305000,
|
|
resultAfterFinancial: 302000,
|
|
},
|
|
companyInfo: {
|
|
companyName: 'Test AB',
|
|
orgNumber: '556677-8899',
|
|
addressLine1: 'Testgatan 1',
|
|
postalCode: '11122',
|
|
city: 'Stockholm',
|
|
email: 'test@example.com',
|
|
},
|
|
warnings: [],
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('INK2 SRU Generator', () => {
|
|
describe('generateSRUSubmission', () => {
|
|
it('produces valid INFO.SRU', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
|
|
expect(submission.infoSru).toContain('#DATABESKRIVNING_START')
|
|
expect(submission.infoSru).toContain('#PRODUKT SRU')
|
|
expect(submission.infoSru).toContain('#FILNAMN BLANKETTER.SRU')
|
|
expect(submission.infoSru).toContain('#DATABESKRIVNING_SLUT')
|
|
expect(submission.infoSru).toContain('#MEDIELEV_START')
|
|
expect(submission.infoSru).toContain('#ORGNR 165566778899')
|
|
expect(submission.infoSru).toContain('#NAMN Test AB')
|
|
expect(submission.infoSru).toContain('#POSTNR 11122')
|
|
expect(submission.infoSru).toContain('#POSTORT Stockholm')
|
|
expect(submission.infoSru).toContain('#MEDIELEV_SLUT')
|
|
})
|
|
|
|
it('formats org number as 12-digit with century prefix 16', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
|
|
// In INFO.SRU
|
|
expect(submission.infoSru).toContain('#ORGNR 165566778899')
|
|
// In BLANKETTER.SRU
|
|
expect(submission.blanketterSru).toContain('#IDENTITET 165566778899')
|
|
})
|
|
|
|
it('handles org number already in 12-digit format', () => {
|
|
const declaration = makeDeclaration({
|
|
companyInfo: {
|
|
companyName: 'Test AB',
|
|
orgNumber: '165566778899',
|
|
addressLine1: null,
|
|
postalCode: '11122',
|
|
city: 'Stockholm',
|
|
email: null,
|
|
},
|
|
})
|
|
const submission = generateSRUSubmission(declaration)
|
|
expect(submission.infoSru).toContain('#ORGNR 165566778899')
|
|
})
|
|
|
|
it('produces three blankett blocks in BLANKETTER.SRU', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
|
|
expect(submission.blanketterSru).toContain('#BLANKETT INK2-2025P4')
|
|
expect(submission.blanketterSru).toContain('#BLANKETT INK2R-2025P4')
|
|
expect(submission.blanketterSru).toContain('#BLANKETT INK2S-2025P4')
|
|
expect(submission.blanketterSru).toContain('#FIL_SLUT')
|
|
})
|
|
|
|
it('validates the generated BLANKETTER.SRU', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
const validation = validateBlanketterSru(submission.blanketterSru)
|
|
expect(validation.isValid).toBe(true)
|
|
expect(validation.errors).toEqual([])
|
|
})
|
|
|
|
it('uses correct period suffix for calendar year', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
// Dec = P4
|
|
expect(submission.blanketterSru).toContain('INK2-2025P4')
|
|
})
|
|
|
|
it('uses P1 suffix for fiscal year ending in Jan-Apr', () => {
|
|
const declaration = makeDeclaration({
|
|
fiscalYear: {
|
|
id: 'p1',
|
|
name: 'FY',
|
|
start: '2024-05-01',
|
|
end: '2025-04-30',
|
|
isClosed: true,
|
|
},
|
|
})
|
|
const submission = generateSRUSubmission(declaration)
|
|
expect(submission.blanketterSru).toContain('INK2-2025P1')
|
|
})
|
|
|
|
it('uses P2 suffix for fiscal year ending in May-Aug', () => {
|
|
const declaration = makeDeclaration({
|
|
fiscalYear: {
|
|
id: 'p2',
|
|
name: 'FY',
|
|
start: '2024-09-01',
|
|
end: '2025-06-30',
|
|
isClosed: true,
|
|
},
|
|
})
|
|
const submission = generateSRUSubmission(declaration)
|
|
expect(submission.blanketterSru).toContain('INK2-2025P2')
|
|
})
|
|
|
|
it('excludes zero-value #UPPGIFT lines', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
|
|
// 7201 is 0, should not appear
|
|
const ink2rBlock = extractBlock(submission.blanketterSru, 'INK2R')
|
|
expect(ink2rBlock).not.toContain('#UPPGIFT 7201')
|
|
// 7215 is 50000, should appear
|
|
expect(ink2rBlock).toContain('#UPPGIFT 7215 50000')
|
|
})
|
|
|
|
it('includes fiscal year fields 7011 and 7012 in each block', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
|
|
// Each block should have 7011 and 7012
|
|
const blocks = [
|
|
extractBlock(submission.blanketterSru, 'INK2-'),
|
|
extractBlock(submission.blanketterSru, 'INK2R'),
|
|
extractBlock(submission.blanketterSru, 'INK2S'),
|
|
]
|
|
for (const block of blocks) {
|
|
expect(block).toContain('#UPPGIFT 7011 20250101')
|
|
expect(block).toContain('#UPPGIFT 7012 20251231')
|
|
}
|
|
})
|
|
|
|
it('each blankett block has #IDENTITET and #NAMN', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
|
|
const blocks = submission.blanketterSru.split('#BLANKETT ').slice(1)
|
|
expect(blocks).toHaveLength(3)
|
|
|
|
for (const block of blocks) {
|
|
expect(block).toContain('#IDENTITET 165566778899')
|
|
expect(block).toContain('#NAMN Test AB')
|
|
expect(block).toContain('#BLANKETTSLUT')
|
|
}
|
|
})
|
|
|
|
it('reports cost fields as positive values per Skatteverket convention', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
|
|
const ink2rBlock = extractBlock(submission.blanketterSru, 'INK2R')
|
|
expect(ink2rBlock).toContain('#UPPGIFT 7513 100000')
|
|
expect(ink2rBlock).toContain('#UPPGIFT 7522 3000')
|
|
})
|
|
|
|
it('handles bokslutsdispositioner fields with correct sign', () => {
|
|
const declaration = makeDeclaration({
|
|
ink2r: {
|
|
...makeDeclaration().ink2r,
|
|
'7524': 50000, // Lämnade koncernbidrag (debit-normal cost, positive)
|
|
'7525': 30000, // Avsättning periodiseringsfond (debit-normal cost, positive)
|
|
'7419': 20000, // Mottagna koncernbidrag (credit-normal income, positive)
|
|
},
|
|
})
|
|
const submission = generateSRUSubmission(declaration)
|
|
const ink2rBlock = extractBlock(submission.blanketterSru, 'INK2R')
|
|
|
|
// All bokslutsdispositioner fields are positive in the SRU output
|
|
expect(ink2rBlock).toContain('#UPPGIFT 7524 50000')
|
|
expect(ink2rBlock).toContain('#UPPGIFT 7525 30000')
|
|
expect(ink2rBlock).toContain('#UPPGIFT 7419 20000')
|
|
})
|
|
|
|
it('includes INK2S with överskott/underskott', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
|
|
const ink2sBlock = extractBlock(submission.blanketterSru, 'INK2S')
|
|
expect(ink2sBlock).toContain('#UPPGIFT 7650 302000')
|
|
expect(ink2sBlock).toContain('#UPPGIFT 8020 302000')
|
|
// 7750 and 8021 are 0, should not appear
|
|
expect(ink2sBlock).not.toContain('#UPPGIFT 7750')
|
|
expect(ink2sBlock).not.toContain('#UPPGIFT 8021')
|
|
})
|
|
|
|
it('INK2 block includes överskott', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
|
|
const ink2Block = extractBlock(submission.blanketterSru, 'INK2-')
|
|
expect(ink2Block).toContain('#UPPGIFT 7113 302000')
|
|
// 7114 (underskott) is 0, should not appear
|
|
expect(ink2Block).not.toContain('#UPPGIFT 7114')
|
|
})
|
|
|
|
it('uses CRLF line endings', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
expect(submission.infoSru).toContain('\r\n')
|
|
expect(submission.blanketterSru).toContain('\r\n')
|
|
})
|
|
|
|
it('sanitizes # from company name', () => {
|
|
const declaration = makeDeclaration({
|
|
companyInfo: {
|
|
companyName: 'Test #1 AB',
|
|
orgNumber: '556677-8899',
|
|
addressLine1: null,
|
|
postalCode: '11122',
|
|
city: 'Stockholm',
|
|
email: null,
|
|
},
|
|
})
|
|
const submission = generateSRUSubmission(declaration)
|
|
expect(submission.infoSru).toContain('#NAMN Test 1 AB')
|
|
})
|
|
})
|
|
|
|
describe('validateBlanketterSru', () => {
|
|
it('validates a correct BLANKETTER.SRU', () => {
|
|
const declaration = makeDeclaration()
|
|
const submission = generateSRUSubmission(declaration)
|
|
const result = validateBlanketterSru(submission.blanketterSru)
|
|
expect(result.isValid).toBe(true)
|
|
})
|
|
|
|
it('detects missing INK2R block', () => {
|
|
const result = validateBlanketterSru(
|
|
'#BLANKETT INK2-2025P4\r\n#IDENTITET 165566778899 20250101 100000\r\n#NAMN Test\r\n#BLANKETTSLUT\r\n' +
|
|
'#BLANKETT INK2S-2025P4\r\n#IDENTITET 165566778899 20250101 100002\r\n#NAMN Test\r\n#BLANKETTSLUT\r\n' +
|
|
'#FIL_SLUT\r\n'
|
|
)
|
|
expect(result.isValid).toBe(false)
|
|
expect(result.errors).toContain('Missing INK2R blankett block')
|
|
})
|
|
|
|
it('detects missing #FIL_SLUT', () => {
|
|
const result = validateBlanketterSru(
|
|
'#BLANKETT INK2-2025P4\r\n#IDENTITET x\r\n#NAMN T\r\n#BLANKETTSLUT\r\n' +
|
|
'#BLANKETT INK2R-2025P4\r\n#IDENTITET x\r\n#NAMN T\r\n#BLANKETTSLUT\r\n' +
|
|
'#BLANKETT INK2S-2025P4\r\n#IDENTITET x\r\n#NAMN T\r\n#BLANKETTSLUT\r\n'
|
|
)
|
|
expect(result.isValid).toBe(false)
|
|
expect(result.errors).toContain('Missing #FIL_SLUT terminator')
|
|
})
|
|
})
|
|
|
|
describe('getZipFilename', () => {
|
|
it('returns correct filename format', () => {
|
|
const declaration = makeDeclaration()
|
|
expect(getZipFilename(declaration)).toBe('INK2_SRU_5566778899_2025.zip')
|
|
})
|
|
|
|
it('handles missing org number', () => {
|
|
const declaration = makeDeclaration({
|
|
companyInfo: {
|
|
companyName: 'Test AB',
|
|
orgNumber: null,
|
|
addressLine1: null,
|
|
postalCode: null,
|
|
city: null,
|
|
email: null,
|
|
},
|
|
})
|
|
expect(getZipFilename(declaration)).toBe('INK2_SRU_unknown_2025.zip')
|
|
})
|
|
})
|
|
})
|
|
|
|
/** Extract a specific blankett block from BLANKETTER.SRU content */
|
|
function extractBlock(content: string, blockPrefix: string): string {
|
|
const regex = new RegExp(`#BLANKETT ${blockPrefix}[^\\r\\n]*[\\s\\S]*?#BLANKETTSLUT`)
|
|
const match = content.match(regex)
|
|
return match ? match[0] : ''
|
|
}
|