Staff can lock a section (or the whole event) to their device, download its tickets and keep scanning with no connection. Scans are queued in IndexedDB and synced idempotently when back online; conflicts go into a sync report for supervisors, who can also force-release a lost device. The server now validates scan sections and refuses scans in a scope locked to another device. Adds a web app manifest and a service worker for the scanner page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
2.3 KiB
JavaScript
55 lines
2.3 KiB
JavaScript
const { locksClash, lockCovers, judgeSyncedScan } = require('../src/utils/scanLocks');
|
|
|
|
describe('locksClash', () => {
|
|
test('a whole-event lock clashes with everything', () => {
|
|
expect(locksClash({ sectionId: null }, 'hall')).toBe(true);
|
|
expect(locksClash({ sectionId: 'hall' }, null)).toBe(true);
|
|
});
|
|
|
|
test('section locks only clash with the same section', () => {
|
|
expect(locksClash({ sectionId: 'hall' }, 'hall')).toBe(true);
|
|
expect(locksClash({ sectionId: 'hall' }, 'dinner')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('lockCovers', () => {
|
|
const dinnerLock = { sectionId: 'dinner', section: { allowedOptions: [{ eventOptionId: 'meal' }] } };
|
|
|
|
test('a whole-event lock covers every scan', () => {
|
|
expect(lockCovers({ sectionId: null }, { sectionId: 'x', optionId: 'y' })).toBe(true);
|
|
});
|
|
|
|
test('with a section picked, only that section is covered', () => {
|
|
expect(lockCovers(dinnerLock, { sectionId: 'dinner', optionId: 'meal' })).toBe(true);
|
|
expect(lockCovers(dinnerLock, { sectionId: 'hall', optionId: 'meal' })).toBe(false);
|
|
});
|
|
|
|
test('with no section picked, tickets the locked section admits are covered', () => {
|
|
expect(lockCovers(dinnerLock, { optionId: 'meal' })).toBe(true);
|
|
expect(lockCovers(dinnerLock, { optionId: 'entry' })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('judgeSyncedScan', () => {
|
|
const ok = { remaining: 2, cancelled: false, inScope: true, consentOk: true };
|
|
|
|
test('records a normal scan', () => {
|
|
expect(judgeSyncedScan(ok, 1)).toEqual({ redeem: 1, conflict: null });
|
|
});
|
|
|
|
test('never redeems more than is left', () => {
|
|
expect(judgeSyncedScan(ok, 3)).toEqual({ redeem: 2, conflict: 'partly_used' });
|
|
expect(judgeSyncedScan({ ...ok, remaining: 0 }, 1)).toEqual({ redeem: 0, conflict: 'already_used' });
|
|
});
|
|
|
|
test('flags unknown, cancelled and out-of-scope tickets without recording them', () => {
|
|
expect(judgeSyncedScan(null, 1)).toEqual({ redeem: 0, conflict: 'not_found' });
|
|
expect(judgeSyncedScan({ ...ok, cancelled: true }, 1)).toEqual({ redeem: 0, conflict: 'cancelled' });
|
|
expect(judgeSyncedScan({ ...ok, inScope: false }, 1)).toEqual({ redeem: 0, conflict: 'wrong_scope' });
|
|
});
|
|
|
|
test('records but flags a check-in without consent', () => {
|
|
expect(judgeSyncedScan({ ...ok, consentOk: false }, 1)).toEqual({ redeem: 1, conflict: 'consent' });
|
|
});
|
|
});
|