forked from Yara724/api
YARA-877
This commit is contained in:
@@ -113,12 +113,24 @@ import {
|
||||
catalogItemToSelectedPart,
|
||||
catalogLikeKeyFromPart,
|
||||
coerceDamagedPartsMediaToArray,
|
||||
type DamageSelectedPartV2,
|
||||
migrateExpertReplyCarPartDamageToUnified,
|
||||
normalizeDamageSelectedParts,
|
||||
partIdentityKey,
|
||||
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);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ClaimRequestManagementService {
|
||||
private readonly logger = new Logger(ClaimRequestManagementService.name);
|
||||
@@ -163,6 +175,79 @@ export class ClaimRequestManagementService {
|
||||
private readonly sandHubService: SandHubService,
|
||||
) {}
|
||||
|
||||
private requiredDocumentKeysV2(isCarBody: boolean): string[] {
|
||||
const damagedPartyKeys = [
|
||||
"damaged_driving_license_front",
|
||||
"damaged_driving_license_back",
|
||||
"damaged_chassis_number",
|
||||
"damaged_engine_photo",
|
||||
"damaged_car_card_front",
|
||||
"damaged_car_card_back",
|
||||
"damaged_metal_plate",
|
||||
];
|
||||
if (isCarBody) return damagedPartyKeys;
|
||||
return [
|
||||
...damagedPartyKeys,
|
||||
"guilty_driving_license_front",
|
||||
"guilty_driving_license_back",
|
||||
"guilty_car_card_front",
|
||||
"guilty_car_card_back",
|
||||
"guilty_metal_plate",
|
||||
];
|
||||
}
|
||||
|
||||
private isRequiredDocumentUploadedOnClaim(claimCase: any, key: string): boolean {
|
||||
const doc =
|
||||
(claimCase.requiredDocuments as any)?.get?.(key) ??
|
||||
(claimCase.requiredDocuments as any)?.[key];
|
||||
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,
|
||||
assumeUploadedKey?: string,
|
||||
): boolean {
|
||||
for (const k of this.requiredDocumentKeysV2(isCarBody)) {
|
||||
const ok =
|
||||
k === assumeUploadedKey ? true : this.isRequiredDocumentUploadedOnClaim(claimCase, k);
|
||||
if (!ok) return false;
|
||||
}
|
||||
if (isCarBody) {
|
||||
const g = ClaimRequiredDocumentType.CAR_GREEN_CARD;
|
||||
const okGreen =
|
||||
assumeUploadedKey === g ? true : this.isRequiredDocumentUploadedOnClaim(claimCase, g);
|
||||
if (!okGreen) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private countRemainingV2OwnerDocuments(
|
||||
claimCase: any,
|
||||
isCarBody: boolean,
|
||||
assumeUploadedKey?: string,
|
||||
): number {
|
||||
let n = 0;
|
||||
for (const k of this.requiredDocumentKeysV2(isCarBody)) {
|
||||
const ok =
|
||||
k === assumeUploadedKey ? true : this.isRequiredDocumentUploadedOnClaim(claimCase, k);
|
||||
if (!ok) n++;
|
||||
}
|
||||
if (isCarBody) {
|
||||
const g = ClaimRequiredDocumentType.CAR_GREEN_CARD;
|
||||
const okGreen =
|
||||
assumeUploadedKey === g ? true : this.isRequiredDocumentUploadedOnClaim(claimCase, g);
|
||||
if (!okGreen) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private parsePlateFromCompactString(
|
||||
plateId: string | undefined,
|
||||
): { leftDigits: number; centerAlphabet: string; centerDigits: number; ir: number } | null {
|
||||
@@ -219,7 +304,56 @@ export class ClaimRequestManagementService {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
getOuterPartsCatalogV2(carType?: ClaimVehicleTypeV2): OuterPartCatalogItem[] {
|
||||
/**
|
||||
* Outer-parts catalog returned to clients (user app + expert panel).
|
||||
*
|
||||
* The shape intentionally mirrors how items are persisted under
|
||||
* `damage.selectedParts` so the front-end can match catalog rows to the
|
||||
* stored selection by `id` / `catalogKey` without renaming fields:
|
||||
*
|
||||
* { id, name, side, label_fa, catalogKey, carType }
|
||||
*
|
||||
* - `name` is side-agnostic (`backWheel`, not `left_backWheel`).
|
||||
* - `label_fa` is disambiguated with the side in parentheses when multiple
|
||||
* catalog rows share the same Farsi title (e.g. `چرخ عقب (چپ)`).
|
||||
* - `catalogKey` keeps the original full key (`left_backWheel`) for clients
|
||||
* that already address parts by it.
|
||||
*/
|
||||
getOuterPartsCatalogV2(
|
||||
carType?: ClaimVehicleTypeV2,
|
||||
): (DamageSelectedPartV2 & { carType: ClaimVehicleTypeV2 })[] {
|
||||
const buildForType = (
|
||||
type: ClaimVehicleTypeV2,
|
||||
items: OuterPartCatalogItem[],
|
||||
): (DamageSelectedPartV2 & { carType: ClaimVehicleTypeV2 })[] =>
|
||||
items.map((p) => ({
|
||||
...catalogItemToSelectedPart(p, items),
|
||||
carType: type,
|
||||
}));
|
||||
|
||||
if (carType) {
|
||||
const items = OUTER_PARTS_BY_CAR_TYPE[carType] || [];
|
||||
return buildForType(carType, items);
|
||||
}
|
||||
|
||||
const out: (DamageSelectedPartV2 & { carType: ClaimVehicleTypeV2 })[] = [];
|
||||
for (const [type, items] of Object.entries(OUTER_PARTS_BY_CAR_TYPE)) {
|
||||
out.push(...buildForType(type as ClaimVehicleTypeV2, items));
|
||||
}
|
||||
return out.sort((a, b) => {
|
||||
if (a.carType === b.carType) return (a.id ?? 0) - (b.id ?? 0);
|
||||
return String(a.carType).localeCompare(String(b.carType));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal raw catalog (kept in the original `OuterPartCatalogItem` shape so
|
||||
* existing internal code that indexes by `key`/`titleFa` keeps working).
|
||||
* Public API consumers should use `getOuterPartsCatalogV2` instead.
|
||||
*/
|
||||
private getOuterPartsRawCatalog(
|
||||
carType?: ClaimVehicleTypeV2,
|
||||
): OuterPartCatalogItem[] {
|
||||
if (carType) {
|
||||
return (OUTER_PARTS_BY_CAR_TYPE[carType] || []).map((p) => ({
|
||||
...p,
|
||||
@@ -233,10 +367,7 @@ export class ClaimRequestManagementService {
|
||||
out.push({ ...p, carType: t });
|
||||
}
|
||||
}
|
||||
return out.sort((a, b) => {
|
||||
if (a.carType === b.carType) return a.id - b.id;
|
||||
return String(a.carType).localeCompare(String(b.carType));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
private userDamageDetail(blRequest: RequestManagementModel) {
|
||||
@@ -4396,12 +4527,14 @@ export class ClaimRequestManagementService {
|
||||
throw new ForbiddenException('Only the claim owner can view capture requirements');
|
||||
}
|
||||
|
||||
// Build car-type aware outer-parts lookup (complete source: static catalog)
|
||||
// Build car-type aware outer-parts lookup (complete source: static catalog).
|
||||
// Uses the raw catalog shape (`{id, key, titleFa, side, ...}`) because the
|
||||
// map below is keyed on the full `key` (e.g. `left_backfender`).
|
||||
const selectedCarType = claimCase.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const catalogForType =
|
||||
selectedCarType && OUTER_PARTS_BY_CAR_TYPE[selectedCarType]
|
||||
? OUTER_PARTS_BY_CAR_TYPE[selectedCarType]
|
||||
: this.getOuterPartsCatalogV2();
|
||||
: this.getOuterPartsRawCatalog();
|
||||
const catalogByKey = new Map<string, OuterPartCatalogItem>();
|
||||
for (const item of catalogForType) {
|
||||
if (!catalogByKey.has(item.key)) catalogByKey.set(item.key, item);
|
||||
@@ -4437,6 +4570,7 @@ export class ClaimRequestManagementService {
|
||||
label_en: doc.label_en,
|
||||
category: doc.category,
|
||||
uploaded: docData?.uploaded || false,
|
||||
preferUploadDuringCapture: isCapturePhaseDamagedPartyDocKey(doc.key),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -4655,10 +4789,23 @@ export class ClaimRequestManagementService {
|
||||
|
||||
const step = claimCase.workflow?.currentStep;
|
||||
const isResendUpload = step === ClaimWorkflowStep.USER_EXPERT_RESEND;
|
||||
if (!isResendUpload && step !== ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS) {
|
||||
throw new BadRequestException(
|
||||
`Invalid workflow step. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS} or ${ClaimWorkflowStep.USER_EXPERT_RESEND}, but current step is ${claimCase.workflow?.currentStep}`,
|
||||
);
|
||||
const isCapturePhaseDocUpload =
|
||||
step === ClaimWorkflowStep.CAPTURE_PART_DAMAGES &&
|
||||
isCapturePhaseDamagedPartyDocKey(body.documentKey);
|
||||
|
||||
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(", ")}).`,
|
||||
);
|
||||
}
|
||||
const allowedInitialUploadStep =
|
||||
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS || isCapturePhaseDocUpload;
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isResendUpload) {
|
||||
if (claimCase.status !== ClaimCaseStatus.WAITING_FOR_USER_RESEND) {
|
||||
@@ -4734,54 +4881,63 @@ export class ClaimRequestManagementService {
|
||||
let remaining = 0;
|
||||
|
||||
if (!isResendUpload) {
|
||||
// Check if all documents are uploaded (8 for CAR_BODY, 13 for THIRD_PARTY)
|
||||
const totalDocsRequired = isCarBodyUpload ? 8 : 13;
|
||||
const currentDocs = claimCase.requiredDocuments || new Map();
|
||||
const uploadedCount =
|
||||
(currentDocs instanceof Map ? currentDocs.size : Object.keys(currentDocs).length) + 1;
|
||||
allDocumentsUploaded = uploadedCount >= totalDocsRequired;
|
||||
remaining = totalDocsRequired - uploadedCount;
|
||||
if (isCapturePhaseDocUpload) {
|
||||
const afterThis = (k: string) =>
|
||||
k === body.documentKey || this.isRequiredDocumentUploadedOnClaim(claimCase, k);
|
||||
remaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter((k) => !afterThis(k)).length;
|
||||
allDocumentsUploaded = false;
|
||||
} else {
|
||||
allDocumentsUploaded = this.allV2OwnerDocumentsComplete(
|
||||
claimCase,
|
||||
isCarBodyUpload,
|
||||
body.documentKey,
|
||||
);
|
||||
remaining = this.countRemainingV2OwnerDocuments(
|
||||
claimCase,
|
||||
isCarBodyUpload,
|
||||
body.documentKey,
|
||||
);
|
||||
|
||||
// If all documents uploaded, user flow complete (captures were done earlier in v2)
|
||||
if (allDocumentsUploaded) {
|
||||
updateData["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT;
|
||||
updateData["claimStatus"] = ClaimStatus.PENDING;
|
||||
updateData["workflow.currentStep"] =
|
||||
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||
updateData["workflow.nextStep"] =
|
||||
ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||
updateData.$push.history = [
|
||||
updateData.$push.history,
|
||||
{
|
||||
type: "STEP_COMPLETED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || "User",
|
||||
actorType: "user",
|
||||
if (allDocumentsUploaded) {
|
||||
updateData["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT;
|
||||
updateData["claimStatus"] = ClaimStatus.PENDING;
|
||||
updateData["workflow.currentStep"] =
|
||||
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||
updateData["workflow.nextStep"] =
|
||||
ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||
updateData.$push.history = [
|
||||
updateData.$push.history,
|
||||
{
|
||||
type: "STEP_COMPLETED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || "User",
|
||||
actorType: "user",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
description: "All required documents uploaded",
|
||||
},
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
description: "All required documents uploaded",
|
||||
{
|
||||
type: "STEP_COMPLETED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || "User",
|
||||
actorType: "user",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
description:
|
||||
"User submission complete. Claim ready for damage expert review.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "STEP_COMPLETED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || "User",
|
||||
actorType: "user",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
description:
|
||||
"User submission complete. Claim ready for damage expert review.",
|
||||
},
|
||||
},
|
||||
];
|
||||
updateData.$push["workflow.completedSteps"] =
|
||||
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
|
||||
];
|
||||
updateData.$push["workflow.completedSteps"] =
|
||||
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4808,18 +4964,24 @@ export class ClaimRequestManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
const message = allDocumentsUploaded
|
||||
? "All documents uploaded successfully. Your claim is now ready for damage expert review."
|
||||
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||
const message = isCapturePhaseDocUpload
|
||||
? 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."
|
||||
: allDocumentsUploaded
|
||||
? "All documents uploaded successfully. Your claim is now ready for damage expert review."
|
||||
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||
|
||||
return {
|
||||
claimRequestId: claimCase._id.toString(),
|
||||
documentKey: body.documentKey,
|
||||
fileUrl,
|
||||
allDocumentsUploaded,
|
||||
currentStep: allDocumentsUploaded
|
||||
? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
|
||||
: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
currentStep: isCapturePhaseDocUpload
|
||||
? ClaimWorkflowStep.CAPTURE_PART_DAMAGES
|
||||
: allDocumentsUploaded
|
||||
? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
|
||||
: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||
message,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -5069,8 +5231,11 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}).length;
|
||||
|
||||
const capturePhaseDocsDone = this.capturePhaseDamagedPartyDocsComplete(updatedClaim);
|
||||
const allCapturesComplete =
|
||||
anglesCaptured >= 4 && partsCaptured >= selectedNormAfter.length;
|
||||
anglesCaptured >= 4 &&
|
||||
partsCaptured >= selectedNormAfter.length &&
|
||||
capturePhaseDocsDone;
|
||||
|
||||
if (allCapturesComplete) {
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
@@ -5090,18 +5255,23 @@ export class ClaimRequestManagementService {
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
description:
|
||||
'All car angles and damaged parts captured. Please upload required documents.',
|
||||
'Angles, damaged parts, and capture-phase vehicle evidence (chassis, engine, metal plate) are complete. Please upload remaining required documents.',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const captureDocsRemaining = CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.filter(
|
||||
(k) => !this.isRequiredDocumentUploadedOnClaim(updatedClaim, k),
|
||||
).length;
|
||||
const remaining =
|
||||
(4 - anglesCaptured) + (selectedNormAfter.length - partsCaptured);
|
||||
(4 - anglesCaptured) +
|
||||
(selectedNormAfter.length - partsCaptured) +
|
||||
captureDocsRemaining;
|
||||
const message = allCapturesComplete
|
||||
? 'All captures complete. Please proceed to upload required documents.'
|
||||
: `${body.captureType === 'angle' ? 'Angle' : 'Part'} captured successfully. ${remaining} captures remaining.`;
|
||||
: `${body.captureType === 'angle' ? 'Angle' : 'Part'} captured successfully. ${remaining} items remaining (angles, parts, and capture-phase documents).`;
|
||||
|
||||
return {
|
||||
claimRequestId: claimCase._id.toString(),
|
||||
|
||||
Reference in New Issue
Block a user