forked from Yara724/api
533 lines
15 KiB
TypeScript
533 lines
15 KiB
TypeScript
import { BadRequestException } from "@nestjs/common";
|
|
import { Types } from "mongoose";
|
|
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 { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
|
import { ExpertClaimService } from "./expert-claim.service";
|
|
|
|
const V2_EXPERT_ID = "66ec0e480e321873c0900001";
|
|
|
|
const blankPricingReply = {
|
|
description: "Damage assessment",
|
|
parts: [
|
|
{
|
|
partId: 201,
|
|
typeOfDamage: TypeOfDamage.Repair,
|
|
price: "",
|
|
salary: "",
|
|
totalPayment: "",
|
|
factorNeeded: false,
|
|
daghi: { option: DaghiOption.NO_VALUE },
|
|
},
|
|
],
|
|
};
|
|
|
|
function createService() {
|
|
return new (ExpertClaimService as any)(
|
|
...new Array(21).fill(undefined),
|
|
) as ExpertClaimService;
|
|
}
|
|
|
|
function setupSuccessfulV2Submit(blameType: BlameRequestType) {
|
|
const service = createService() as any;
|
|
const findByIdAndUpdate = jest.fn().mockResolvedValue(undefined);
|
|
const claim = {
|
|
_id: "66ec0e480e321873c0900002",
|
|
publicId: "CLM-1",
|
|
blameRequestId: "66ec0e480e321873c0900003",
|
|
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
|
workflow: { locked: true, lockedBy: { actorId: V2_EXPERT_ID } },
|
|
vehicle: { carType: "sedan" },
|
|
damage: {
|
|
selectedParts: [
|
|
{
|
|
id: 1,
|
|
name: "front",
|
|
side: "",
|
|
label_fa: "جلو کامل",
|
|
catalogKey: "1",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
service.claimCaseDbService = {
|
|
findById: jest.fn().mockResolvedValue(claim),
|
|
findByIdAndUpdate,
|
|
};
|
|
service.blameRequestDbService = {
|
|
findById: jest.fn().mockResolvedValue({
|
|
type: blameType,
|
|
expertInitiated: true,
|
|
creationMethod: "IN_PERSON",
|
|
}),
|
|
};
|
|
service.assertExpertActorOnClaim = jest.fn().mockResolvedValue(undefined);
|
|
service.snapshotDamageExpert = jest.fn().mockResolvedValue(undefined);
|
|
service.recordClaimExpertActivity = jest.fn().mockResolvedValue(undefined);
|
|
|
|
return { service, findByIdAndUpdate };
|
|
}
|
|
|
|
const validV2Reply = {
|
|
parts: [
|
|
{
|
|
partId: 1,
|
|
typeOfDamage: TypeOfDamage.Repair,
|
|
salary: "1000000",
|
|
totalPayment: "1000000",
|
|
factorNeeded: false,
|
|
},
|
|
],
|
|
};
|
|
|
|
describe("ExpertClaimService expert-reply pricing", () => {
|
|
it("requires the current car price for a car-body expert reply", async () => {
|
|
const { service, findByIdAndUpdate } = setupSuccessfulV2Submit(
|
|
BlameRequestType.CAR_BODY,
|
|
);
|
|
|
|
await expect(
|
|
service.submitExpertReplyV2("v2-claim", validV2Reply, {
|
|
sub: V2_EXPERT_ID,
|
|
fullName: "Expert One",
|
|
role: RoleEnum.FIELD_EXPERT,
|
|
}),
|
|
).rejects.toMatchObject({
|
|
response: {
|
|
code: "CAR_PRICE_REQUIRED",
|
|
field: "carPrice",
|
|
},
|
|
});
|
|
|
|
expect(findByIdAndUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("normalizes and persists the current car price on car-body replies", async () => {
|
|
const { service, findByIdAndUpdate } = setupSuccessfulV2Submit(
|
|
BlameRequestType.CAR_BODY,
|
|
);
|
|
|
|
await service.submitExpertReplyV2(
|
|
"v2-claim",
|
|
{ ...validV2Reply, carPrice: "۱٬۲۵۰٬۰۰۰٬۰۰۰" },
|
|
{
|
|
sub: V2_EXPERT_ID,
|
|
fullName: "Expert One",
|
|
role: RoleEnum.FIELD_EXPERT,
|
|
},
|
|
);
|
|
|
|
expect(findByIdAndUpdate).toHaveBeenCalledTimes(1);
|
|
expect(findByIdAndUpdate.mock.calls[0][1]).toMatchObject({
|
|
"vehicle.price": "1250000000",
|
|
});
|
|
});
|
|
|
|
it("rejects the current car price on third-party replies", async () => {
|
|
const { service, findByIdAndUpdate } = setupSuccessfulV2Submit(
|
|
BlameRequestType.THIRD_PARTY,
|
|
);
|
|
|
|
await expect(
|
|
service.submitExpertReplyV2(
|
|
"v2-claim",
|
|
{ ...validV2Reply, carPrice: "1250000000" },
|
|
{
|
|
sub: V2_EXPERT_ID,
|
|
fullName: "Expert One",
|
|
role: RoleEnum.FIELD_EXPERT,
|
|
},
|
|
),
|
|
).rejects.toMatchObject({
|
|
response: {
|
|
code: "CAR_PRICE_NOT_ALLOWED",
|
|
field: "carPrice",
|
|
},
|
|
});
|
|
|
|
expect(findByIdAndUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not overwrite vehicle.price on third-party replies", async () => {
|
|
const { service, findByIdAndUpdate } = setupSuccessfulV2Submit(
|
|
BlameRequestType.THIRD_PARTY,
|
|
);
|
|
|
|
await service.submitExpertReplyV2("v2-claim", validV2Reply, {
|
|
sub: V2_EXPERT_ID,
|
|
fullName: "Expert One",
|
|
role: RoleEnum.FIELD_EXPERT,
|
|
});
|
|
|
|
expect(findByIdAndUpdate).toHaveBeenCalledTimes(1);
|
|
expect(findByIdAndUpdate.mock.calls[0][1]).not.toHaveProperty(
|
|
"vehicle.price",
|
|
);
|
|
});
|
|
|
|
it("enforces the configured total-payment cap for a V1 user-created file", async () => {
|
|
const previousEnabled = process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED;
|
|
process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED = "true";
|
|
|
|
try {
|
|
const { service, findByIdAndUpdate } = setupSuccessfulV2Submit(
|
|
BlameRequestType.THIRD_PARTY,
|
|
);
|
|
(service as any).blameRequestDbService.findById.mockResolvedValue({
|
|
type: BlameRequestType.THIRD_PARTY,
|
|
creationMethod: "NORMAL",
|
|
});
|
|
|
|
await expect(
|
|
service.submitExpertReplyV2(
|
|
"v1-claim",
|
|
{
|
|
...validV2Reply,
|
|
parts: [
|
|
{
|
|
...validV2Reply.parts[0],
|
|
salary: "1000000",
|
|
totalPayment: "600000000",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
sub: V2_EXPERT_ID,
|
|
fullName: "Expert One",
|
|
role: RoleEnum.FIELD_EXPERT,
|
|
},
|
|
),
|
|
).rejects.toMatchObject({ response: { code: "PRICE_CAP_ERROR" } });
|
|
|
|
expect(findByIdAndUpdate).not.toHaveBeenCalled();
|
|
} finally {
|
|
if (previousEnabled === undefined) {
|
|
delete process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED;
|
|
} else {
|
|
process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED = previousEnabled;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("does not enforce the V1 cap for an expert-initiated flow", async () => {
|
|
const previousEnabled = process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED;
|
|
process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED = "true";
|
|
|
|
try {
|
|
const { service, findByIdAndUpdate } = setupSuccessfulV2Submit(
|
|
BlameRequestType.THIRD_PARTY,
|
|
);
|
|
|
|
await service.submitExpertReplyV2(
|
|
"v3-claim",
|
|
{
|
|
...validV2Reply,
|
|
parts: [
|
|
{
|
|
...validV2Reply.parts[0],
|
|
salary: "1000000",
|
|
totalPayment: "600000000",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
sub: V2_EXPERT_ID,
|
|
fullName: "Expert One",
|
|
role: RoleEnum.FIELD_EXPERT,
|
|
},
|
|
);
|
|
|
|
expect(findByIdAndUpdate).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
if (previousEnabled === undefined) {
|
|
delete process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED;
|
|
} else {
|
|
process.env.CLAIM_V2_TOTAL_PAYMENT_CAP_ENABLED = previousEnabled;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("allows a repair line without daghi and removes a stray daghi payload", () => {
|
|
const service = createService() as any;
|
|
|
|
expect(
|
|
service.validateAndNormalizeDaghiForExpertReplyV2([
|
|
{
|
|
partId: 201,
|
|
typeOfDamage: TypeOfDamage.Repair,
|
|
price: "10000",
|
|
salary: "0",
|
|
totalPayment: "10000",
|
|
daghi: { option: DaghiOption.NO_VALUE },
|
|
},
|
|
]),
|
|
).toEqual([
|
|
{
|
|
partId: 201,
|
|
typeOfDamage: TypeOfDamage.Repair,
|
|
price: "10000",
|
|
salary: "0",
|
|
totalPayment: "10000",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("allows a deliver-damaged-part line without a branch", () => {
|
|
const service = createService() as any;
|
|
|
|
expect(
|
|
service.validateAndNormalizeDaghiForExpertReplyV2([
|
|
{
|
|
partId: 201,
|
|
typeOfDamage: TypeOfDamage.Change,
|
|
price: "100000",
|
|
salary: "100000",
|
|
totalPayment: "200000",
|
|
daghi: { option: DaghiOption.DELIVER_DAMAGED_PART },
|
|
},
|
|
]),
|
|
).toEqual([
|
|
{
|
|
partId: 201,
|
|
typeOfDamage: TypeOfDamage.Change,
|
|
price: "100000",
|
|
salary: "100000",
|
|
totalPayment: "200000",
|
|
daghi: { option: DaghiOption.DELIVER_DAMAGED_PART },
|
|
},
|
|
]);
|
|
});
|
|
|
|
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({
|
|
blameRequestId: "blame-1",
|
|
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
|
workflow: { locked: true, lockedBy: { actorId: "expert-1" } },
|
|
}),
|
|
findByIdAndUpdate,
|
|
};
|
|
service.blameRequestDbService = {
|
|
findById: jest.fn().mockResolvedValue({ type: "CAR_BODY" }),
|
|
};
|
|
service.assertExpertActorOnClaim = jest.fn().mockResolvedValue(undefined);
|
|
service.snapshotDamageExpert = jest.fn().mockResolvedValue(undefined);
|
|
|
|
await expect(
|
|
service.submitExpertReplyV2(
|
|
"v2-claim",
|
|
{ ...blankPricingReply, carPrice: "1000000" },
|
|
{
|
|
sub: "expert-1",
|
|
role: RoleEnum.FIELD_EXPERT,
|
|
},
|
|
),
|
|
).rejects.toBeInstanceOf(BadRequestException);
|
|
|
|
expect(findByIdAndUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns a Persian, field-specific error when a V2 price is below the minimum", async () => {
|
|
const service = createService() as any;
|
|
const findByIdAndUpdate = jest.fn();
|
|
service.claimCaseDbService = {
|
|
findById: jest.fn().mockResolvedValue({
|
|
blameRequestId: "blame-1",
|
|
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
|
workflow: { locked: true, lockedBy: { actorId: "expert-1" } },
|
|
}),
|
|
findByIdAndUpdate,
|
|
};
|
|
service.blameRequestDbService = {
|
|
findById: jest.fn().mockResolvedValue({ type: "CAR_BODY" }),
|
|
};
|
|
service.assertExpertActorOnClaim = jest.fn().mockResolvedValue(undefined);
|
|
service.snapshotDamageExpert = jest.fn().mockResolvedValue(undefined);
|
|
|
|
await expect(
|
|
service.submitExpertReplyV2(
|
|
"v2-claim",
|
|
{
|
|
description: "Damage assessment",
|
|
carPrice: "1000000",
|
|
parts: [
|
|
{
|
|
partId: 201,
|
|
typeOfDamage: TypeOfDamage.Repair,
|
|
price: "99,999",
|
|
salary: "100000",
|
|
totalPayment: "100000",
|
|
factorNeeded: false,
|
|
},
|
|
],
|
|
},
|
|
{ sub: "expert-1", role: RoleEnum.FIELD_EXPERT },
|
|
),
|
|
).rejects.toMatchObject({
|
|
response: {
|
|
code: "EXPERT_REPLY_VALIDATION_ERROR",
|
|
field: "parts[0].price",
|
|
message: expect.stringContaining("حداقل"),
|
|
},
|
|
});
|
|
|
|
expect(findByIdAndUpdate).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("ExpertClaimService damaged-part audit", () => {
|
|
it("archives removed capture evidence before replacing the live arrays", async () => {
|
|
const service = createService() as any;
|
|
const expertId = "66ec0e480e321873c0900001";
|
|
const findByIdAndUpdate = jest.fn().mockResolvedValue(undefined);
|
|
service.claimCaseDbService = {
|
|
findById: jest.fn().mockResolvedValue({
|
|
_id: "66ec0e480e321873c0900002",
|
|
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
|
workflow: { locked: true, lockedBy: { actorId: expertId } },
|
|
vehicle: { carType: "SEDAN" },
|
|
damage: {
|
|
selectedParts: [
|
|
{
|
|
id: 101,
|
|
name: "hood",
|
|
side: "front",
|
|
label_fa: "کاپوت",
|
|
catalogKey: "front_hood",
|
|
},
|
|
{
|
|
id: 202,
|
|
name: "door",
|
|
side: "left",
|
|
label_fa: "درب چپ",
|
|
catalogKey: "left_door",
|
|
},
|
|
],
|
|
},
|
|
media: {
|
|
damagedParts: [
|
|
{ path: "claims/hood.jpg", fileName: "hood.jpg" },
|
|
{ path: "claims/door.jpg", fileName: "door.jpg" },
|
|
],
|
|
},
|
|
}),
|
|
findByIdAndUpdate,
|
|
};
|
|
service.assertExpertActorOnClaim = jest.fn().mockResolvedValue(undefined);
|
|
service.snapshotDamageExpert = jest.fn().mockResolvedValue({
|
|
firstName: "Expert",
|
|
});
|
|
|
|
await service.updateClaimDamagedPartsV2(
|
|
"66ec0e480e321873c0900002",
|
|
{
|
|
selectedParts: [
|
|
{
|
|
id: 202,
|
|
name: "door",
|
|
side: "left",
|
|
label_fa: "درب چپ",
|
|
catalogKey: "left_door",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
sub: expertId,
|
|
fullName: "Expert One",
|
|
role: RoleEnum.FIELD_EXPERT,
|
|
},
|
|
);
|
|
|
|
expect(findByIdAndUpdate).toHaveBeenCalledTimes(1);
|
|
const update = findByIdAndUpdate.mock.calls[0][1];
|
|
expect(update.$set["media.damagedParts"]).toEqual([
|
|
expect.objectContaining({ path: "claims/door.jpg" }),
|
|
]);
|
|
expect(update.$push["damage.partSelectionHistory"]).toEqual(
|
|
expect.objectContaining({
|
|
changedBy: expect.objectContaining({ actorId: expertId }),
|
|
removedParts: [
|
|
expect.objectContaining({
|
|
id: 101,
|
|
capture: expect.objectContaining({ path: "claims/hood.jpg" }),
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("ExpertClaimService FileReviewer assignment", () => {
|
|
it("lets the assigned reviewer reopen a file while it is still awaiting FileReviewer work", async () => {
|
|
const service = createService() as any;
|
|
const reviewerId = new Types.ObjectId();
|
|
const clientId = new Types.ObjectId();
|
|
const blameId = new Types.ObjectId();
|
|
const claimId = new Types.ObjectId();
|
|
|
|
service.expireClaimWorkflowLockV2IfStale = jest.fn().mockResolvedValue(undefined);
|
|
service.claimCaseDbService = {
|
|
findById: jest.fn().mockResolvedValue({
|
|
_id: claimId,
|
|
blameRequestId: blameId,
|
|
status: ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
|
}),
|
|
};
|
|
service.blameRequestDbService = {
|
|
findById: jest.fn().mockResolvedValue({
|
|
_id: blameId,
|
|
type: BlameRequestType.CAR_BODY,
|
|
status: "WAITING_FOR_FILE_REVIEWER",
|
|
assignedFileReviewerId: reviewerId,
|
|
parties: [
|
|
{
|
|
role: "FIRST",
|
|
person: { clientId },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
service.assertExpertActorOnClaim = jest.fn();
|
|
|
|
await expect(
|
|
service.assignClaimForReviewV2(String(claimId), {
|
|
sub: String(reviewerId),
|
|
role: RoleEnum.FILE_REVIEWER,
|
|
clientKey: String(clientId),
|
|
}),
|
|
).resolves.toMatchObject({
|
|
success: true,
|
|
status: "already_assigned_to_you",
|
|
});
|
|
|
|
expect(service.assertExpertActorOnClaim).not.toHaveBeenCalled();
|
|
});
|
|
});
|