import { Injectable, Logger } from '@nestjs/common' import { TimeSlotStatus, TimeSlotSource, MembershipStatus, BookingStatus, SLOT_GENERATION_DAYS, DEFAULT_SLOT_CAPACITY, getDefaultTimeSlots, } from '@mp-pilates/shared' import { PrismaService } from '../prisma/prisma.service' /** Build a UTC Date for midnight of a local calendar date */ function toUtcMidnight(date: Date): Date { const d = new Date(date) d.setUTCHours(0, 0, 0, 0) return d } @Injectable() export class SlotGeneratorService { private readonly logger = new Logger(SlotGeneratorService.name) constructor(private readonly prisma: PrismaService) {} /** * Generate time slots for the next `daysAhead` days based on the fixed * default schedule (Mon-Sun: 08:00-09:00, then 09:30-21:30 hourly). * Uses `createMany` with `skipDuplicates` so re-runs are safe. * * @returns Number of newly created slots */ async generateSlots(daysAhead: number = SLOT_GENERATION_DAYS): Promise { const defaultSlots = getDefaultTimeSlots() const tomorrow = new Date() tomorrow.setDate(tomorrow.getDate() + 1) tomorrow.setUTCHours(0, 0, 0, 0) const slotsToCreate: Array<{ date: Date startTime: string endTime: string capacity: number source: TimeSlotSource }> = [] for (let offset = 0; offset < daysAhead; offset++) { const target = new Date(tomorrow) target.setDate(target.getDate() + offset) for (const slot of defaultSlots) { slotsToCreate.push({ date: toUtcMidnight(target), startTime: slot.startTime, endTime: slot.endTime, capacity: DEFAULT_SLOT_CAPACITY, source: TimeSlotSource.TEMPLATE, }) } } if (slotsToCreate.length === 0) { return 0 } const result = await this.prisma.timeSlot.createMany({ data: slotsToCreate, skipDuplicates: true, }) this.logger.log(`Generated ${result.count} new time slots`) return result.count } /** * Mark all OPEN slots whose date is strictly before today as CLOSED. * * @returns Number of slots updated */ async cleanupExpiredSlots(): Promise { const today = new Date() today.setUTCHours(0, 0, 0, 0) const result = await this.prisma.timeSlot.updateMany({ where: { status: TimeSlotStatus.OPEN, date: { lt: today }, }, data: { status: TimeSlotStatus.CLOSED }, }) this.logger.log(`Closed ${result.count} expired time slots`) return result.count } /** * Expire memberships whose end date has passed or whose remaining sessions * have been exhausted. * * @returns Total number of memberships updated */ async checkExpiredMemberships(): Promise { const now = new Date() const [expired, usedUp] = await Promise.all([ this.prisma.membership.updateMany({ where: { status: MembershipStatus.ACTIVE, expireDate: { lt: now }, }, data: { status: MembershipStatus.EXPIRED }, }), this.prisma.membership.updateMany({ where: { status: MembershipStatus.ACTIVE, remainingTimes: 0, }, data: { status: MembershipStatus.USED_UP }, }), ]) const total = expired.count + usedUp.count this.logger.log( `Expired ${expired.count} memberships by date, ${usedUp.count} by sessions`, ) return total } /** * Mark CONFIRMED bookings whose associated time slot is in the past as * COMPLETED. * * @returns Number of bookings updated */ async completeBookings(): Promise { const today = new Date() today.setUTCHours(0, 0, 0, 0) const result = await this.prisma.booking.updateMany({ where: { status: BookingStatus.CONFIRMED, timeSlot: { date: { lt: today }, }, }, data: { status: BookingStatus.COMPLETED, completedAt: new Date(), reviewReminderDueAt: new Date(Date.now() + 24 * 3600000) }, }) this.logger.log(`Completed ${result.count} past bookings`) return result.count } }