- Waitlist per session: entries for pick-sessions tickets name a session; offers use that session's free places, never run past its start, and claim links tick it. Held offers count towards session capacity. - At the door: the Select items pop-up has the session picker, with today's session preselected for new attendees. - On pick-sessions events every ticket shows the session list straight away. "All sessions" tickets record the ticked sessions (RegistrationOption.sessionIds) at one price; scanning, attendance, certificates, reminders and capacity follow them. - Fix: per-session stock limits weren't enforced (takenCount filtered WaitlistEntry by a sessionId column that didn't exist, and the error was swallowed), and the public event page lost its stock info. - Fix: editing a door registration with picked sessions couldn't be saved. - Fix: the offline-scanning service worker served /_next/static cache-first forever, so dev browsers that had opened the scanner ran stale code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
57 lines
2.3 KiB
JavaScript
57 lines
2.3 KiB
JavaScript
/**
|
|
* Ticket stock — the one place that decides how many places are taken.
|
|
*
|
|
* Taken = places on registrations that aren't cancelled + places held for a live
|
|
* waitlist offer (until it expires). An option's stock limit covers all its variants;
|
|
* a variant can also have its own limit. 0 means unlimited. For a "pick sessions"
|
|
* option the limit is per session (pass sessionId).
|
|
*/
|
|
|
|
const prisma = require('../config/db');
|
|
|
|
/**
|
|
* @param {{ eventOptionId: string, variantId?: string|null, sessionId?: string|null }} line -
|
|
* variantId given → that variant only; sessionId given → that session's picked places only
|
|
* @param {{ excludeEntryId?: string, db?: object }} [opts] - excludeEntryId: ignore this
|
|
* waitlist offer's hold (the person claiming it)
|
|
*/
|
|
async function takenCount({ eventOptionId, variantId = null, sessionId = null }, { excludeEntryId = null, db = prisma } = {}) {
|
|
const scope = { ...(variantId ? { variantId } : { eventOptionId }), ...(sessionId ? { sessionId } : {}) };
|
|
const [registered, held] = await Promise.all([
|
|
db.registrationOption.aggregate({
|
|
where: { ...scope, registration: { status: { not: 'cancelled' } } },
|
|
_sum: { quantity: true },
|
|
}),
|
|
db.waitlistEntry.aggregate({
|
|
where: {
|
|
...scope,
|
|
status: 'offered',
|
|
offerExpiresAt: { gt: new Date() },
|
|
...(excludeEntryId ? { id: { not: excludeEntryId } } : {}),
|
|
},
|
|
_sum: { offeredQuantity: true },
|
|
}),
|
|
]);
|
|
return (registered._sum?.quantity || 0) + (held._sum?.offeredQuantity || 0);
|
|
}
|
|
|
|
/**
|
|
* Places still free on a line: the tighter of the option and variant limits.
|
|
* @param {{ sessionId?: string|null, excludeEntryId?: string, db?: object }} [opts] -
|
|
* sessionId: a "pick sessions" line (the limits are places per session)
|
|
* @returns {Promise<number|null>} null when unlimited
|
|
*/
|
|
async function remainingFor(option, variant, { sessionId = null, ...opts } = {}) {
|
|
const limits = [];
|
|
if (option?.stockLimit > 0) {
|
|
limits.push(option.stockLimit - await takenCount({ eventOptionId: option.id, sessionId }, opts));
|
|
}
|
|
if (variant?.stockLimit > 0) {
|
|
limits.push(variant.stockLimit - await takenCount({ eventOptionId: option.id, variantId: variant.id, sessionId }, opts));
|
|
}
|
|
if (limits.length === 0) return null;
|
|
return Math.max(0, Math.min(...limits));
|
|
}
|
|
|
|
module.exports = { takenCount, remainingFor };
|