Payers and holders can hand a ticket to someone else before the event. The new holder gets a new QR code (and an inactive account with an activation link if they're new); the old code stops working and scans of it, online or in offline sync, say it was transferred. One place can be split off a shared group ticket. Transfers can be turned off per event (supervisors can always transfer from At the door), youth events need the new holder's own consent, and every transfer is audited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 lines
2.0 KiB
JavaScript
47 lines
2.0 KiB
JavaScript
const { transferBlocker } = require('../src/utils/ticketTransfer');
|
|
const { judgeSyncedScan } = require('../src/utils/scanLocks');
|
|
|
|
describe('transferBlocker', () => {
|
|
const now = new Date('2026-10-01T10:00:00Z');
|
|
const ticket = {
|
|
userId: 'payer', holderUserId: 'holder', usages: [], registrationStatus: 'paid',
|
|
event: { startDate: '2026-10-05T08:00:00Z', allowTicketTransfer: true },
|
|
};
|
|
const user = (id, role = 'user') => ({ id, role });
|
|
|
|
test('the payer and the holder can transfer', () => {
|
|
expect(transferBlocker(ticket, user('payer'), now)).toBeNull();
|
|
expect(transferBlocker(ticket, user('holder'), now)).toBeNull();
|
|
});
|
|
|
|
test('anyone else needs to be a supervisor', () => {
|
|
expect(transferBlocker(ticket, user('someone'), now).status).toBe(403);
|
|
expect(transferBlocker(ticket, user('staffer', 'staff'), now).status).toBe(403);
|
|
expect(transferBlocker(ticket, user('sup', 'supervisor'), now)).toBeNull();
|
|
});
|
|
|
|
test('not once the event has started', () => {
|
|
expect(transferBlocker({ ...ticket, event: { ...ticket.event, startDate: '2026-10-01T09:00:00Z' } }, user('sup', 'supervisor'), now).message).toMatch(/before the event starts/);
|
|
});
|
|
|
|
test('not once any of it has been scanned', () => {
|
|
expect(transferBlocker({ ...ticket, usages: [{ id: 'u1' }] }, user('payer'), now).message).toMatch(/already been used/);
|
|
});
|
|
|
|
test('not for a cancelled registration', () => {
|
|
expect(transferBlocker({ ...ticket, registrationStatus: 'cancelled' }, user('payer'), now)).not.toBeNull();
|
|
});
|
|
|
|
test('turned off per event, except for supervisors', () => {
|
|
const off = { ...ticket, event: { ...ticket.event, allowTicketTransfer: false } };
|
|
expect(transferBlocker(off, user('payer'), now).message).toMatch(/turned off/);
|
|
expect(transferBlocker(off, user('admin', 'admin'), now)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('offline sync of a transferred code', () => {
|
|
test('is flagged and not recorded', () => {
|
|
expect(judgeSyncedScan({ transferred: true }, 1)).toEqual({ redeem: 0, conflict: 'transferred' });
|
|
});
|
|
});
|