From 0d90703827aa18e587b7cf7c38d063bd43b98d73 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Mon, 1 Jun 2026 13:26:25 +0330 Subject: [PATCH] update concurrentCapacity --- src/common/utils/booking-schedule.util.ts | 10 +++++++--- ...046300000-AddBusinessConcurrentCapacity.ts | 20 +++++++++++++++++++ src/entities/business.entity.ts | 4 ++++ src/modules/bookings/bookings.service.ts | 13 ++++++++++++ .../businesses/dto/update-business.dto.ts | 15 ++++++++++++++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 src/database/migrations/1789046300000-AddBusinessConcurrentCapacity.ts diff --git a/src/common/utils/booking-schedule.util.ts b/src/common/utils/booking-schedule.util.ts index 928069f..6d65ed8 100644 --- a/src/common/utils/booking-schedule.util.ts +++ b/src/common/utils/booking-schedule.util.ts @@ -154,6 +154,7 @@ export function generateAvailableSlotStarts(params: { slotStepMinutes: number; windows: AvailabilityWindow[]; busy: BusyInterval[]; + concurrentCapacity?: number; now?: Date; }): string[] { const { @@ -163,9 +164,12 @@ export function generateAvailableSlotStarts(params: { slotStepMinutes, windows, busy, + concurrentCapacity = 1, now = new Date(), } = params; + const capacity = Math.max(1, concurrentCapacity); + const dayAnchor = tehranDateTime(dateYmd, 12, 0); const dow = tehranDayOfWeek(dayAnchor); const dayWindows = windows.filter((w) => w.dayOfWeek === dow); @@ -205,10 +209,10 @@ export function generateAvailableSlotStarts(params: { continue; } - const blocked = busy.some((b) => + const overlapCount = busy.filter((b) => intervalsOverlap(startMs, endMs, b.startMs, b.endMs), - ); - if (blocked) { + ).length; + if (overlapCount >= capacity) { continue; } diff --git a/src/database/migrations/1789046300000-AddBusinessConcurrentCapacity.ts b/src/database/migrations/1789046300000-AddBusinessConcurrentCapacity.ts new file mode 100644 index 0000000..5980be7 --- /dev/null +++ b/src/database/migrations/1789046300000-AddBusinessConcurrentCapacity.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddBusinessConcurrentCapacity1789046300000 + implements MigrationInterface +{ + name = 'AddBusinessConcurrentCapacity1789046300000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "businesses" + ADD COLUMN IF NOT EXISTS "concurrentCapacity" integer NOT NULL DEFAULT 1 + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "businesses" DROP COLUMN IF EXISTS "concurrentCapacity" + `); + } +} diff --git a/src/entities/business.entity.ts b/src/entities/business.entity.ts index 2cbfca6..1d12f39 100644 --- a/src/entities/business.entity.ts +++ b/src/entities/business.entity.ts @@ -49,6 +49,10 @@ export class BusinessEntity { @Column({ default: false }) serviceClassesEnabled!: boolean; + /** Max simultaneous bookings per overlapping time window */ + @Column({ type: 'int', default: 1 }) + concurrentCapacity!: number; + @ManyToOne(() => BusinessCategoryEntity, (category) => category.businesses, { onDelete: 'RESTRICT', }) diff --git a/src/modules/bookings/bookings.service.ts b/src/modules/bookings/bookings.service.ts index 974d80a..d228b55 100644 --- a/src/modules/bookings/bookings.service.ts +++ b/src/modules/bookings/bookings.service.ts @@ -388,6 +388,7 @@ export class BookingsService { const windows = await this.loadAvailabilityWindows(businessId); const busy = await this.loadBusyIntervals(businessId, dateYmd); + const concurrentCapacity = await this.getConcurrentCapacity(businessId); const slots = generateAvailableSlotStarts({ dateYmd, @@ -396,11 +397,21 @@ export class BookingsService { slotStepMinutes: 15, windows, busy, + concurrentCapacity, }); return { date: dateYmd, slots, totalDurationMinutes }; } + private async getConcurrentCapacity(businessId: string): Promise { + const business = await this.businessRepo.findOne({ + where: { id: businessId }, + select: ['id', 'concurrentCapacity'], + }); + const value = business?.concurrentCapacity ?? 1; + return Math.max(1, value); + } + private async loadAvailabilityWindows( businessId: string, ): Promise { @@ -647,6 +658,7 @@ export class BookingsService { ); } + const concurrentCapacity = await this.getConcurrentCapacity(businessId); const slots = generateAvailableSlotStarts({ dateYmd, durationMinutes, @@ -657,6 +669,7 @@ export class BookingsService { excludeBookingId, excludeBookingGroupId, }), + concurrentCapacity, }); const scheduledMs = scheduledAt.getTime(); diff --git a/src/modules/businesses/dto/update-business.dto.ts b/src/modules/businesses/dto/update-business.dto.ts index 436d698..6c3f59f 100644 --- a/src/modules/businesses/dto/update-business.dto.ts +++ b/src/modules/businesses/dto/update-business.dto.ts @@ -1,13 +1,16 @@ import { ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, + IsInt, IsLatitude, IsLongitude, IsOptional, IsString, IsUUID, Matches, + Max, MaxLength, + Min, } from 'class-validator'; export class UpdateBusinessDto { @@ -76,4 +79,16 @@ export class UpdateBusinessDto { @IsOptional() @IsBoolean() serviceClassesEnabled?: boolean; + + @ApiPropertyOptional({ + example: 1, + description: 'How many customers can be served at the same time', + minimum: 1, + maximum: 20, + }) + @IsOptional() + @IsInt() + @Min(1) + @Max(20) + concurrentCapacity?: number; }