1
0
forked from Yara724/api
This commit is contained in:
SepehrYahyaee
2026-04-27 17:03:29 +03:30
parent c2f996cc28
commit e90f8dc33c
5 changed files with 288 additions and 28 deletions

View File

@@ -84,6 +84,11 @@ import {
normalizeResendPartKeys, normalizeResendPartKeys,
partKeyAllowedForExpertResend, partKeyAllowedForExpertResend,
} from "src/helpers/claim-expert-resend"; } from "src/helpers/claim-expert-resend";
import {
ClaimVehicleTypeV2,
OUTER_PARTS_BY_CAR_TYPE,
OuterPartCatalogItem,
} from "src/static/outer-car-parts-catalog";
@Injectable() @Injectable()
export class ClaimRequestManagementService { export class ClaimRequestManagementService {
@@ -132,6 +137,26 @@ export class ClaimRequestManagementService {
return new Promise((resolve) => setTimeout(resolve, ms)); 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) { private userDamageDetail(blRequest: RequestManagementModel) {
const { firstPartyDetails: first, secondPartyDetails: second } = blRequest; const { firstPartyDetails: first, secondPartyDetails: second } = blRequest;
switch (blRequest.expertSubmitReply.guiltyUserId) { switch (blRequest.expertSubmitReply.guiltyUserId) {
@@ -3734,8 +3759,37 @@ export class ClaimRequestManagementService {
); );
} }
// 5. Convert selected parts to storage format (simple array of strings) // 5. Validate by selected car type and resolve selected parts by ids/keys
// Backward compatibility: some clients may still send legacy `carPartDamage` object. 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<number, OuterPartCatalogItem>(catalog.map((p) => [p.id, p]));
const byKey = new Map<string, OuterPartCatalogItem>(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) let selectedParts = Array.isArray((body as any)?.selectedParts)
? ((body as any).selectedParts as string[]) ? ((body as any).selectedParts as string[])
: []; : [];
@@ -3747,17 +3801,52 @@ export class ClaimRequestManagementService {
(body as any).carPartDamage as CarDamagePartDto, (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( 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 // 6. Update claim case with selected parts and move to next step
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate( const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
claimRequestId, claimRequestId,
{ {
'damage.selectedParts': selectedParts, 'damage.selectedParts': selectedParts,
'damage.selectedOuterParts': selectedOuterParts,
'vehicle.carType': selectedCarType,
'status': ClaimCaseStatus.SELECTING_OTHER_PARTS, 'status': ClaimCaseStatus.SELECTING_OTHER_PARTS,
'workflow.currentStep': ClaimWorkflowStep.SELECT_OTHER_PARTS, 'workflow.currentStep': ClaimWorkflowStep.SELECT_OTHER_PARTS,
'workflow.nextStep': ClaimWorkflowStep.CAPTURE_PART_DAMAGES, 'workflow.nextStep': ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
@@ -3774,6 +3863,8 @@ export class ClaimRequestManagementService {
metadata: { metadata: {
stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS, stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS,
selectedParts: selectedParts, selectedParts: selectedParts,
selectedPartIds,
carType: selectedCarType,
partsCount: selectedParts?.length, partsCount: selectedParts?.length,
description: `User selected ${selectedParts?.length} damaged outer parts`, description: `User selected ${selectedParts?.length} damaged outer parts`,
}, },
@@ -3795,6 +3886,7 @@ export class ClaimRequestManagementService {
claimRequestId: updatedClaim._id.toString(), claimRequestId: updatedClaim._id.toString(),
publicId: updatedClaim.publicId, publicId: updatedClaim.publicId,
selectedParts: selectedParts, selectedParts: selectedParts,
selectedPartIds,
currentStep: ClaimWorkflowStep.SELECT_OTHER_PARTS, currentStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
nextStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, nextStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
message: 'Outer parts selected successfully. Please proceed to select other parts and provide bank information.', message: 'Outer parts selected successfully. Please proceed to select other parts and provide bank information.',

View File

@@ -3,6 +3,7 @@ import {
HttpException, HttpException,
InternalServerErrorException, InternalServerErrorException,
Param, Param,
Query,
Post, Post,
Patch, Patch,
Put, Put,
@@ -22,7 +23,11 @@ import { Roles } from "src/decorators/roles.decorator";
import { CurrentUser } from "src/decorators/user.decorator"; import { CurrentUser } from "src/decorators/user.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum"; import { RoleEnum } from "src/Types&Enums/role.enum";
import { ClaimRequestManagementService } from "./claim-request-management.service"; 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 { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto } from "./dto/select-other-parts-v2.dto";
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto"; import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-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 { 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")
@@ -250,6 +256,23 @@ export class ClaimRequestManagementV2Controller {
/** /**
* V2 API: Select damaged outer car parts (Step 2 of claim workflow) * 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<OuterPartCatalogItemDto[]> {
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType);
}
@Patch("select-outer-parts/:claimRequestId") @Patch("select-outer-parts/:claimRequestId")
@ApiOperation({ @ApiOperation({
summary: "Select Damaged Outer Car Parts (V2 - Step 2)", summary: "Select Damaged Outer Car Parts (V2 - Step 2)",
@@ -284,41 +307,35 @@ export class ClaimRequestManagementV2Controller {
}) })
@ApiBody({ @ApiBody({
type: SelectOuterPartsV2Dto, type: SelectOuterPartsV2Dto,
description: "Array of selected damaged outer parts", description:
"Selected vehicle type + selected outer part IDs from catalog",
examples: { examples: {
example1: { example1: {
summary: "Minor front damage", summary: "Sedan - minor front damage",
value: { value: {
selectedParts: ["hood", "front_bumper", "front_right_fender"], carType: "sedan",
selectedPartIds: [19, 21, 16],
}, },
}, },
example2: { example2: {
summary: "Side impact damage", summary: "SUV - left side impact",
value: { value: {
selectedParts: [ carType: "suv",
"front_left_door", selectedPartIds: [102, 103, 104, 107],
"rear_left_door",
"front_left_fender",
"rear_left_fender",
],
}, },
}, },
example3: { example3: {
summary: "Rear-end collision", summary: "Hatchback - rear-end collision",
value: { value: {
selectedParts: ["rear_bumper", "trunk", "rear_right_fender"], carType: "hatchback",
selectedPartIds: [225, 226, 210],
}, },
}, },
example4: { example4: {
summary: "Multiple damage areas", summary: "Pickup - two sides + roof",
value: { value: {
selectedParts: [ carType: "pickup",
"hood", selectedPartIds: [319, 312, 330],
"front_bumper",
"front_right_door",
"rear_bumper",
"trunk",
],
}, },
}, },
}, },

View File

@@ -1,5 +1,9 @@
import { ApiProperty } from '@nestjs/swagger'; 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 * Enum for valid outer car parts that can be damaged
@@ -43,7 +47,7 @@ export class SelectOuterPartsV2Dto {
minItems: 1, minItems: 1,
maxItems: 13, maxItems: 13,
}) })
@IsNotEmpty({ message: 'Selected parts array cannot be empty' }) @IsOptional()
@IsArray({ message: 'selectedParts must be an array' }) @IsArray({ message: 'selectedParts must be an array' })
@ArrayMinSize(1, { message: 'At least one damaged part must be selected' }) @ArrayMinSize(1, { message: 'At least one damaged part must be selected' })
@ArrayUnique({ message: 'Duplicate parts are not allowed' }) @ArrayUnique({ message: 'Duplicate parts are not allowed' })
@@ -51,7 +55,29 @@ export class SelectOuterPartsV2Dto {
each: true, each: true,
message: 'Invalid part name. Must be one of the valid outer car parts', 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[]; selectedParts: string[];
@ApiProperty({
description: 'Selected part IDs',
example: [9, 7, 11],
type: [Number],
})
selectedPartIds: number[];
@ApiProperty({ @ApiProperty({
description: 'Current workflow step', description: 'Current workflow step',
example: 'SELECT_OUTER_PARTS', example: 'SELECT_OUTER_PARTS',
@@ -94,3 +127,30 @@ export class SelectOuterPartsV2ResponseDto {
}) })
message: string; 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;
}

View File

@@ -1,6 +1,20 @@
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { CarDamagePartModel, CarDamagePartOtherModel } from "./car-parts.schema"; 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 }) @Schema({ _id: false })
export class ClaimDamageSelection { export class ClaimDamageSelection {
/** /**
@@ -10,6 +24,10 @@ export class ClaimDamageSelection {
@Prop({ type: [String], default: [] }) @Prop({ type: [String], default: [] })
selectedParts?: string[]; 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 * V2: Array of selected other (non-body) damaged parts
* Examples: ['engine', 'suspension', 'headlight'] * Examples: ['engine', 'suspension', 'headlight']

View File

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