import { decryptPersonnummer } from '../personnummer' import { getBranding } from '@/lib/branding/service' import { stripOrgNumberFormatting } from '@/lib/invariants/org-number' /** * KU10 (Kontrolluppgift): Annual employee income statement. * * Per Skatteförfarandelagen 15 kap: Every employer must file KU10 for each * employee by January 31 of the following year. Reports total income, tax * withheld, and benefit values for the calendar year. * * The KU10 is filed electronically via Skatteverket's Filöverföring or API. * XML format follows Skatteverket Teknisk beskrivning for KU. * * Penalties: Late filing = 500 SEK per KU per commenced 5-day period * (max 5,000 SEK per KU or 500,000 SEK total per filing deadline). */ export interface KU10EmployeeData { personnummer: string // Encrypted, will be decrypted specificationNumber: number // FK570 totalGross: number // Ruta 011: Total kontant bruttolön for year totalTax: number // Ruta 001: Total avdragen skatt for year totalAvgifterBasis: number // Ruta 020: Total avgiftsunderlag benefitCar?: number // Ruta 012: Total bilförmån benefitHousing?: number // Ruta 014: Total bostadsförmån benefitMeals?: number // Ruta 015: Total kostförmån benefitOther?: number // Ruta 019: Total övrigt sickDays?: number // Total sjukdagar employmentStart?: string // YYYY-MM-DD employmentEnd?: string // YYYY-MM-DD (if terminated during year) } export interface KU10CompanyData { orgNumber: string companyName: string year: number contactName: string contactPhone: string contactEmail: string } /** * Generate KU10 XML for all employees for a calendar year. * * Per BFL 7 kap: The KU10 file is räkenskapsinformation, retained 7 years. */ export function generateKU10Xml( company: KU10CompanyData, 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) lines.push('') lines.push('') // Avsändare lines.push(' ') lines.push(` ${escapeXml(getBranding().appName.toLowerCase())}`) lines.push(` ${orgNr}`) lines.push(' ') lines.push(` ${escapeXml(company.contactName)}`) lines.push(` ${escapeXml(company.contactPhone)}`) lines.push(` ${escapeXml(company.contactEmail)}`) lines.push(' ') lines.push(' ') // Blankettgemensamt lines.push(' ') lines.push(` `) lines.push(` ${orgNr}`) lines.push(` ${escapeXml(company.companyName)}`) lines.push(` `) lines.push(' ') // Per-employee KU10 for (const emp of employees) { let pnr: string try { pnr = decryptPersonnummer(emp.personnummer) } catch { pnr = '000000000000' } lines.push(' ') lines.push(' ') lines.push(` ${orgNr}`) lines.push(` ${company.year}`) lines.push(' ') lines.push(' ') lines.push(' ') // Employee identification lines.push(` ${pnr}`) lines.push(` ${emp.specificationNumber}`) // Income and tax if (emp.totalGross > 0) { lines.push(` ${Math.round(emp.totalGross)}`) } if (emp.totalTax > 0) { lines.push(` ${Math.round(emp.totalTax)}`) } // Benefits if (emp.benefitCar && emp.benefitCar > 0) { lines.push(` ${Math.round(emp.benefitCar)}`) } if (emp.benefitHousing && emp.benefitHousing > 0) { lines.push(` ${Math.round(emp.benefitHousing)}`) } if (emp.benefitMeals && emp.benefitMeals > 0) { lines.push(` ${Math.round(emp.benefitMeals)}`) } if (emp.benefitOther && emp.benefitOther > 0) { lines.push(` ${Math.round(emp.benefitOther)}`) } // Avgifter basis if (emp.totalAvgifterBasis > 0) { lines.push(` ${Math.round(emp.totalAvgifterBasis)}`) } // Employment period (if not full year) if (emp.employmentStart) { lines.push(` ${emp.employmentStart.replace(/-/g, '')}`) } if (emp.employmentEnd) { lines.push(` ${emp.employmentEnd.replace(/-/g, '')}`) } lines.push(' ') lines.push(' ') lines.push(' ') } lines.push('') return lines.join('\n') } function escapeXml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') }