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
+12 -1
View File
@@ -3392,7 +3392,18 @@ async function runScheduledTasks() {
// Throttled to one email per 24h per invoice via
// invoices.last_payment_check_at.
const remindersEnabled = await getAppSetting('crm_invoices_reminders_enabled');
if (remindersEnabled !== false) {
// Mutual exclusion with the workflow engine: when the `workflows` flag is on
// AND the invoice_dunning built-in is enabled, the engine fires the same
// payment-check emails — running this hardcoded ladder too would double-send.
let engineDrivesDunning = false;
try {
const { isFeatureEnabled } = require('../middleware/requireFeatureFlag');
if (await isFeatureEnabled('workflows')) {
const dunning = await db('workflows').where({ builtin_key: 'invoice_dunning', enabled: true }).first();
engineDrivesDunning = !!dunning;
}
} catch (_) { /* workflows tables absent / flag system down → ladder stays on */ }
if (remindersEnabled !== false && !engineDrivesDunning) {
const firstDays = ensureInt(await getAppSetting('crm_invoices_reminder_first_days')) || 14;
const secondDays = ensureInt(await getAppSetting('crm_invoices_reminder_second_days')) || 30;