import { extname } from "node:path"; import { Body, Controller, Get, Param, Post, Put, Req, UploadedFile, UploadedFiles, UseGuards, UseInterceptors, } from "@nestjs/common"; import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiTags, } from "@nestjs/swagger"; import { FileFieldsInterceptor, FileInterceptor } from "@nestjs/platform-express"; import { diskStorage } from "multer"; import { Request } from "express"; import { GlobalGuard } from "src/auth/guards/global.guard"; import { RolesGuard } from "src/auth/guards/role.guard"; import { Roles } from "src/decorators/roles.decorator"; import { CurrentUser } from "src/decorators/user.decorator"; import { RoleEnum } from "src/Types&Enums/role.enum"; import { AddPlateDto } from "src/profile/dto/user/AddPlateDto"; import { BlameConfessionDtoV2, CreateBlameRequestDtoV2, DescriptionDto, LocationDto, CarBodyFormDto, } from "./dto/create-request-management.dto"; import { RequestManagementService } from "./request-management.service"; /** * V2 API surface for the redesigned `BlameRequest` model. * * Versioning strategy: prefix the controller route with `v2/` so * all endpoints under this controller are automatically V2. */ @Controller("v2/blame-request-management") @ApiTags("blame-request-management (v2)") @ApiBearerAuth() @UseGuards(GlobalGuard, RolesGuard) @Roles(RoleEnum.USER) export class RequestManagementV2Controller { constructor( private readonly requestManagementService: RequestManagementService, ) { } @Post() @UseGuards(GlobalGuard) async createRequestV2( @CurrentUser() user, @Body() createRequestDto: CreateBlameRequestDtoV2, ) { return this.requestManagementService.createRequestV2( user, createRequestDto.type, ); } @Get() @Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT) async getAllBlameRequestsV2( @CurrentUser() user: any, ) { return this.requestManagementService.getAllBlameRequestsV2(user); } /** * Get one blame request by id. Allowed for the request owner (party) or the initiating field expert. */ @Get(":requestId") @ApiOperation({ summary: "Get one blame request (v2)", description: "Response is the blame document plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }) — computed for the current user so the app can route the damaged party to create-claim when the blame is COMPLETED and no v2 claim exists yet.", }) @ApiParam({ name: "requestId", description: "Blame request ID" }) @Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT) async getBlameRequestV2( @Param("requestId") requestId: string, @CurrentUser() user: any, ) { return this.requestManagementService.getBlameRequestV2(requestId, user); } @Post("/blame-confession/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: BlameConfessionDtoV2 }) @UseGuards(GlobalGuard) async blameConfessionV2( @Param("requestId") requestId: string, @Body() body: BlameConfessionDtoV2, @CurrentUser() user, ) { return this.requestManagementService.blameConfessionV2(requestId, body, user); } /** CAR_BODY only: submit accident type (car vs object). Call this when nextStep is CAR_BODY_ACCIDENT_TYPE. */ @Post("/car-body-form/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: CarBodyFormDto }) @UseGuards(GlobalGuard) async carBodyFormV2( @Param("requestId") requestId: string, @Body() body: CarBodyFormDto, @CurrentUser() user, ) { return this.requestManagementService.carBodyAccidentTypeFormV2(requestId, body, user); } @ApiBody({ schema: { type: "object", properties: { file: { type: "string", format: "binary", }, }, }, }) @ApiConsumes("multipart/form-data") @UseInterceptors( FileInterceptor("file", { limits: { fileSize: 20 * 1024 * 1024, }, storage: diskStorage({ destination: "./files/video", filename: (req, file, callback) => { const unique = Date.now(); const ex = extname(file.originalname); const filename = `${file.originalname}-${unique}${ex}`; callback(null, filename); }, }), }), ) @ApiParam({ name: "requestId" }) @Post("upload-video/:requestId") @UseGuards(GlobalGuard) async uploadVideoV2( @Param("requestId") requestId: string, @CurrentUser() user, @UploadedFile() file?: Express.Multer.File, ) { return this.requestManagementService.uploadFirstPartyVideoV2( requestId, file, user, ); } @Post("/initial-form/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: AddPlateDto }) @UseGuards(GlobalGuard) async initialFormV2( @Param("requestId") requestId: string, @Body() body: AddPlateDto, @CurrentUser() user, ) { return this.requestManagementService.initialFormV2(requestId, body, user); } @Post("/add-detail-location/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: LocationDto }) @UseGuards(GlobalGuard) async addLocationV2( @Param("requestId") requestId: string, @Body() body: LocationDto, @CurrentUser() user, ) { return this.requestManagementService.addDetailLocationV2(requestId, body, user); } @ApiBody({ schema: { type: "object", properties: { file: { type: "string", format: "binary", }, }, }, }) @ApiConsumes("multipart/form-data") @UseInterceptors( FileInterceptor("file", { limits: { fileSize: 10 * 1024 * 1024, }, storage: diskStorage({ destination: "./files/voice", filename: (req, file, callback) => { const unique = Date.now(); const ex = extname(file.originalname); const flname = file.originalname.split(".")[0]; const filename = `${flname}-${unique}${ex}`; callback(null, filename); }, }), }), ) @ApiParam({ name: "requestId" }) @Post("upload-voice/:requestId") @UseGuards(GlobalGuard) async uploadVoiceV2( @Param("requestId") requestId: string, @CurrentUser() user, @UploadedFile() voice?: Express.Multer.File, ) { return this.requestManagementService.uploadVoiceV2(requestId, voice, user); } @Post("/add-detail-description/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ description: "THIRD_PARTY: send only desc. CAR_BODY: send desc + accidentDate, accidentTime, weatherCondition, roadCondition, lightCondition (all required).", type: DescriptionDto, examples: { thirdParty: { summary: "THIRD_PARTY (only desc)", value: { desc: "Accident description text" }, }, carBody: { summary: "CAR_BODY (desc + conditions)", value: { desc: "Accident description", accidentDate: "2025-12-08", accidentTime: "14:30", weatherCondition: "صاف", roadCondition: "خشک", lightCondition: "روز", }, }, }, }) @UseGuards(GlobalGuard) async addDescriptionV2( @Param("requestId") requestId: string, @Body() body: DescriptionDto, @CurrentUser() user, ) { return this.requestManagementService.addDescriptionV2(requestId, body, user); } @Post("add-second-party/:phoneNumber/:requestId/:frontendRoute") @ApiParam({ name: "phoneNumber" }) @ApiParam({ name: "requestId" }) @ApiParam({ name: "frontendRoute" }) @UseGuards(GlobalGuard) async addSecondPartyV2( @Param("phoneNumber") phoneNumber: string, @Param("requestId") requestId: string, @Param("frontendRoute") frontendRoute: string, @CurrentUser() user, ) { return this.requestManagementService.addSecondPartyV2( requestId, phoneNumber, frontendRoute, user, ); } /** * V2: Get list of documents/items the current user needs to resend */ @Get("resend-requirements/:requestId") @ApiParam({ name: "requestId", description: "Blame request ID" }) async getResendRequirements( @Param("requestId") requestId: string, @CurrentUser() user: any, ) { return this.requestManagementService.getResendRequirementsV2( requestId, user.sub, ); } /** * V2: User signs/accepts or rejects expert decision */ @Put("sign/:requestId") @ApiParam({ name: "requestId", description: "Blame request ID" }) @ApiConsumes("multipart/form-data") @ApiBody({ description: "Upload signature file and acceptance decision", schema: { type: "object", required: ["sign", "isAccept"], properties: { sign: { type: "string", format: "binary", description: "Signature image file", }, isAccept: { type: "boolean", description: "true to accept expert decision, false to reject", }, }, }, }) @UseInterceptors( FileInterceptor("sign", { limits: { fileSize: 10 * 1024 * 1024, // 10MB }, storage: diskStorage({ destination: "./files/signs", filename: (req, file, callback) => { const unique = Date.now(); const ext = extname(file.originalname); const filename = `${file.originalname.split(/[.,\s-]/)[0]}-${unique}${ext}`; callback(null, filename); }, }), }), ) async userSignDecision( @Param("requestId") requestId: string, @Body("isAccept") isAccept: string | boolean, @CurrentUser() user: any, @UploadedFile() sign: Express.Multer.File, ) { // Convert string "true"/"false" to boolean const acceptDecision = typeof isAccept === "string" ? isAccept === "true" : Boolean(isAccept); return this.requestManagementService.userSignDecisionV2( requestId, user.sub, acceptDecision, sign, ); } /** * V2: Upload requested documents/evidence for resend * Field names must match ResendItemType enum values (e.g., drivingLicense, carCertificate, voice, video) */ @Put("resend/:requestId") @ApiParam({ name: "requestId", description: "Blame request ID" }) @ApiConsumes("multipart/form-data") @ApiBody({ description: "Upload files with field names matching requested items (e.g., drivingLicense, carCertificate, voice, video)", schema: { type: "object", properties: { drivingLicense: { type: "string", format: "binary", description: "Driving license file (if requested)", }, carCertificate: { type: "string", format: "binary", description: "Car certificate file (if requested)", }, nationalCertificate: { type: "string", format: "binary", description: "National certificate file (if requested)", }, carGreenCard: { type: "string", format: "binary", description: "Car green card file (if requested)", }, voice: { type: "string", format: "binary", description: "Voice recording file (if requested)", }, video: { type: "string", format: "binary", description: "Video file (if requested)", }, }, }, }) @UseInterceptors( FileFieldsInterceptor( [ { name: "drivingLicense", maxCount: 1 }, { name: "carCertificate", maxCount: 1 }, { name: "nationalCertificate", maxCount: 1 }, { name: "carGreenCard", maxCount: 1 }, { name: "voice", maxCount: 1 }, { name: "video", maxCount: 1 }, ], { storage: diskStorage({ destination: (req, file, cb) => { cb(null, "./uploads"); }, filename: (req, file, cb) => { const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9); const ext = extname(file.originalname); cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`); }, }), }, ), ) async uploadResendDocuments( @Param("requestId") requestId: string, @CurrentUser() user: any, @UploadedFiles() files: { drivingLicense?: Express.Multer.File[]; carCertificate?: Express.Multer.File[]; nationalCertificate?: Express.Multer.File[]; carGreenCard?: Express.Multer.File[]; voice?: Express.Multer.File[]; video?: Express.Multer.File[]; }, ) { return this.requestManagementService.uploadResendDocumentsV2( requestId, user.sub, files, ); } }