Files
accounted/lib/salary/payment/__tests__/bg-lb-generator.test.ts
T
Mattsson 7e81f661b2 Add/skv salary agi (#423)
* feat: add Bankgirot LB-fil support for salary payments and tax payments

- Implemented `generateBgLb` for salary batch payments, producing opening, payment, and closing records.
- Added tests for `generateBgLb` to ensure correct record generation and validation.
- Created `generateBankgiroPaymentBgLb` for single tax payments to Skatteverket, including validation and formatting.
- Added tests for `generateBankgiroPaymentBgLb` to verify record structure and data integrity.
- Introduced `generateSkattekontoOcr` for generating valid OCR references for Skattekonto payments, with tests for various input formats.
- Updated database schema to track payment file formats and timestamps for salary runs and AGI declarations.
- Created a new table for logging salary payslip deliveries to ensure compliance with audit requirements.

* feat: add write permission check and company ID validation for payment file generation

* feat: add write permission check for salary payment file generation
2026-05-09 12:41:16 +02:00

153 lines
6.1 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { generateBgLb } from '../bg-lb-generator'
import type { BgLbCompanyData, BgLbEmployee, BgLbOptions } from '../bg-lb-generator'
const company: BgLbCompanyData = {
name: 'Acme AB',
senderBankgiro: '123-4567',
}
const baseOptions: BgLbOptions = {
paymentDate: '2026-04-25',
periodLabel: '2026-04',
}
describe('generateBgLb', () => {
it('produces a file with opening, payment, and closing records', () => {
const employees: BgLbEmployee[] = [
{
name: 'Anna Andersson',
clearingNumber: '6000',
bankAccountNumber: '1234567',
netSalary: 25000,
},
{
name: 'Bo Bergström',
clearingNumber: '6000',
bankAccountNumber: '7654321',
netSalary: 30500.5,
},
]
const result = generateBgLb(company, employees, baseOptions)
const lines = result.content.split('\r\n').filter((l) => l.length > 0)
expect(lines).toHaveLength(4) // 1 opening + 2 payments + 1 closing
expect(lines[0]).toMatch(/^11/)
expect(lines[1]).toMatch(/^54/)
expect(lines[2]).toMatch(/^54/)
expect(lines[3]).toMatch(/^29/)
expect(result.recordCount).toBe(2)
expect(result.totalAmount).toBe(55500.5)
})
it('writes records exactly 80 characters wide', () => {
const employees: BgLbEmployee[] = [
{ name: 'Cecilia Carlsson', clearingNumber: '6000', bankAccountNumber: '1234567', netSalary: 28000 },
]
const result = generateBgLb(company, employees, baseOptions)
const lines = result.content.split('\r\n').filter((l) => l.length > 0)
for (const line of lines) {
expect(line.length).toBe(80)
}
})
it('encodes amounts in öre (no decimal)', () => {
const employees: BgLbEmployee[] = [
{ name: 'Test', clearingNumber: '6000', bankAccountNumber: '1234567', netSalary: 12345.67 },
]
const result = generateBgLb(company, employees, baseOptions)
const paymentLine = result.content.split('\r\n')[1]
// Amount field for TK 54 is positions 42-53 (12 chars, zero-padded)
const amountField = paymentLine.slice(41, 53)
expect(amountField).toBe('000001234567') // 12345.67 SEK = 1234567 öre
})
it('handles 5-digit Swedbank clearings by shifting the 5th digit into the account', () => {
const employees: BgLbEmployee[] = [
{ name: 'Swedbank', clearingNumber: '83271', bankAccountNumber: '123456789', netSalary: 1000 },
]
const result = generateBgLb(company, employees, baseOptions)
const paymentLine = result.content.split('\r\n')[1]
// Pos 3-6 = clearing (4 digits)
expect(paymentLine.slice(2, 6)).toBe('8327')
// Pos 7-16 = account (10 digits): 5th clearing digit "1" + account "123456789"
expect(paymentLine.slice(6, 16)).toBe('1123456789')
})
it('rejects invalid bankgiro number', () => {
expect(() =>
generateBgLb({ ...company, senderBankgiro: 'invalid' }, [], baseOptions)
).toThrow(/Ogiltigt bankgironummer/)
})
it('rejects 5-digit clearing not starting with 8', () => {
const employees: BgLbEmployee[] = [
{ name: 'X', clearingNumber: '90001', bankAccountNumber: '1234567', netSalary: 100 },
]
expect(() => generateBgLb(company, employees, baseOptions)).toThrow(/clearing/)
})
it('skips employees with zero or negative net salary', () => {
const employees: BgLbEmployee[] = [
{ name: 'A', clearingNumber: '6000', bankAccountNumber: '1234567', netSalary: 25000 },
{ name: 'B', clearingNumber: '6000', bankAccountNumber: '7654321', netSalary: 0 },
{ name: 'C', clearingNumber: '6000', bankAccountNumber: '1111111', netSalary: -500 },
]
const result = generateBgLb(company, employees, baseOptions)
expect(result.recordCount).toBe(1)
expect(result.totalAmount).toBe(25000)
})
it('opening record contains LEVERANTÖRSBETALNINGAR and LEVE markers', () => {
const result = generateBgLb(company, [
{ name: 'X', clearingNumber: '6000', bankAccountNumber: '1234567', netSalary: 100 },
], baseOptions)
const opening = result.content.split('\r\n')[0]
expect(opening.slice(18, 40)).toBe('LEVERANTÖRSBETALNINGAR')
expect(opening.slice(40, 44)).toBe('LEVE')
})
it('closing record sums total amount in öre across all payments', () => {
const employees: BgLbEmployee[] = [
{ name: 'A', clearingNumber: '6000', bankAccountNumber: '1234567', netSalary: 100 },
{ name: 'B', clearingNumber: '6000', bankAccountNumber: '7654321', netSalary: 250.5 },
]
const result = generateBgLb(company, employees, baseOptions)
const lines = result.content.split('\r\n').filter((l) => l)
const closing = lines[lines.length - 1]
expect(closing.startsWith('29')).toBe(true)
// Pos 21-32 = total amount in öre (12 digits)
expect(closing.slice(20, 32)).toBe('000000035050') // 350.50 SEK = 35050 öre
})
it('encodes payment date as YYMMDD on opening and payment records', () => {
const result = generateBgLb(company, [
{ name: 'X', clearingNumber: '6000', bankAccountNumber: '1234567', netSalary: 100 },
], { ...baseOptions, paymentDate: '2026-12-31' })
const lines = result.content.split('\r\n')
// Opening: pos 45-50 = payment date YYMMDD
expect(lines[0].slice(44, 50)).toBe('261231')
// Payment: pos 54-59 = payment date YYMMDD
expect(lines[1].slice(53, 59)).toBe('261231')
})
it('truncates over-long employee names safely', () => {
const longName = 'A'.repeat(50)
const result = generateBgLb(company, [
{ name: longName, clearingNumber: '6000', bankAccountNumber: '1234567', netSalary: 100 },
], baseOptions)
const paymentLine = result.content.split('\r\n')[1]
// Pos 17-41 = receiver name (25 chars)
expect(paymentLine.slice(16, 41)).toBe('A'.repeat(25))
})
it('preserves Swedish characters å ä ö in receiver name', () => {
const result = generateBgLb(company, [
{ name: 'Åke Östberg', clearingNumber: '6000', bankAccountNumber: '1234567', netSalary: 100 },
], baseOptions)
const paymentLine = result.content.split('\r\n')[1]
expect(paymentLine.slice(16, 41).trimEnd()).toBe('Åke Östberg')
})
})