import { extname } from "node:path"; import { Body, Controller, Get, 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 { CreationMethod } from "./entities/schema/request-management.schema"; import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto"; import { SendPartyOtpDto, VerifyPartyOtpDto } from "./dto/party-otp.dto"; import { RunInquiriesV3Dto } from "./dto/run-inquiries-v3.dto"; import { CarBodyFormDto, DescriptionDto, LocationDto, } from "./dto/create-request-management.dto"; 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 { PartyRole } from "./entities/schema/partyRole.enum"; 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"; /** * V3 field-expert in-person flow (one party at a time, standard workflow steps). * * THIRD_PARTY: * create → OTP/verify (guilty) → [car-body-form if CAR_BODY] → run-inquiries (guilty + claim) * → location/description/voice → sign → OTP/verify (damaged) → run-inquiries (damaged) * → location/description/voice → sign → accident-fields → docs → outer parts → other parts * → capture-part → car-capture → upload-video (blame accident video, last) → WAITING_FOR_EXPERT * * CAR_BODY: same but single party — omit second-party OTP/inquiries/sign steps. */ @ApiTags("expert-initiated blame (mirror v3 in-person)") @Controller("v3/expert-initiated/blame-request-management") @ApiBearerAuth() @UseGuards(LocalActorAuthGuard, RolesGuard) @Roles(RoleEnum.FIELD_EXPERT) export class ExpertInitiatedBlameV3MirrorController { constructor( private readonly requestManagementService: RequestManagementService, private readonly claimRequestManagementService: ClaimRequestManagementService, private readonly mediaPolicyService: MediaPolicyService, ) {} @Post() @ApiOperation({ summary: "Create IN_PERSON blame file" }) @ApiBody({ type: CreateExpertInitiatedFileDto }) async create( @CurrentUser() expert: any, @Body() dto: CreateExpertInitiatedFileDto, ) { return this.requestManagementService.createExpertInitiatedBlameV2(expert, { ...dto, creationMethod: CreationMethod.IN_PERSON, }); } @Get("claim-id/:requestId") @ApiParam({ name: "requestId", description: "Blame request ID" }) @ApiOperation({ summary: "Get linked claim ID", description: "Returns the claim auto-created during guilty-party run-inquiries. Use this claim ID for capture-requirements, upload-document, and part-selection endpoints.", }) async getLinkedClaimId( @Param("requestId") requestId: string, @CurrentUser() expert: any, ) { return this.requestManagementService.getV3LinkedClaimForExpert( expert, requestId, ); } @Post("send-party-otp/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: SendPartyOtpDto }) @ApiOperation({ summary: "Send OTP to one party", description: "Guilty party first; damaged party after guilty party has signed (THIRD_PARTY).", }) async sendPartyOtp( @CurrentUser() expert: any, @Param("requestId") requestId: string, @Body() dto: SendPartyOtpDto, ) { return this.requestManagementService.sendPartyOtpV2(expert, requestId, dto); } @Post("verify-party-otp/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: VerifyPartyOtpDto }) @ApiOperation({ summary: "Verify one party OTP", description: "FIRST (guilty) then SECOND (damaged) on THIRD_PARTY files.", }) async verifyPartyOtp( @CurrentUser() expert: any, @Param("requestId") requestId: string, @Body() dto: VerifyPartyOtpDto, ) { return this.requestManagementService.verifyPartyOtpV2(expert, requestId, dto); } @Post("car-body-form/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: CarBodyFormDto }) @ApiOperation({ summary: "CAR_BODY only — accident type before inquiries", description: "Submit after guilty-party OTP verify, before run-inquiries.", }) async carBodyForm( @Param("requestId") requestId: string, @Body() body: CarBodyFormDto, @CurrentUser() expert: any, ) { return this.requestManagementService.carBodyAccidentTypeFormV3( requestId, body, expert, ); } @Post("run-inquiries/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: RunInquiriesV3Dto }) @ApiOperation({ summary: "Run external inquiries for the current party", description: "1st call = guilty party (+ auto claim). 2nd call = damaged party (THIRD_PARTY, after guilty sign). " + "Advances workflow to FIRST_INITIAL_FORM or SECOND_INITIAL_FORM.", }) async runInquiries( @CurrentUser() expert: any, @Param("requestId") requestId: string, @Body() dto: RunInquiriesV3Dto, ) { return this.requestManagementService.runInquiriesV3(requestId, expert, dto); } @Post("add-detail-location/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: LocationDto }) @ApiOperation({ summary: "Add location for current party (partyRole FIRST or SECOND)" }) async addLocation( @Param("requestId") requestId: string, @Body() body: LocationDto, @CurrentUser() expert: any, @Body("partyRole") partyRole?: string, ) { return this.requestManagementService.addPartyLocationV3( requestId, expert, body, partyRole, ); } @Post("add-detail-description/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: DescriptionDto }) @ApiOperation({ summary: "Add description for current party" }) async addDescription( @Param("requestId") requestId: string, @Body() body: DescriptionDto, @CurrentUser() expert: any, @Body("partyRole") partyRole?: string, ) { return this.requestManagementService.addPartyDescriptionV3( requestId, expert, body, partyRole, ); } @ApiBody({ schema: { type: "object", properties: { file: { type: "string", format: "binary" }, partyRole: { type: "string", enum: ["FIRST", "SECOND"] }, }, required: ["file"], }, }) @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); callback(null, `v3-expert-voice-${unique}${ex}`); }, }), }), ) @Post("upload-voice/:requestId") @ApiParam({ name: "requestId" }) @ApiOperation({ summary: "Upload voice for current party" }) async uploadVoice( @Param("requestId") requestId: string, @CurrentUser() expert: any, @UploadedFile() voice: Express.Multer.File, @Body("partyRole") partyRole?: string, ) { await this.mediaPolicyService.assertForBlame(voice, requestId, "voice"); return this.requestManagementService.addPartyVoiceV3( requestId, expert, voice, partyRole, ); } @Put("sign/:requestId") @ApiParam({ name: "requestId" }) @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Party on-site signature" }) @ApiBody({ schema: { type: "object", required: ["partyRole", "sign"], properties: { partyRole: { type: "string", enum: ["FIRST", "SECOND"] }, isAccept: { type: "boolean", default: true }, sign: { type: "string", format: "binary" }, }, }, }) @UseInterceptors( FileInterceptor("sign", { limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES }, storage: diskStorage({ destination: "./files/signs", filename: (req, file, callback) => { const unique = Date.now(); const ex = extname(file.originalname); callback(null, `v3-expert-party-${unique}${ex}`); }, }), }), ) async sign( @Param("requestId") requestId: string, @Body() body: { partyRole?: string; isAccept?: string | boolean }, @CurrentUser() expert: any, @UploadedFile() sign: Express.Multer.File, ) { await this.mediaPolicyService.assertForBlame(sign, requestId, "image"); const partyRole = body?.partyRole === "SECOND" ? PartyRole.SECOND : PartyRole.FIRST; const isAccept = !(body?.isAccept === false || body?.isAccept === "false"); return this.requestManagementService.expertUploadPartySignatureV3( expert, requestId, partyRole, isAccept, sign, ); } @Post("accident-fields/:requestId") @ApiParam({ name: "requestId" }) @ApiBody({ type: ExpertAccidentFieldsDto }) @ApiOperation({ summary: "Accident type / fields (after all signatures)", description: "Saves accident fields only. 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() expert: any, ) { return this.requestManagementService.expertAddAccidentFieldsForBlameV3( expert, requestId, fields, ); } @Get("capture-requirements/:claimRequestId") @ApiParam({ name: "claimRequestId" }) @ApiOperation({ summary: "List required claim documents" }) async getCaptureRequirements( @Param("claimRequestId") claimRequestId: string, @CurrentUser() expert: any, ): Promise { return this.claimRequestManagementService.getCaptureRequirementsV3( claimRequestId, expert.sub, expert, ); } @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" }) async uploadDocument( @Param("claimRequestId") claimRequestId: string, @Body() body: UploadRequiredDocumentV2Dto, @UploadedFile() file: Express.Multer.File, @CurrentUser() expert: any, ): Promise { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image"); return this.claimRequestManagementService.uploadRequiredDocumentV3( claimRequestId, body, file, expert.sub, expert, ); } @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() expert: any, ): Promise { return this.claimRequestManagementService.selectOuterPartsV3( claimRequestId, body, expert.sub, expert, ); } @Patch("select-other-parts/:claimRequestId") @ApiParam({ name: "claimRequestId", 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, `v3-other-parts-${unique}${ex}`); }, }), }), ) @ApiOperation({ summary: "Select other damaged parts (V3)", description: "Other parts only — sheba and national code were collected during 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"], }, file: { type: "string", format: "binary", description: "Optional car green card" }, }, }, }) async selectOtherParts( @Param("claimRequestId") claimRequestId: string, @Body() body: SelectOtherPartsV3Dto, @CurrentUser() expert: any, @UploadedFile() file?: Express.Multer.File, ): Promise { if (file) { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image"); } return this.claimRequestManagementService.selectOtherPartsV3( claimRequestId, body, expert.sub, expert, file, ); } @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, `v3-capture-${unique}${ex}`); }, }), }), ) @ApiOperation({ summary: "Field — 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() expert: any, ): Promise { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image"); return this.claimRequestManagementService.capturePartV3( claimRequestId, body, file, expert.sub, expert, ); } @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, `v3-claim-video-${unique}${ex}`); }, }), }), ) @ApiBody({ schema: { type: "object", required: ["file"], properties: { file: { type: "string", format: "binary" } }, }, }) @ApiOperation({ summary: "Field — walk-around video", description: "Upload after capture-part is complete (parts, angles, capture-phase docs). Required before the final blame accident video.", }) async carCapture( @Param("claimRequestId") claimRequestId: string, @UploadedFile("file") file: Express.Multer.File, @CurrentUser() expert: any, ) { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "video"); return this.claimRequestManagementService.setVideoCaptureV3( claimRequestId, file, expert.sub, expert, ); } @ApiBody({ schema: { type: "object", required: ["file"], 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, `v3-blame-accident-${unique}${ex}`); }, }), }), ) @Post("upload-video/:requestId") @ApiParam({ name: "requestId" }) @ApiOperation({ summary: "Blame accident video (final step)", description: "Last step of the V3 flow. Requires claim walk-around video and completed capture. Sets blame status to WAITING_FOR_EXPERT.", }) async uploadBlameVideo( @Param("requestId") requestId: string, @CurrentUser() expert: any, @UploadedFile() file: Express.Multer.File, ) { await this.mediaPolicyService.assertForBlame(file, requestId, "video"); return this.requestManagementService.expertUploadBlameVideoV3( expert, requestId, file, ); } }