/** * 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, };