import { Controller, HttpException, InternalServerErrorException, Param, Query, Post, Patch, Put, Body, UseGuards, Get, UseInterceptors, UploadedFile, } from "@nestjs/common"; import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger"; import { FileInterceptor } from "@nestjs/platform-express"; import { diskStorage } from "multer"; import { extname } from "path"; 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 { 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 { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto"; import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-v2.dto"; import { CapturePartV2Dto, CapturePartV2ResponseDto, VideoCaptureV2ResponseDto, } from "./dto/capture-part-v2.dto"; import { GetMyClaimsV2ResponseDto } from "./dto/my-claims-v2.dto"; import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto"; import { UserObjectionV2Dto } from "./dto/user-objection-v2.dto"; import { UserRatingDto } from "./dto/user-rating.dto"; import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog"; @ApiTags("claim-request-management (v2)") @Controller("v2/claim-request-management") @ApiBearerAuth() @UseGuards(GlobalGuard, RolesGuard) @Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT) export class ClaimRequestManagementV2Controller { constructor( private readonly claimRequestManagementService: ClaimRequestManagementService, ) {} @Get("requests") @ApiOperation({ summary: "Get My Claims (V2)", description: "Get list of all claim requests for the current user.", }) @ApiResponse({ status: 200, description: "List of user claims", type: GetMyClaimsV2ResponseDto, }) async getMyClaims(@CurrentUser() user: any): Promise { try { return await this.claimRequestManagementService.getMyClaimsV2(user.sub, user); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to get claims list", ); } } @Get("request/:claimRequestId") @ApiParam({ name: "claimRequestId", description: "The claim case ID (MongoDB ObjectId)", example: "507f1f77bcf86cd799439011", }) @ApiOperation({ summary: "Get Claim Details (V2)", description: "Get claim details for current actor. USER receives only minimal own-needed payload (no extra owner/security fields). FIELD_EXPERT keeps full view for expert workflows.", }) @ApiResponse({ status: 200, description: "Claim details", type: ClaimDetailsV2ResponseDto, }) @ApiResponse({ status: 403, description: "User is not the claim owner", }) @ApiResponse({ status: 404, description: "Claim not found", }) async getClaimDetails( @Param("claimRequestId") claimRequestId: string, @CurrentUser() user: any, ): Promise { try { return await this.claimRequestManagementService.getClaimDetailsV2( claimRequestId, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to get claim details", ); } } /** * V2: Acknowledge a damage-expert resend that only contains instructions (no extra documents or part photos). */ @Post("request/:claimRequestId/expert-resend/acknowledge") @ApiOperation({ summary: "Acknowledge expert resend (instructions only)", description: "Use when `workflow.currentStep` is USER_EXPERT_RESEND and the expert did not list any `resendDocuments` or `resendCarParts`. " + "Returns the claim to WAITING_FOR_DAMAGE_EXPERT. If documents or parts were requested, upload them via the existing upload/capture endpoints instead.", }) @ApiParam({ name: "claimRequestId" }) @ApiResponse({ status: 200, description: "Claim returned to expert queue" }) @ApiResponse({ status: 400, description: "Resend requires uploads or wrong step" }) async acknowledgeExpertResend( @Param("claimRequestId") claimRequestId: string, @CurrentUser() user: any, ) { try { return await this.claimRequestManagementService.acknowledgeExpertResendInstructionsV2( claimRequestId, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to acknowledge expert resend", ); } } /** * V2: User objection after expert resend (same intent as v1 PUT …/request/resend/:id/objection). */ @Put("request/:claimRequestId/objection") @ApiOperation({ summary: "Submit user objection (V2)", description: "After the damage expert submits a resend request (`damageExpertResend`), the owner may dispute priced parts " + "and/or propose additional damaged parts. Stores a structured payload on `evaluation.objection`, merges " + "`newParts` into `damage.selectedParts`, and moves the case back to `WAITING_FOR_DAMAGE_EXPERT` for re-review.", }) @ApiParam({ name: "claimRequestId", description: "The claim case ID (MongoDB ObjectId)", example: "507f1f77bcf86cd799439011", }) @ApiBody({ type: UserObjectionV2Dto }) @ApiResponse({ status: 200, description: "Objection stored" }) @ApiResponse({ status: 400, description: "No active resend or empty payload" }) @ApiResponse({ status: 403, description: "Not the claim owner" }) @ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 409, description: "Objection already submitted" }) async submitUserObjectionV2( @Param("claimRequestId") claimRequestId: string, @Body() body: UserObjectionV2Dto, @CurrentUser() user: any, ) { try { return await this.claimRequestManagementService.handleUserObjectionV2( claimRequestId, body, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to submit objection", ); } } /** * V2: User satisfaction rating after the claim case is completed (same intent as v1 PUT …/request/:id/user-rating). */ @Put("request/:claimRequestId/user-rating") @ApiOperation({ summary: "Submit user satisfaction rating (V2)", description: "Only the claim owner (damaged party) may submit. Allowed when `status` is `COMPLETED`. " + "Stores scores on `ClaimCase.userRating` (0–5). One submission per case.", }) @ApiParam({ name: "claimRequestId", description: "The claim case ID (MongoDB ObjectId)", example: "507f1f77bcf86cd799439011", }) @ApiBody({ type: UserRatingDto }) @ApiResponse({ status: 200, description: "Rating saved" }) @ApiResponse({ status: 400, description: "Claim not completed or invalid scores" }) @ApiResponse({ status: 403, description: "Not the claim owner" }) @ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 409, description: "Rating already submitted" }) async addUserRatingV2( @Param("claimRequestId") claimRequestId: string, @Body() ratingDto: UserRatingDto, @CurrentUser() user: any, ) { try { return await this.claimRequestManagementService.addUserRatingV2( claimRequestId, ratingDto, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to save rating", ); } } @Post("create-from-blame/:blameRequestId") @ApiParam({ name: "blameRequestId", description: "ID of the completed blame case to create claim from", }) async createClaimFromBlame( @Param("blameRequestId") blameRequestId: string, @CurrentUser() user: any, ) { try { return await this.claimRequestManagementService.createClaimFromBlameV2( blameRequestId, user.sub, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to create claim request", ); } } /** * V2 API: Select damaged outer car parts (Step 2 of claim workflow) */ @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, ): Promise { return this.claimRequestManagementService.getOuterPartsCatalogV2(carType); } @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:** User selects which outer car parts (body parts) were damaged in the accident. **Validations:** - Claim must exist - User must be the claim owner (damaged party from blame case) - Current workflow step must be CLAIM_CREATED - Parts array must contain at least 1 part - No duplicate parts allowed - Parts cannot be re-selected once submitted **Valid Outer Parts:** - hood, trunk, roof - front_right_door, front_left_door, rear_right_door, rear_left_door - front_bumper, rear_bumper - front_right_fender, front_left_fender, rear_right_fender, rear_left_fender **After Success:** - Workflow moves to: SELECT_OTHER_PARTS (Step 3) - User can proceed to select non-body parts and provide bank info `, }) @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: 403, description: "User is not the claim owner", }) @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() user: any, ): Promise { try { return await this.claimRequestManagementService.selectOuterPartsV2( claimRequestId, body, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to select outer parts", ); } } /** * V2 API: Select other damaged parts and provide bank information (Step 3 of claim workflow) */ @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:** User selects non-body damaged parts and provides bank information for payment. Optional: upload car green card file in the same step. **Validations:** - Claim must exist - User must be the claim owner (damaged party from blame case) - Current workflow step must be SELECT_OTHER_PARTS - Bank information must not have been submitted previously - Sheba (sheba) accepted as IR + 24 digits or only 24 digits - National code must be exactly 10 digits **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) - User captures car angles and damaged-part photos, then uploads required documents (Step 5) `, }) @ApiParam({ name: "claimRequestId", description: "The claim case ID (MongoDB ObjectId)", example: "507f1f77bcf86cd799439011", }) @ApiConsumes("multipart/form-data") @UseInterceptors( FileInterceptor("file", { limits: { fileSize: 10 * 1024 * 1024 }, 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` like THIRD_PARTY flow. 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: 403, description: "User is not the claim owner", }) @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() user: any, @UploadedFile() file?: Express.Multer.File, ): Promise { try { return await this.claimRequestManagementService.selectOtherPartsV2( claimRequestId, body, user.sub, user, file, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to select other parts", ); } } /** * V2 API: Get capture requirements (documents, angles, parts) */ @Get("capture-requirements/:claimRequestId") @ApiOperation({ summary: "Get Capture Requirements (V2)", description: ` **Get list of what needs to be captured:** - Required documents (13 items) - Car angles (4 items: front, back, left, right) - Damaged parts (based on selected outer parts) Returns status of each item (uploaded/captured or not). **V2 order:** Complete angles and part photos first, then required documents. `, }) @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: "User is not the claim owner", }) @ApiResponse({ status: 404, description: "Claim case not found", }) async getCaptureRequirements( @Param("claimRequestId") claimRequestId: string, @CurrentUser() user: any, ): Promise { try { return await this.claimRequestManagementService.getCaptureRequirementsV2( claimRequestId, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to get capture requirements", ); } } /** * V2 API: Upload required document */ @Post("upload-document/:claimRequestId") @UseInterceptors( FileInterceptor("file", { limits: { fileSize: 10 * 1024 * 1024, // 10MB }, 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 and the claim is ready for damage expert review. **Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to upload documents on behalf of the damaged party. `, }) @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() user: any, ): Promise { try { return await this.claimRequestManagementService.uploadRequiredDocumentV2( claimRequestId, body, file, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to upload document", ); } } /** * V2 API: Capture car angle or damaged part */ @Post("capture-part/:claimRequestId") @UseInterceptors( FileInterceptor("file", { limits: { fileSize: 10 * 1024 * 1024, // 10MB }, 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). **Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to capture photos on behalf of the damaged party. `, }) @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() user: any, ): Promise { try { return await this.claimRequestManagementService.capturePartV2( claimRequestId, body, file, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to capture part", ); } } /** * V2: Upload repair factor for a part (same intent as v1 PATCH …/request/reply/:claimRequestId/:partId/upload-factor). */ @Patch("request/reply/:claimRequestId/:partId/upload-factor") @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Upload factor file for a priced part (V2)", description: "Use when the damage expert reply marks `factorNeeded: true` for this `partId`. " + "Stores file in `claim-factors-image`, sets `factorLink` / `factorStatus` on the matching part in " + "`evaluation.damageExpertReply` or `evaluation.damageExpertReplyFinal`. " + "Requires claim `claimStatus` NEEDS_REVISION or UNDER_REVIEW.", }) @ApiParam({ name: "claimRequestId", description: "Claim case ID" }) @ApiParam({ name: "partId", description: "Part id from expert reply" }) @ApiBody({ schema: { type: "object", properties: { file: { type: "string", format: "binary" }, }, }, }) @UseInterceptors( FileInterceptor("file", { storage: diskStorage({ destination: "./files/claim-factors", filename: (req, file, callback) => { const unique = Date.now(); callback(null, `-${unique}-${file.originalname}`); }, }), limits: { fileSize: 10 * 1024 * 1024 }, }), ) async uploadFactorForPartV2( @Param("claimRequestId") claimRequestId: string, @Param("partId") partId: string, @UploadedFile() file: Express.Multer.File, @CurrentUser() user: any, ) { try { return await this.claimRequestManagementService.uploadClaimFactorV2( claimRequestId, partId, file, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to upload factor", ); } } /** * V2 API: Upload car walk-around video (same behavior as v1 PATCH claim-request-management/car-capture/:id) */ @ApiBody({ schema: { type: "object", properties: { file: { type: "string", format: "binary" }, }, }, }) @UseInterceptors( FileInterceptor("file", { limits: { fileSize: 50 * 1024 * 1024 }, storage: diskStorage({ destination: "./files/car-capture-videos/", filename: (req, file, callback) => { const unique = Date.now(); const ex = extname(file.originalname); const filename = `claim-video-${unique}${ex}`; callback(null, filename); }, }), }), ) @ApiConsumes("multipart/form-data") @Patch("car-capture/:claimRequestId") @ApiOperation({ summary: "Upload car walk-around video (V2)", description: "Multipart upload of the car capture video during CAPTURE_PART_DAMAGES. " + "Stores file metadata in `claim-video-capture` and sets `ClaimCase.media.videoCaptureId`. " + "Only one video per claim; path is stored on the video document (not duplicated on the case).", }) @ApiParam({ name: "claimRequestId", description: "The claim case ID (MongoDB ObjectId)", example: "507f1f77bcf86cd799439011", }) @ApiResponse({ status: 200, description: "Video uploaded successfully", type: VideoCaptureV2ResponseDto, }) @ApiResponse({ status: 400, description: "Wrong workflow step or missing file" }) @ApiResponse({ status: 403, description: "Not the claim owner" }) @ApiResponse({ status: 404, description: "Claim not found" }) @ApiResponse({ status: 409, description: "Video already uploaded" }) async captureVideoCaptureV2( @Param("claimRequestId") claimRequestId: string, @UploadedFile("file") file: Express.Multer.File, @CurrentUser() user: any, ): Promise { try { return await this.claimRequestManagementService.setVideoCaptureV2( claimRequestId, file, user.sub, user, ); } catch (error) { if (error instanceof HttpException) throw error; throw new InternalServerErrorException( error instanceof Error ? error.message : "Failed to upload car capture video", ); } } }