fix(events): wire customer notifications into both public API entry points (#647)
Two entry points for event creation were missing customer notifications, both discovered while triaging @Rekoo-PS's report that "API created events" don't send WhatsApp after #649/#650 landed. POST /api/v1/events (the OpenAPI-spec'd bearer-token API at v1/events.js): - gallery_created email was NEVER queued — only the webhook fired. - WhatsApp was NEVER queued either. POST /api/events (legacy admin-auth route at routes/events.js): - gallery_created email was queued, but WhatsApp was not. - customer_phone wasn't read from the body at all. Both routes now mirror the adminEvents.js create-and-publish path: best-effort queues that never block the API response, gated on customer_email / customer_phone presence and the global event_phone_field_enabled toggle for the phone field. The webhook subject from POST /api/events now also includes customer_phone, so downstream integrations get the same shape as the v1 API. No schema change. No migration. customer_phone column already exists on events (migration 080). WhatsApp config + template_language + template_params resolve through the existing queue processor.
This commit is contained in:
@@ -18,6 +18,24 @@ const { IDENTITY_PRESERVING_NORMALIZE_EMAIL } = require('../utils/emailNormaliza
|
||||
// Use parseStringInput from shared parsers for customer data extraction
|
||||
const getCustomerNameFromPayload = (payload = {}) => parseStringInput(payload.customer_name);
|
||||
const getCustomerEmailFromPayload = (payload = {}) => parseStringInput(payload.customer_email);
|
||||
const getCustomerPhoneFromPayload = (payload = {}) => parseStringInput(payload.customer_phone);
|
||||
|
||||
// Whether the global "phone field" toggle (#322) is enabled. Same shape as
|
||||
// the helper in adminEvents.js — kept local so this route doesn't import
|
||||
// from a sibling route file.
|
||||
const isPhoneFieldEnabled = async () => {
|
||||
try {
|
||||
const row = await db('app_settings').where('setting_key', 'event_phone_field_enabled').first();
|
||||
if (!row) return false;
|
||||
let value = row.setting_value;
|
||||
if (typeof value === 'string') {
|
||||
try { value = JSON.parse(value); } catch { /* keep raw */ }
|
||||
}
|
||||
return value === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const mapEventForApi = (event) => {
|
||||
if (!event || typeof event !== 'object') {
|
||||
@@ -69,6 +87,9 @@ router.post('/', adminAuth, [
|
||||
body('event_date').isDate(),
|
||||
body('customer_name').notEmpty().trim(),
|
||||
body('customer_email').isEmail().normalizeEmail(IDENTITY_PRESERVING_NORMALIZE_EMAIL),
|
||||
body('customer_phone').optional({ nullable: true, checkFalsy: true })
|
||||
.isString().trim()
|
||||
.isLength({ max: 32 }).withMessage('Phone number must be at most 32 characters'),
|
||||
body('admin_email').isEmail(),
|
||||
body('require_password').optional().isBoolean(),
|
||||
body('password').optional().isString().custom((value, { req }) => {
|
||||
@@ -109,6 +130,8 @@ router.post('/', adminAuth, [
|
||||
}
|
||||
|
||||
const customerColumnsAvailable = await hasCustomerContactColumns();
|
||||
const phoneEnabled = await isPhoneFieldEnabled();
|
||||
const customerPhone = phoneEnabled ? getCustomerPhoneFromPayload(req.body) : null;
|
||||
|
||||
const requirePassword = parseBooleanInput(requirePasswordInput, true);
|
||||
|
||||
@@ -163,6 +186,7 @@ router.post('/', adminAuth, [
|
||||
event_name,
|
||||
event_date,
|
||||
...(customerColumnsAvailable ? { customer_name: customerName, customer_email: customerEmail } : {}),
|
||||
...(customerPhone ? { customer_phone: customerPhone } : {}),
|
||||
host_name: customerName,
|
||||
host_email: customerEmail,
|
||||
admin_email,
|
||||
@@ -192,6 +216,29 @@ router.post('/', adminAuth, [
|
||||
welcome_message: welcome_message || ''
|
||||
});
|
||||
|
||||
// WhatsApp gallery_ready notification (#647 follow-up). Mirrors the
|
||||
// adminEvents.js path: fires when the customer supplied a phone, the
|
||||
// feature is enabled, and a config exists. Non-fatal — a queue failure
|
||||
// must never block gallery creation.
|
||||
if (customerPhone) {
|
||||
try {
|
||||
const { queueWhatsapp, getWhatsAppConfig } = require('../services/whatsappProcessor');
|
||||
const waConfig = await getWhatsAppConfig();
|
||||
if (waConfig && waConfig.enabled) {
|
||||
await queueWhatsapp(eventId, customerPhone, 'gallery_created', {
|
||||
customer_name: customerName || '',
|
||||
event_name,
|
||||
gallery_link: shareUrl,
|
||||
gallery_password: requirePassword ? password : '',
|
||||
expiry_date: expires_at ? expires_at.toISOString() : null,
|
||||
language: null,
|
||||
});
|
||||
}
|
||||
} catch (waError) {
|
||||
console.warn('Failed to queue WhatsApp notification on create', waError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook lifecycle (#327). Legacy public endpoint — events go live
|
||||
// immediately so created + published fire together. Payload uses the
|
||||
// canonical event subject (#341) — every event.* webhook now includes
|
||||
@@ -208,6 +255,7 @@ router.post('/', adminAuth, [
|
||||
share_token: shareToken,
|
||||
customer_name: customerName,
|
||||
customer_email: customerEmail,
|
||||
customer_phone: customerPhone,
|
||||
});
|
||||
await webhookService.fire('event.created', { event: eventSubject });
|
||||
await webhookService.fire('event.published', { event: eventSubject });
|
||||
|
||||
@@ -308,6 +308,48 @@ router.post(
|
||||
type: 'admin', id: req.admin.id, name: req.admin.username
|
||||
});
|
||||
|
||||
// Customer notifications (#647 follow-up). v1 events go live in the
|
||||
// same call (not draft-aware), so the gallery_created email + WhatsApp
|
||||
// fire here — mirroring the adminEvents.js create-and-publish path.
|
||||
// Both are best-effort: a queue failure must not block the API response.
|
||||
const expiryIso = expires_at ? new Date(expires_at).toISOString() : null;
|
||||
if (customer_email) {
|
||||
try {
|
||||
const { queueEmail } = require('../../services/emailProcessor');
|
||||
await queueEmail(id, customer_email, 'gallery_created', {
|
||||
customer_name: customer_name || '',
|
||||
customer_email,
|
||||
host_name: customer_name || '',
|
||||
event_name,
|
||||
event_date: event_date || null,
|
||||
gallery_link: shareUrl,
|
||||
gallery_password: require_password ? password : 'No password required',
|
||||
expiry_date: expiryIso,
|
||||
welcome_message: ''
|
||||
});
|
||||
} catch (emailError) {
|
||||
logger.warn('v1 POST /events: failed to queue gallery_created email', { error: emailError.message });
|
||||
}
|
||||
}
|
||||
if (persistPhone) {
|
||||
try {
|
||||
const { queueWhatsapp, getWhatsAppConfig } = require('../../services/whatsappProcessor');
|
||||
const waConfig = await getWhatsAppConfig();
|
||||
if (waConfig && waConfig.enabled) {
|
||||
await queueWhatsapp(id, persistPhone, 'gallery_created', {
|
||||
customer_name: customer_name || '',
|
||||
event_name,
|
||||
gallery_link: shareUrl,
|
||||
gallery_password: require_password ? password : '',
|
||||
expiry_date: expiryIso,
|
||||
language: null,
|
||||
});
|
||||
}
|
||||
} catch (waError) {
|
||||
logger.warn('v1 POST /events: failed to queue WhatsApp notification', { error: waError.message });
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook lifecycle (#327). v1 events are not draft-aware, so they're
|
||||
// both created AND published in the same call. Canonical event
|
||||
// subject (#341) — customer contact + share_token always included.
|
||||
|
||||
Reference in New Issue
Block a user