forked from Yara724/api
HOTFIX: not allowing empty price when expert tries to submit
This commit is contained in:
27
src/expert-claim/dto/expert-claim-v2.dto.spec.ts
Normal file
27
src/expert-claim/dto/expert-claim-v2.dto.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { validate } from "class-validator";
|
||||
import { DaghiOption } from "src/Types&Enums/claim-request-management/daghi-option.enum";
|
||||
import { SubmitExpertReplyV2Dto } from "./expert-claim-v2.dto";
|
||||
|
||||
describe("SubmitExpertReplyV2Dto", () => {
|
||||
it("rejects a selected damaged part with blank pricing", async () => {
|
||||
const dto = plainToInstance(SubmitExpertReplyV2Dto, {
|
||||
description: "Damage assessment",
|
||||
parts: [
|
||||
{
|
||||
partId: 201,
|
||||
typeOfDamage: "Minor",
|
||||
price: "",
|
||||
salary: "",
|
||||
totalPayment: "",
|
||||
factorNeeded: false,
|
||||
daghi: { option: DaghiOption.NO_VALUE },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const errors = await validate(dto);
|
||||
|
||||
expect(errors).not.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsArray,
|
||||
ArrayMinSize,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
ValidateNested,
|
||||
@@ -56,6 +57,7 @@ export class PartPricingV2Dto {
|
||||
description: "Part price in Toman (integer string). Use 0 if the full amount is in salary.",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsRepairLineAmountToman({ allowZero: true })
|
||||
price: string;
|
||||
|
||||
@@ -64,6 +66,7 @@ export class PartPricingV2Dto {
|
||||
description: "Labor in Toman (integer string). Use 0 if the full amount is in price.",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsRepairLineAmountToman({ allowZero: true })
|
||||
salary: string;
|
||||
|
||||
@@ -72,6 +75,7 @@ export class PartPricingV2Dto {
|
||||
description: "Line total in Toman (integer string).",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsRepairLineAmountToman()
|
||||
totalPayment: string;
|
||||
|
||||
@@ -101,6 +105,7 @@ export class SubmitExpertReplyV2Dto {
|
||||
"One line per damaged part (`partId` from claim detail `damagedParts[]`), plus pricing, `daghi`, and `factorNeeded`.",
|
||||
})
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PartPricingV2Dto)
|
||||
parts: PartPricingV2Dto[];
|
||||
|
||||
69
src/expert-claim/expert-claim.service.spec.ts
Normal file
69
src/expert-claim/expert-claim.service.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { ExpertClaimService } from "./expert-claim.service";
|
||||
|
||||
const blankPricingReply = {
|
||||
description: "Damage assessment",
|
||||
parts: [
|
||||
{
|
||||
partId: 201,
|
||||
price: "",
|
||||
salary: "",
|
||||
totalPayment: "",
|
||||
factorNeeded: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function createService() {
|
||||
return new (ExpertClaimService as any)(
|
||||
...new Array(21).fill(undefined),
|
||||
) as ExpertClaimService;
|
||||
}
|
||||
|
||||
describe("ExpertClaimService expert-reply pricing", () => {
|
||||
it("rejects a blank pricing line on the legacy submit endpoint before changing status", async () => {
|
||||
const service = createService() as any;
|
||||
const findAndUpdate = jest.fn();
|
||||
service.claimRequestManagementDbService = {
|
||||
findOne: jest.fn().mockResolvedValue({
|
||||
_id: "legacy-claim",
|
||||
actorLocked: { actorId: "expert-1" },
|
||||
unlockTime: new Date(Date.now() + 60_000),
|
||||
lockFile: true,
|
||||
}),
|
||||
findAndUpdate,
|
||||
};
|
||||
|
||||
await expect(
|
||||
service.submitReplyRequest("legacy-claim", blankPricingReply, {
|
||||
sub: "expert-1",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(findAndUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a blank pricing line on the V2 submit endpoint before completion", async () => {
|
||||
const service = createService() as any;
|
||||
const findByIdAndUpdate = jest.fn();
|
||||
service.claimCaseDbService = {
|
||||
findById: jest.fn().mockResolvedValue({
|
||||
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
||||
workflow: { locked: true, lockedBy: { actorId: "expert-1" } },
|
||||
}),
|
||||
findByIdAndUpdate,
|
||||
};
|
||||
service.assertExpertActorOnClaim = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
await expect(
|
||||
service.submitExpertReplyV2("v2-claim", blankPricingReply, {
|
||||
sub: "expert-1",
|
||||
role: RoleEnum.FIELD_EXPERT,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(findByIdAndUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -168,6 +168,7 @@ import {
|
||||
} from "src/helpers/unified-file-status";
|
||||
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
|
||||
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
|
||||
import { getExpertReplyPricingValidationError } from "src/helpers/expert-reply-pricing";
|
||||
|
||||
@Injectable()
|
||||
export class ExpertClaimService {
|
||||
@@ -1658,6 +1659,13 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
const pricingValidationError = getExpertReplyPricingValidationError(
|
||||
reply.parts,
|
||||
);
|
||||
if (pricingValidationError) {
|
||||
throw new BadRequestException(pricingValidationError);
|
||||
}
|
||||
|
||||
// Validate total price cap (priced lines sum), when enabled.
|
||||
const priceCap = getClaimV2TotalPaymentCapToman();
|
||||
if (priceCap !== null && reply.parts && reply.parts.length > 0) {
|
||||
@@ -3261,6 +3269,13 @@ export class ExpertClaimService {
|
||||
throw new ForbiddenException("This claim is locked by another expert");
|
||||
}
|
||||
|
||||
const pricingValidationError = getExpertReplyPricingValidationError(
|
||||
reply.parts,
|
||||
);
|
||||
if (pricingValidationError) {
|
||||
throw new BadRequestException(pricingValidationError);
|
||||
}
|
||||
|
||||
// Price cap validation, when enabled.
|
||||
const priceCap = getClaimV2TotalPaymentCapToman();
|
||||
if (priceCap !== null) {
|
||||
|
||||
Reference in New Issue
Block a user