forked from Yara724/api
480 lines
17 KiB
TypeScript
480 lines
17 KiB
TypeScript
import { extname } from "node:path";
|
|
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Get,
|
|
HttpException,
|
|
InternalServerErrorException,
|
|
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 { 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 {
|
|
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";
|
|
|
|
/**
|
|
* V4 FileReviewer flow — second half of the split blame workflow.
|
|
*
|
|
* The FileReviewer picks up a sealed file (both signatures collected by the
|
|
* FileMaker) and completes the damage-assessment portion: accident fields,
|
|
* documents, part selection, photo capture, and walk-around video (final step).
|
|
*
|
|
* Sequence:
|
|
* accident-fields
|
|
* → capture-requirements (via linked claimRequestId)
|
|
* → upload-document (licenses, car cards)
|
|
* → select-outer-parts
|
|
* → select-other-parts
|
|
* → capture-part (damaged-part photos + four angles)
|
|
* → upload-document (chassis / engine — capture phase)
|
|
* → car-capture (walk-around video — FINAL FileReviewer step)
|
|
* ↳ claim → WAITING_FOR_DAMAGE_EXPERT, blame → COMPLETED
|
|
*
|
|
* NOTE: upload-video is a no-op for V4 (blame is already COMPLETED by car-capture).
|
|
*/
|
|
@ApiTags("v4 FileReviewer — blame file review & capture")
|
|
@Controller("v4/file-reviewer/blame-request-management")
|
|
@ApiBearerAuth()
|
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
|
@Roles(RoleEnum.FILE_REVIEWER)
|
|
export class FileReviewerBlameV4Controller {
|
|
constructor(
|
|
private readonly requestManagementService: RequestManagementService,
|
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
|
private readonly mediaPolicyService: MediaPolicyService,
|
|
) {}
|
|
|
|
// ─── Linked claim lookup ──────────────────────────────────────────────────────
|
|
|
|
@Get("claim-id/:requestId")
|
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
|
@ApiOperation({
|
|
summary: "Get linked claim ID",
|
|
description:
|
|
"Returns the claim auto-created during FileMaker's guilty-party run-inquiries. " +
|
|
"Use this claim ID for all capture/document/parts endpoints below.",
|
|
})
|
|
async getLinkedClaimId(
|
|
@Param("requestId") requestId: string,
|
|
@CurrentUser() fileReviewer: any,
|
|
) {
|
|
return this.requestManagementService.getV3LinkedClaimForExpert(
|
|
fileReviewer,
|
|
requestId,
|
|
);
|
|
}
|
|
|
|
// ─── Accident fields ──────────────────────────────────────────────────────────
|
|
|
|
@Post("accident-fields/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiBody({ type: ExpertAccidentFieldsDto })
|
|
@ApiOperation({
|
|
summary: "Accident type / fields (FileReviewer first step)",
|
|
description:
|
|
"Saves accident fields on the sealed file. " +
|
|
"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() fileReviewer: any,
|
|
) {
|
|
return this.requestManagementService.expertAddAccidentFieldsForBlameV3(
|
|
fileReviewer,
|
|
requestId,
|
|
fields,
|
|
);
|
|
}
|
|
|
|
// ─── Capture requirements ─────────────────────────────────────────────────────
|
|
|
|
@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() fileReviewer: any,
|
|
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
|
return this.claimRequestManagementService.getCaptureRequirementsV3(
|
|
claimRequestId,
|
|
fileReviewer.sub,
|
|
fileReviewer,
|
|
);
|
|
}
|
|
|
|
// ─── 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",
|
|
description:
|
|
"Initial phase (UPLOAD_REQUIRED_DOCUMENTS): licenses and car cards only. " +
|
|
"Capture phase (CAPTURE_PART_DAMAGES, after parts + angles): chassis, engine, metal plate only.",
|
|
})
|
|
async uploadDocument(
|
|
@Param("claimRequestId") claimRequestId: string,
|
|
@Body() body: UploadRequiredDocumentV2Dto,
|
|
@UploadedFile() file: Express.Multer.File,
|
|
@CurrentUser() fileReviewer: any,
|
|
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
|
return this.claimRequestManagementService.uploadRequiredDocumentV3(
|
|
claimRequestId,
|
|
body,
|
|
file,
|
|
fileReviewer.sub,
|
|
fileReviewer,
|
|
);
|
|
}
|
|
|
|
// ─── Part selection ───────────────────────────────────────────────────────────
|
|
|
|
@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() fileReviewer: any,
|
|
): Promise<SelectOuterPartsV2ResponseDto> {
|
|
return this.claimRequestManagementService.selectOuterPartsV3(
|
|
claimRequestId,
|
|
body,
|
|
fileReviewer.sub,
|
|
fileReviewer,
|
|
);
|
|
}
|
|
|
|
@Patch("select-other-parts/:claimRequestId")
|
|
@ApiParam({ name: "claimRequestId", example: "507f1f77bcf86cd799439011" })
|
|
@ApiOperation({
|
|
summary: "Select other damaged parts (V4)",
|
|
description:
|
|
"Other parts only — sheba and national code were collected during FileMaker's 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"],
|
|
},
|
|
},
|
|
},
|
|
})
|
|
async selectOtherParts(
|
|
@Param("claimRequestId") claimRequestId: string,
|
|
@Body() body: SelectOtherPartsV3Dto,
|
|
@CurrentUser() fileReviewer: any,
|
|
): Promise<SelectOtherPartsV2ResponseDto> {
|
|
return this.claimRequestManagementService.selectOtherPartsV3(
|
|
claimRequestId,
|
|
body,
|
|
fileReviewer.sub,
|
|
fileReviewer,
|
|
);
|
|
}
|
|
|
|
// ─── Photo capture ────────────────────────────────────────────────────────────
|
|
|
|
@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, `v4-capture-${unique}${ex}`);
|
|
},
|
|
}),
|
|
}),
|
|
)
|
|
@ApiOperation({
|
|
summary: "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() fileReviewer: any,
|
|
): Promise<CapturePartV2ResponseDto> {
|
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
|
return this.claimRequestManagementService.capturePartV3(
|
|
claimRequestId,
|
|
body,
|
|
file,
|
|
fileReviewer.sub,
|
|
fileReviewer,
|
|
);
|
|
}
|
|
|
|
// ─── Owner signature on expert pricing ───────────────────────────────────────
|
|
|
|
@Put("claim-sign/:claimRequestId")
|
|
@ApiParam({ name: "claimRequestId" })
|
|
@ApiConsumes("multipart/form-data")
|
|
@ApiBody({
|
|
description: "Signature file, agreement, and branch",
|
|
schema: {
|
|
type: "object",
|
|
required: ["sign", "agree", "branchId"],
|
|
properties: {
|
|
sign: { type: "string", format: "binary", description: "Signature image" },
|
|
agree: { type: "boolean", description: "true to accept, false to reject" },
|
|
branchId: { type: "string", description: "Insurer branch ID" },
|
|
},
|
|
},
|
|
})
|
|
@ApiOperation({
|
|
summary: "Owner signature on expert pricing (V4 — FileReviewer acts on behalf of user)",
|
|
description:
|
|
"FileReviewer submits the damaged party's signature during the final approval stage. " +
|
|
"Delegates to the same service method as the user sign endpoint; the FileReviewer's " +
|
|
"identity is resolved to the claim owner via `resolveClaimEffectiveUserId`.",
|
|
})
|
|
@UseInterceptors(
|
|
FileInterceptor("sign", {
|
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
|
storage: diskStorage({
|
|
destination: "./files/claim-sign",
|
|
filename: (req, file, callback) => {
|
|
const unique = Date.now();
|
|
const ex = extname(file.originalname);
|
|
const base = file.originalname.split(/[.,\s-]/)[0] || "sign";
|
|
callback(null, `${base}-${unique}${ex}`);
|
|
},
|
|
}),
|
|
}),
|
|
)
|
|
async submitOwnerSign(
|
|
@Param("claimRequestId") claimRequestId: string,
|
|
@Body("agree") agree: string | boolean,
|
|
@Body("branchId") branchId: string,
|
|
@CurrentUser() fileReviewer: any,
|
|
@UploadedFile() sign: Express.Multer.File,
|
|
) {
|
|
await this.mediaPolicyService.assertForClaim(sign, claimRequestId, "image");
|
|
const agreed =
|
|
typeof agree === "string"
|
|
? agree === "true" || agree === "1"
|
|
: Boolean(agree);
|
|
try {
|
|
return await this.claimRequestManagementService.submitOwnerInsurerApprovalSignV2(
|
|
claimRequestId,
|
|
agreed,
|
|
typeof branchId === "string" ? branchId : "",
|
|
sign,
|
|
fileReviewer.sub,
|
|
fileReviewer,
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof HttpException) throw error;
|
|
throw new InternalServerErrorException(
|
|
error instanceof Error ? error.message : "Failed to submit signature",
|
|
);
|
|
}
|
|
}
|
|
|
|
// ─── Walk-around video ────────────────────────────────────────────────────────
|
|
|
|
@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, `v4-claim-video-${unique}${ex}`);
|
|
},
|
|
}),
|
|
}),
|
|
)
|
|
@ApiBody({
|
|
schema: {
|
|
type: "object",
|
|
required: ["file"],
|
|
properties: { file: { type: "string", format: "binary" } },
|
|
},
|
|
})
|
|
@ApiOperation({
|
|
summary: "Walk-around video (final FileReviewer step — V4)",
|
|
description:
|
|
"Last FileReviewer action. Upload after capture-part is complete (parts, angles, chassis/engine docs). " +
|
|
"Finalises the claim → WAITING_FOR_DAMAGE_EXPERT and marks the blame as COMPLETED.",
|
|
})
|
|
async carCapture(
|
|
@Param("claimRequestId") claimRequestId: string,
|
|
@UploadedFile("file") file: Express.Multer.File,
|
|
@CurrentUser() fileReviewer: any,
|
|
) {
|
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "video");
|
|
return this.claimRequestManagementService.setVideoCaptureV3(
|
|
claimRequestId,
|
|
file,
|
|
fileReviewer.sub,
|
|
fileReviewer,
|
|
);
|
|
}
|
|
|
|
// ─── Final blame video ────────────────────────────────────────────────────────
|
|
|
|
@ApiBody({
|
|
schema: {
|
|
type: "object",
|
|
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, `v4-blame-accident-${unique}${ex}`);
|
|
},
|
|
}),
|
|
}),
|
|
)
|
|
@Post("upload-video/:requestId")
|
|
@ApiParam({ name: "requestId" })
|
|
@ApiOperation({
|
|
summary: "Blame accident video (no-op for V4 — car-capture is the final step)",
|
|
description:
|
|
"Deprecated for V4. The blame is already COMPLETED by car-capture. " +
|
|
"Calling this endpoint returns an idempotent success response.",
|
|
})
|
|
async uploadBlameVideo(
|
|
@Param("requestId") requestId: string,
|
|
@CurrentUser() fileReviewer: any,
|
|
@UploadedFile() file: Express.Multer.File,
|
|
) {
|
|
if (file) {
|
|
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
|
|
}
|
|
return this.requestManagementService.expertUploadBlameVideoV3(
|
|
fileReviewer,
|
|
requestId,
|
|
file,
|
|
);
|
|
}
|
|
}
|