price_formula_enabled); } public static function isTvEnabled(?Product $product): bool { if (! Product::hasTvPriceFormulaColumns()) { return false; } return (bool) ($product?->show_on_tv) && (bool) ($product?->tv_price_formula_enabled); } /** * @param array|null $mazanehRow */ public static function calculateTvForProduct(Product $product, ?array $mazanehRow): ?int { if (! self::isTvEnabled($product) || $mazanehRow === null) { return null; } if (array_key_exists('active', $mazanehRow) && $mazanehRow['active'] == false) { return null; } $p = (float) ($mazanehRow['price'] ?? 0); if ($p <= 0) { return null; } $formula = trim((string) ($product->tv_price_formula ?? '')); if ($formula === '') { $formula = self::DEFAULT_FORMULA; } return self::evaluate($formula, $p, self::resolveWeight($product->amount ?? 0)); } /** * P = قیمت وسط مرجع (بدون تفاوت ارائه‌دهنده / بدون price_offset_percent) * * @param array|null $mazanehRow */ public static function calculateForProduct(Product $product, ?array $mazanehRow): ?int { if (! self::isEnabled($product) || $mazanehRow === null) { return null; } if (array_key_exists('active', $mazanehRow) && $mazanehRow['active'] == false) { return null; } $p = (float) ($mazanehRow['price'] ?? 0); if ($p <= 0) { return null; } $formula = trim((string) ($product->price_formula ?? '')); if ($formula === '') { $formula = self::DEFAULT_FORMULA; } return self::evaluate($formula, $p, self::resolveWeight($product->amount ?? 0)); } public static function resolveWeight(float $amount): float { return $amount == 0.0 ? 1.0 : (float) $amount; } public static function evaluate(string $formula, float $p, float $w): ?int { $formula = trim($formula); if ($formula === '') { return null; } try { $result = self::evaluateExpression($formula, [ 'P' => $p, 'W' => $w, 'MG' => self::MG, ]); } catch (InvalidArgumentException $e) { return null; } if (! is_finite($result)) { return null; } return (int) round($result); } /** * @param array $variables */ private static function evaluateExpression(string $expression, array $variables): float { $tokens = self::tokenize($expression); $position = 0; $result = self::parseExpression($tokens, $position, $variables); if ($position < count($tokens)) { throw new InvalidArgumentException('Invalid formula syntax'); } return $result; } /** * @return array */ private static function tokenize(string $expression): array { $expression = str_replace(['×', '÷'], ['*', '/'], $expression); $expression = preg_replace('/\s+/', '', $expression) ?? ''; if ($expression === '' || ! preg_match('/^[0-9A-Za-z_.,+\-*\/()]+$/', $expression)) { throw new InvalidArgumentException('Invalid formula characters'); } preg_match_all('/([0-9]+(?:\.[0-9]+)?|[A-Za-z_][A-Za-z0-9_]*|[+\-*\/(),])/', $expression, $matches); if (($matches[0] ?? []) === [] || implode('', $matches[0]) !== $expression) { throw new InvalidArgumentException('Invalid formula syntax'); } $tokens = []; foreach ($matches[0] as $part) { if (preg_match('/^[0-9]/', $part)) { $tokens[] = ['type' => 'number', 'value' => $part]; } elseif (preg_match('/^[A-Za-z_]/', $part)) { $tokens[] = ['type' => 'ident', 'value' => strtoupper($part)]; } else { $tokens[] = ['type' => 'op', 'value' => $part]; } } return $tokens; } /** * @param array $tokens * @param array $variables */ private static function parseExpression(array $tokens, int &$position, array $variables): float { $value = self::parseTerm($tokens, $position, $variables); while ($position < count($tokens) && in_array($tokens[$position]['value'], ['+', '-'], true)) { $operator = $tokens[$position]['value']; $position++; $right = self::parseTerm($tokens, $position, $variables); $value = $operator === '+' ? $value + $right : $value - $right; } return $value; } /** * @param array $tokens * @param array $variables */ private static function parseTerm(array $tokens, int &$position, array $variables): float { $value = self::parseFactor($tokens, $position, $variables); while ($position < count($tokens) && in_array($tokens[$position]['value'], ['*', '/'], true)) { $operator = $tokens[$position]['value']; $position++; $right = self::parseFactor($tokens, $position, $variables); if ($operator === '/') { if (abs($right) < 1e-12) { throw new InvalidArgumentException('Division by zero'); } $value /= $right; } else { $value *= $right; } } return $value; } /** * @param array $tokens * @param array $variables */ private static function parseFactor(array $tokens, int &$position, array $variables): float { if ($position >= count($tokens)) { throw new InvalidArgumentException('Unexpected end of formula'); } $token = $tokens[$position]; if ($token['type'] === 'number') { $position++; return (float) $token['value']; } if ($token['type'] === 'ident') { $position++; $name = $token['value']; if ($position < count($tokens) && $tokens[$position]['value'] === '(') { $position++; $args = []; if ($position >= count($tokens) || $tokens[$position]['value'] !== ')') { $args[] = self::parseExpression($tokens, $position, $variables); while ($position < count($tokens) && $tokens[$position]['value'] === ',') { $position++; $args[] = self::parseExpression($tokens, $position, $variables); } } if ($position >= count($tokens) || $tokens[$position]['value'] !== ')') { throw new InvalidArgumentException('Missing closing parenthesis'); } $position++; return self::applyFunction($name, $args); } if (! array_key_exists($name, $variables)) { throw new InvalidArgumentException("Unknown variable: {$name}"); } return (float) $variables[$name]; } if ($token['value'] === '(') { $position++; $value = self::parseExpression($tokens, $position, $variables); if ($position >= count($tokens) || $tokens[$position]['value'] !== ')') { throw new InvalidArgumentException('Missing closing parenthesis'); } $position++; return $value; } if ($token['value'] === '-') { $position++; return -self::parseFactor($tokens, $position, $variables); } if ($token['value'] === '+') { $position++; return self::parseFactor($tokens, $position, $variables); } throw new InvalidArgumentException('Invalid formula syntax'); } /** * @param list $args */ private static function applyFunction(string $name, array $args): float { if ($args === []) { throw new InvalidArgumentException("Invalid arguments for {$name}"); } $value = (float) $args[0]; $precision = isset($args[1]) ? (int) $args[1] : 0; return match ($name) { 'ROUND' => (float) round($value, $precision), 'FLOOR' => self::floorPrecision($value, $precision), 'CEIL', 'CEILING' => self::ceilPrecision($value, $precision), default => throw new InvalidArgumentException("Unknown function: {$name}"), }; } private static function floorPrecision(float $value, int $precision): float { $factor = 10 ** $precision; return floor($value * $factor) / $factor; } private static function ceilPrecision(float $value, int $precision): float { $factor = 10 ** $precision; return ceil($value * $factor) / $factor; } }