diff --git a/src/database/migrations/1789046800000-AddBookingCancellationReason.ts b/src/database/migrations/1789046800000-AddBookingCancellationReason.ts new file mode 100644 index 0000000..1a5a92b --- /dev/null +++ b/src/database/migrations/1789046800000-AddBookingCancellationReason.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddBookingCancellationReason1789046800000 + implements MigrationInterface +{ + name = 'AddBookingCancellationReason1789046800000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "bookings" + ADD COLUMN IF NOT EXISTS "cancellationReason" text + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "bookings" + DROP COLUMN IF EXISTS "cancellationReason" + `); + } +} diff --git a/src/entities/booking.entity.ts b/src/entities/booking.entity.ts index e98e837..bdec315 100644 --- a/src/entities/booking.entity.ts +++ b/src/entities/booking.entity.ts @@ -66,6 +66,9 @@ export class BookingEntity { @Column({ type: 'text', nullable: true }) note!: string | null; + @Column({ type: 'text', nullable: true }) + cancellationReason!: string | null; + @CreateDateColumn() createdAt!: Date; diff --git a/src/modules/bookings/bookings.controller.ts b/src/modules/bookings/bookings.controller.ts index dfebe3e..6902d78 100644 --- a/src/modules/bookings/bookings.controller.ts +++ b/src/modules/bookings/bookings.controller.ts @@ -20,6 +20,7 @@ import { RolesGuard } from '../auth/guards/roles.guard'; import { BookingsService } from './bookings.service'; import { CreateBookingDto } from './dto/create-booking.dto'; import { CreateBookingBatchDto } from './dto/create-booking-batch.dto'; +import { CancelBookingDto } from './dto/cancel-booking.dto'; import { UpdateBookingDto } from './dto/update-booking.dto'; @Controller('bookings') @@ -128,9 +129,10 @@ export class BookingsController { @Roles(UserRole.ADMIN, UserRole.BUSINESS_OWNER, UserRole.CUSTOMER) cancel( @Param('id') id: string, + @Body() payload: CancelBookingDto, @CurrentUser() user: AuthUser, ): Promise { - return this.service.cancel(id, user); + return this.service.cancel(id, user, payload); } @Delete(':id') diff --git a/src/modules/bookings/bookings.service.ts b/src/modules/bookings/bookings.service.ts index dcb9b3c..cf95376 100644 --- a/src/modules/bookings/bookings.service.ts +++ b/src/modules/bookings/bookings.service.ts @@ -324,9 +324,22 @@ export class BookingsService { return this.repo.save(row); } - async cancel(id: string, actor?: AuthUser): Promise { + async cancel( + id: string, + actor?: AuthUser, + payload?: { cancellationReason?: string }, + ): Promise { const row = await this.findOne(id); this.assertCanCancelBooking(row, actor); + + const reason = payload?.cancellationReason?.trim() ?? ''; + if (actor?.role === UserRole.BUSINESS_OWNER && !reason) { + throw new BadRequestException('Cancellation reason is required'); + } + if (reason) { + row.cancellationReason = reason; + } + this.applyStatusTransition(row, BookingStatus.CANCELLED, actor); return this.repo.save(row); } diff --git a/src/modules/bookings/dto/cancel-booking.dto.ts b/src/modules/bookings/dto/cancel-booking.dto.ts new file mode 100644 index 0000000..65603a3 --- /dev/null +++ b/src/modules/bookings/dto/cancel-booking.dto.ts @@ -0,0 +1,10 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsOptional, IsString, MaxLength } from 'class-validator'; + +export class CancelBookingDto { + @ApiPropertyOptional({ example: 'ظرفیت این بازه تکمیل شده است.' }) + @IsOptional() + @IsString() + @MaxLength(500) + cancellationReason?: string; +}