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 @@
| POST | run-inquiries/:id / run-inquiries-vin/:id | Run plate or VIN inquiry. First call = guilty (+ auto-creates claim). Second call = damaged (THIRD_PARTY only). |
| POST | add-detail-location/:id / add-detail-description/:id / upload-voice/:id | Add location, description, and voice for current party (partyRole param selects FIRST/SECOND). |
| PUT | sign/:id | Upload party signature (partyRole=FIRST then SECOND). After second signature, file is sealed. |
- | POST | upload-document/:claimId | Upload licences / car cards against the auto-created claim. |
+ | POST | upload-document/:claimId | Upload licences / car cards and the optional accident_sketch (کروکی) against the auto-created claim. The sketch never gates completion. |
| GET | capture-requirements/:claimId | Step-aware capture requirements (phases: pre-capture docs vs damaged parts + chassis/engine). |
@@ -498,7 +498,7 @@
| GET | claim-id/:requestId | Get the auto-created claim ID (from FileMaker's guilty-party inquiry). |
| POST | accident-fields/:requestId | Step 1 (FileReviewer): save accident fields (accidentWay, accidentReason, accidentType). |
| GET | capture-requirements/:claimId | Step-aware capture requirements (pre-capture docs phase vs capture-parts phase). |
- | POST | upload-document/:claimId | Upload chassis / engine / metal-plate documents. |
+ | POST | upload-document/:claimId | Upload chassis / engine / metal-plate documents; accident_sketch (کروکی) remains optional and does not affect completion. |
| PATCH | select-outer-parts/:claimId | Select outer (body) damaged parts. |
| PATCH | select-other-parts/:claimId | Select other (non-body) damaged parts. |
| POST | capture-part/:claimId | Capture 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",
},