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>
This commit is contained in:
@@ -80,6 +80,17 @@ async function loadRegistrationFull(registrationId) {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTicketFull(ticketId) {
|
||||
return prisma.ticket.findUnique({
|
||||
where: { id: ticketId },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true, phoneNumber: true, notificationPreference: true } },
|
||||
event: true,
|
||||
registrationOption: { include: { eventOption: true, variant: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function loadPaymentFull(paymentId) {
|
||||
return prisma.payment.findUnique({
|
||||
where: { id: paymentId },
|
||||
@@ -488,6 +499,33 @@ function buildPaymentReceipt(payment) {
|
||||
return { subject, text, html: emailWrapper(body, { preheader }) };
|
||||
}
|
||||
|
||||
// ─── Check-in confirmation (Main Tickets, door check-in) ───────────────────────
|
||||
|
||||
function buildCheckInConfirmation(ticket, qtyRedeemed, totalRedeemed, remaining) {
|
||||
const org = getOrg();
|
||||
const eventTitle = ticket.event?.title || 'the event';
|
||||
const optionName = ticket.registrationOption?.eventOption?.name || 'Main Ticket';
|
||||
const fullyCheckedIn = remaining <= 0;
|
||||
const subject = `Checked in – ${eventTitle}`;
|
||||
const preheader = `${qtyRedeemed} checked in for ${eventTitle}.`;
|
||||
|
||||
const body = `
|
||||
<p style="font-size:22px;font-weight:800;color:#0f172a;margin:0 0 8px 0;letter-spacing:-0.3px">You're checked in! ✅</p>
|
||||
<p style="font-size:14px;color:#64748b;margin:0 0 32px 0">See you inside</p>
|
||||
|
||||
<p style="margin:0 0 4px 0;color:#374151;font-family:${ff}">Hi <strong>${ticket.user?.name || 'there'}</strong>,</p>
|
||||
<p style="margin:0 0 28px 0;color:#374151;font-family:${ff}">
|
||||
<strong>${qtyRedeemed}</strong> ${optionName}${qtyRedeemed === 1 ? '' : 's'} just checked in for <strong>${eventTitle}</strong>.
|
||||
</p>
|
||||
|
||||
${callout(`<strong style="font-size:15px">${totalRedeemed} of ${ticket.quantity || 1} checked in</strong>${
|
||||
fullyCheckedIn ? '' : `<br/><span style="font-size:13px">${remaining} remaining on this ticket</span>`
|
||||
}`, fullyCheckedIn ? 'success' : 'info')}`;
|
||||
|
||||
const text = `You're checked in!\n\nHi ${ticket.user?.name || 'there'},\n\n${qtyRedeemed} ${optionName}(s) just checked in for ${eventTitle}.\n\n${totalRedeemed} of ${ticket.quantity || 1} checked in${fullyCheckedIn ? '' : `, ${remaining} remaining`}.\n\n${org.name} — ${org.email}`;
|
||||
return { subject, text, html: emailWrapper(body, { preheader }) };
|
||||
}
|
||||
|
||||
// ─── Donation applied to a registration ────────────────────────────────────────
|
||||
//
|
||||
// Distinct from buildPaymentReceipt: this is sent to the REGISTRANT when staff apply
|
||||
@@ -893,6 +931,32 @@ async function sendRefundEmail(refundPaymentId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCheckInEmails(ticketId, qtyRedeemed, totalRedeemed, remaining) {
|
||||
try {
|
||||
const ticket = await loadTicketFull(ticketId);
|
||||
if (!ticket) return;
|
||||
const user = ticket.user;
|
||||
const { shouldEmail, waText, waTextAny } = require('./notify');
|
||||
const { buildWACheckIn } = require('./waMessages');
|
||||
|
||||
const sends = [];
|
||||
const hasValidEmail = user?.email && !user.email.endsWith('@guest.local') && !user.email.endsWith('@deleted.invalid');
|
||||
if (hasValidEmail && shouldEmail(user)) {
|
||||
const msg = buildCheckInConfirmation(ticket, qtyRedeemed, totalRedeemed, remaining);
|
||||
sends.push(sendMail({ to: user.email, subject: msg.subject, html: msg.html, text: msg.text }));
|
||||
}
|
||||
const waMsg = buildWACheckIn(ticket, qtyRedeemed, totalRedeemed, remaining);
|
||||
if (hasValidEmail) {
|
||||
sends.push(waText(user, waMsg));
|
||||
} else {
|
||||
sends.push(waTextAny(user, waMsg));
|
||||
}
|
||||
await Promise.all(sends);
|
||||
} catch (e) {
|
||||
console.error('Failed to send check-in emails:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Sent when staff apply a donation to someone's registration. Only the registrant is
|
||||
// notified (anonymously, per design) — the donor already received their donation-received
|
||||
// notification when the donation was originally made, so they are deliberately not emailed
|
||||
@@ -981,4 +1045,6 @@ module.exports = {
|
||||
sendRefundEmail,
|
||||
sendSelfServiceRegistrationEmails,
|
||||
sendDonationAssignmentEmails,
|
||||
sendCheckInEmails,
|
||||
buildCheckInConfirmation,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user