From 680e56c8112dda6b9f82352e3c3cc061e2c839e9 Mon Sep 17 00:00:00 2001 From: Noble Date: Sat, 29 Aug 2026 04:41:49 +0000 Subject: [PATCH] booking service --- backend/src/bookings/bookings.controller.ts | 12 +++ backend/src/bookings/bookings.module.ts | 2 + backend/src/bookings/bookings.service.ts | 9 +++ .../src/modules/bookings/recurring-booking.ts | 74 +++++++++++++++++++ 4 files changed, 97 insertions(+) diff --git a/backend/src/bookings/bookings.controller.ts b/backend/src/bookings/bookings.controller.ts index b12005d..b185cd6 100644 --- a/backend/src/bookings/bookings.controller.ts +++ b/backend/src/bookings/bookings.controller.ts @@ -37,6 +37,7 @@ import { ApiErrorDto } from '../common/dto/api-error.dto'; import { Booking } from './entities/booking.entity'; import { CsvExportService } from '../common/csv-export/csv-export.service'; import { ExportBookingsProvider } from './providers/export-bookings.provider'; +import { CreateRecurringBookingDto } from '../modules/bookings/recurring-booking'; @ApiTags('bookings') @ApiBearerAuth('bearer') @@ -64,6 +65,17 @@ export class BookingsController { return { message: 'Booking created successfully', data: booking }; } + @Post('recurring') + @ApiOperation({ summary: 'Create recurring bookings' }) + @ApiOkResponse({ description: 'Recurring bookings created', type: [Booking] }) + async createRecurring( + @Body() dto: CreateRecurringBookingDto, + @GetCurrentUser('id') userId: string, + ) { + const bookings = await this.bookingsService.createRecurring(dto, userId); + return { message: 'Recurring bookings created successfully', data: bookings }; + } + @Get() @ApiOperation({ summary: 'List bookings (own for users, all for admin/staff)', diff --git a/backend/src/bookings/bookings.module.ts b/backend/src/bookings/bookings.module.ts index d9731a1..62183e1 100644 --- a/backend/src/bookings/bookings.module.ts +++ b/backend/src/bookings/bookings.module.ts @@ -15,6 +15,7 @@ import { CacheInvalidationProvider } from '../common/providers/cache-invalidatio import { ExportBookingsProvider } from './providers/export-bookings.provider'; import { CsvExportService } from '../common/csv-export/csv-export.service'; import { BookingReminderScheduler } from './providers/booking-reminder.provider'; +import { RecurringBookingService } from '../modules/bookings/recurring-booking'; @Module({ imports: [TypeOrmModule.forFeature([Booking, User]), WorkspacesModule], @@ -31,6 +32,7 @@ import { BookingReminderScheduler } from './providers/booking-reminder.provider' ExportBookingsProvider, CsvExportService, BookingReminderScheduler, + RecurringBookingService, ], exports: [BookingsService], }) diff --git a/backend/src/bookings/bookings.service.ts b/backend/src/bookings/bookings.service.ts index 4ac2872..a5b6287 100644 --- a/backend/src/bookings/bookings.service.ts +++ b/backend/src/bookings/bookings.service.ts @@ -12,6 +12,10 @@ import { UserRole } from '../users/enums/userRoles.enum'; import { Booking } from './entities/booking.entity'; import { PricingService } from './pricing/pricing.service'; import { PlanType } from './enums/plan-type.enum'; +import { + CreateRecurringBookingDto, + RecurringBookingService, +} from '../modules/bookings/recurring-booking'; @Injectable() export class BookingsService { @@ -23,6 +27,7 @@ export class BookingsService { private readonly findBookingsProvider: FindBookingsProvider, private readonly pricingService: PricingService, private readonly exportBookingsProvider: ExportBookingsProvider, + private readonly recurringBookingService: RecurringBookingService, ) {} //create booking id @@ -30,6 +35,10 @@ export class BookingsService { return this.createBookingProvider.create(dto, userId); } + createRecurring(dto: CreateRecurringBookingDto, userId: string) { + return this.recurringBookingService.create(dto, userId); + } + /** * Confirm a booking, optionally reusing the caller's open transaction * (BE-12) so the Paystack webhook doesn't accidentally nest a diff --git a/backend/src/modules/bookings/recurring-booking.ts b/backend/src/modules/bookings/recurring-booking.ts index 13f29bb..0053df0 100644 --- a/backend/src/modules/bookings/recurring-booking.ts +++ b/backend/src/modules/bookings/recurring-booking.ts @@ -1,5 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsDateString, IsEnum, IsInt, IsOptional, Min } from 'class-validator'; +import { CreateBookingDto } from '../../bookings/dto/create-booking.dto'; +import { CreateBookingProvider } from '../../bookings/providers/create-booking.provider'; + export type RecurrencePattern = 'daily' | 'weekly' | 'monthly' | 'yearly'; +export class CreateRecurringBookingDto extends CreateBookingDto { + @ApiProperty({ enum: ['daily', 'weekly', 'monthly', 'yearly'] }) + @IsEnum(['daily', 'weekly', 'monthly', 'yearly']) + pattern: RecurrencePattern; + + @ApiProperty({ example: 1, minimum: 1 }) + @IsInt() + @Min(1) + interval: number; + + @ApiPropertyOptional({ example: 12, minimum: 1 }) + @IsOptional() + @IsInt() + @Min(1) + maxOccurrences?: number; + + @ApiPropertyOptional({ example: '2026-12-31' }) + @IsOptional() + @IsDateString() + recurrenceEndDate?: string; +} + export interface RecurringBooking { bookingId: string; pattern: RecurrencePattern; @@ -10,7 +38,40 @@ export interface RecurringBooking { interval: number; } +@Injectable() export class RecurringBookingService { + constructor(private readonly createBookingProvider: CreateBookingProvider) {} + + async create( + dto: CreateRecurringBookingDto, + userId: string, + ) { + const duration = this.dateDifferenceInDays(dto.startDate, dto.endDate); + const occurrences = this.generateOccurrences({ + bookingId: dto.workspaceId, + pattern: dto.pattern, + startDate: new Date(dto.startDate), + endDate: dto.recurrenceEndDate + ? new Date(dto.recurrenceEndDate) + : undefined, + maxOccurrences: dto.maxOccurrences, + interval: dto.interval, + }); + + return Promise.all( + occurrences.map((startDate) => { + const start = this.formatDate(startDate); + const end = new Date(startDate); + end.setUTCDate(end.getUTCDate() + duration); + + return this.createBookingProvider.create( + { ...dto, startDate: start, endDate: this.formatDate(end) }, + userId, + ); + }), + ); + } + generateOccurrences(config: RecurringBooking): Date[] { const occurrences: Date[] = []; let current = new Date(config.startDate); @@ -32,10 +93,23 @@ export class RecurringBookingService { case 'monthly': current.setMonth(current.getMonth() + config.interval); break; + case 'yearly': + current.setFullYear(current.getFullYear() + config.interval); + break; } count++; } return occurrences; } + + private dateDifferenceInDays(startDate: string, endDate: string): number { + return Math.round( + (Date.parse(endDate) - Date.parse(startDate)) / (24 * 60 * 60 * 1000), + ); + } + + private formatDate(date: Date): string { + return date.toISOString().slice(0, 10); + } }