diff --git a/src/Types&Enums/claim-request-management/type-of-damage.enum.ts b/src/Types&Enums/claim-request-management/type-of-damage.enum.ts index 3b10a39..8c0e37f 100644 --- a/src/Types&Enums/claim-request-management/type-of-damage.enum.ts +++ b/src/Types&Enums/claim-request-management/type-of-damage.enum.ts @@ -1,4 +1,4 @@ export enum TypeOfDamage { - Repair = "repair", - Change = "change", + Repair = "تعمیر", + Change = "تعویض", } diff --git a/src/expert-claim/dto/expert-claim-v2.dto.spec.ts b/src/expert-claim/dto/expert-claim-v2.dto.spec.ts index ffc0bbc..d731c70 100644 --- a/src/expert-claim/dto/expert-claim-v2.dto.spec.ts +++ b/src/expert-claim/dto/expert-claim-v2.dto.spec.ts @@ -1,6 +1,7 @@ import { plainToInstance } from "class-transformer"; import { validate } from "class-validator"; import { DaghiOption } from "src/Types&Enums/claim-request-management/daghi-option.enum"; +import { TypeOfDamage } from "src/Types&Enums/claim-request-management/type-of-damage.enum"; import { SubmitExpertReplyV2Dto } from "./expert-claim-v2.dto"; describe("SubmitExpertReplyV2Dto", () => { @@ -10,7 +11,7 @@ describe("SubmitExpertReplyV2Dto", () => { parts: [ { partId: 201, - typeOfDamage: "Minor", + typeOfDamage: TypeOfDamage.Repair, price: "", salary: "", totalPayment: "", @@ -24,4 +25,22 @@ describe("SubmitExpertReplyV2Dto", () => { expect(errors).not.toHaveLength(0); }); + + it("accepts a replacement part without a price", async () => { + const dto = plainToInstance(SubmitExpertReplyV2Dto, { + description: "Damage assessment", + parts: [ + { + partId: 201, + typeOfDamage: TypeOfDamage.Change, + salary: "0", + totalPayment: "0", + factorNeeded: false, + daghi: { option: DaghiOption.NO_VALUE }, + }, + ], + }); + + expect(await validate(dto)).toHaveLength(0); + }); }); diff --git a/src/expert-claim/dto/expert-claim-v2.dto.ts b/src/expert-claim/dto/expert-claim-v2.dto.ts index d68e38d..8c12e96 100644 --- a/src/expert-claim/dto/expert-claim-v2.dto.ts +++ b/src/expert-claim/dto/expert-claim-v2.dto.ts @@ -6,6 +6,7 @@ import { ArrayMinSize, IsBoolean, IsOptional, + ValidateIf, ValidateNested, IsEnum, IsInt, @@ -15,6 +16,7 @@ import { IsRepairLineAmountToman } from 'src/common/validators/repair-line-amoun import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum'; import { DamagedPartItem } from 'src/claim-request-management/dto/capture-requirements-v2.dto'; import { DaghiOption } from 'src/Types&Enums/claim-request-management/daghi-option.enum'; +import { TypeOfDamage } from 'src/Types&Enums/claim-request-management/type-of-damage.enum'; export class DaghiDetailsV2Dto { @ApiProperty({ enum: DaghiOption, @@ -27,8 +29,12 @@ export class DaghiDetailsV2Dto { @ApiPropertyOptional({ description: `Required when option is '${DaghiOption.RECYCLED_PARTS_VALUE}' (Toman)`, }) - @IsOptional() + @ValidateIf( + (daghi: DaghiDetailsV2Dto) => + daghi.option === DaghiOption.RECYCLED_PARTS_VALUE, + ) @IsString() + @IsNotEmpty() @IsRepairLineAmountToman() price?: string; @@ -48,14 +54,23 @@ export class PartPricingV2Dto { @IsInt() partId: number; - @ApiProperty({ example: 'Minor' }) - @IsString() - typeOfDamage: string; - @ApiProperty({ - example: "5000000", - description: "Part price in Toman (integer string). Use 0 if the full amount is in salary.", + enum: TypeOfDamage, + description: "'repair' requires price; 'change' may omit it.", }) + @IsEnum(TypeOfDamage) + typeOfDamage: TypeOfDamage; + + @ApiPropertyOptional({ + example: "5000000", + description: "Required for repair lines; omitted for change lines. Use 0 if the full amount is in salary.", + }) + @ValidateIf( + (part: PartPricingV2Dto) => + part.typeOfDamage === TypeOfDamage.Repair || + (part.price != null && + (typeof part.price !== 'string' || part.price.trim() !== '')), + ) @IsString() @IsNotEmpty() @IsRepairLineAmountToman({ allowZero: true }) @@ -76,10 +91,11 @@ export class PartPricingV2Dto { }) @IsString() @IsNotEmpty() - @IsRepairLineAmountToman() + @IsRepairLineAmountToman({ allowZero: true }) totalPayment: string; @ApiProperty({ type: DaghiDetailsV2Dto }) + @IsNotEmpty() @ValidateNested() @Type(() => DaghiDetailsV2Dto) daghi: DaghiDetailsV2Dto; diff --git a/src/expert-claim/expert-claim.service.spec.ts b/src/expert-claim/expert-claim.service.spec.ts index 9309c3a..3455681 100644 --- a/src/expert-claim/expert-claim.service.spec.ts +++ b/src/expert-claim/expert-claim.service.spec.ts @@ -1,6 +1,8 @@ import { BadRequestException } from "@nestjs/common"; import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum"; +import { DaghiOption } from "src/Types&Enums/claim-request-management/daghi-option.enum"; import { RoleEnum } from "src/Types&Enums/role.enum"; +import { TypeOfDamage } from "src/Types&Enums/claim-request-management/type-of-damage.enum"; import { ExpertClaimService } from "./expert-claim.service"; const blankPricingReply = { @@ -8,10 +10,12 @@ const blankPricingReply = { parts: [ { partId: 201, + typeOfDamage: TypeOfDamage.Repair, price: "", salary: "", totalPayment: "", factorNeeded: false, + daghi: { option: DaghiOption.NO_VALUE }, }, ], }; diff --git a/src/helpers/expert-reply-pricing.spec.ts b/src/helpers/expert-reply-pricing.spec.ts index fb43724..d87bfef 100644 --- a/src/helpers/expert-reply-pricing.spec.ts +++ b/src/helpers/expert-reply-pricing.spec.ts @@ -1,19 +1,92 @@ import { getExpertReplyPricingValidationError } from "./expert-reply-pricing"; +import { TypeOfDamage } from "src/Types&Enums/claim-request-management/type-of-damage.enum"; +import { DaghiOption } from "src/Types&Enums/claim-request-management/daghi-option.enum"; describe("getExpertReplyPricingValidationError", () => { it("rejects a blank pricing line", () => { expect( getExpertReplyPricingValidationError([ - { partId: 201, price: "", salary: "", totalPayment: "" }, + { + partId: 201, + typeOfDamage: TypeOfDamage.Repair, + price: "", + salary: "", + totalPayment: "", + daghi: { option: DaghiOption.NO_VALUE }, + }, ]), - ).toMatch(/requires valid price, salary, and totalPayment/); + ).toMatch(/requires valid salary and totalPayment/); }); it("accepts a fully priced line with a zero-valued split", () => { expect( getExpertReplyPricingValidationError([ - { partId: 201, price: "0", salary: "10000", totalPayment: "10000" }, + { + partId: 201, + typeOfDamage: TypeOfDamage.Repair, + price: "0", + salary: "10000", + totalPayment: "10000", + daghi: { option: DaghiOption.NO_VALUE }, + }, ]), ).toBeNull(); }); + + it("rejects a repair line without a price", () => { + expect( + getExpertReplyPricingValidationError([ + { + partId: 201, + typeOfDamage: TypeOfDamage.Repair, + salary: "0", + totalPayment: "0", + daghi: { option: DaghiOption.NO_VALUE }, + }, + ]), + ).toMatch(/price is also required/); + }); + + it("rejects recycled-value daghi without its price", () => { + expect( + getExpertReplyPricingValidationError([ + { + partId: 201, + typeOfDamage: TypeOfDamage.Change, + salary: "0", + totalPayment: "0", + daghi: { option: DaghiOption.RECYCLED_PARTS_VALUE }, + }, + ]), + ).toMatch(/requires a valid daghi price/); + }); + + it("accepts a replacement line without a part price", () => { + expect( + getExpertReplyPricingValidationError([ + { + partId: 201, + typeOfDamage: TypeOfDamage.Change, + salary: "0", + totalPayment: "0", + daghi: { option: DaghiOption.NO_VALUE }, + }, + ]), + ).toBeNull(); + }); + + it("rejects an invalid price when a replacement line supplies one", () => { + expect( + getExpertReplyPricingValidationError([ + { + partId: 201, + typeOfDamage: TypeOfDamage.Change, + price: "not-a-number", + salary: "0", + totalPayment: "0", + daghi: { option: DaghiOption.NO_VALUE }, + }, + ]), + ).toMatch(/requires valid salary and totalPayment/); + }); }); diff --git a/src/helpers/expert-reply-pricing.ts b/src/helpers/expert-reply-pricing.ts index 6b21604..796171b 100644 --- a/src/helpers/expert-reply-pricing.ts +++ b/src/helpers/expert-reply-pricing.ts @@ -1,11 +1,15 @@ import { REPAIR_LINE_AMOUNT_TOMAN } from "src/constants/repair-amount-limits"; +import { DaghiOption } from "src/Types&Enums/claim-request-management/daghi-option.enum"; +import { TypeOfDamage } from "src/Types&Enums/claim-request-management/type-of-damage.enum"; import { parseMoneyAmountToman } from "src/utils/unicode-digits"; type ExpertReplyPricingPart = { partId?: unknown; + typeOfDamage?: unknown; price?: unknown; salary?: unknown; totalPayment?: unknown; + daghi?: { option?: unknown; price?: unknown }; }; /** @@ -28,13 +32,30 @@ export function getExpertReplyPricingValidationError( ? `Part ${String(part.partId)}` : `Damaged part ${index + 1}`; - if (!part || typeof part !== "object") { - return `${label} must include price, salary, and totalPayment.`; + if (!part || typeof part !== "object" || part.partId == null) { + return `${label} must include a damaged-part partId.`; + } + + if ( + !Object.values(TypeOfDamage).includes(part.typeOfDamage as TypeOfDamage) + ) { + return `${label} requires typeOfDamage to be either '${TypeOfDamage.Repair}' or '${TypeOfDamage.Change}'.`; + } + + if ( + !part.daghi || + !Object.values(DaghiOption).includes(part.daghi.option as DaghiOption) + ) { + return `${label} requires a valid daghi option.`; } const price = parseMoneyAmountToman(part.price); const salary = parseMoneyAmountToman(part.salary); const totalPayment = parseMoneyAmountToman(part.totalPayment); + const daghiPrice = parseMoneyAmountToman(part.daghi.price); + const hasPrice = + part.price != null && + (typeof part.price !== "string" || part.price.trim() !== ""); const splitAmountIsValid = (amount: number | null) => amount !== null && @@ -43,15 +64,28 @@ export function getExpertReplyPricingValidationError( amount <= REPAIR_LINE_AMOUNT_TOMAN.MAX)); const totalIsValid = totalPayment !== null && - totalPayment >= REPAIR_LINE_AMOUNT_TOMAN.MIN && + totalPayment >= 0 && totalPayment <= REPAIR_LINE_AMOUNT_TOMAN.MAX; + const priceIsRequired = part.typeOfDamage === TypeOfDamage.Repair; + const priceIsValid = + !hasPrice || (price !== null && splitAmountIsValid(price)); if ( - !splitAmountIsValid(price) || !splitAmountIsValid(salary) || - !totalIsValid + !totalIsValid || + (priceIsRequired && !splitAmountIsValid(price)) || + !priceIsValid ) { - return `${label} requires valid price, salary, and totalPayment. Price and salary may be 0; totalPayment must be between ${REPAIR_LINE_AMOUNT_TOMAN.MIN.toLocaleString("en-US")} and ${REPAIR_LINE_AMOUNT_TOMAN.MAX.toLocaleString("en-US")} Toman.`; + return `${label} requires valid salary and totalPayment; price is also required for '${TypeOfDamage.Repair}' damage. Price and salary may be 0; totalPayment may be 0.`; + } + + if ( + part.daghi.option === DaghiOption.RECYCLED_PARTS_VALUE && + (daghiPrice === null || + daghiPrice < REPAIR_LINE_AMOUNT_TOMAN.MIN || + daghiPrice > REPAIR_LINE_AMOUNT_TOMAN.MAX) + ) { + return `${label} requires a valid daghi price when option is '${DaghiOption.RECYCLED_PARTS_VALUE}'.`; } }