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>
This commit is contained in:
Jakob Wennberg
2026-09-06 21:04:53 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5.1
parent ebbe50c0f3
commit 6906bc4aa2
14 changed files with 730 additions and 65 deletions
@@ -50,7 +50,8 @@ const mockCount = vi.mocked(countRowlessInvoices)
const EMPTY = {
candidates: 0, providerInvoices: 0, matched: 0, unmatched: 0, completed: 0, headersUpdated: 0,
totalMismatch: 0, noLinesAtProvider: 0, rowsMismatch: 0, notHydrated: 0, vatUnresolved: 0, failed: 0, remaining: 0,
totalMismatch: 0, noLinesAtProvider: 0, rowsMismatch: 0, notHydrated: 0, vatUnresolved: 0, failed: 0,
historyAppended: 0, remaining: 0,
hydration: { needed: 0, hydrated: 0, failed: 0, skippedForBudget: 0 }, dryRun: false,
}
@@ -159,6 +159,7 @@ export const GET = withCronContext('cron.arcim_migration_complete_invoice_lines'
totalMismatch: 0,
rowsMismatch: 0,
failed: 0,
historyAppended: 0,
}
const completing = await ctx.forEach('register', registers, async ({ consent, candidates }, itemCtx) => {
@@ -188,6 +189,7 @@ export const GET = withCronContext('cron.arcim_migration_complete_invoice_lines'
totals.totalMismatch += result.totalMismatch
totals.rowsMismatch += result.rowsMismatch
totals.failed += result.failed
totals.historyAppended += result.historyAppended
itemCtx.log.info('migrated invoice rows completed for company', {
companyId: consent.company_id,
provider: consent.provider,
@@ -198,6 +200,8 @@ export const GET = withCronContext('cron.arcim_migration_complete_invoice_lines'
notHydrated: result.notHydrated,
totalMismatch: result.totalMismatch,
rowsMismatch: result.rowsMismatch,
failed: result.failed,
historyAppended: result.historyAppended,
hydration: result.hydration,
})
})