- Fix VAT declaration ruta mappings to match SKV 4700 form correctly (ruta 05 = total taxable sales, ruta 10/11/12 = output VAT per rate) - Add INK2 declaration report for aktiebolag with SRU export - Add full archive ZIP export for 7-year retention compliance - Add AI consent gate requiring user approval before AI extension API calls - Add DPA and privacy policy public pages - Add audit trail API routes - Update VAT registration threshold from 80k to 120k kr in onboarding - Update CLAUDE.md documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
159 lines
4.2 KiB
TypeScript
159 lines
4.2 KiB
TypeScript
import type { INK2Declaration, INK2SRUCode, SRUFile, SRURecord } from './types'
|
|
|
|
/**
|
|
* SRU File Generator for INK2
|
|
*
|
|
* Generates SRU (Standardiserat Räkenskapsutdrag) files for electronic
|
|
* submission to Skatteverket. The SRU format is used for tax declarations.
|
|
*
|
|
* INK2 field codes are the SRU codes directly (7201-7380).
|
|
*/
|
|
|
|
/** All INK2 SRU field codes in order */
|
|
const INK2_FIELD_CODES: INK2SRUCode[] = [
|
|
'7201', '7202', '7203', '7210', '7211', '7212',
|
|
'7220', '7221', '7222', '7230', '7231',
|
|
'7310', '7320', '7330', '7340', '7350', '7360', '7370', '7380',
|
|
]
|
|
|
|
/**
|
|
* Generate SRU file content from INK2 declaration
|
|
*/
|
|
export function generateSRUFile(declaration: INK2Declaration): SRUFile {
|
|
const records: SRURecord[] = []
|
|
const now = new Date()
|
|
|
|
// File header
|
|
records.push({ fieldCode: 'PRODUKT', value: 'KONTROLLUPPGIFTER' })
|
|
records.push({ fieldCode: 'SESSION', value: '1' })
|
|
records.push({ fieldCode: 'PROGRAMNAMN', value: 'ERPBase' })
|
|
records.push({ fieldCode: 'PROGRAMVERSION', value: '1.0' })
|
|
records.push({
|
|
fieldCode: 'SKAPAT',
|
|
value: formatSRUDate(now),
|
|
})
|
|
|
|
// Form declaration
|
|
records.push({ fieldCode: 'BLANKETT', value: 'INK2' })
|
|
|
|
// Company identification
|
|
if (declaration.companyInfo.orgNumber) {
|
|
const cleanOrgNumber = declaration.companyInfo.orgNumber.replace(/-/g, '')
|
|
records.push({
|
|
fieldCode: 'IDENTITET',
|
|
value: cleanOrgNumber,
|
|
})
|
|
}
|
|
|
|
// Fiscal year
|
|
records.push({
|
|
fieldCode: 'UPPGIFT',
|
|
value: `7000 ${formatSRUDateRange(declaration.fiscalYear.start, declaration.fiscalYear.end)}`,
|
|
})
|
|
|
|
// INK2 field values
|
|
for (const code of INK2_FIELD_CODES) {
|
|
const value = declaration.rutor[code]
|
|
if (value !== 0) {
|
|
records.push({
|
|
fieldCode: 'UPPGIFT',
|
|
value: `${code} ${formatSRUAmount(value)}`,
|
|
})
|
|
}
|
|
}
|
|
|
|
// End of form
|
|
records.push({ fieldCode: 'BLANKETTSLUT', value: '' })
|
|
|
|
return {
|
|
records,
|
|
generatedAt: now.toISOString(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert SRU file to string content
|
|
*/
|
|
export function sruFileToString(sruFile: SRUFile): string {
|
|
const lines: string[] = []
|
|
|
|
for (const record of sruFile.records) {
|
|
if (record.value === '') {
|
|
lines.push(`#${record.fieldCode}`)
|
|
} else {
|
|
lines.push(`#${record.fieldCode} ${record.value}`)
|
|
}
|
|
}
|
|
|
|
return lines.join('\r\n') + '\r\n'
|
|
}
|
|
|
|
/**
|
|
* Format date for SRU: YYYYMMDD
|
|
*/
|
|
function formatSRUDate(date: Date): string {
|
|
const y = date.getFullYear()
|
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
const d = String(date.getDate()).padStart(2, '0')
|
|
return `${y}${m}${d}`
|
|
}
|
|
|
|
/**
|
|
* Format date string (YYYY-MM-DD) to SRU format (YYYYMMDD)
|
|
*/
|
|
function dateStringToSRU(dateStr: string): string {
|
|
return dateStr.replace(/-/g, '')
|
|
}
|
|
|
|
/**
|
|
* Format fiscal year date range for SRU
|
|
*/
|
|
function formatSRUDateRange(startDate: string, endDate: string): string {
|
|
return `${dateStringToSRU(startDate)}-${dateStringToSRU(endDate)}`
|
|
}
|
|
|
|
/**
|
|
* Format amount for SRU: whole numbers, no thousands separator, negative with minus
|
|
*/
|
|
function formatSRUAmount(amount: number): string {
|
|
return Math.round(amount).toString()
|
|
}
|
|
|
|
/**
|
|
* Validate SRU file content
|
|
*/
|
|
export function validateSRUFile(sruFile: SRUFile): {
|
|
isValid: boolean
|
|
errors: string[]
|
|
} {
|
|
const errors: string[] = []
|
|
|
|
const hasHeader = sruFile.records.some(r => r.fieldCode === 'PRODUKT')
|
|
const hasBlankett = sruFile.records.some(r => r.fieldCode === 'BLANKETT')
|
|
const hasBlankettslut = sruFile.records.some(r => r.fieldCode === 'BLANKETTSLUT')
|
|
|
|
if (!hasHeader) errors.push('Missing PRODUKT header')
|
|
if (!hasBlankett) errors.push('Missing BLANKETT declaration')
|
|
if (!hasBlankettslut) errors.push('Missing BLANKETTSLUT')
|
|
|
|
// Verify it's INK2
|
|
const blankettRecord = sruFile.records.find(r => r.fieldCode === 'BLANKETT')
|
|
if (blankettRecord && blankettRecord.value !== 'INK2') {
|
|
errors.push(`Expected BLANKETT INK2, got ${blankettRecord.value}`)
|
|
}
|
|
|
|
return {
|
|
isValid: errors.length === 0,
|
|
errors,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get filename for SRU file download
|
|
*/
|
|
export function getSRUFilename(declaration: INK2Declaration): string {
|
|
const year = declaration.fiscalYear.start.substring(0, 4)
|
|
const orgNumber = declaration.companyInfo.orgNumber?.replace(/-/g, '') || 'unknown'
|
|
return `INK2_${orgNumber}_${year}.sru`
|
|
}
|