Files
accounted/tests/pg/invoice-rows-completed-event.pg.test.ts
6906bc4aa2 feat(compliance): InvoiceRowsCompleted behandlingshistorik event for migrated invoice rows (#2312) (#2357)
Every migrated sales invoice whose rows complete_invoice_rows writes, from
the migration wizard or the hourly row-completion pass, now leaves one
InvoiceRowsCompleted row in processing_history on a new Invoice aggregate:
the writer, the provider, the consent, the row count, and the header VAT
split before and after when the pass rewrote it (BFL 5 kap 11 §, BFNAR
2013:2 p. 9.16). One run shares one correlation id.

lib/invoices/complete-invoice-rows.ts is the one TypeScript call site for
the RPC and the one emitter: it appends only on wrote = true, records
nothing for already_filled or failed, and keeps the append best-effort
(logged, eventId null) like every other processing_history writer. The
wizard runs on the user's session client, so MigrationOptions takes a lazy
createHistoryClient for the service role. Invoice numbers stay out of the
payload (the personnummer guard would drop ten-digit ones).

Migration 20260906210100 widens the aggregate_type CHECK with Invoice and
registers the event type; pg test covers the catalog row, the aggregate,
and that the CHECK still refuses unknown aggregates.


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

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 21:04:53 +02:00

67 lines
2.8 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { randomUUID } from 'node:crypto'
import { getPool } from './setup'
import { seedCompany } from './fixtures'
// pg-real coverage for 20260906210100_invoice_rows_completed_event: the
// behandlingshistorik event lib/invoices/complete-invoice-rows.ts writes for
// every migrated invoice whose rows complete_invoice_rows filled (#2312).
// The type must be in the catalog (the append is best-effort, so an
// unregistered type is a silently lost record) and the invoice must be an
// admitted aggregate (the CHECK did not know 'Invoice' before).
describe('migration 20260906210100: InvoiceRowsCompleted event', () => {
it('registers InvoiceRowsCompleted in the processing history catalog', async () => {
const { rows } = await getPool().query(
`SELECT event_type
FROM public.processing_event_types
WHERE event_type = 'InvoiceRowsCompleted'`,
)
expect(rows).toEqual([{ event_type: 'InvoiceRowsCompleted' }])
})
it('accepts the event on the Invoice aggregate, shaped the way the emitter writes it', async () => {
const { companyId } = await seedCompany()
const invoiceId = randomUUID()
const runId = randomUUID()
const { rows } = await getPool().query<{ aggregate_type: string; event_type: string }>(
`INSERT INTO public.processing_history
(company_id, correlation_id, aggregate_type, aggregate_id, event_type, payload, actor, occurred_at)
VALUES ($1, $2, 'Invoice', $3, 'InvoiceRowsCompleted',
$4::jsonb, '{"type":"cron","id":"complete-invoice-lines"}', now())
RETURNING aggregate_type, event_type`,
[
companyId,
runId,
invoiceId,
JSON.stringify({
source: 'complete-invoice-lines',
provider: 'fortnox',
consent_id: randomUUID(),
rows: 2,
header_updated: true,
header_before: { subtotal: 1250, vat_amount: 0, vat_rate: 25, vat_treatment: 'standard_25' },
header_after: { subtotal: 1000, vat_amount: 250, vat_rate: 25, vat_treatment: 'standard_25' },
}),
],
)
expect(rows).toEqual([{ aggregate_type: 'Invoice', event_type: 'InvoiceRowsCompleted' }])
})
it('still refuses an aggregate the constraint does not name: the CHECK was widened, not dropped', async () => {
const { companyId } = await seedCompany()
const id = randomUUID()
await expect(
getPool().query(
`INSERT INTO public.processing_history
(company_id, correlation_id, aggregate_type, aggregate_id, event_type, payload, actor, occurred_at)
VALUES ($1, $2, 'Kitten', $2, 'InvoiceRowsCompleted', '{}'::jsonb,
'{"type":"system","id":"invoice-rows-completed-test"}', now())`,
[companyId, id],
),
).rejects.toMatchObject({ code: '23514' })
})
})