Events can take a name (and optional email/phone) for each main-ticket place. Each named person gets their own quantity-1 ticket and attendee form; unnamed places share one ticket. Tickets go to the payer or to each person, attendees matching one existing account are linked to it, and the payer can edit names and send form links from My Events. Ticket generation is now planned per line (utils/attendees.js), free registrations use the same generator, and guest registration matches accounts by phone too, refusing conflicting email/phone pairs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
5.0 KiB
JavaScript
105 lines
5.0 KiB
JavaScript
const { validateAttendees, planLineTickets, lineKey } = require('../src/utils/attendees');
|
|
|
|
const EVENT = {
|
|
allowNamedAttendees: true,
|
|
eventOptions: [
|
|
{ id: 'kid', name: 'Camper', isMainTicket: true },
|
|
{ id: 'shirt', name: 'T-shirt', isMainTicket: false },
|
|
],
|
|
};
|
|
const places = (n) => ({ [lineKey('kid', null)]: n });
|
|
|
|
describe('validateAttendees', () => {
|
|
test('cleans up names and contact details', () => {
|
|
const out = validateAttendees(EVENT, places(2), [
|
|
{ eventOptionId: 'kid', name: ' Sam ', email: ' ', phone: '0821234567' },
|
|
{ eventOptionId: 'kid', name: 'Alex', email: 'alex@example.com' },
|
|
]);
|
|
expect(out).toEqual([
|
|
{ eventOptionId: 'kid', variantId: null, name: 'Sam', email: null, phone: '0821234567' },
|
|
{ eventOptionId: 'kid', variantId: null, name: 'Alex', email: 'alex@example.com', phone: null },
|
|
]);
|
|
});
|
|
|
|
test('nothing to check when no attendees are given', () => {
|
|
expect(validateAttendees({ ...EVENT, allowNamedAttendees: false }, places(1), [])).toEqual([]);
|
|
});
|
|
|
|
test('refuses names on events that do not take them', () => {
|
|
expect(() => validateAttendees({ ...EVENT, allowNamedAttendees: false }, places(1), [{ eventOptionId: 'kid', name: 'Sam' }])).toThrow(/doesn't take attendee names/);
|
|
});
|
|
|
|
test('only main tickets can be named', () => {
|
|
expect(() => validateAttendees(EVENT, { [lineKey('shirt', null)]: 1 }, [{ eventOptionId: 'shirt', name: 'Sam' }])).toThrow(/main tickets/);
|
|
});
|
|
|
|
test('needs a name and a valid email', () => {
|
|
expect(() => validateAttendees(EVENT, places(1), [{ eventOptionId: 'kid', name: ' ' }])).toThrow(/enter a name/);
|
|
expect(() => validateAttendees(EVENT, places(1), [{ eventOptionId: 'kid', name: 'Sam', email: 'nope' }])).toThrow(/valid email/);
|
|
});
|
|
|
|
test('no more names than places, counting names already given', () => {
|
|
expect(() => validateAttendees(EVENT, places(1), [{ eventOptionId: 'kid', name: 'A' }, { eventOptionId: 'kid', name: 'B' }])).toThrow(/More names/);
|
|
expect(() => validateAttendees(EVENT, places(3), [{ eventOptionId: 'kid', name: 'C' }], places(2))).not.toThrow();
|
|
expect(() => validateAttendees(EVENT, places(2), [{ eventOptionId: 'kid', name: 'C' }], places(2))).toThrow(/More names/);
|
|
});
|
|
});
|
|
|
|
describe('planLineTickets', () => {
|
|
const scan = (q = 1) => [{ quantityRedeemed: q }];
|
|
|
|
test('no attendees: one shared ticket for the whole quantity', () => {
|
|
expect(planLineTickets({ quantity: 3 }, [], [])).toEqual({ create: [{ attendeeId: null, holderUserId: null, quantity: 3 }], update: [], remove: [] });
|
|
});
|
|
|
|
test('every attendee gets their own ticket; unnamed places share one', () => {
|
|
const plan = planLineTickets({ quantity: 3 }, [{ id: 'a1', userId: 'u9' }, { id: 'a2', userId: null }], []);
|
|
expect(plan.create).toEqual([
|
|
{ attendeeId: 'a1', holderUserId: 'u9', quantity: 1 },
|
|
{ attendeeId: 'a2', holderUserId: null, quantity: 1 },
|
|
{ attendeeId: null, holderUserId: null, quantity: 1 },
|
|
]);
|
|
});
|
|
|
|
test('naming every place replaces the shared ticket', () => {
|
|
const plan = planLineTickets({ quantity: 2 }, [{ id: 'a1' }, { id: 'a2' }], [{ id: 't0', attendeeId: null, quantity: 2, usages: [] }]);
|
|
expect(plan.remove).toEqual(['t0']);
|
|
expect(plan.create).toHaveLength(2);
|
|
});
|
|
|
|
test('a scanned shared ticket is kept and never shrunk below what was used', () => {
|
|
const plan = planLineTickets({ quantity: 2 }, [{ id: 'a1' }, { id: 'a2' }], [{ id: 't0', attendeeId: null, quantity: 2, usages: scan(1) }]);
|
|
expect(plan.remove).toEqual([]);
|
|
expect(plan.update).toEqual([{ id: 't0', data: { quantity: 1 } }]);
|
|
});
|
|
|
|
test('removing an attendee removes their unscanned ticket', () => {
|
|
const plan = planLineTickets({ quantity: 1 }, [{ id: 'a1' }], [
|
|
{ id: 't1', attendeeId: 'a1', quantity: 1, usages: [] },
|
|
{ id: 't2', attendeeId: 'gone', quantity: 1, usages: [] },
|
|
]);
|
|
expect(plan).toEqual({ create: [], update: [], remove: ['t2'] });
|
|
});
|
|
|
|
test("keeps a scanned ticket whose attendee was removed, and counts it as a used place", () => {
|
|
const plan = planLineTickets({ quantity: 2 }, [{ id: 'a1' }], [
|
|
{ id: 't1', attendeeId: 'a1', quantity: 1, usages: [] },
|
|
{ id: 't2', attendeeId: 'gone', quantity: 1, usages: scan() },
|
|
]);
|
|
expect(plan).toEqual({ create: [], update: [], remove: [] });
|
|
});
|
|
|
|
test('follows the attendee to a newly linked account', () => {
|
|
const plan = planLineTickets({ quantity: 1 }, [{ id: 'a1', userId: 'u2' }], [{ id: 't1', attendeeId: 'a1', quantity: 1, holderUserId: null, usages: [] }]);
|
|
expect(plan.update).toEqual([{ id: 't1', data: { holderUserId: 'u2', quantity: 1 } }]);
|
|
});
|
|
|
|
test('existing single shared ticket is resized, duplicates dropped', () => {
|
|
const plan = planLineTickets({ quantity: 4 }, [], [
|
|
{ id: 't1', attendeeId: null, quantity: 2, usages: [] },
|
|
{ id: 't2', attendeeId: null, quantity: 2, usages: [] },
|
|
]);
|
|
expect(plan).toEqual({ create: [], update: [{ id: 't1', data: { quantity: 4 } }], remove: ['t2'] });
|
|
});
|
|
});
|