From 8ac185c861b41056c0e01ea50aead0b80a2c08ba Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Tue, 22 Sep 2026 11:37:21 +0330 Subject: [PATCH] feat(claims): support optional accident sketch --- docs/blame-claim-flow-architecture.html | 10 ++ docs/panel-roles-reference.html | 4 +- .../required-document-type.enum.ts | 4 +- .../claim-optional-document.spec.ts | 45 +++++++++ .../claim-request-management.service.ts | 93 ++++++++++++++++--- .../claim-request-management.v2.controller.ts | 2 + .../dto/capture-requirements-v2.dto.ts | 7 ++ ...xpert-initiated-claim.mirror.controller.ts | 1 + .../registrar-claim.mirror.controller.ts | 1 + 9 files changed, 151 insertions(+), 16 deletions(-) create mode 100644 src/claim-request-management/claim-optional-document.spec.ts diff --git a/docs/blame-claim-flow-architecture.html b/docs/blame-claim-flow-architecture.html index b9e7c14..d31a87d 100644 --- a/docs/blame-claim-flow-architecture.html +++ b/docs/blame-claim-flow-architecture.html @@ -461,6 +461,16 @@ +
+
Optional document shared by every claim flow
+

+ Every V1–V6 document-upload step also offers + accident_sketch (کروکی). It is optional for both + THIRD_PARTY and CAR_BODY, may be omitted without + blocking progression, and is returned in expert and insurer case details when uploaded. +

+
+

Flow V1 / V2 — User-Initiated Blame & Claim

diff --git a/docs/panel-roles-reference.html b/docs/panel-roles-reference.html index 0c490e9..220f064 100644 --- a/docs/panel-roles-reference.html +++ b/docs/panel-roles-reference.html @@ -462,7 +462,7 @@ POSTrun-inquiries/:id / run-inquiries-vin/:idRun plate or VIN inquiry. First call = guilty (+ auto-creates claim). Second call = damaged (THIRD_PARTY only). POSTadd-detail-location/:id / add-detail-description/:id / upload-voice/:idAdd location, description, and voice for current party (partyRole param selects FIRST/SECOND). PUTsign/:idUpload party signature (partyRole=FIRST then SECOND). After second signature, file is sealed. - POSTupload-document/:claimIdUpload licences / car cards against the auto-created claim. + POSTupload-document/:claimIdUpload licences / car cards and the optional accident_sketch (کروکی) against the auto-created claim. The sketch never gates completion. GETcapture-requirements/:claimIdStep-aware capture requirements (phases: pre-capture docs vs damaged parts + chassis/engine). @@ -498,7 +498,7 @@ GETclaim-id/:requestIdGet the auto-created claim ID (from FileMaker's guilty-party inquiry). POSTaccident-fields/:requestIdStep 1 (FileReviewer): save accident fields (accidentWay, accidentReason, accidentType). GETcapture-requirements/:claimIdStep-aware capture requirements (pre-capture docs phase vs capture-parts phase). - POSTupload-document/:claimIdUpload chassis / engine / metal-plate documents. + POSTupload-document/:claimIdUpload chassis / engine / metal-plate documents; accident_sketch (کروکی) remains optional and does not affect completion. PATCHselect-outer-parts/:claimIdSelect outer (body) damaged parts. PATCHselect-other-parts/:claimIdSelect other (non-body) damaged parts. POSTcapture-part/:claimIdCapture part photos + angles for each selected damaged part. diff --git a/src/Types&Enums/claim-request-management/required-document-type.enum.ts b/src/Types&Enums/claim-request-management/required-document-type.enum.ts index 841ab1a..39ffb3f 100644 --- a/src/Types&Enums/claim-request-management/required-document-type.enum.ts +++ b/src/Types&Enums/claim-request-management/required-document-type.enum.ts @@ -1,4 +1,7 @@ export enum ClaimRequiredDocumentType { + /** Optional police accident sketch (Persian: کروکی). Never gates claim progress. */ + ACCIDENT_SKETCH = "accident_sketch", + // Car green card CAR_GREEN_CARD = "car_green_card", CAR_CERTIFICATE = "car_certificate", @@ -34,4 +37,3 @@ export enum CarAngle { LEFT = "left", RIGHT = "right", } - diff --git a/src/claim-request-management/claim-optional-document.spec.ts b/src/claim-request-management/claim-optional-document.spec.ts new file mode 100644 index 0000000..cae67af --- /dev/null +++ b/src/claim-request-management/claim-optional-document.spec.ts @@ -0,0 +1,45 @@ +import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum"; +import { ClaimRequestManagementService } from "./claim-request-management.service"; + +describe("optional accident sketch", () => { + const service = Object.create( + ClaimRequestManagementService.prototype, + ) as ClaimRequestManagementService; + + it("is excluded from legacy required-document completion", () => { + const requiredTypes = (service as any).getRequiredDocumentTypes({ + blameFile: { type: "THIRD_PARTY" }, + }); + + expect(requiredTypes).not.toContain( + ClaimRequiredDocumentType.ACCIDENT_SKETCH, + ); + }); + + it("does not gate V2 owner-document completion", () => { + const mandatoryKeys = (service as any).requiredDocumentKeysV2(false); + const requiredDocuments = Object.fromEntries( + [...mandatoryKeys, ClaimRequiredDocumentType.CAR_GREEN_CARD].map( + (key) => [key, { uploaded: true }], + ), + ); + + expect( + (service as any).allV2OwnerDocumentsComplete( + { requiredDocuments }, + false, + ), + ).toBe(true); + + requiredDocuments[ClaimRequiredDocumentType.ACCIDENT_SKETCH] = { + uploaded: false, + }; + + expect( + (service as any).allV2OwnerDocumentsComplete( + { requiredDocuments }, + false, + ), + ).toBe(true); + }); +}); diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 435c5e8..5ccb1ab 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -1639,7 +1639,9 @@ export class ClaimRequestManagementService { private getRequiredDocumentTypes( claimRequest: any, ): ClaimRequiredDocumentType[] { - const allTypes = Object.values(ClaimRequiredDocumentType); + const allTypes = Object.values(ClaimRequiredDocumentType).filter( + (type) => type !== ClaimRequiredDocumentType.ACCIDENT_SKETCH, + ); // Check if it's CAR_BODY type const isCarBody = claimRequest.blameFile?.type === "CAR_BODY"; @@ -10332,7 +10334,8 @@ export class ClaimRequestManagementService { if (!catalogByKey.has(item.key)) catalogByKey.set(item.key, item); } - // Required documents: CAR_BODY needs 7 (damaged + green card); THIRD_PARTY needs 13 (add guilty_*) + // Required documents vary by claim type. The accident sketch is offered + // in every flow, but is excluded from every completion calculation. const blameRequest = claimCase.blameRequestId ? await this.blameRequestDbService.findById( claimCase.blameRequestId.toString(), @@ -10340,53 +10343,68 @@ export class ClaimRequestManagementService { : null; const isCarBody = blameRequest?.type === BlameRequestType.CAR_BODY; const requiredDocsDefinition = [ + { + key: ClaimRequiredDocumentType.ACCIDENT_SKETCH, + label_fa: "کروکی", + label_en: "Accident Sketch", + category: "general", + required: false, + }, { key: "damaged_driving_license_front", label_fa: "گواهینامه طرف آسیب‌دیده - جلو", label_en: "Damaged Party License - Front", category: "damaged_party", + required: true, }, { key: "damaged_driving_license_back", label_fa: "گواهینامه طرف آسیب‌دیده - پشت", label_en: "Damaged Party License - Back", category: "damaged_party", + required: true, }, { key: "damaged_chassis_number", label_fa: "شماره شاسی", label_en: "Chassis Number", category: "damaged_party", + required: true, }, { key: "damaged_engine_photo", label_fa: "عکس موتور", label_en: "Engine Photo", category: "damaged_party", + required: true, }, { key: "damaged_car_card_front", label_fa: "کارت خودرو آسیب‌دیده - جلو", label_en: "Damaged Car Card - Front", category: "damaged_party", + required: true, }, { key: "damaged_car_card_back", label_fa: "کارت خودرو آسیب‌دیده - پشت", label_en: "Damaged Car Card - Back", category: "damaged_party", + required: true, }, { key: "damaged_metal_plate", label_fa: "پلاک فلزی آسیب‌دیده", label_en: "Damaged Metal Plate", category: "damaged_party", + required: true, }, { key: "car_green_card", label_fa: "کارت سبز خودرو", label_en: "Car Green Card", category: "damaged_party", + required: true, }, ...(isCarBody ? [] @@ -10396,30 +10414,35 @@ export class ClaimRequestManagementService { label_fa: "گواهینامه طرف مقصر - جلو", label_en: "Guilty Party License - Front", category: "guilty_party", + required: true, }, { key: "guilty_driving_license_back", label_fa: "گواهینامه طرف مقصر - پشت", label_en: "Guilty Party License - Back", category: "guilty_party", + required: true, }, { key: "guilty_car_card_front", label_fa: "کارت خودرو مقصر - جلو", label_en: "Guilty Car Card - Front", category: "guilty_party", + required: true, }, { key: "guilty_car_card_back", label_fa: "کارت خودرو مقصر - پشت", label_en: "Guilty Car Card - Back", category: "guilty_party", + required: true, }, { key: "guilty_metal_plate", label_fa: "پلاک فلزی مقصر", label_en: "Guilty Metal Plate", category: "guilty_party", + required: true, }, ]), ]; @@ -10431,6 +10454,7 @@ export class ClaimRequestManagementService { label_fa: doc.label_fa, label_en: doc.label_en, category: doc.category, + required: doc.required, uploaded: docData?.uploaded || false, preferUploadDuringCapture: isCapturePhaseDamagedPartyDocKey(doc.key), }; @@ -10493,7 +10517,10 @@ export class ClaimRequestManagementService { }); // Calculate progress - const documentsUploaded = requiredDocuments.filter( + const requiredDocumentItems = requiredDocuments.filter( + (d) => d.required, + ); + const documentsUploaded = requiredDocumentItems.filter( (d) => d.uploaded, ).length; const anglesCaptured = carAngles.filter((a) => a.captured).length; @@ -10511,7 +10538,7 @@ export class ClaimRequestManagementService { captureProgress.anglesTotal - captureProgress.anglesCaptured, ); const postCaptureDocsRemaining = requiredDocuments.filter( - (d) => !d.preferUploadDuringCapture && !d.uploaded, + (d) => d.required && !d.preferUploadDuringCapture && !d.uploaded, ).length; const totalRemaining = @@ -10531,7 +10558,7 @@ export class ClaimRequestManagementService { totalRemaining, progress: { documentsUploaded, - documentsTotal: requiredDocuments.length, + documentsTotal: requiredDocumentItems.length, anglesCaptured, anglesTotal: carAngles.length, partsCaptured, @@ -10694,6 +10721,8 @@ export class ClaimRequestManagementService { const step = claimCase.workflow?.currentStep; const isResendUpload = step === ClaimWorkflowStep.USER_EXPERT_RESEND; + const isOptionalDocument = + body.documentKey === ClaimRequiredDocumentType.ACCIDENT_SKETCH; const isCapturePhaseDocUpload = step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES && (isCapturePhaseDamagedPartyDocKey(body.documentKey) || @@ -10724,7 +10753,9 @@ export class ClaimRequestManagementService { } const allowedInitialUploadStep = step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS || - isCapturePhaseDocUpload; + isCapturePhaseDocUpload || + (isOptionalDocument && + step === ClaimWorkflowStep.USER_SUBMISSION_COMPLETE); if (!allowedInitialUploadStep) { throw new BadRequestException( `Invalid workflow step. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES} (capture-phase documents only), or ${ClaimWorkflowStep.USER_EXPERT_RESEND}, but current step is ${claimCase.workflow?.currentStep}`, @@ -10843,7 +10874,7 @@ export class ClaimRequestManagementService { let remaining = 0; let captureStepCompletedOnThisUpload = false; - if (!isResendUpload) { + if (!isResendUpload && !isOptionalDocument) { if (isCapturePhaseDocUpload) { const afterThis = (k: string) => k === body.documentKey || @@ -10981,6 +11012,28 @@ export class ClaimRequestManagementService { } } + if (!isResendUpload && isOptionalDocument) { + allDocumentsUploaded = options?.v3InPersonFlow + ? this.allV3PreCaptureDocumentsComplete( + claimCase, + isCarBodyUpload, + undefined, + options?.skipMetalPlate, + ) + : this.allV2OwnerDocumentsComplete(claimCase, isCarBodyUpload); + remaining = options?.v3InPersonFlow + ? this.countRemainingV3PreCaptureDocuments( + claimCase, + isCarBodyUpload, + undefined, + options?.skipMetalPlate, + ) + : this.countRemainingV2OwnerDocuments( + claimCase, + isCarBodyUpload, + ); + } + // Build the final MongoDB update — all operators explicit and clean const updatePayload: Record = { $set }; @@ -11085,7 +11138,12 @@ export class ClaimRequestManagementService { } // Auto-submit Fanavaran attachments when all documents are uploaded - if (allDocumentsUploaded && !isResendUpload && !options?.v3InPersonFlow) { + if ( + allDocumentsUploaded && + !isOptionalDocument && + !isResendUpload && + !options?.v3InPersonFlow + ) { this.submitFanavaranAttachmentsV2(claimRequestId, resolveFanavaranClientKey()).catch( (err) => this.logger.warn(`Fanavaran attachments auto-submit failed: ${err?.message}`), ); @@ -11125,7 +11183,9 @@ export class ClaimRequestManagementService { }; } - const message = isCapturePhaseDocUpload + const message = isOptionalDocument + ? "Optional accident sketch uploaded successfully." + : isCapturePhaseDocUpload ? captureStepCompletedOnThisUpload ? options?.v3InPersonFlow ? "Capture-phase documents and all damage captures are complete. Upload the walk-around video (car-capture) to finalise." @@ -11148,7 +11208,9 @@ export class ClaimRequestManagementService { documentKey: body.documentKey, fileUrl, allDocumentsUploaded, - currentStep: isCapturePhaseDocUpload + currentStep: isOptionalDocument + ? step || ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS + : isCapturePhaseDocUpload ? captureStepCompletedOnThisUpload ? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS : ClaimWorkflowStep.CAPTURE_PART_DAMAGES @@ -12905,7 +12967,10 @@ export class ClaimRequestManagementService { const preCaptureDocs = base.requiredDocuments.filter( (d) => !d.preferUploadDuringCapture, ); - const remaining = preCaptureDocs.filter((d) => !d.uploaded).length; + const requiredPreCaptureDocs = preCaptureDocs.filter((d) => d.required); + const remaining = requiredPreCaptureDocs.filter( + (d) => !d.uploaded, + ).length; return { ...base, requiredDocuments: preCaptureDocs, @@ -12917,8 +12982,9 @@ export class ClaimRequestManagementService { totalRemaining: remaining, progress: { ...base.progress, - documentsTotal: preCaptureDocs.length, - documentsUploaded: preCaptureDocs.filter((d) => d.uploaded).length, + documentsTotal: requiredPreCaptureDocs.length, + documentsUploaded: requiredPreCaptureDocs.filter((d) => d.uploaded) + .length, partsCaptured: 0, partsTotal: 0, anglesCaptured: 0, @@ -12973,6 +13039,7 @@ export class ClaimRequestManagementService { label_fa: "تصویر نقطه آسیب‌دیده مقصر", label_en: "Guilty Car Damaged Area", category: "guilty_party", + required: true, uploaded: guiltyAreaUploaded, preferUploadDuringCapture: true, }; diff --git a/src/claim-request-management/claim-request-management.v2.controller.ts b/src/claim-request-management/claim-request-management.v2.controller.ts index 40913fa..3317fe8 100644 --- a/src/claim-request-management/claim-request-management.v2.controller.ts +++ b/src/claim-request-management/claim-request-management.v2.controller.ts @@ -801,6 +801,7 @@ Returns status of each item (uploaded/captured or not). **Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim) **Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements): +- accident_sketch (optional; never blocks workflow completion) - damaged_driving_license_front/back - damaged_chassis_number, damaged_engine_photo - damaged_car_card_front/back, damaged_metal_plate @@ -836,6 +837,7 @@ Returns status of each item (uploaded/captured or not). "guilty_car_card_front", "guilty_car_card_back", "guilty_metal_plate", + "accident_sketch", ], example: "damaged_driving_license_front", }, diff --git a/src/claim-request-management/dto/capture-requirements-v2.dto.ts b/src/claim-request-management/dto/capture-requirements-v2.dto.ts index 7f223a8..1b6492f 100644 --- a/src/claim-request-management/dto/capture-requirements-v2.dto.ts +++ b/src/claim-request-management/dto/capture-requirements-v2.dto.ts @@ -42,6 +42,13 @@ export class RequiredDocumentItem { example: true, }) preferUploadDuringCapture?: boolean; + + @ApiProperty({ + description: + 'Whether this document is mandatory for workflow completion. Optional documents are uploadable but never included in remaining/progress counts.', + example: true, + }) + required: boolean; } /** diff --git a/src/claim-request-management/expert-initiated-claim.mirror.controller.ts b/src/claim-request-management/expert-initiated-claim.mirror.controller.ts index e09660a..9e895af 100644 --- a/src/claim-request-management/expert-initiated-claim.mirror.controller.ts +++ b/src/claim-request-management/expert-initiated-claim.mirror.controller.ts @@ -416,6 +416,7 @@ Returns status of each item (uploaded/captured or not). "guilty_car_card_front", "guilty_car_card_back", "guilty_metal_plate", + "accident_sketch", ], example: "damaged_driving_license_front", }, diff --git a/src/claim-request-management/registrar-claim.mirror.controller.ts b/src/claim-request-management/registrar-claim.mirror.controller.ts index 845dde9..ba5dbb6 100644 --- a/src/claim-request-management/registrar-claim.mirror.controller.ts +++ b/src/claim-request-management/registrar-claim.mirror.controller.ts @@ -373,6 +373,7 @@ Returns status of each item (uploaded/captured or not). "guilty_car_card_front", "guilty_car_card_back", "guilty_metal_plate", + "accident_sketch", ], example: "damaged_driving_license_front", },