add booking status workflow

This commit is contained in:
2026-05-12 16:48:23 +03:30
parent 460537090b
commit 537b5713ca
2 changed files with 133 additions and 1 deletions
@@ -67,6 +67,36 @@ export class BookingsController {
return this.service.update(id, payload, user);
}
@Post(':id/confirm')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
confirm(
@Param('id') id: string,
@CurrentUser() user: AuthUser,
): Promise<BookingEntity> {
return this.service.confirm(id, user);
}
@Post(':id/complete')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER)
complete(
@Param('id') id: string,
@CurrentUser() user: AuthUser,
): Promise<BookingEntity> {
return this.service.complete(id, user);
}
@Post(':id/cancel')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER, UserRole.CUSTOMER)
cancel(
@Param('id') id: string,
@CurrentUser() user: AuthUser,
): Promise<BookingEntity> {
return this.service.cancel(id, user);
}
@Delete(':id')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
+103 -1
View File
@@ -152,6 +152,7 @@ export class BookingsService {
): Promise<BookingEntity> {
const row = await this.findOne(id);
this.assertCanManageBooking(row, actor);
this.assertBookingIsMutable(row);
if (dto.scheduledAt !== undefined) {
const scheduledAt = this.parseScheduledAt(dto.scheduledAt);
@@ -160,7 +161,7 @@ export class BookingsService {
}
if (dto.status !== undefined) {
row.status = dto.status;
this.applyStatusTransition(row, dto.status, actor);
}
if (dto.note !== undefined) {
@@ -170,6 +171,27 @@ export class BookingsService {
return this.repo.save(row);
}
async confirm(id: string, actor?: AuthUser): Promise<BookingEntity> {
const row = await this.findOne(id);
this.assertCanManageBooking(row, actor);
this.applyStatusTransition(row, BookingStatus.CONFIRMED, actor);
return this.repo.save(row);
}
async complete(id: string, actor?: AuthUser): Promise<BookingEntity> {
const row = await this.findOne(id);
this.assertCanManageBooking(row, actor);
this.applyStatusTransition(row, BookingStatus.COMPLETED, actor);
return this.repo.save(row);
}
async cancel(id: string, actor?: AuthUser): Promise<BookingEntity> {
const row = await this.findOne(id);
this.assertCanCancelBooking(row, actor);
this.applyStatusTransition(row, BookingStatus.CANCELLED, actor);
return this.repo.save(row);
}
async remove(id: string): Promise<void> {
const result = await this.repo.delete(id);
if (!result.affected) {
@@ -195,6 +217,67 @@ export class BookingsService {
}
}
private assertBookingIsMutable(row: BookingEntity): void {
if (
row.status === BookingStatus.CANCELLED ||
row.status === BookingStatus.COMPLETED
) {
throw new BadRequestException(
'Cancelled or completed bookings cannot be edited',
);
}
}
private applyStatusTransition(
row: BookingEntity,
nextStatus: BookingStatus,
actor?: AuthUser,
): void {
if (row.status === nextStatus) {
return;
}
// همه قوانین تغییر وضعیت رزرو در همین متد نگه داشته می‌شود تا مسیرهای مختلف
// (PATCH، confirm، complete، cancel) رفتار یکسان داشته باشند.
if (
row.status === BookingStatus.CANCELLED ||
row.status === BookingStatus.COMPLETED
) {
throw new BadRequestException(
'Cancelled or completed bookings cannot change status',
);
}
if (nextStatus === BookingStatus.CANCELLED) {
// مشتری فقط رزرو خودش را لغو می‌کند؛ صاحب کسب‌وکار فقط رزرو کسب‌وکار خودش را.
this.assertCanCancelBooking(row, actor);
row.status = BookingStatus.CANCELLED;
return;
}
if (actor?.role === UserRole.CUSTOMER) {
throw new ForbiddenException('Customers can only cancel bookings');
}
if (nextStatus === BookingStatus.CONFIRMED) {
if (row.status !== BookingStatus.PENDING) {
throw new BadRequestException('Only pending bookings can be confirmed');
}
row.status = BookingStatus.CONFIRMED;
return;
}
if (nextStatus === BookingStatus.COMPLETED) {
if (row.status !== BookingStatus.CONFIRMED) {
throw new BadRequestException('Only confirmed bookings can be completed');
}
row.status = BookingStatus.COMPLETED;
return;
}
throw new BadRequestException('Unsupported booking status transition');
}
private assertCanViewBooking(row: BookingEntity, actor?: AuthUser): void {
if (!actor || actor.role === UserRole.ADMIN) {
return;
@@ -214,6 +297,25 @@ export class BookingsService {
throw new ForbiddenException('You cannot access this booking');
}
private assertCanCancelBooking(row: BookingEntity, actor?: AuthUser): void {
if (!actor || actor.role === UserRole.ADMIN) {
return;
}
if (actor.role === UserRole.CUSTOMER && row.customerId === actor.id) {
return;
}
if (
actor.role === UserRole.BUSINESS_OWNER &&
row.business?.ownerId === actor.id
) {
return;
}
throw new ForbiddenException('You cannot cancel this booking');
}
private assertCanManageBooking(row: BookingEntity, actor?: AuthUser): void {
if (!actor || actor.role === UserRole.ADMIN) {
return;