YARA-1177

This commit is contained in:
2026-07-31 19:45:33 +03:30
parent e742d43201
commit 77cf420c33
9 changed files with 1100 additions and 303 deletions

View File

@@ -38,6 +38,7 @@ import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto";
import {
OuterPartCatalogItemDto,
SelectOuterPartsV2Dto,
SelectOuterPartsV2ResponseDto,
} from "./dto/select-outer-parts-v2.dto";
@@ -140,7 +141,7 @@ import {
} from "src/helpers/claim-car-angle-media";
import {
ClaimVehicleTypeV2,
OUTER_PARTS_BY_CAR_TYPE,
FANAVARAN_CAR_PARTS_CATALOG,
OuterPartCatalogItem,
} from "src/static/outer-car-parts-catalog";
import {
@@ -563,67 +564,53 @@ export class ClaimRequestManagementService {
/**
* 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.
* Fetches the live `car/base-info/car-components` lookup from Fanavaran so
* the list always matches what Fanavaran accepts in DmgSections[].DmgSectionId.
* The lookup service handles caching to disk; `carType` is ignored because
* Fanavaran uses a single flat catalog across all vehicle types.
*/
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));
});
async getOuterPartsCatalogV2(): Promise<OuterPartCatalogItemDto[]> {
const clientKey = resolveFanavaranClientKey();
const rows = await this.getFanavaranLookupRows(clientKey, "car-components");
return rows
.filter(
(r): r is { Id: number; Caption: string } =>
!!r &&
typeof r === "object" &&
typeof (r as any).Id === "number" &&
typeof (r as any).Caption === "string",
)
.map((r) => ({ id: r.Id, label_fa: r.Caption }));
}
/**
* 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.
* Resolve the live Fanavaran car-components catalog as `OuterPartCatalogItem`
* rows for internal use (e.g. validating selectedPartIds).
* Falls back to the static snapshot when the remote lookup is unavailable.
*/
private getOuterPartsRawCatalog(
carType?: ClaimVehicleTypeV2,
): OuterPartCatalogItem[] {
if (carType) {
return (OUTER_PARTS_BY_CAR_TYPE[carType] || []).map((p) => ({
...p,
carType,
private async getLiveFanavaranCatalogItems(): Promise<OuterPartCatalogItem[]> {
const clientKey = resolveFanavaranClientKey();
const rows = await this.getFanavaranLookupRows(clientKey, "car-components");
if (!rows.length) {
this.logger.warn(
"getLiveFanavaranCatalogItems: remote lookup returned empty, falling back to snapshot",
);
return FANAVARAN_CAR_PARTS_CATALOG;
}
return rows
.filter(
(r): r is { Id: number; Caption: string } =>
!!r &&
typeof r === "object" &&
typeof (r as any).Id === "number" &&
typeof (r as any).Caption === "string",
)
.map((r): OuterPartCatalogItem => ({
id: r.Id,
key: String(r.Id),
titleFa: r.Caption,
side: "",
}));
}
const out: OuterPartCatalogItem[] = [];
for (const [type, items] of Object.entries(OUTER_PARTS_BY_CAR_TYPE)) {
const t = type as ClaimVehicleTypeV2;
for (const p of items) {
out.push({ ...p, carType: t });
}
}
return out;
}
private userDamageDetail(blRequest: RequestManagementModel) {
@@ -7628,22 +7615,14 @@ export class ClaimRequestManagementService {
);
}
// 5. Validate by selected car type and resolve selected parts by ids/keys
// 5. Fetch the live Fanavaran catalog and resolve selected parts by ids
const selectedCarType = (body as any)?.carType as
| ClaimVehicleTypeV2
| undefined;
if (!selectedCarType || !OUTER_PARTS_BY_CAR_TYPE[selectedCarType]) {
throw new BadRequestException(
"Vehicle type is required before selecting outer parts.",
);
}
const catalog = OUTER_PARTS_BY_CAR_TYPE[selectedCarType];
const liveCatalog = await this.getLiveFanavaranCatalogItems();
const byId = new Map<number, OuterPartCatalogItem>(
catalog.map((p) => [p.id, p]),
);
const byKey = new Map<string, OuterPartCatalogItem>(
catalog.map((p) => [p.key, p]),
liveCatalog.map((p) => [p.id, p]),
);
const selectedFromIds: OuterPartCatalogItem[] = [];
@@ -7655,43 +7634,20 @@ export class ClaimRequestManagementService {
const item = byId.get(id);
if (!item) {
throw new BadRequestException(
`Invalid outer part id for ${selectedCarType}: ${id}`,
`Invalid outer part id: ${id}. Use GET outer-parts-catalog to see valid IDs.`,
);
}
selectedFromIds.push(item);
}
}
// Backward compatibility with selectedParts + legacy carPartDamage
let selectedParts = Array.isArray((body as any)?.selectedParts)
? ((body as any).selectedParts as string[])
: [];
if (selectedParts.length === 0 && (body as any)?.carPartDamage) {
this.assertCarPartDamageAtMostTwoOfFourSides(
(body as any).carPartDamage as CarDamagePartDto,
if (selectedFromIds.length === 0) {
throw new BadRequestException(
"selectedPartIds is required and must contain at least one valid Fanavaran part ID",
);
selectedParts = this.carDamagePartDtoToOuterPartSlugs(
(body as any).carPartDamage as CarDamagePartDto,
);
}
const selectedFromKeys: OuterPartCatalogItem[] = [];
for (const key of selectedParts) {
const item = byKey.get(key);
if (!item) {
throw new BadRequestException(
`Invalid outer part key for ${selectedCarType}: ${key}`,
);
}
selectedFromKeys.push(item);
}
const selectedItems =
selectedFromIds.length > 0 ? selectedFromIds : selectedFromKeys;
if (selectedItems.length === 0) {
throw new BadRequestException(
"selectedPartIds or selectedParts is required and must contain at least one item",
);
}
const selectedItems = selectedFromIds;
// DISABLED: At most two non-top sides allowed
// const sideSet = new Set(
@@ -7706,7 +7662,7 @@ export class ClaimRequestManagementService {
// }
const selectedPartDocs = selectedItems.map((p) =>
catalogItemToSelectedPart(p, catalog),
catalogItemToSelectedPart(p, liveCatalog),
);
const damagedPartsInitial = selectedPartDocs.map((p) => ({
id: p.id ?? undefined,
@@ -8061,18 +8017,12 @@ export class ClaimRequestManagementService {
"Only the claim owner can view capture requirements",
);
// 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.getOuterPartsRawCatalog();
// Build outer-parts lookup for resolving stored DB parts.
// Uses the static snapshot (FANAVARAN_CAR_PARTS_CATALOG) which is keyed
// by numeric id (key === String(id)). This is only needed for legacy
// migration of parts stored before the Fanavaran catalog change.
const catalogByKey = new Map<string, OuterPartCatalogItem>();
for (const item of catalogForType) {
for (const item of FANAVARAN_CAR_PARTS_CATALOG) {
if (!catalogByKey.has(item.key)) catalogByKey.set(item.key, item);
}