feat(workflows): per-quote booking-workflow picker + quote→invoice (no gallery) built-in

A quote can now choose which flow runs on acceptance instead of every enabled
quote.accepted flow firing. Migration 147 adds quotes.booking_workflow_id; the
editor shows a "Booking workflow (on acceptance)" dropdown listing the
quote.accepted flows (workflow-engine flag only); emitQuoteEvent passes it as
the new emitWorkflowEvent targetWorkflowId so ONLY the picked flow runs (still
gated on enabled + trigger match → a disabled/None selection runs nothing).

Adds the booking_invoice_only built-in (quote.accepted → prepare invoice →
review gate → send; no event/gallery, no wait), the variant requested for
shoots billed without an online gallery. Disabled stub like the other booking
flows until the prepare_*/send_document cutover.

Tests: targetWorkflowId runs only the selected flow; invoice-only built-in has
no wait/prepare_event.
This commit is contained in:
Luca
2026-06-23 23:15:31 +02:00
parent eec262b0a7
commit d14f1d850c
10 changed files with 170 additions and 2 deletions
+8 -2
View File
@@ -240,7 +240,7 @@ async function resumeRun(runId, { decisionHandle = null } = {}) {
* workflow (idempotent via dedup_key) and starts it. Never throws — safe to
* call after a caller's commit. Fails CLOSED if the flag system is unavailable.
*/
async function emitWorkflowEvent(triggerType, { entityType = null, entityId = null, payload = {} } = {}) {
async function emitWorkflowEvent(triggerType, { entityType = null, entityId = null, payload = {}, targetWorkflowId = null } = {}) {
try {
const { isFeatureEnabled } = require('../../middleware/requireFeatureFlag');
let enabled = false;
@@ -250,7 +250,13 @@ async function emitWorkflowEvent(triggerType, { entityType = null, entityId = nu
}
if (!enabled) return [];
const workflows = await db('workflows').where({ enabled: true, trigger_type: triggerType });
// targetWorkflowId restricts the fan-out to a SINGLE chosen flow — used when
// the entity explicitly selected which flow to run (e.g. a quote picks its
// booking workflow). Still gated on enabled + matching trigger_type, so a
// disabled/mismatched selection simply runs nothing.
const q = db('workflows').where({ enabled: true, trigger_type: triggerType });
if (targetWorkflowId != null) q.where({ id: targetWorkflowId });
const workflows = await q;
const runIds = [];
for (const wf of workflows) {
const tcfg = parseJson(wf.trigger_config, {});