Files
boursecu_admin/src/utils/formulaPrice.ts
T
mohmdsdgandCursor 4dde172ed7 Add formula price controls to the product editor.
Admins can enable a reference-based formula (P/W/MG) with live calculated unit price preview, matching the colleague-app pricing flow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 16:47:39 +03:30

210 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** ضریب تبدیل مثقال به گرم — در فرمول با MG */
export const FORMULA_MG = 4.331802
export const DEFAULT_PRICE_FORMULA = '((P)/MG)*W+100000'
/** اگر وزن ۰ یا نامعتبر باشد، ۱ در نظر گرفته می‌شود */
export function resolveFormulaWeight(amount: number | null | undefined): number {
const w = Number(amount)
return !Number.isFinite(w) || w === 0 ? 1 : w
}
export const FORMULA_VARIABLE_HELP_BASE = [
{ symbol: 'P', label: 'قیمت وسط مرجع (فقط مظنه انتخاب‌شده، بدون تفاوت ارائه‌دهنده)' },
{ symbol: 'MG', label: `ضریب مثقال به گرم (${FORMULA_MG})` }
] as const
export const FORMULA_FUNCTION_HELP = [
{ symbol: 'ROUND(x)', label: 'رند به نزدیک‌ترین عدد صحیح' },
{ symbol: 'ROUND(x, -3)', label: 'رند به هزار (مثل اکسل؛ -4 ده‌هزار، -5 صدهزار)' },
{ symbol: 'FLOOR(x)', label: 'گرد به پایین' },
{ symbol: 'CEIL(x)', label: 'گرد به بالا' }
] as const
export function getFormulaVariableHelp(productAmount: number) {
const w = resolveFormulaWeight(productAmount)
return [
FORMULA_VARIABLE_HELP_BASE[0],
{
symbol: 'W',
label: `وزن محصول (${w})`
},
FORMULA_VARIABLE_HELP_BASE[1],
...FORMULA_FUNCTION_HELP
]
}
type FormulaToken =
| { type: 'number'; value: string }
| { type: 'ident'; value: string }
| { type: 'op'; value: string }
function tokenize(expression: string): FormulaToken[] {
const normalized = expression.replace(/×/g, '*').replace(/÷/g, '/').replace(/\s+/g, '')
if (!normalized || !/^[0-9A-Za-z_.,+\-*/()]+$/.test(normalized)) {
throw new Error('Invalid formula characters')
}
const parts = normalized.match(/([0-9]+(?:\.[0-9]+)?|[A-Za-z_][A-Za-z0-9_]*|[+\-*/(),])/g)
if (!parts || parts.join('') !== normalized) {
throw new Error('Invalid formula syntax')
}
return parts.map((part) => {
if (/^[0-9]/.test(part)) return { type: 'number', value: part }
if (/^[A-Za-z_]/.test(part)) return { type: 'ident', value: part.toUpperCase() }
return { type: 'op', value: part }
})
}
function roundPrecision(value: number, precision = 0): number {
const p = Number(precision) || 0
const factor = 10 ** p
return Math.round(value * factor) / factor
}
function floorPrecision(value: number, precision = 0): number {
const p = Number(precision) || 0
const factor = 10 ** p
return Math.floor(value * factor) / factor
}
function ceilPrecision(value: number, precision = 0): number {
const p = Number(precision) || 0
const factor = 10 ** p
return Math.ceil(value * factor) / factor
}
function applyFunction(name: string, args: number[]): number {
if (args.length < 1) throw new Error(`Invalid arguments for ${name}`)
switch (name) {
case 'ROUND':
return roundPrecision(args[0], args[1] ?? 0)
case 'FLOOR':
return floorPrecision(args[0], args[1] ?? 0)
case 'CEIL':
case 'CEILING':
return ceilPrecision(args[0], args[1] ?? 0)
default:
throw new Error(`Unknown function: ${name}`)
}
}
function parseFactor(tokens: FormulaToken[], position: { i: number }, variables: Record<string, number>): number {
const token = tokens[position.i]
if (!token) throw new Error('Unexpected end of formula')
if (token.type === 'number') {
position.i++
return Number(token.value)
}
if (token.type === 'ident') {
position.i++
const name = token.value
const next = tokens[position.i]
if (next && next.type === 'op' && next.value === '(') {
position.i++
const args: number[] = []
if (!(tokens[position.i] && tokens[position.i].type === 'op' && tokens[position.i].value === ')')) {
args.push(parseExpression(tokens, position, variables))
while (tokens[position.i] && tokens[position.i].type === 'op' && tokens[position.i].value === ',') {
position.i++
args.push(parseExpression(tokens, position, variables))
}
}
if (!tokens[position.i] || tokens[position.i].type !== 'op' || tokens[position.i].value !== ')') {
throw new Error('Missing closing parenthesis')
}
position.i++
return applyFunction(name, args)
}
if (!(name in variables)) throw new Error(`Unknown variable: ${name}`)
return variables[name]
}
if (token.type === 'op' && token.value === '(') {
position.i++
const value = parseExpression(tokens, position, variables)
if (!tokens[position.i] || tokens[position.i].type !== 'op' || tokens[position.i].value !== ')') {
throw new Error('Missing closing parenthesis')
}
position.i++
return value
}
if (token.type === 'op' && token.value === '-') {
position.i++
return -parseFactor(tokens, position, variables)
}
if (token.type === 'op' && token.value === '+') {
position.i++
return parseFactor(tokens, position, variables)
}
throw new Error('Invalid formula syntax')
}
function parseTerm(tokens: FormulaToken[], position: { i: number }, variables: Record<string, number>): number {
let value = parseFactor(tokens, position, variables)
while (position.i < tokens.length && tokens[position.i].type === 'op' && ['*', '/'].includes(tokens[position.i].value)) {
const operator = tokens[position.i].value
position.i++
const right = parseFactor(tokens, position, variables)
value = operator === '/' ? value / right : value * right
}
return value
}
function parseExpression(tokens: FormulaToken[], position: { i: number }, variables: Record<string, number>): number {
let value = parseTerm(tokens, position, variables)
while (position.i < tokens.length && tokens[position.i].type === 'op' && ['+', '-'].includes(tokens[position.i].value)) {
const operator = tokens[position.i].value
position.i++
const right = parseTerm(tokens, position, variables)
value = operator === '+' ? value + right : value - right
}
return value
}
export function evaluatePriceFormula(formula: string, p: number, w: number): number | null {
const trimmed = (formula || '').trim()
if (!trimmed) return null
try {
const tokens = tokenize(trimmed)
const position = { i: 0 }
const result = parseExpression(tokens, position, {
P: Number(p) || 0,
W: Number(w) || 0,
MG: FORMULA_MG
})
if (position.i < tokens.length || !Number.isFinite(result)) {
return null
}
return Math.round(result)
} catch {
return null
}
}
/** P = قیمت وسط مرجع (بدون تفاوت ارائه‌دهنده) */
export function computeFormulaProductPrice(
referenceMiddlePrice: number,
formula: string | undefined | null,
weight: number
): number {
const expression = (formula || '').trim() || DEFAULT_PRICE_FORMULA
return evaluatePriceFormula(expression, referenceMiddlePrice, resolveFormulaWeight(weight)) ?? 0
}