1
0
forked from Yara724/api

Fixed claim/blame damagedParts

This commit is contained in:
2026-05-03 13:54:48 +03:30
parent c2d59112cf
commit c579f8fa1d
16 changed files with 1347 additions and 299 deletions

View File

@@ -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:

View File

@@ -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()

View File

@@ -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[];
}

View File

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