Add calendar export, SEO, error monitoring, backups, audit trail, and a starter test suite
Six site improvements picked from a "what could be better" review, plus a Jest test suite covering the two areas with the trickiest money-handling history in this project (early-bird pricing tranches, donation-leg accounting): - "Add to calendar" .ics download on event pages and in confirmation emails - sitemap.xml, robots.txt, and Open Graph/Twitter metadata for public pages - Sentry error monitoring (backend + frontend), a no-op until SENTRY_DSN is set - Nightly local pg_dump backups with a Site Settings tab to browse/trigger/download - Admin audit trail for refunds, donations, manual registrations, event and settings changes, and staff-initiated cancellations - Jest tests reproducing and guarding against the 1.8.0 tranche-pricing bug and the 1.4.2 donation-balance-inflation bug Wallet passes (Google/Apple) were scoped out of this round — Apple Wallet needs a paid Apple Developer account the project doesn't have yet, and the user preferred shipping both together later rather than Google alone now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@ const prisma = require('../config/db');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { generateTicketsForRegistration } = require('../utils/ticketUtils');
|
||||
const { computeRegistrationTotalDue, refreshPricingForRegistration } = require('../utils/pricing');
|
||||
const { computeDonationRemaining } = require('../utils/donationUtils');
|
||||
const { logAdminAction } = require('../utils/adminAudit');
|
||||
const { getClientIp } = require('../utils/requestUtils');
|
||||
const axios = require('axios');
|
||||
const { emailTickets } = require('./ticketController');
|
||||
const { safeErrorMessage } = require('../utils/errorUtils');
|
||||
@@ -611,17 +614,12 @@ const assignDonationToRegistration = async (req, res) => {
|
||||
throw new Error('Only donations can be assigned to registrations');
|
||||
}
|
||||
|
||||
// Donations are never mutated once created — their remaining balance is the original
|
||||
// amount minus every leg (a Payment row with isDonation:false and originalPaymentId
|
||||
// pointing back at this donation) already allocated from it. A refund of the donation
|
||||
// itself also creates such a leg, with a negative amount — Math.abs() so a refund reduces
|
||||
// the remaining balance (money that's left the building) instead of increasing it (which a
|
||||
// raw signed sum would do, since subtracting a negative adds).
|
||||
// See computeDonationRemaining's doc comment for why refund legs (negative amount) reduce
|
||||
// rather than inflate the remaining balance.
|
||||
const existingLegs = await prisma.payment.findMany({
|
||||
where: { originalPaymentId: payment.id, isDonation: false }
|
||||
});
|
||||
const alreadyUsed = existingLegs.reduce((sum, leg) => sum + Math.abs(leg.amount), 0);
|
||||
const remainingDonation = payment.amount - alreadyUsed;
|
||||
const remainingDonation = computeDonationRemaining(payment.amount, existingLegs);
|
||||
|
||||
if (remainingDonation <= 0.000001) {
|
||||
res.status(400);
|
||||
@@ -766,6 +764,16 @@ const assignDonationToRegistration = async (req, res) => {
|
||||
donationRemaining: remainingDonation - allocateAmount
|
||||
};
|
||||
|
||||
logAdminAction({
|
||||
actorId: req.user.id,
|
||||
actorRole: req.user.role,
|
||||
action: 'donation_assigned',
|
||||
targetType: 'Registration',
|
||||
targetId: registrationId,
|
||||
metadata: { paymentId, legId: leg.id, allocateAmount },
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) });
|
||||
@@ -885,6 +893,16 @@ const unassignDonationFromRegistration = async (req, res) => {
|
||||
catch (e) { console.error('Failed to send emails after unassigning donation:', e); }
|
||||
})();
|
||||
|
||||
logAdminAction({
|
||||
actorId: req.user.id,
|
||||
actorRole: req.user.role,
|
||||
action: 'donation_unassigned',
|
||||
targetType: 'Registration',
|
||||
targetId: leg.registrationId,
|
||||
metadata: { legId: leg.id, donationId: donation.id, amount: leg.amount },
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
message: 'Donation unassigned',
|
||||
updatedRegistration: finalRegistration,
|
||||
@@ -1357,6 +1375,16 @@ const createRefund = async (req, res) => {
|
||||
const { sendRefundEmail } = require('../utils/notifications');
|
||||
sendRefundEmail(negativePayment.id).catch(e => console.error('Failed to send refund email:', e));
|
||||
|
||||
logAdminAction({
|
||||
actorId: req.user.id,
|
||||
actorRole: req.user.role,
|
||||
action: 'refund_created',
|
||||
targetType: 'Payment',
|
||||
targetId: negativePayment.id,
|
||||
metadata: { amount: amt, method: method || 'refund', reason: reason || null, registrationId: linkRegistrationId },
|
||||
ip: getClientIp(req),
|
||||
});
|
||||
|
||||
return res.status(201).json(negativePayment);
|
||||
} catch (error) {
|
||||
res.status(res.statusCode === 200 ? 400 : res.statusCode).json({ message: safeErrorMessage(error) });
|
||||
|
||||
Reference in New Issue
Block a user