feat: add recurring bookings feature with related handlers and services

- Introduced recurring booking groups with the ability to create and cancel them.
- Added database migrations for recurring bookings, including new tables and relationships.
- Implemented handlers for creating and canceling recurring bookings in the admin booking module.
- Enhanced existing booking services to support recurring bookings logic.
- Updated API contract to include new schemas for recurring bookings.
- Refactored existing code for improved readability and maintainability.
This commit is contained in:
Jose Selesan
2026-06-16 11:58:06 -03:00
parent 1b5eb253f2
commit 1318e3bf57
38 changed files with 2025 additions and 265 deletions

View File

@@ -14,6 +14,7 @@ model Complex {
users ComplexUser[]
invitations ComplexInvitation[]
courts Court[]
recurringGroups RecurringBookingGroup[]
@@index([planCode])
@@index([complexSlug])

View File

@@ -15,6 +15,11 @@ enum CourtBookingStatus {
NOSHOW
}
enum RecurringBookingGroupStatus {
ACTIVE
CANCELLED
}
model Sport {
id String @id @db.Uuid
name String @unique
@@ -44,6 +49,7 @@ model Court {
priceRules CourtPriceRule[]
bookings CourtBooking[]
maintenances CourtMaintenance[]
recurringGroups RecurringBookingGroup[]
@@index([complexId])
@@index([sportId])
@@ -95,23 +101,26 @@ model CourtPriceRule {
}
model CourtBooking {
id String @id @db.Uuid
bookingCode String @unique @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)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String @map("customer_email") @db.VarChar(254)
status CourtBookingStatus @default(CONFIRMED)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
id String @id @db.Uuid
bookingCode String @unique @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)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String @map("customer_email") @db.VarChar(254)
status CourtBookingStatus @default(CONFIRMED)
recurringGroupId String? @map("recurring_group_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
recurringGroup RecurringBookingGroup? @relation(fields: [recurringGroupId], references: [id])
@@unique([courtId, bookingDate, startTime])
@@index([courtId, bookingDate])
@@index([bookingDate])
@@index([recurringGroupId])
@@map("court_bookings")
}
@@ -132,3 +141,28 @@ model CourtBookingLog {
@@map("court_booking_logs")
@@index([courtId, newStatus])
}
model RecurringBookingGroup {
id String @id @db.Uuid
complexId String @map("complex_id") @db.Uuid
courtId String @map("court_id") @db.Uuid
startTime String @map("start_time") @db.VarChar(5)
endTime String @map("end_time") @db.VarChar(5)
dayOfWeek DayOfWeek @map("day_of_week")
startDate DateTime @map("start_date") @db.Date
endDate DateTime? @map("end_date") @db.Date
status RecurringBookingGroupStatus @default(ACTIVE)
customerName String @map("customer_name") @db.VarChar(120)
customerPhone String @map("customer_phone") @db.VarChar(30)
customerEmail String @map("customer_email") @db.VarChar(254)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
court Court @relation(fields: [courtId], references: [id], onDelete: Cascade)
complex Complex @relation(fields: [complexId], references: [id], onDelete: Cascade)
bookings CourtBooking[]
@@index([complexId])
@@index([courtId])
@@index([status])
@@map("recurring_booking_groups")
}

View File

@@ -0,0 +1,46 @@
-- CreateEnum
CREATE TYPE "RecurringBookingGroupStatus" AS ENUM ('ACTIVE', 'CANCELLED');
-- AlterTable
ALTER TABLE "court_bookings" ADD COLUMN "recurring_group_id" UUID;
-- CreateTable
CREATE TABLE "recurring_booking_groups" (
"id" UUID NOT NULL,
"complex_id" UUID NOT NULL,
"court_id" UUID NOT NULL,
"start_time" VARCHAR(5) NOT NULL,
"end_time" VARCHAR(5) NOT NULL,
"day_of_week" "DayOfWeek" NOT NULL,
"start_date" DATE NOT NULL,
"end_date" DATE,
"status" "RecurringBookingGroupStatus" NOT NULL DEFAULT 'ACTIVE',
"customer_name" VARCHAR(120) NOT NULL,
"customer_phone" VARCHAR(30) NOT NULL,
"customer_email" VARCHAR(254) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "recurring_booking_groups_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "recurring_booking_groups_complex_id_idx" ON "recurring_booking_groups"("complex_id");
-- CreateIndex
CREATE INDEX "recurring_booking_groups_court_id_idx" ON "recurring_booking_groups"("court_id");
-- CreateIndex
CREATE INDEX "recurring_booking_groups_status_idx" ON "recurring_booking_groups"("status");
-- CreateIndex
CREATE INDEX "court_bookings_recurring_group_id_idx" ON "court_bookings"("recurring_group_id");
-- AddForeignKey
ALTER TABLE "court_bookings" ADD CONSTRAINT "court_bookings_recurring_group_id_fkey" FOREIGN KEY ("recurring_group_id") REFERENCES "recurring_booking_groups"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "recurring_booking_groups" ADD CONSTRAINT "recurring_booking_groups_court_id_fkey" FOREIGN KEY ("court_id") REFERENCES "courts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "recurring_booking_groups" ADD CONSTRAINT "recurring_booking_groups_complex_id_fkey" FOREIGN KEY ("complex_id") REFERENCES "complexes"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -21,6 +21,7 @@ export const planSeeds = [
publicBookingPage: true,
advancedReports: false,
whatsappReminders: false,
fixedSlots: false,
},
pricing: {
overrides: {
@@ -51,6 +52,7 @@ export const planSeeds = [
publicBookingPage: true,
advancedReports: true,
whatsappReminders: true,
fixedSlots: true,
},
pricing: {
overrides: {
@@ -81,6 +83,7 @@ export const planSeeds = [
publicBookingPage: true,
advancedReports: true,
whatsappReminders: true,
fixedSlots: true,
},
pricing: {
overrides: {