import { extname } from "node:path"; import { BadRequestException, Body, Controller, Get, HttpException, InternalServerErrorException, Param, Patch, Post, Put, UploadedFile, UseGuards, UseInterceptors, } from "@nestjs/common"; import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiTags, } from "@nestjs/swagger"; import { FileInterceptor } from "@nestjs/platform-express"; import { diskStorage } from "multer"; import { LocalActorAuthGuard } from "src/auth/guards/actor-local.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 { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto"; import { RequestManagementService } from "./request-management.service"; import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service"; import { SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto, } from "src/claim-request-management/dto/select-outer-parts-v2.dto"; import { SelectOtherPartsV2ResponseDto } from "src/claim-request-management/dto/select-other-parts-v2.dto"; import { SelectOtherPartsV3Dto } from "src/claim-request-management/dto/select-other-parts-v3.dto"; import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto, } from "src/claim-request-management/dto/upload-document-v2.dto"; import { CapturePartV2Dto, CapturePartV2ResponseDto, } from "src/claim-request-management/dto/capture-part-v2.dto"; import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto"; /** * V4 FileReviewer flow — second half of the split blame workflow. * * The FileReviewer picks up a sealed file (both signatures collected by the * FileMaker) and completes the damage-assessment portion: accident fields, * documents, part selection, photo capture, and walk-around video (final step). * * Sequence: * accident-fields * → capture-requirements (via linked claimRequestId) * → upload-document (licenses, car cards) * → select-outer-parts * → select-other-parts * → capture-part (damaged-part photos + four angles) * → upload-document (chassis / engine — capture phase) * → car-capture (walk-around video — FINAL FileReviewer step) * ↳ claim → WAITING_FOR_DAMAGE_EXPERT, blame → COMPLETED * * NOTE: upload-video is a no-op for V4 (blame is already COMPLETED by car-capture). */ @ApiTags("v4 FileReviewer — blame file review & capture") @Controller("v4/file-reviewer/blame-request-management") @ApiBearerAuth() @UseGuards(LocalActorAuthGuard, RolesGuard) @Roles(RoleEnum.FILE_REVIEWER) export class FileReviewerBlameV4Controller { constructor( private readonly requestManagementService: RequestManagementService, private readonly claimRequestManagementService: ClaimRequestManagementService, private readonly mediaPolicyService: MediaPolicyService, ) {} // ─── Linked claim lookup ────────────────────────────────────────────────────── @Get("claim-id/:requestId") @ApiParam({ name: "requestId", description: "Blame request ID" }) @ApiOperation({ summary: "Get linked claim ID", description: "Returns the claim auto-created during FileMaker's guilty-party run-inquiries. " + "Use this claim ID for all capture/document/parts endpoints below.", }) async getLinkedClaimId( @Param("requestId") requestId: string, @CurrentUser() fileReviewer: any, ) { return this.requestManagementService.getV3LinkedClaimForExpert( fileReviewer, requestId, ); } // ─── Accident fields ────────────────────────────────────────────────────────── @Post("accident-fields/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: ExpertAccidentFieldsDto }) @ApiOperation({ summary: "Accident type / fields (FileReviewer first step)", description: "Saves accident fields on the sealed file. " + "Does not move to WAITING_FOR_EXPERT — that happens after the final blame accident video.", }) async addAccidentFields( @Param("requestId") requestId: string, @Body() fields: ExpertAccidentFieldsDto, @CurrentUser() fileReviewer: any, ) { return this.requestManagementService.expertAddAccidentFieldsForBlameV3( fileReviewer, requestId, fields, ); } // ─── Capture requirements ───────────────────────────────────────────────────── @Get("capture-requirements/:claimRequestId") @ApiParam({ name: "claimRequestId" }) @ApiOperation({ summary: "Capture requirements (step-aware)", description: "Initial documents phase: pre-capture docs only (no chassis/engine/metal plate). " + "CAPTURE_PART_DAMAGES phase: damaged parts + angles via capture-part, then chassis/engine/metal plate via upload-document. " + "Use `captureSequencePhase` and `captureSequenceHint` to drive the UI.", }) async getCaptureRequirements( @Param("claimRequestId") claimRequestId: string, @CurrentUser() fileReviewer: any, ): Promise { return this.claimRequestManagementService.getCaptureRequirementsV3( claimRequestId, fileReviewer.sub, fileReviewer, ); } // ─── Document upload ────────────────────────────────────────────────────────── @Post("upload-document/:claimRequestId") @ApiParam({ name: "claimRequestId" }) @ApiConsumes("multipart/form-data") @UseInterceptors( FileInterceptor("file", { limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES }, storage: diskStorage({ destination: "./files/claim-documents", filename: (req, file, callback) => { const unique = Date.now(); const ex = extname(file.originalname); callback(null, `${file.originalname.split(".")[0]}-${unique}${ex}`); }, }), }), ) @ApiOperation({ summary: "Upload one required claim document", description: "Initial phase (UPLOAD_REQUIRED_DOCUMENTS): licenses and car cards only. " + "Capture phase (CAPTURE_PART_DAMAGES, after parts + angles): chassis, engine, metal plate only.", }) async uploadDocument( @Param("claimRequestId") claimRequestId: string, @Body() body: UploadRequiredDocumentV2Dto, @UploadedFile() file: Express.Multer.File, @CurrentUser() fileReviewer: any, ): Promise { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image"); return this.claimRequestManagementService.uploadRequiredDocumentV3( claimRequestId, body, file, fileReviewer.sub, fileReviewer, ); } // ─── Part selection ─────────────────────────────────────────────────────────── @Patch("select-outer-parts/:claimRequestId") @ApiParam({ name: "claimRequestId" }) @ApiBody({ type: SelectOuterPartsV2Dto }) @ApiOperation({ summary: "Select damaged outer parts" }) async selectOuterParts( @Param("claimRequestId") claimRequestId: string, @Body() body: SelectOuterPartsV2Dto, @CurrentUser() fileReviewer: any, ): Promise { return this.claimRequestManagementService.selectOuterPartsV3( claimRequestId, body, fileReviewer.sub, fileReviewer, ); } @Patch("select-other-parts/:claimRequestId") @ApiParam({ name: "claimRequestId", example: "507f1f77bcf86cd799439011" }) @ApiOperation({ summary: "Select other damaged parts (V4)", description: "Other parts only — sheba and national code were collected during FileMaker's run-inquiries. Optional car green card file.", }) @ApiBody({ description: "Other parts selection. Bank info is already on the claim from run-inquiries.", schema: { type: "object", properties: { otherParts: { oneOf: [ { type: "array", items: { type: "string", enum: [ "engine", "suspension", "brake_system", "electrical", "radiator", "transmission", "exhaust", "headlight", "taillight", "mirror", "glass", ], }, }, { type: "string", description: "JSON string array for multipart" }, ], example: ["engine", "suspension"], }, }, }, }) async selectOtherParts( @Param("claimRequestId") claimRequestId: string, @Body() body: SelectOtherPartsV3Dto, @CurrentUser() fileReviewer: any, ): Promise { return this.claimRequestManagementService.selectOtherPartsV3( claimRequestId, body, fileReviewer.sub, fileReviewer, ); } // ─── Photo capture ──────────────────────────────────────────────────────────── @Post("capture-part/:claimRequestId") @ApiParam({ name: "claimRequestId", example: "507f1f77bcf86cd799439011" }) @ApiConsumes("multipart/form-data") @UseInterceptors( FileInterceptor("file", { limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES }, storage: diskStorage({ destination: "./files/claim-captures", filename: (req, file, callback) => { const unique = Date.now(); const ex = extname(file.originalname); callback(null, `v4-capture-${unique}${ex}`); }, }), }), ) @ApiOperation({ summary: "Car angles and damaged-part photos", description: "CAPTURE_PART_DAMAGES: (1) all damaged-part photos, (2) four car angles, " + "(3) chassis/engine/metal-plate via upload-document. Then car-capture walk-around video.", }) @ApiBody({ schema: { type: "object", required: ["captureType", "captureKey", "file"], properties: { captureType: { type: "string", enum: ["angle", "part"], example: "angle", }, captureKey: { type: "string", example: "front", description: "For angle: front/back/left/right. For part: hood/front_bumper/etc.", }, file: { type: "string", format: "binary" }, }, }, }) async capturePart( @Param("claimRequestId") claimRequestId: string, @Body() body: CapturePartV2Dto, @UploadedFile() file: Express.Multer.File, @CurrentUser() fileReviewer: any, ): Promise { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image"); return this.claimRequestManagementService.capturePartV3( claimRequestId, body, file, fileReviewer.sub, fileReviewer, ); } // ─── Owner signature on expert pricing ─────────────────────────────────────── @Put("claim-sign/:claimRequestId") @ApiParam({ name: "claimRequestId" }) @ApiConsumes("multipart/form-data") @ApiBody({ description: "Signature file, agreement, and branch", schema: { type: "object", required: ["sign", "agree", "branchId"], properties: { sign: { type: "string", format: "binary", description: "Signature image" }, agree: { type: "boolean", description: "true to accept, false to reject" }, branchId: { type: "string", description: "Insurer branch ID" }, }, }, }) @ApiOperation({ summary: "Owner signature on expert pricing (V4 — FileReviewer acts on behalf of user)", description: "FileReviewer submits the damaged party's signature during the final approval stage. " + "Delegates to the same service method as the user sign endpoint; the FileReviewer's " + "identity is resolved to the claim owner via `resolveClaimEffectiveUserId`.", }) @UseInterceptors( FileInterceptor("sign", { limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES }, storage: diskStorage({ destination: "./files/claim-sign", filename: (req, file, callback) => { const unique = Date.now(); const ex = extname(file.originalname); const base = file.originalname.split(/[.,\s-]/)[0] || "sign"; callback(null, `${base}-${unique}${ex}`); }, }), }), ) async submitOwnerSign( @Param("claimRequestId") claimRequestId: string, @Body("agree") agree: string | boolean, @Body("branchId") branchId: string, @CurrentUser() fileReviewer: any, @UploadedFile() sign: Express.Multer.File, ) { await this.mediaPolicyService.assertForClaim(sign, claimRequestId, "image"); const agreed = typeof agree === "string" ? agree === "true" || agree === "1" : Boolean(agree); try { return await this.claimRequestManagementService.submitOwnerInsurerApprovalSignV2( claimRequestId, agreed, typeof branchId === "string" ? branchId : "", sign, fileReviewer.sub, fileReviewer, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to submit signature", ); } } // ─── Walk-around video ──────────────────────────────────────────────────────── @Patch("car-capture/:claimRequestId") @ApiParam({ name: "claimRequestId", example: "507f1f77bcf86cd799439011" }) @ApiConsumes("multipart/form-data") @UseInterceptors( FileInterceptor("file", { limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES }, storage: diskStorage({ destination: "./files/car-capture-videos/", filename: (req, file, callback) => { const unique = Date.now(); const ex = extname(file.originalname); callback(null, `v4-claim-video-${unique}${ex}`); }, }), }), ) @ApiBody({ schema: { type: "object", required: ["file"], properties: { file: { type: "string", format: "binary" } }, }, }) @ApiOperation({ summary: "Walk-around video (final FileReviewer step — V4)", description: "Last FileReviewer action. Upload after capture-part is complete (parts, angles, chassis/engine docs). " + "Finalises the claim → WAITING_FOR_DAMAGE_EXPERT and marks the blame as COMPLETED.", }) async carCapture( @Param("claimRequestId") claimRequestId: string, @UploadedFile("file") file: Express.Multer.File, @CurrentUser() fileReviewer: any, ) { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "video"); return this.claimRequestManagementService.setVideoCaptureV3( claimRequestId, file, fileReviewer.sub, fileReviewer, ); } // ─── Final blame video ──────────────────────────────────────────────────────── @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); callback(null, `v4-blame-accident-${unique}${ex}`); }, }), }), ) @Post("upload-video/:requestId") @ApiParam({ name: "requestId" }) @ApiOperation({ summary: "Blame accident video (no-op for V4 — car-capture is the final step)", description: "Deprecated for V4. The blame is already COMPLETED by car-capture. " + "Calling this endpoint returns an idempotent success response.", }) async uploadBlameVideo( @Param("requestId") requestId: string, @CurrentUser() fileReviewer: any, @UploadedFile() file: Express.Multer.File, ) { if (file) { await this.mediaPolicyService.assertForBlame(file, requestId, "video"); } return this.requestManagementService.expertUploadBlameVideoV3( fileReviewer, requestId, file, ); } }