import { extname } from "node:path"; import { Body, Controller, Get, Param, 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 { RequestManagementService } from "./request-management.service"; import { PartyRole } from "./entities/schema/partyRole.enum"; import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service"; import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto, } from "src/claim-request-management/dto/upload-document-v2.dto"; import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto"; /** * V5 FileMaker flow — identical to V4 but under the /v5/ prefix. * * The only behavioural difference in the V5 flow is in the FileReviewer's final * step: instead of moving directly to WAITING_FOR_EXPERT, the blame video moves * the file to WAITING_FOR_FINANCIAL_EXPERT so a FinancialExpert can approve or * reject before fanavaran submission. * * The FileMaker side is unchanged; all sequence and endpoints are the same. * * THIRD_PARTY sequence: * create → send-party-otp (guilty) → verify-party-otp (guilty) * → [car-body-form if CAR_BODY] → run-inquiries (guilty + auto claim) * → add-detail-location / add-detail-description / upload-voice (guilty) * → sign (guilty) * → send-party-otp (damaged) → verify-party-otp (damaged) * → run-inquiries (damaged) * → add-detail-location / add-detail-description / upload-voice (damaged) * → sign (damaged) ← FileMaker job done here */ @ApiTags("v5 FileMaker — blame file creation (with FinancialExpert approval)") @Controller("v5/file-maker/blame-request-management") @ApiBearerAuth() @UseGuards(LocalActorAuthGuard, RolesGuard) @Roles(RoleEnum.FILE_MAKER) export class FileMakerBlameV5Controller { constructor( private readonly requestManagementService: RequestManagementService, private readonly mediaPolicyService: MediaPolicyService, private readonly claimRequestManagementService: ClaimRequestManagementService, ) {} // ─── File creation ─────────────────────────────────────────────────────────── @Post() @ApiOperation({ summary: "Create IN_PERSON blame file (FileMaker V5)" }) @ApiBody({ type: CreateExpertInitiatedFileDto }) async create( @CurrentUser() fileMaker: any, @Body() dto: CreateExpertInitiatedFileDto, ) { return this.requestManagementService.createExpertInitiatedBlameV2( fileMaker, { ...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. " + "Share this claim ID with the FileReviewer.", }) async getLinkedClaimId( @Param("requestId") requestId: string, @CurrentUser() fileMaker: any, ) { return this.requestManagementService.getV3LinkedClaimForExpert( fileMaker, requestId, ); } // ─── OTP ───────────────────────────────────────────────────────────────────── @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() fileMaker: any, @Param("requestId") requestId: string, @Body() dto: SendPartyOtpDto, ) { return this.requestManagementService.sendPartyOtpV2( fileMaker, 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() fileMaker: any, @Param("requestId") requestId: string, @Body() dto: VerifyPartyOtpDto, ) { return this.requestManagementService.verifyPartyOtpV2( fileMaker, requestId, dto, ); } // ─── Pre-inquiry form ──────────────────────────────────────────────────────── @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() fileMaker: any, ) { return this.requestManagementService.carBodyAccidentTypeFormV3( requestId, body, fileMaker, ); } @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).", }) async runInquiries( @CurrentUser() fileMaker: any, @Param("requestId") requestId: string, @Body() dto: RunInquiriesV3Dto, ) { return this.requestManagementService.runInquiriesV3( requestId, fileMaker, dto, ); } // ─── Party details ──────────────────────────────────────────────────────────── @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() fileMaker: any, @Body("partyRole") partyRole?: string, ) { return this.requestManagementService.addPartyLocationV3( requestId, fileMaker, 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() fileMaker: any, @Body("partyRole") partyRole?: string, ) { return this.requestManagementService.addPartyDescriptionV3( requestId, fileMaker, 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, `v5-filemaker-voice-${unique}${ex}`); }, }), }), ) @Post("upload-voice/:requestId") @ApiParam({ name: "requestId" }) @ApiOperation({ summary: "Upload voice for current party" }) async uploadVoice( @Param("requestId") requestId: string, @CurrentUser() fileMaker: any, @UploadedFile() voice: Express.Multer.File, @Body("partyRole") partyRole?: string, ) { await this.mediaPolicyService.assertForBlame(voice, requestId, "voice"); return this.requestManagementService.addPartyVoiceV3( requestId, fileMaker, voice, partyRole, ); } // ─── Signatures (final FileMaker step) ─────────────────────────────────────── @Put("sign/:requestId") @ApiParam({ name: "requestId" }) @ApiConsumes("multipart/form-data") @ApiOperation({ summary: "Party on-site signature (FileMaker final step)", description: "Collect FIRST (guilty) then SECOND (damaged) signatures. " + "After the last signature the file is ready for FileReviewer pickup.", }) @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, `v5-filemaker-party-${unique}${ex}`); }, }), }), ) async sign( @Param("requestId") requestId: string, @Body() body: { partyRole?: string; isAccept?: string | boolean }, @CurrentUser() fileMaker: 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( fileMaker, requestId, partyRole, isAccept, sign, ); } // ─── Claim 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 (FileMaker)", description: "Upload licenses and car cards against the auto-created claim. " + "Use claim-id/:requestId to obtain the claimRequestId after run-inquiries.", }) async uploadDocument( @Param("claimRequestId") claimRequestId: string, @Body() body: UploadRequiredDocumentV2Dto, @UploadedFile() file: Express.Multer.File, @CurrentUser() fileMaker: any, ): Promise { await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image"); return this.claimRequestManagementService.uploadRequiredDocumentV3( claimRequestId, body, file, fileMaker.sub, fileMaker, ); } @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() fileMaker: any, ): Promise { return this.claimRequestManagementService.getCaptureRequirementsV3( claimRequestId, fileMaker.sub, fileMaker, ); } }