Speed up payment/refund processing on the supervisor dashboard
Reconciling a Yoco payment and sending payment links blocked the HTTP response on ticket-PDF generation and email/WhatsApp sends; they now run in the background like the other payment flows already did. Registration/payment option loops (pricing, stock checks, ticket generation) now resolve concurrently instead of sequentially. The Payments page dropped a per-registration N+1 fetch and now refreshes its lists in parallel after each action. Added missing indexes for dashboard stats and donation-leg lookups, and made GET /api/registrations optionally paginated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,16 +136,16 @@ const createRegistration = async (req, res) => {
|
||||
throw new Error('At least one option must be selected');
|
||||
}
|
||||
|
||||
// Validate each option and resolve prices / check stock
|
||||
const resolvedOptions = [];
|
||||
for (const option of options) {
|
||||
// Validate each option and resolve prices / check stock.
|
||||
// Options are independent of each other, so resolve them concurrently. Errors are marked
|
||||
// with `.isStockError` rather than relying on shared `res.statusCode` (which would race
|
||||
// across concurrent iterations) — the outer catch always responds 400 regardless.
|
||||
const resolvedOptions = await Promise.all(options.map(async (option) => {
|
||||
const eventOption = event.eventOptions.find(eo => eo.id === option.eventOptionId);
|
||||
if (!eventOption) {
|
||||
res.status(400);
|
||||
throw new Error(`Option with ID ${option.eventOptionId} not found for this event`);
|
||||
}
|
||||
if (!option.quantity || option.quantity < 1) {
|
||||
res.status(400);
|
||||
throw new Error('Quantity must be at least 1');
|
||||
}
|
||||
|
||||
@@ -155,11 +155,12 @@ const createRegistration = async (req, res) => {
|
||||
try {
|
||||
const stockCheck = await checkOptionStock(eventOption, qty);
|
||||
if (!stockCheck.available) {
|
||||
res.status(400);
|
||||
throw new Error(`"${eventOption.name}" is sold out or does not have enough stock (${stockCheck.remaining ?? 0} remaining).`);
|
||||
const err = new Error(`"${eventOption.name}" is sold out or does not have enough stock (${stockCheck.remaining ?? 0} remaining).`);
|
||||
err.isStockError = true;
|
||||
throw err;
|
||||
}
|
||||
} catch (e) {
|
||||
if (res.statusCode !== 200) throw e; // propagate stock errors
|
||||
if (e.isStockError) throw e; // propagate stock errors
|
||||
// If stock check function fails (pre-migration), continue without stock check
|
||||
}
|
||||
|
||||
@@ -169,17 +170,17 @@ const createRegistration = async (req, res) => {
|
||||
if (variantId && canIncludeVariants) {
|
||||
const variant = (eventOption.variants || []).find(v => v.id === variantId);
|
||||
if (!variant) {
|
||||
res.status(400);
|
||||
throw new Error(`Variant not found for option "${eventOption.name}"`);
|
||||
}
|
||||
try {
|
||||
const vStock = await checkVariantStock(variant, qty);
|
||||
if (!vStock.available) {
|
||||
res.status(400);
|
||||
throw new Error(`Variant "${variant.name}" is sold out (${vStock.remaining ?? 0} remaining).`);
|
||||
const err = new Error(`Variant "${variant.name}" is sold out (${vStock.remaining ?? 0} remaining).`);
|
||||
err.isStockError = true;
|
||||
throw err;
|
||||
}
|
||||
} catch (e) {
|
||||
if (res.statusCode !== 200) throw e;
|
||||
if (e.isStockError) throw e;
|
||||
}
|
||||
variantPrice = variant.price; // null = use option price
|
||||
}
|
||||
@@ -213,8 +214,8 @@ const createRegistration = async (req, res) => {
|
||||
}
|
||||
if (priceSnapshot === null) priceSnapshot = eventOption.price;
|
||||
|
||||
resolvedOptions.push({ ...option, variantId, appliedTierId, priceSnapshot });
|
||||
}
|
||||
return { ...option, variantId, appliedTierId, priceSnapshot };
|
||||
}));
|
||||
|
||||
// Check for existing non-cancelled registration for this user+event → merge instead
|
||||
const existingReg = await prisma.registration.findFirst({
|
||||
@@ -378,28 +379,43 @@ const createRegistration = async (req, res) => {
|
||||
// @access Private/Admin
|
||||
const getRegistrations = async (req, res) => {
|
||||
try {
|
||||
const registrations = await prisma.registration.findMany({
|
||||
include: {
|
||||
payments: { include: { recordedBy: { select: { id: true, name: true, email: true } } } },
|
||||
registrationOptions: {
|
||||
include: {
|
||||
eventOption: { include: { earlyBirdTiers: true } },
|
||||
variant: { select: { id: true, name: true, price: true } },
|
||||
}
|
||||
},
|
||||
event: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
phoneNumber: true
|
||||
}
|
||||
const include = {
|
||||
payments: { include: { recordedBy: { select: { id: true, name: true, email: true } } } },
|
||||
registrationOptions: {
|
||||
include: {
|
||||
eventOption: { include: { earlyBirdTiers: true } },
|
||||
variant: { select: { id: true, name: true, price: true } },
|
||||
}
|
||||
},
|
||||
event: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
phoneNumber: true
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
res.json(registrations);
|
||||
// Pagination is opt-in via ?page/?limit to keep existing callers (which expect a plain
|
||||
// array of every registration) working unchanged; callers that pass either param get back
|
||||
// the { data, total, page, limit, pages } shape used by /api/payments and /api/users.
|
||||
if (typeof req.query.page === 'undefined' && typeof req.query.limit === 'undefined') {
|
||||
const registrations = await prisma.registration.findMany({ include });
|
||||
return res.json(registrations);
|
||||
}
|
||||
|
||||
const page = Math.max(1, parseInt(req.query.page) || 1);
|
||||
const limit = Math.min(200, Math.max(1, parseInt(req.query.limit) || 100));
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [registrations, total] = await prisma.$transaction([
|
||||
prisma.registration.findMany({ include, orderBy: { createdAt: 'desc' }, skip, take: limit }),
|
||||
prisma.registration.count()
|
||||
]);
|
||||
|
||||
res.json({ data: registrations, total, page, limit, pages: Math.ceil(total / limit) });
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: error.message });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user