forked from Yara724/api
225 lines
7.5 KiB
TypeScript
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;
|
|
}
|