forked from Yara724/api
fix: align inquiry errors and expert review rules
This commit is contained in:
@@ -53,11 +53,19 @@ export class ClaimDetailV2ResponseDto {
|
||||
blameRequestType?: BlameRequestType;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "How the blame file was initiated: IN_PERSON or LINK",
|
||||
description: "How the blame file was initiated: NORMAL, IN_PERSON, or LINK",
|
||||
example: "IN_PERSON",
|
||||
})
|
||||
creationMethod?: string;
|
||||
|
||||
@ApiProperty({
|
||||
nullable: true,
|
||||
description:
|
||||
"Maximum expert-reply total for V1 user-created files. Null for V2-V6 flows or when the cap is disabled.",
|
||||
example: 530000000,
|
||||
})
|
||||
priceCap: number | null;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"CAR_BODY only: first-step flags — another car (`car`) and/or object (`object`)",
|
||||
@@ -278,7 +286,7 @@ export class ClaimDetailV2ResponseDto {
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Linked blame case (`blameCases`), same shape as expert-blame detail: parties with video/voice URLs, workflow, expert, formatted dates.",
|
||||
"Linked blame case (`blameCases`), same shape as expert-blame detail. Each party exposes its authoritative normalized `participants` and `participantRoles`, plus video/voice URLs, workflow, expert, and formatted dates.",
|
||||
})
|
||||
blameCase?: Record<string, unknown>;
|
||||
|
||||
|
||||
@@ -168,6 +168,88 @@ describe("ExpertClaimService expert-reply pricing", () => {
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ import {
|
||||
ExpertFileKind,
|
||||
} from "src/users/entities/schema/expert-file-activity.schema";
|
||||
|
||||
/** Maximum sum of line `totalPayment` across the claim (Toman; priced parts + factor lines after validation). */
|
||||
/** Configured V1 maximum sum of line `totalPayment` across the claim (Rial). */
|
||||
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
|
||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
import {
|
||||
@@ -180,6 +180,7 @@ import {
|
||||
normalizeMoneyAmountString,
|
||||
parseMoneyAmountToman,
|
||||
} from "src/utils/unicode-digits";
|
||||
import { claimPriceCapAppliesToBlame } from "src/helpers/claim-price-cap";
|
||||
|
||||
@Injectable()
|
||||
export class ExpertClaimService {
|
||||
@@ -1694,8 +1695,13 @@ export class ExpertClaimService {
|
||||
throw new BadRequestException(pricingValidationError);
|
||||
}
|
||||
|
||||
// Validate total price cap (priced lines sum), when enabled.
|
||||
const priceCap = getClaimV2TotalPaymentCapToman();
|
||||
// The total cap is a V1-only rule. Legacy claims embed their blame file.
|
||||
const configuredPriceCap = getClaimV2TotalPaymentCapToman();
|
||||
const priceCap =
|
||||
configuredPriceCap !== null &&
|
||||
claimPriceCapAppliesToBlame(request.blameFile)
|
||||
? configuredPriceCap
|
||||
: null;
|
||||
if (priceCap !== null && reply.parts && reply.parts.length > 0) {
|
||||
let totalPrice = 0;
|
||||
|
||||
@@ -1727,7 +1733,7 @@ export class ExpertClaimService {
|
||||
|
||||
if (totalPrice > priceCap) {
|
||||
throw new BadRequestException({
|
||||
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
|
||||
message: `You have reached the maximum acceptable total price (Rial). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
|
||||
error: "PRICE_CAP_ERROR",
|
||||
code: "PRICE_CAP_ERROR",
|
||||
totalPrice: totalPrice,
|
||||
@@ -2293,7 +2299,8 @@ export class ExpertClaimService {
|
||||
* Preconditions: all `factorNeeded` parts have `factorLink`; case is UNDER_REVIEW at EXPERT_COST_EVALUATION.
|
||||
* — All approved → COMPLETED + APPROVED (expert-entered line totals; no extra owner signature).
|
||||
* — Any rejected (repriced) → COMPLETED + APPROVED (auto-close for now; owner sign may be added later).
|
||||
* When enabled by env, total of all repair lines must be ≤ the claim v2 total payment cap.
|
||||
* For V1 user-created files, when enabled by env, the total of all repair
|
||||
* lines must be no greater than the configured total-payment cap.
|
||||
* Response: `claimStatus` = `ClaimStatus`; `caseStatus` = `ClaimCaseStatus`.
|
||||
*/
|
||||
async validateClaimFactorsV2(
|
||||
@@ -2433,7 +2440,12 @@ export class ExpertClaimService {
|
||||
};
|
||||
}
|
||||
|
||||
const priceCap = getClaimV2TotalPaymentCapToman();
|
||||
const configuredPriceCap = getClaimV2TotalPaymentCapToman();
|
||||
const priceCap =
|
||||
configuredPriceCap !== null &&
|
||||
claimPriceCapAppliesToBlame(await this.loadBlameForClaim(claim))
|
||||
? configuredPriceCap
|
||||
: null;
|
||||
if (priceCap !== null) {
|
||||
let totalPrice = 0;
|
||||
for (const part of updatedReply.parts || []) {
|
||||
@@ -2448,7 +2460,7 @@ export class ExpertClaimService {
|
||||
}
|
||||
if (totalPrice > priceCap) {
|
||||
throw new BadRequestException({
|
||||
message: `You have reached the maximum acceptable total price (Toman). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
|
||||
message: `You have reached the maximum acceptable total price (Rial). The sum of priced parts and factor lines (${totalPrice.toLocaleString()}) exceeds the limit (${priceCap.toLocaleString()}).`,
|
||||
error: "PRICE_CAP_ERROR",
|
||||
totalPrice,
|
||||
priceCap,
|
||||
@@ -3259,7 +3271,7 @@ export class ExpertClaimService {
|
||||
* - Claim must exist
|
||||
* - Must be locked by this expert (workflow.lockedBy.actorId === actor.sub)
|
||||
* - Must be in EXPERT_REVIEWING status
|
||||
* - Total payment across all parts must not exceed 53,000,000 (same cap as factor validation totals)
|
||||
* - V1 only: total payment across all parts must not exceed the configured cap
|
||||
* - Each part must include `daghi` (option + conditional price) like V1
|
||||
*
|
||||
* On success:
|
||||
@@ -3364,8 +3376,12 @@ export class ExpertClaimService {
|
||||
throw new BadRequestException(pricingValidationError);
|
||||
}
|
||||
|
||||
// Price cap validation, when enabled.
|
||||
const priceCap = getClaimV2TotalPaymentCapToman();
|
||||
// The configured total cap is enforced only for V1 user-created files.
|
||||
const configuredPriceCap = getClaimV2TotalPaymentCapToman();
|
||||
const priceCap =
|
||||
configuredPriceCap !== null && claimPriceCapAppliesToBlame(blame)
|
||||
? configuredPriceCap
|
||||
: null;
|
||||
if (priceCap !== null) {
|
||||
let totalPrice = 0;
|
||||
for (const part of reply.parts || []) {
|
||||
@@ -3376,7 +3392,7 @@ export class ExpertClaimService {
|
||||
}
|
||||
if (totalPrice > priceCap) {
|
||||
throw new BadRequestException({
|
||||
message: `مجموع مبلغ قطعات (${totalPrice.toLocaleString("fa-IR")}) از سقف مجاز (${priceCap.toLocaleString("fa-IR")}) تومان بیشتر است.`,
|
||||
message: `مجموع مبلغ قطعات (${totalPrice.toLocaleString("fa-IR")}) از سقف مجاز (${priceCap.toLocaleString("fa-IR")}) ریال بیشتر است.`,
|
||||
error: "PRICE_CAP_ERROR",
|
||||
code: "PRICE_CAP_ERROR",
|
||||
totalPrice,
|
||||
@@ -5125,7 +5141,11 @@ export class ExpertClaimService {
|
||||
claim.blameRequestId
|
||||
? this.blameRequestDbService.find(
|
||||
{ _id: new Types.ObjectId(claim.blameRequestId.toString()) },
|
||||
{ lean: true, select: "type parties expert.decision" },
|
||||
{
|
||||
lean: true,
|
||||
select:
|
||||
"type parties expert.decision creationMethod expertInitiated registrarInitiated callCenterInitiated initiatedByFieldExpertId initiatedByRegistrarId initiatedByCallCenterId",
|
||||
},
|
||||
)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
@@ -5160,6 +5180,11 @@ export class ExpertClaimService {
|
||||
const blameFileContext = blameLean
|
||||
? this.blameFileContextForExpert(blameLean)
|
||||
: {};
|
||||
const configuredPriceCap = getClaimV2TotalPaymentCapToman();
|
||||
const priceCap =
|
||||
configuredPriceCap !== null && claimPriceCapAppliesToBlame(linkedBlame)
|
||||
? configuredPriceCap
|
||||
: null;
|
||||
|
||||
let videoCapture: ClaimDetailV2ResponseDto["videoCapture"] = undefined;
|
||||
if (videoCaptureRow) {
|
||||
@@ -5269,6 +5294,7 @@ export class ExpertClaimService {
|
||||
? this.sanitizeVehicleInquiryForApi(vehiclePayload)
|
||||
: undefined,
|
||||
...blameFileContext,
|
||||
priceCap,
|
||||
blameRequestId: claim.blameRequestId?.toString(),
|
||||
blameRequestNo: claim.blameRequestNo,
|
||||
money: moneyPayload,
|
||||
|
||||
@@ -268,7 +268,7 @@ export class ExpertClaimV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Submit expert damage assessment reply",
|
||||
description:
|
||||
"**Preconditions:** claim locked by this expert (`EXPERT_REVIEWING`). **Unlocks** the claim. Each `parts[]` line needs `partId` (from GET claim detail `damagedParts[].partId`), plus pricing, `daghi`, and optional `factorNeeded`. Every supplied monetary field (`price`, `salary`, `totalPayment`, and `daghi.price`) must be 100,000–10,000,000,000 **Toman**. **Cap:** sum of line `totalPayment` values ≤ 53,000,000 **Toman** (same limit as factor-validation totals across priced + factor lines). Clears any prior `evaluation.ownerInsurerApproval` / `ownerPricedPartsApproval`.\n\n" +
|
||||
"**Preconditions:** claim locked by this expert (`EXPERT_REVIEWING`). **Unlocks** the claim. Each `parts[]` line needs `partId` (from GET claim detail `damagedParts[].partId`), plus pricing, `daghi`, and optional `factorNeeded`. Every supplied monetary field (`price`, `salary`, `totalPayment`, and `daghi.price`) must be 1,000,000–100,000,000,000 **Rial**. **V1-only cap:** for user-created V1 files, sum of line `totalPayment` values ≤ 530,000,000 **Rial** (same limit as factor-validation totals across priced + factor lines). V2–V6 files are uncapped. Claim detail exposes the effective `priceCap` (`null` when uncapped). Clears any prior `evaluation.ownerInsurerApproval` / `ownerPricedPartsApproval`.\n\n" +
|
||||
"**Frontend routing by `ClaimCaseStatus` (`status`):**\n" +
|
||||
"- **All parts `factorNeeded`:** `OWNER_REPAIR_FACTOR_UPLOAD_PENDING`, `claimStatus=NEEDS_REVISION`, `workflow.currentStep=OWNER_UPLOAD_FACTOR_DOCUMENTS`, `workflow.nextStep=EXPERT_COST_EVALUATION` → owner uploads all factors; then `status` becomes **`EXPERT_VALIDATING_REPAIR_FACTORS`**, `claimStatus=UNDER_REVIEW`, `currentStep=EXPERT_COST_EVALUATION` for expert **validate-factors**.\n" +
|
||||
"- **Mixed (some priced, some factorNeeded):** `INSURER_REVIEW_MIXED_FACTORS_PENDING`, `claimStatus=NEEDS_REVISION`, `currentStep=INSURER_REVIEW`, `nextStep=OWNER_UPLOAD_FACTOR_DOCUMENTS` → owner must call **owner-insurer-approval/sign** first (priced-line acceptance); `currentStep` then moves to `OWNER_UPLOAD_FACTOR_DOCUMENTS` (same case `status` until factors are done).\n" +
|
||||
@@ -353,7 +353,7 @@ export class ExpertClaimV2Controller {
|
||||
"**Response:** `claimStatus` = `ClaimStatus` (e.g. APPROVED). `caseStatus` = `ClaimCaseStatus` (e.g. COMPLETED vs insurer-review) — they are not interchangeable.\n\n" +
|
||||
"**Preconditions:** `status=EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`), `claimStatus=UNDER_REVIEW`, `workflow.currentStep=EXPERT_COST_EVALUATION`, every `factorNeeded` line has `factorLink`.\n\n" +
|
||||
"**Decisions:** each factor line gets `APPROVED` or `REJECTED`. **Every** decided line must include expert-entered `totalPayment` **or** both `price` and `salary` (factor photos are not read for amounts).\n\n" +
|
||||
"**Cap (when every factor line is decided):** sum of **all** reply lines (priced parts + factor lines) must be ≤ **53,000,000 Toman**; otherwise `PRICE_CAP_ERROR` with message that the maximum acceptable total was exceeded.\n\n" +
|
||||
"**V1-only cap (when every factor line is decided):** for user-created V1 files, sum of **all** reply lines (priced parts + factor lines) must be ≤ **530,000,000 Rial**; otherwise `PRICE_CAP_ERROR` is returned. V2–V6 files are uncapped.\n\n" +
|
||||
"**Outcomes:**\n" +
|
||||
"- **All approved:** `caseStatus=COMPLETED`, `claimStatus=APPROVED`, workflow `CLAIM_COMPLETED` — no owner signature. V5 instead waits for FileMaker approval.\n" +
|
||||
"- **Any rejected (repriced):** same completion behavior for now (V5 waits for FileMaker approval).\n" +
|
||||
|
||||
Reference in New Issue
Block a user