update concurrentCapacity

This commit is contained in:
2026-06-01 13:26:25 +03:30
parent 150b189be5
commit 0d90703827
5 changed files with 59 additions and 3 deletions
+7 -3
View File
@@ -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;
}
@@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddBusinessConcurrentCapacity1789046300000
implements MigrationInterface
{
name = 'AddBusinessConcurrentCapacity1789046300000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "businesses"
ADD COLUMN IF NOT EXISTS "concurrentCapacity" integer NOT NULL DEFAULT 1
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "businesses" DROP COLUMN IF EXISTS "concurrentCapacity"
`);
}
}
+4
View File
@@ -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',
})
+13
View File
@@ -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<number> {
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<AvailabilityWindow[]> {
@@ -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();
@@ -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;
}