import { readFile } from "node:fs/promises"; import { extname } from "node:path"; import { Body, Controller, Get, Param, Patch, Post, Query, UploadedFile, UseGuards, UseInterceptors, } from "@nestjs/common"; import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiResponse, 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 { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog"; import { ClaimRequestManagementService } from "./claim-request-management.service"; import { OuterPartCatalogItemDto, SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto, } from "./dto/select-outer-parts-v2.dto"; import { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto, } from "./dto/select-other-parts-v2.dto"; import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto, } from "./dto/upload-document-v2.dto"; import { CapturePartV2Dto, CapturePartV2ResponseDto, } from "./dto/capture-part-v2.dto"; import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto"; /** * Registrar claim flow that mirrors the normal user claim API * (`v2/claim-request-management`) one-to-one. The frontend reuses the same * claim pages by only swapping the route prefix: * * `v2/claim-request-management/*` -> `v2/registrar/claim-request-management/*` * * The registrar fills the claim (parts, captures, documents) on behalf of the * damaged party for a claim created from a registrar-initiated IN_PERSON blame. * After submission the file follows the exact normal review lifecycle: * the damage expert prices it, and the owner can later object/resend. * The registrar's job ends here — no reviewing panel is needed. */ @ApiTags("registrar claim (mirror v2)") @Controller("v2/registrar/claim-request-management") @ApiBearerAuth() @UseGuards(LocalActorAuthGuard, RolesGuard) @Roles(RoleEnum.REGISTRAR) export class RegistrarClaimMirrorController { constructor( private readonly claimRequestManagementService: ClaimRequestManagementService, private readonly mediaPolicyService: MediaPolicyService, ) {} @Post("create-from-blame/:blameRequestId") @ApiParam({ name: "blameRequestId" }) @ApiOperation({ summary: "[Registrar mirror] Create claim from registrar-initiated IN_PERSON blame", description: "Blame must be COMPLETED. Creates a ClaimCase owned by the damaged (non-guilty) party.", }) async createFromBlame( @Param("blameRequestId") blameRequestId: string, @CurrentUser() registrar: any, ) { return this.claimRequestManagementService.createClaimFromBlameForRegistrarV1( blameRequestId, registrar, ); } @Get("outer-parts-catalog") @ApiOperation({ summary: "Get outer parts catalog (V2)", description: "Returns outer-damage parts with id/key/side. Optional `carType` filter returns only that type catalog.", }) @ApiResponse({ status: 200, description: "Outer parts catalog", type: [OuterPartCatalogItemDto], }) async getOuterPartsCatalog(@Query("carType") carType?: ClaimVehicleTypeV2) { return this.claimRequestManagementService.getOuterPartsCatalogV2(carType); } @Get("car-other-part") @ApiOperation({ summary: "Get other (non-body) parts catalog", description: "Returns legacy other-parts catalog used by frontend. Response is parsed JSON.", }) @ApiResponse({ status: 200, description: "Other parts catalog" }) async getCarOtherParts() { const raw = await readFile( `${process.cwd()}/src/static/car-part.json`, "utf-8", ); try { return JSON.parse(raw); } catch { return raw; } } @Get("branches/:insuranceId") @ApiParam({ name: "insuranceId", description: "Insurer client id (MongoDB ObjectId)", example: "60d5ec49e7b2f8001c8e4d2a", }) @ApiOperation({ summary: "Get insurer branches (V2)", description: "Returns branch list for a given insurer/client id so frontend can render branch options.", }) @ApiResponse({ status: 200, description: "List of branches for insurer" }) async getInsuranceBranches(@Param("insuranceId") insuranceId: string) { return this.claimRequestManagementService.retrieveInsuranceBranches( insuranceId, ); } @Patch("select-outer-parts/:claimRequestId") @ApiOperation({ summary: "Select Damaged Outer Car Parts (V2 - Step 2)", description: ` **Workflow Step:** SELECT_OUTER_PARTS (Step 2 of Claim) **Purpose:** Registrar selects which outer car parts (body parts) were damaged on behalf of the damaged party. **After Success:** - Workflow moves to: SELECT_OTHER_PARTS (Step 3) `, }) @ApiParam({ name: "claimRequestId", description: "The claim case ID (MongoDB ObjectId)", example: "507f1f77bcf86cd799439011", }) @ApiBody({ type: SelectOuterPartsV2Dto, description: "Selected vehicle type + selected outer part IDs from catalog", examples: { example1: { summary: "Sedan - minor front damage", value: { carType: "sedan", selectedPartIds: [19, 21, 16] }, }, example2: { summary: "SUV - left side impact", value: { carType: "suv", selectedPartIds: [102, 103, 104, 107] }, }, example3: { summary: "Hatchback - rear-end collision", value: { carType: "hatchback", selectedPartIds: [225, 226, 210] }, }, example4: { summary: "Pickup - two sides + roof", value: { carType: "pickup", selectedPartIds: [319, 312, 330] }, }, }, }) @ApiResponse({ status: 200, description: "Outer parts selected successfully", type: SelectOuterPartsV2ResponseDto, }) @ApiResponse({ status: 400, description: "Invalid workflow step or validation failed" }) @ApiResponse({ status: 404, description: "Claim case not found" }) @ApiResponse({ status: 409, description: "Outer parts already selected" }) async selectOuterParts( @Param("claimRequestId") claimRequestId: string, @Body() body: SelectOuterPartsV2Dto, @CurrentUser() registrar: any, ): Promise { return this.claimRequestManagementService.selectOuterPartsV2( claimRequestId, body, registrar.sub, registrar, ); } @Patch("select-other-parts/:claimRequestId") @ApiOperation({ summary: "Select Other Parts & Bank Information (V2 - Step 3)", description: ` **Workflow Step:** SELECT_OTHER_PARTS (Step 3 of Claim) **Purpose:** Registrar selects non-body damaged parts and provides bank information on behalf of the damaged party. Optional: upload car green card file in the same step. **Valid Other Parts (Optional):** - engine, suspension, brake_system, electrical - radiator, transmission, exhaust - headlight, taillight, mirror, glass **After Success:** - Workflow moves to: CAPTURE_PART_DAMAGES (Step 4) `, }) @ApiParam({ name: "claimRequestId", description: "The claim case ID (MongoDB ObjectId)", example: "507f1f77bcf86cd799439011", }) @ApiConsumes("multipart/form-data") @UseInterceptors( FileInterceptor("file", { limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES }, storage: diskStorage({ destination: "./files/claim-required-document", filename: (req, file, callback) => { const unique = Date.now(); const ex = extname(file.originalname); callback(null, `other-parts-${unique}${ex}`); }, }), }), ) @ApiBody({ description: "Other parts + bank information. Use `sheba` and `nationalCodeOfInsurer`. Optional file can be uploaded as car green card.", schema: { type: "object", properties: { otherParts: { oneOf: [ { type: "array", items: { type: "string" } }, { type: "string", description: "JSON string array for multipart" }, ], example: ["engine", "suspension"], }, sheba: { type: "string", example: "IR123456789012345678901234" }, nationalCodeOfInsurer: { type: "string", example: "1234567890" }, file: { type: "string", format: "binary" }, }, required: ["sheba", "nationalCodeOfInsurer"], }, }) @ApiResponse({ status: 200, description: "Other parts and bank information saved successfully", type: SelectOtherPartsV2ResponseDto, }) @ApiResponse({ status: 400, description: "Invalid workflow step or validation failed" }) @ApiResponse({ status: 404, description: "Claim case not found" }) @ApiResponse({ status: 409, description: "Bank information already submitted" }) async selectOtherParts( @Param("claimRequestId") claimRequestId: string, @Body() body: SelectOtherPartsV2Dto, @CurrentUser() registrar: any, @UploadedFile() file?: Express.Multer.File, ): Promise { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image"); return this.claimRequestManagementService.selectOtherPartsV2( claimRequestId, body, registrar.sub, registrar, file, ); } @Get("capture-requirements/:claimRequestId") @ApiOperation({ summary: "Get Capture Requirements (V2)", description: ` **Get list of what needs to be captured:** - Required documents (10 remaining at the documents step for third-party) - Car angles (4 items: front, back, left, right) - Damaged parts (based on selected outer parts) Returns status of each item (uploaded/captured or not). `, }) @ApiParam({ name: "claimRequestId", description: "The claim case ID (MongoDB ObjectId)", example: "507f1f77bcf86cd799439011", }) @ApiResponse({ status: 200, description: "Capture requirements retrieved successfully", type: GetCaptureRequirementsV2ResponseDto, }) @ApiResponse({ status: 403, description: "Access denied" }) @ApiResponse({ status: 404, description: "Claim case not found" }) async getCaptureRequirements( @Param("claimRequestId") claimRequestId: string, @CurrentUser() registrar: any, ): Promise { return this.claimRequestManagementService.getCaptureRequirementsV2( claimRequestId, registrar.sub, registrar, ); } @Post("upload-document/:claimRequestId") @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); const filename = `${file.originalname.split(".")[0]}-${unique}${ex}`; callback(null, filename); }, }), }), ) @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Upload Required Document (V2 - Step 5)", description: ` **Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim) **Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements): - damaged_driving_license_front/back - damaged_chassis_number, damaged_engine_photo - damaged_car_card_front/back, damaged_metal_plate - guilty_driving_license_front/back, guilty_car_card_front/back, guilty_metal_plate (THIRD_PARTY) **When all required documents are uploaded:** Workflow moves to USER_SUBMISSION_COMPLETE. `, }) @ApiParam({ name: "claimRequestId", description: "The claim case ID", example: "507f1f77bcf86cd799439011", }) @ApiBody({ schema: { type: "object", required: ["documentKey", "file"], properties: { documentKey: { type: "string", enum: [ "damaged_driving_license_front", "damaged_driving_license_back", "damaged_chassis_number", "damaged_engine_photo", "damaged_car_card_front", "damaged_car_card_back", "damaged_metal_plate", "guilty_driving_license_front", "guilty_driving_license_back", "guilty_car_card_front", "guilty_car_card_back", "guilty_metal_plate", ], example: "damaged_driving_license_front", }, file: { type: "string", format: "binary" }, }, }, }) @ApiResponse({ status: 200, description: "Document uploaded successfully", type: UploadRequiredDocumentV2ResponseDto, }) @ApiResponse({ status: 400, description: "Invalid workflow step" }) @ApiResponse({ status: 409, description: "Document already uploaded" }) async uploadDocument( @Param("claimRequestId") claimRequestId: string, @Body() body: UploadRequiredDocumentV2Dto, @UploadedFile() file: Express.Multer.File, @CurrentUser() registrar: any, ): Promise { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image"); return this.claimRequestManagementService.uploadRequiredDocumentV2( claimRequestId, body, file, registrar.sub, registrar, ); } @Post("capture-part/:claimRequestId") @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); const filename = `${file.originalname.split(".")[0]}-${unique}${ex}`; callback(null, filename); }, }), }), ) @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Capture Car Angle or Damaged Part (V2 - Step 4)", description: ` **Workflow Step:** CAPTURE_PART_DAMAGES (Step 4 of Claim) **Capture types:** 1. **angle**: Car angles (front, back, left, right) - 4 required 2. **part**: Damaged parts based on selectedParts from Step 2 **When all captures are complete:** Workflow moves to UPLOAD_REQUIRED_DOCUMENTS (Step 5). `, }) @ApiParam({ name: "claimRequestId", description: "The claim case ID", example: "507f1f77bcf86cd799439011", }) @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" }, }, }, }) @ApiResponse({ status: 200, description: "Capture saved successfully", type: CapturePartV2ResponseDto, }) @ApiResponse({ status: 400, description: "Invalid workflow step" }) async capturePart( @Param("claimRequestId") claimRequestId: string, @Body() body: CapturePartV2Dto, @UploadedFile() file: Express.Multer.File, @CurrentUser() registrar: any, ): Promise { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image"); return this.claimRequestManagementService.capturePartV2( claimRequestId, body, file, registrar.sub, registrar, ); } @Patch("car-capture/:claimRequestId") @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, `claim-video-${unique}${ex}`); }, }), }), ) @ApiParam({ name: "claimRequestId", description: "The claim case ID", example: "507f1f77bcf86cd799439011", }) @ApiBody({ schema: { type: "object", required: ["file"], properties: { file: { type: "string", format: "binary" } }, }, }) @ApiOperation({ summary: "Upload Car Walk-Around Video (V2 - Step 4b)", description: "Upload a walk-around video of the damaged car during the capture phase.", }) @ApiResponse({ status: 200, description: "Video uploaded successfully" }) async carCapture( @Param("claimRequestId") claimRequestId: string, @UploadedFile("file") file: Express.Multer.File, @CurrentUser() registrar: any, ) { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "video"); return this.claimRequestManagementService.setVideoCaptureV2( claimRequestId, file, registrar.sub, registrar, ); } }