* 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>
63 lines
2.9 KiB
TypeScript
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 })
|
|
}
|
|
})
|