Files
accounted/app/api/reports/ink2/route.ts
T
Jakob WennbergandClaude Opus 4.6 7a18d89c70 feat: INK2 declaration improvements, invoice delivery date & Swedish compliance skills (#204)
* feat: add INK2 declaration improvements, invoice delivery date, and Swedish compliance skills

Expand INK2 engine with full INK2S/INK2R support and improved SRU generation.
Add delivery_date field to invoices and corresponding PDF/migration support.
Add Claude skills for Swedish asset accounting, invoice compliance, SIE import/export, SRU filing, and tax planning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review — map BAS 4500–4899, strip CRLF in SRU, document P3

- Map BAS accounts 4500–4599 (legoarbeten), 4700–4899 (diverse
  varuinköpskostnader) to SRU 7512 so they are not silently dropped
  from INK2R declarations
- Strip \r\n in sanitizeString to prevent CRLF injection in SRU fields
- Document P3 period suffix limitation for brutet räkenskapsår

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: correct BAS 4500-4599, 4700-4899 mapping from 7512 to 7511

Per the official BAS-to-SRU mapping, these account ranges are cost of
goods (legoarbeten, inkurans, svinn) and belong under 7511 (Råvaror
och förnödenheter), not 7512 (Handelsvaror). 7512 remains 4600-4699.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 13:16:10 +02:00

90 lines
2.7 KiB
TypeScript

import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateINK2Declaration } from '@/lib/reports/ink2/ink2-engine'
import {
generateSRUSubmission,
getZipFilename,
} from '@/lib/reports/ink2/sru-generator'
import { requireCompanyId } from '@/lib/company/context'
import JSZip from 'jszip'
/**
* GET /api/reports/ink2
*
* Generate INK2 declaration for aktiebolag.
*
* Query parameters:
* - period_id: Fiscal period ID (required)
* - format: 'json' (default) or 'sru' for SRU file download (ZIP with INFO.SRU + BLANKETTER.SRU)
*/
export async function GET(request: Request) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const companyId = await requireCompanyId(supabase, user.id)
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
const format = searchParams.get('format') || 'json'
if (!periodId) {
return NextResponse.json(
{ error: 'period_id is required' },
{ status: 400 }
)
}
try {
const declaration = await generateINK2Declaration(supabase, companyId, periodId)
if (format === 'sru') {
const submission = generateSRUSubmission(declaration)
// Encode both files as ISO 8859-1 (Latin-1) — required by Skatteverket
const infoBytes = encodeISO88591(submission.infoSru)
const blanketterBytes = encodeISO88591(submission.blanketterSru)
// Create ZIP with both files
const zip = new JSZip()
zip.file('INFO.SRU', infoBytes)
zip.file('BLANKETTER.SRU', blanketterBytes)
const zipArrayBuffer = await zip.generateAsync({ type: 'arraybuffer' })
const filename = getZipFilename(declaration)
return new NextResponse(zipArrayBuffer, {
status: 200,
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="${filename}"`,
},
})
}
return NextResponse.json({ data: declaration })
} catch (err) {
console.error('Error generating INK2 declaration:', err)
return NextResponse.json(
{ error: err instanceof Error ? err.message : 'Failed to generate INK2 declaration' },
{ status: 500 }
)
}
}
/**
* Encode a string as ISO 8859-1 (Latin-1) bytes.
* Characters outside the Latin-1 range are replaced with '?'.
*/
function encodeISO88591(str: string): Uint8Array {
const bytes = new Uint8Array(str.length)
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i)
bytes[i] = code <= 0xFF ? code : 0x3F // '?' for unmappable chars
}
return bytes
}