154 lines
4.1 KiB
TypeScript
154 lines
4.1 KiB
TypeScript
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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<number> {
|
|
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
|
|
}
|
|
}
|