Files
accounted/app/api/reports/ink2/route.ts
T
Jakob Wennberg 46039f14f4 fix(reports): valid two-file NE-bilaga SRU submission (#318, #319) (#844)
* fix(reports): generate valid two-file NE-bilaga SRU submission (#318, #319)

The NE-bilaga "Ladda ner SRU" export produced a file Skatteverket rejects: it
was served as UTF-8 text/plain (å/ä/ö mojibake, #319) and was structurally
invalid — a single blob with #PRODUKT KONTROLLUPPGIFTER (the KU code), no
INFO.SRU/BLANKETTER.SRU split, a #SKAPAT typo, no #FIL_SLUT, and suspect field
codes 7310–7350 (#318).

Rewrite the generator to mirror the working INK2 generator: a two-file
INFO.SRU + BLANKETTER.SRU submission, ISO 8859-1 encoded and zipped, with
#PRODUKT SRU, #DATABESKRIVNING_*/#MEDIELEV_*, #BLANKETT NE-<år>P<x>,
#IDENTITET <personnummer12> <date> <time>, and #FIL_SLUT. Field codes use the
authoritative BAS NE_EJ_K1 coupling table (R1→7400 … R10→7505, R11→7440;
period dates 7011/7012). Enskild-firma identity is the owner's 12-digit
personnummer (birth-century prefix, not INK2's juridisk-person "16").

- Extract the shared ISO-8859-1 encoder to lib/reports/sru-encoding.ts (was
  inline in the INK2 route).
- Extend the NE engine/types to carry address/postort/email for INFO.SRU.
- Frontend: NE SRU download uses the INK2 blob pattern; fix a pre-existing
  param bug in EfDeclarationSection (fiscal_period_id → period_id, +format=sru).
- Add generator tests (structure, BAS field codes, zero-omission, ISO-8859-1).

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

* fix(reports): address review feedback on NE-bilaga SRU generator (#318)

- getZipFilename uses the income year (fiscal year END) so the filename matches
  the blankett type/identity for broken fiscal years.
- Refuse to generate a submission when the personnummer is missing/invalid
  (compute + validate the 12-digit identity once in generateNESRUSubmission and
  throw) instead of silently emitting a placeholder #IDENTITET that Skatteverket
  would reject after upload.
- validateBlanketterSru now asserts the mandatory räkenskapsår date fields
  (#UPPGIFT 7011/7012) — their absence is a level-2 rejection.
- 10-digit personnummer century is inferred from adult age (≥18, <110) at the
  income year, fixing the e.g. 1924-born/yy=24 edge that mapped to 2024.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:01:02 +02:00

71 lines
2.3 KiB
TypeScript

import { NextResponse } from 'next/server'
import { generateINK2Declaration } from '@/lib/reports/ink2/ink2-engine'
import {
generateSRUSubmission,
getZipFilename,
} from '@/lib/reports/ink2/sru-generator'
import { withRouteContext } from '@/lib/api/with-route-context'
import { errorResponseFromCode } from '@/lib/errors/get-structured-error'
import { encodeISO88591 } from '@/lib/reports/sru-encoding'
import JSZip from 'jszip'
/**
* GET /api/reports/ink2
*
* Query parameters:
* period_id: fiscal period id (required)
* format: 'json' (default) or 'sru' for SRU file download (ZIP with INFO.SRU + BLANKETTER.SRU)
*/
export const GET = withRouteContext(
'report.ink2',
async (request, ctx) => {
const { supabase, companyId, log, requestId } = ctx
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
const format = searchParams.get('format') || 'json'
if (!periodId) {
return errorResponseFromCode('REPORT_PERIOD_REQUIRED', log, { requestId })
}
const opLog = log.child({ periodId, format })
try {
const declaration = await generateINK2Declaration(supabase, companyId!, periodId)
if (format === 'sru') {
const submission = generateSRUSubmission(declaration)
// Skatteverket requires ISO 8859-1 (Latin-1)
const infoBytes = encodeISO88591(submission.infoSru)
const blanketterBytes = encodeISO88591(submission.blanketterSru)
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}"`,
'X-Request-Id': requestId,
},
})
}
return NextResponse.json({ data: declaration })
} catch (err) {
opLog.error('ink2 declaration generation failed', err as Error)
return errorResponseFromCode('TAX_DECL_GENERATION_FAILED', opLog, {
requestId,
details: { reason: err instanceof Error ? err.message : 'unknown' },
})
}
},
)