feat(workflows): migrate the dunning ladder onto the engine (cutover)

Makes the built-in dunning flow a faithful replacement for the hardcoded
reminder ladder instead of a disabled representation:

- queue_payment_check action delegates to invoiceService.queuePaymentCheckEmail,
  so the proven confirm + reminder_level + Mahngebühr state machine
  (recordPaymentCheckAction) stays the single source of truth — the workflow
  only decides WHEN the payment-check email (the gate) fires.
- runScheduledTasks now SKIPS the hardcoded reminder batches when workflows is
  on AND the invoice_dunning built-in is enabled, so the two never double-send.
- The built-in graph is re-authored to the delegation model (wait→due, grace,
  loop: check-paid → payment-check → wait-gap), dropping the redundant gate +
  generic reminder emails. A SEED_VERSION re-seeds the disabled, never-activated
  built-in on boot but never touches an enabled/edited one.

Tests: delegation graph shape, re-seed-when-stale, enabled-protection (9 engine
+ 8 route = 17 passing).
This commit is contained in:
Luca
2026-06-23 11:12:19 +02:00
parent 88881d0428
commit 5259ee9705
4 changed files with 131 additions and 42 deletions
+13
View File
@@ -66,6 +66,19 @@ registry.registerAction('send_email', async (ctx) => {
return { sent_to: to, recipientClass, respectBusinessHours };
});
// Fire the existing admin payment-check email (the dunning gate). Delegates to
// invoiceService.queuePaymentCheckEmail so the proven escalation +
// Mahngebühr / reminder_level state machine (recordPaymentCheckAction) stays
// the single source of truth — the workflow only decides WHEN it fires. This
// is what makes the built-in dunning flow a faithful replacement for the
// hardcoded ladder (paired with the mutual-exclusion guard in runScheduledTasks).
registry.registerAction('queue_payment_check', async (ctx) => {
const id = ctx.run.entity_id;
if (!id) return { skipped: true, reason: 'no invoice entity' };
await require('../invoiceService').queuePaymentCheckEmail(id);
return { payment_check_queued: id };
});
// Create/prepare-document actions — registered so flows referencing them are
// valid; service wiring is a follow-up. Records a skipped step (observable).
for (const key of DOCUMENT_ACTIONS) {