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; slotStepMinutes: number;
windows: AvailabilityWindow[]; windows: AvailabilityWindow[];
busy: BusyInterval[]; busy: BusyInterval[];
concurrentCapacity?: number;
now?: Date; now?: Date;
}): string[] { }): string[] {
const { const {
@@ -163,9 +164,12 @@ export function generateAvailableSlotStarts(params: {
slotStepMinutes, slotStepMinutes,
windows, windows,
busy, busy,
concurrentCapacity = 1,
now = new Date(), now = new Date(),
} = params; } = params;
const capacity = Math.max(1, concurrentCapacity);
const dayAnchor = tehranDateTime(dateYmd, 12, 0); const dayAnchor = tehranDateTime(dateYmd, 12, 0);
const dow = tehranDayOfWeek(dayAnchor); const dow = tehranDayOfWeek(dayAnchor);
const dayWindows = windows.filter((w) => w.dayOfWeek === dow); const dayWindows = windows.filter((w) => w.dayOfWeek === dow);
@@ -205,10 +209,10 @@ export function generateAvailableSlotStarts(params: {
continue; continue;
} }
const blocked = busy.some((b) => const overlapCount = busy.filter((b) =>
intervalsOverlap(startMs, endMs, b.startMs, b.endMs), intervalsOverlap(startMs, endMs, b.startMs, b.endMs),
); ).length;
if (blocked) { if (overlapCount >= capacity) {
continue; 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 }) @Column({ default: false })
serviceClassesEnabled!: boolean; serviceClassesEnabled!: boolean;
/** Max simultaneous bookings per overlapping time window */
@Column({ type: 'int', default: 1 })
concurrentCapacity!: number;
@ManyToOne(() => BusinessCategoryEntity, (category) => category.businesses, { @ManyToOne(() => BusinessCategoryEntity, (category) => category.businesses, {
onDelete: 'RESTRICT', onDelete: 'RESTRICT',
}) })
+13
View File
@@ -388,6 +388,7 @@ export class BookingsService {
const windows = await this.loadAvailabilityWindows(businessId); const windows = await this.loadAvailabilityWindows(businessId);
const busy = await this.loadBusyIntervals(businessId, dateYmd); const busy = await this.loadBusyIntervals(businessId, dateYmd);
const concurrentCapacity = await this.getConcurrentCapacity(businessId);
const slots = generateAvailableSlotStarts({ const slots = generateAvailableSlotStarts({
dateYmd, dateYmd,
@@ -396,11 +397,21 @@ export class BookingsService {
slotStepMinutes: 15, slotStepMinutes: 15,
windows, windows,
busy, busy,
concurrentCapacity,
}); });
return { date: dateYmd, slots, totalDurationMinutes }; 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( private async loadAvailabilityWindows(
businessId: string, businessId: string,
): Promise<AvailabilityWindow[]> { ): Promise<AvailabilityWindow[]> {
@@ -647,6 +658,7 @@ export class BookingsService {
); );
} }
const concurrentCapacity = await this.getConcurrentCapacity(businessId);
const slots = generateAvailableSlotStarts({ const slots = generateAvailableSlotStarts({
dateYmd, dateYmd,
durationMinutes, durationMinutes,
@@ -657,6 +669,7 @@ export class BookingsService {
excludeBookingId, excludeBookingId,
excludeBookingGroupId, excludeBookingGroupId,
}), }),
concurrentCapacity,
}); });
const scheduledMs = scheduledAt.getTime(); const scheduledMs = scheduledAt.getTime();
@@ -1,13 +1,16 @@
import { ApiPropertyOptional } from '@nestjs/swagger'; import { ApiPropertyOptional } from '@nestjs/swagger';
import { import {
IsBoolean, IsBoolean,
IsInt,
IsLatitude, IsLatitude,
IsLongitude, IsLongitude,
IsOptional, IsOptional,
IsString, IsString,
IsUUID, IsUUID,
Matches, Matches,
Max,
MaxLength, MaxLength,
Min,
} from 'class-validator'; } from 'class-validator';
export class UpdateBusinessDto { export class UpdateBusinessDto {
@@ -76,4 +79,16 @@ export class UpdateBusinessDto {
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
serviceClassesEnabled?: boolean; 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;
} }