forked from Yara724/api
Fixed 3 upload documents for capture part
This commit is contained in:
@@ -106,6 +106,9 @@
|
|||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node",
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^src/(.*)$": "<rootDir>/$1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,16 +120,13 @@ import {
|
|||||||
resolvePartCaptureIndex,
|
resolvePartCaptureIndex,
|
||||||
} from "src/helpers/outer-damage-parts";
|
} from "src/helpers/outer-damage-parts";
|
||||||
|
|
||||||
/** Same `requiredDocuments` keys as in getCaptureRequirementsV2; upload allowed during CAPTURE_PART_DAMAGES. */
|
import {
|
||||||
const CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS = [
|
CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS,
|
||||||
"damaged_chassis_number",
|
capturePhaseSequenceMessage,
|
||||||
"damaged_engine_photo",
|
getClaimCaptureProgress,
|
||||||
"damaged_metal_plate",
|
isCapturePhaseDamagedPartyDocKey,
|
||||||
] as const;
|
isClaimCaptureStepComplete,
|
||||||
|
} from "src/helpers/claim-capture-phase";
|
||||||
function isCapturePhaseDamagedPartyDocKey(key: string): boolean {
|
|
||||||
return (CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS as readonly string[]).includes(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ClaimRequestManagementService {
|
export class ClaimRequestManagementService {
|
||||||
@@ -203,12 +200,6 @@ export class ClaimRequestManagementService {
|
|||||||
return !!doc?.uploaded;
|
return !!doc?.uploaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private capturePhaseDamagedPartyDocsComplete(claimCase: any): boolean {
|
|
||||||
return CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.every((k) =>
|
|
||||||
this.isRequiredDocumentUploadedOnClaim(claimCase, k),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private allV2OwnerDocumentsComplete(
|
private allV2OwnerDocumentsComplete(
|
||||||
claimCase: any,
|
claimCase: any,
|
||||||
isCarBody: boolean,
|
isCarBody: boolean,
|
||||||
@@ -4633,15 +4624,31 @@ export class ClaimRequestManagementService {
|
|||||||
const anglesCaptured = carAngles.filter(a => a.captured).length;
|
const anglesCaptured = carAngles.filter(a => a.captured).length;
|
||||||
const partsCaptured = damagedParts.filter(p => p.captured).length;
|
const partsCaptured = damagedParts.filter(p => p.captured).length;
|
||||||
|
|
||||||
|
const captureProgress = getClaimCaptureProgress(claimCase);
|
||||||
|
const capturePhaseDocsRemaining = captureProgress.capturePhaseDocsRemaining;
|
||||||
|
const partsRemaining = Math.max(
|
||||||
|
0,
|
||||||
|
captureProgress.partsTotal - captureProgress.partsCaptured,
|
||||||
|
);
|
||||||
|
const anglesRemaining = Math.max(
|
||||||
|
0,
|
||||||
|
captureProgress.anglesTotal - captureProgress.anglesCaptured,
|
||||||
|
);
|
||||||
|
const postCaptureDocsRemaining = requiredDocuments.filter(
|
||||||
|
(d) => !d.preferUploadDuringCapture && !d.uploaded,
|
||||||
|
).length;
|
||||||
|
|
||||||
const totalRemaining =
|
const totalRemaining =
|
||||||
(requiredDocuments.length - documentsUploaded) +
|
partsRemaining + anglesRemaining + capturePhaseDocsRemaining;
|
||||||
(carAngles.length - anglesCaptured) +
|
|
||||||
(damagedParts.length - partsCaptured);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claimRequestId: claimCase._id.toString(),
|
claimRequestId: claimCase._id.toString(),
|
||||||
publicId: claimCase.publicId,
|
publicId: claimCase.publicId,
|
||||||
currentStep: claimCase.workflow?.currentStep || '',
|
currentStep: claimCase.workflow?.currentStep || '',
|
||||||
|
captureSequencePhase: captureProgress.sequencePhase,
|
||||||
|
captureSequenceHint: capturePhaseSequenceMessage(
|
||||||
|
captureProgress.sequencePhase,
|
||||||
|
),
|
||||||
requiredDocuments,
|
requiredDocuments,
|
||||||
carAngles,
|
carAngles,
|
||||||
damagedParts,
|
damagedParts,
|
||||||
@@ -4653,6 +4660,8 @@ export class ClaimRequestManagementService {
|
|||||||
anglesTotal: carAngles.length,
|
anglesTotal: carAngles.length,
|
||||||
partsCaptured,
|
partsCaptured,
|
||||||
partsTotal: damagedParts.length,
|
partsTotal: damagedParts.length,
|
||||||
|
capturePhaseDocsRemaining,
|
||||||
|
postCaptureDocumentsRemaining: postCaptureDocsRemaining,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -4796,9 +4805,22 @@ export class ClaimRequestManagementService {
|
|||||||
if (!isResendUpload) {
|
if (!isResendUpload) {
|
||||||
if (step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES && !isCapturePhaseDocUpload) {
|
if (step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES && !isCapturePhaseDocUpload) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`During ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES} only chassis, engine, and damaged metal plate photos may be uploaded here (${CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.join(", ")}).`,
|
`During ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES} only chassis, engine, and damaged metal plate photos may be uploaded here (${CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.join(", ")}), and only after all damaged parts and car angles are captured.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isCapturePhaseDocUpload) {
|
||||||
|
const captureProgress = getClaimCaptureProgress(claimCase);
|
||||||
|
if (!captureProgress.partsComplete) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Upload photos for all selected damaged parts before uploading chassis, engine, or metal plate photos.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!captureProgress.anglesComplete) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Capture all four car angles (front, back, left, right) before uploading chassis, engine, or metal plate photos.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
const allowedInitialUploadStep =
|
const allowedInitialUploadStep =
|
||||||
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS || isCapturePhaseDocUpload;
|
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS || isCapturePhaseDocUpload;
|
||||||
if (!allowedInitialUploadStep) {
|
if (!allowedInitialUploadStep) {
|
||||||
@@ -4896,31 +4918,14 @@ export class ClaimRequestManagementService {
|
|||||||
allDocumentsUploaded = false;
|
allDocumentsUploaded = false;
|
||||||
|
|
||||||
if (remaining === 0) {
|
if (remaining === 0) {
|
||||||
// After this upload all 3 capture-phase docs will be on the claim.
|
const progressAfterDoc = getClaimCaptureProgress(claimCase, {
|
||||||
// Check angles + parts captures using the in-memory claim media.
|
assumeCapturePhaseDocKey: body.documentKey,
|
||||||
const carAnglesData = (claimCase.media as any)?.carAngles;
|
});
|
||||||
const damagedPartsData = (claimCase.media as any)?.damagedParts;
|
|
||||||
const selectedNorm = normalizeDamageSelectedParts(
|
|
||||||
claimCase.damage?.selectedParts,
|
|
||||||
claimCase.vehicle?.carType as ClaimVehicleTypeV2,
|
|
||||||
(claimCase.damage as any)?.selectedOuterParts,
|
|
||||||
);
|
|
||||||
const anglesKeys = ["front", "back", "left", "right"];
|
|
||||||
const anglesCaptured = anglesKeys.filter((k) =>
|
|
||||||
hasClaimCarAngleCapture(
|
|
||||||
carAnglesData,
|
|
||||||
damagedPartsData,
|
|
||||||
k as ClaimCarAngleKey,
|
|
||||||
),
|
|
||||||
).length;
|
|
||||||
const partsCaptured = selectedNorm.filter((sp) => {
|
|
||||||
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
|
|
||||||
return hasDamagedPartCapture(damagedPartsData, ck, selectedNorm);
|
|
||||||
}).length;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
anglesCaptured >= 4 &&
|
progressAfterDoc.partsComplete &&
|
||||||
partsCaptured >= selectedNorm.length
|
progressAfterDoc.anglesComplete &&
|
||||||
|
progressAfterDoc.capturePhaseDocsComplete
|
||||||
) {
|
) {
|
||||||
captureStepCompletedOnThisUpload = true;
|
captureStepCompletedOnThisUpload = true;
|
||||||
updateData["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS;
|
updateData["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS;
|
||||||
@@ -5031,8 +5036,12 @@ export class ClaimRequestManagementService {
|
|||||||
? captureStepCompletedOnThisUpload
|
? captureStepCompletedOnThisUpload
|
||||||
? "Capture-phase documents and all damage captures are complete. Please proceed to upload the remaining required documents."
|
? "Capture-phase documents and all damage captures are complete. Please proceed to upload the remaining required documents."
|
||||||
: remaining > 0
|
: remaining > 0
|
||||||
? `Document saved. ${remaining} capture-phase document(s) still required (chassis, engine, metal plate) before you can finish damage capture.`
|
? `Document saved. ${remaining} capture-phase document(s) still required (chassis, engine, metal plate).`
|
||||||
: "Document saved. All capture-phase documents are uploaded; finish car angles and part photos to proceed."
|
: capturePhaseSequenceMessage(
|
||||||
|
getClaimCaptureProgress(claimCase, {
|
||||||
|
assumeCapturePhaseDocKey: body.documentKey,
|
||||||
|
}).sequencePhase,
|
||||||
|
)
|
||||||
: allDocumentsUploaded
|
: allDocumentsUploaded
|
||||||
? "All documents uploaded successfully. Your claim is now ready for damage expert review."
|
? "All documents uploaded successfully. Your claim is now ready for damage expert review."
|
||||||
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||||
@@ -5114,6 +5123,19 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isResendCapture) {
|
||||||
|
const captureProgressBefore = getClaimCaptureProgress(claimCase);
|
||||||
|
const willBeAngle =
|
||||||
|
body.captureType === "angle" ||
|
||||||
|
(body.captureType === "part" &&
|
||||||
|
canonicalizeClaimCarAngleKey(body.captureKey) !== null);
|
||||||
|
if (willBeAngle && !captureProgressBefore.partsComplete) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Upload photos for all selected damaged parts before capturing car angles.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const angleCanon = canonicalizeClaimCarAngleKey(body.captureKey);
|
const angleCanon = canonicalizeClaimCarAngleKey(body.captureKey);
|
||||||
if (body.captureType === "angle" && !angleCanon) {
|
if (body.captureType === "angle" && !angleCanon) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -5269,40 +5291,8 @@ export class ClaimRequestManagementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasCapture = (data: any, key: string) =>
|
const captureProgress = getClaimCaptureProgress(updatedClaim);
|
||||||
data && (data instanceof Map ? data.get(key) : data[key]);
|
const allCapturesComplete = isClaimCaptureStepComplete(updatedClaim);
|
||||||
|
|
||||||
// Use same logic as getCaptureRequirementsV2 to determine completion
|
|
||||||
const anglesKeys = ["front", "back", "left", "right"];
|
|
||||||
const carAnglesData = updatedClaim?.media?.carAngles as any;
|
|
||||||
const damagedPartsData = updatedClaim?.media?.damagedParts as any;
|
|
||||||
const selectedNormAfter = normalizeDamageSelectedParts(
|
|
||||||
updatedClaim?.damage?.selectedParts,
|
|
||||||
updatedClaim?.vehicle?.carType as ClaimVehicleTypeV2,
|
|
||||||
(updatedClaim?.damage as any)?.selectedOuterParts,
|
|
||||||
);
|
|
||||||
|
|
||||||
const anglesCaptured = anglesKeys.filter((k) =>
|
|
||||||
hasClaimCarAngleCapture(
|
|
||||||
carAnglesData,
|
|
||||||
damagedPartsData,
|
|
||||||
k as ClaimCarAngleKey,
|
|
||||||
),
|
|
||||||
).length;
|
|
||||||
const partsCaptured = selectedNormAfter.filter((sp) => {
|
|
||||||
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
|
|
||||||
return hasDamagedPartCapture(
|
|
||||||
damagedPartsData,
|
|
||||||
ck,
|
|
||||||
selectedNormAfter,
|
|
||||||
);
|
|
||||||
}).length;
|
|
||||||
|
|
||||||
const capturePhaseDocsDone = this.capturePhaseDamagedPartyDocsComplete(updatedClaim);
|
|
||||||
const allCapturesComplete =
|
|
||||||
anglesCaptured >= 4 &&
|
|
||||||
partsCaptured >= selectedNormAfter.length &&
|
|
||||||
capturePhaseDocsDone;
|
|
||||||
|
|
||||||
if (allCapturesComplete) {
|
if (allCapturesComplete) {
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
@@ -5329,16 +5319,21 @@ export class ClaimRequestManagementService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const captureDocsRemaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter(
|
const partsRemaining = Math.max(
|
||||||
(k) => !this.isRequiredDocumentUploadedOnClaim(updatedClaim, k),
|
0,
|
||||||
).length;
|
captureProgress.partsTotal - captureProgress.partsCaptured,
|
||||||
|
);
|
||||||
|
const anglesRemaining = Math.max(
|
||||||
|
0,
|
||||||
|
captureProgress.anglesTotal - captureProgress.anglesCaptured,
|
||||||
|
);
|
||||||
const remaining =
|
const remaining =
|
||||||
(4 - anglesCaptured) +
|
partsRemaining +
|
||||||
(selectedNormAfter.length - partsCaptured) +
|
anglesRemaining +
|
||||||
captureDocsRemaining;
|
captureProgress.capturePhaseDocsRemaining;
|
||||||
const message = allCapturesComplete
|
const message = allCapturesComplete
|
||||||
? 'All captures complete. Please proceed to upload required documents.'
|
? "All captures complete. Please proceed to upload the remaining required documents."
|
||||||
: `${body.captureType === 'angle' ? 'Angle' : 'Part'} captured successfully. ${remaining} items remaining (angles, parts, and capture-phase documents).`;
|
: `${body.captureType === "angle" ? "Angle" : "Part"} captured successfully. ${capturePhaseSequenceMessage(captureProgress.sequencePhase)} (${remaining} item(s) left in this step).`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claimRequestId: claimCase._id.toString(),
|
claimRequestId: claimCase._id.toString(),
|
||||||
@@ -5393,6 +5388,18 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const captureProgress = getClaimCaptureProgress(claimCase);
|
||||||
|
if (!captureProgress.partsComplete) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Upload photos for all selected damaged parts before uploading the walk-around video.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!captureProgress.anglesComplete) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Capture all four car angles (front, back, left, right) before uploading the walk-around video.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (claimCase.media?.videoCaptureId) {
|
if (claimCase.media?.videoCaptureId) {
|
||||||
throw new ConflictException('A video has already been uploaded for this claim.');
|
throw new ConflictException('A video has already been uploaded for this claim.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -649,7 +649,7 @@ Optional: upload car green card file in the same step.
|
|||||||
|
|
||||||
Returns status of each item (uploaded/captured or not).
|
Returns status of each item (uploaded/captured or not).
|
||||||
|
|
||||||
**V2 order:** During \`CAPTURE_PART_DAMAGES\`, complete angles, part photos, walk-around video, and the three \`preferUploadDuringCapture\` documents (same upload-document API). Then complete the remaining documents in \`UPLOAD_REQUIRED_DOCUMENTS\`.
|
**V2 order (enforced by API):** During \`CAPTURE_PART_DAMAGES\`, (1) all damaged-part photos, (2) four car angles, (3) chassis/engine/metal-plate via upload-document, then walk-around video. Remaining documents in \`UPLOAD_REQUIRED_DOCUMENTS\`. Use \`captureSequencePhase\` / \`captureSequenceHint\` in the response.
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
@@ -828,7 +828,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
1. **angle**: Car angles (front, back, left, right) - 4 required
|
1. **angle**: Car angles (front, back, left, right) - 4 required
|
||||||
2. **part**: Damaged parts based on selectedParts from Step 2
|
2. **part**: Damaged parts based on selectedParts from Step 2
|
||||||
|
|
||||||
**When all captures are complete:** Workflow moves to UPLOAD_REQUIRED_DOCUMENTS (Step 5).
|
**When all captures are complete (parts, angles, capture-phase docs):** Workflow moves to UPLOAD_REQUIRED_DOCUMENTS (Step 5). Angles are blocked until all parts are captured; capture-phase documents are blocked until all angles are captured.
|
||||||
|
|
||||||
**Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to capture photos on behalf of the damaged party.
|
**Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to capture photos on behalf of the damaged party.
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -144,6 +144,20 @@ export class GetCaptureRequirementsV2ResponseDto {
|
|||||||
})
|
})
|
||||||
currentStep: string;
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description:
|
||||||
|
'Ordered capture phase during CAPTURE_PART_DAMAGES: parts → angles → capture_phase_documents',
|
||||||
|
example: 'angles',
|
||||||
|
enum: ['parts', 'angles', 'capture_phase_documents', 'complete'],
|
||||||
|
})
|
||||||
|
captureSequencePhase: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Human-readable hint for what the user should do next in the capture step',
|
||||||
|
example: 'Capture all four car angles (front, back, left, right) next.',
|
||||||
|
})
|
||||||
|
captureSequenceHint: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'List of required documents to upload',
|
description: 'List of required documents to upload',
|
||||||
type: [RequiredDocumentItem],
|
type: [RequiredDocumentItem],
|
||||||
@@ -186,5 +200,7 @@ export class GetCaptureRequirementsV2ResponseDto {
|
|||||||
anglesTotal: number;
|
anglesTotal: number;
|
||||||
partsCaptured: number;
|
partsCaptured: number;
|
||||||
partsTotal: number;
|
partsTotal: number;
|
||||||
|
capturePhaseDocsRemaining: number;
|
||||||
|
postCaptureDocumentsRemaining: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
95
src/helpers/claim-capture-phase.spec.ts
Normal file
95
src/helpers/claim-capture-phase.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import {
|
||||||
|
getClaimCaptureProgress,
|
||||||
|
isClaimCaptureStepComplete,
|
||||||
|
} from "./claim-capture-phase";
|
||||||
|
|
||||||
|
describe("claim-capture-phase", () => {
|
||||||
|
const baseClaim = {
|
||||||
|
vehicle: { carType: "sedan" },
|
||||||
|
damage: {
|
||||||
|
selectedParts: [{ name: "hood", side: "front", label_fa: "کاپوت" }],
|
||||||
|
},
|
||||||
|
media: { carAngles: {}, damagedParts: [] },
|
||||||
|
requiredDocuments: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("starts in parts phase", () => {
|
||||||
|
const p = getClaimCaptureProgress(baseClaim);
|
||||||
|
expect(p.sequencePhase).toBe("parts");
|
||||||
|
expect(p.partsComplete).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves to angles after all parts", () => {
|
||||||
|
const claim = {
|
||||||
|
...baseClaim,
|
||||||
|
media: {
|
||||||
|
carAngles: {},
|
||||||
|
damagedParts: [
|
||||||
|
{
|
||||||
|
name: "hood",
|
||||||
|
side: "front",
|
||||||
|
label_fa: "کاپوت",
|
||||||
|
path: "/x.jpg",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const p = getClaimCaptureProgress(claim);
|
||||||
|
expect(p.sequencePhase).toBe("angles");
|
||||||
|
expect(p.partsComplete).toBe(true);
|
||||||
|
expect(p.anglesComplete).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not complete capture step with only capture-phase docs uploaded", () => {
|
||||||
|
const claim = {
|
||||||
|
...baseClaim,
|
||||||
|
media: {
|
||||||
|
carAngles: {},
|
||||||
|
damagedParts: [
|
||||||
|
{
|
||||||
|
name: "hood",
|
||||||
|
side: "front",
|
||||||
|
label_fa: "کاپوت",
|
||||||
|
path: "/x.jpg",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
requiredDocuments: {
|
||||||
|
damaged_chassis_number: { uploaded: true },
|
||||||
|
damaged_engine_photo: { uploaded: true },
|
||||||
|
damaged_metal_plate: { uploaded: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(isClaimCaptureStepComplete(claim)).toBe(false);
|
||||||
|
expect(getClaimCaptureProgress(claim).sequencePhase).toBe("angles");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is complete only when parts, angles, and capture-phase docs are done", () => {
|
||||||
|
const claim = {
|
||||||
|
...baseClaim,
|
||||||
|
media: {
|
||||||
|
carAngles: {
|
||||||
|
front: { path: "/f.jpg" },
|
||||||
|
back: { path: "/b.jpg" },
|
||||||
|
left: { path: "/l.jpg" },
|
||||||
|
right: { path: "/r.jpg" },
|
||||||
|
},
|
||||||
|
damagedParts: [
|
||||||
|
{
|
||||||
|
name: "hood",
|
||||||
|
side: "front",
|
||||||
|
label_fa: "کاپوت",
|
||||||
|
path: "/x.jpg",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
requiredDocuments: {
|
||||||
|
damaged_chassis_number: { uploaded: true },
|
||||||
|
damaged_engine_photo: { uploaded: true },
|
||||||
|
damaged_metal_plate: { uploaded: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(isClaimCaptureStepComplete(claim)).toBe(true);
|
||||||
|
expect(getClaimCaptureProgress(claim).sequencePhase).toBe("complete");
|
||||||
|
});
|
||||||
|
});
|
||||||
127
src/helpers/claim-capture-phase.ts
Normal file
127
src/helpers/claim-capture-phase.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
CLAIM_CAR_ANGLE_KEYS,
|
||||||
|
hasClaimCarAngleCapture,
|
||||||
|
hasDamagedPartCapture,
|
||||||
|
} from "src/helpers/claim-car-angle-media";
|
||||||
|
import {
|
||||||
|
catalogLikeKeyFromPart,
|
||||||
|
normalizeDamageSelectedParts,
|
||||||
|
} from "src/helpers/outer-damage-parts";
|
||||||
|
import type { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||||
|
|
||||||
|
/** Upload during CAPTURE_PART_DAMAGES (after parts + angles). */
|
||||||
|
export const CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS = [
|
||||||
|
"damaged_chassis_number",
|
||||||
|
"damaged_engine_photo",
|
||||||
|
"damaged_metal_plate",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type CapturePhaseSequence =
|
||||||
|
| "parts"
|
||||||
|
| "angles"
|
||||||
|
| "capture_phase_documents"
|
||||||
|
| "complete";
|
||||||
|
|
||||||
|
export interface ClaimCaptureProgress {
|
||||||
|
partsCaptured: number;
|
||||||
|
partsTotal: number;
|
||||||
|
partsComplete: boolean;
|
||||||
|
anglesCaptured: number;
|
||||||
|
anglesTotal: number;
|
||||||
|
anglesComplete: boolean;
|
||||||
|
capturePhaseDocsComplete: boolean;
|
||||||
|
capturePhaseDocsRemaining: number;
|
||||||
|
sequencePhase: CapturePhaseSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCapturePhaseDamagedPartyDocKey(key: string): boolean {
|
||||||
|
return (CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS as readonly string[]).includes(
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRequiredDocumentUploadedOnClaim(
|
||||||
|
claimCase: any,
|
||||||
|
key: string,
|
||||||
|
): boolean {
|
||||||
|
const doc =
|
||||||
|
(claimCase?.requiredDocuments as any)?.get?.(key) ??
|
||||||
|
claimCase?.requiredDocuments?.[key];
|
||||||
|
return !!doc?.uploaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClaimCaptureProgress(
|
||||||
|
claimCase: any,
|
||||||
|
options?: { assumeCapturePhaseDocKey?: string },
|
||||||
|
): ClaimCaptureProgress {
|
||||||
|
const carType = claimCase?.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||||
|
const selectedNorm = normalizeDamageSelectedParts(
|
||||||
|
claimCase?.damage?.selectedParts,
|
||||||
|
carType,
|
||||||
|
claimCase?.damage?.selectedOuterParts,
|
||||||
|
);
|
||||||
|
const carAnglesData = claimCase?.media?.carAngles;
|
||||||
|
const damagedPartsData = claimCase?.media?.damagedParts;
|
||||||
|
|
||||||
|
const anglesCaptured = CLAIM_CAR_ANGLE_KEYS.filter((k) =>
|
||||||
|
hasClaimCarAngleCapture(carAnglesData, damagedPartsData, k),
|
||||||
|
).length;
|
||||||
|
const partsCaptured = selectedNorm.filter((sp) => {
|
||||||
|
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
|
||||||
|
return hasDamagedPartCapture(damagedPartsData, ck, selectedNorm);
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const partsTotal = selectedNorm.length;
|
||||||
|
const partsComplete = partsTotal === 0 || partsCaptured >= partsTotal;
|
||||||
|
const anglesTotal = CLAIM_CAR_ANGLE_KEYS.length;
|
||||||
|
const anglesComplete = anglesCaptured >= anglesTotal;
|
||||||
|
|
||||||
|
const capturePhaseDocsRemaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter(
|
||||||
|
(k) => {
|
||||||
|
if (k === options?.assumeCapturePhaseDocKey) return false;
|
||||||
|
return !isRequiredDocumentUploadedOnClaim(claimCase, k);
|
||||||
|
},
|
||||||
|
).length;
|
||||||
|
const capturePhaseDocsComplete = capturePhaseDocsRemaining === 0;
|
||||||
|
|
||||||
|
let sequencePhase: CapturePhaseSequence = "parts";
|
||||||
|
if (partsComplete && !anglesComplete) {
|
||||||
|
sequencePhase = "angles";
|
||||||
|
} else if (partsComplete && anglesComplete && !capturePhaseDocsComplete) {
|
||||||
|
sequencePhase = "capture_phase_documents";
|
||||||
|
} else if (partsComplete && anglesComplete && capturePhaseDocsComplete) {
|
||||||
|
sequencePhase = "complete";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
partsCaptured,
|
||||||
|
partsTotal,
|
||||||
|
partsComplete,
|
||||||
|
anglesCaptured,
|
||||||
|
anglesTotal,
|
||||||
|
anglesComplete,
|
||||||
|
capturePhaseDocsComplete,
|
||||||
|
capturePhaseDocsRemaining,
|
||||||
|
sequencePhase,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isClaimCaptureStepComplete(claimCase: any): boolean {
|
||||||
|
const p = getClaimCaptureProgress(claimCase);
|
||||||
|
return (
|
||||||
|
p.partsComplete && p.anglesComplete && p.capturePhaseDocsComplete
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function capturePhaseSequenceMessage(phase: CapturePhaseSequence): string {
|
||||||
|
switch (phase) {
|
||||||
|
case "parts":
|
||||||
|
return "Upload photos for all selected damaged parts first.";
|
||||||
|
case "angles":
|
||||||
|
return "Capture all four car angles (front, back, left, right) next.";
|
||||||
|
case "capture_phase_documents":
|
||||||
|
return "Upload chassis number, engine photo, and damaged metal plate photos last.";
|
||||||
|
default:
|
||||||
|
return "Damage capture is complete.";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user