forked from Yara724/api
Fixed claim/blame damagedParts
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { BlameRequestType } from 'src/Types&Enums/blame-request-management/blameRequestType.enum';
|
||||
import { DamageSelectedPartV2BodyDto } from 'src/claim-request-management/dto/damage-selected-part-v2.dto';
|
||||
|
||||
export class ClaimDetailV2ResponseDto {
|
||||
@ApiProperty()
|
||||
@@ -64,8 +65,11 @@ export class ClaimDetailV2ResponseDto {
|
||||
@ApiPropertyOptional({ description: 'Blame request number', example: 'BL12345' })
|
||||
blameRequestNo?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Selected outer damaged parts' })
|
||||
selectedParts?: string[];
|
||||
@ApiPropertyOptional({
|
||||
description: 'Selected outer damaged parts (id, name, side, label_fa)',
|
||||
type: [DamageSelectedPartV2BodyDto],
|
||||
})
|
||||
selectedParts?: DamageSelectedPartV2BodyDto[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Selected other damaged parts' })
|
||||
otherParts?: string[];
|
||||
@@ -81,8 +85,32 @@ export class ClaimDetailV2ResponseDto {
|
||||
@ApiPropertyOptional({ description: 'Car angles captured' })
|
||||
carAngles?: Record<string, { captured: boolean; url?: string }>;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Damaged parts captured' })
|
||||
damagedParts?: Record<string, { captured: boolean; url?: string }>;
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Per-part captures (array index matches selectedParts; includes id, name, side, label_fa, captured, url)',
|
||||
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' },
|
||||
},
|
||||
},
|
||||
})
|
||||
damagedParts?: Array<{
|
||||
index: number;
|
||||
id?: number | null;
|
||||
name: string;
|
||||
side: string;
|
||||
label_fa: string;
|
||||
captured: boolean;
|
||||
url?: string;
|
||||
}>;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
|
||||
@@ -7,20 +7,57 @@ import {
|
||||
IsOptional,
|
||||
ValidateNested,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
||||
import { DamagedPartItem } from 'src/claim-request-management/dto/capture-requirements-v2.dto';
|
||||
import { DaghiOption } from 'src/Types&Enums/claim-request-management/daghi-option.enum';
|
||||
|
||||
export class CarPartDamageV2Dto {
|
||||
@ApiProperty({ example: 'left' })
|
||||
@IsString()
|
||||
side: string;
|
||||
/**
|
||||
* Damage line part reference — same unified shape as `damage.selectedParts` / catalog rows.
|
||||
* Send either `{ name, side, label_fa?, id?, catalogKey? }` or legacy `{ part, side }`
|
||||
* (e.g. part `backFender`, side `left`); the API persists canonical `{ id, name, side, label_fa, catalogKey? }`.
|
||||
*/
|
||||
export class ExpertReplyCarPartDamageV2Dto {
|
||||
@ApiPropertyOptional({ description: 'Catalog id when known' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
id?: number;
|
||||
|
||||
@ApiProperty({ example: 'front_door' })
|
||||
@ApiPropertyOptional({
|
||||
description: 'Side-agnostic catalog segment, e.g. backfender',
|
||||
example: 'backfender',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
part: string;
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'left' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
side?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Farsi label; optional when server can resolve from catalog + vehicle.carType',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
label_fa?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'left_backfender' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
catalogKey?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Legacy expert UI: camelCase segment with `side`, e.g. backFender',
|
||||
example: 'backFender',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
part?: string;
|
||||
}
|
||||
|
||||
/** Same shape as V1 {@link PartsList} `daghi` (damage expert reply). */
|
||||
@@ -54,10 +91,10 @@ export class PartPricingV2Dto {
|
||||
@IsNotEmpty()
|
||||
partId: string;
|
||||
|
||||
@ApiProperty({ type: CarPartDamageV2Dto })
|
||||
@ApiProperty({ type: ExpertReplyCarPartDamageV2Dto })
|
||||
@ValidateNested()
|
||||
@Type(() => CarPartDamageV2Dto)
|
||||
carPartDamage: CarPartDamageV2Dto;
|
||||
@Type(() => ExpertReplyCarPartDamageV2Dto)
|
||||
carPartDamage: ExpertReplyCarPartDamageV2Dto;
|
||||
|
||||
@ApiProperty({ example: 'Minor' })
|
||||
@IsString()
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import {
|
||||
ArrayMinSize,
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { OuterCarPart } from "src/claim-request-management/dto/select-outer-parts-v2.dto";
|
||||
import { Type } from "class-transformer";
|
||||
import { DamageSelectedPartV2BodyDto } from "src/claim-request-management/dto/damage-selected-part-v2.dto";
|
||||
|
||||
export class UpdateClaimDamagedPartsV2Dto {
|
||||
@ApiProperty({
|
||||
description: "Updated list of selected damaged outer parts",
|
||||
enum: OuterCarPart,
|
||||
isArray: true,
|
||||
example: ["hood", "front_bumper", "front_left_fender"],
|
||||
type: [DamageSelectedPartV2BodyDto],
|
||||
description: "Full replacement list of selected damaged outer parts (id, name, side, label_fa)",
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ArrayUnique()
|
||||
@IsEnum(OuterCarPart, { each: true })
|
||||
selectedParts: OuterCarPart[];
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DamageSelectedPartV2BodyDto)
|
||||
selectedParts: DamageSelectedPartV2BodyDto[];
|
||||
}
|
||||
|
||||
|
||||
@@ -71,8 +71,16 @@ import {
|
||||
import {
|
||||
getClaimCarAngleCaptureBlob,
|
||||
getDamagedPartCaptureBlob,
|
||||
hasDamagedPartCapture,
|
||||
type ClaimCarAngleKey,
|
||||
} from "src/helpers/claim-car-angle-media";
|
||||
import {
|
||||
catalogLikeKeyFromPart,
|
||||
coerceDamagedPartsMediaToArray,
|
||||
normalizeCarPartDamageForExpertReply,
|
||||
normalizeDamageSelectedParts,
|
||||
} from "src/helpers/outer-damage-parts";
|
||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot";
|
||||
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
|
||||
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||
@@ -2532,10 +2540,27 @@ export class ExpertClaimService {
|
||||
});
|
||||
}
|
||||
|
||||
const processedParts =
|
||||
const carTypeSubmit = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const daghiNormalized =
|
||||
reply.parts?.length > 0
|
||||
? this.validateAndNormalizeDaghiForExpertReplyV2(reply.parts)
|
||||
: [];
|
||||
const processedParts = daghiNormalized.map((p) => {
|
||||
let carPartDamage: Record<string, unknown>;
|
||||
try {
|
||||
carPartDamage = normalizeCarPartDamageForExpertReply(
|
||||
p.carPartDamage as unknown,
|
||||
carTypeSubmit,
|
||||
);
|
||||
} catch (err) {
|
||||
throw new BadRequestException(
|
||||
`Invalid carPartDamage for part ${p.partId}: ${
|
||||
err instanceof Error ? err.message : String(err)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
return { ...p, carPartDamage };
|
||||
});
|
||||
|
||||
const { mixedFactorAndPrice, allFactorLines } = classifyV2ExpertPricingParts(
|
||||
processedParts,
|
||||
@@ -3317,40 +3342,35 @@ export class ExpertClaimService {
|
||||
};
|
||||
}
|
||||
|
||||
// Build damaged parts map
|
||||
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 damagedPartsData = claim.media?.damagedParts as any;
|
||||
const damagedParts: Record<
|
||||
string,
|
||||
{ label_fa: string; captured: boolean; url?: string }
|
||||
> = {};
|
||||
for (const p of claim.damage?.selectedParts || []) {
|
||||
const cap = getDamagedPartCaptureBlob(damagedPartsData, p) as
|
||||
| { url?: string; path?: string }
|
||||
| undefined;
|
||||
damagedParts[p] = {
|
||||
label_fa: getPartLabelFa(p),
|
||||
captured: !!cap,
|
||||
// Build damaged parts list (aligned with normalized selected parts)
|
||||
const carTypeExpert = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const selectedNormExpert = normalizeDamageSelectedParts(
|
||||
claim.damage?.selectedParts,
|
||||
carTypeExpert,
|
||||
(claim.damage as any)?.selectedOuterParts,
|
||||
);
|
||||
const damagedPartsDataExpert = claim.media?.damagedParts as any;
|
||||
const damagedParts = selectedNormExpert.map((sp, index) => {
|
||||
const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp);
|
||||
const cap = getDamagedPartCaptureBlob(
|
||||
damagedPartsDataExpert,
|
||||
ck,
|
||||
selectedNormExpert,
|
||||
) as { url?: string; path?: string } | undefined;
|
||||
return {
|
||||
index,
|
||||
id: sp.id,
|
||||
name: sp.name,
|
||||
side: sp.side,
|
||||
label_fa: sp.label_fa,
|
||||
captured: hasDamagedPartCapture(
|
||||
damagedPartsDataExpert,
|
||||
ck,
|
||||
selectedNormExpert,
|
||||
),
|
||||
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// --- Combine both branches of the conflict, preserving necessary logic from both ---
|
||||
|
||||
@@ -3480,7 +3500,7 @@ export class ExpertClaimService {
|
||||
blameRequestId: claim.blameRequestId?.toString(),
|
||||
blameRequestNo: claim.blameRequestNo,
|
||||
money: moneyPayload,
|
||||
selectedParts: claim.damage?.selectedParts,
|
||||
selectedParts: selectedNormExpert,
|
||||
otherParts: claim.damage?.otherParts,
|
||||
requiredDocuments:
|
||||
Object.keys(requiredDocumentsStatus).length > 0
|
||||
@@ -3535,35 +3555,70 @@ export class ExpertClaimService {
|
||||
|
||||
const damagedPartsEditSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
const carType = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||
const previousNorm = normalizeDamageSelectedParts(
|
||||
(claim as any).damage?.selectedParts,
|
||||
carType,
|
||||
(claim as any).damage?.selectedOuterParts,
|
||||
);
|
||||
const prevMedia = coerceDamagedPartsMediaToArray(
|
||||
(claim as any).media?.damagedParts,
|
||||
previousNorm,
|
||||
);
|
||||
|
||||
const nextNorm = body.selectedParts.map((p) => ({
|
||||
id: p.id ?? null,
|
||||
name: String(p.name || "").trim(),
|
||||
side: String(p.side || ""),
|
||||
label_fa: String(p.label_fa || "").trim() || String(p.name || "").trim(),
|
||||
...(p.catalogKey?.trim() ? { catalogKey: p.catalogKey.trim() } : {}),
|
||||
}));
|
||||
|
||||
const matchPreviousIndex = (
|
||||
next: (typeof nextNorm)[0],
|
||||
previous: typeof previousNorm,
|
||||
): number => {
|
||||
if (next.id != null) {
|
||||
const byId = previous.findIndex((x) => x.id === next.id);
|
||||
if (byId >= 0) return byId;
|
||||
}
|
||||
if (next.catalogKey) {
|
||||
const byCk = previous.findIndex((x) => x.catalogKey === next.catalogKey);
|
||||
if (byCk >= 0) return byCk;
|
||||
}
|
||||
return previous.findIndex(
|
||||
(x) => x.name === next.name && x.side === next.side,
|
||||
);
|
||||
};
|
||||
|
||||
const nextMedia = nextNorm.map((sp) => {
|
||||
const j = matchPreviousIndex(sp, previousNorm);
|
||||
const row =
|
||||
j >= 0 && prevMedia[j] && typeof prevMedia[j] === "object"
|
||||
? (prevMedia[j] as Record<string, unknown>)
|
||||
: {};
|
||||
return {
|
||||
...(sp.id != null ? { id: sp.id } : {}),
|
||||
name: sp.name,
|
||||
side: sp.side,
|
||||
label_fa: sp.label_fa,
|
||||
...(sp.catalogKey ? { catalogKey: sp.catalogKey } : {}),
|
||||
...(row.path ? { path: row.path } : {}),
|
||||
...(row.fileName ? { fileName: row.fileName } : {}),
|
||||
...(row.url ? { url: row.url } : {}),
|
||||
...(row.capturedAt ? { capturedAt: row.capturedAt } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
const previous = Array.isArray((claim as any).damage?.selectedParts)
|
||||
? [...(claim as any).damage.selectedParts]
|
||||
: [];
|
||||
const selectedParts = body.selectedParts as string[];
|
||||
|
||||
const $set: Record<string, unknown> = {
|
||||
"damage.selectedParts": selectedParts,
|
||||
"damage.selectedParts": nextNorm,
|
||||
"media.damagedParts": nextMedia,
|
||||
};
|
||||
|
||||
const damagedPartsMedia = (claim as any).media?.damagedParts;
|
||||
if (damagedPartsMedia) {
|
||||
const selectedSet = new Set(selectedParts);
|
||||
if (damagedPartsMedia instanceof Map) {
|
||||
const m = new Map(damagedPartsMedia as Map<string, unknown>);
|
||||
for (const key of Array.from(m.keys())) {
|
||||
if (!selectedSet.has(key)) m.delete(key);
|
||||
}
|
||||
$set["media.damagedParts"] = Object.fromEntries(m.entries());
|
||||
} else {
|
||||
const o = {
|
||||
...(damagedPartsMedia as Record<string, unknown>),
|
||||
};
|
||||
for (const key of Object.keys(o)) {
|
||||
if (!selectedSet.has(key)) delete o[key];
|
||||
}
|
||||
$set["media.damagedParts"] = o;
|
||||
}
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set,
|
||||
$push: {
|
||||
@@ -3577,7 +3632,7 @@ export class ExpertClaimService {
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
previousSelectedParts: previous,
|
||||
selectedParts,
|
||||
selectedParts: nextNorm,
|
||||
...(damagedPartsEditSnapshot && {
|
||||
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||
}),
|
||||
@@ -3588,7 +3643,7 @@ export class ExpertClaimService {
|
||||
|
||||
return {
|
||||
claimRequestId: claim._id.toString(),
|
||||
selectedParts,
|
||||
selectedParts: nextNorm,
|
||||
previousSelectedParts: previous,
|
||||
message: "Damaged parts updated successfully.",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user