Files
accounted/app/api/reports/bokslutsbilagor/route.ts
T
9ce1ebc65f feat(reports): bokslutsbilagor, the pärm per räkenskapsår (Reko bilagor, PR 4) (#1874)
* feat(reports): bokslutsbilagor, the pärm per räkenskapsår (Reko bilagor, PR 4)

One bilaga per balance account as of the balansdag: IB, movement and UB
from the trial balance, what it was reconciled against, the difference,
the sign-off with who, when and note, and every attached file with its
SHA-256; the closing checklist as the first page. JSON and PDF through
/api/reports/bokslutsbilagor, in the reports library and on the
Avstämning page, and written into every period folder of the full
archive. Built from the attested rows, never by recomputing live status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

* fix(reports): load the pärm renderer on demand in the full archive so PDF stubs elsewhere keep working

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

* fix(reconciliation): neutral rail dot for a manual account that is merely not attested yet

An unsigned manual account without a system specification has nothing to
compare against, so an amber dot read as a problem on every balance account
of a freshly migrated company. Neutral until it is signed or a
specification differs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 09:35:02 +02:00

63 lines
2.9 KiB
TypeScript

import { NextResponse } from 'next/server'
import { z } from 'zod'
import { renderToBuffer } from '@react-pdf/renderer'
import { withRouteContext } from '@/lib/api/with-route-context'
import { validateQuery } from '@/lib/api/validate'
import { contentDisposition } from '@/lib/api/content-disposition'
import { privateNoStore } from '@/lib/api/private-no-store'
import { errorResponseFromCode } from '@/lib/errors/get-structured-error'
import { createServiceClient } from '@/lib/supabase/server'
import { generateBokslutsbilagor } from '@/lib/reports/bokslutsbilagor'
import { BokslutsbilagorPDF } from '@/lib/reports/bokslutsbilagor-pdf-template'
import { resolveUserLabelsFromProfiles } from '@/lib/reports/behandlingshistorik'
import { currentAppVersion } from '@/lib/reports/app-version'
import { slugifyCompanyName } from '@/lib/reports/xlsx-export'
const BokslutsbilagorQuerySchema = z.object({
period_id: z.string().uuid(),
format: z.enum(['json', 'pdf']).default('json'),
})
/**
* GET /api/reports/bokslutsbilagor?period_id=&format=json|pdf
*
* The bokslutsbilagor pärm for one räkenskapsår: every balance account as of
* the balansdag with balances, specification or stated balance, sign-off,
* underlag files with hashes, and the checklist. Read-only; signer and
* uploader labels resolve through a service-role lookup on `profiles`
* restricted to the ids in the result, like behandlingshistorik.
*/
export const GET = withRouteContext('report.bokslutsbilagor', async (request, ctx) => {
const { supabase, user, companyId, log, requestId } = ctx
const query = validateQuery(request, BokslutsbilagorQuerySchema, { log, operation: 'report.bokslutsbilagor' })
if (!query.success) return query.response
const { period_id: periodId, format } = query.data
try {
const serviceClient = createServiceClient()
const report = await generateBokslutsbilagor(supabase, companyId, periodId, {
userId: user.id,
resolveUserLabels: (ids) => resolveUserLabelsFromProfiles(serviceClient, ids),
appVersion: currentAppVersion(),
})
if (!report) return errorResponseFromCode('FISCAL_PERIOD_NOT_FOUND', log, { requestId })
if (format === 'json') {
return privateNoStore(NextResponse.json({ data: report }))
}
const pdf = await renderToBuffer(BokslutsbilagorPDF({ report }))
const filename = `bokslutsbilagor-${slugifyCompanyName(report.company.name)}-${report.period.end.replace(/-/g, '')}.pdf`
return new NextResponse(new Uint8Array(pdf), {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': contentDisposition('attachment', filename),
'Cache-Control': 'private, no-store',
},
})
} catch (err) {
// Raw message stays server-side: it can carry table names / SQL.
log.error('bokslutsbilagor generation failed', err as Error, { periodId })
return errorResponseFromCode('REPORT_GENERATION_FAILED', log, { requestId })
}
})