forked from Yara724/api
557 lines
19 KiB
TypeScript
557 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 { ExpertCompleteLocationV2Dto } from "./dto/expert-complete-location.v2.dto";
|
|
import { SendExpertInitiatedLinkV2Dto } from "./dto/send-expert-initiated-link.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: "IN_PERSON",
|
|
firstPartyPhoneNumber: "09123456789",
|
|
secondPartyPhoneNumber: "09187654321",
|
|
},
|
|
},
|
|
thirdPartyLink: {
|
|
summary: "Third-party, LINK",
|
|
description:
|
|
"Expert sends link to first and second party by phone.",
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
@ApiResponse({
|
|
status: 201,
|
|
description: "File created",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
requestId: { type: "string" },
|
|
publicId: { type: "string" },
|
|
linkUrl: { type: "string", description: "Present for LINK method" },
|
|
},
|
|
},
|
|
})
|
|
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 party/parties (LINK)",
|
|
description:
|
|
"For expert-initiated LINK files only. Sends SMS with template `yara-field-expert-link` to first party (and second party for THIRD_PARTY). Tokens: token=#1(بدنه/ثالث), token2=#2(expert name), token3=#3(invite link built exactly like add-second-party flow: `${URL}/{frontendRoute}?token={requestId}`).",
|
|
})
|
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
|
@ApiBody({ type: SendExpertInitiatedLinkV2Dto })
|
|
@ApiResponse({
|
|
status: 200,
|
|
description: "Link sent; recipients can open and fill via normal user flow",
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
sent: { type: "boolean" },
|
|
linkUrl: { type: "string" },
|
|
sentTo: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
role: { type: "string", enum: ["FIRST", "SECOND"] },
|
|
phoneNumber: { type: "string" },
|
|
smsSent: { type: "boolean" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
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,
|
|
);
|
|
}
|
|
}
|