Added booking logs to keep track of cancelled bookings and no shows

This commit is contained in:
Jose Selesan
2026-05-06 16:54:48 -03:00
parent bb48d9c164
commit 41a217e8a9
13 changed files with 232 additions and 34 deletions

View File

@@ -12,6 +12,7 @@ enum CourtBookingStatus {
CONFIRMED
CANCELLED
COMPLETED
NOSHOW
}
model Sport {
@@ -94,3 +95,20 @@ model CourtBooking {
@@index([bookingDate])
@@map("court_bookings")
}
model CourtBookingLog {
id String @id @db.Uuid
bookingCode String @map("booking_code") @db.VarChar(8)
courtId String @map("court_id") @db.Uuid
bookingDate DateTime @map("booking_date") @db.Date
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
previousStatus CourtBookingStatus @map("previous_status")
newStatus CourtBookingStatus @map("new_status")
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
changedAt DateTime @default(now()) @map("changed_at")
@@map("court_booking_logs")
@@index([courtId, newStatus])
}

View File

@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "court_booking_logs" (
"id" UUID NOT NULL,
"booking_code" UUID NOT NULL,
"court_id" UUID NOT NULL,
"booking_date" DATE NOT NULL,
"start_time" VARCHAR(5) NOT NULL,
"end_time" VARCHAR(5) NOT NULL,
"previous_status" "CourtBookingStatus" NOT NULL,
"new_status" "CourtBookingStatus" NOT NULL,
"changed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "court_booking_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "court_booking_logs_court_id_new_status_idx" ON "court_booking_logs"("court_id", "new_status");

View File

@@ -0,0 +1,13 @@
/*
Warnings:
- Added the required column `customer_name` to the `court_booking_logs` table without a default value. This is not possible if the table is not empty.
- Added the required column `customer_phone` to the `court_booking_logs` table without a default value. This is not possible if the table is not empty.
*/
-- AlterEnum
ALTER TYPE "CourtBookingStatus" ADD VALUE 'NOSHOW';
-- AlterTable
ALTER TABLE "court_booking_logs" ADD COLUMN "customer_name" VARCHAR(120) NOT NULL,
ADD COLUMN "customer_phone" VARCHAR(30) NOT NULL;

View File

@@ -0,0 +1,9 @@
/*
Warnings:
- Changed the type of `booking_code` on the `court_booking_logs` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- AlterTable
ALTER TABLE "court_booking_logs" DROP COLUMN "booking_code",
ADD COLUMN "booking_code" VARCHAR(8) NOT NULL;

View File

@@ -77,6 +77,11 @@ export type CourtPriceRule = Prisma.CourtPriceRuleModel
*
*/
export type CourtBooking = Prisma.CourtBookingModel
/**
* Model CourtBookingLog
*
*/
export type CourtBookingLog = Prisma.CourtBookingLogModel
/**
* Model OnboardingRequest
*

View File

@@ -101,6 +101,11 @@ export type CourtPriceRule = Prisma.CourtPriceRuleModel
*
*/
export type CourtBooking = Prisma.CourtBookingModel
/**
* Model CourtBookingLog
*
*/
export type CourtBookingLog = Prisma.CourtBookingLogModel
/**
* Model OnboardingRequest
*

View File

@@ -33,7 +33,8 @@ export type DayOfWeek = (typeof DayOfWeek)[keyof typeof DayOfWeek]
export const CourtBookingStatus = {
CONFIRMED: 'CONFIRMED',
CANCELLED: 'CANCELLED',
COMPLETED: 'COMPLETED'
COMPLETED: 'COMPLETED',
NOSHOW: 'NOSHOW'
} as const
export type CourtBookingStatus = (typeof CourtBookingStatus)[keyof typeof CourtBookingStatus]

File diff suppressed because one or more lines are too long

View File

@@ -396,6 +396,7 @@ export const ModelName = {
CourtAvailability: 'CourtAvailability',
CourtPriceRule: 'CourtPriceRule',
CourtBooking: 'CourtBooking',
CourtBookingLog: 'CourtBookingLog',
OnboardingRequest: 'OnboardingRequest',
PasswordResetRequest: 'PasswordResetRequest',
Plan: 'Plan'
@@ -414,7 +415,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "onboardingRequest" | "passwordResetRequest" | "plan"
modelProps: "user" | "session" | "account" | "verification" | "complex" | "complexUser" | "complexInvitation" | "sport" | "court" | "courtAvailability" | "courtPriceRule" | "courtBooking" | "courtBookingLog" | "onboardingRequest" | "passwordResetRequest" | "plan"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -1306,6 +1307,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
CourtBookingLog: {
payload: Prisma.$CourtBookingLogPayload<ExtArgs>
fields: Prisma.CourtBookingLogFieldRefs
operations: {
findUnique: {
args: Prisma.CourtBookingLogFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload> | null
}
findUniqueOrThrow: {
args: Prisma.CourtBookingLogFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
findFirst: {
args: Prisma.CourtBookingLogFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload> | null
}
findFirstOrThrow: {
args: Prisma.CourtBookingLogFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
findMany: {
args: Prisma.CourtBookingLogFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>[]
}
create: {
args: Prisma.CourtBookingLogCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
createMany: {
args: Prisma.CourtBookingLogCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.CourtBookingLogCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>[]
}
delete: {
args: Prisma.CourtBookingLogDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
update: {
args: Prisma.CourtBookingLogUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
deleteMany: {
args: Prisma.CourtBookingLogDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.CourtBookingLogUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.CourtBookingLogUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>[]
}
upsert: {
args: Prisma.CourtBookingLogUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$CourtBookingLogPayload>
}
aggregate: {
args: Prisma.CourtBookingLogAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateCourtBookingLog>
}
groupBy: {
args: Prisma.CourtBookingLogGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.CourtBookingLogGroupByOutputType>[]
}
count: {
args: Prisma.CourtBookingLogCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.CourtBookingLogCountAggregateOutputType> | number
}
}
}
OnboardingRequest: {
payload: Prisma.$OnboardingRequestPayload<ExtArgs>
fields: Prisma.OnboardingRequestFieldRefs
@@ -1738,6 +1813,23 @@ export const CourtBookingScalarFieldEnum = {
export type CourtBookingScalarFieldEnum = (typeof CourtBookingScalarFieldEnum)[keyof typeof CourtBookingScalarFieldEnum]
export const CourtBookingLogScalarFieldEnum = {
id: 'id',
bookingCode: 'bookingCode',
courtId: 'courtId',
bookingDate: 'bookingDate',
startTime: 'startTime',
endTime: 'endTime',
previousStatus: 'previousStatus',
newStatus: 'newStatus',
customerName: 'customerName',
customerPhone: 'customerPhone',
changedAt: 'changedAt'
} as const
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
export const OnboardingRequestScalarFieldEnum = {
id: 'id',
fullName: 'fullName',
@@ -2068,6 +2160,7 @@ export type GlobalOmitConfig = {
courtAvailability?: Prisma.CourtAvailabilityOmit
courtPriceRule?: Prisma.CourtPriceRuleOmit
courtBooking?: Prisma.CourtBookingOmit
courtBookingLog?: Prisma.CourtBookingLogOmit
onboardingRequest?: Prisma.OnboardingRequestOmit
passwordResetRequest?: Prisma.PasswordResetRequestOmit
plan?: Prisma.PlanOmit

View File

@@ -63,6 +63,7 @@ export const ModelName = {
CourtAvailability: 'CourtAvailability',
CourtPriceRule: 'CourtPriceRule',
CourtBooking: 'CourtBooking',
CourtBookingLog: 'CourtBookingLog',
OnboardingRequest: 'OnboardingRequest',
PasswordResetRequest: 'PasswordResetRequest',
Plan: 'Plan'
@@ -255,6 +256,23 @@ export const CourtBookingScalarFieldEnum = {
export type CourtBookingScalarFieldEnum = (typeof CourtBookingScalarFieldEnum)[keyof typeof CourtBookingScalarFieldEnum]
export const CourtBookingLogScalarFieldEnum = {
id: 'id',
bookingCode: 'bookingCode',
courtId: 'courtId',
bookingDate: 'bookingDate',
startTime: 'startTime',
endTime: 'endTime',
previousStatus: 'previousStatus',
newStatus: 'newStatus',
customerName: 'customerName',
customerPhone: 'customerPhone',
changedAt: 'changedAt'
} as const
export type CourtBookingLogScalarFieldEnum = (typeof CourtBookingLogScalarFieldEnum)[keyof typeof CourtBookingLogScalarFieldEnum]
export const OnboardingRequestScalarFieldEnum = {
id: 'id',
fullName: 'fullName',

View File

@@ -20,6 +20,7 @@ export type * from './models/Court'
export type * from './models/CourtAvailability'
export type * from './models/CourtPriceRule'
export type * from './models/CourtBooking'
export type * from './models/CourtBookingLog'
export type * from './models/OnboardingRequest'
export type * from './models/PasswordResetRequest'
export type * from './models/Plan'

View File

@@ -164,7 +164,7 @@ function mapBookingResponse(booking: {
endTime: string;
customerName: string;
customerPhone: string;
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED';
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW';
createdAt: Date;
updatedAt: Date;
court: {
@@ -494,34 +494,42 @@ export async function updateAdminBookingStatus(
);
}
const updated = await db.courtBooking.update({
where: { id: booking.id },
if(input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') {
throw new AdminBookingServiceError(
'Solo se pueden marcar como no show las reservas confirmadas.',
409
);
}
await db.courtBookingLog.create({
data: {
status:
input.status === 'COMPLETED' ? CourtBookingStatus.COMPLETED : CourtBookingStatus.CANCELLED,
},
include: {
court: {
select: {
id: true,
name: true,
sport: {
select: {
id: true,
name: true,
slug: true,
},
},
complex: {
select: {
id: true,
complexName: true,
},
},
},
},
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.court.id,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
previousStatus: booking.status,
newStatus: input.status,
changedAt: new Date(),
},
});
return mapBookingResponse(updated);
if(input.status === 'COMPLETED' || input.status ==='NOSHOW') {
await db.courtBooking.update({
where: { id: booking.id },
data: {
status: input.status,
},
});
} else {
// if the booking is cancelled we delete it to free up the slot, but we keep a log of it with the cancelled status
await db.courtBooking.delete({
where: { id: booking.id },
})
}
return mapBookingResponse({...booking, status: input.status });
}