fix(workflows): scope dunning backfill to its own flow via targetWorkflowId

Address review on #764: backfillDunningRuns emitted invoice.sent without a
target, so enabling dunning would also enroll every historical open invoice
into any custom invoice.sent flow. Pass the enabled flow's id through to
emitWorkflowEvent so the backfill only touches dunning. Also note the
computeWakeAt both-fields (untilVar + delay) behaviour change in its comment.
This commit is contained in:
Luca
2026-07-07 10:33:01 +02:00
parent 2c7b351458
commit da3a77dac4
2 changed files with 15 additions and 4 deletions
+4 -2
View File
@@ -256,10 +256,12 @@ router.patch('/:id/enabled', requirePermission('workflows.manage'), async (req,
await db('workflows').where({ id }).update(patch);
// Turning dunning ON enrolls existing open/unpaid invoices (anchored to
// their due date) so it starts chasing current debtors, not only invoices
// sent after enabling (#750). Best-effort — never fail the toggle over it.
// sent after enabling (#750). Scoped to this flow's id so the backfill only
// enrolls dunning, not any custom invoice.sent flow. Best-effort — never
// fail the toggle over it.
if (enabled && wf.builtin_key === 'invoice_dunning') {
try {
const n = await require('../services/workflows').backfillDunningRuns();
const n = await require('../services/workflows').backfillDunningRuns(id);
require('../utils/logger').info('[workflow] dunning enabled — enrolled existing invoices', { enrolled: n });
} catch (e) {
require('../utils/logger').warn('[workflow] dunning backfill failed', { error: e.message });
+11 -2
View File
@@ -52,7 +52,10 @@ function computeWakeAt(config = {}, vars = {}) {
// Anchor to a context var when given (e.g. dueDate), plus any delay offset —
// so `{ untilVar: 'dueDate', delayDays: 7 }` means "due date + 7 days"
// (absolute), and an already-past anchor resumes immediately. Backward
// compatible: untilVar-only → the var; delay-only → now + delay.
// compatible: untilVar-only → the var; delay-only → now + delay. Behaviour
// change for the both-fields case (untilVar + delay): previously the delay was
// ignored and only the var returned; now they add (this is the intended
// waitGrace semantics — no seeded node relied on the old both-fields path).
const base = (cfg.untilVar && vars[cfg.untilVar]) ? new Date(vars[cfg.untilVar]) : new Date();
return new Date(base.getTime() + ms).toISOString();
}
@@ -432,8 +435,13 @@ async function isBuiltinFlowActive(builtinKey) {
* Idempotent: emitWorkflowEvent's per-(flow, entity) dedup means at most one
* run per invoice, so re-enabling is safe. Paired with the due-date-anchored
* grace wait, already-overdue invoices dun on their real timeline immediately.
*
* Scoped to `targetWorkflowId` (the dunning flow being enabled) so the backfill
* only enrolls invoices into dunning — never into unrelated custom `invoice.sent`
* flows an admin may have built, which would fire their actions for every
* historical invoice.
*/
async function backfillDunningRuns() {
async function backfillDunningRuns(targetWorkflowId) {
let enrolled = 0;
try {
if (!(await db.schema.hasTable('invoices'))) return 0;
@@ -445,6 +453,7 @@ async function backfillDunningRuns() {
const ids = await emitWorkflowEvent('invoice.sent', {
entityType: 'invoice',
entityId: inv.id,
targetWorkflowId,
payload: {
invoiceId: inv.id,
invoiceNumber: inv.invoice_number,