a4ceaafa4f
* feat(invoice-inbox): per-item underlag status and daily reconcile of stranded booked items (#1548) The inbox derives "booked" from the matched transaction's verifikat, but that says nothing about whether THIS item's document reached it: a link that failed at propagation time, or a document anchored to another verifikat, read as booked while the verifikat sat without its underlag (BFL 5 kap 6-7 §). GET /items and /items/:id now also emit underlag_status (anchored | unlinked | anchored_elsewhere) from one batched document_attachments read; the workspace keeps divergent items in "Att göra", drops the booking bridge for them (the book routes 409 on a booked transaction) and shows one explanatory line with a link to the verifikat. The backfill script's loop moves into lib/transactions/ inbox-underlag-reconcile.ts and runs daily from a new extension-owned cron (vercel.json plus the generated Docker crontabs): transient link failures heal without an ad-hoc script run, permanent conflicts are counted in one summary, and each repaired transaction leaves an InboxUnderlagReconciled row in behandlingshistorik. That event type is registered by migration 20260828154800: processing_history.event_type has an FK to processing_event_types, and the script's previous InboxUnderlagBackfilled type was never registered, so its appends had always failed silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(invoice-inbox): address review findings on the underlag reconcile (#1548) Findings 1, 3, 6 (scan cap starves the tail): the reconcile no longer caps the read. The matched-unconsumed candidate set holds permanent residents (samlingsverifikat siblings, anchored-elsewhere items) that never leave it, so a uuid-ordered read cap would revisit the same 1000 rows every night and never reach a stranded item sorting past the cut. The scan now pages through every candidate (four columns per row) and maxItems bounds the WORK: at most that many unlinked (or unreadable) items are propagated per run; already-anchored, anchored-elsewhere and locked items are counted from the pre-state without a propagation or budget. Items past the budget are counted as deferred and truncated is logged at warn level. Findings 2, 5 (false "linked automatically" promise for locked periods): resolveUnderlagAnchoring reads the fiscal period lock state of the verifikat for every unlinked item and reports unlinked_locked when is_closed or locked_at is set, the same pair enforce_period_lock_documents checks. The reconciler counts it separately (unlinkedLocked), never propagates it and never warns "still unlinked after re-run"; the rail shows a message that says the period must be unlocked first. Findings 4, 7 (absent anchoring read as booked): the list and detail enrichment emit underlag_status 'unknown' when the helper could not read the document row, and the workspace treats any status but 'anchored' as divergent (stays in Att göra, no booking bridge, own message). classify() counts a repair only when the pre-state was explicitly unlinked, so an unreadable before-read never earns an InboxUnderlagReconciled event. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna * fix(invoice-inbox): address round-2 review findings (#1548) 1. [minor] Round-1 fix dropped propagation for transactions whose inbox items already read anchored, so the pinned-document leg (transactions.document_id) was never repaired and settled items never received their created_journal_entry_id stamp, staying in the scan and inflating alreadyAnchored every night. reconcileCompany now propagates every stranded transaction that has an unlinked (budgeted) item or an anchored / document-less item, outside the maxItems budget: the helper is idempotent and the stamp shrinks its own population. Locked-only and anchored-elsewhere-only transactions stay skipped. Counting and the behandlingshistorik trail are unchanged (anchored items keep their pre-state verdict, no event). Tests updated and a new case pins the anchored-item plus document-less-item transaction: propagated, no after-read, no history. DECISIONS line amended. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015nAd8XJ2RPCmG2eKoLBdna --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
40 lines
1.6 KiB
TypeScript
40 lines
1.6 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { randomUUID } from 'node:crypto'
|
|
import { getPool } from './setup'
|
|
import { seedCompany } from './fixtures'
|
|
|
|
describe('migration 20260828154800: InboxUnderlagReconciled event type', () => {
|
|
it('registers InboxUnderlagReconciled in the processing history catalog', async () => {
|
|
// reconcileStrandedInboxUnderlag() swallows an append failure (the
|
|
// repair is already done), so an unregistered type would be invisible
|
|
// outside the logs: this is what happened to the script's previous
|
|
// 'InboxUnderlagBackfilled' type.
|
|
const { rows } = await getPool().query(
|
|
`SELECT event_type
|
|
FROM public.processing_event_types
|
|
WHERE event_type = 'InboxUnderlagReconciled'`,
|
|
)
|
|
|
|
expect(rows).toEqual([{ event_type: 'InboxUnderlagReconciled' }])
|
|
})
|
|
|
|
it('accepts an InboxUnderlagReconciled row through the event_type FK', async () => {
|
|
const { companyId } = await seedCompany()
|
|
const txId = randomUUID()
|
|
const { rows } = await getPool().query<{ 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, 'BankTransaction', $2, 'InboxUnderlagReconciled',
|
|
$3::jsonb, '{"type":"system","id":"inbox-underlag-reconcile"}', now())
|
|
RETURNING event_type`,
|
|
[
|
|
companyId,
|
|
txId,
|
|
JSON.stringify({ transaction_id: txId, inbox_item_ids: [], source: 'inbox-underlag-reconcile' }),
|
|
],
|
|
)
|
|
|
|
expect(rows).toEqual([{ event_type: 'InboxUnderlagReconciled' }])
|
|
})
|
|
})
|