Files
boursecu_server/app/Services/FormulaPriceService.php
T
mohmdsdgandCursor a321ebdaad feat(product): add show_in_list toggle and TV price formula
- New `show_in_list` column to control product visibility in customer list
- New `tv_price_formula_enabled` + `tv_price_formula` columns for separate TV pricing
- FormulaPriceService: isTvEnabled() and calculateTvForProduct()
- listTvProducts applies TV formula overrides before returning
- filterProductData handles both new field sets

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-19 11:51:54 +03:30

317 lines
9.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);
}
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<string, mixed>|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<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;
}
}