add booking status workflow
This commit is contained in:
@@ -67,6 +67,36 @@ export class BookingsController {
|
|||||||
return this.service.update(id, payload, user);
|
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')
|
@Delete(':id')
|
||||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
@Roles(UserRole.ADMIN)
|
@Roles(UserRole.ADMIN)
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export class BookingsService {
|
|||||||
): Promise<BookingEntity> {
|
): Promise<BookingEntity> {
|
||||||
const row = await this.findOne(id);
|
const row = await this.findOne(id);
|
||||||
this.assertCanManageBooking(row, actor);
|
this.assertCanManageBooking(row, actor);
|
||||||
|
this.assertBookingIsMutable(row);
|
||||||
|
|
||||||
if (dto.scheduledAt !== undefined) {
|
if (dto.scheduledAt !== undefined) {
|
||||||
const scheduledAt = this.parseScheduledAt(dto.scheduledAt);
|
const scheduledAt = this.parseScheduledAt(dto.scheduledAt);
|
||||||
@@ -160,7 +161,7 @@ export class BookingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (dto.status !== undefined) {
|
if (dto.status !== undefined) {
|
||||||
row.status = dto.status;
|
this.applyStatusTransition(row, dto.status, actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dto.note !== undefined) {
|
if (dto.note !== undefined) {
|
||||||
@@ -170,6 +171,27 @@ export class BookingsService {
|
|||||||
return this.repo.save(row);
|
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> {
|
async remove(id: string): Promise<void> {
|
||||||
const result = await this.repo.delete(id);
|
const result = await this.repo.delete(id);
|
||||||
if (!result.affected) {
|
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 {
|
private assertCanViewBooking(row: BookingEntity, actor?: AuthUser): void {
|
||||||
if (!actor || actor.role === UserRole.ADMIN) {
|
if (!actor || actor.role === UserRole.ADMIN) {
|
||||||
return;
|
return;
|
||||||
@@ -214,6 +297,25 @@ export class BookingsService {
|
|||||||
throw new ForbiddenException('You cannot access this booking');
|
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 {
|
private assertCanManageBooking(row: BookingEntity, actor?: AuthUser): void {
|
||||||
if (!actor || actor.role === UserRole.ADMIN) {
|
if (!actor || actor.role === UserRole.ADMIN) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user