From e90f8dc33cf3722e5f303d63b6333a7598672957 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 27 Apr 2026 17:03:29 +0330 Subject: [PATCH] YARA-833 --- .../claim-request-management.service.ts | 100 +++++++++++++++++- .../claim-request-management.v2.controller.ts | 59 +++++++---- .../dto/select-outer-parts-v2.dto.ts | 66 +++++++++++- .../schema/claim-case.damage.schema.ts | 18 ++++ src/static/outer-car-parts-catalog.ts | 73 +++++++++++++ 5 files changed, 288 insertions(+), 28 deletions(-) create mode 100644 src/static/outer-car-parts-catalog.ts diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index a93a0ec..eff852d 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -84,6 +84,11 @@ import { normalizeResendPartKeys, partKeyAllowedForExpertResend, } from "src/helpers/claim-expert-resend"; +import { + ClaimVehicleTypeV2, + OUTER_PARTS_BY_CAR_TYPE, + OuterPartCatalogItem, +} from "src/static/outer-car-parts-catalog"; @Injectable() export class ClaimRequestManagementService { @@ -132,6 +137,26 @@ export class ClaimRequestManagementService { return new Promise((resolve) => setTimeout(resolve, ms)); } + getOuterPartsCatalogV2(carType?: ClaimVehicleTypeV2): OuterPartCatalogItem[] { + if (carType) { + return (OUTER_PARTS_BY_CAR_TYPE[carType] || []).map((p) => ({ + ...p, + carType, + })); + } + 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.sort((a, b) => { + if (a.carType === b.carType) return a.id - b.id; + return String(a.carType).localeCompare(String(b.carType)); + }); + } + private userDamageDetail(blRequest: RequestManagementModel) { const { firstPartyDetails: first, secondPartyDetails: second } = blRequest; switch (blRequest.expertSubmitReply.guiltyUserId) { @@ -3734,8 +3759,37 @@ export class ClaimRequestManagementService { ); } - // 5. Convert selected parts to storage format (simple array of strings) - // Backward compatibility: some clients may still send legacy `carPartDamage` object. + // 5. Validate by selected car type and resolve selected parts by ids/keys + 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 byId = new Map(catalog.map((p) => [p.id, p])); + const byKey = new Map(catalog.map((p) => [p.key, p])); + + const selectedFromIds: OuterPartCatalogItem[] = []; + if ( + Array.isArray((body as any)?.selectedPartIds) && + (body as any).selectedPartIds.length > 0 + ) { + for (const id of (body as any).selectedPartIds as number[]) { + const item = byId.get(id); + if (!item) { + throw new BadRequestException( + `Invalid outer part id for ${selectedCarType}: ${id}`, + ); + } + selectedFromIds.push(item); + } + } + + // Backward compatibility with selectedParts + legacy carPartDamage let selectedParts = Array.isArray((body as any)?.selectedParts) ? ((body as any).selectedParts as string[]) : []; @@ -3747,17 +3801,52 @@ export class ClaimRequestManagementService { (body as any).carPartDamage as CarDamagePartDto, ); } - if (selectedParts.length === 0) { + 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( - "selectedParts is required and must contain at least one item", + "selectedPartIds or selectedParts is required and must contain at least one item", ); } + // At most two non-top sides allowed + const sideSet = new Set( + selectedItems + .map((p) => p.side) + .filter((s) => s !== "top"), + ); + if (sideSet.size > 2) { + throw new BadRequestException( + `At most two of left/right/front/back can be selected. Selected: ${[...sideSet].join(", ")}`, + ); + } + + selectedParts = selectedItems.map((p) => p.key); + const selectedPartIds = selectedItems.map((p) => p.id); + const selectedOuterParts = selectedItems.map((p) => ({ + id: p.id, + key: p.key, + side: p.side, + })); + // 6. Update claim case with selected parts and move to next step const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate( claimRequestId, { 'damage.selectedParts': selectedParts, + 'damage.selectedOuterParts': selectedOuterParts, + 'vehicle.carType': selectedCarType, 'status': ClaimCaseStatus.SELECTING_OTHER_PARTS, 'workflow.currentStep': ClaimWorkflowStep.SELECT_OTHER_PARTS, 'workflow.nextStep': ClaimWorkflowStep.CAPTURE_PART_DAMAGES, @@ -3774,6 +3863,8 @@ export class ClaimRequestManagementService { metadata: { stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS, selectedParts: selectedParts, + selectedPartIds, + carType: selectedCarType, partsCount: selectedParts?.length, description: `User selected ${selectedParts?.length} damaged outer parts`, }, @@ -3795,6 +3886,7 @@ export class ClaimRequestManagementService { claimRequestId: updatedClaim._id.toString(), publicId: updatedClaim.publicId, selectedParts: selectedParts, + selectedPartIds, currentStep: ClaimWorkflowStep.SELECT_OTHER_PARTS, nextStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, message: 'Outer parts selected successfully. Please proceed to select other parts and provide bank information.', diff --git a/src/claim-request-management/claim-request-management.v2.controller.ts b/src/claim-request-management/claim-request-management.v2.controller.ts index d2b3874..14165db 100644 --- a/src/claim-request-management/claim-request-management.v2.controller.ts +++ b/src/claim-request-management/claim-request-management.v2.controller.ts @@ -3,6 +3,7 @@ import { HttpException, InternalServerErrorException, Param, + Query, Post, Patch, Put, @@ -22,7 +23,11 @@ import { Roles } from "src/decorators/roles.decorator"; import { CurrentUser } from "src/decorators/user.decorator"; import { RoleEnum } from "src/Types&Enums/role.enum"; import { ClaimRequestManagementService } from "./claim-request-management.service"; -import { SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto } from "./dto/select-outer-parts-v2.dto"; +import { + OuterPartCatalogItemDto, + SelectOuterPartsV2Dto, + SelectOuterPartsV2ResponseDto, +} from "./dto/select-outer-parts-v2.dto"; import { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto } from "./dto/select-other-parts-v2.dto"; import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto"; import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-v2.dto"; @@ -35,6 +40,7 @@ import { GetMyClaimsV2ResponseDto } from "./dto/my-claims-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") @@ -250,6 +256,23 @@ export class ClaimRequestManagementV2Controller { /** * V2 API: Select damaged outer car parts (Step 2 of claim workflow) */ + @Get("outer-parts-catalog") + @ApiOperation({ + summary: "Get outer parts catalog (V2)", + description: + "Returns outer-damage parts with id/key/side. Optional `carType` filter returns only that type catalog.", + }) + @ApiResponse({ + status: 200, + description: "Outer parts catalog", + type: [OuterPartCatalogItemDto], + }) + async getOuterPartsCatalog( + @Query("carType") carType?: ClaimVehicleTypeV2, + ): Promise { + return this.claimRequestManagementService.getOuterPartsCatalogV2(carType); + } + @Patch("select-outer-parts/:claimRequestId") @ApiOperation({ summary: "Select Damaged Outer Car Parts (V2 - Step 2)", @@ -284,41 +307,35 @@ export class ClaimRequestManagementV2Controller { }) @ApiBody({ type: SelectOuterPartsV2Dto, - description: "Array of selected damaged outer parts", + description: + "Selected vehicle type + selected outer part IDs from catalog", examples: { example1: { - summary: "Minor front damage", + summary: "Sedan - minor front damage", value: { - selectedParts: ["hood", "front_bumper", "front_right_fender"], + carType: "sedan", + selectedPartIds: [19, 21, 16], }, }, example2: { - summary: "Side impact damage", + summary: "SUV - left side impact", value: { - selectedParts: [ - "front_left_door", - "rear_left_door", - "front_left_fender", - "rear_left_fender", - ], + carType: "suv", + selectedPartIds: [102, 103, 104, 107], }, }, example3: { - summary: "Rear-end collision", + summary: "Hatchback - rear-end collision", value: { - selectedParts: ["rear_bumper", "trunk", "rear_right_fender"], + carType: "hatchback", + selectedPartIds: [225, 226, 210], }, }, example4: { - summary: "Multiple damage areas", + summary: "Pickup - two sides + roof", value: { - selectedParts: [ - "hood", - "front_bumper", - "front_right_door", - "rear_bumper", - "trunk", - ], + carType: "pickup", + selectedPartIds: [319, 312, 330], }, }, }, diff --git a/src/claim-request-management/dto/select-outer-parts-v2.dto.ts b/src/claim-request-management/dto/select-outer-parts-v2.dto.ts index f1e956d..3f8a1e8 100644 --- a/src/claim-request-management/dto/select-outer-parts-v2.dto.ts +++ b/src/claim-request-management/dto/select-outer-parts-v2.dto.ts @@ -1,5 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsArray, IsEnum, IsNotEmpty, ArrayMinSize, ArrayUnique } from 'class-validator'; +import { IsArray, IsEnum, IsNotEmpty, ArrayMinSize, ArrayUnique, IsOptional, IsInt } from 'class-validator'; +import { + ClaimVehicleTypeV2, + OuterPartSideV2, +} from "src/static/outer-car-parts-catalog"; /** * Enum for valid outer car parts that can be damaged @@ -43,7 +47,7 @@ export class SelectOuterPartsV2Dto { minItems: 1, maxItems: 13, }) - @IsNotEmpty({ message: 'Selected parts array cannot be empty' }) + @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' }) @@ -51,7 +55,29 @@ export class SelectOuterPartsV2Dto { each: true, message: 'Invalid part name. Must be one of the valid outer car parts', }) - selectedParts: OuterCarPart[]; + selectedParts?: OuterCarPart[]; + + @ApiProperty({ + description: 'Selected outer part IDs from catalog', + example: [9, 10, 4], + type: [Number], + required: false, + }) + @IsOptional() + @IsArray({ message: 'selectedPartIds must be an array' }) + @ArrayMinSize(1, { message: 'At least one part ID must be selected' }) + @ArrayUnique({ message: 'Duplicate part IDs are not allowed' }) + @IsInt({ each: true, message: 'Each selected part ID must be an integer' }) + selectedPartIds?: number[]; + + @ApiProperty({ + description: 'Vehicle type for validating available outer parts', + enum: ClaimVehicleTypeV2, + required: true, + }) + @IsNotEmpty() + @IsEnum(ClaimVehicleTypeV2) + carType?: ClaimVehicleTypeV2; } /** @@ -76,6 +102,13 @@ export class SelectOuterPartsV2ResponseDto { }) selectedParts: string[]; + @ApiProperty({ + description: 'Selected part IDs', + example: [9, 7, 11], + type: [Number], + }) + selectedPartIds: number[]; + @ApiProperty({ description: 'Current workflow step', example: 'SELECT_OUTER_PARTS', @@ -94,3 +127,30 @@ export class SelectOuterPartsV2ResponseDto { }) message: string; } + +export class SetClaimVehicleTypeV2Dto { + @ApiProperty({ + description: "Vehicle type selected by user", + enum: ClaimVehicleTypeV2, + }) + @IsNotEmpty() + @IsEnum(ClaimVehicleTypeV2) + carType: ClaimVehicleTypeV2; +} + +export class OuterPartCatalogItemDto { + @ApiProperty() + id: number; + + @ApiProperty() + key: string; + + @ApiProperty() + titleFa: string; + + @ApiProperty({ enum: OuterPartSideV2 }) + side: OuterPartSideV2; + + @ApiProperty({ enum: ClaimVehicleTypeV2, required: false }) + carType?: ClaimVehicleTypeV2; +} diff --git a/src/claim-request-management/entites/schema/claim-case.damage.schema.ts b/src/claim-request-management/entites/schema/claim-case.damage.schema.ts index 6dfb5fd..4065bbd 100644 --- a/src/claim-request-management/entites/schema/claim-case.damage.schema.ts +++ b/src/claim-request-management/entites/schema/claim-case.damage.schema.ts @@ -1,6 +1,20 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { CarDamagePartModel, CarDamagePartOtherModel } from "./car-parts.schema"; +@Schema({ _id: false }) +export class SelectedOuterPartV2 { + @Prop({ type: Number }) + id: number; + + @Prop({ type: String }) + key: string; + + @Prop({ type: String }) + side: string; +} +export const SelectedOuterPartV2Schema = + SchemaFactory.createForClass(SelectedOuterPartV2); + @Schema({ _id: false }) export class ClaimDamageSelection { /** @@ -10,6 +24,10 @@ export class ClaimDamageSelection { @Prop({ type: [String], default: [] }) selectedParts?: string[]; + /** Structured selected outer parts with id + side for better downstream handling. */ + @Prop({ type: [SelectedOuterPartV2Schema], default: [] }) + selectedOuterParts?: SelectedOuterPartV2[]; + /** * V2: Array of selected other (non-body) damaged parts * Examples: ['engine', 'suspension', 'headlight'] diff --git a/src/static/outer-car-parts-catalog.ts b/src/static/outer-car-parts-catalog.ts new file mode 100644 index 0000000..3ad8a59 --- /dev/null +++ b/src/static/outer-car-parts-catalog.ts @@ -0,0 +1,73 @@ +export enum ClaimVehicleTypeV2 { + SEDAN = "sedan", + SUV = "suv", + HATCHBACK = "hatchback", + PICKUP = "pickup", + VAN = "van", +} + +export enum OuterPartSideV2 { + LEFT = "left", + RIGHT = "right", + FRONT = "front", + BACK = "back", + TOP = "top", +} + +export type OuterPartCatalogItem = { + id: number; + key: string; + titleFa: string; + side: OuterPartSideV2; + carType?: ClaimVehicleTypeV2; +}; + +const baseFor = (offset: number): OuterPartCatalogItem[] => [ + // left + { id: offset + 1, key: "left_backfender", titleFa: "گلگیر عقب", side: OuterPartSideV2.LEFT }, + { id: offset + 2, key: "left_backWheel", titleFa: "چرخ عقب", side: OuterPartSideV2.LEFT }, + { id: offset + 3, key: "left_backDoor", titleFa: "در عقب", side: OuterPartSideV2.LEFT }, + { id: offset + 4, key: "left_frontDoor", titleFa: "در جلو", side: OuterPartSideV2.LEFT }, + { id: offset + 5, key: "left_mirror", titleFa: "آیینه", side: OuterPartSideV2.LEFT }, + { id: offset + 6, key: "left_frontWheel", titleFa: "چرخ جلو", side: OuterPartSideV2.LEFT }, + { id: offset + 7, key: "left_frontFender", titleFa: "گلگیر جلو", side: OuterPartSideV2.LEFT }, + { id: offset + 8, key: "left_backWindow", titleFa: "شیشه عقب", side: OuterPartSideV2.LEFT }, + { id: offset + 9, key: "left_frontWindow", titleFa: "شیشه جلو", side: OuterPartSideV2.LEFT }, + // right + { id: offset + 10, key: "right_backfender", titleFa: "گلگیر عقب", side: OuterPartSideV2.RIGHT }, + { id: offset + 11, key: "right_backWheel", titleFa: "چرخ عقب", side: OuterPartSideV2.RIGHT }, + { id: offset + 12, key: "right_backDoor", titleFa: "در عقب", side: OuterPartSideV2.RIGHT }, + { id: offset + 13, key: "right_frontDoor", titleFa: "در جلو", side: OuterPartSideV2.RIGHT }, + { id: offset + 14, key: "right_mirror", titleFa: "آیینه", side: OuterPartSideV2.RIGHT }, + { id: offset + 15, key: "right_frontWheel", titleFa: "چرخ جلو", side: OuterPartSideV2.RIGHT }, + { id: offset + 16, key: "right_frontFender", titleFa: "گلگیر جلو", side: OuterPartSideV2.RIGHT }, + { id: offset + 17, key: "right_backWindow", titleFa: "شیشه عقب", side: OuterPartSideV2.RIGHT }, + { id: offset + 18, key: "right_frontWindow", titleFa: "شیشه جلو", side: OuterPartSideV2.RIGHT }, + // front + { id: offset + 19, key: "front_frontBumper", titleFa: "سپر جلو", side: OuterPartSideV2.FRONT }, + { id: offset + 20, key: "front_frontCarWindshield", titleFa: "شیشه جلو", side: OuterPartSideV2.FRONT }, + { id: offset + 21, key: "front_carHood", titleFa: "کاپوت", side: OuterPartSideV2.FRONT }, + { id: offset + 22, key: "front_leftLight", titleFa: "چراغ چپ", side: OuterPartSideV2.FRONT }, + { id: offset + 23, key: "front_rightLight", titleFa: "چراغ راست", side: OuterPartSideV2.FRONT }, + { id: offset + 24, key: "front_frontGrille", titleFa: "جلو پنجره", side: OuterPartSideV2.FRONT }, + // back + { id: offset + 25, key: "back_backBumper", titleFa: "سپر عقب", side: OuterPartSideV2.BACK }, + { id: offset + 26, key: "back_carTrunk", titleFa: "صندوق عقب", side: OuterPartSideV2.BACK }, + { id: offset + 27, key: "back_backCarWindshield", titleFa: "شیشه عقب", side: OuterPartSideV2.BACK }, + { id: offset + 28, key: "back_leftLight", titleFa: "چراغ چپ", side: OuterPartSideV2.BACK }, + { id: offset + 29, key: "back_rightLight", titleFa: "چراغ راست", side: OuterPartSideV2.BACK }, + // top + { id: offset + 30, key: "top_roof", titleFa: "سقف", side: OuterPartSideV2.TOP }, +]; + +export const OUTER_PARTS_BY_CAR_TYPE: Record< + ClaimVehicleTypeV2, + OuterPartCatalogItem[] +> = { + [ClaimVehicleTypeV2.SEDAN]: baseFor(0), + [ClaimVehicleTypeV2.SUV]: baseFor(100), + [ClaimVehicleTypeV2.HATCHBACK]: baseFor(200), + [ClaimVehicleTypeV2.PICKUP]: baseFor(300), + [ClaimVehicleTypeV2.VAN]: baseFor(400), +}; +