import { extname } from "node:path"; import { mkdirSync } from "node:fs"; import { Body, Controller, Get, Param, Post, Put, Query, 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 { MediaPolicyService } from "src/media-policy/media-policy.service"; import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service"; 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"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { GetUserBlameListV2ResponseDto } from "./dto/blame-list-user-v2.dto"; /** * 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, private readonly mediaPolicyService: MediaPolicyService, ) { } @Post() @UseGuards(GlobalGuard) async createRequestV2( @CurrentUser() user, @Body() createRequestDto: CreateBlameRequestDtoV2, ) { return this.requestManagementService.createRequestV2( user, createRequestDto.type, ); } @Get() @Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT) @ApiOperation({ summary: "List my blame requests (V2)", description: "All blame files for the current user. Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`. Without `page`/`limit`, returns the full filtered list.", }) async getAllBlameRequestsV2( @CurrentUser() user: any, @Query() query: ListQueryV2Dto, ): Promise { return this.requestManagementService.getAllBlameRequestsV2(user, query); } /** * 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: "Returns a minimal, user-safe payload: requestNo, publicId, type, status, blameStatus, workflow, parties (PII stripped), expert (with **expertName**), carBodyInsuranceDetail, plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }). History and other internal fields are omitted.", }) @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: DEFAULT_MEDIA_MAX_BYTES, }, 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, ) { await this.mediaPolicyService.assertForBlame(file, requestId, "video"); 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: DEFAULT_MEDIA_MAX_BYTES, }, 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, ) { await this.mediaPolicyService.assertForBlame(voice, requestId, "voice"); 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: DEFAULT_MEDIA_MAX_BYTES, }, 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, ) { await this.mediaPolicyService.assertForBlame(sign, requestId, "image"); // 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)", }, description: { type: "string", description: "Text description when expert requested ResendItemType.description", }, textDescription: { type: "string", description: "Alias for `description` (same semantics); either field may be used.", }, }, }, }) @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) => { const uploadDir = "./files/blame-resend"; mkdirSync(uploadDir, { recursive: true }); cb(null, uploadDir); }, 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[]; }, @Body("description") description?: string, @Body("textDescription") textDescription?: string, ) { // Each multipart field maps to a media kind so the policy gate can pick // the right per-client bounds. Document images = `image`, audio = `voice`, // video = `video`. await this.mediaPolicyService.assertForBlameMany(files, requestId, { drivingLicense: "image", carCertificate: "image", nationalCertificate: "image", carGreenCard: "image", voice: "voice", video: "video", }); const descCandidates = [description, textDescription] .map((x) => (x != null ? String(x).trim() : "")) .filter((s) => s.length > 0); const mergedDescription = descCandidates.length > 0 ? descCandidates[0] : undefined; return this.requestManagementService.uploadResendDocumentsV2( requestId, user.sub, files, mergedDescription, ); } }