1
0
forked from Yara724/api

YARA-1094, YARA-1095, YARA-1096

This commit is contained in:
SepehrYahyaee
2026-07-12 14:07:27 +03:30
parent c955deda5c
commit 72dec7a917
17 changed files with 1589 additions and 2 deletions

View File

@@ -0,0 +1,426 @@
import { extname } from "node:path";
import {
Body,
Controller,
Get,
Param,
Patch,
Post,
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";
/**
* V5 FileReviewer flow — same as V4 except the final blame accident video moves
* the file to WAITING_FOR_FINANCIAL_EXPERT instead of WAITING_FOR_EXPERT.
*
* A FinancialExpert must then approve the file (→ WAITING_FOR_EXPERT) or
* reject it back (→ FINANCIAL_EXPERT_REJECTED) for the FileReviewer to correct.
*
* 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 / metal plate — capture phase)
* → car-capture (walk-around video)
* → upload-video (blame accident video — final, moves to WAITING_FOR_FINANCIAL_EXPERT)
*/
@ApiTags("v5 FileReviewer — blame file review & capture (with FinancialExpert approval)")
@Controller("v5/file-reviewer/blame-request-management")
@ApiBearerAuth()
@UseGuards(LocalActorAuthGuard, RolesGuard)
@Roles(RoleEnum.FILE_REVIEWER)
export class FileReviewerBlameV5Controller {
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_FINANCIAL_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 (V5)",
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, `v5-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,
);
}
// ─── 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, `v5-claim-video-${unique}${ex}`);
},
}),
}),
)
@ApiBody({
schema: {
type: "object",
required: ["file"],
properties: { file: { type: "string", format: "binary" } },
},
})
@ApiOperation({
summary: "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() fileReviewer: any,
) {
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "video");
return this.claimRequestManagementService.setVideoCaptureV3(
claimRequestId,
file,
fileReviewer.sub,
fileReviewer,
);
}
// ─── Final blame video ─────────────────────────────────────────────────────────
//
// V5 difference: moves file to WAITING_FOR_FINANCIAL_EXPERT, not WAITING_FOR_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, `v5-blame-accident-${unique}${ex}`);
},
}),
}),
)
@Post("upload-video/:requestId")
@ApiParam({ name: "requestId" })
@ApiOperation({
summary: "Blame accident video (FileReviewer final step — V5)",
description:
"Last step of the V5 FileReviewer flow. Requires claim walk-around video and completed capture. " +
"Sets blame status to WAITING_FOR_FINANCIAL_EXPERT (pending FinancialExpert approval).",
})
async uploadBlameVideo(
@Param("requestId") requestId: string,
@CurrentUser() fileReviewer: any,
@UploadedFile() file: Express.Multer.File,
) {
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
return this.requestManagementService.expertUploadBlameVideoV5(
fileReviewer,
requestId,
file,
);
}
// ─── Re-submission after FinancialExpert rejection ────────────────────────────
@Post("resubmit-to-financial-expert/:requestId")
@ApiParam({ name: "requestId" })
@ApiOperation({
summary: "Re-submit rejected file to FinancialExpert (FileReviewer)",
description:
"Use after the FinancialExpert has rejected the file (FINANCIAL_EXPERT_REJECTED). " +
"Moves status back to WAITING_FOR_FINANCIAL_EXPERT for re-review.",
})
async resubmitToFinancialExpert(
@Param("requestId") requestId: string,
@CurrentUser() fileReviewer: any,
) {
return this.requestManagementService.fileReviewerResubmitToFinancialExpert(
fileReviewer,
requestId,
);
}
}