1
0
forked from Yara724/api
Files
yara724-api/src/request-management/expert-initiated.v2.controller.ts
2026-06-22 13:05:11 +03:30

512 lines
19 KiB
TypeScript

import { extname } from "node:path";
import {
Controller,
Post,
Body,
Param,
UseGuards,
Get,
UseInterceptors,
UploadedFile,
} from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import { diskStorage } from "multer";
import {
ApiBearerAuth,
ApiBody,
ApiParam,
ApiTags,
ApiOperation,
ApiResponse,
ApiConsumes,
} from "@nestjs/swagger";
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
import { RolesGuard } from "src/auth/guards/role.guard";
import { CurrentUser } from "src/decorators/user.decorator";
import { Roles } from "src/decorators/roles.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 { RequestManagementService } from "./request-management.service";
import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto";
import { ExpertCompleteThirdPartyFormV2Dto } from "./dto/expert-complete-third-party-form.v2.dto";
import { ExpertCompleteCarBodyFormV2Dto } from "./dto/expert-complete-car-body-form.v2.dto";
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
import { ExpertCompleteClaimDataDto } from "./dto/expert-complete-claim-data.dto";
import { ExpertUploadPartySignatureDto } from "./dto/expert-upload-party-signature.dto";
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
import { SendExpertInitiatedLinkV2Dto } from "./dto/send-expert-initiated-link.v2.dto";
import { ExpertCompleteLocationV2Dto } from "./dto/expert-complete-location.v2.dto";
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
import { PartyRole } from "./entities/schema/partyRole.enum";
/**
* V2 expert-initiated blame API: uses BlameRequest (workflow) model.
* Field experts create files via LINK (send link to user) or IN_PERSON (expert fills on-site).
* Only the initiating field expert can see/review these files.
*/
// @ApiTags("expert-initiated-blame (v2)")
@Controller("v2/expert-initiated-blame")
@ApiBearerAuth()
@UseGuards(LocalActorAuthGuard, RolesGuard)
@Roles(RoleEnum.FIELD_EXPERT)
export class ExpertInitiatedV2Controller {
constructor(
private readonly requestManagementService: RequestManagementService,
private readonly claimRequestManagementService: ClaimRequestManagementService,
private readonly mediaPolicyService: MediaPolicyService,
) {}
@Get("my-files")
// @ApiOperation({
// summary: "[V2] List my expert-initiated blame files",
// description:
// "Returns all BlameRequest (workflow) files created by the current field expert.",
// })
// @ApiResponse({ status: 200, description: "List of expert-initiated files" })
async getMyFilesV2(@CurrentUser() expert: any) {
return this.requestManagementService.getMyExpertInitiatedFilesV2(expert);
}
@Get("blame/:requestId")
// @ApiOperation({
// summary: "[V2] Get one expert-initiated blame request",
// description: "Returns a single blame request. Only the initiating field expert can access.",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiResponse({ status: 200, description: "Blame request" })
async getBlameRequestV2(
@Param("requestId") requestId: string,
@CurrentUser() expert: any,
) {
return this.requestManagementService.getBlameRequestV2(requestId, expert);
}
@Post("create")
// @ApiOperation({
// summary: "[V2] Create expert-initiated blame file",
// description:
// "Creates a BlameRequest with workflow. LINK: parties identified by phone, expert sends link. IN_PERSON: expert fills form later.",
// })
// @ApiBody({
// type: CreateExpertInitiatedFileDto,
// examples: {
// thirdPartyInPerson: {
// summary: "Third-party, IN_PERSON (both parties)",
// description:
// "Expert fills form on-site for both parties.",
// value: {
// type: "THIRD_PARTY",
// creationMethod: "LINK",
// firstPartyPhoneNumber: "09123456789",
// },
// },
// carBodyInPerson: {
// summary: "Car-body, IN_PERSON",
// description: "Expert fills form on-site for single party (car body).",
// value: {
// type: "CAR_BODY",
// creationMethod: "IN_PERSON",
// firstPartyPhoneNumber: "09123456789",
// },
// },
// carBodyLink: {
// summary: "Car-body, LINK",
// description:
// "Expert sends link to party by phone. Only first party phone needed.",
// value: {
// type: "CAR_BODY",
// creationMethod: "LINK",
// firstPartyPhoneNumber: "09123456789",
// },
// },
// },
// })
async createV2(
@CurrentUser() expert: any,
@Body() dto: CreateExpertInitiatedFileDto,
) {
return this.requestManagementService.createExpertInitiatedBlameV2(
expert,
dto,
);
}
@Post("send-link/:requestId")
// @ApiOperation({
// summary: "[V2] Send blame link to first party (LINK)",
// description:
// "For expert-initiated LINK files only. Provide the first party phone number; the service stores it, registers the user if needed, and sends the invite link via SMS.",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiBody({ type: SendExpertInitiatedLinkV2Dto })
async sendLinkV2(
@CurrentUser() expert: any,
@Param("requestId") requestId: string,
@Body() dto: SendExpertInitiatedLinkV2Dto,
) {
return this.requestManagementService.sendLinkV2(expert, requestId, dto);
}
@Post("send-party-otps/:requestId")
// @ApiOperation({
// summary: "[V2] Send OTP to party/parties (IN_PERSON)",
// description:
// "Sends OTP via SMS to first party (and second party for THIRD_PARTY) using the same flow as /user/send-otp. Parties receive the code; collect it from them and call verify-party-otps. Call this before filling the blame form.",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiBody({ type: SendPartyOtpsDto })
// @ApiResponse({ status: 200, description: "OTP(s) sent; collect codes and call verify-party-otps" })
async sendPartyOtpsV2(
@CurrentUser() expert: any,
@Param("requestId") requestId: string,
@Body() dto: SendPartyOtpsDto,
) {
return this.requestManagementService.sendPartyOtpsV2(expert, requestId, dto);
}
@Post("verify-party-otps/:requestId")
// @ApiOperation({
// summary: "[V2] Verify party OTPs (IN_PERSON)",
// description:
// "After send-party-otps, parties receive SMS. They tell you the code; submit it here. Required before complete-blame-data.",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiBody({ type: VerifyPartyOtpsDto })
// @ApiResponse({ status: 200, description: "OTPs verified; expert can proceed to complete-blame-data" })
async verifyPartyOtpsV2(
@CurrentUser() expert: any,
@Param("requestId") requestId: string,
@Body() dto: VerifyPartyOtpsDto,
) {
return this.requestManagementService.verifyPartyOtpsV2(expert, requestId, dto);
}
@Post("complete-blame-data/:requestId")
// @ApiOperation({
// summary: "[V2] Submit all blame data (IN_PERSON)",
// description:
// "For IN_PERSON files only. Send THIRD_PARTY or CAR_BODY form EXCEPT location. Use one shared expertDescription (not per-party descriptions). After this, call add-locations once to submit scene lat/lon and move workflow to WAITING_FOR_SIGNATURES.",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiBody({
// description: "Choose THIRD_PARTY or CAR_BODY example according to the file type. IN_PERSON v2 uses one shared expertDescription for the scene.",
// examples: {
// THIRD_PARTY: {
// summary: "THIRD_PARTY",
// description: "Use when the blame file type is THIRD_PARTY",
// value: {
// firstPartyPhoneNumber: "09123456789",
// firstPartyInitialForm: {
// expertOpinion: false,
// imDamaged: false,
// imGuilty: true,
// },
// firstPartyPlate: {
// nationalCodeOfInsurer: "",
// nationalCodeOfDriver: "",
// insurerLicense: "",
// driverLicense: "",
// plate: { leftDigits: 12, centerAlphabet: "الف", centerDigits: 345, ir: 22 },
// driverIsInsurer: true,
// isNewCar: false,
// userNoCertificate: false,
// insurerBirthday: 13770624,
// driverBirthday: "1370-01-01",
// },
// expertDescription: { desc: "توضیح حادثه کارشناس (مشترک برای صحنه)" },
// secondParty: {
// phoneNumber: "09187654321",
// initialForm: {
// expertOpinion: false,
// imDamaged: true,
// imGuilty: false,
// },
// plate: {
// nationalCodeOfInsurer: "",
// nationalCodeOfDriver: "",
// insurerLicense: "",
// driverLicense: "",
// plate: { leftDigits: 91, centerAlphabet: "ن", centerDigits: 174, ir: 79 },
// driverIsInsurer: true,
// isNewCar: false,
// userNoCertificate: false,
// insurerBirthday: 13700720,
// driverBirthday: "1370-01-01",
// },
// },
// guiltyPartyPhoneNumber: "09123456789",
// },
// },
// CAR_BODY: {
// summary: "CAR_BODY",
// description: "Use when the blame file type is CAR_BODY",
// value: {
// firstPartyPhoneNumber: "09123456789",
// firstPartyInitialForm: {
// expertOpinion: false,
// imDamaged: false,
// imGuilty: true,
// },
// carBodyForm: { car: true, object: false },
// firstPartyPlate: {
// plateId: "",
// nationalCodeOfInsurer: "",
// nationalCodeOfDriver: "",
// insurerLicense: "",
// driverLicense: "",
// plate: { leftDigits: 12, centerAlphabet: "الف", centerDigits: 345, ir: 22 },
// driverIsInsurer: true,
// isNewCar: false,
// userNoCertificate: false,
// insurerBirthday: 1370,
// driverBirthday: "1370-01-01",
// },
// expertDescription: {
// desc: "توضیح حادثه",
// accidentDate: "2025-01-15",
// accidentTime: "14:30",
// weatherCondition: "صاف",
// roadCondition: "خشک",
// lightCondition: "روز",
// },
// },
// },
// },
// schema: { type: "object" },
// })
// @ApiResponse({ status: 200, description: "Blame form completed; next: add-locations" })
async completeBlameDataV2(
@CurrentUser() expert: any,
@Param("requestId") requestId: string,
@Body() formData: ExpertCompleteThirdPartyFormV2Dto | ExpertCompleteCarBodyFormV2Dto,
) {
return this.requestManagementService.expertCompleteBlameDataV2(
expert,
requestId,
formData,
);
}
@Post("add-locations/:requestId")
// @ApiOperation({
// summary: "[V2] Submit one scene location for expert-initiated IN_PERSON blame",
// description:
// "Submit one scene location after complete-blame-data. This transitions workflow to WAITING_FOR_SIGNATURES.",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiBody({
// type: ExpertCompleteLocationV2Dto,
// examples: {
// scene: {
// summary: "One scene location (all IN_PERSON types)",
// value: {
// location: { lat: 35.6892, lon: 51.389 },
// },
// },
// },
// })
// @ApiResponse({ status: 200, description: "Locations saved; next: upload party signature(s)" })
async addLocationsV2(
@CurrentUser() expert: any,
@Param("requestId") requestId: string,
@Body() dto: ExpertCompleteLocationV2Dto,
) {
return this.requestManagementService.expertAddLocationsForBlameV2(
expert,
requestId,
dto as any,
);
}
@Post("upload-video/:requestId")
// @ApiOperation({
// summary: "[V2] Expert uploads video for expert-initiated BlameRequest",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiConsumes("multipart/form-data")
// @ApiBody({
// schema: {
// type: "object",
// properties: { file: { type: "string", format: "binary" } },
// },
// })
@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, `expert-${file.originalname}-${unique}${ex}`);
},
}),
}),
)
// @ApiResponse({ status: 200, description: "Video uploaded" })
async uploadVideoV2(
@CurrentUser() expert: any,
@Param("requestId") requestId: string,
@UploadedFile() file?: Express.Multer.File,
) {
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
return this.requestManagementService.expertUploadVideoForBlameV2(
expert,
requestId,
file,
);
}
@Post("upload-voice/:requestId")
// @ApiOperation({
// summary: "[V2] Expert uploads voice for expert-initiated BlameRequest",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiConsumes("multipart/form-data")
// @ApiBody({
// schema: {
// type: "object",
// properties: { voice: { type: "string", format: "binary" } },
// },
// })
@UseInterceptors(
FileInterceptor("voice", {
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
storage: diskStorage({
destination: "./files/voice",
filename: (req, file, callback) => {
const unique = Date.now();
const ex = extname(file.originalname);
const flname = file.originalname.split(".")[0];
callback(null, `expert-${flname}-${unique}${ex}`);
},
}),
}),
)
// @ApiResponse({ status: 200, description: "Voice uploaded" })
async uploadVoiceV2(
@CurrentUser() expert: any,
@Param("requestId") requestId: string,
@UploadedFile() voice?: Express.Multer.File,
) {
await this.mediaPolicyService.assertForBlame(voice, requestId, "voice");
return this.requestManagementService.expertUploadVoiceForBlameV2(
expert,
requestId,
voice,
);
}
@Post("add-accident-fields/:requestId")
// @ApiOperation({
// summary: "[V2] Expert adds accident fields to expert-initiated BlameRequest",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiBody({ type: ExpertAccidentFieldsDto })
// @ApiResponse({ status: 200, description: "Accident fields added" })
async addAccidentFieldsV2(
@CurrentUser() expert: any,
@Param("requestId") requestId: string,
@Body() fields: ExpertAccidentFieldsDto,
) {
return this.requestManagementService.expertAddAccidentFieldsForBlameV2(
expert,
requestId,
fields,
);
}
@Post("upload-party-signature/:requestId")
// @ApiOperation({
// summary: "[V2] Expert uploads party signature (IN_PERSON)",
// description:
// "For IN_PERSON only. Upload a party's signature collected on-site. CAR_BODY: use partyRole FIRST once. THIRD_PARTY: upload FIRST then SECOND. When all required parties have signed, blame case completes.",
// })
// @ApiParam({ name: "requestId", description: "Blame request ID" })
// @ApiConsumes("multipart/form-data")
// @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, `expert-party-${unique}${ex}`);
},
}),
}),
)
// @ApiResponse({ status: 200, description: "Signature recorded" })
async uploadPartySignatureV2(
@CurrentUser() expert: any,
@Param("requestId") requestId: string,
@Body() body: { partyRole?: string; isAccept?: string | boolean },
@UploadedFile() sign?: Express.Multer.File,
) {
await this.mediaPolicyService.assertForBlame(sign, requestId, "image");
const partyRole = (body.partyRole === "FIRST" || body.partyRole === "SECOND")
? body.partyRole
: ("FIRST" as const);
const isAccept = body.isAccept === false || body.isAccept === "false" ? false : true;
return this.requestManagementService.expertUploadPartySignatureV2(
expert,
requestId,
partyRole as PartyRole,
isAccept,
sign!,
);
}
@Post("create-claim-from-blame/:blameRequestId")
// @ApiOperation({
// summary: "[V2] Create claim from expert-initiated IN_PERSON blame",
// description:
// "Field expert creates a claim on behalf of the damaged party. Blame must be COMPLETED (signatures collected). Then use claim v2 endpoints to fill parts, documents, and captures.",
// })
// @ApiParam({ name: "blameRequestId", description: "Completed blame request ID" })
// @ApiResponse({ status: 201, description: "Claim created" })
async createClaimFromBlame(
@CurrentUser() expert: any,
@Param("blameRequestId") blameRequestId: string,
) {
return this.claimRequestManagementService.createClaimFromBlameForExpertV2(
blameRequestId,
expert,
);
}
@Post("complete-claim-data/:claimRequestId")
// @ApiOperation({
// summary: "Submit claim-needed data (expert-initiated IN_PERSON)",
// })
// @ApiParam({ name: "claimRequestId", description: "Claim file ID" })
// @ApiBody({ type: ExpertCompleteClaimDataDto })
// @ApiResponse({ status: 200, description: "Claim data updated" })
async completeClaimData(
@CurrentUser() expert: any,
@Param("claimRequestId") claimRequestId: string,
@Body() dto: ExpertCompleteClaimDataDto,
) {
return this.claimRequestManagementService.expertCompleteClaimData(
claimRequestId,
expert,
dto,
);
}
}