Add PDF invoices/receipts with manual send, fix early-bird tier edit data loss
- Registration confirmations attach an invoice PDF (itemized breakdown, early-bird discount, balance due, Yoco pay-now link/QR) whenever a balance is outstanding; payment/donation confirmations attach a payment receipt PDF. Sent as an email attachment and, over WhatsApp, as the PDF itself with the existing message as its caption. - Users can also (re)send either document on demand: an "Invoice" button on the registration detail popup, and a "Receipt" button next to each payment there and on the Payment history page, each opening an Email/WhatsApp choice popup, via two new endpoints restricted to the registration/payment's own owner. - Fix: editing an event option's early-bird tiers deleted and recreated every tier for that option with brand-new ids, silently severing the appliedTierId link on all historical purchases (losing early-bird attribution and undercounting stock-limit usage) even for tiers the admin didn't touch. Tiers are now upserted by id. - Update the "My Events" help content and the API docs index for the new endpoints. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
const fs = require('fs');
|
||||
const prisma = require('../config/db');
|
||||
const { sendMail, emailWrapper, ctaButton, fallbackLink, divider, callout, paymentOption } = require('./email');
|
||||
const { computeRegistrationTotalDue, computeOptionLineTotal } = require('./pricing');
|
||||
@@ -72,7 +73,7 @@ async function loadRegistrationFull(registrationId) {
|
||||
return prisma.registration.findUnique({
|
||||
where: { id: registrationId },
|
||||
include: {
|
||||
registrationOptions: { include: { eventOption: { include: { earlyBirdTiers: true } }, tranches: true } },
|
||||
registrationOptions: { include: { eventOption: { include: { earlyBirdTiers: true } }, variant: true, tranches: true } },
|
||||
payments: true,
|
||||
user: { select: { id: true, name: true, email: true, phoneNumber: true, isActive: true, notificationPreference: true } },
|
||||
event: { include: { createdBy: { select: { id: true, name: true, email: true } }, notifyRecipients: { select: { id: true, name: true, email: true } } } },
|
||||
@@ -98,7 +99,7 @@ async function loadPaymentFull(paymentId) {
|
||||
user: { select: { id: true, name: true, email: true, phoneNumber: true, notificationPreference: true } },
|
||||
registration: {
|
||||
include: {
|
||||
registrationOptions: { include: { eventOption: { include: { earlyBirdTiers: true } }, tranches: true } },
|
||||
registrationOptions: { include: { eventOption: { include: { earlyBirdTiers: true } }, variant: true, tranches: true } },
|
||||
payments: true,
|
||||
user: { select: { id: true, name: true, email: true, phoneNumber: true, isActive: true, notificationPreference: true } },
|
||||
event: { include: { createdBy: { select: { id: true, name: true, email: true } }, notifyRecipients: { select: { id: true, name: true, email: true } } } },
|
||||
@@ -898,25 +899,74 @@ function buildDailySummary(ev, registrations, payments, now) {
|
||||
|
||||
// ─── Send functions ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generates an invoice PDF for a registration when it still has an outstanding balance,
|
||||
* reusing (or, if none was supplied, creating) a Yoco checkout link so the invoice can carry
|
||||
* a clickable "pay now" link and a scannable QR code. Returns null when nothing is owed or
|
||||
* PDF generation fails — callers fall back to the existing text-only notification either way.
|
||||
*/
|
||||
/**
|
||||
* Resolves a Yoco checkout link for an invoice's "pay now" link/QR code. Returns the given
|
||||
* hint as-is when supplied; otherwise creates a fresh checkout, or null if nothing is owed
|
||||
* (no point paying) or checkout creation fails (invoice still generates, just without a link).
|
||||
*/
|
||||
async function resolveInvoicePaymentUrl(reg, { totalDue, totalPaid, paymentUrlHint = null }) {
|
||||
if (paymentUrlHint) return paymentUrlHint;
|
||||
if (totalDue - totalPaid <= 0.01) return null;
|
||||
try {
|
||||
const { createRegistrationCheckoutInternal } = require('../controllers/paymentController');
|
||||
const baseUrl = process.env.FRONTEND_URL || process.env.APP_BASE_URL || 'http://localhost:3001';
|
||||
const checkout = await createRegistrationCheckoutInternal(reg.id, reg.userId, {
|
||||
successUrl: `${baseUrl}/payment/success`,
|
||||
cancelUrl: `${baseUrl}/payment/cancel`,
|
||||
failureUrl: `${baseUrl}/payment/failure`,
|
||||
});
|
||||
return checkout.redirectUrl || null;
|
||||
} catch (e) {
|
||||
console.warn('Could not create Yoco checkout for invoice PDF:', e?.message || e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Only used by the automatic registration-confirmation sends, which should stay silent (no
|
||||
// invoice attached) when nothing is owed — receipts already cover the fully-paid case.
|
||||
async function maybeGenerateInvoicePdf(reg, { totalDue, totalPaid, paymentUrlHint = null }) {
|
||||
if (totalDue - totalPaid <= 0.01) return null;
|
||||
const paymentUrl = await resolveInvoicePaymentUrl(reg, { totalDue, totalPaid, paymentUrlHint });
|
||||
try {
|
||||
const { generateInvoicePdf } = require('./pdfDocs');
|
||||
return await generateInvoicePdf(reg, { paymentUrl, totalDue, totalPaid });
|
||||
} catch (e) {
|
||||
console.error('Failed to generate invoice PDF:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendRegistrationEmails(registrationId) {
|
||||
let invoicePdf = null;
|
||||
try {
|
||||
const reg = await loadRegistrationFull(registrationId);
|
||||
if (!reg) return;
|
||||
const { shouldEmail, waText } = require('./notify');
|
||||
const { shouldEmail, waPdf, waText } = require('./notify');
|
||||
const { buildWARegistration } = require('./waMessages');
|
||||
const { computeRegistrationTotalDue } = require('./pricing');
|
||||
|
||||
const sends = [];
|
||||
const totalDue = computeRegistrationTotalDue(reg, new Date());
|
||||
const totalPaid = (reg.payments || []).reduce((s, p) => s + (p.amount || 0), 0);
|
||||
invoicePdf = await maybeGenerateInvoicePdf(reg, { totalDue, totalPaid });
|
||||
|
||||
// Email: only for real addresses (skip guest.local placeholders)
|
||||
if (reg.user?.email && !reg.user.email.endsWith('@guest.local')) {
|
||||
const msg = buildRegistrationConfirmation(reg, { isNew: true });
|
||||
if (shouldEmail(reg.user)) sends.push(sendMail({ to: reg.user.email, subject: msg.subject, html: msg.html, text: msg.text }));
|
||||
if (shouldEmail(reg.user)) sends.push(sendMail({
|
||||
to: reg.user.email, subject: msg.subject, html: msg.html, text: msg.text,
|
||||
...(invoicePdf ? { attachments: [{ filename: invoicePdf.filename, path: invoicePdf.filePath, contentType: 'application/pdf' }] } : {}),
|
||||
}));
|
||||
}
|
||||
// WhatsApp: always attempt — waText checks canWhatsApp (preference + valid phone) internally
|
||||
sends.push(waText(reg.user, buildWARegistration(reg, { isNew: true, totalDue, totalPaid, balance: Math.max(totalDue - totalPaid, 0) })));
|
||||
// WhatsApp: always attempt — waText/waPdf check canWhatsApp (preference + valid phone) internally
|
||||
const waCaption = buildWARegistration(reg, { isNew: true, totalDue, totalPaid, balance: Math.max(totalDue - totalPaid, 0) });
|
||||
sends.push(invoicePdf ? waPdf(reg.user, invoicePdf.filePath, invoicePdf.filename, waCaption) : waText(reg.user, waCaption));
|
||||
|
||||
const adminMsg = buildRegistrationAdminNotice(reg, { isNew: true });
|
||||
if (adminMsg.to && adminMsg.to.length) {
|
||||
@@ -925,25 +975,34 @@ async function sendRegistrationEmails(registrationId) {
|
||||
await Promise.all(sends);
|
||||
} catch (e) {
|
||||
console.error('Failed to send registration emails:', e);
|
||||
} finally {
|
||||
if (invoicePdf) try { fs.unlinkSync(invoicePdf.filePath); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendRegistrationUpdatedEmails(registrationId) {
|
||||
let invoicePdf = null;
|
||||
try {
|
||||
const reg = await loadRegistrationFull(registrationId);
|
||||
if (!reg) return;
|
||||
const { shouldEmail, waText } = require('./notify');
|
||||
const { shouldEmail, waPdf, waText } = require('./notify');
|
||||
const { buildWARegistration } = require('./waMessages');
|
||||
const { computeRegistrationTotalDue } = require('./pricing');
|
||||
|
||||
const sends = [];
|
||||
const totalDue = computeRegistrationTotalDue(reg, new Date());
|
||||
const totalPaid = (reg.payments || []).reduce((s, p) => s + (p.amount || 0), 0);
|
||||
invoicePdf = await maybeGenerateInvoicePdf(reg, { totalDue, totalPaid });
|
||||
|
||||
if (reg.user?.email && !reg.user.email.endsWith('@guest.local')) {
|
||||
const msg = buildRegistrationConfirmation(reg, { isNew: false });
|
||||
if (shouldEmail(reg.user)) sends.push(sendMail({ to: reg.user.email, subject: msg.subject, html: msg.html, text: msg.text }));
|
||||
if (shouldEmail(reg.user)) sends.push(sendMail({
|
||||
to: reg.user.email, subject: msg.subject, html: msg.html, text: msg.text,
|
||||
...(invoicePdf ? { attachments: [{ filename: invoicePdf.filename, path: invoicePdf.filePath, contentType: 'application/pdf' }] } : {}),
|
||||
}));
|
||||
}
|
||||
sends.push(waText(reg.user, buildWARegistration(reg, { isNew: false, totalDue, totalPaid, balance: Math.max(totalDue - totalPaid, 0) })));
|
||||
const waCaption = buildWARegistration(reg, { isNew: false, totalDue, totalPaid, balance: Math.max(totalDue - totalPaid, 0) });
|
||||
sends.push(invoicePdf ? waPdf(reg.user, invoicePdf.filePath, invoicePdf.filename, waCaption) : waText(reg.user, waCaption));
|
||||
const adminMsg = buildRegistrationAdminNotice(reg, { isNew: false, isUpdated: true });
|
||||
if (adminMsg.to && adminMsg.to.length) {
|
||||
sends.push(sendMail({ to: adminMsg.to.join(','), subject: adminMsg.subject, html: adminMsg.html, text: adminMsg.text }));
|
||||
@@ -951,25 +1010,34 @@ async function sendRegistrationUpdatedEmails(registrationId) {
|
||||
await Promise.all(sends);
|
||||
} catch (e) {
|
||||
console.error('Failed to send registration updated emails:', e);
|
||||
} finally {
|
||||
if (invoicePdf) try { fs.unlinkSync(invoicePdf.filePath); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendSelfServiceRegistrationEmails(registrationId, { paymentUrl = null, formRequired = false, isNew = true } = {}) {
|
||||
let invoicePdf = null;
|
||||
try {
|
||||
const reg = await loadRegistrationFull(registrationId);
|
||||
if (!reg) return;
|
||||
const { shouldEmail, waText } = require('./notify');
|
||||
const { shouldEmail, waPdf, waText } = require('./notify');
|
||||
const { buildWARegistration } = require('./waMessages');
|
||||
const { computeRegistrationTotalDue } = require('./pricing');
|
||||
|
||||
const sends = [];
|
||||
const totalDue = computeRegistrationTotalDue(reg, new Date());
|
||||
const totalPaid = (reg.payments || []).reduce((s, p) => s + (p.amount || 0), 0);
|
||||
invoicePdf = await maybeGenerateInvoicePdf(reg, { totalDue, totalPaid, paymentUrlHint: paymentUrl });
|
||||
|
||||
if (reg.user?.email && !reg.user.email.endsWith('@guest.local')) {
|
||||
const msg = buildAdminInitiatedRegistrationConfirmation(reg, { yocoLink: paymentUrl, formRequired, isNew });
|
||||
if (shouldEmail(reg.user)) sends.push(sendMail({ to: reg.user.email, subject: msg.subject, html: msg.html, text: msg.text }));
|
||||
if (shouldEmail(reg.user)) sends.push(sendMail({
|
||||
to: reg.user.email, subject: msg.subject, html: msg.html, text: msg.text,
|
||||
...(invoicePdf ? { attachments: [{ filename: invoicePdf.filename, path: invoicePdf.filePath, contentType: 'application/pdf' }] } : {}),
|
||||
}));
|
||||
}
|
||||
sends.push(waText(reg.user, buildWARegistration(reg, { isNew, totalDue, totalPaid, balance: Math.max(totalDue - totalPaid, 0) })));
|
||||
const waCaption = buildWARegistration(reg, { isNew, totalDue, totalPaid, balance: Math.max(totalDue - totalPaid, 0) });
|
||||
sends.push(invoicePdf ? waPdf(reg.user, invoicePdf.filePath, invoicePdf.filename, waCaption) : waText(reg.user, waCaption));
|
||||
const adminMsg = buildRegistrationAdminNotice(reg, { isNew, isUpdated: !isNew });
|
||||
if (adminMsg.to && adminMsg.to.length) {
|
||||
sends.push(sendMail({ to: adminMsg.to.join(','), subject: adminMsg.subject, html: adminMsg.html, text: adminMsg.text }));
|
||||
@@ -977,28 +1045,40 @@ async function sendSelfServiceRegistrationEmails(registrationId, { paymentUrl =
|
||||
await Promise.all(sends);
|
||||
} catch (e) {
|
||||
console.error('Failed to send self-service registration emails:', e);
|
||||
} finally {
|
||||
if (invoicePdf) try { fs.unlinkSync(invoicePdf.filePath); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPaymentEmails(paymentId) {
|
||||
let receiptPdf = null;
|
||||
try {
|
||||
const payment = await loadPaymentFull(paymentId);
|
||||
if (!payment) return;
|
||||
const user = payment.registration?.user || payment.user;
|
||||
const { shouldEmail, waText, waTextAny } = require('./notify');
|
||||
const { shouldEmail, waPdf, waPdfAny, waText, waTextAny } = require('./notify');
|
||||
const { buildWAPayment } = require('./waMessages');
|
||||
const { generateReceiptPdf } = require('./pdfDocs');
|
||||
|
||||
try { receiptPdf = await generateReceiptPdf(payment); } catch (e) { console.error('Failed to generate receipt PDF:', e); }
|
||||
|
||||
const sends = [];
|
||||
const hasValidEmail = user?.email && !user.email.endsWith('@guest.local') && !user.email.endsWith('@deleted.invalid');
|
||||
if (hasValidEmail) {
|
||||
const msg = buildPaymentReceipt(payment);
|
||||
if (shouldEmail(user)) sends.push(sendMail({ to: user.email, subject: msg.subject, html: msg.html, text: msg.text }));
|
||||
if (shouldEmail(user)) sends.push(sendMail({
|
||||
to: user.email, subject: msg.subject, html: msg.html, text: msg.text,
|
||||
...(receiptPdf ? { attachments: [{ filename: receiptPdf.filename, path: receiptPdf.filePath, contentType: 'application/pdf' }] } : {}),
|
||||
}));
|
||||
}
|
||||
// WhatsApp: respect preference when email is available; use as unconditional fallback when it isn't
|
||||
// WhatsApp: respect preference when email is available; use as unconditional fallback when it isn't.
|
||||
// Send the receipt PDF (with the usual text as its caption) when generation succeeded,
|
||||
// otherwise fall back to the plain text message so a PDF failure never blocks delivery.
|
||||
const waCaption = buildWAPayment(payment);
|
||||
if (hasValidEmail) {
|
||||
sends.push(waText(user, buildWAPayment(payment)));
|
||||
sends.push(receiptPdf ? waPdf(user, receiptPdf.filePath, receiptPdf.filename, waCaption) : waText(user, waCaption));
|
||||
} else {
|
||||
sends.push(waTextAny(user, buildWAPayment(payment)));
|
||||
sends.push(receiptPdf ? waPdfAny(user, receiptPdf.filePath, receiptPdf.filename, waCaption) : waTextAny(user, waCaption));
|
||||
}
|
||||
const hasEvent = !!(payment.registration?.eventId || payment.eventId);
|
||||
if (hasEvent) {
|
||||
@@ -1010,6 +1090,8 @@ async function sendPaymentEmails(paymentId) {
|
||||
await Promise.all(sends);
|
||||
} catch (e) {
|
||||
console.error('Failed to send payment emails:', e);
|
||||
} finally {
|
||||
if (receiptPdf) try { fs.unlinkSync(receiptPdf.filePath); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1152,6 +1234,137 @@ async function sendDonationUnassignmentEmails(leg) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Manual "send to me" requests ──────────────────────────────────────────────
|
||||
// Unlike the fire-and-forget sends above, these are awaited directly from a
|
||||
// request handler (dashboard "Email/WhatsApp invoice|receipt" buttons) — they
|
||||
// throw an Error with a `statusCode` on failure instead of swallowing it, so the
|
||||
// controller can surface a real error to the user rather than failing silently.
|
||||
|
||||
function userFacingError(message, statusCode) {
|
||||
const e = new Error(message);
|
||||
e.statusCode = statusCode;
|
||||
return e;
|
||||
}
|
||||
|
||||
/**
|
||||
* (re)generates and sends a registration's invoice PDF to the registrant on a single,
|
||||
* explicitly-chosen channel. Only the registrant themself may request their own invoice.
|
||||
* @param {string} registrationId
|
||||
* @param {string} requesterId - req.user.id of the caller
|
||||
* @param {'email'|'whatsapp'} channel
|
||||
*/
|
||||
async function sendInvoiceToUser(registrationId, requesterId, channel) {
|
||||
if (channel !== 'email' && channel !== 'whatsapp') throw userFacingError('Invalid channel', 400);
|
||||
|
||||
const reg = await loadRegistrationFull(registrationId);
|
||||
if (!reg) throw userFacingError('Registration not found', 404);
|
||||
if (reg.userId !== requesterId) throw userFacingError('Not authorized to access this registration', 403);
|
||||
|
||||
if (channel === 'email' && (!reg.user?.email || reg.user.email.endsWith('@guest.local'))) {
|
||||
throw userFacingError('No valid email address on file. Add one in your profile.', 400);
|
||||
}
|
||||
if (channel === 'whatsapp' && !reg.user?.phoneNumber) {
|
||||
throw userFacingError('No phone number on file. Add one in your profile.', 400);
|
||||
}
|
||||
|
||||
const totalDue = computeRegistrationTotalDue(reg, new Date());
|
||||
const totalPaid = (reg.payments || []).reduce((s, p) => s + (p.amount || 0), 0);
|
||||
|
||||
// Unlike the automatic post-registration send, a manually-requested invoice is generated
|
||||
// regardless of balance — someone may want it as a paid-in-full record too.
|
||||
const paymentUrl = await resolveInvoicePaymentUrl(reg, { totalDue, totalPaid });
|
||||
let invoicePdf;
|
||||
try {
|
||||
const { generateInvoicePdf } = require('./pdfDocs');
|
||||
invoicePdf = await generateInvoicePdf(reg, { paymentUrl, totalDue, totalPaid });
|
||||
} catch (e) {
|
||||
console.error('Failed to generate invoice PDF:', e);
|
||||
throw userFacingError('Could not generate the invoice right now. Please try again shortly.', 500);
|
||||
}
|
||||
|
||||
try {
|
||||
const eventTitle = reg.event?.title || 'your registration';
|
||||
if (channel === 'email') {
|
||||
await sendMail({
|
||||
to: reg.user.email,
|
||||
subject: `Your invoice for ${eventTitle}`,
|
||||
html: emailWrapper(
|
||||
`<p style="font-size:18px;font-weight:700;color:#0f172a;margin:0 0 12px 0">Your invoice is attached</p>
|
||||
<p style="margin:0;color:#374151;font-family:${ff}">Hi <strong>${reg.user?.name || 'there'}</strong>, here's the invoice you requested for <strong>${eventTitle}</strong>.</p>`
|
||||
),
|
||||
text: `Hi ${reg.user?.name || 'there'},\n\nHere's the invoice you requested for ${eventTitle} — see the attached PDF.`,
|
||||
attachments: [{ filename: invoicePdf.filename, path: invoicePdf.filePath, contentType: 'application/pdf' }],
|
||||
});
|
||||
} else {
|
||||
// Explicit user-requested channel — bypass the notification-preference gate (waPdfAny)
|
||||
// the same way ticket resends do, so "WhatsApp invoice" works even if the user's saved
|
||||
// preference is email-only.
|
||||
const { waPdfAny } = require('./notify');
|
||||
const { buildWARegistration } = require('./waMessages');
|
||||
const caption = buildWARegistration(reg, { isNew: false, totalDue, totalPaid, balance: Math.max(totalDue - totalPaid, 0) });
|
||||
await waPdfAny(reg.user, invoicePdf.filePath, invoicePdf.filename, caption);
|
||||
}
|
||||
} finally {
|
||||
try { fs.unlinkSync(invoicePdf.filePath); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (Re)generates and sends a payment's receipt PDF to the payer on a single, explicitly-chosen
|
||||
* channel. Only the person the payment belongs to (via the payment itself or its registration)
|
||||
* may request their own receipt.
|
||||
* @param {string} paymentId
|
||||
* @param {string} requesterId - req.user.id of the caller
|
||||
* @param {'email'|'whatsapp'} channel
|
||||
*/
|
||||
async function sendReceiptToUser(paymentId, requesterId, channel) {
|
||||
if (channel !== 'email' && channel !== 'whatsapp') throw userFacingError('Invalid channel', 400);
|
||||
|
||||
const payment = await loadPaymentFull(paymentId);
|
||||
if (!payment) throw userFacingError('Payment not found', 404);
|
||||
const owner = payment.registration?.user || payment.user;
|
||||
const ownerId = payment.registration?.userId || payment.userId;
|
||||
if (ownerId !== requesterId) throw userFacingError('Not authorized to access this payment', 403);
|
||||
|
||||
if (channel === 'email' && (!owner?.email || owner.email.endsWith('@guest.local'))) {
|
||||
throw userFacingError('No valid email address on file. Add one in your profile.', 400);
|
||||
}
|
||||
if (channel === 'whatsapp' && !owner?.phoneNumber) {
|
||||
throw userFacingError('No phone number on file. Add one in your profile.', 400);
|
||||
}
|
||||
|
||||
const { generateReceiptPdf } = require('./pdfDocs');
|
||||
let receiptPdf;
|
||||
try {
|
||||
receiptPdf = await generateReceiptPdf(payment);
|
||||
} catch (e) {
|
||||
console.error('Failed to generate receipt PDF:', e);
|
||||
throw userFacingError('Could not generate the receipt right now. Please try again shortly.', 500);
|
||||
}
|
||||
|
||||
try {
|
||||
const eventTitle = payment.registration?.event?.title || payment.event?.title || 'your registration';
|
||||
if (channel === 'email') {
|
||||
await sendMail({
|
||||
to: owner.email,
|
||||
subject: `Your payment receipt for ${eventTitle}`,
|
||||
html: emailWrapper(
|
||||
`<p style="font-size:18px;font-weight:700;color:#0f172a;margin:0 0 12px 0">Your receipt is attached</p>
|
||||
<p style="margin:0;color:#374151;font-family:${ff}">Hi <strong>${owner?.name || 'there'}</strong>, here's the receipt you requested for your payment of <strong>${fmtAmount(payment.amount)}</strong> towards <strong>${eventTitle}</strong>.</p>`
|
||||
),
|
||||
text: `Hi ${owner?.name || 'there'},\n\nHere's the receipt you requested for your payment of ${fmtAmount(payment.amount)} towards ${eventTitle} — see the attached PDF.`,
|
||||
attachments: [{ filename: receiptPdf.filename, path: receiptPdf.filePath, contentType: 'application/pdf' }],
|
||||
});
|
||||
} else {
|
||||
const { waPdfAny } = require('./notify');
|
||||
const { buildWAPayment } = require('./waMessages');
|
||||
await waPdfAny(owner, receiptPdf.filePath, receiptPdf.filename, buildWAPayment(payment));
|
||||
}
|
||||
} finally {
|
||||
try { fs.unlinkSync(receiptPdf.filePath); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendDailyEventSummaries(now = new Date()) {
|
||||
try {
|
||||
const today = new Date(now);
|
||||
@@ -1210,4 +1423,6 @@ module.exports = {
|
||||
sendDonationUnassignmentEmails,
|
||||
sendCheckInEmails,
|
||||
buildCheckInConfirmation,
|
||||
sendInvoiceToUser,
|
||||
sendReceiptToUser,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user