diff --git a/src/common/validators/money-amount-string.validator.ts b/src/common/validators/money-amount-string.validator.ts index 2f5f985..e724a12 100644 --- a/src/common/validators/money-amount-string.validator.ts +++ b/src/common/validators/money-amount-string.validator.ts @@ -5,22 +5,18 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from "class-validator"; -import { normalizeMoneyAmountString } from "src/utils/unicode-digits"; +import { parseMoneyAmountToman } from "src/utils/unicode-digits"; @ValidatorConstraint({ name: "isMoneyAmountString", async: false }) -export class IsMoneyAmountStringConstraint - implements ValidatorConstraintInterface -{ +export class IsMoneyAmountStringConstraint implements ValidatorConstraintInterface { validate(value: unknown): boolean { if (value == null || value === "") return true; if (typeof value !== "string") return false; - const n = normalizeMoneyAmountString(value); - if (!n) return false; - return /^\d+(\.\d+)?$/.test(n); + return parseMoneyAmountToman(value) !== null; } defaultMessage(): string { - return "Must be a non-negative amount (digits only, optional decimal)."; + return "Must be a non-negative whole-Toman amount."; } } 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 ecf6bb3..7f45966 100644 --- a/src/expert-claim/dto/expert-claim-v2.dto.spec.ts +++ b/src/expert-claim/dto/expert-claim-v2.dto.spec.ts @@ -26,7 +26,7 @@ describe("SubmitExpertReplyV2Dto", () => { expect(errors).not.toHaveLength(0); }); - it("accepts a replacement part without a price", async () => { + it("rejects a replacement part without a price", async () => { const dto = plainToInstance(SubmitExpertReplyV2Dto, { description: "Damage assessment", parts: [ @@ -41,7 +41,7 @@ describe("SubmitExpertReplyV2Dto", () => { ], }); - expect(await validate(dto)).toHaveLength(0); + expect(await validate(dto)).not.toHaveLength(0); }); it("accepts a repair part without daghi", async () => { @@ -61,4 +61,33 @@ describe("SubmitExpertReplyV2Dto", () => { expect(await validate(dto)).toHaveLength(0); }); + + it("accepts the expert's repair and replacement pricing rules", async () => { + const dto = plainToInstance(SubmitExpertReplyV2Dto, { + description: "Damage assessment", + parts: [ + { + partId: 11, + typeOfDamage: TypeOfDamage.Repair, + salary: "۵,۰۰۰", + totalPayment: "5000", + factorNeeded: false, + }, + { + partId: 23, + typeOfDamage: TypeOfDamage.Change, + price: "۵۰,۰۰۰", + salary: "۱۰۰,۰۰۰", + totalPayment: "150000", + factorNeeded: false, + daghi: { + option: DaghiOption.RECYCLED_PARTS_VALUE, + price: "۱۰۰,۵۰۰,۰۰۰", + }, + }, + ], + }); + + 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 3a8418b..9e49507 100644 --- a/src/expert-claim/dto/expert-claim-v2.dto.ts +++ b/src/expert-claim/dto/expert-claim-v2.dto.ts @@ -12,7 +12,7 @@ import { IsInt, } from 'class-validator'; import { Type } from 'class-transformer'; -import { IsRepairLineAmountToman } from 'src/common/validators/repair-line-amount-toman.validator'; +import { IsMoneyAmountString } from 'src/common/validators/money-amount-string.validator'; 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'; @@ -35,7 +35,7 @@ export class DaghiDetailsV2Dto { ) @IsString() @IsNotEmpty() - @IsRepairLineAmountToman() + @IsMoneyAmountString() price?: string; @ApiPropertyOptional({ @@ -56,24 +56,24 @@ export class PartPricingV2Dto { @ApiProperty({ enum: TypeOfDamage, - description: "'repair' requires price; 'change' may omit it.", + description: "'change' requires price; 'repair' 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.", + description: "Required for change lines; omitted for repair lines. Use 0 if unused.", }) @ValidateIf( (part: PartPricingV2Dto) => - part.typeOfDamage === TypeOfDamage.Repair || + part.typeOfDamage === TypeOfDamage.Change || (part.price != null && (typeof part.price !== 'string' || part.price.trim() !== '')), ) @IsString() @IsNotEmpty() - @IsRepairLineAmountToman({ allowZero: true }) + @IsMoneyAmountString() price: string; @ApiProperty({ @@ -82,7 +82,7 @@ export class PartPricingV2Dto { }) @IsString() @IsNotEmpty() - @IsRepairLineAmountToman({ allowZero: true }) + @IsMoneyAmountString() salary: string; @ApiProperty({ @@ -91,7 +91,7 @@ export class PartPricingV2Dto { }) @IsString() @IsNotEmpty() - @IsRepairLineAmountToman({ allowZero: true }) + @IsMoneyAmountString() totalPayment: string; @ApiPropertyOptional({ diff --git a/src/helpers/expert-reply-pricing.spec.ts b/src/helpers/expert-reply-pricing.spec.ts index efd425e..8d2ad04 100644 --- a/src/helpers/expert-reply-pricing.spec.ts +++ b/src/helpers/expert-reply-pricing.spec.ts @@ -47,7 +47,7 @@ describe("getExpertReplyPricingValidationError", () => { ).toBeNull(); }); - it("rejects a repair line without a price", () => { + it("accepts a repair line without a price", () => { expect( getExpertReplyPricingValidationError([ { @@ -55,10 +55,9 @@ describe("getExpertReplyPricingValidationError", () => { typeOfDamage: TypeOfDamage.Repair, salary: "0", totalPayment: "0", - daghi: { option: DaghiOption.NO_VALUE }, }, ]), - ).toMatch(/price is also required/); + ).toBeNull(); }); it("rejects recycled-value daghi without its price", () => { @@ -67,6 +66,7 @@ describe("getExpertReplyPricingValidationError", () => { { partId: 201, typeOfDamage: TypeOfDamage.Change, + price: "0", salary: "0", totalPayment: "0", daghi: { option: DaghiOption.RECYCLED_PARTS_VALUE }, @@ -75,7 +75,7 @@ describe("getExpertReplyPricingValidationError", () => { ).toMatch(/requires a valid daghi price/); }); - it("accepts a replacement line without a part price", () => { + it("rejects a replacement line without a part price", () => { expect( getExpertReplyPricingValidationError([ { @@ -86,7 +86,7 @@ describe("getExpertReplyPricingValidationError", () => { daghi: { option: DaghiOption.NO_VALUE }, }, ]), - ).toBeNull(); + ).toMatch(/price is also required/); }); it("rejects an invalid price when a replacement line supplies one", () => { @@ -103,4 +103,22 @@ describe("getExpertReplyPricingValidationError", () => { ]), ).toMatch(/requires valid salary and totalPayment/); }); + + it("accepts a recycled-parts daghi price above the repair-line cap", () => { + expect( + getExpertReplyPricingValidationError([ + { + partId: 23, + typeOfDamage: TypeOfDamage.Change, + price: "۵۰,۰۰۰", + salary: "۱۰۰,۰۰۰", + totalPayment: "150000", + daghi: { + option: DaghiOption.RECYCLED_PARTS_VALUE, + price: "۱۰۰,۵۰۰,۰۰۰", + }, + }, + ]), + ).toBeNull(); + }); }); diff --git a/src/helpers/expert-reply-pricing.ts b/src/helpers/expert-reply-pricing.ts index c32d318..c60d1a0 100644 --- a/src/helpers/expert-reply-pricing.ts +++ b/src/helpers/expert-reply-pricing.ts @@ -1,4 +1,3 @@ -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"; @@ -59,33 +58,22 @@ export function getExpertReplyPricingValidationError( part.price != null && (typeof part.price !== "string" || part.price.trim() !== ""); - const splitAmountIsValid = (amount: number | null) => - amount !== null && - (amount === 0 || - (amount >= REPAIR_LINE_AMOUNT_TOMAN.MIN && - amount <= REPAIR_LINE_AMOUNT_TOMAN.MAX)); - const totalIsValid = - totalPayment !== null && - totalPayment >= 0 && - totalPayment <= REPAIR_LINE_AMOUNT_TOMAN.MAX; - const priceIsRequired = part.typeOfDamage === TypeOfDamage.Repair; - const priceIsValid = - !hasPrice || (price !== null && splitAmountIsValid(price)); + const amountIsValid = (amount: number | null) => amount !== null; + const priceIsRequired = part.typeOfDamage === TypeOfDamage.Change; + const priceIsValid = !hasPrice || amountIsValid(price); if ( - !splitAmountIsValid(salary) || - !totalIsValid || - (priceIsRequired && !splitAmountIsValid(price)) || + !amountIsValid(salary) || + !amountIsValid(totalPayment) || + (priceIsRequired && !amountIsValid(price)) || !priceIsValid ) { - 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.`; + return `${label} requires valid salary and totalPayment; price is also required for '${TypeOfDamage.Change}' damage. Price, salary, and totalPayment may be 0.`; } if ( daghi?.option === DaghiOption.RECYCLED_PARTS_VALUE && - (daghiPrice === null || - daghiPrice < REPAIR_LINE_AMOUNT_TOMAN.MIN || - daghiPrice > REPAIR_LINE_AMOUNT_TOMAN.MAX) + daghiPrice === null ) { return `${label} requires a valid daghi price when option is '${DaghiOption.RECYCLED_PARTS_VALUE}'.`; }