forked from Yara724/api
Merge pull request 'YARA-1177' (#235) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#235
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
|
||||
import { UserObjectionV2Dto } from "./dto/user-objection-v2.dto";
|
||||
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
|
||||
@ApiTags("claim-request-management (v2)")
|
||||
@Controller("v2/claim-request-management")
|
||||
@@ -452,17 +451,15 @@ export class ClaimRequestManagementV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Get outer parts catalog (V2)",
|
||||
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({
|
||||
status: 200,
|
||||
description: "Outer parts catalog",
|
||||
type: [OuterPartCatalogItemDto],
|
||||
})
|
||||
async getOuterPartsCatalog(
|
||||
@Query("carType") carType?: ClaimVehicleTypeV2,
|
||||
): Promise<OuterPartCatalogItemDto[]> {
|
||||
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType);
|
||||
async getOuterPartsCatalog(): Promise<OuterPartCatalogItemDto[]> {
|
||||
return await this.claimRequestManagementService.getOuterPartsCatalogV2();
|
||||
}
|
||||
|
||||
@Get("branches/:insuranceId")
|
||||
|
||||
@@ -8,69 +8,20 @@ import {
|
||||
IsOptional,
|
||||
IsInt,
|
||||
} from "class-validator";
|
||||
import {
|
||||
ClaimVehicleTypeV2,
|
||||
OuterPartSideV2,
|
||||
} from "src/static/outer-car-parts-catalog";
|
||||
import { ClaimVehicleTypeV2 } 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
|
||||
* Follows the naming convention from workflow step definition
|
||||
*/
|
||||
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
|
||||
* V2 DTO for selecting damaged outer car parts.
|
||||
* Submit `selectedPartIds` with IDs from the `GET outer-parts-catalog` response.
|
||||
*/
|
||||
export class SelectOuterPartsV2Dto {
|
||||
@ApiProperty({
|
||||
description: "Array of selected damaged outer car parts",
|
||||
example: ["hood", "front_right_door", "rear_bumper", "roof"],
|
||||
enum: OuterCarPart,
|
||||
isArray: true,
|
||||
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],
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Selected outer part IDs from the Fanavaran car-components catalog. " +
|
||||
"IDs must match values returned by GET outer-parts-catalog.",
|
||||
example: [9, 10, 30],
|
||||
type: [Number],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@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" })
|
||||
selectedPartIds?: number[];
|
||||
|
||||
@ApiProperty({
|
||||
description: "Vehicle type for validating available outer parts",
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Vehicle type (sedan, suv, hatchback, pickup, van). Optional — stored for context only; " +
|
||||
"all vehicle types share the same Fanavaran parts catalog.",
|
||||
enum: ClaimVehicleTypeV2,
|
||||
required: true,
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsOptional()
|
||||
@IsEnum(ClaimVehicleTypeV2)
|
||||
carType?: ClaimVehicleTypeV2;
|
||||
}
|
||||
@@ -156,53 +108,21 @@ export class SetClaimVehicleTypeV2Dto {
|
||||
}
|
||||
|
||||
/**
|
||||
* Shape returned by `GET .../outer-parts-catalog` (both user and expert
|
||||
* controllers). It mirrors how items are persisted under
|
||||
* `damage.selectedParts` (see `DamageSelectedPartV2BodyDto`) so the front-end
|
||||
* 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`.
|
||||
* Shape returned by `GET .../outer-parts-catalog`.
|
||||
* Sourced live from the Fanavaran `car/base-info/car-components` lookup so that
|
||||
* the list always matches what Fanavaran accepts in `DmgSections[].DmgSectionId`.
|
||||
*/
|
||||
export class OuterPartCatalogItemDto {
|
||||
@ApiProperty({
|
||||
description: "Static catalog id (unique across all car types)",
|
||||
example: 102,
|
||||
description:
|
||||
"Fanavaran DmgSectionId — use this value when submitting selectedPartIds",
|
||||
example: 30,
|
||||
})
|
||||
id: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Side-agnostic part name (matches stored part `name`)",
|
||||
example: "backWheel",
|
||||
})
|
||||
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: "چرخ عقب (چپ)",
|
||||
description: "Display label in Farsi (Caption from Fanavaran)",
|
||||
example: "درب جلو سمت راننده",
|
||||
})
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
@@ -33,7 +32,6 @@ import { CurrentUser } from "src/decorators/user.decorator";
|
||||
import { MediaPolicyService } from "src/media-policy/media-policy.service";
|
||||
import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service";
|
||||
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 {
|
||||
OuterPartCatalogItemDto,
|
||||
@@ -101,15 +99,15 @@ export class ExpertInitiatedClaimMirrorController {
|
||||
@ApiOperation({
|
||||
summary: "Get outer parts catalog (V2)",
|
||||
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({
|
||||
status: 200,
|
||||
description: "Outer parts catalog",
|
||||
type: [OuterPartCatalogItemDto],
|
||||
})
|
||||
async getOuterPartsCatalog(@Query("carType") carType?: ClaimVehicleTypeV2) {
|
||||
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType);
|
||||
async getOuterPartsCatalog() {
|
||||
return await this.claimRequestManagementService.getOuterPartsCatalogV2();
|
||||
}
|
||||
|
||||
@Get("car-other-part")
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
@@ -30,7 +29,6 @@ import { CurrentUser } from "src/decorators/user.decorator";
|
||||
import { MediaPolicyService } from "src/media-policy/media-policy.service";
|
||||
import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service";
|
||||
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 {
|
||||
OuterPartCatalogItemDto,
|
||||
@@ -96,15 +94,15 @@ export class RegistrarClaimMirrorController {
|
||||
@ApiOperation({
|
||||
summary: "Get outer parts catalog (V2)",
|
||||
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({
|
||||
status: 200,
|
||||
description: "Outer parts catalog",
|
||||
type: [OuterPartCatalogItemDto],
|
||||
})
|
||||
async getOuterPartsCatalog(@Query("carType") carType?: ClaimVehicleTypeV2) {
|
||||
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType);
|
||||
async getOuterPartsCatalog() {
|
||||
return await this.claimRequestManagementService.getOuterPartsCatalogV2();
|
||||
}
|
||||
|
||||
@Get("car-other-part")
|
||||
|
||||
Reference in New Issue
Block a user