forked from Yara724/api
Better error handling and validation and min/max prices added for expert submit
This commit is contained in:
@@ -11,6 +11,34 @@ type ExpertReplyPricingPart = {
|
||||
daghi?: { option?: unknown; price?: unknown };
|
||||
};
|
||||
|
||||
export const EXPERT_REPLY_MIN_AMOUNT_TOMAN = 100_000;
|
||||
export const EXPERT_REPLY_MAX_AMOUNT_TOMAN = 10_000_000_000;
|
||||
|
||||
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:
|
||||
@@ -19,9 +47,33 @@ type ExpertReplyPricingPart = {
|
||||
*/
|
||||
export function getExpertReplyPricingValidationError(
|
||||
parts: unknown,
|
||||
): string | null {
|
||||
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 "At least one damaged part with pricing is required to submit an expert reply.";
|
||||
return localizedError(
|
||||
"برای ثبت پاسخ کارشناسی، حداقل یک قطعه آسیبدیده همراه با مبلغ لازم است.",
|
||||
"parts",
|
||||
"required",
|
||||
);
|
||||
}
|
||||
|
||||
for (const [index, rawPart] of parts.entries()) {
|
||||
@@ -32,13 +84,22 @@ export function getExpertReplyPricingValidationError(
|
||||
: `Damaged part ${index + 1}`;
|
||||
|
||||
if (!part || typeof part !== "object" || part.partId == null) {
|
||||
return `${label} must include a damaged-part partId.`;
|
||||
return localizedError(
|
||||
`شناسه قطعه در ردیف ${index + 1} الزامی است.`,
|
||||
`parts[${index}].partId`,
|
||||
"required",
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!Object.values(TypeOfDamage).includes(part.typeOfDamage as TypeOfDamage)
|
||||
) {
|
||||
return `${label} requires typeOfDamage to be either '${TypeOfDamage.Repair}' or '${TypeOfDamage.Change}'.`;
|
||||
return localizedError(
|
||||
`نوع خسارت قطعه ${String(part.partId)} معتبر نیست.`,
|
||||
`parts[${index}].typeOfDamage`,
|
||||
"invalid_value",
|
||||
String(part.partId),
|
||||
);
|
||||
}
|
||||
|
||||
const daghi = part.daghi;
|
||||
@@ -47,7 +108,12 @@ export function getExpertReplyPricingValidationError(
|
||||
(!daghi ||
|
||||
!Object.values(DaghiOption).includes(daghi.option as DaghiOption))
|
||||
) {
|
||||
return `${label} requires a valid daghi option.`;
|
||||
return localizedError(
|
||||
`گزینه داغی برای قطعه ${String(part.partId)} الزامی و باید معتبر باشد.`,
|
||||
`parts[${index}].daghi.option`,
|
||||
"required",
|
||||
String(part.partId),
|
||||
);
|
||||
}
|
||||
|
||||
const price = parseMoneyAmountToman(part.price);
|
||||
@@ -60,21 +126,96 @@ export function getExpertReplyPricingValidationError(
|
||||
|
||||
const amountIsValid = (amount: number | null) => amount !== null;
|
||||
const priceIsRequired = part.typeOfDamage === TypeOfDamage.Change;
|
||||
const priceIsValid = !hasPrice || amountIsValid(price);
|
||||
|
||||
if (
|
||||
!amountIsValid(salary) ||
|
||||
!amountIsValid(totalPayment) ||
|
||||
(priceIsRequired && !amountIsValid(price)) ||
|
||||
!priceIsValid
|
||||
) {
|
||||
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}'.`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user