Files
accounted/lib/reports/__tests__/behandlingshistorik-pdf-template.test.ts
T
99a872987e feat(reports): behandlingshistorik as PDF + systemdokumentation pointer and version (#1790)
PR 2 of the behandlingshistorik plan (stacked on #1787).

- lib/reports/behandlingshistorik-pdf-template.tsx: landscape A4 react-pdf
  document. Fixed header (räkenskapsår, urval, legal reference, company) and
  footer (page x of y, generated in Europe/Stockholm), repeated table header,
  wrap={false} rows, no `break` props. Two sections in the order the reader
  needs them: "Ändringar i bokföringssystemet" (p. 9.16 second paragraph)
  then "Bokföringsposter i registreringsordning" (first paragraph). Meta row:
  generated, programversion, antal händelser, källor. Details as one wrapped
  paragraph per row (real-data render 371 events: 1.5 s, 23 pages). Glyphs the
  bundled Helvetica lacks (arrow, true minus) are mapped to ASCII.
- GET /api/reports/behandlingshistorik?format=pdf with a 4 000-event guard
  (413 REPORT_PDF_TOO_LARGE, CSV/XLSX remain complete); PDF first in the
  export menu; catalog exports pdf+xlsx.
- lib/reports/app-version.ts shared by the route and the archive:
  revision/systemdokumentation.json now carries system.version and a
  behandlingshistorik block (where and how it is produced, p. 9.15); the
  shipped systemdokumentation template §9.3 points at Rapporter >
  Behandlingshistorik (PDF/CSV/Excel) as well as the backup ZIP.
- Settings values that are objects render as "key: value" pairs in every
  format; report carries category_filter so the document states its urval.
- Tests: 4 PDF template tests (valid PDF, empty report, filtered range,
  220-row pagination), route pdf 200 + 413, route "unknown format" moved off
  pdf. Prod read-only render verified visually (header, sections, paging).


Claude-Session: https://claude.ai/code/session_01Kw2CFCEt8MxzbJiXMAgMVi

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 16:47:19 +02:00

109 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest'
import { renderToBuffer } from '@react-pdf/renderer'
import { BehandlingshistorikPDF, pdfText } from '../behandlingshistorik-pdf-template'
import type { BehandlingshistorikEvent, BehandlingshistorikReport } from '../behandlingshistorik-types'
// Real @react-pdf/renderer layout is CPU-heavy; give it room on a saturated runner.
const RENDER_TIMEOUT = 30_000
function event(overrides: Partial<BehandlingshistorikEvent>): BehandlingshistorikEvent {
return {
id: `ev-${Math.random().toString(36).slice(2, 8)}`,
occurred_at: '2026-03-10T09:30:00.000Z',
category: 'verifikation',
code: 'journal_entry.committed',
event: 'Verifikation bokförd',
object: 'A12',
actor: { type: 'user', user_id: 'user-1', label: 'anna@example.se' },
details: ['Datum: 2026-03-09', 'Text: Hyra mars', 'Källa: Manuell'],
source: 'journal_entries',
count: 1,
...overrides,
}
}
function report(overrides: Partial<BehandlingshistorikReport> = {}): BehandlingshistorikReport {
const events = overrides.events ?? [
event({ id: 'a1', category: 'kontoplan', code: 'account.created.bulk', event: 'Kontoplan upplagd', object: '41 konton', details: ['Konton 1510 till 8410'], source: 'audit_log', count: 41, occurred_at: '2026-01-05T08:00:00.000Z' }),
event({ id: 'a2', category: 'installningar', code: 'settings.updated', event: 'Företagsinställningar ändrade', object: null, details: ['Momsperiod: Kvartal → Helår'], source: 'audit_log', occurred_at: '2026-02-01T08:00:00.000Z' }),
event({ id: 'e1' }),
event({ id: 'e2', object: 'A13', code: 'journal_entry.reversed', event: 'Verifikation makulerad (storno)', actor: { type: 'api_key', user_id: null, label: 'API-nyckel: Revisorn' }, occurred_at: '2026-04-02T10:00:00.000Z' }),
]
const by_category = { verifikation: 0, kontoplan: 0, installningar: 0, period: 0, import: 0, atkomst: 0, ovrigt: 0 }
for (const e of events) by_category[e.category] += 1
return {
company: { name: 'Testbolaget AB', org_number: '5566778899' },
period: { id: 'p', name: 'Räkenskapsår 2026', start: '2026-01-01', end: '2026-12-31' },
range: { from: '2026-01-01', to: '2026-12-31' },
mode: 'fiscal_year',
generated_at: '2026-08-21T12:00:00.000Z',
app_version: 'abc123def456',
total_events: events.length,
by_category,
events,
category_filter: null,
...overrides,
}
}
describe('pdfText', () => {
it('maps glyphs the bundled Helvetica lacks to ASCII', () => {
expect(pdfText('Momsperiod: Kvartal → Helår')).toBe('Momsperiod: Kvartal -> Helår')
expect(pdfText('−1 200,00')).toBe('-1 200,00')
expect(pdfText('åäö ÅÄÖ … ·')).toBe('åäö ÅÄÖ … ·')
})
})
describe('BehandlingshistorikPDF', () => {
it(
'renders a valid PDF with both sections',
async () => {
const buffer = await renderToBuffer(BehandlingshistorikPDF({ report: report() }))
expect(buffer).toBeInstanceOf(Buffer)
expect(buffer.slice(0, 5).toString()).toBe('%PDF-')
expect(buffer.length).toBeGreaterThan(1000)
},
RENDER_TIMEOUT,
)
it(
'renders an empty report (both sections show the empty line) and a category-filtered date range',
async () => {
const empty = await renderToBuffer(
BehandlingshistorikPDF({
report: report({ events: [], total_events: 0, by_category: { verifikation: 0, kontoplan: 0, installningar: 0, period: 0, import: 0, atkomst: 0, ovrigt: 0 } }),
}),
)
expect(empty.slice(0, 5).toString()).toBe('%PDF-')
const filtered = await renderToBuffer(
BehandlingshistorikPDF({
report: report({ mode: 'date_range', range: { from: '2026-03-01', to: '2026-03-31' }, category_filter: ['verifikation'], app_version: null }),
}),
)
expect(filtered.slice(0, 5).toString()).toBe('%PDF-')
},
RENDER_TIMEOUT,
)
it(
'paginates a long report without splitting rows (no break props: must not hang)',
async () => {
const events = Array.from({ length: 220 }, (_, i) =>
event({
id: `e${i}`,
object: `A${i + 1}`,
occurred_at: new Date(Date.parse('2026-01-01T08:00:00.000Z') + i * 3_600_000).toISOString(),
details: ['Datum: 2026-01-01', `Text: Rad ${i} med en ganska lång beskrivning som ska radbrytas i detaljkolumnen`, 'Källa: Banktransaktion'],
}),
)
const buffer = await renderToBuffer(BehandlingshistorikPDF({ report: report({ events, total_events: events.length }) }))
expect(buffer.slice(0, 5).toString()).toBe('%PDF-')
// A 220-row landscape table is several pages; /Type /Page objects prove pagination happened.
const pages = (buffer.toString('latin1').match(/\/Type\s*\/Page[^s]/g) ?? []).length
expect(pages).toBeGreaterThan(3)
},
RENDER_TIMEOUT,
)
})