feat: 完善课程订阅
This commit is contained in:
@@ -38,6 +38,7 @@ enum TimeSlotSource {
|
||||
}
|
||||
|
||||
enum BookingStatus {
|
||||
PENDING_CONFIRMATION
|
||||
CONFIRMED
|
||||
CANCELLED
|
||||
COMPLETED
|
||||
@@ -152,8 +153,11 @@ model Booking {
|
||||
userId String @map("user_id")
|
||||
timeSlotId String @map("time_slot_id")
|
||||
membershipId String @map("membership_id")
|
||||
status BookingStatus @default(CONFIRMED)
|
||||
status BookingStatus @default(PENDING_CONFIRMATION)
|
||||
cancelledAt DateTime? @map("cancelled_at")
|
||||
confirmedAt DateTime? @map("confirmed_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
operatorId String? @map("operator_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@ -161,12 +165,29 @@ model Booking {
|
||||
timeSlot TimeSlot @relation(fields: [timeSlotId], references: [id])
|
||||
membership Membership @relation(fields: [membershipId], references: [id])
|
||||
|
||||
statusHistory BookingStatusHistory[]
|
||||
|
||||
@@unique([userId, timeSlotId])
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@map("bookings")
|
||||
}
|
||||
|
||||
model BookingStatusHistory {
|
||||
id String @id @default(uuid())
|
||||
bookingId String @map("booking_id")
|
||||
fromStatus String? @map("from_status")
|
||||
toStatus String @map("to_status")
|
||||
operatorId String? @map("operator_id")
|
||||
remark String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
booking Booking @relation(fields: [bookingId], references: [id])
|
||||
|
||||
@@index([bookingId])
|
||||
@@map("booking_status_history")
|
||||
}
|
||||
|
||||
model Order {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
|
||||
@@ -130,6 +130,7 @@ function buildTxMock(overrides: Record<string, unknown> = {}) {
|
||||
},
|
||||
booking: {
|
||||
findUnique: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
@@ -205,7 +206,7 @@ describe('BookingService', () => {
|
||||
it('creates booking, increments bookedCount, and deducts membership (TIMES card)', async () => {
|
||||
const tx = buildTxMock()
|
||||
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
|
||||
tx.booking.findUnique.mockResolvedValue(null) // no duplicate
|
||||
tx.booking.findFirst.mockResolvedValue(null) // no duplicate
|
||||
tx.membership.findUnique.mockResolvedValue(mockActiveMembership)
|
||||
tx.booking.create.mockResolvedValue(mockConfirmedBooking)
|
||||
tx.timeSlot.update.mockResolvedValue({ ...mockOpenSlot, bookedCount: 1 })
|
||||
@@ -258,7 +259,7 @@ describe('BookingService', () => {
|
||||
|
||||
const tx = buildTxMock()
|
||||
tx.timeSlot.findUnique.mockResolvedValue(nearFullSlot)
|
||||
tx.booking.findUnique.mockResolvedValue(null)
|
||||
tx.booking.findFirst.mockResolvedValue(null)
|
||||
tx.membership.findUnique.mockResolvedValue(mockActiveMembership)
|
||||
tx.booking.create.mockResolvedValue(mockConfirmedBooking)
|
||||
tx.timeSlot.update.mockResolvedValue({ ...nearFullSlot, bookedCount: 5, status: TimeSlotStatus.FULL })
|
||||
@@ -286,7 +287,7 @@ describe('BookingService', () => {
|
||||
|
||||
const tx = buildTxMock()
|
||||
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
|
||||
tx.booking.findUnique.mockResolvedValue(null)
|
||||
tx.booking.findFirst.mockResolvedValue(null)
|
||||
tx.membership.findUnique.mockResolvedValue(mockDurationMembership)
|
||||
tx.booking.create.mockResolvedValue({ ...mockConfirmedBooking, membershipId: mockDurationMembership.id })
|
||||
tx.timeSlot.update.mockResolvedValue({ ...mockOpenSlot, bookedCount: 1 })
|
||||
@@ -310,7 +311,7 @@ describe('BookingService', () => {
|
||||
|
||||
const tx = buildTxMock()
|
||||
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
|
||||
tx.booking.findUnique.mockResolvedValue(null)
|
||||
tx.booking.findFirst.mockResolvedValue(null)
|
||||
tx.membership.findUnique.mockResolvedValue(lastTimeMembership)
|
||||
tx.booking.create.mockResolvedValue(mockConfirmedBooking)
|
||||
tx.timeSlot.update.mockResolvedValue({ ...mockOpenSlot, bookedCount: 1 })
|
||||
@@ -351,7 +352,7 @@ describe('BookingService', () => {
|
||||
it('throws ConflictException on duplicate booking (same user + slot)', async () => {
|
||||
const tx = buildTxMock()
|
||||
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
|
||||
tx.booking.findUnique.mockResolvedValue(mockConfirmedBooking) // duplicate exists
|
||||
tx.booking.findFirst.mockResolvedValue(mockConfirmedBooking) // duplicate exists
|
||||
|
||||
;(prisma.$transaction as jest.Mock).mockImplementation((fn) => fn(tx))
|
||||
|
||||
@@ -363,7 +364,7 @@ describe('BookingService', () => {
|
||||
it('throws BadRequestException when membership is not ACTIVE (expired status)', async () => {
|
||||
const tx = buildTxMock()
|
||||
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
|
||||
tx.booking.findUnique.mockResolvedValue(null)
|
||||
tx.booking.findFirst.mockResolvedValue(null)
|
||||
tx.membership.findUnique.mockResolvedValue(mockExpiredMembership)
|
||||
|
||||
;(prisma.$transaction as jest.Mock).mockImplementation((fn) => fn(tx))
|
||||
@@ -376,7 +377,7 @@ describe('BookingService', () => {
|
||||
it('throws BadRequestException when TIMES membership has 0 remaining', async () => {
|
||||
const tx = buildTxMock()
|
||||
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
|
||||
tx.booking.findUnique.mockResolvedValue(null)
|
||||
tx.booking.findFirst.mockResolvedValue(null)
|
||||
tx.membership.findUnique.mockResolvedValue(mockMembershipNoTimes)
|
||||
|
||||
;(prisma.$transaction as jest.Mock).mockImplementation((fn) => fn(tx))
|
||||
@@ -403,7 +404,7 @@ describe('BookingService', () => {
|
||||
|
||||
const tx = buildTxMock()
|
||||
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
|
||||
tx.booking.findUnique.mockResolvedValue(null)
|
||||
tx.booking.findFirst.mockResolvedValue(null)
|
||||
tx.membership.findUnique.mockResolvedValue(otherUserMembership)
|
||||
|
||||
;(prisma.$transaction as jest.Mock).mockImplementation((fn) => fn(tx))
|
||||
|
||||
@@ -62,6 +62,18 @@ export class BookingController {
|
||||
)
|
||||
}
|
||||
|
||||
@Get('booking/:id/history')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async getBookingStatusHistory(@Param('id') id: string) {
|
||||
return this.bookingService.getBookingStatusHistory(id)
|
||||
}
|
||||
|
||||
@Get('booking/:id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async getBookingById(@Param('id') id: string) {
|
||||
return this.bookingService.getBookingById(id)
|
||||
}
|
||||
|
||||
// ─── Admin Endpoints ──────────────────────────────────────────────────────
|
||||
|
||||
@Get('admin/bookings')
|
||||
@@ -78,4 +90,37 @@ export class BookingController {
|
||||
status,
|
||||
)
|
||||
}
|
||||
|
||||
@Put('booking/:id/confirm')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
async confirmBooking(
|
||||
@CurrentUser('sub') operatorId: string,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { remark?: string },
|
||||
) {
|
||||
return this.bookingService.confirmBooking(id, operatorId, body.remark)
|
||||
}
|
||||
|
||||
@Put('booking/:id/complete')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
async completeBooking(
|
||||
@CurrentUser('sub') operatorId: string,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { remark?: string },
|
||||
) {
|
||||
return this.bookingService.completeBooking(id, operatorId, body.remark)
|
||||
}
|
||||
|
||||
@Put('booking/:id/noshow')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(UserRole.ADMIN)
|
||||
async markNoShow(
|
||||
@CurrentUser('sub') operatorId: string,
|
||||
@Param('id') id: string,
|
||||
@Body() body: { remark?: string },
|
||||
) {
|
||||
return this.bookingService.markNoShow(id, operatorId, body.remark)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common'
|
||||
import { Booking, Membership, TimeSlot } from '@prisma/client'
|
||||
import { Booking, Membership, TimeSlot, BookingStatusHistory } from '@prisma/client'
|
||||
import { BookingStatus, CardTypeCategory, MembershipStatus, TimeSlotStatus } from '@mp-pilates/shared'
|
||||
import { PrismaService } from '../prisma/prisma.service'
|
||||
import { MembershipService } from '../membership/membership.service'
|
||||
@@ -31,10 +31,9 @@ export interface CancelBookingResult {
|
||||
refunded: boolean
|
||||
}
|
||||
|
||||
// ─── Helpers ───────────────────────────────────────────────────────────────
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function buildSlotStartMs(slotDate: Date, startTime: string): number {
|
||||
// slotDate is stored as DATE (midnight UTC); startTime is "HH:mm"
|
||||
const [hours, minutes] = startTime.split(':').map(Number)
|
||||
const d = new Date(slotDate)
|
||||
d.setUTCHours(hours, minutes, 0, 0)
|
||||
@@ -71,9 +70,13 @@ export class BookingService {
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Check for duplicate booking (@@unique [userId, timeSlotId])
|
||||
const existing = await tx.booking.findUnique({
|
||||
where: { userId_timeSlotId: { userId, timeSlotId: dto.timeSlotId } },
|
||||
// 2. Check for active (PENDING_CONFIRMATION or CONFIRMED) booking — cancelled bookings don't block re-booking
|
||||
const existing = await tx.booking.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
timeSlotId: dto.timeSlotId,
|
||||
status: { in: [BookingStatus.PENDING_CONFIRMATION, BookingStatus.CONFIRMED] },
|
||||
},
|
||||
})
|
||||
if (existing) {
|
||||
throw new ConflictException('You have already booked this time slot')
|
||||
@@ -102,10 +105,7 @@ export class BookingService {
|
||||
cardType.type === CardTypeCategory.TRIAL
|
||||
|
||||
if (isTimeBased) {
|
||||
// 4a. TIMES / TRIAL: must have remaining times
|
||||
if ((membership.remainingTimes ?? 0) <= 0) {
|
||||
throw new BadRequestException('No remaining times on this membership')
|
||||
}
|
||||
// 4a. TIMES / TRIAL: must have remaining times (check at confirm time, not booking time)
|
||||
} else {
|
||||
// 4b. DURATION: must not be expired
|
||||
if (membership.expireDate <= new Date()) {
|
||||
@@ -113,38 +113,107 @@ export class BookingService {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Create booking
|
||||
// 5. Create booking with PENDING_CONFIRMATION status
|
||||
const newBooking = await tx.booking.create({
|
||||
data: {
|
||||
userId,
|
||||
timeSlotId: dto.timeSlotId,
|
||||
membershipId: dto.membershipId,
|
||||
status: BookingStatus.CONFIRMED,
|
||||
status: BookingStatus.PENDING_CONFIRMATION,
|
||||
},
|
||||
})
|
||||
|
||||
// 6. Increment bookedCount; set FULL if at capacity
|
||||
const newBookedCount = timeSlot.bookedCount + 1
|
||||
// 6. Record status history: created
|
||||
await tx.bookingStatusHistory.create({
|
||||
data: {
|
||||
bookingId: newBooking.id,
|
||||
fromStatus: null,
|
||||
toStatus: BookingStatus.PENDING_CONFIRMATION,
|
||||
operatorId: userId,
|
||||
remark: '学员发起预约',
|
||||
},
|
||||
})
|
||||
|
||||
return newBooking
|
||||
})
|
||||
|
||||
// Re-fetch with relations after transaction
|
||||
return this.fetchBookingWithRelations(booking.id)
|
||||
}
|
||||
|
||||
// ─── Confirm Booking (Admin) ─────────────────────────────────────────────
|
||||
|
||||
async confirmBooking(
|
||||
bookingId: string,
|
||||
operatorId: string,
|
||||
remark?: string,
|
||||
): Promise<BookingWithRelations> {
|
||||
const booking = await this.prisma.$transaction(async (tx) => {
|
||||
// 1. Find booking with timeSlot and membership
|
||||
const existing = await tx.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
timeSlot: true,
|
||||
membership: { include: { cardType: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundException(`Booking ${bookingId} not found`)
|
||||
}
|
||||
if (existing.status !== BookingStatus.PENDING_CONFIRMATION) {
|
||||
throw new BadRequestException(
|
||||
`Cannot confirm booking with status: ${existing.status}`,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Validate membership still has available times
|
||||
const cardType = existing.membership.cardType
|
||||
const isTimeBased =
|
||||
cardType.type === CardTypeCategory.TIMES ||
|
||||
cardType.type === CardTypeCategory.TRIAL
|
||||
|
||||
if (isTimeBased) {
|
||||
if ((existing.membership.remainingTimes ?? 0) <= 0) {
|
||||
throw new BadRequestException('No remaining times on this membership')
|
||||
}
|
||||
} else {
|
||||
if (existing.membership.expireDate <= new Date()) {
|
||||
throw new BadRequestException('Membership has expired')
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update booking status to CONFIRMED
|
||||
const updated = await tx.booking.update({
|
||||
where: { id: bookingId },
|
||||
data: {
|
||||
status: BookingStatus.CONFIRMED,
|
||||
confirmedAt: new Date(),
|
||||
operatorId,
|
||||
},
|
||||
})
|
||||
|
||||
// 4. Increment bookedCount; set FULL if at capacity
|
||||
const newBookedCount = existing.timeSlot.bookedCount + 1
|
||||
const newSlotStatus =
|
||||
newBookedCount >= timeSlot.capacity ? TimeSlotStatus.FULL : TimeSlotStatus.OPEN
|
||||
newBookedCount >= existing.timeSlot.capacity ? TimeSlotStatus.FULL : TimeSlotStatus.OPEN
|
||||
|
||||
await tx.timeSlot.update({
|
||||
where: { id: dto.timeSlotId },
|
||||
where: { id: existing.timeSlotId },
|
||||
data: {
|
||||
bookedCount: newBookedCount,
|
||||
status: newSlotStatus,
|
||||
},
|
||||
})
|
||||
|
||||
// 7. Deduct membership (inside transaction — replicate logic to avoid
|
||||
// calling the service method which uses the outer prisma client)
|
||||
// 5. Deduct membership times
|
||||
if (isTimeBased) {
|
||||
const newRemainingTimes = (membership.remainingTimes ?? 0) - 1
|
||||
const newRemainingTimes = (existing.membership.remainingTimes ?? 0) - 1
|
||||
const newMembershipStatus =
|
||||
newRemainingTimes <= 0 ? MembershipStatus.USED_UP : MembershipStatus.ACTIVE
|
||||
|
||||
await tx.membership.update({
|
||||
where: { id: dto.membershipId },
|
||||
where: { id: existing.membershipId },
|
||||
data: {
|
||||
remainingTimes: newRemainingTimes,
|
||||
status: newMembershipStatus,
|
||||
@@ -152,10 +221,88 @@ export class BookingService {
|
||||
})
|
||||
}
|
||||
|
||||
return newBooking
|
||||
// 6. Record status history
|
||||
await tx.bookingStatusHistory.create({
|
||||
data: {
|
||||
bookingId,
|
||||
fromStatus: BookingStatus.PENDING_CONFIRMATION,
|
||||
toStatus: BookingStatus.CONFIRMED,
|
||||
operatorId,
|
||||
remark: remark || '老师确认预约',
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
})
|
||||
|
||||
return this.fetchBookingWithRelations(booking.id)
|
||||
}
|
||||
|
||||
// ─── Complete / NoShow Booking (Admin) ──────────────────────────────────
|
||||
|
||||
async completeBooking(
|
||||
bookingId: string,
|
||||
operatorId: string,
|
||||
remark?: string,
|
||||
): Promise<BookingWithRelations> {
|
||||
return this.markBookingStatus(bookingId, operatorId, BookingStatus.COMPLETED, remark || '老师核销完成')
|
||||
}
|
||||
|
||||
async markNoShow(
|
||||
bookingId: string,
|
||||
operatorId: string,
|
||||
remark?: string,
|
||||
): Promise<BookingWithRelations> {
|
||||
return this.markBookingStatus(bookingId, operatorId, BookingStatus.NO_SHOW, remark || '学员未出席')
|
||||
}
|
||||
|
||||
private async markBookingStatus(
|
||||
bookingId: string,
|
||||
operatorId: string,
|
||||
toStatus: BookingStatus,
|
||||
remark: string,
|
||||
): Promise<BookingWithRelations> {
|
||||
const booking = await this.prisma.$transaction(async (tx) => {
|
||||
const existing = await tx.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: { timeSlot: true },
|
||||
})
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundException(`Booking ${bookingId} not found`)
|
||||
}
|
||||
if (existing.status !== BookingStatus.CONFIRMED) {
|
||||
throw new BadRequestException(
|
||||
`Cannot mark as ${toStatus} with status: ${existing.status}`,
|
||||
)
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
status: toStatus,
|
||||
operatorId,
|
||||
}
|
||||
if (toStatus === BookingStatus.COMPLETED) {
|
||||
updateData.completedAt = new Date()
|
||||
}
|
||||
|
||||
const updated = await tx.booking.update({
|
||||
where: { id: bookingId },
|
||||
data: updateData,
|
||||
})
|
||||
|
||||
await tx.bookingStatusHistory.create({
|
||||
data: {
|
||||
bookingId,
|
||||
fromStatus: BookingStatus.CONFIRMED,
|
||||
toStatus,
|
||||
operatorId,
|
||||
remark,
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
})
|
||||
|
||||
// Re-fetch with relations after transaction
|
||||
return this.fetchBookingWithRelations(booking.id)
|
||||
}
|
||||
|
||||
@@ -165,7 +312,6 @@ export class BookingService {
|
||||
userId: string,
|
||||
bookingId: string,
|
||||
): Promise<CancelBookingResult> {
|
||||
// 1. Find booking with timeSlot and membership
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
@@ -180,13 +326,37 @@ export class BookingService {
|
||||
if (booking.userId !== userId) {
|
||||
throw new ForbiddenException('This booking does not belong to you')
|
||||
}
|
||||
|
||||
let refunded = false
|
||||
|
||||
// PENDING_CONFIRMATION: can cancel directly, no refund needed (times never deducted)
|
||||
if (booking.status === BookingStatus.PENDING_CONFIRMATION) {
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.booking.update({
|
||||
where: { id: bookingId },
|
||||
data: { status: BookingStatus.CANCELLED },
|
||||
})
|
||||
|
||||
await tx.bookingStatusHistory.create({
|
||||
data: {
|
||||
bookingId,
|
||||
fromStatus: BookingStatus.PENDING_CONFIRMATION,
|
||||
toStatus: BookingStatus.CANCELLED,
|
||||
operatorId: userId,
|
||||
remark: '学员取消预约(待确认状态)',
|
||||
},
|
||||
})
|
||||
})
|
||||
return { booking: { ...booking, status: BookingStatus.CANCELLED }, refunded }
|
||||
}
|
||||
|
||||
// CONFIRMED: check cancel time limit and potentially refund
|
||||
if (booking.status !== BookingStatus.CONFIRMED) {
|
||||
throw new BadRequestException(
|
||||
`Cannot cancel booking with status: ${booking.status}`,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Determine refund eligibility
|
||||
const studioConfig = await this.studioService.getInfo()
|
||||
const { cancelHoursLimit } = studioConfig
|
||||
|
||||
@@ -194,9 +364,7 @@ export class BookingService {
|
||||
const deadlineMs = Date.now() + cancelHoursLimit * 3600 * 1000
|
||||
const withinLimit = slotStartMs >= deadlineMs
|
||||
|
||||
// 3. Transaction: cancel booking, restore slot, conditionally restore membership
|
||||
const updatedBooking = await this.prisma.$transaction(async (tx) => {
|
||||
// Cancel the booking
|
||||
const cancelled = await tx.booking.update({
|
||||
where: { id: bookingId },
|
||||
data: {
|
||||
@@ -241,13 +409,48 @@ export class BookingService {
|
||||
status: newStatus,
|
||||
},
|
||||
})
|
||||
refunded = true
|
||||
}
|
||||
}
|
||||
|
||||
await tx.bookingStatusHistory.create({
|
||||
data: {
|
||||
bookingId,
|
||||
fromStatus: BookingStatus.CONFIRMED,
|
||||
toStatus: BookingStatus.CANCELLED,
|
||||
operatorId: userId,
|
||||
remark: refunded ? '学员取消预约(超时退款)' : '学员取消预约(未超时不退款)',
|
||||
},
|
||||
})
|
||||
|
||||
return cancelled
|
||||
})
|
||||
|
||||
return { booking: { ...updatedBooking }, refunded: withinLimit }
|
||||
return { booking: { ...updatedBooking }, refunded }
|
||||
}
|
||||
|
||||
// ─── Get Booking Status History ──────────────────────────────────────────
|
||||
|
||||
async getBookingStatusHistory(bookingId: string): Promise<BookingStatusHistory[]> {
|
||||
const history = await this.prisma.bookingStatusHistory.findMany({
|
||||
where: { bookingId },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
})
|
||||
return history
|
||||
}
|
||||
|
||||
// ─── Get Booking By Id ─────────────────────────────────────────────────
|
||||
|
||||
async getBookingById(bookingId: string): Promise<BookingWithRelations | null> {
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
timeSlot: true,
|
||||
membership: { include: { cardType: true } },
|
||||
user: { select: { id: true, nickname: true, phone: true } },
|
||||
},
|
||||
})
|
||||
return booking as BookingWithRelations | null
|
||||
}
|
||||
|
||||
// ─── Get My Bookings ─────────────────────────────────────────────────────
|
||||
@@ -294,7 +497,7 @@ export class BookingService {
|
||||
const bookings = await this.prisma.booking.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: BookingStatus.CONFIRMED,
|
||||
status: { in: [BookingStatus.PENDING_CONFIRMATION, BookingStatus.CONFIRMED] },
|
||||
timeSlot: {
|
||||
date: { gte: today },
|
||||
},
|
||||
@@ -346,7 +549,7 @@ export class BookingService {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Private Helpers ──────────────────────────────────────────────────────
|
||||
// ─── Private Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
private async fetchBookingWithRelations(bookingId: string): Promise<BookingWithRelations> {
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user