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; }