add booking status workflow
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user