Fix donation-assignment admin email reading as a real payment

The internal admin notice for applying a donation to a registration
reused buildPaymentAdminNotice as-is, so it read "Payment recorded" /
"Registration payment" — indistinguishable from an actual incoming
payment even though no new money changed hands. Adds a dedicated
buildDonationAssignmentAdminNotice wrapper (mirroring the one already
added for unassignment) so the subject, heading, and Type field all
say "Donation applied" instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 00:59:24 +02:00
co-authored by Claude Sonnet 5
parent 252f80fadb
commit 34f9826829
2 changed files with 33 additions and 3 deletions
+4
View File
@@ -11,6 +11,10 @@ and this project follows [Semantic Versioning](https://semver.org/).
- Supervisors can now unassign a donation that was previously applied to a registration, from a new "Assigned donations" list on the Payments page. This reverses the allocation (the registration's balance goes back up and the donation becomes available again), reverting the registration's status and revoking any tickets issued only because that allocation completed payment — blocked if a ticket has already been scanned. The registrant is notified by email/WhatsApp, mirroring the notification sent when a donation is first applied. - Supervisors can now unassign a donation that was previously applied to a registration, from a new "Assigned donations" list on the Payments page. This reverses the allocation (the registration's balance goes back up and the donation becomes available again), reverting the registration's status and revoking any tickets issued only because that allocation completed payment — blocked if a ticket has already been scanned. The registrant is notified by email/WhatsApp, mirroring the notification sent when a donation is first applied.
### Fixed
- The internal admin notification for a donation applied to a registration read "Payment recorded" / "Registration payment", indistinguishable from a real incoming payment even though no new money changed hands — it now says "Donation applied" throughout, matching the registrant-facing email's distinct wording.
### Performance ### Performance
- Reconciling a Yoco card payment or sending a payment link on the supervisor Payments page no longer blocks the response on ticket-PDF generation and email/WhatsApp sends — these now run in the background, matching how manual payments already worked. - Reconciling a Yoco card payment or sending a payment link on the supervisor Payments page no longer blocks the response on ticket-PDF generation and email/WhatsApp sends — these now run in the background, matching how manual payments already worked.
+29 -3
View File
@@ -697,6 +697,29 @@ function buildDonationUnassignedFromRegistrant(payment) {
return { subject, text, html: emailWrapper(body, { preheader }) }; return { subject, text, html: emailWrapper(body, { preheader }) };
} }
// Internal admin notice for a donation-assignment — same table layout as
// buildPaymentAdminNotice (which reads payment.user as "Payer" — for a leg that's the donor,
// since legs copy userId from the original donation, not the registrant), relabeled so it
// doesn't read as a fresh incoming payment: no new money changed hands here, an
// already-recorded donation was just reallocated to a registration.
function buildDonationAssignmentAdminNotice(payment) {
const notice = buildPaymentAdminNotice(payment);
const eventTitle = payment.registration?.event?.title || 'Event';
const payerName = payment.user?.name || '—';
const subject = `Donation applied: ${fmtAmount(payment.amount)}${payerName} (${eventTitle})`;
return {
...notice,
subject,
html: notice.html
.replace('Payment recorded', 'Donation applied')
.replace('Internal notification', 'Internal notification — donation applied to a registration')
.replace('>Registration payment<', '>Donation applied<'),
text: notice.text
.replace('Payment recorded', 'Donation applied')
.replace('Type: Registration payment', 'Type: Donation applied'),
};
}
// Internal admin notice for a donation-unassignment — same table layout as // Internal admin notice for a donation-unassignment — same table layout as
// buildPaymentAdminNotice (which reads payment.user as "Payer" — for a leg that's the donor, // buildPaymentAdminNotice (which reads payment.user as "Payer" — for a leg that's the donor,
// since legs copy userId from the original donation, not the registrant), with copy adjusted // since legs copy userId from the original donation, not the registrant), with copy adjusted
@@ -711,8 +734,11 @@ function buildDonationUnassignmentAdminNotice(payment) {
subject, subject,
html: notice.html html: notice.html
.replace('Payment recorded', 'Donation unassigned') .replace('Payment recorded', 'Donation unassigned')
.replace('Internal notification', 'Internal notification — donation removed from registration'), .replace('Internal notification', 'Internal notification — donation removed from registration')
text: notice.text.replace('Payment recorded', 'Donation unassigned'), .replace('>Registration payment<', '>Donation unassigned<'),
text: notice.text
.replace('Payment recorded', 'Donation unassigned')
.replace('Type: Registration payment', 'Type: Donation unassigned'),
}; };
} }
@@ -1057,7 +1083,7 @@ async function sendDonationAssignmentEmails(paymentId) {
} else { } else {
sends.push(waTextAny(user, buildWADonationAppliedToRegistrant(payment))); sends.push(waTextAny(user, buildWADonationAppliedToRegistrant(payment)));
} }
const adminMsg = buildPaymentAdminNotice(payment); const adminMsg = buildDonationAssignmentAdminNotice(payment);
if (adminMsg.to && adminMsg.to.length) { if (adminMsg.to && adminMsg.to.length) {
sends.push(sendMail({ to: adminMsg.to.join(','), subject: adminMsg.subject, html: adminMsg.html, text: adminMsg.text })); sends.push(sendMail({ to: adminMsg.to.join(','), subject: adminMsg.subject, html: adminMsg.html, text: adminMsg.text }));
} }