- 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>
92 lines
3.7 KiB
JavaScript
92 lines
3.7 KiB
JavaScript
const { pickOffers } = require('../src/utils/waitlist');
|
|
|
|
const q = (...qs) => qs.map((quantity, i) => ({ id: `e${i + 1}`, quantity }));
|
|
|
|
describe('pickOffers', () => {
|
|
test('offers people in order while places last', () => {
|
|
expect(pickOffers(q(1, 2, 1), 3, true)).toEqual([
|
|
{ entryId: 'e1', quantity: 1 },
|
|
{ entryId: 'e2', quantity: 2 },
|
|
]);
|
|
});
|
|
|
|
test('nothing free, no offers', () => {
|
|
expect(pickOffers(q(1), 0, true)).toEqual([]);
|
|
});
|
|
|
|
test('partial offers: the first in line gets what is free, nobody jumps ahead', () => {
|
|
expect(pickOffers(q(3, 1), 2, true)).toEqual([{ entryId: 'e1', quantity: 2 }]);
|
|
});
|
|
|
|
test('without partial offers, skips to the next person who fits', () => {
|
|
expect(pickOffers(q(3, 1, 1), 2, false)).toEqual([
|
|
{ entryId: 'e2', quantity: 1 },
|
|
{ entryId: 'e3', quantity: 1 },
|
|
]);
|
|
});
|
|
|
|
test('without partial offers, nobody fits', () => {
|
|
expect(pickOffers(q(3, 4), 2, false)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
const { lineOpen, offerExpiry, lineName } = require('../src/utils/waitlist');
|
|
const { sessionPlacesFree } = require('../src/utils/sessions');
|
|
|
|
const at = (h) => new Date(Date.UTC(2026, 10, 4, h));
|
|
const sessions = [
|
|
{ id: 's1', startsAt: at(8), endsAt: at(9), capacity: 10, cancelledAt: null },
|
|
{ id: 's2', startsAt: at(10), endsAt: at(11), capacity: 4, cancelledAt: null },
|
|
{ id: 's3', startsAt: at(12), endsAt: at(13), capacity: null, cancelledAt: null },
|
|
{ id: 's4', startsAt: at(14), endsAt: at(15), capacity: 1, cancelledAt: at(1) },
|
|
];
|
|
|
|
describe('sessionPlacesFree', () => {
|
|
const booked = new Map([['s1', 7], ['s2', 4], ['s3', 50]]);
|
|
test('a picked session: its own capacity', () => {
|
|
expect(sessionPlacesFree(sessions, booked, 's1')).toBe(3);
|
|
expect(sessionPlacesFree(sessions, booked, 's2')).toBe(0);
|
|
});
|
|
test('a session without a capacity is unlimited', () => {
|
|
expect(sessionPlacesFree(sessions, booked, 's3')).toBeNull();
|
|
});
|
|
test('whole-course: the fullest limited session, cancelled ones ignored', () => {
|
|
expect(sessionPlacesFree(sessions, booked, null)).toBe(0);
|
|
expect(sessionPlacesFree(sessions, new Map([['s1', 2], ['s2', 1]]), null)).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('lineOpen', () => {
|
|
const event = { scheduleType: 'sessions', attendanceMode: 'pick', endDate: at(13), sessions };
|
|
test('a session can be offered until it starts', () => {
|
|
expect(lineOpen(event, 's2', at(9))).toBe(true);
|
|
expect(lineOpen(event, 's2', at(10))).toBe(false);
|
|
});
|
|
test('cancelled or unknown sessions are not offered', () => {
|
|
expect(lineOpen(event, 's4', at(1))).toBe(false);
|
|
expect(lineOpen(event, 'nope', at(1))).toBe(false);
|
|
});
|
|
test('whole-course lines close when the course does', () => {
|
|
expect(lineOpen(event, null, at(7))).toBe(true);
|
|
expect(lineOpen(event, null, at(8))).toBe(false);
|
|
expect(lineOpen({ scheduleType: 'single' }, null, at(20))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('offerExpiry', () => {
|
|
test('the offer hours, but never after the session starts', () => {
|
|
expect(offerExpiry(24, null, at(0))).toEqual(new Date(at(0).getTime() + 24 * 3600 * 1000));
|
|
expect(offerExpiry(24, sessions[1], at(0))).toEqual(at(10));
|
|
expect(offerExpiry(2, sessions[1], at(0))).toEqual(at(2));
|
|
});
|
|
});
|
|
|
|
describe('lineName', () => {
|
|
test('names the session for picked lines', () => {
|
|
expect(lineName({ eventOption: { name: 'Drop-in' }, variant: null, session: null })).toBe('Drop-in');
|
|
expect(lineName({ eventOption: { name: 'Drop-in' }, variant: { name: 'Adult' }, session: { ...sessions[1], label: 'Class 2' } }, sessions))
|
|
.toBe('Drop-in (Adult) — Class 2, Wed 4 Nov');
|
|
expect(lineName({ eventOption: { name: 'Drop-in' }, session: sessions[1] }, sessions)).toBe('Drop-in — Session 2, Wed 4 Nov');
|
|
});
|
|
});
|