Files
yara724api/src/helpers/expert-reply-pricing.ts
2026-09-14 16:48:34 +03:30

225 lines
7.5 KiB
TypeScript

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 };
};
export const EXPERT_REPLY_MIN_AMOUNT_TOMAN = 1_000_000; // It is Rial from now on but we managed to keep the variables named as *Toman so that we won't have conflicts elsewhere.
export const EXPERT_REPLY_MAX_AMOUNT_TOMAN = 100_000_000_000; // It it Rial from now on
export type ExpertReplyPricingValidationError = {
message: string;
error: "EXPERT_REPLY_VALIDATION_ERROR";
code: "EXPERT_REPLY_VALIDATION_ERROR";
field: string;
partId?: string | number;
rule:
| "required"
| "invalid_value"
| "invalid_amount"
| "amount_out_of_range";
minAmount?: number;
maxAmount?: number;
};
type ValidationOptions = {
/** Preserve legacy English strings unless the V2 endpoint explicitly asks for a localized response. */
locale?: "fa";
enforceAmountBounds?: boolean;
};
function toPersianAmount(amount: number): string {
return amount.toLocaleString("fa-IR");
}
/**
* Validates the pricing that an expert must provide for every submitted
* damaged-part line. This is deliberately independent of DTO validation:
* legacy endpoints and deployments without a global ValidationPipe call the
* service directly.
*/
export function getExpertReplyPricingValidationError(
parts: unknown,
options: ValidationOptions = {},
): string | ExpertReplyPricingValidationError | null {
const localizedError = (
message: string,
field: string,
rule: ExpertReplyPricingValidationError["rule"],
partId?: string | number,
range?: { minAmount: number; maxAmount: number },
): string | ExpertReplyPricingValidationError => {
if (options.locale !== "fa") return message;
return {
message,
error: "EXPERT_REPLY_VALIDATION_ERROR",
code: "EXPERT_REPLY_VALIDATION_ERROR",
field,
...(partId != null && { partId }),
rule,
...(range ?? {}),
};
};
if (!Array.isArray(parts) || parts.length === 0) {
return localizedError(
"برای ثبت پاسخ کارشناسی، حداقل یک قطعه آسیب‌دیده همراه با مبلغ لازم است.",
"parts",
"required",
);
}
for (const [index, rawPart] of parts.entries()) {
const part = rawPart as ExpertReplyPricingPart | null;
const label =
part && part.partId != null && String(part.partId).trim() !== ""
? `Part ${String(part.partId)}`
: `Damaged part ${index + 1}`;
if (!part || typeof part !== "object" || part.partId == null) {
return localizedError(
`شناسه قطعه در ردیف ${index + 1} الزامی است.`,
`parts[${index}].partId`,
"required",
);
}
if (
!Object.values(TypeOfDamage).includes(part.typeOfDamage as TypeOfDamage)
) {
return localizedError(
`نوع خسارت قطعه ${String(part.partId)} معتبر نیست.`,
`parts[${index}].typeOfDamage`,
"invalid_value",
String(part.partId),
);
}
const daghi = part.daghi;
if (
part.typeOfDamage === TypeOfDamage.Change &&
(!daghi ||
!Object.values(DaghiOption).includes(daghi.option as DaghiOption))
) {
return localizedError(
`گزینه داغی برای قطعه ${String(part.partId)} الزامی و باید معتبر باشد.`,
`parts[${index}].daghi.option`,
"required",
String(part.partId),
);
}
const price = parseMoneyAmountToman(part.price);
const salary = parseMoneyAmountToman(part.salary);
const totalPayment = parseMoneyAmountToman(part.totalPayment);
const daghiPrice = parseMoneyAmountToman(daghi?.price);
const hasPrice =
part.price != null &&
(typeof part.price !== "string" || part.price.trim() !== "");
const amountIsValid = (amount: number | null) => amount !== null;
const priceIsRequired = part.typeOfDamage === TypeOfDamage.Change;
const requiredAmountFields: Array<[
"price" | "salary" | "totalPayment",
number | null,
boolean,
]> = [
["price", price, priceIsRequired || hasPrice],
["salary", salary, true],
["totalPayment", totalPayment, true],
];
const invalidAmount = requiredAmountFields.find(
([, amount, required]) => required && !amountIsValid(amount),
);
if (invalidAmount) {
const field = invalidAmount[0] as "price" | "salary" | "totalPayment";
if (options.locale === "fa") {
const fieldName =
field === "price"
? "قیمت قطعه"
: field === "salary"
? "دستمزد"
: "مبلغ کل";
return localizedError(
`${fieldName} قطعه ${String(part.partId)} باید به‌صورت مبلغ صحیح و غیرمنفی (ریال) وارد شود.`,
`parts[${index}].${field}`,
"invalid_amount",
String(part.partId),
);
}
return `${label} requires valid salary and totalPayment; price is also required for '${TypeOfDamage.Change}' damage. Price, salary, and totalPayment may be 0.`;
}
if (options.enforceAmountBounds) {
const monetaryFields: Array<[
"price" | "salary" | "totalPayment" | "daghi.price",
number | null,
boolean,
]> = [
["price", price, priceIsRequired || hasPrice],
["salary", salary, true],
["totalPayment", totalPayment, true],
[
"daghi.price",
daghiPrice,
daghi?.option === DaghiOption.RECYCLED_PARTS_VALUE,
],
];
const outOfRange = monetaryFields.find(
([, amount, required]) =>
required &&
amount !== null &&
(amount < EXPERT_REPLY_MIN_AMOUNT_TOMAN ||
amount > EXPERT_REPLY_MAX_AMOUNT_TOMAN),
);
if (outOfRange) {
const [field] = outOfRange;
const fieldName =
field === "price"
? "قیمت قطعه"
: field === "salary"
? "دستمزد"
: field === "totalPayment"
? "مبلغ کل"
: "قیمت داغی";
return localizedError(
`${fieldName} قطعه ${String(part.partId)} باید حداقل ${toPersianAmount(EXPERT_REPLY_MIN_AMOUNT_TOMAN)} و حداکثر ${toPersianAmount(EXPERT_REPLY_MAX_AMOUNT_TOMAN)} تومان باشد.`,
`parts[${index}].${field}`,
"amount_out_of_range",
String(part.partId),
{
minAmount: EXPERT_REPLY_MIN_AMOUNT_TOMAN,
maxAmount: EXPERT_REPLY_MAX_AMOUNT_TOMAN,
},
);
}
}
if (
daghi?.option === DaghiOption.RECYCLED_PARTS_VALUE &&
daghiPrice === null
) {
if (options.locale === "fa") {
return localizedError(
`قیمت داغی قطعه ${String(part.partId)} باید به‌صورت مبلغ صحیح و غیرمنفی (تومان) وارد شود.`,
`parts[${index}].daghi.price`,
"invalid_amount",
String(part.partId),
);
}
return `${label} requires a valid daghi price when option is '${DaghiOption.RECYCLED_PARTS_VALUE}'.`;
}
}
return null;
}