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, ); } }