jest.mock('../models/Attendance', () => ({ findOne: jest.fn(), })); jest.mock('../models/AttendanceParticipant', () => ({ find: jest.fn(), findOne: jest.fn(), create: jest.fn(), })); jest.mock('../models/Participant', () => ({ findById: jest.fn(), })); jest.mock('../utils/participantMembership', () => ({ getMembership: jest.fn(), flattenParticipantForEvent: jest.fn((p) => p), })); const mongoose = require('mongoose'); const Attendance = require('../models/Attendance'); const AttendanceParticipant = require('../models/AttendanceParticipant'); const Participant = require('../models/Participant'); const { getMembership } = require('../utils/participantMembership'); const { performBulkCheckInSync } = require('./attendanceCheckIn.service'); const eventId = new mongoose.Types.ObjectId().toString(); const attendanceId = new mongoose.Types.ObjectId().toString(); const participantId = new mongoose.Types.ObjectId().toString(); describe('performBulkCheckInSync', () => { beforeEach(() => { jest.resetAllMocks(); Attendance.findOne.mockReturnValue({ lean: () => Promise.resolve({ _id: attendanceId, event: eventId, name: 'Door A', status: 'active', settings: { allowDuplicateCheckIn: false }, }), }); AttendanceParticipant.find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([]) }), }); AttendanceParticipant.findOne.mockReset(); AttendanceParticipant.create.mockReset(); Participant.findById.mockReset(); getMembership.mockReset(); }); test('rejects empty operations', async () => { const out = await performBulkCheckInSync({ attendanceId, eventId, operations: [], checkedInBy: 'user1', }); expect(out.err).toBeTruthy(); expect(out.statusCode).toBe(400); }); test('rejects invalid checkedInAt', async () => { AttendanceParticipant.find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([]) }), }); const out = await performBulkCheckInSync({ attendanceId, eventId, checkedInBy: 'user1', operations: [ { clientOpId: 'op-1', participantId, checkedInAt: 'not-a-date', }, ], }); expect(out.results[0].status).toBe('rejected'); expect(out.results[0].message).toMatch(/checkedInAt/i); }); test('returns duplicate for existing clientOpId', async () => { AttendanceParticipant.find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([ { participant: participantId, checkIns: [ { clientOpId: 'op-dup', checkedInAt: new Date('2026-08-01T10:00:00.000Z'), }, ], }, ]), }), }); const out = await performBulkCheckInSync({ attendanceId, eventId, checkedInBy: 'user1', operations: [ { clientOpId: 'op-dup', participantId, checkedInAt: '2026-08-01T10:00:00.000Z', }, ], }); expect(out.results[0].status).toBe('duplicate'); }); test('conflicts when already checked in and duplicates disallowed', async () => { AttendanceParticipant.find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([]) }), }); getMembership.mockReturnValue({ checkedIn: false }); Participant.findById.mockResolvedValue({ _id: participantId, firstName: 'Ada', lastName: 'Lovelace', email: 'ada@example.com', phone: '', toObject: () => ({}), markModified: jest.fn(), save: jest.fn(), }); AttendanceParticipant.findOne .mockResolvedValueOnce({ checkIns: [{ checkedInAt: new Date('2026-08-01T09:00:00.000Z') }], }) .mockReturnValueOnce({ select: () => ({ lean: () => Promise.resolve(null) }), }); const out = await performBulkCheckInSync({ attendanceId, eventId, checkedInBy: 'user1', operations: [ { clientOpId: 'op-conflict', participantId, checkedInAt: '2026-08-01T11:00:00.000Z', }, ], }); expect(out.results[0].status).toBe('conflict'); }); test('applies offline check-in with client timestamp', async () => { AttendanceParticipant.find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([]) }), }); getMembership.mockReturnValue({ checkedIn: false }); const participantDoc = { _id: participantId, firstName: 'Ada', lastName: 'Lovelace', email: 'ada@example.com', phone: '', toObject: () => ({}), markModified: jest.fn(), save: jest.fn().mockResolvedValue(undefined), }; Participant.findById.mockResolvedValue(participantDoc); AttendanceParticipant.findOne .mockResolvedValueOnce(null) .mockReturnValueOnce({ select: () => ({ lean: () => Promise.resolve(null) }), }); const created = { checkIns: [ { clientOpId: 'op-new', checkedInAt: new Date('2026-08-01T11:00:00.000Z'), source: 'offline_sync', }, ], }; AttendanceParticipant.create.mockResolvedValue(created); const out = await performBulkCheckInSync({ attendanceId, eventId, checkedInBy: 'user1', deviceId: 'device-1', operations: [ { clientOpId: 'op-new', participantId, checkedInAt: '2026-08-01T11:00:00.000Z', }, ], }); expect(out.results[0].status).toBe('applied'); expect(AttendanceParticipant.create).toHaveBeenCalled(); const entry = AttendanceParticipant.create.mock.calls[0][0].checkIns[0]; expect(entry.source).toBe('offline_sync'); expect(entry.clientOpId).toBe('op-new'); expect(new Date(entry.checkedInAt).toISOString()).toBe('2026-08-01T11:00:00.000Z'); }); test('rejects assign-blank profile when setting is off', async () => { AttendanceParticipant.find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([]) }), }); const out = await performBulkCheckInSync({ attendanceId, eventId, checkedInBy: 'user1', operations: [ { clientOpId: 'op-blank', participantId, checkedInAt: '2026-08-01T11:00:00.000Z', profile: { firstName: 'Walk', lastName: 'In', email: '', phone: '' }, }, ], }); expect(out.results[0].status).toBe('rejected'); expect(out.results[0].message).toMatch(/Assign a Blank/i); }); test('applies undo by popping last check-in', async () => { AttendanceParticipant.find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([]) }), }); getMembership.mockReturnValue({ checkedIn: true }); Participant.findById.mockResolvedValue({ _id: participantId, firstName: 'Ada', lastName: 'Lovelace', email: 'ada@example.com', phone: '', }); const apDoc = { checkIns: [ { checkedInAt: new Date('2026-08-01T09:00:00.000Z'), clientOpId: 'op-in' }, ], undos: [], save: jest.fn().mockResolvedValue(undefined), }; AttendanceParticipant.findOne.mockResolvedValue(apDoc); const out = await performBulkCheckInSync({ attendanceId, eventId, checkedInBy: 'user1', operations: [ { clientOpId: 'op-undo', participantId, checkedInAt: '2026-08-01T11:00:00.000Z', action: 'undo', }, ], }); expect(out.results[0].status).toBe('applied'); expect(out.results[0].checkInCount).toBe(0); expect(apDoc.checkIns).toHaveLength(0); expect(apDoc.undos[0].clientOpId).toBe('op-undo'); expect(apDoc.save).toHaveBeenCalled(); }); test('returns duplicate for existing undo clientOpId', async () => { AttendanceParticipant.find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([ { participant: participantId, checkIns: [], undos: [{ clientOpId: 'op-undo-dup', undoneAt: new Date() }], }, ]), }), }); const out = await performBulkCheckInSync({ attendanceId, eventId, checkedInBy: 'user1', operations: [ { clientOpId: 'op-undo-dup', participantId, checkedInAt: '2026-08-01T11:00:00.000Z', action: 'undo', }, ], }); expect(out.results[0].status).toBe('duplicate'); }); test('conflicts undo when participant is not checked in', async () => { AttendanceParticipant.find.mockReturnValue({ select: () => ({ lean: () => Promise.resolve([]) }), }); getMembership.mockReturnValue({ checkedIn: false }); Participant.findById.mockResolvedValue({ _id: participantId, firstName: 'Ada', lastName: 'Lovelace', }); AttendanceParticipant.findOne.mockResolvedValue({ checkIns: [], undos: [] }); const out = await performBulkCheckInSync({ attendanceId, eventId, checkedInBy: 'user1', operations: [ { clientOpId: 'op-undo-empty', participantId, checkedInAt: '2026-08-01T11:00:00.000Z', action: 'undo', }, ], }); expect(out.results[0].status).toBe('conflict'); expect(out.results[0].message).toMatch(/not checked in/i); }); });