forked from Yara724/api
merge upstream
This commit is contained in:
@@ -99,8 +99,6 @@ import {
|
||||
hasClaimCarAngleCapture,
|
||||
hasDamagedPartCapture,
|
||||
legacyAngleDamagedPartFieldKeysToUnset,
|
||||
legacyDamagedPartTitleEnFromPartKey,
|
||||
resolveCanonicalDamagedPartStorageKey,
|
||||
type ClaimCarAngleKey,
|
||||
} from "src/helpers/claim-car-angle-media";
|
||||
import {
|
||||
@@ -108,6 +106,16 @@ import {
|
||||
OUTER_PARTS_BY_CAR_TYPE,
|
||||
OuterPartCatalogItem,
|
||||
} from "src/static/outer-car-parts-catalog";
|
||||
import {
|
||||
carPartDamageLabelForFactorRecord,
|
||||
catalogItemToSelectedPart,
|
||||
catalogLikeKeyFromPart,
|
||||
coerceDamagedPartsMediaToArray,
|
||||
migrateExpertReplyCarPartDamageToUnified,
|
||||
normalizeDamageSelectedParts,
|
||||
partIdentityKey,
|
||||
resolvePartCaptureIndex,
|
||||
} from "src/helpers/outer-damage-parts";
|
||||
|
||||
@Injectable()
|
||||
export class ClaimRequestManagementService {
|
||||
@@ -718,17 +726,32 @@ export class ClaimRequestManagementService {
|
||||
throw new BadRequestException("Car part damage is already set.");
|
||||
}
|
||||
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage);
|
||||
const selectedParts = this.carDamagePartDtoToOuterPartSlugs(dto.carPartDamage);
|
||||
if (!selectedParts.length) {
|
||||
const selectedSlugStrings = this.carDamagePartDtoToOuterPartSlugs(
|
||||
dto.carPartDamage,
|
||||
);
|
||||
if (!selectedSlugStrings.length) {
|
||||
throw new BadRequestException(
|
||||
"No outer damaged parts could be derived from carPartDamage. Check part selections.",
|
||||
);
|
||||
}
|
||||
const selectedPartObjs = normalizeDamageSelectedParts(
|
||||
selectedSlugStrings,
|
||||
working.vehicle?.carType as ClaimVehicleTypeV2 | undefined,
|
||||
undefined,
|
||||
);
|
||||
const damagedPartsStubs = selectedPartObjs.map((p) => ({
|
||||
id: p.id ?? undefined,
|
||||
name: p.name,
|
||||
side: p.side,
|
||||
label_fa: p.label_fa,
|
||||
...(p.catalogKey ? { catalogKey: p.catalogKey } : {}),
|
||||
}));
|
||||
|
||||
const updated = await this.claimCaseDbService.findByIdAndUpdate(
|
||||
claimRequestId,
|
||||
{
|
||||
"damage.selectedParts": selectedParts,
|
||||
"damage.selectedParts": selectedPartObjs,
|
||||
"media.damagedParts": damagedPartsStubs,
|
||||
status: ClaimCaseStatus.SELECTING_OTHER_PARTS,
|
||||
"workflow.currentStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
"workflow.nextStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
@@ -746,7 +769,7 @@ export class ClaimRequestManagementService {
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
selectedParts,
|
||||
selectedParts: selectedPartObjs,
|
||||
description: "Field expert submitted outer damage parts",
|
||||
},
|
||||
},
|
||||
@@ -1924,7 +1947,10 @@ export class ClaimRequestManagementService {
|
||||
* - convert factorLink ObjectId -> public URL
|
||||
* - enrich daghi.branchId with branchName when available
|
||||
*/
|
||||
private async mapEvaluationForClient(evaluation: any): Promise<any | undefined> {
|
||||
private async mapEvaluationForClient(
|
||||
evaluation: any,
|
||||
carType?: ClaimVehicleTypeV2,
|
||||
): Promise<any | undefined> {
|
||||
if (!evaluation) return undefined;
|
||||
|
||||
const mapReply = async (reply: any) => {
|
||||
@@ -1990,6 +2016,13 @@ export class ClaimRequestManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
if (nextPart.carPartDamage != null) {
|
||||
nextPart.carPartDamage = migrateExpertReplyCarPartDamageToUnified(
|
||||
nextPart.carPartDamage,
|
||||
carType,
|
||||
);
|
||||
}
|
||||
|
||||
return nextPart;
|
||||
});
|
||||
|
||||
@@ -2034,7 +2067,9 @@ export class ClaimRequestManagementService {
|
||||
const parts = finalReply?.parts || [];
|
||||
const missingFactors = parts
|
||||
.filter((p) => p.factorNeeded === true && !p.factorLink)
|
||||
.map((p) => p.carPartDamage || p.partId);
|
||||
.map((p) =>
|
||||
carPartDamageLabelForFactorRecord(p.carPartDamage) || p.partId,
|
||||
);
|
||||
|
||||
if (missingFactors.length > 0) {
|
||||
throw new HttpException(
|
||||
@@ -2261,7 +2296,7 @@ export class ClaimRequestManagementService {
|
||||
const factorRecord = await this.claimFactorsImageDbService.create({
|
||||
claimId,
|
||||
partId,
|
||||
partName: part.carPartDamage,
|
||||
partName: carPartDamageLabelForFactorRecord(part.carPartDamage),
|
||||
path: file.path,
|
||||
fileName: file.filename,
|
||||
uploadedAt: new Date(),
|
||||
@@ -2398,7 +2433,9 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
const partNameForRecord = part.carPartDamage ?? part.partId;
|
||||
const partNameForRecord =
|
||||
carPartDamageLabelForFactorRecord(part.carPartDamage) ||
|
||||
String(part.partId ?? "");
|
||||
|
||||
const factorRecord = await this.claimFactorsImageDbService.create({
|
||||
claimId: new Types.ObjectId(claimRequestId),
|
||||
@@ -3983,41 +4020,46 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
selectedParts = selectedItems.map((p) => p.key);
|
||||
const selectedPartIds = selectedItems.map((p) => p.id);
|
||||
const selectedOuterParts = selectedItems.map((p) => ({
|
||||
id: p.id,
|
||||
key: p.key,
|
||||
const selectedPartDocs = selectedItems.map((p) =>
|
||||
catalogItemToSelectedPart(p, catalog),
|
||||
);
|
||||
const damagedPartsInitial = selectedPartDocs.map((p) => ({
|
||||
id: p.id ?? undefined,
|
||||
name: p.name,
|
||||
side: p.side,
|
||||
label_fa: p.label_fa,
|
||||
...(p.catalogKey ? { catalogKey: p.catalogKey } : {}),
|
||||
}));
|
||||
const selectedPartIds = selectedItems.map((p) => p.id);
|
||||
|
||||
// 6. Update claim case with selected parts and move to next step
|
||||
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
|
||||
claimRequestId,
|
||||
{
|
||||
'damage.selectedParts': selectedParts,
|
||||
'damage.selectedOuterParts': selectedOuterParts,
|
||||
'vehicle.carType': selectedCarType,
|
||||
'status': ClaimCaseStatus.SELECTING_OTHER_PARTS,
|
||||
'workflow.currentStep': ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
'workflow.nextStep': ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
"damage.selectedParts": selectedPartDocs,
|
||||
"media.damagedParts": damagedPartsInitial,
|
||||
"vehicle.carType": selectedCarType,
|
||||
status: ClaimCaseStatus.SELECTING_OTHER_PARTS,
|
||||
"workflow.currentStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
"workflow.nextStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
$unset: { "damage.selectedOuterParts": "" },
|
||||
$push: {
|
||||
'workflow.completedSteps': ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
"workflow.completedSteps": ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
history: {
|
||||
type: 'STEP_COMPLETED',
|
||||
type: "STEP_COMPLETED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(currentUserId),
|
||||
actorName: claimCase.owner?.fullName || 'User',
|
||||
actorType: 'user',
|
||||
actorName: claimCase.owner?.fullName || "User",
|
||||
actorType: "user",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
selectedParts: selectedParts,
|
||||
selectedParts: selectedPartDocs,
|
||||
selectedPartIds,
|
||||
carType: selectedCarType,
|
||||
partsCount: selectedParts?.length,
|
||||
description: `User selected ${selectedParts?.length} damaged outer parts`,
|
||||
partsCount: selectedPartDocs?.length,
|
||||
description: `User selected ${selectedPartDocs?.length} damaged outer parts`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -4025,22 +4067,23 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
|
||||
if (!updatedClaim) {
|
||||
throw new InternalServerErrorException('Failed to update claim case');
|
||||
throw new InternalServerErrorException("Failed to update claim case");
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Outer parts selected for claim ${claimRequestId}: ${selectedParts?.length} parts`,
|
||||
`Outer parts selected for claim ${claimRequestId}: ${selectedPartDocs?.length} parts`,
|
||||
);
|
||||
|
||||
// 7. Return response
|
||||
return {
|
||||
claimRequestId: updatedClaim._id.toString(),
|
||||
publicId: updatedClaim.publicId,
|
||||
selectedParts: selectedParts,
|
||||
selectedParts: selectedPartDocs,
|
||||
selectedPartIds,
|
||||
currentStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
nextStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
message: 'Outer parts selected successfully. Please proceed to select other parts and provide bank information.',
|
||||
message:
|
||||
"Outer parts selected successfully. Please proceed to select other parts and provide bank information.",
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
@@ -4293,19 +4336,6 @@ export class ClaimRequestManagementService {
|
||||
if (!catalogByKey.has(item.key)) catalogByKey.set(item.key, item);
|
||||
}
|
||||
|
||||
const getPartLabel = (key: string): { fa: string; en: string } => {
|
||||
const catalogItem = catalogByKey.get(key);
|
||||
if (catalogItem) {
|
||||
const en = key
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.replace(/_/g, " ")
|
||||
.trim()
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
return { fa: catalogItem.titleFa, en };
|
||||
}
|
||||
return { fa: key, en: key };
|
||||
};
|
||||
|
||||
// Required documents: CAR_BODY needs 7 (damaged + green card); THIRD_PARTY needs 13 (add guilty_*)
|
||||
const blameRequest = claimCase.blameRequestId
|
||||
? await this.blameRequestDbService.findById(claimCase.blameRequestId.toString())
|
||||
@@ -4359,46 +4389,36 @@ export class ClaimRequestManagementService {
|
||||
),
|
||||
}));
|
||||
|
||||
// Damaged parts from selected parts
|
||||
const selectedParts = claimCase.damage?.selectedParts || [];
|
||||
const selectedOuterParts = Array.isArray(claimCase.damage?.selectedOuterParts)
|
||||
? claimCase.damage.selectedOuterParts
|
||||
: [];
|
||||
const selectedOuterPartByKey = new Map<
|
||||
string,
|
||||
{ id?: number; side?: string }
|
||||
>(
|
||||
selectedOuterParts.map((p) => [
|
||||
p.key,
|
||||
{
|
||||
id: p.id,
|
||||
side: p.side,
|
||||
},
|
||||
]),
|
||||
const carType = claimCase.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const selectedNorm = normalizeDamageSelectedParts(
|
||||
claimCase.damage?.selectedParts,
|
||||
carType,
|
||||
(claimCase.damage as any)?.selectedOuterParts,
|
||||
);
|
||||
|
||||
const damagedParts = selectedParts.map(partKey => {
|
||||
const label = getPartLabel(partKey);
|
||||
const selectedMeta = selectedOuterPartByKey.get(partKey);
|
||||
const catalogMeta = catalogByKey.get(partKey);
|
||||
const side = selectedMeta?.side ?? catalogMeta?.side;
|
||||
const sideFaMap: Record<string, string> = {
|
||||
left: "چپ",
|
||||
right: "راست",
|
||||
front: "جلو",
|
||||
back: "عقب",
|
||||
top: "بالا",
|
||||
};
|
||||
const sideFa = side ? sideFaMap[side] : undefined;
|
||||
const damagedParts = selectedNorm.map((sp) => {
|
||||
const catalogMeta = sp.catalogKey
|
||||
? catalogByKey.get(sp.catalogKey)
|
||||
: sp.name
|
||||
? catalogByKey.get(catalogLikeKeyFromPart(sp))
|
||||
: undefined;
|
||||
const labelEnName = sp.name
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.replace(/_/g, " ")
|
||||
.trim()
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
|
||||
return {
|
||||
id: selectedMeta?.id ?? catalogMeta?.id,
|
||||
key: partKey,
|
||||
side,
|
||||
label_fa: sideFa ? `${label.fa} (${sideFa})` : label.fa,
|
||||
label_en: label.en,
|
||||
id: sp.id ?? catalogMeta?.id,
|
||||
key: sp.name,
|
||||
name: sp.name,
|
||||
side: sp.side,
|
||||
label_fa: sp.label_fa,
|
||||
label_en: labelEnName,
|
||||
captured: hasDamagedPartCapture(
|
||||
claimCase.media?.damagedParts as any,
|
||||
partKey,
|
||||
ck,
|
||||
selectedNorm,
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -4835,48 +4855,90 @@ export class ClaimRequestManagementService {
|
||||
},
|
||||
};
|
||||
|
||||
const selectedBefore = (claimCase.damage?.selectedParts || []) as string[];
|
||||
const carType = claimCase.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const selectedOuterLegacy = (claimCase.damage as any)?.selectedOuterParts;
|
||||
const selectedBeforeNorm = normalizeDamageSelectedParts(
|
||||
claimCase.damage?.selectedParts,
|
||||
carType,
|
||||
selectedOuterLegacy,
|
||||
);
|
||||
let nextSelected = [...selectedBeforeNorm];
|
||||
let nextMedia = coerceDamagedPartsMediaToArray(
|
||||
claimCase.media?.damagedParts,
|
||||
nextSelected,
|
||||
);
|
||||
|
||||
if (treatAsAngle && angleCanon) {
|
||||
updateData[`media.carAngles.${angleCanon}`] = captureData;
|
||||
const unsetFieldKeys = legacyAngleDamagedPartFieldKeysToUnset(
|
||||
angleCanon,
|
||||
body.captureKey,
|
||||
selectedBefore,
|
||||
);
|
||||
if (unsetFieldKeys.length) {
|
||||
updateData.$unset = {};
|
||||
for (const k of unsetFieldKeys) {
|
||||
updateData.$unset[`media.damagedParts.${k}`] = "";
|
||||
if (!Array.isArray(claimCase.media?.damagedParts)) {
|
||||
const unsetFieldKeys = legacyAngleDamagedPartFieldKeysToUnset(
|
||||
angleCanon,
|
||||
body.captureKey,
|
||||
claimCase.damage?.selectedParts,
|
||||
carType,
|
||||
selectedOuterLegacy,
|
||||
);
|
||||
if (unsetFieldKeys.length) {
|
||||
updateData.$unset = {};
|
||||
for (const k of unsetFieldKeys) {
|
||||
updateData.$unset[`media.damagedParts.${k}`] = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const canonicalPartKey = resolveCanonicalDamagedPartStorageKey(
|
||||
let idx = resolvePartCaptureIndex(
|
||||
body.captureKey,
|
||||
selectedBefore,
|
||||
nextSelected,
|
||||
nextMedia,
|
||||
);
|
||||
updateData[`media.damagedParts.${canonicalPartKey}`] = captureData;
|
||||
const raw = String(body.captureKey ?? "").trim();
|
||||
const titleLegacy =
|
||||
legacyDamagedPartTitleEnFromPartKey(canonicalPartKey);
|
||||
const unsetPartKeys = new Set<string>();
|
||||
if (raw && raw !== canonicalPartKey) unsetPartKeys.add(raw);
|
||||
if (titleLegacy && titleLegacy !== canonicalPartKey) {
|
||||
unsetPartKeys.add(titleLegacy);
|
||||
if (isResendCapture && body.captureType === "part" && idx < 0) {
|
||||
const added = normalizeDamageSelectedParts(
|
||||
[body.captureKey],
|
||||
carType,
|
||||
undefined,
|
||||
);
|
||||
const row =
|
||||
added[0] || {
|
||||
id: null,
|
||||
name: body.captureKey,
|
||||
side: "",
|
||||
label_fa: body.captureKey,
|
||||
catalogKey: body.captureKey,
|
||||
};
|
||||
nextSelected.push(row);
|
||||
idx = nextSelected.length - 1;
|
||||
nextMedia.push({});
|
||||
}
|
||||
if (unsetPartKeys.size) {
|
||||
updateData.$unset = updateData.$unset || {};
|
||||
for (const k of unsetPartKeys) {
|
||||
updateData.$unset[`media.damagedParts.${k}`] = "";
|
||||
}
|
||||
if (idx < 0) {
|
||||
throw new BadRequestException(
|
||||
`Unknown damaged-part captureKey "${body.captureKey}". Use catalog id, 0-based index, or catalog key.`,
|
||||
);
|
||||
}
|
||||
while (nextMedia.length <= idx) {
|
||||
nextMedia.push({});
|
||||
}
|
||||
const base = nextSelected[idx] || {
|
||||
id: null,
|
||||
name: String(body.captureKey),
|
||||
side: "",
|
||||
label_fa: String(body.captureKey),
|
||||
};
|
||||
nextMedia[idx] = {
|
||||
...(base.id != null ? { id: base.id } : {}),
|
||||
name: base.name,
|
||||
side: base.side,
|
||||
label_fa: base.label_fa,
|
||||
...(base.catalogKey ? { catalogKey: base.catalogKey } : {}),
|
||||
...nextMedia[idx],
|
||||
...captureData,
|
||||
};
|
||||
updateData["media.damagedParts"] = nextMedia;
|
||||
if (
|
||||
isResendCapture &&
|
||||
nextSelected.length !== selectedBeforeNorm.length
|
||||
) {
|
||||
updateData["damage.selectedParts"] = nextSelected;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isResendCapture &&
|
||||
body.captureType === "part" &&
|
||||
!selectedBefore.includes(body.captureKey)
|
||||
) {
|
||||
updateData.$addToSet = { "damage.selectedParts": body.captureKey };
|
||||
}
|
||||
|
||||
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
|
||||
@@ -4911,10 +4973,14 @@ export class ClaimRequestManagementService {
|
||||
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||
|
||||
// Use same logic as getCaptureRequirementsV2 to determine completion
|
||||
const anglesKeys = ['front', 'back', 'left', 'right'];
|
||||
const anglesKeys = ["front", "back", "left", "right"];
|
||||
const carAnglesData = updatedClaim?.media?.carAngles as any;
|
||||
const damagedPartsData = updatedClaim?.media?.damagedParts as any;
|
||||
const selectedParts = updatedClaim?.damage?.selectedParts || [];
|
||||
const selectedNormAfter = normalizeDamageSelectedParts(
|
||||
updatedClaim?.damage?.selectedParts,
|
||||
updatedClaim?.vehicle?.carType as ClaimVehicleTypeV2,
|
||||
(updatedClaim?.damage as any)?.selectedOuterParts,
|
||||
);
|
||||
|
||||
const anglesCaptured = anglesKeys.filter((k) =>
|
||||
hasClaimCarAngleCapture(
|
||||
@@ -4923,12 +4989,17 @@ export class ClaimRequestManagementService {
|
||||
k as ClaimCarAngleKey,
|
||||
),
|
||||
).length;
|
||||
const partsCaptured = selectedParts.filter((p) =>
|
||||
hasDamagedPartCapture(damagedPartsData, p),
|
||||
).length;
|
||||
const partsCaptured = selectedNormAfter.filter((sp) => {
|
||||
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
|
||||
return hasDamagedPartCapture(
|
||||
damagedPartsData,
|
||||
ck,
|
||||
selectedNormAfter,
|
||||
);
|
||||
}).length;
|
||||
|
||||
const allCapturesComplete =
|
||||
anglesCaptured >= 4 && partsCaptured >= selectedParts.length;
|
||||
anglesCaptured >= 4 && partsCaptured >= selectedNormAfter.length;
|
||||
|
||||
if (allCapturesComplete) {
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
@@ -4955,7 +5026,8 @@ export class ClaimRequestManagementService {
|
||||
});
|
||||
}
|
||||
|
||||
const remaining = (4 - anglesCaptured) + (selectedParts.length - partsCaptured);
|
||||
const remaining =
|
||||
(4 - anglesCaptured) + (selectedNormAfter.length - partsCaptured);
|
||||
const message = allCapturesComplete
|
||||
? 'All captures complete. Please proceed to upload required documents.'
|
||||
: `${body.captureType === 'angle' ? 'Angle' : 'Part'} captured successfully. ${remaining} captures remaining.`;
|
||||
@@ -5193,11 +5265,29 @@ export class ClaimRequestManagementService {
|
||||
side: p.side,
|
||||
}));
|
||||
|
||||
const mergedSelected = [...(claimCase.damage?.selectedParts ?? [])];
|
||||
const objectionCarType = claimCase.vehicle?.carType as
|
||||
| ClaimVehicleTypeV2
|
||||
| undefined;
|
||||
const mergedNorm = normalizeDamageSelectedParts(
|
||||
claimCase.damage?.selectedParts,
|
||||
objectionCarType,
|
||||
(claimCase.damage as any)?.selectedOuterParts,
|
||||
);
|
||||
const mergedKeys = new Set(mergedNorm.map((p) => partIdentityKey(p)));
|
||||
for (const np of newParts) {
|
||||
const key = np.partName?.trim();
|
||||
if (key && !mergedSelected.includes(key)) {
|
||||
mergedSelected.push(key);
|
||||
const name = np.partName?.trim();
|
||||
if (!name) continue;
|
||||
const side = String(np.side ?? "").trim();
|
||||
const row = {
|
||||
id: null as number | null,
|
||||
name,
|
||||
side,
|
||||
label_fa: name,
|
||||
};
|
||||
const k = partIdentityKey(row);
|
||||
if (!mergedKeys.has(k)) {
|
||||
mergedNorm.push(row);
|
||||
mergedKeys.add(k);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5215,7 +5305,7 @@ export class ClaimRequestManagementService {
|
||||
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
'workflow.nextStep': ClaimWorkflowStep.EXPERT_COST_EVALUATION,
|
||||
'evaluation.objection': objectionPayload,
|
||||
'damage.selectedParts': mergedSelected,
|
||||
'damage.selectedParts': mergedNorm,
|
||||
},
|
||||
$unset: {
|
||||
'evaluation.ownerPricedPartsApproval': "",
|
||||
@@ -5695,47 +5785,56 @@ export class ClaimRequestManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
const getPartLabelFa = (key: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
hood: 'کاپوت',
|
||||
trunk: 'صندوق عقب',
|
||||
roof: 'سقف',
|
||||
front_right_door: 'درب جلو راست',
|
||||
front_left_door: 'درب جلو چپ',
|
||||
rear_right_door: 'درب عقب راست',
|
||||
rear_left_door: 'درب عقب چپ',
|
||||
front_bumper: 'سپر جلو',
|
||||
rear_bumper: 'سپر عقب',
|
||||
front_right_fender: 'گلگیر جلو راست',
|
||||
front_left_fender: 'گلگیر جلو چپ',
|
||||
rear_right_fender: 'گلگیر عقب راست',
|
||||
rear_left_fender: 'گلگیر عقب چپ',
|
||||
};
|
||||
return labels[key] || key;
|
||||
};
|
||||
|
||||
const carTypeDetails = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const selectedNormDetails = normalizeDamageSelectedParts(
|
||||
claim.damage?.selectedParts,
|
||||
carTypeDetails,
|
||||
(claim.damage as any)?.selectedOuterParts,
|
||||
);
|
||||
const damagedPartsData = claim.media?.damagedParts as any;
|
||||
const damagedParts: Record<
|
||||
string,
|
||||
{ label_fa: string; captured: boolean; url?: string }
|
||||
> = {};
|
||||
const resendPartKeys = normalizeResendPartKeys(
|
||||
claim.evaluation?.damageExpertResend?.resendCarParts,
|
||||
);
|
||||
const partKeysForDetails = new Set<string>([
|
||||
...((claim.damage?.selectedParts || []) as string[]),
|
||||
...resendPartKeys,
|
||||
]);
|
||||
for (const p of partKeysForDetails) {
|
||||
const cap = getDamagedPartCaptureBlob(damagedPartsData, p) as
|
||||
| { url?: string; path?: string }
|
||||
| undefined;
|
||||
damagedParts[p] = {
|
||||
label_fa: getPartLabelFa(p),
|
||||
const displayParts = [...selectedNormDetails];
|
||||
const seenDetailKeys = new Set(
|
||||
selectedNormDetails.map((p) => partIdentityKey(p)),
|
||||
);
|
||||
for (const rk of resendPartKeys) {
|
||||
const extra =
|
||||
normalizeDamageSelectedParts([rk], carTypeDetails, undefined)[0] ||
|
||||
{
|
||||
id: null,
|
||||
name: rk,
|
||||
side: "",
|
||||
label_fa: rk,
|
||||
catalogKey: rk,
|
||||
};
|
||||
const k = partIdentityKey(extra);
|
||||
if (!seenDetailKeys.has(k)) {
|
||||
displayParts.push(extra);
|
||||
seenDetailKeys.add(k);
|
||||
}
|
||||
}
|
||||
|
||||
const damagedParts = displayParts.map((sp, index) => {
|
||||
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
|
||||
const cap = getDamagedPartCaptureBlob(
|
||||
damagedPartsData,
|
||||
ck,
|
||||
selectedNormDetails,
|
||||
) as { url?: string; path?: string; fileName?: string } | undefined;
|
||||
return {
|
||||
index,
|
||||
id: sp.id,
|
||||
name: sp.name,
|
||||
side: sp.side,
|
||||
label_fa: sp.label_fa,
|
||||
captured: !!cap,
|
||||
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
|
||||
...(cap?.path ? { path: cap.path } : {}),
|
||||
...(cap?.fileName ? { fileName: cap.fileName } : {}),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const er = claim.evaluation?.damageExpertResend;
|
||||
const expertResend =
|
||||
@@ -5776,7 +5875,10 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const mappedEvaluation = await this.mapEvaluationForClient(claim.evaluation);
|
||||
const mappedEvaluation = await this.mapEvaluationForClient(
|
||||
claim.evaluation,
|
||||
claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined,
|
||||
);
|
||||
|
||||
return {
|
||||
claimRequestId: claim._id.toString(),
|
||||
@@ -5790,7 +5892,7 @@ export class ClaimRequestManagementService {
|
||||
blameRequestNo: claim.blameRequestNo,
|
||||
...(ownerData ? { owner: ownerData } : {}),
|
||||
vehicle: claim.vehicle,
|
||||
selectedParts: claim.damage?.selectedParts,
|
||||
selectedParts: selectedNormDetails,
|
||||
otherParts: claim.damage?.otherParts,
|
||||
money: isExpertViewer ? moneyForExpert : moneyForUser,
|
||||
requiredDocuments: Object.keys(requiredDocumentsStatus).length > 0 ? requiredDocumentsStatus : undefined,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { CarAngle } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
||||
|
||||
/**
|
||||
* V2 DTO for capturing car angle or damaged part
|
||||
@@ -18,7 +17,8 @@ export class CapturePartV2Dto {
|
||||
captureType: 'angle' | 'part';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Key of the angle or part being captured',
|
||||
description:
|
||||
'When captureType is angle: front | back | left | right. When part: catalog id as string (e.g. "101"), 0-based index (e.g. "0"), or full catalog key (e.g. left_backfender). Prefer id or index for parts.',
|
||||
example: 'front',
|
||||
})
|
||||
@IsNotEmpty({ message: 'Capture key is required' })
|
||||
|
||||
@@ -72,10 +72,17 @@ export class CarAngleItem {
|
||||
*/
|
||||
export class DamagedPartItem {
|
||||
@ApiProperty({
|
||||
description: 'Part key',
|
||||
example: 'hood',
|
||||
description:
|
||||
'Side-agnostic part name (matches catalog suffix); use with `side` to disambiguate',
|
||||
example: 'backfender',
|
||||
})
|
||||
key: string;
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Vehicle side / region (left, right, front, back, top)',
|
||||
example: 'left',
|
||||
})
|
||||
side?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Display label in Farsi',
|
||||
@@ -83,6 +90,12 @@ export class DamagedPartItem {
|
||||
})
|
||||
label_fa: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Deprecated: same as `name` (kept for older clients)',
|
||||
example: 'backfender',
|
||||
})
|
||||
key?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Display label in English',
|
||||
example: 'Hood',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { DamageSelectedPartV2BodyDto } from './damage-selected-part-v2.dto';
|
||||
|
||||
/** Suggested HTTP call for the owner UI (`pathTemplate`: replace placeholders). */
|
||||
export class ClaimDetailsOwnerNextActionV2Dto {
|
||||
@@ -123,8 +124,11 @@ export class ClaimDetailsV2ResponseDto {
|
||||
plate?: any;
|
||||
};
|
||||
|
||||
@ApiPropertyOptional({ description: 'Selected outer damaged parts' })
|
||||
selectedParts?: string[];
|
||||
@ApiPropertyOptional({
|
||||
description: 'Selected outer damaged parts (ordered objects with id, name, side, label_fa)',
|
||||
type: [DamageSelectedPartV2BodyDto],
|
||||
})
|
||||
selectedParts?: DamageSelectedPartV2BodyDto[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Selected other damaged parts' })
|
||||
otherParts?: string[];
|
||||
@@ -141,8 +145,36 @@ export class ClaimDetailsV2ResponseDto {
|
||||
@ApiPropertyOptional({ description: 'Car angles captured' })
|
||||
carAngles?: Record<string, { captured: boolean; url?: string }>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Damaged parts captured' })
|
||||
damagedParts?: Record<string, { label_fa: string; captured: boolean; url?: string }>;
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Per-part capture status and URLs (array index aligns with selectedParts; includes id, name, side, label_fa)',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
index: { type: 'number' },
|
||||
id: { type: 'number', nullable: true },
|
||||
name: { type: 'string' },
|
||||
side: { type: 'string' },
|
||||
label_fa: { type: 'string' },
|
||||
captured: { type: 'boolean' },
|
||||
url: { type: 'string' },
|
||||
path: { type: 'string' },
|
||||
fileName: { type: 'string' },
|
||||
},
|
||||
},
|
||||
})
|
||||
damagedParts?: Array<{
|
||||
index: number;
|
||||
id?: number | null;
|
||||
name: string;
|
||||
side: string;
|
||||
label_fa: string;
|
||||
captured: boolean;
|
||||
url?: string;
|
||||
path?: string;
|
||||
fileName?: string;
|
||||
}>;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsInt, IsOptional, IsString } from "class-validator";
|
||||
|
||||
/** Stored shape / API payload for one selected outer damaged part (V2). */
|
||||
export class DamageSelectedPartV2BodyDto {
|
||||
@ApiPropertyOptional({ description: "Catalog id when from outer-parts catalog" })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
id?: number;
|
||||
|
||||
@ApiProperty({ example: "backfender" })
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ example: "left" })
|
||||
@IsString()
|
||||
side: string;
|
||||
|
||||
@ApiProperty({ example: "گلگیر عقب" })
|
||||
@IsString()
|
||||
label_fa: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Original catalog key, e.g. left_backfender" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
catalogKey?: string;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ClaimVehicleTypeV2,
|
||||
OuterPartSideV2,
|
||||
} from "src/static/outer-car-parts-catalog";
|
||||
import { DamageSelectedPartV2BodyDto } from "./damage-selected-part-v2.dto";
|
||||
|
||||
/**
|
||||
* Enum for valid outer car parts that can be damaged
|
||||
@@ -97,10 +98,11 @@ export class SelectOuterPartsV2ResponseDto {
|
||||
publicId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Array of selected damaged parts',
|
||||
example: ['hood', 'front_right_door', 'rear_bumper'],
|
||||
description:
|
||||
"Ordered selected parts: id, side-agnostic name, side, label_fa (and optional catalogKey)",
|
||||
type: [DamageSelectedPartV2BodyDto],
|
||||
})
|
||||
selectedParts: string[];
|
||||
selectedParts: DamageSelectedPartV2BodyDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Selected part IDs',
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Schema as MongooseSchema } from "mongoose";
|
||||
import { CarDamagePartModel, CarDamagePartOtherModel } from "./car-parts.schema";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class SelectedOuterPartV2 {
|
||||
@Prop({ type: Number })
|
||||
id: number;
|
||||
|
||||
@Prop({ type: String })
|
||||
key: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
side: string;
|
||||
}
|
||||
export const SelectedOuterPartV2Schema =
|
||||
SchemaFactory.createForClass(SelectedOuterPartV2);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimDamageSelection {
|
||||
/**
|
||||
* V2: Array of selected damaged outer car part names
|
||||
* Examples: ['hood', 'front_right_door', 'rear_bumper']
|
||||
* V2: Selected outer damaged parts (ordered). Prefer objects
|
||||
* `{ id, name, side, label_fa, catalogKey? }`; legacy string keys are still accepted until migrated.
|
||||
*/
|
||||
@Prop({ type: [String], default: [] })
|
||||
selectedParts?: string[];
|
||||
|
||||
/** Structured selected outer parts with id + side for better downstream handling. */
|
||||
@Prop({ type: [SelectedOuterPartV2Schema], default: [] })
|
||||
selectedOuterParts?: SelectedOuterPartV2[];
|
||||
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
|
||||
selectedParts?: unknown[];
|
||||
|
||||
/**
|
||||
* V2: Array of selected other (non-body) damaged parts
|
||||
@@ -46,4 +29,3 @@ export class ClaimDamageSelection {
|
||||
}
|
||||
export const ClaimDamageSelectionSchema =
|
||||
SchemaFactory.createForClass(ClaimDamageSelection);
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@ export class ClaimPartPricing {
|
||||
partId: string;
|
||||
|
||||
/**
|
||||
* Legacy string or structured `{ part, side }` from expert reply DTOs (V1/V2).
|
||||
* Unified outer-part snapshot `{ id?, name, side, label_fa, catalogKey? }` (same as
|
||||
* `damage.selectedParts` / capture). Legacy: string or `{ part?, side? }` (expert UI).
|
||||
* Must be Mixed — objects cannot cast to String.
|
||||
*/
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
carPartDamage?: string | { part?: string; side?: string };
|
||||
carPartDamage?: unknown;
|
||||
|
||||
@Prop({ type: String })
|
||||
typeOfDamage?: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import { Schema as MongooseSchema, Types } from "mongoose";
|
||||
import { AiImagesModel } from "./ai-image.schema";
|
||||
import { CarGreenCardModel } from "./car-green-card.schema";
|
||||
import { ImageRequiredModel } from "./image-required.schema";
|
||||
@@ -20,6 +20,39 @@ export class CapturedImage {
|
||||
}
|
||||
export const CapturedImageSchema = SchemaFactory.createForClass(CapturedImage);
|
||||
|
||||
/** One row per selected damaged part (index-aligned with `damage.selectedParts`). */
|
||||
@Schema({ _id: false })
|
||||
export class DamagedPartMediaV2Row {
|
||||
@Prop({ type: Number })
|
||||
id?: number;
|
||||
|
||||
@Prop({ type: String })
|
||||
name?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
side?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
label_fa?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
catalogKey?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
path?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
fileName?: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
url?: string;
|
||||
|
||||
@Prop({ type: Date })
|
||||
capturedAt?: Date;
|
||||
}
|
||||
export const DamagedPartMediaV2RowSchema =
|
||||
SchemaFactory.createForClass(DamagedPartMediaV2Row);
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimMedia {
|
||||
@Prop({ type: CarGreenCardModel })
|
||||
@@ -46,15 +79,10 @@ export class ClaimMedia {
|
||||
carAngles?: Map<string, CapturedImage>;
|
||||
|
||||
/**
|
||||
* V2: Damaged parts captures
|
||||
* Map of part key to captured image
|
||||
* V2: Damaged parts captures — prefer an array (index matches `damage.selectedParts`).
|
||||
* Legacy documents may store a plain object map keyed by part slug until migrated on write.
|
||||
*/
|
||||
@Prop({
|
||||
type: Map,
|
||||
of: CapturedImageSchema,
|
||||
default: () => ({}),
|
||||
})
|
||||
damagedParts?: Map<string, CapturedImage>;
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
damagedParts?: DamagedPartMediaV2Row[] | Record<string, unknown>;
|
||||
}
|
||||
export const ClaimMediaSchema = SchemaFactory.createForClass(ClaimMedia);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user