Merge pull request 'YARA-1177' (#235) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#235
This commit is contained in:
2026-07-31 19:50:03 +03:30
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 { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto"; import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto";
import { import {
OuterPartCatalogItemDto,
SelectOuterPartsV2Dto, SelectOuterPartsV2Dto,
SelectOuterPartsV2ResponseDto, SelectOuterPartsV2ResponseDto,
} from "./dto/select-outer-parts-v2.dto"; } from "./dto/select-outer-parts-v2.dto";
@@ -140,7 +141,7 @@ import {
} from "src/helpers/claim-car-angle-media"; } from "src/helpers/claim-car-angle-media";
import { import {
ClaimVehicleTypeV2, ClaimVehicleTypeV2,
OUTER_PARTS_BY_CAR_TYPE, FANAVARAN_CAR_PARTS_CATALOG,
OuterPartCatalogItem, OuterPartCatalogItem,
} from "src/static/outer-car-parts-catalog"; } from "src/static/outer-car-parts-catalog";
import { import {
@@ -563,68 +564,54 @@ export class ClaimRequestManagementService {
/** /**
* Outer-parts catalog returned to clients (user app + expert panel). * Outer-parts catalog returned to clients (user app + expert panel).
* *
* The shape intentionally mirrors how items are persisted under * Fetches the live `car/base-info/car-components` lookup from Fanavaran so
* `damage.selectedParts` so the front-end can match catalog rows to the * the list always matches what Fanavaran accepts in DmgSections[].DmgSectionId.
* stored selection by `id` / `catalogKey` without renaming fields: * The lookup service handles caching to disk; `carType` is ignored because
* * Fanavaran uses a single flat catalog across all vehicle types.
* { 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( async getOuterPartsCatalogV2(): Promise<OuterPartCatalogItemDto[]> {
carType?: ClaimVehicleTypeV2, const clientKey = resolveFanavaranClientKey();
): (DamageSelectedPartV2 & { carType: ClaimVehicleTypeV2 })[] { const rows = await this.getFanavaranLookupRows(clientKey, "car-components");
const buildForType = ( return rows
type: ClaimVehicleTypeV2, .filter(
items: OuterPartCatalogItem[], (r): r is { Id: number; Caption: string } =>
): (DamageSelectedPartV2 & { carType: ClaimVehicleTypeV2 })[] => !!r &&
items.map((p) => ({ typeof r === "object" &&
...catalogItemToSelectedPart(p, items), typeof (r as any).Id === "number" &&
carType: type, typeof (r as any).Caption === "string",
})); )
.map((r) => ({ id: r.Id, label_fa: r.Caption }));
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 * Resolve the live Fanavaran car-components catalog as `OuterPartCatalogItem`
* existing internal code that indexes by `key`/`titleFa` keeps working). * rows for internal use (e.g. validating selectedPartIds).
* Public API consumers should use `getOuterPartsCatalogV2` instead. * Falls back to the static snapshot when the remote lookup is unavailable.
*/ */
private getOuterPartsRawCatalog( private async getLiveFanavaranCatalogItems(): Promise<OuterPartCatalogItem[]> {
carType?: ClaimVehicleTypeV2, const clientKey = resolveFanavaranClientKey();
): OuterPartCatalogItem[] { const rows = await this.getFanavaranLookupRows(clientKey, "car-components");
if (carType) { if (!rows.length) {
return (OUTER_PARTS_BY_CAR_TYPE[carType] || []).map((p) => ({ this.logger.warn(
...p, "getLiveFanavaranCatalogItems: remote lookup returned empty, falling back to snapshot",
carType, );
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) { private userDamageDetail(blRequest: RequestManagementModel) {
const { firstPartyDetails: first, secondPartyDetails: second } = blRequest; const { firstPartyDetails: first, secondPartyDetails: second } = blRequest;
@@ -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 const selectedCarType = (body as any)?.carType as
| ClaimVehicleTypeV2 | ClaimVehicleTypeV2
| undefined; | 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>( const byId = new Map<number, OuterPartCatalogItem>(
catalog.map((p) => [p.id, p]), liveCatalog.map((p) => [p.id, p]),
);
const byKey = new Map<string, OuterPartCatalogItem>(
catalog.map((p) => [p.key, p]),
); );
const selectedFromIds: OuterPartCatalogItem[] = []; const selectedFromIds: OuterPartCatalogItem[] = [];
@@ -7655,43 +7634,20 @@ export class ClaimRequestManagementService {
const item = byId.get(id); const item = byId.get(id);
if (!item) { if (!item) {
throw new BadRequestException( 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); selectedFromIds.push(item);
} }
} }
// Backward compatibility with selectedParts + legacy carPartDamage if (selectedFromIds.length === 0) {
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,
);
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( throw new BadRequestException(
`Invalid outer part key for ${selectedCarType}: ${key}`, "selectedPartIds is required and must contain at least one valid Fanavaran part ID",
); );
} }
selectedFromKeys.push(item);
}
const selectedItems = const selectedItems = selectedFromIds;
selectedFromIds.length > 0 ? selectedFromIds : selectedFromKeys;
if (selectedItems.length === 0) {
throw new BadRequestException(
"selectedPartIds or selectedParts is required and must contain at least one item",
);
}
// DISABLED: At most two non-top sides allowed // DISABLED: At most two non-top sides allowed
// const sideSet = new Set( // const sideSet = new Set(
@@ -7706,7 +7662,7 @@ export class ClaimRequestManagementService {
// } // }
const selectedPartDocs = selectedItems.map((p) => const selectedPartDocs = selectedItems.map((p) =>
catalogItemToSelectedPart(p, catalog), catalogItemToSelectedPart(p, liveCatalog),
); );
const damagedPartsInitial = selectedPartDocs.map((p) => ({ const damagedPartsInitial = selectedPartDocs.map((p) => ({
id: p.id ?? undefined, id: p.id ?? undefined,
@@ -8061,18 +8017,12 @@ export class ClaimRequestManagementService {
"Only the claim owner can view capture requirements", "Only the claim owner can view capture requirements",
); );
// Build car-type aware outer-parts lookup (complete source: static catalog). // Build outer-parts lookup for resolving stored DB parts.
// Uses the raw catalog shape (`{id, key, titleFa, side, ...}`) because the // Uses the static snapshot (FANAVARAN_CAR_PARTS_CATALOG) which is keyed
// map below is keyed on the full `key` (e.g. `left_backfender`). // by numeric id (key === String(id)). This is only needed for legacy
const selectedCarType = claimCase.vehicle?.carType as // migration of parts stored before the Fanavaran catalog change.
| ClaimVehicleTypeV2
| undefined;
const catalogForType =
selectedCarType && OUTER_PARTS_BY_CAR_TYPE[selectedCarType]
? OUTER_PARTS_BY_CAR_TYPE[selectedCarType]
: this.getOuterPartsRawCatalog();
const catalogByKey = new Map<string, OuterPartCatalogItem>(); 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); if (!catalogByKey.has(item.key)) catalogByKey.set(item.key, item);
} }

View File

@@ -61,7 +61,6 @@ import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto"; import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
import { UserObjectionV2Dto } from "./dto/user-objection-v2.dto"; import { UserObjectionV2Dto } from "./dto/user-objection-v2.dto";
import { UserRatingDto } from "./dto/user-rating.dto"; import { UserRatingDto } from "./dto/user-rating.dto";
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
@ApiTags("claim-request-management (v2)") @ApiTags("claim-request-management (v2)")
@Controller("v2/claim-request-management") @Controller("v2/claim-request-management")
@@ -452,17 +451,15 @@ export class ClaimRequestManagementV2Controller {
@ApiOperation({ @ApiOperation({
summary: "Get outer parts catalog (V2)", summary: "Get outer parts catalog (V2)",
description: description:
"Returns outer-damage parts with id/key/side. Optional `carType` filter returns only that type catalog.", "Returns the Fanavaran car-components list. All vehicle types share the same catalog.",
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: "Outer parts catalog", description: "Outer parts catalog",
type: [OuterPartCatalogItemDto], type: [OuterPartCatalogItemDto],
}) })
async getOuterPartsCatalog( async getOuterPartsCatalog(): Promise<OuterPartCatalogItemDto[]> {
@Query("carType") carType?: ClaimVehicleTypeV2, return await this.claimRequestManagementService.getOuterPartsCatalogV2();
): Promise<OuterPartCatalogItemDto[]> {
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType);
} }
@Get("branches/:insuranceId") @Get("branches/:insuranceId")

View File

@@ -8,69 +8,20 @@ import {
IsOptional, IsOptional,
IsInt, IsInt,
} from "class-validator"; } from "class-validator";
import { import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
ClaimVehicleTypeV2,
OuterPartSideV2,
} from "src/static/outer-car-parts-catalog";
import { DamageSelectedPartV2BodyDto } from "./damage-selected-part-v2.dto"; import { DamageSelectedPartV2BodyDto } from "./damage-selected-part-v2.dto";
/** /**
* Enum for valid outer car parts that can be damaged * V2 DTO for selecting damaged outer car parts.
* Follows the naming convention from workflow step definition * Submit `selectedPartIds` with IDs from the `GET outer-parts-catalog` response.
*/
export enum OuterCarPart {
// Hood
HOOD = "hood",
// Doors
FRONT_RIGHT_DOOR = "front_right_door",
FRONT_LEFT_DOOR = "front_left_door",
REAR_RIGHT_DOOR = "rear_right_door",
REAR_LEFT_DOOR = "rear_left_door",
// Bumpers
FRONT_BUMPER = "front_bumper",
REAR_BUMPER = "rear_bumper",
// Fenders
FRONT_RIGHT_FENDER = "front_right_fender",
FRONT_LEFT_FENDER = "front_left_fender",
REAR_RIGHT_FENDER = "rear_right_fender",
REAR_LEFT_FENDER = "rear_left_fender",
// Trunk & Roof
TRUNK = "trunk",
ROOF = "roof",
}
/**
* V2 DTO for selecting damaged outer car parts
* Much cleaner than the nested boolean structure
*/ */
export class SelectOuterPartsV2Dto { export class SelectOuterPartsV2Dto {
@ApiProperty({ @ApiPropertyOptional({
description: "Array of selected damaged outer car parts", description:
example: ["hood", "front_right_door", "rear_bumper", "roof"], "Selected outer part IDs from the Fanavaran car-components catalog. " +
enum: OuterCarPart, "IDs must match values returned by GET outer-parts-catalog.",
isArray: true, example: [9, 10, 30],
minItems: 1,
maxItems: 13,
})
@IsOptional()
@IsArray({ message: "selectedParts must be an array" })
@ArrayMinSize(1, { message: "At least one damaged part must be selected" })
@ArrayUnique({ message: "Duplicate parts are not allowed" })
@IsEnum(OuterCarPart, {
each: true,
message: "Invalid part name. Must be one of the valid outer car parts",
})
selectedParts?: OuterCarPart[];
@ApiProperty({
description: "Selected outer part IDs from catalog",
example: [9, 10, 4],
type: [Number], type: [Number],
required: false,
}) })
@IsOptional() @IsOptional()
@IsArray({ message: "selectedPartIds must be an array" }) @IsArray({ message: "selectedPartIds must be an array" })
@@ -79,12 +30,13 @@ export class SelectOuterPartsV2Dto {
@IsInt({ each: true, message: "Each selected part ID must be an integer" }) @IsInt({ each: true, message: "Each selected part ID must be an integer" })
selectedPartIds?: number[]; selectedPartIds?: number[];
@ApiProperty({ @ApiPropertyOptional({
description: "Vehicle type for validating available outer parts", description:
"Vehicle type (sedan, suv, hatchback, pickup, van). Optional — stored for context only; " +
"all vehicle types share the same Fanavaran parts catalog.",
enum: ClaimVehicleTypeV2, enum: ClaimVehicleTypeV2,
required: true,
}) })
@IsNotEmpty() @IsOptional()
@IsEnum(ClaimVehicleTypeV2) @IsEnum(ClaimVehicleTypeV2)
carType?: ClaimVehicleTypeV2; carType?: ClaimVehicleTypeV2;
} }
@@ -156,53 +108,21 @@ export class SetClaimVehicleTypeV2Dto {
} }
/** /**
* Shape returned by `GET .../outer-parts-catalog` (both user and expert * Shape returned by `GET .../outer-parts-catalog`.
* controllers). It mirrors how items are persisted under * Sourced live from the Fanavaran `car/base-info/car-components` lookup so that
* `damage.selectedParts` (see `DamageSelectedPartV2BodyDto`) so the front-end * the list always matches what Fanavaran accepts in `DmgSections[].DmgSectionId`.
* can match catalog rows to stored selections without any field renaming:
* `name` is side-agnostic, `label_fa` is disambiguated with the side in
* parentheses, and the original full catalog key (`left_backfender`) is
* exposed as `catalogKey`.
*/ */
export class OuterPartCatalogItemDto { export class OuterPartCatalogItemDto {
@ApiProperty({ @ApiProperty({
description: "Static catalog id (unique across all car types)", description:
example: 102, "Fanavaran DmgSectionId — use this value when submitting selectedPartIds",
example: 30,
}) })
id: number; id: number;
@ApiProperty({ @ApiProperty({
description: "Side-agnostic part name (matches stored part `name`)", description: "Display label in Farsi (Caption from Fanavaran)",
example: "backWheel", example: "درب جلو سمت راننده",
})
name: string;
@ApiProperty({
description: "Vehicle side / region",
enum: OuterPartSideV2,
example: "left",
})
side: string;
@ApiProperty({
description:
"Display label in Farsi, with side disambiguator in parentheses when needed",
example: "چرخ عقب (چپ)",
}) })
label_fa: string; label_fa: string;
@ApiProperty({
description: "Original full catalog key (matches stored `catalogKey`)",
example: "left_backWheel",
required: false,
})
catalogKey?: string;
@ApiProperty({
description: "Vehicle type this catalog row belongs to",
enum: ClaimVehicleTypeV2,
required: false,
example: "suv",
})
carType?: ClaimVehicleTypeV2;
} }

View File

@@ -10,7 +10,6 @@ import {
Patch, Patch,
Post, Post,
Put, Put,
Query,
UploadedFile, UploadedFile,
UseGuards, UseGuards,
UseInterceptors, UseInterceptors,
@@ -33,7 +32,6 @@ import { CurrentUser } from "src/decorators/user.decorator";
import { MediaPolicyService } from "src/media-policy/media-policy.service"; import { MediaPolicyService } from "src/media-policy/media-policy.service";
import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service"; import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service";
import { RoleEnum } from "src/Types&Enums/role.enum"; import { RoleEnum } from "src/Types&Enums/role.enum";
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
import { ClaimRequestManagementService } from "./claim-request-management.service"; import { ClaimRequestManagementService } from "./claim-request-management.service";
import { import {
OuterPartCatalogItemDto, OuterPartCatalogItemDto,
@@ -101,15 +99,15 @@ export class ExpertInitiatedClaimMirrorController {
@ApiOperation({ @ApiOperation({
summary: "Get outer parts catalog (V2)", summary: "Get outer parts catalog (V2)",
description: description:
"Returns outer-damage parts with id/key/side. Optional `carType` filter returns only that type catalog.", "Returns the Fanavaran car-components list. All vehicle types share the same catalog.",
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: "Outer parts catalog", description: "Outer parts catalog",
type: [OuterPartCatalogItemDto], type: [OuterPartCatalogItemDto],
}) })
async getOuterPartsCatalog(@Query("carType") carType?: ClaimVehicleTypeV2) { async getOuterPartsCatalog() {
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType); return await this.claimRequestManagementService.getOuterPartsCatalogV2();
} }
@Get("car-other-part") @Get("car-other-part")

View File

@@ -7,7 +7,6 @@ import {
Param, Param,
Patch, Patch,
Post, Post,
Query,
UploadedFile, UploadedFile,
UseGuards, UseGuards,
UseInterceptors, UseInterceptors,
@@ -30,7 +29,6 @@ import { CurrentUser } from "src/decorators/user.decorator";
import { MediaPolicyService } from "src/media-policy/media-policy.service"; import { MediaPolicyService } from "src/media-policy/media-policy.service";
import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service"; import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service";
import { RoleEnum } from "src/Types&Enums/role.enum"; import { RoleEnum } from "src/Types&Enums/role.enum";
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
import { ClaimRequestManagementService } from "./claim-request-management.service"; import { ClaimRequestManagementService } from "./claim-request-management.service";
import { import {
OuterPartCatalogItemDto, OuterPartCatalogItemDto,
@@ -96,15 +94,15 @@ export class RegistrarClaimMirrorController {
@ApiOperation({ @ApiOperation({
summary: "Get outer parts catalog (V2)", summary: "Get outer parts catalog (V2)",
description: description:
"Returns outer-damage parts with id/key/side. Optional `carType` filter returns only that type catalog.", "Returns the Fanavaran car-components list. All vehicle types share the same catalog.",
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: "Outer parts catalog", description: "Outer parts catalog",
type: [OuterPartCatalogItemDto], type: [OuterPartCatalogItemDto],
}) })
async getOuterPartsCatalog(@Query("carType") carType?: ClaimVehicleTypeV2) { async getOuterPartsCatalog() {
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType); return await this.claimRequestManagementService.getOuterPartsCatalogV2();
} }
@Get("car-other-part") @Get("car-other-part")

View File

@@ -50,7 +50,6 @@ import {
import { FactorValidationV2Dto } from "./dto/factor-validation.dto"; import { FactorValidationV2Dto } from "./dto/factor-validation.dto";
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service"; import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
import { OuterPartCatalogItemDto } from "src/claim-request-management/dto/select-outer-parts-v2.dto"; import { OuterPartCatalogItemDto } from "src/claim-request-management/dto/select-outer-parts-v2.dto";
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
class InPersonVisitV2Dto { class InPersonVisitV2Dto {
@ApiPropertyOptional({ example: "Paint damage requires physical inspection" }) @ApiPropertyOptional({ example: "Paint damage requires physical inspection" })
@@ -105,17 +104,15 @@ export class ExpertClaimV2Controller {
@ApiOperation({ @ApiOperation({
summary: "Get outer parts catalog (V2)", summary: "Get outer parts catalog (V2)",
description: description:
"Returns outer-damage parts with id/key/side. Optional `carType` filter returns only that type catalog.", "Returns the Fanavaran car-components list. All vehicle types share the same catalog.",
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: "Outer parts catalog", description: "Outer parts catalog",
type: [OuterPartCatalogItemDto], type: [OuterPartCatalogItemDto],
}) })
async getOuterPartsCatalog( async getOuterPartsCatalog(): Promise<OuterPartCatalogItemDto[]> {
@Query("carType") carType?: ClaimVehicleTypeV2, return await this.claimRequestManagementService.getOuterPartsCatalogV2();
): Promise<OuterPartCatalogItemDto[]> {
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType);
} }
@Get("inner-parts-catalog") @Get("inner-parts-catalog")

View File

@@ -1,5 +1,6 @@
import { import {
ClaimVehicleTypeV2, ClaimVehicleTypeV2,
FANAVARAN_CAR_PARTS_CATALOG,
OUTER_PARTS_BY_CAR_TYPE, OUTER_PARTS_BY_CAR_TYPE,
OuterPartCatalogItem, OuterPartCatalogItem,
OuterPartSideV2, OuterPartSideV2,
@@ -93,12 +94,14 @@ export function sameCatalogPartId(
return na != null && nb != null && na === nb; return na != null && nb != null && na === nb;
} }
/** API part id: numeric catalog id only (`null` for internal / non-catalog lines). */ /** API part id: numeric catalog id only (`null` for free-text / non-catalog lines). */
export function catalogPartIdFromSelectedPart( export function catalogPartIdFromSelectedPart(
sp: DamageSelectedPartV2, sp: DamageSelectedPartV2,
): number | null { ): number | null {
if (isInternalDamageSide(sp.side)) return null; // Parts from the Fanavaran catalog always carry a numeric id regardless of side.
if (sp.id != null && Number.isFinite(sp.id)) return sp.id; if (sp.id != null && Number.isFinite(sp.id)) return sp.id;
// Legacy parts without an id and with an internal/unknown side are free-text only.
if (isInternalDamageSide(sp.side)) return null;
return null; return null;
} }
@@ -153,12 +156,13 @@ export function disambiguateOuterPartLabelFa(
return `${t} (${sideFa})`; return `${t} (${sideFa})`;
} }
const CATALOG_ITEM_BY_ID = new Map<number, OuterPartCatalogItem>(); /**
for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) { * Static id → item map for legacy DB part resolution.
for (const it of list) { * Built from the Fanavaran snapshot; unique because all car types share the same list.
CATALOG_ITEM_BY_ID.set(it.id, it); */
} const CATALOG_ITEM_BY_ID = new Map<number, OuterPartCatalogItem>(
} FANAVARAN_CAR_PARTS_CATALOG.map((it) => [it.id, it]),
);
function findCatalogItemByKey(key: string): OuterPartCatalogItem | undefined { function findCatalogItemByKey(key: string): OuterPartCatalogItem | undefined {
for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) { for (const list of Object.values(OUTER_PARTS_BY_CAR_TYPE)) {
@@ -681,7 +685,7 @@ export function normalizeCarPartDamageForExpertReply(
? o.side.trim().toLowerCase() ? o.side.trim().toLowerCase()
: ""; : "";
if (name && sideIn) { if (name) {
const id = toNum(o.id); const id = toNum(o.id);
const ck = const ck =
typeof o.catalogKey === "string" && o.catalogKey.trim() typeof o.catalogKey === "string" && o.catalogKey.trim()
@@ -689,36 +693,28 @@ export function normalizeCarPartDamageForExpertReply(
: undefined; : undefined;
const catalog = carType ? OUTER_PARTS_BY_CAR_TYPE[carType] : undefined; const catalog = carType ? OUTER_PARTS_BY_CAR_TYPE[carType] : undefined;
let catItem: OuterPartCatalogItem | undefined; let catItem: OuterPartCatalogItem | undefined;
if (catalog) { // Try id-based lookup first (works for Fanavaran parts which have no side)
if (id != null) {
catItem = catalog?.find((c) => c.id === id) ?? CATALOG_ITEM_BY_ID.get(id) ?? undefined;
}
if (!catItem && catalog) {
const byKey = new Map(catalog.map((c) => [c.key, c])); const byKey = new Map(catalog.map((c) => [c.key, c]));
if (ck) catItem = byKey.get(ck); if (ck) catItem = byKey.get(ck);
if (!catItem && id != null) catItem = catalog.find((c) => c.id === id); if (!catItem && sideIn) {
if (!catItem) {
catItem = byKey.get(catalogLikeKeyFromPart({ side: sideIn, name })); catItem = byKey.get(catalogLikeKeyFromPart({ side: sideIn, name }));
} }
} }
if (!catItem && id != null) { if (!catItem && ck) catItem = findCatalogItemByKey(ck);
catItem = CATALOG_ITEM_BY_ID.get(id!) ?? undefined; if (catItem) {
}
const list = catalog?.length
? catalog
: catItem
? outerCatalogListForItem(catItem)
: [];
if (catItem && list.length) {
return selectedPartToStoredRecord( return selectedPartToStoredRecord(
catalogItemToSelectedPart(catItem, list), catalogItemToSelectedPart(catItem, outerCatalogListForItem(catItem)),
); );
} }
const label_fa = const label_fa =
typeof o.label_fa === "string" && o.label_fa.trim() typeof o.label_fa === "string" && o.label_fa.trim()
? o.label_fa.trim() ? o.label_fa.trim()
: name; : name;
const out: Record<string, unknown> = { const out: Record<string, unknown> = { name, side: sideIn, label_fa };
name,
side: sideIn,
label_fa,
};
if (id != null) out.id = id; if (id != null) out.id = id;
if (ck) out.catalogKey = ck; if (ck) out.catalogKey = ck;
return out; return out;
@@ -731,9 +727,8 @@ export function normalizeCarPartDamageForExpertReply(
carType, carType,
); );
if (catItem) { if (catItem) {
const list = outerCatalogListForItem(catItem);
return selectedPartToStoredRecord( return selectedPartToStoredRecord(
catalogItemToSelectedPart(catItem, list), catalogItemToSelectedPart(catItem, outerCatalogListForItem(catItem)),
); );
} }
const sideFa = SIDE_LABEL_FA[legacySide] || legacySide; const sideFa = SIDE_LABEL_FA[legacySide] || legacySide;
@@ -744,7 +739,7 @@ export function normalizeCarPartDamageForExpertReply(
}; };
} }
throw new Error("carPartDamage must include side and (name or part)"); throw new Error("carPartDamage must include a name or part field");
} }
/** Best-effort migration for API output; never throws. */ /** Best-effort migration for API output; never throws. */

View File

@@ -0,0 +1,857 @@
[
{
"Caption": "جلو کامل",
"DmgBusinessLineId": 5454,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 1,
"ToDate": "1379/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "جلو چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 2,
"ToDate": "1379/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "جلو راست",
"DmgBusinessLineId": 5454,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 3,
"ToDate": "1379/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "عقب کامل",
"DmgBusinessLineId": 5454,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 4,
"ToDate": "1379/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "عقب چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 5,
"ToDate": "1379/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "عقب راست",
"DmgBusinessLineId": 5454,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 6,
"ToDate": "1379/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "بدنه راست",
"DmgBusinessLineId": 5454,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 7,
"ToDate": "1379/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "بدنه چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 8,
"ToDate": "1379/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "سپر جلو",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 9,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "سپر عقب",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 10,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "آينه سمت راننده",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 93,
"Id": 11,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "آينه سمت سرنشين",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 93,
"Id": 12,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "چراغ جلو چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 13,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "چراغ جلو راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 14,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "چراغ عقب چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 15,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "چراغ عقب راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 16,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "لاستيک جلو چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 90,
"Id": 17,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "لاستيک جلو راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 90,
"Id": 18,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "لاستيک عقب چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 90,
"Id": 19,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "لاستيک عقب راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 90,
"Id": 20,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "لاستيک زاپاس",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 90,
"Id": 21,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "رينگ جلو چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 90,
"Id": 22,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "رينگ جلو راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 90,
"Id": 23,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "رينگ عقب چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 90,
"Id": 24,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "رينگ عقب راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 90,
"Id": 25,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "راهنماي جلو چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 26,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "راهنماي جلو راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 27,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "راهنماي عقب چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 28,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "راهنماي عقب راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 29,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "درب جلو سمت راننده",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 30,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "درب جلو سمت سرنشين",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 31,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "درب عقب سمت راننده",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 32,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "درب عقب سمت سرنشين",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 33,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "گلگير جلو سمت راننده",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 34,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "گلگير جلو سمت سرنشين",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 35,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "گلگير عقب سمت راننده",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 36,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "گلگير عقب سمت سرنشين",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 37,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "ستون جلو چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 38,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "ستون جلو راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 39,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "ستون وسط چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 40,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "ستون وسط راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 41,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "ستون عقب چپ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 42,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "ستون عقب راست",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 43,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "شيشه جلو",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 95,
"Id": 44,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "شيشه عقب",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 95,
"Id": 45,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "سيني جلو (زير موتور)",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 46,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "درب موتور جلو",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 47,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "درب صندوق عقب",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 48,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "جلو پنجره",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 49,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "سقف خودرو",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 50,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "سانروف",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 51,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "سيني کف صندوق",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 52,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "متعلقات جلوبندي",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 53,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "متعلقات داخل خودرو",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 54,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "اگزوز و متعلقات",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 55,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "بلوکه سيلندر",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 56,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "موتور و متعلقات ( به غير از سيلندر)",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 57,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "شاسي جلو",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 58,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "شاسي عقب",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 59,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "ايربگ",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 60,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "شيشه پنجره درب جلو سمت راننده",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 95,
"Id": 61,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "شيشه پنجره درب جلو سمت سرنشين",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 95,
"Id": 62,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "شيشه پنجره درب عقب سمت راننده",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 95,
"Id": 63,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "شيشه پنجره درب عقب سمت سرنشين",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": 95,
"Id": 64,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "بادگير شيشه ها",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 65,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "برف پاک کن",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 66,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "رکاب ها",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 67,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "درپوش باک بنزين",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 68,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "سامانه برق رساني (به جزء باتري)",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 69,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "باتري",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 70,
"ToDate": "1999/12/29",
"UsedPlaceId": 5450
},
{
"Caption": "سيستم تهويه مطبوع (به جزء کولر)",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 71,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "کولر",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 72,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "خسارتهاي پنهان",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 73,
"ToDate": "1405/03/03",
"UsedPlaceId": 5449
},
{
"Caption": "کلاف",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 74,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "کف اتاق",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 75,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "سيني عقب",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 76,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "حسگرها",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 77,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "قفل در",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 78,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "لوازم تزيني",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 79,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "دستگيره ها",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 80,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "زه ها",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 81,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "ديفيوژر",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 82,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "رادياتور",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 83,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "فلاپ ها",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 84,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "دوربين",
"DmgBusinessLineId": 5454,
"FromDate": "1380/01/01",
"GroupId": null,
"Id": 85,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "اتاق",
"DmgBusinessLineId": 5454,
"FromDate": "1404/03/04",
"GroupId": null,
"Id": 86,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "موتور",
"DmgBusinessLineId": 5454,
"FromDate": "1404/03/04",
"GroupId": null,
"Id": 87,
"ToDate": "1999/12/29",
"UsedPlaceId": 5449
},
{
"Caption": "سيستم صوتي و تصويري",
"DmgBusinessLineId": 5453,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 88,
"ToDate": "1999/12/29",
"UsedPlaceId": 5451
},
{
"Caption": "ساير وسايل صوتي",
"DmgBusinessLineId": 5453,
"FromDate": "1300/01/01",
"GroupId": 88,
"Id": 89,
"ToDate": "1999/12/29",
"UsedPlaceId": 5450
},
{
"Caption": "رينگ و لاستيك",
"DmgBusinessLineId": 5453,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 90,
"ToDate": "1999/12/29",
"UsedPlaceId": 5448
},
{
"Caption": "ساير (بجز لوازم صوتي و تصويري )",
"DmgBusinessLineId": 5453,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 91,
"ToDate": "1999/12/29",
"UsedPlaceId": 5450
},
{
"Caption": "كليه قطعات فابريك",
"DmgBusinessLineId": 5453,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 92,
"ToDate": "1999/12/29",
"UsedPlaceId": 5448
},
{
"Caption": "آينه بغل",
"DmgBusinessLineId": 5453,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 93,
"ToDate": "1999/12/29",
"UsedPlaceId": 5448
},
{
"Caption": "راديو پخش",
"DmgBusinessLineId": 5453,
"FromDate": "1300/01/01",
"GroupId": 88,
"Id": 94,
"ToDate": "1999/12/29",
"UsedPlaceId": 5450
},
{
"Caption": "شيشه",
"DmgBusinessLineId": 5453,
"FromDate": "1300/01/01",
"GroupId": null,
"Id": 95,
"ToDate": "1999/12/29",
"UsedPlaceId": 5448
}
]

View File

@@ -1,3 +1,14 @@
/**
* Outer car parts catalog — shaped around the Fanavaran `car/base-info/car-components`
* API response.
*
* The *live* catalog is fetched at runtime through `FanavaranLookupService` so
* any changes Fanavaran makes to their part list are picked up automatically.
* `FANAVARAN_CAR_PARTS_CATALOG` below is a **static fallback snapshot** used
* only for legacy in-DB part resolution (i.e. parts that were stored before
* this change). It must not be used as the source for API responses.
*/
export enum ClaimVehicleTypeV2 { export enum ClaimVehicleTypeV2 {
SEDAN = "sedan", SEDAN = "sedan",
SUV = "suv", SUV = "suv",
@@ -16,58 +27,132 @@ export enum OuterPartSideV2 {
export type OuterPartCatalogItem = { export type OuterPartCatalogItem = {
id: number; id: number;
/** String key — equals `String(id)` for Fanavaran-sourced items. */
key: string; key: string;
titleFa: string; titleFa: string;
side: OuterPartSideV2; /** Side is encoded in the Farsi caption for Fanavaran items; empty string for those. */
side: OuterPartSideV2 | "";
carType?: ClaimVehicleTypeV2; carType?: ClaimVehicleTypeV2;
}; };
const baseFor = (offset: number): OuterPartCatalogItem[] => [ /**
// left * Snapshot of the Fanavaran `car/base-info/car-components` lookup.
{ id: offset + 1, key: "left_backfender", titleFa: "گلگیر عقب", side: OuterPartSideV2.LEFT }, * Used ONLY as a static fallback when resolving legacy part IDs already stored
{ id: offset + 2, key: "left_backWheel", titleFa: "چرخ عقب", side: OuterPartSideV2.LEFT }, * in the database. All live API endpoints must call `FanavaranLookupService`
{ id: offset + 3, key: "left_backDoor", titleFa: "در عقب", side: OuterPartSideV2.LEFT }, * instead.
{ id: offset + 4, key: "left_frontDoor", titleFa: "در جلو", side: OuterPartSideV2.LEFT }, */
{ id: offset + 5, key: "left_mirror", titleFa: "آیینه", side: OuterPartSideV2.LEFT }, export const FANAVARAN_CAR_PARTS_CATALOG: OuterPartCatalogItem[] = [
{ id: offset + 6, key: "left_frontWheel", titleFa: "چرخ جلو", side: OuterPartSideV2.LEFT }, { id: 1, key: "1", titleFa: "جلو کامل", side: "" },
{ id: offset + 7, key: "left_frontFender", titleFa: "گلگیر جلو", side: OuterPartSideV2.LEFT }, { id: 2, key: "2", titleFa: "جلو چپ", side: "" },
{ id: offset + 8, key: "left_backWindow", titleFa: "شیشه عقب", side: OuterPartSideV2.LEFT }, { id: 3, key: "3", titleFa: "جلو راست", side: "" },
{ id: offset + 9, key: "left_frontWindow", titleFa: "شیشه جلو", side: OuterPartSideV2.LEFT }, { id: 4, key: "4", titleFa: "عقب کامل", side: "" },
// right { id: 5, key: "5", titleFa: "عقب چپ", side: "" },
{ id: offset + 10, key: "right_backfender", titleFa: "گلگیر عقب", side: OuterPartSideV2.RIGHT }, { id: 6, key: "6", titleFa: "عقب راست", side: "" },
{ id: offset + 11, key: "right_backWheel", titleFa: "چرخ عقب", side: OuterPartSideV2.RIGHT }, { id: 7, key: "7", titleFa: "بدنه راست", side: "" },
{ id: offset + 12, key: "right_backDoor", titleFa: "در عقب", side: OuterPartSideV2.RIGHT }, { id: 8, key: "8", titleFa: "بدنه چپ", side: "" },
{ id: offset + 13, key: "right_frontDoor", titleFa: "در جلو", side: OuterPartSideV2.RIGHT }, { id: 9, key: "9", titleFa: "سپر جلو", side: "" },
{ id: offset + 14, key: "right_mirror", titleFa: "آیینه", side: OuterPartSideV2.RIGHT }, { id: 10, key: "10", titleFa: "سپر عقب", side: "" },
{ id: offset + 15, key: "right_frontWheel", titleFa: "چرخ جلو", side: OuterPartSideV2.RIGHT }, { id: 11, key: "11", titleFa: "آينه سمت راننده", side: "" },
{ id: offset + 16, key: "right_frontFender", titleFa: "گلگیر جلو", side: OuterPartSideV2.RIGHT }, { id: 12, key: "12", titleFa: "آينه سمت سرنشين", side: "" },
{ id: offset + 17, key: "right_backWindow", titleFa: "شیشه عقب", side: OuterPartSideV2.RIGHT }, { id: 13, key: "13", titleFa: "چراغ جلو چپ", side: "" },
{ id: offset + 18, key: "right_frontWindow", titleFa: "شیشه جلو", side: OuterPartSideV2.RIGHT }, { id: 14, key: "14", titleFa: "چراغ جلو راست", side: "" },
// front { id: 15, key: "15", titleFa: "چراغ عقب چپ", side: "" },
{ id: offset + 19, key: "front_frontBumper", titleFa: "سپر جلو", side: OuterPartSideV2.FRONT }, { id: 16, key: "16", titleFa: "چراغ عقب راست", side: "" },
{ id: offset + 20, key: "front_frontCarWindshield", titleFa: "شیشه جلو", side: OuterPartSideV2.FRONT }, { id: 17, key: "17", titleFa: "لاستيک جلو چپ", side: "" },
{ id: offset + 21, key: "front_carHood", titleFa: "کاپوت", side: OuterPartSideV2.FRONT }, { id: 18, key: "18", titleFa: "لاستيک جلو راست", side: "" },
{ id: offset + 22, key: "front_leftLight", titleFa: "چراغ چپ", side: OuterPartSideV2.FRONT }, { id: 19, key: "19", titleFa: "لاستيک عقب چپ", side: "" },
{ id: offset + 23, key: "front_rightLight", titleFa: "چراغ راست", side: OuterPartSideV2.FRONT }, { id: 20, key: "20", titleFa: "لاستيک عقب راست", side: "" },
{ id: offset + 24, key: "front_frontGrille", titleFa: "جلو پنجره", side: OuterPartSideV2.FRONT }, { id: 21, key: "21", titleFa: "لاستيک زاپاس", side: "" },
// back { id: 22, key: "22", titleFa: "رينگ جلو چپ", side: "" },
{ id: offset + 25, key: "back_backBumper", titleFa: "سپر عقب", side: OuterPartSideV2.BACK }, { id: 23, key: "23", titleFa: "رينگ جلو راست", side: "" },
{ id: offset + 26, key: "back_carTrunk", titleFa: "صندوق عقب", side: OuterPartSideV2.BACK }, { id: 24, key: "24", titleFa: "رينگ عقب چپ", side: "" },
{ id: offset + 27, key: "back_backCarWindshield", titleFa: "شیشه عقب", side: OuterPartSideV2.BACK }, { id: 25, key: "25", titleFa: "رينگ عقب راست", side: "" },
{ id: offset + 28, key: "back_leftLight", titleFa: "چراغ چپ", side: OuterPartSideV2.BACK }, { id: 26, key: "26", titleFa: "راهنماي جلو چپ", side: "" },
{ id: offset + 29, key: "back_rightLight", titleFa: "چراغ راست", side: OuterPartSideV2.BACK }, { id: 27, key: "27", titleFa: "راهنماي جلو راست", side: "" },
// top { id: 28, key: "28", titleFa: "راهنماي عقب چپ", side: "" },
{ id: offset + 30, key: "top_roof", titleFa: "سقف", side: OuterPartSideV2.TOP }, { id: 29, key: "29", titleFa: "راهنماي عقب راست", side: "" },
{ id: 30, key: "30", titleFa: "درب جلو سمت راننده", side: "" },
{ id: 31, key: "31", titleFa: "درب جلو سمت سرنشين", side: "" },
{ id: 32, key: "32", titleFa: "درب عقب سمت راننده", side: "" },
{ id: 33, key: "33", titleFa: "درب عقب سمت سرنشين", side: "" },
{ id: 34, key: "34", titleFa: "گلگير جلو سمت راننده", side: "" },
{ id: 35, key: "35", titleFa: "گلگير جلو سمت سرنشين", side: "" },
{ id: 36, key: "36", titleFa: "گلگير عقب سمت راننده", side: "" },
{ id: 37, key: "37", titleFa: "گلگير عقب سمت سرنشين", side: "" },
{ id: 38, key: "38", titleFa: "ستون جلو چپ", side: "" },
{ id: 39, key: "39", titleFa: "ستون جلو راست", side: "" },
{ id: 40, key: "40", titleFa: "ستون وسط چپ", side: "" },
{ id: 41, key: "41", titleFa: "ستون وسط راست", side: "" },
{ id: 42, key: "42", titleFa: "ستون عقب چپ", side: "" },
{ id: 43, key: "43", titleFa: "ستون عقب راست", side: "" },
{ id: 44, key: "44", titleFa: "شيشه جلو", side: "" },
{ id: 45, key: "45", titleFa: "شيشه عقب", side: "" },
{ id: 46, key: "46", titleFa: "سيني جلو (زير موتور)", side: "" },
{ id: 47, key: "47", titleFa: "درب موتور جلو", side: "" },
{ id: 48, key: "48", titleFa: "درب صندوق عقب", side: "" },
{ id: 49, key: "49", titleFa: "جلو پنجره", side: "" },
{ id: 50, key: "50", titleFa: "سقف خودرو", side: "" },
{ id: 51, key: "51", titleFa: "سانروف", side: "" },
{ id: 52, key: "52", titleFa: "سيني کف صندوق", side: "" },
{ id: 53, key: "53", titleFa: "متعلقات جلوبندي", side: "" },
{ id: 54, key: "54", titleFa: "متعلقات داخل خودرو", side: "" },
{ id: 55, key: "55", titleFa: "اگزوز و متعلقات", side: "" },
{ id: 56, key: "56", titleFa: "بلوکه سيلندر", side: "" },
{ id: 57, key: "57", titleFa: "موتور و متعلقات ( به غير از سيلندر)", side: "" },
{ id: 58, key: "58", titleFa: "شاسي جلو", side: "" },
{ id: 59, key: "59", titleFa: "شاسي عقب", side: "" },
{ id: 60, key: "60", titleFa: "ايربگ", side: "" },
{ id: 61, key: "61", titleFa: "شيشه پنجره درب جلو سمت راننده", side: "" },
{ id: 62, key: "62", titleFa: "شيشه پنجره درب جلو سمت سرنشين", side: "" },
{ id: 63, key: "63", titleFa: "شيشه پنجره درب عقب سمت راننده", side: "" },
{ id: 64, key: "64", titleFa: "شيشه پنجره درب عقب سمت سرنشين", side: "" },
{ id: 65, key: "65", titleFa: "بادگير شيشه ها", side: "" },
{ id: 66, key: "66", titleFa: "برف پاک کن", side: "" },
{ id: 67, key: "67", titleFa: "رکاب ها", side: "" },
{ id: 68, key: "68", titleFa: "درپوش باک بنزين", side: "" },
{ id: 69, key: "69", titleFa: "سامانه برق رساني (به جزء باتري)", side: "" },
{ id: 70, key: "70", titleFa: "باتري", side: "" },
{ id: 71, key: "71", titleFa: "سيستم تهويه مطبوع (به جزء کولر)", side: "" },
{ id: 72, key: "72", titleFa: "کولر", side: "" },
{ id: 73, key: "73", titleFa: "خسارتهاي پنهان", side: "" },
{ id: 74, key: "74", titleFa: "کلاف", side: "" },
{ id: 75, key: "75", titleFa: "کف اتاق", side: "" },
{ id: 76, key: "76", titleFa: "سيني عقب", side: "" },
{ id: 77, key: "77", titleFa: "حسگرها", side: "" },
{ id: 78, key: "78", titleFa: "قفل در", side: "" },
{ id: 79, key: "79", titleFa: "لوازم تزيني", side: "" },
{ id: 80, key: "80", titleFa: "دستگيره ها", side: "" },
{ id: 81, key: "81", titleFa: "زه ها", side: "" },
{ id: 82, key: "82", titleFa: "ديفيوژر", side: "" },
{ id: 83, key: "83", titleFa: "رادياتور", side: "" },
{ id: 84, key: "84", titleFa: "فلاپ ها", side: "" },
{ id: 85, key: "85", titleFa: "دوربين", side: "" },
{ id: 86, key: "86", titleFa: "اتاق", side: "" },
{ id: 87, key: "87", titleFa: "موتور", side: "" },
{ id: 88, key: "88", titleFa: "سيستم صوتي و تصويري", side: "" },
{ id: 89, key: "89", titleFa: "ساير وسايل صوتي", side: "" },
{ id: 90, key: "90", titleFa: "رينگ و لاستيك", side: "" },
{ id: 91, key: "91", titleFa: "ساير (بجز لوازم صوتي و تصويري )", side: "" },
{ id: 92, key: "92", titleFa: "كليه قطعات فابريك", side: "" },
{ id: 93, key: "93", titleFa: "آينه بغل", side: "" },
{ id: 94, key: "94", titleFa: "راديو پخش", side: "" },
{ id: 95, key: "95", titleFa: "شيشه", side: "" },
]; ];
/**
* Backward-compatible alias used by `outer-damage-parts.ts` for legacy DB part
* resolution. All vehicle types share the same flat Fanavaran catalog because
* Fanavaran does not differentiate parts by car type.
*
* Do NOT use this for serving API responses — use `FanavaranLookupService` instead.
*/
export const OUTER_PARTS_BY_CAR_TYPE: Record< export const OUTER_PARTS_BY_CAR_TYPE: Record<
ClaimVehicleTypeV2, ClaimVehicleTypeV2,
OuterPartCatalogItem[] OuterPartCatalogItem[]
> = { > = {
[ClaimVehicleTypeV2.SEDAN]: baseFor(0), [ClaimVehicleTypeV2.SEDAN]: FANAVARAN_CAR_PARTS_CATALOG,
[ClaimVehicleTypeV2.SUV]: baseFor(100), [ClaimVehicleTypeV2.SUV]: FANAVARAN_CAR_PARTS_CATALOG,
[ClaimVehicleTypeV2.HATCHBACK]: baseFor(200), [ClaimVehicleTypeV2.HATCHBACK]: FANAVARAN_CAR_PARTS_CATALOG,
[ClaimVehicleTypeV2.PICKUP]: baseFor(300), [ClaimVehicleTypeV2.PICKUP]: FANAVARAN_CAR_PARTS_CATALOG,
[ClaimVehicleTypeV2.VAN]: baseFor(400), [ClaimVehicleTypeV2.VAN]: FANAVARAN_CAR_PARTS_CATALOG,
}; };