Local dunning workflow
The local workflow is deliberately narrower than a payment allocation or automated dunning system. It can create level 1 only when all of the following are true:
-
DunningEnabled=trueand exactly one enabled level 1 is configured; -
AfterDayshas elapsed using the merchant’s local calendar; -
the configured level fee is zero;
-
the invoice is a native issued invoice with an intact immutable PDF;
-
its immutable
AmountPayableis positive; -
the contract has exactly one ledger row: the same invoice’s positive
Receivable, with matching customer, contract, currency,GrossAmount, amount and contract balance; -
no correction references the invoice.
The supported configuration is editable at /admin/settings?section=dunning. The page accepts at most one level row, requires level 1, a bounded nonnegative AfterDays, Fee=0, and an explicit enabled flag. Higher levels and template keys are not offered. Invalid submissions retain their typed rows and leave the previous complete configuration unchanged.
This proof makes the invoice’s full AmountPayable the remaining amount without deriving it from an aggregate balance. Existing invoices with payments, refunds, write-offs, manual or unlinked adjustments, multiple ledger rows, corrections or incomplete evidence fail with DunningAllocationRequired.
Generation requires an explicit confirmation and idempotency key. One merchant lock and transaction allocate the number, create the draft, render and hash its PDF, add the local DunningCreated event and completion audit. A business fingerprint prevents a second level-1 draft for the same invoice state even under another idempotency key. Failures at any persistence seam roll the sequence and all generated records back.
The separate queue action revalidates the complete frozen dunning snapshot and PDF hash. It creates one local email with Status=ToBeSent and DeliveryState=LocalQueueOnly, plus a local outbox event and audit. It never invokes SMTP and never sets SentAt; the dunning remains a draft. Queue replay rejects a changed dunning, PDF, recipient, sender, subject, body or attachment snapshot.
Admin routes:
-
POST /admin/dunnings/generate -
GET /admin/dunnings/:id/pdf -
POST /admin/dunnings/:id/queue
The PDF response is merchant scoped, uses a sanitized filename, returns the exact stored bytes with application/pdf, and sets Cache-Control: private, no-store. Import-only merchants hide generation. An imported invoice submitted through the generic invoice-ID form fails closed with no effects; an imported Dunning detail hides queue controls. Historical records without a local immutable Dunning PDF do not expose the local PDF action.
Higher levels, fees, invoice allocation, partial payments, corrections, templates, automatic scheduling, SMTP delivery, postal delivery, provider behavior and Frisbii parity remain outside this bounded workflow.