feat(workflows): seed invoice-dunning ladder as an editable built-in flow

Boot self-heal seeds the corrected gate-in-loop dunning graph (wait→due,
grace wait, invoice_paid check, confirm-no-payment gate, bounded reminder
loop with re-check, final notice) keyed on builtin_key='invoice_dunning',
sized from the reminder_first/second_days settings. Seeded DISABLED and
is_builtin: live reminder behaviour is UNCHANGED (the hardcoded scheduler
ladder still runs) — enabling it pre-cutover would double-send, so the
engine cutover is a deliberate follow-up. Idempotent (preserves admin edits).
Built-ins refuse delete (enforced in the CRUD route). Test covers seed shape
+ idempotency.
This commit is contained in:
Luca
2026-06-23 02:44:05 +02:00
parent 1a0d6de04d
commit 9b557efbf3
3 changed files with 130 additions and 0 deletions
@@ -233,4 +233,23 @@ describe('workflow engine', () => {
const again = await engine.actByToken(rawToken, 'confirm');
expect(again.already).toBe(true);
});
test('seeds the invoice-dunning built-in flow (disabled, idempotent)', async () => {
const { seedBuiltinWorkflowsAtBoot, DUNNING_KEY } = require('../../src/services/_workflowSeedBoot');
const noopLogger = { info() {}, warn() {} };
await seedBuiltinWorkflowsAtBoot(db, noopLogger);
const wf = await db('workflows').where({ builtin_key: DUNNING_KEY }).first();
expect(wf).toBeTruthy();
expect(!!wf.is_builtin).toBe(true);
expect(!!wf.enabled).toBe(false);
const nodes = await db('workflow_nodes').where({ workflow_id: wf.id, version: 1 });
expect(nodes.filter((n) => n.type === 'trigger')).toHaveLength(1);
expect(nodes.length).toBeGreaterThanOrEqual(10);
await seedBuiltinWorkflowsAtBoot(db, noopLogger); // idempotent
const all = await db('workflows').where({ builtin_key: DUNNING_KEY });
expect(all.length).toBe(1);
});
});