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:
@@ -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() {} });
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user