feat(server): add membership and time-slot modules
Membership: card type CRUD, deduction/restore logic, valid card lookup (15 tests) TimeSlot: slot generation from week templates, availability query with booking status, admin management, cleanup tasks (26 tests) 65 total tests passing
This commit is contained in:
141
packages/server/src/time-slot/time-slot.service.ts
Normal file
141
packages/server/src/time-slot/time-slot.service.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common'
|
||||
import { TimeSlotStatus, BookingStatus, DEFAULT_SLOT_CAPACITY } from '@mp-pilates/shared'
|
||||
import { TimeSlotSource } from '@mp-pilates/shared'
|
||||
import { PrismaService } from '../prisma/prisma.service'
|
||||
import type { TimeSlotWithBookingStatus } from '@mp-pilates/shared'
|
||||
import type { CreateManualSlotDto } from './dto/create-manual-slot.dto'
|
||||
|
||||
@Injectable()
|
||||
export class TimeSlotService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async getAvailableSlots(
|
||||
date: string,
|
||||
userId?: string,
|
||||
): Promise<TimeSlotWithBookingStatus[]> {
|
||||
const parsedDate = new Date(date)
|
||||
|
||||
// Build start/end of day boundaries for the query
|
||||
const startOfDay = new Date(parsedDate)
|
||||
startOfDay.setUTCHours(0, 0, 0, 0)
|
||||
const endOfDay = new Date(parsedDate)
|
||||
endOfDay.setUTCHours(23, 59, 59, 999)
|
||||
|
||||
const slots = await this.prisma.timeSlot.findMany({
|
||||
where: {
|
||||
date: {
|
||||
gte: startOfDay,
|
||||
lte: endOfDay,
|
||||
},
|
||||
status: { not: TimeSlotStatus.CLOSED },
|
||||
},
|
||||
orderBy: { startTime: 'asc' },
|
||||
include: {
|
||||
bookings: userId
|
||||
? {
|
||||
where: {
|
||||
userId,
|
||||
status: BookingStatus.CONFIRMED,
|
||||
},
|
||||
select: { id: true },
|
||||
}
|
||||
: false,
|
||||
},
|
||||
})
|
||||
|
||||
return slots.map((slot) => {
|
||||
const myBooking =
|
||||
userId && Array.isArray(slot.bookings) && slot.bookings.length > 0
|
||||
? slot.bookings[0]
|
||||
: null
|
||||
|
||||
return {
|
||||
id: slot.id,
|
||||
date: slot.date.toISOString().split('T')[0],
|
||||
startTime: slot.startTime,
|
||||
endTime: slot.endTime,
|
||||
capacity: slot.capacity,
|
||||
bookedCount: slot.bookedCount,
|
||||
status: slot.status as TimeSlotStatus,
|
||||
source: slot.source as TimeSlotSource,
|
||||
templateId: slot.templateId,
|
||||
createdAt: slot.createdAt.toISOString(),
|
||||
updatedAt: slot.updatedAt.toISOString(),
|
||||
isBookedByMe: myBooking !== null,
|
||||
myBookingId: myBooking?.id ?? null,
|
||||
} satisfies TimeSlotWithBookingStatus
|
||||
})
|
||||
}
|
||||
|
||||
async getSlotById(id: string) {
|
||||
const slot = await this.prisma.timeSlot.findUnique({
|
||||
where: { id },
|
||||
include: { bookings: true },
|
||||
})
|
||||
|
||||
if (!slot) {
|
||||
throw new NotFoundException(`TimeSlot ${id} not found`)
|
||||
}
|
||||
|
||||
return slot
|
||||
}
|
||||
|
||||
async createManualSlot(dto: CreateManualSlotDto) {
|
||||
const date = new Date(dto.date)
|
||||
date.setUTCHours(0, 0, 0, 0)
|
||||
|
||||
return this.prisma.timeSlot.create({
|
||||
data: {
|
||||
date,
|
||||
startTime: dto.startTime,
|
||||
endTime: dto.endTime,
|
||||
capacity: dto.capacity ?? DEFAULT_SLOT_CAPACITY,
|
||||
source: TimeSlotSource.MANUAL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async closeSlot(id: string) {
|
||||
const slot = await this.prisma.timeSlot.findUnique({ where: { id } })
|
||||
if (!slot) {
|
||||
throw new NotFoundException(`TimeSlot ${id} not found`)
|
||||
}
|
||||
|
||||
return this.prisma.timeSlot.update({
|
||||
where: { id },
|
||||
data: { status: TimeSlotStatus.CLOSED },
|
||||
})
|
||||
}
|
||||
|
||||
async getWeekTemplates() {
|
||||
return this.prisma.weekTemplate.findMany({
|
||||
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
|
||||
})
|
||||
}
|
||||
|
||||
async replaceWeekTemplates(
|
||||
items: Array<{
|
||||
dayOfWeek: number
|
||||
startTime: string
|
||||
endTime: string
|
||||
capacity?: number
|
||||
isActive?: boolean
|
||||
}>,
|
||||
) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
await tx.weekTemplate.deleteMany()
|
||||
|
||||
const created = await tx.weekTemplate.createMany({
|
||||
data: items.map((item) => ({
|
||||
dayOfWeek: item.dayOfWeek,
|
||||
startTime: item.startTime,
|
||||
endTime: item.endTime,
|
||||
capacity: item.capacity ?? DEFAULT_SLOT_CAPACITY,
|
||||
isActive: item.isActive ?? true,
|
||||
})),
|
||||
})
|
||||
|
||||
return created
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user