Files
yara724api/src/request-management/file-maker-blame-v4.controller.ts
2026-07-01 12:15:22 +03:30

390 lines
13 KiB
TypeScript

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";
/**
* V4 FileMaker flow — first half of the split blame workflow.
*
* The FileMaker is responsible for everything up to and including the final
* party signature. After both signatures are collected the file is considered
* "sealed" and a FileReviewer can pick it up.
*
* 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
*
* CAR_BODY sequence: same but single party — omit second-party steps.
*/
@ApiTags("v4 FileMaker — blame file creation")
@Controller("v4/file-maker/blame-request-management")
@ApiBearerAuth()
@UseGuards(LocalActorAuthGuard, RolesGuard)
@Roles(RoleEnum.FILE_MAKER)
export class FileMakerBlameV4Controller {
constructor(
private readonly requestManagementService: RequestManagementService,
private readonly mediaPolicyService: MediaPolicyService,
private readonly claimRequestManagementService: ClaimRequestManagementService,
) {}
// ─── File creation ───────────────────────────────────────────────────────────
@Post()
@ApiOperation({ summary: "Create IN_PERSON blame file (FileMaker)" })
@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, `v4-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, `v4-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<UploadRequiredDocumentV2ResponseDto> {
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<GetCaptureRequirementsV2ResponseDto> {
return this.claimRequestManagementService.getCaptureRequirementsV3(
claimRequestId,
fileMaker.sub,
fileMaker,
);
}
}