import { Test, TestingModule } from '@nestjs/testing' import { SlotGeneratorService } from '../slot-generator.service' import { PrismaService } from '../../prisma/prisma.service' import { TimeSlotStatus, TimeSlotSource, MembershipStatus, BookingStatus, getDefaultTimeSlots, } from '@mp-pilates/shared' // --------------------------------------------------------------------------- // Mock PrismaService // --------------------------------------------------------------------------- const mockPrisma = { timeSlot: { createMany: jest.fn(), updateMany: jest.fn(), }, membership: { updateMany: jest.fn(), }, booking: { updateMany: jest.fn(), }, } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('SlotGeneratorService', () => { let service: SlotGeneratorService beforeEach(async () => { jest.clearAllMocks() const module: TestingModule = await Test.createTestingModule({ providers: [ SlotGeneratorService, { provide: PrismaService, useValue: mockPrisma }, ], }).compile() service = module.get(SlotGeneratorService) }) // ------------------------------------------------------------------------- // generateSlots // ------------------------------------------------------------------------- describe('generateSlots', () => { it('creates slots for every day using the default schedule (14 slots per day)', async () => { const defaultSlots = getDefaultTimeSlots() mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: defaultSlots.length * 7 }) const count = await service.generateSlots(7) expect(mockPrisma.timeSlot.createMany).toHaveBeenCalledTimes(1) const { data, skipDuplicates } = mockPrisma.timeSlot.createMany.mock.calls[0][0] as { data: unknown[] skipDuplicates: boolean } expect(skipDuplicates).toBe(true) // 7 days × 14 slots per day = 98 expect(data).toHaveLength(defaultSlots.length * 7) expect(count).toBe(defaultSlots.length * 7) }) it('creates 14 slots per day (08:00-22:00 hourly)', async () => { mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 14 }) await service.generateSlots(1) const { data } = mockPrisma.timeSlot.createMany.mock.calls[0][0] as { data: Array<{ startTime: string; endTime: string }> } expect(data).toHaveLength(14) expect(data[0].startTime).toBe('08:00') expect(data[0].endTime).toBe('09:00') expect(data[13].startTime).toBe('21:00') expect(data[13].endTime).toBe('22:00') }) it('passes skipDuplicates: true to handle existing date+time combinations', async () => { mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 0 }) await service.generateSlots(1) const call = mockPrisma.timeSlot.createMany.mock.calls[0][0] as { skipDuplicates: boolean } expect(call.skipDuplicates).toBe(true) }) it('sets source to TEMPLATE for all generated slots', async () => { mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: 14 }) await service.generateSlots(1) const { data } = mockPrisma.timeSlot.createMany.mock.calls[0][0] as { data: Array<{ source: TimeSlotSource }> } for (const slot of data) { expect(slot.source).toBe(TimeSlotSource.TEMPLATE) } }) }) // ------------------------------------------------------------------------- // cleanupExpiredSlots // ------------------------------------------------------------------------- describe('cleanupExpiredSlots', () => { it('marks past OPEN slots as CLOSED', async () => { mockPrisma.timeSlot.updateMany.mockResolvedValueOnce({ count: 3 }) const count = await service.cleanupExpiredSlots() expect(count).toBe(3) expect(mockPrisma.timeSlot.updateMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ status: TimeSlotStatus.OPEN, }), data: { status: TimeSlotStatus.CLOSED }, }), ) }) it('queries slots with date lt today', async () => { mockPrisma.timeSlot.updateMany.mockResolvedValueOnce({ count: 0 }) await service.cleanupExpiredSlots() const where = (mockPrisma.timeSlot.updateMany.mock.calls[0][0] as { where: { date: { lt: Date } } }).where const today = new Date() today.setUTCHours(0, 0, 0, 0) // The lt date should be today at midnight (within 1 second of test run) const diff = Math.abs(where.date.lt.getTime() - today.getTime()) expect(diff).toBeLessThan(1000) }) }) // ------------------------------------------------------------------------- // checkExpiredMemberships // ------------------------------------------------------------------------- describe('checkExpiredMemberships', () => { it('marks expired-date memberships as EXPIRED', async () => { mockPrisma.membership.updateMany .mockResolvedValueOnce({ count: 2 }) // date-expired .mockResolvedValueOnce({ count: 1 }) // used-up const total = await service.checkExpiredMemberships() expect(total).toBe(3) const firstCall = ( mockPrisma.membership.updateMany.mock.calls[0][0] as { data: { status: MembershipStatus } } ).data expect(firstCall.status).toBe(MembershipStatus.EXPIRED) }) it('marks zero-remaining memberships as USED_UP', async () => { mockPrisma.membership.updateMany .mockResolvedValueOnce({ count: 0 }) .mockResolvedValueOnce({ count: 4 }) const total = await service.checkExpiredMemberships() expect(total).toBe(4) const secondCall = ( mockPrisma.membership.updateMany.mock.calls[1][0] as { where: { remainingTimes: number } data: { status: MembershipStatus } } ) expect(secondCall.where.remainingTimes).toBe(0) expect(secondCall.data.status).toBe(MembershipStatus.USED_UP) }) it('runs both updates in parallel and sums results', async () => { mockPrisma.membership.updateMany .mockResolvedValueOnce({ count: 3 }) .mockResolvedValueOnce({ count: 2 }) const total = await service.checkExpiredMemberships() expect(total).toBe(5) expect(mockPrisma.membership.updateMany).toHaveBeenCalledTimes(2) }) }) // ------------------------------------------------------------------------- // completeBookings // ------------------------------------------------------------------------- describe('completeBookings', () => { it('marks past CONFIRMED bookings as COMPLETED', async () => { mockPrisma.booking.updateMany.mockResolvedValueOnce({ count: 5 }) const count = await service.completeBookings() expect(count).toBe(5) expect(mockPrisma.booking.updateMany).toHaveBeenCalledWith( expect.objectContaining({ where: expect.objectContaining({ status: BookingStatus.CONFIRMED, }), data: { status: BookingStatus.COMPLETED }, }), ) }) it('filters by timeSlot.date lt today', async () => { mockPrisma.booking.updateMany.mockResolvedValueOnce({ count: 0 }) await service.completeBookings() const where = (mockPrisma.booking.updateMany.mock.calls[0][0] as { where: { timeSlot: { date: { lt: Date } } } }).where const today = new Date() today.setUTCHours(0, 0, 0, 0) const diff = Math.abs(where.timeSlot.date.lt.getTime() - today.getTime()) expect(diff).toBeLessThan(1000) }) }) })