Files
boursecu_server/app/Services/FormulaPriceService.php
T
mohmdsdgandCursor f421fcb83f Add formula-based product pricing (P/W/MG) for Parsian-style unit prices.
Port FormulaPriceService from colleague app so admins can compute a single unit price from reference mid, weight, and wage instead of offset coefficients.

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

282 lines
8.6 KiB
PHP
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.
<?php
namespace App\Services;
use App\Models\Product;
use InvalidArgumentException;
class FormulaPriceService
{
/** ضریب تبدیل مثقال به گرم — قابل استفاده در فرمول با MG */
public const MG = 4.331802;
public const DEFAULT_FORMULA = '((P)/MG)*W+100000';
public static function isEnabled(?Product $product): bool
{
if (! Product::hasPriceFormulaColumns()) {
return false;
}
return (bool) ($product?->price_formula_enabled);
}
/**
* P = قیمت وسط مرجع (بدون تفاوت ارائه‌دهنده / بدون price_offset_percent)
*
* @param array<string, mixed>|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<string, float> $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<int, array{type: string, value: string}>
*/
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<int, array{type: string, value: string}> $tokens
* @param array<string, float> $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<int, array{type: string, value: string}> $tokens
* @param array<string, float> $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<int, array{type: string, value: string}> $tokens
* @param array<string, float> $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<float> $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;
}
}