Merge pull request #676 from the-luap/fix/whatsapp-public-events-route-647
fix(events): wire customer notifications into both public API entry points (#647)
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
|
// Use parseStringInput from shared parsers for customer data extraction
|
||||||
const getCustomerNameFromPayload = (payload = {}) => parseStringInput(payload.customer_name);
|
const getCustomerNameFromPayload = (payload = {}) => parseStringInput(payload.customer_name);
|
||||||
const getCustomerEmailFromPayload = (payload = {}) => parseStringInput(payload.customer_email);
|
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) => {
|
const mapEventForApi = (event) => {
|
||||||
if (!event || typeof event !== 'object') {
|
if (!event || typeof event !== 'object') {
|
||||||
@@ -69,6 +87,9 @@ router.post('/', adminAuth, [
|
|||||||
body('event_date').isDate(),
|
body('event_date').isDate(),
|
||||||
body('customer_name').notEmpty().trim(),
|
body('customer_name').notEmpty().trim(),
|
||||||
body('customer_email').isEmail().normalizeEmail(IDENTITY_PRESERVING_NORMALIZE_EMAIL),
|
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('admin_email').isEmail(),
|
||||||
body('require_password').optional().isBoolean(),
|
body('require_password').optional().isBoolean(),
|
||||||
body('password').optional().isString().custom((value, { req }) => {
|
body('password').optional().isString().custom((value, { req }) => {
|
||||||
@@ -109,6 +130,8 @@ router.post('/', adminAuth, [
|
|||||||
}
|
}
|
||||||
|
|
||||||
const customerColumnsAvailable = await hasCustomerContactColumns();
|
const customerColumnsAvailable = await hasCustomerContactColumns();
|
||||||
|
const phoneEnabled = await isPhoneFieldEnabled();
|
||||||
|
const customerPhone = phoneEnabled ? getCustomerPhoneFromPayload(req.body) : null;
|
||||||
|
|
||||||
const requirePassword = parseBooleanInput(requirePasswordInput, true);
|
const requirePassword = parseBooleanInput(requirePasswordInput, true);
|
||||||
|
|
||||||
@@ -163,6 +186,7 @@ router.post('/', adminAuth, [
|
|||||||
event_name,
|
event_name,
|
||||||
event_date,
|
event_date,
|
||||||
...(customerColumnsAvailable ? { customer_name: customerName, customer_email: customerEmail } : {}),
|
...(customerColumnsAvailable ? { customer_name: customerName, customer_email: customerEmail } : {}),
|
||||||
|
...(customerPhone ? { customer_phone: customerPhone } : {}),
|
||||||
host_name: customerName,
|
host_name: customerName,
|
||||||
host_email: customerEmail,
|
host_email: customerEmail,
|
||||||
admin_email,
|
admin_email,
|
||||||
@@ -192,6 +216,29 @@ router.post('/', adminAuth, [
|
|||||||
welcome_message: welcome_message || ''
|
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
|
// Webhook lifecycle (#327). Legacy public endpoint — events go live
|
||||||
// immediately so created + published fire together. Payload uses the
|
// immediately so created + published fire together. Payload uses the
|
||||||
// canonical event subject (#341) — every event.* webhook now includes
|
// canonical event subject (#341) — every event.* webhook now includes
|
||||||
@@ -208,6 +255,7 @@ router.post('/', adminAuth, [
|
|||||||
share_token: shareToken,
|
share_token: shareToken,
|
||||||
customer_name: customerName,
|
customer_name: customerName,
|
||||||
customer_email: customerEmail,
|
customer_email: customerEmail,
|
||||||
|
customer_phone: customerPhone,
|
||||||
});
|
});
|
||||||
await webhookService.fire('event.created', { event: eventSubject });
|
await webhookService.fire('event.created', { event: eventSubject });
|
||||||
await webhookService.fire('event.published', { 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
|
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
|
// Webhook lifecycle (#327). v1 events are not draft-aware, so they're
|
||||||
// both created AND published in the same call. Canonical event
|
// both created AND published in the same call. Canonical event
|
||||||
// subject (#341) — customer contact + share_token always included.
|
// subject (#341) — customer contact + share_token always included.
|
||||||
|
|||||||
Reference in New Issue
Block a user