Files
hope-events/backend/src/utils/waMessages.js
T
joshuaandClaude Sonnet 5 c07e9c928a Add door check-in flow for Main Tickets
Lets staff redeem a registration's Main Tickets by quantity at the door
(via the Payment/registration flow) instead of scanning each QR code,
and automatically emails/WhatsApps a check-in confirmation to the
attendee. Also routes the At The Door "Open" button and post-payment
flow dynamically: paid registrations jump straight to Check-In instead
of a forced ticket print, since tickets are already sent automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:56:31 +02:00

279 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* WhatsApp-formatted message builders.
*
* Styling reference: https://faq.whatsapp.com/539178204879377/
* *bold* _italic_ ~strikethrough~ ```monospace```
* > blockquote - unordered list 1. numbered list
* # Heading 1 ## Heading 2 ### Heading 3
*/
function fmtAmount(amt) {
return `R${Number(amt || 0).toFixed(2)}`;
}
function fmtDateShort(d) {
try {
return new Date(d).toLocaleDateString('en-GB', {
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
});
} catch { return String(d); }
}
const { getSettingSync } = require('./settingsCache');
function getOrg() {
return {
name: getSettingSync('org_name', process.env.ORG_NAME || 'Hope Events'),
email: process.env.EMAIL_FROM || process.env.EMAIL_USER || '',
url: (process.env.FRONTEND_URL || process.env.APP_BASE_URL || 'http://localhost:3001').replace(/\/$/, ''),
};
}
// ─── Registration confirmation ────────────────────────────────────────────────
/**
* @param {object} reg - full registration from loadRegistrationFull
* @param {{ isNew?: boolean, balance?: number, totalDue?: number, totalPaid?: number }} opts
*/
function buildWARegistration(reg, { isNew = true, balance, totalDue, totalPaid } = {}) {
const org = getOrg();
const eventTitle = reg.event?.title || 'the event';
const eventDate = reg.event?.startDate ? fmtDateShort(reg.event.startDate) : '';
const name = reg.user?.name || 'there';
const heading = isNew ? '🎉 *Registration Confirmed!*' : '✏️ *Registration Updated*';
const intro = isNew
? `You're registered for *${eventTitle}*${eventDate ? ` on ${eventDate}` : ''}.`
: `Your registration for *${eventTitle}* has been updated.`;
const items = (reg.registrationOptions || [])
.map(ro => `- ${ro.eventOption?.name || 'Option'} ×${ro.quantity}${fmtAmount((ro.eventOption?.price || 0) * ro.quantity)}`)
.join('\n');
const paid = totalPaid ?? (reg.payments || []).reduce((s, p) => s + (p.amount || 0), 0);
const due = totalDue ?? 0;
const bal = balance ?? Math.max(due - paid, 0);
const finLine = bal <= 0
? `✅ *Fully paid — you're all set!*`
: `*Balance due:* ${fmtAmount(bal)}\n_Pay at ${org.url} or at the door._`;
return [
heading,
'',
`Hi ${name},`,
'',
intro,
'',
'*Your selections:*',
items || '—',
'',
`*Total:* ${fmtAmount(due)} *Paid:* ${fmtAmount(paid)}`,
finLine,
'',
`_${org.name}_ | ${org.url}`,
].join('\n');
}
// ─── Payment receipt ──────────────────────────────────────────────────────────
function buildWAPayment(payment) {
const org = getOrg();
const eventTitle = payment.registration?.event?.title || payment.event?.title || 'the event';
const name = (payment.registration?.user || payment.user)?.name || 'there';
const amount = fmtAmount(payment.amount);
const reg = payment.registration;
let balLine = '';
if (reg) {
const { computeRegistrationTotalDue } = require('./pricing');
const totalDue = computeRegistrationTotalDue(reg, payment.createdAt || new Date());
const totalPaid = (reg.payments || []).reduce((s, p) => s + (p.amount || 0), 0);
const balance = Math.max(totalDue - totalPaid, 0);
balLine = balance <= 0
? `\n✅ *Fully paid!* Your tickets have been sent.`
: `\n*Remaining balance:* ${fmtAmount(balance)}\n_Pay the remainder at ${org.url} or at the door._`;
}
return [
`✅ *Payment Received*`,
'',
`Hi ${name},`,
'',
`We've received your payment of *${amount}* for *${eventTitle}*.`,
balLine,
'',
`_${org.name}_ | ${org.url}`,
].join('\n');
}
// ─── Check-in confirmation (Main Tickets, door check-in) ───────────────────────
function buildWACheckIn(ticket, qtyRedeemed, totalRedeemed, remaining) {
const org = getOrg();
const eventTitle = ticket.event?.title || 'the event';
const name = ticket.user?.name || 'there';
const fullyCheckedIn = remaining <= 0;
return [
`✅ *Checked In*`,
'',
`Hi ${name},`,
'',
`*${qtyRedeemed}* checked in for *${eventTitle}*.`,
`*${totalRedeemed} of ${ticket.quantity || 1}* checked in${fullyCheckedIn ? '' : `, *${remaining}* remaining`}.`,
'',
`_${org.name}_ | ${org.url}`,
].join('\n');
}
// ─── Login notification ───────────────────────────────────────────────────────
function buildWALogin({ name, when, location, userAgent }) {
const org = getOrg();
return [
`🔐 *New Login Detected*`,
'',
`Hi ${name || 'there'},`,
'',
`A new login to your *${org.name}* account was detected.`,
'',
`*Time:* ${when}`,
`*Location:* ${location}`,
`*Device:* ${userAgent}`,
'',
`> _Not you?_ Change your password immediately at ${org.url} or contact ${org.email}.`,
'',
`_If this was you, no action is needed._`,
].join('\n');
}
// ─── Welcome ──────────────────────────────────────────────────────────────────
function buildWAWelcome({ name, events }) {
const org = getOrg();
const hasEvents = Array.isArray(events) && events.length > 0;
const eventsBlock = hasEvents
? [
'*Upcoming events:*',
...events.map(e =>
`- *${e.title}* — ${new Date(e.startDate).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}`
),
].join('\n')
: 'Keep an eye on our website for upcoming events.';
return [
`🎉 *Welcome to ${org.name}!*`,
'',
`Hi ${name || 'there'},`,
'',
`Your account is set up and ready. Use it to register for events, manage your bookings, and access your tickets.`,
'',
eventsBlock,
'',
`${org.url}`,
].join('\n');
}
// ─── Account closed ───────────────────────────────────────────────────────────
function buildWAAccountClosed({ name, dataDeleted }) {
const org = getOrg();
const detail = dataDeleted
? 'All personal data associated with your account has been permanently deleted.'
: `Your account has been deactivated. To also delete your personal data, contact ${org.email}.`;
return [
`🔒 *Account Closed*`,
'',
`Hi ${name || 'there'},`,
'',
`Your *${org.name}* account has been successfully closed.`,
'',
detail,
'',
`_${org.name}_ | ${org.email}`,
].join('\n');
}
// ─── Ticket delivery caption ──────────────────────────────────────────────────
function buildWATicketCaption({ name, eventTitle, eventDate }) {
return [
`🎟️ *Your tickets for ${eventTitle}*`,
'',
`Hi ${name || 'there'}! Your tickets${eventDate ? ` for *${eventDate}*` : ''} are attached.`,
'Please show this PDF (printed or on your phone) at the event entrance.',
].join('\n');
}
// ─── Refund notification ──────────────────────────────────────────────────────
function buildWARefund(payment) {
const org = getOrg();
const user = payment.user;
const eventTitle = payment.registration?.event?.title || payment.event?.title || 'the event';
const amt = fmtAmount(Math.abs(payment.amount || 0));
const name = user?.name || 'there';
return [
`💸 *Refund Processed*`,
'',
`Hi ${name},`,
'',
`A refund of *${amt}* for *${eventTitle}* has been processed.`,
'',
`Refunds may take a few business days to appear depending on your bank.`,
'',
`_${org.name}_ | ${org.email}`,
].join('\n');
}
// ─── Donation applied to a registration ────────────────────────────────────────
//
// Distinct from buildWAPayment: sent to the REGISTRANT when staff apply someone else's
// donation to their registration — anonymous (no donor name), and never "payment received"
// wording since they didn't pay anything themselves.
function buildWADonationAppliedToRegistrant(payment) {
const org = getOrg();
const reg = payment.registration;
const eventTitle = reg?.event?.title || 'the event';
const name = reg?.user?.name || 'there';
const amount = fmtAmount(payment.amount);
let balLine = '';
if (reg) {
const { computeRegistrationTotalDue } = require('./pricing');
const totalDue = computeRegistrationTotalDue(reg, payment.createdAt || new Date());
const totalPaid = (reg.payments || []).reduce((s, p) => s + (p.amount || 0), 0);
const balance = Math.max(totalDue - totalPaid, 0);
balLine = balance <= 0
? `\n✅ *Fully paid!* Your tickets have been sent.`
: `\n*Remaining balance:* ${fmtAmount(balance)}\n_Pay the remainder at ${org.url} or at the door._`;
}
return [
`🎁 *Donation Applied*`,
'',
`Hi ${name},`,
'',
`A donation of *${amount}* was applied to your registration for *${eventTitle}*.`,
balLine,
'',
`_${org.name}_ | ${org.url}`,
].join('\n');
}
module.exports = {
buildWARegistration,
buildWAPayment,
buildWARefund,
buildWADonationAppliedToRegistrant,
buildWALogin,
buildWAWelcome,
buildWAAccountClosed,
buildWATicketCaption,
buildWACheckIn,
};