1
0
forked from Yara724/api

Fixed 3 upload documents for capture part

This commit is contained in:
SepehrYahyaee
2026-05-16 11:16:08 +03:30
parent 2c851725a5
commit 7c76149c95
6 changed files with 339 additions and 91 deletions

View File

@@ -120,16 +120,13 @@ import {
resolvePartCaptureIndex,
} from "src/helpers/outer-damage-parts";
/** Same `requiredDocuments` keys as in getCaptureRequirementsV2; upload allowed during CAPTURE_PART_DAMAGES. */
const CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS = [
"damaged_chassis_number",
"damaged_engine_photo",
"damaged_metal_plate",
] as const;
function isCapturePhaseDamagedPartyDocKey(key: string): boolean {
return (CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS as readonly string[]).includes(key);
}
import {
CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS,
capturePhaseSequenceMessage,
getClaimCaptureProgress,
isCapturePhaseDamagedPartyDocKey,
isClaimCaptureStepComplete,
} from "src/helpers/claim-capture-phase";
@Injectable()
export class ClaimRequestManagementService {
@@ -203,12 +200,6 @@ export class ClaimRequestManagementService {
return !!doc?.uploaded;
}
private capturePhaseDamagedPartyDocsComplete(claimCase: any): boolean {
return CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.every((k) =>
this.isRequiredDocumentUploadedOnClaim(claimCase, k),
);
}
private allV2OwnerDocumentsComplete(
claimCase: any,
isCarBody: boolean,
@@ -4633,15 +4624,31 @@ export class ClaimRequestManagementService {
const anglesCaptured = carAngles.filter(a => a.captured).length;
const partsCaptured = damagedParts.filter(p => p.captured).length;
const totalRemaining =
(requiredDocuments.length - documentsUploaded) +
(carAngles.length - anglesCaptured) +
(damagedParts.length - partsCaptured);
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 =
partsRemaining + anglesRemaining + capturePhaseDocsRemaining;
return {
claimRequestId: claimCase._id.toString(),
publicId: claimCase.publicId,
currentStep: claimCase.workflow?.currentStep || '',
captureSequencePhase: captureProgress.sequencePhase,
captureSequenceHint: capturePhaseSequenceMessage(
captureProgress.sequencePhase,
),
requiredDocuments,
carAngles,
damagedParts,
@@ -4653,6 +4660,8 @@ export class ClaimRequestManagementService {
anglesTotal: carAngles.length,
partsCaptured,
partsTotal: damagedParts.length,
capturePhaseDocsRemaining,
postCaptureDocumentsRemaining: postCaptureDocsRemaining,
},
};
} catch (error) {
@@ -4796,9 +4805,22 @@ export class ClaimRequestManagementService {
if (!isResendUpload) {
if (step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES && !isCapturePhaseDocUpload) {
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 =
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS || isCapturePhaseDocUpload;
if (!allowedInitialUploadStep) {
@@ -4896,31 +4918,14 @@ export class ClaimRequestManagementService {
allDocumentsUploaded = false;
if (remaining === 0) {
// After this upload all 3 capture-phase docs will be on the claim.
// Check angles + parts captures using the in-memory claim media.
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;
const progressAfterDoc = getClaimCaptureProgress(claimCase, {
assumeCapturePhaseDocKey: body.documentKey,
});
if (
anglesCaptured >= 4 &&
partsCaptured >= selectedNorm.length
progressAfterDoc.partsComplete &&
progressAfterDoc.anglesComplete &&
progressAfterDoc.capturePhaseDocsComplete
) {
captureStepCompletedOnThisUpload = true;
updateData["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS;
@@ -5031,8 +5036,12 @@ export class ClaimRequestManagementService {
? captureStepCompletedOnThisUpload
? "Capture-phase documents and all damage captures are complete. Please proceed to upload the remaining required documents."
: remaining > 0
? `Document saved. ${remaining} capture-phase document(s) still required (chassis, engine, metal plate) before you can finish damage capture.`
: "Document saved. All capture-phase documents are uploaded; finish car angles and part photos to proceed."
? `Document saved. ${remaining} capture-phase document(s) still required (chassis, engine, metal plate).`
: capturePhaseSequenceMessage(
getClaimCaptureProgress(claimCase, {
assumeCapturePhaseDocKey: body.documentKey,
}).sequencePhase,
)
: allDocumentsUploaded
? "All documents uploaded successfully. Your claim is now ready for damage expert review."
: `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);
if (body.captureType === "angle" && !angleCanon) {
throw new BadRequestException(
@@ -5269,40 +5291,8 @@ export class ClaimRequestManagementService {
};
}
const hasCapture = (data: any, key: string) =>
data && (data instanceof Map ? data.get(key) : data[key]);
// 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;
const captureProgress = getClaimCaptureProgress(updatedClaim);
const allCapturesComplete = isClaimCaptureStepComplete(updatedClaim);
if (allCapturesComplete) {
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
@@ -5329,16 +5319,21 @@ export class ClaimRequestManagementService {
});
}
const captureDocsRemaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter(
(k) => !this.isRequiredDocumentUploadedOnClaim(updatedClaim, k),
).length;
const partsRemaining = Math.max(
0,
captureProgress.partsTotal - captureProgress.partsCaptured,
);
const anglesRemaining = Math.max(
0,
captureProgress.anglesTotal - captureProgress.anglesCaptured,
);
const remaining =
(4 - anglesCaptured) +
(selectedNormAfter.length - partsCaptured) +
captureDocsRemaining;
partsRemaining +
anglesRemaining +
captureProgress.capturePhaseDocsRemaining;
const message = allCapturesComplete
? 'All captures complete. Please proceed to upload required documents.'
: `${body.captureType === 'angle' ? 'Angle' : 'Part'} captured successfully. ${remaining} items remaining (angles, parts, and capture-phase documents).`;
? "All captures complete. Please proceed to upload the remaining required documents."
: `${body.captureType === "angle" ? "Angle" : "Part"} captured successfully. ${capturePhaseSequenceMessage(captureProgress.sequencePhase)} (${remaining} item(s) left in this step).`;
return {
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) {
throw new ConflictException('A video has already been uploaded for this claim.');
}

View File

@@ -649,7 +649,7 @@ Optional: upload car green card file in the same step.
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({
@@ -828,7 +828,7 @@ Returns status of each item (uploaded/captured or not).
1. **angle**: Car angles (front, back, left, right) - 4 required
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.
`,

View File

@@ -144,6 +144,20 @@ export class GetCaptureRequirementsV2ResponseDto {
})
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({
description: 'List of required documents to upload',
type: [RequiredDocumentItem],
@@ -186,5 +200,7 @@ export class GetCaptureRequirementsV2ResponseDto {
anglesTotal: number;
partsCaptured: number;
partsTotal: number;
capturePhaseDocsRemaining: number;
postCaptureDocumentsRemaining: number;
};
}

View 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");
});
});

View 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.";
}
}