Files
accounted/app/api/rot-rut/payout-file/route.ts
T
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

94 lines
3.5 KiB
TypeScript

import { NextResponse } from 'next/server'
import { withRouteContext } from '@/lib/api/with-route-context'
import { validateBody } from '@/lib/api/validate'
import { RotRutPayoutFileSchema } from '@/lib/api/schemas'
import { errorResponseFromCode } from '@/lib/errors/get-structured-error'
import { createRotRutPayoutRequest } from '@/lib/invoices/rot-rut-service'
import { uploadDocument } from '@/lib/core/documents/document-service'
/**
* POST /api/rot-rut/payout-file
*
* Generates the begäran-om-utbetalning XML (Skatteverket husavdrag, schema
* V6) for the selected invoices, records a rot_rut_payout_requests row (one
* active begäran per invoice, DB-enforced), archives the file as
* räkenskapsinformation, and returns the XML for download.
*
* All-or-nothing: if any selected invoice fails eligibility the request is
* rejected with per-invoice blockers: a silently thinner file would be a
* guess about the user's intent.
*
* DELIBERATE: the XML (which embeds buyers' personnummer, as Skatteverkets
* schema requires) is returned inline. The file only exists to be saved and
* uploaded manually on skatteverket.se: there is no UI download surface for
* this headless flow, and a document-reference indirection would dead-end the
* user whenever the (best-effort) archive failed. Transport is TLS,
* authenticated, MFA-gated and write-role-gated via withRouteContext.
*/
export const POST = withRouteContext(
'rot_rut.payout_file',
async (request, ctx) => {
const { user, supabase, companyId, log, requestId } = ctx
const validation = await validateBody(request, RotRutPayoutFileSchema)
if (!validation.success) return validation.response
const input = validation.data
const result = await createRotRutPayoutRequest(supabase, companyId!, user.id, {
type: input.deduction_type,
invoiceIds: input.invoice_ids,
name: input.name,
})
if (!result.ok) {
return errorResponseFromCode(result.code, log, {
requestId,
details: {
...(result.blockers ? { blockers: result.blockers } : {}),
...(result.missingInvoiceIds ? { missing_invoice_ids: result.missingInvoiceIds } : {}),
},
})
}
// Archive the XML as räkenskapsinformation (7-year retention via the
// document WORM chain). Best-effort: the user gets the file either way
// and can re-generate; a failed archive must not orphan the begäran.
let fileDocumentId: string | null = null
try {
const buffer = new TextEncoder().encode(result.file.xml!).buffer as ArrayBuffer
const doc = await uploadDocument(
supabase,
user.id,
companyId!,
{ name: result.file.file_name, buffer },
{ upload_source: 'system' },
)
fileDocumentId = doc.id
await supabase
.from('rot_rut_payout_requests')
.update({ file_document_id: doc.id })
.eq('id', result.request.id as string)
} catch (docError) {
log.error('failed to archive rot/rut payout file document', docError as Error)
}
log.info('rot/rut payout file generated', {
requestId: result.request.id,
type: input.deduction_type,
arenden: result.file.arenden.length,
requestedTotal: result.file.requested_total,
})
return NextResponse.json({
data: {
request: { ...result.request, file_document_id: fileDocumentId },
xml: result.file.xml,
file_name: result.file.file_name,
arenden: result.file.arenden,
warnings: result.file.warnings,
},
})
},
{ requireWrite: true },
)