feat(workflows): pre-event reminder picks the template GROUP on the block, type stays automatic

The reminder template family (prefix) is now chosen on the notify_pre_event
block via config.templateGroup (default 'event_reminder'); within that group the
exact template is still auto-resolved per event type:
  <group>_<eventType> if authored  →  else  <group>_default
So an admin can point a flow at a different reminder family, while wedding/
birthday/… routing and the catch-all fallback stay automatic. resolveTemplateKey
now takes (eventType, group) and tolerates a trailing "_" on the group.

Editor: notify_pre_event (+ the gallery notify actions) added to the action
dropdown, with a "Reminder template group" field and hint. Seed sets
templateGroup='event_reminder' on the built-in (v4). EN/DE strings. Tests cover
the per-type / group-default resolution.
This commit is contained in:
Luca
2026-06-24 11:26:02 +02:00
parent 5fbe514db6
commit 10d091b55e
8 changed files with 53 additions and 13 deletions
@@ -402,6 +402,19 @@ describe('workflow engine', () => {
expect(again.reason).toBe('already_sent');
});
test('reminder template resolves per event type within the chosen group, else group default', async () => {
const { _internal } = require('../../src/services/eventReminderService');
// Per-type template exists within a custom group → used.
await db('email_templates').insert({ template_key: 'promo_wedding' });
expect(await _internal.resolveTemplateKey('wedding', 'promo')).toBe('promo_wedding');
// A type with no authored template (in any group) → the group's default.
expect(await _internal.resolveTemplateKey('zzznotype', 'promo')).toBe('promo_default');
// Blank group → the default event_reminder group.
expect(await _internal.resolveTemplateKey('zzznotype')).toBe('event_reminder_default');
// Trailing underscore on the group is tolerated.
expect(await _internal.resolveTemplateKey('zzznotype', 'promo_')).toBe('promo_default');
});
test('isBuiltinFlowActive reflects the built-in ENABLED state (enabled-based mutex)', async () => {
const { seedBuiltinWorkflowsAtBoot } = require('../../src/services/_workflowSeedBoot');
await seedBuiltinWorkflowsAtBoot(db, { info() {}, warn() {} });
+2 -2
View File
@@ -161,7 +161,7 @@ function buildBookingInvoiceOnlyGraph() {
function buildPreEventEmailGraph() {
const nodes = [
{ node_key: 't', type: 'trigger', config: {}, pos_x: 240, pos_y: 0 },
{ node_key: 'notify', type: 'action', config: { action: 'notify_pre_event' }, pos_x: 240, pos_y: 110 },
{ node_key: 'notify', type: 'action', config: { action: 'notify_pre_event', templateGroup: 'event_reminder' }, pos_x: 240, pos_y: 110 },
{ node_key: 'done', type: 'action', config: { action: 'noop' }, pos_x: 240, pos_y: 220 },
];
const edges = [
@@ -267,7 +267,7 @@ const BUILTINS = [
},
{
key: 'pre_event_email',
version: 3,
version: 4,
enabled: false,
name: 'Pre-event reminder (built-in)',
trigger_type: 'event.date_approaching',
+15 -9
View File
@@ -61,6 +61,7 @@ const logger = require('../utils/logger');
const { ensureEventReminderTemplatesSeeded } = require('./eventReminderTemplates');
const DEFAULT_DAYS_BEFORE = 2;
const DEFAULT_TEMPLATE_GROUP = 'event_reminder';
const TEMPLATE_KEY_DEFAULT = 'event_reminder_default';
const TEMPLATE_KEY_PREFIX = 'event_reminder_';
@@ -71,20 +72,23 @@ const TEMPLATE_KEY_PREFIX = 'event_reminder_';
let schemaWarnLogged = false;
/**
* Lookup the most specific available template for an event_type slug.
* Returns the template_key string. The email_processor handles missing
* template rows by failing the send; we don't fetch the row body here
* because emailProcessor.queueEmail does that lookup itself.
* Resolve the reminder template within a GROUP (template-key prefix). The group
* is chosen on the flow block (defaults to `event_reminder`); within it the pick
* is automatic and per-event-type:
* `<group>_<eventType>` if a template exists → else `<group>_default`
* So an exact wedding/birthday/… template wins; otherwise the group's catch-all.
* emailProcessor handles a missing template row itself, so we only return a key.
*/
async function resolveTemplateKey(eventType) {
async function resolveTemplateKey(eventType, group = DEFAULT_TEMPLATE_GROUP) {
const g = String(group || DEFAULT_TEMPLATE_GROUP).replace(/_+$/, ''); // tolerate a trailing "_"
if (eventType) {
const perType = `${TEMPLATE_KEY_PREFIX}${eventType}`;
const perType = `${g}_${eventType}`;
const exists = await db('email_templates')
.where({ template_key: perType })
.first('id');
if (exists) return perType;
}
return TEMPLATE_KEY_DEFAULT;
return `${g}_default`;
}
/**
@@ -259,7 +263,7 @@ async function runEventReminderPass() {
* disabled, already sent, no template-eligible recipient); only DB/queue errors
* propagate so the caller can surface them.
*/
async function sendReminderForEvent(eventId) {
async function sendReminderForEvent(eventId, { templateGroup = null } = {}) {
const hasCols = await hasColumnCached('events', 'event_reminder_sent_at');
if (!hasCols) return { sent: 0, skipped: 1, reason: 'schema_not_migrated' };
@@ -292,7 +296,9 @@ async function sendReminderForEvent(eventId) {
const profile = await db('business_profile').where({ id: 1 }).first('company_name');
const businessName = profile?.company_name || '';
const templateKey = await resolveTemplateKey(row.event_type);
// The flow block chooses the template GROUP (blank → the default group); the
// exact template is still auto-picked by event type within that group.
const templateKey = await resolveTemplateKey(row.event_type, templateGroup || DEFAULT_TEMPLATE_GROUP);
const payload = composePayload({ event: row, recipientEmail, daysBefore: offsetDays, businessName });
if (row.event_reminder_body_override) payload.body_override = row.event_reminder_body_override;
+6 -2
View File
@@ -170,8 +170,12 @@ registry.registerAction('notify_gallery_expired', async (ctx) => {
registry.registerAction('notify_pre_event', async (ctx) => {
const id = ctx.run.entity_id;
if (!id) return { skipped: true, reason: 'no event entity' };
if (ctx.vars?.__dryRun) return { dryRun: true, would: 'notify_pre_event', eventId: id };
const res = await require('../eventReminderService').sendReminderForEvent(id);
// The template GROUP is chosen on THIS block (config.templateGroup, e.g.
// 'event_reminder'); the exact template is still auto-picked by event type
// within that group. Blank → the default group.
const templateGroup = ctx.node.config?.templateGroup || null;
if (ctx.vars?.__dryRun) return { dryRun: true, would: 'notify_pre_event', eventId: id, templateGroup };
const res = await require('../eventReminderService').sendReminderForEvent(id, { templateGroup });
return res;
});