Files
boursecu_server/app/Services/ProductDisplayPriceService.php
T
2026-06-11 21:37:17 +03:30

329 lines
10 KiB
PHP

<?php
namespace App\Services;
use App\Models\Price;
use App\Models\Product;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
/**
* قیمت نمایشی محصولات برای API — فقط از کش/تنظیمات (بدون HTTP روی list).
*
* • آبشده با «قیمت اپ» → montzar_live_price / کش /api/price/last
* • محصول با reading_method → ردیف فید zaarbox از کش کران (۳۰ث)
*/
class ProductDisplayPriceService
{
public const LIVE_PRICE_CACHE_KEY = 'live_price_api_payload';
public const LIVE_PRICE_CACHE_SECONDS = 45;
public function usesAppPrice(Product $product): bool
{
if ($product->reading_method) {
return false;
}
if ((int) ($product->direct_price ?? 0) === 1) {
return false;
}
return $this->isMeltedTradeProduct($product);
}
/**
* پر کردن price روی مدل‌ها برای پاسخ JSON — بدون UPDATE دیتابیس
*/
public function enrichForApi(Collection|iterable $products): void
{
$collection = collect($products);
$zaarbox = app(ZaarboxPriceService::class);
$zaarboxFeed = $zaarbox->getCachedPriceListOnly();
$baseAppPrice = 0;
if ($collection->contains(fn ($p) => $p instanceof Product && $this->usesAppPrice($p))) {
$baseAppPrice = $this->resolveAppBasePrice();
}
foreach ($collection as $product) {
if (!$product instanceof Product) {
continue;
}
if ($this->usesAppPrice($product)) {
$this->applyAppPrice($product, $baseAppPrice);
continue;
}
if ($product->reading_method && is_array($zaarboxFeed)) {
$this->applyReferenceFeedPrice($product, $zaarboxFeed, $zaarbox);
}
}
}
private function applyAppPrice(Product $product, int $basePrice): void
{
$useLast = $this->productUsesLastPriceIfZero($product);
$offset = Product::hasPriceOffsetColumn()
? (float) ($product->price_offset_percent ?? 0)
: 0.0;
if ($basePrice <= 0) {
$this->applyPriceOrFallback($product, 0, $useLast);
return;
}
$computed = ProductPriceService::adjustedTotal($basePrice, 0, $offset);
$this->applyPriceOrFallback($product, $computed, $useLast);
}
private function applyPriceOrFallback(Product $product, int $computed, bool $useLast): void
{
if ($computed > 0) {
$product->setAttribute('price', $computed);
if (Schema::hasColumn($product->getTable(), 'price_profit')) {
$product->setAttribute('price_profit', 0);
}
return;
}
if ($useLast) {
$fallback = $this->resolveLastPriceFallback($product);
if ($fallback > 0) {
$product->setAttribute('price', $fallback);
if (Schema::hasColumn($product->getTable(), 'price_profit')) {
$product->setAttribute('price_profit', 0);
}
return;
}
}
$product->setAttribute('price', 0);
if (Schema::hasColumn($product->getTable(), 'price_profit')) {
$product->setAttribute('price_profit', 0);
}
}
private function productUsesLastPriceIfZero(Product $product): bool
{
return Product::hasUseLastPriceIfZeroColumn()
&& ($product->use_last_price_if_zero ?? false);
}
/**
* آخرین قیمت ذخیره‌شده: اول products.price، بعد جدول prices (تاریخچه مظنه).
*/
public function resolveLastPriceFallback(Product $product): int
{
$stored = (int) ($product->price ?? 0);
if ($stored > 0) {
return $stored;
}
$storedProfit = (int) ($product->price_profit ?? 0);
if ($storedProfit > 0) {
return $stored + $storedProfit;
}
$historical = $this->resolveHistoricalBasePrice();
if ($historical <= 0) {
return 0;
}
if ($this->usesAppPrice($product) || $product->reading_method) {
$offset = Product::hasPriceOffsetColumn()
? (float) ($product->price_offset_percent ?? 0)
: 0.0;
return ProductPriceService::adjustedTotal($historical, 0, $offset);
}
return $historical;
}
public function resolveHistoricalBasePrice(): int
{
$last = Price::orderByDesc('id')->first();
if (!$last || (float) $last->price <= 0) {
return 0;
}
return (int) $last->price;
}
/** @param array<int, array<string, mixed>> $feed */
private function applyReferenceFeedPrice(Product $product, array $feed, ZaarboxPriceService $zaarbox): void
{
$row = $zaarbox->findFeedRow($feed, (string) $product->reading_method);
if ($row === null) {
return;
}
$computed = $zaarbox->computeAdjustedProductPrice($product, $row);
if ($computed <= 0) {
if ($this->productUsesLastPriceIfZero($product)) {
$fallback = $this->resolveLastPriceFallback($product);
if ($fallback > 0) {
$product->setAttribute('price', $fallback);
if (Schema::hasColumn($product->getTable(), 'price_profit')) {
$product->setAttribute('price_profit', 0);
}
}
}
return;
}
$product->setAttribute('price', $computed);
if (Schema::hasColumn($product->getTable(), 'price_profit')) {
$product->setAttribute('price_profit', 0);
}
}
/** @param array<string, mixed> $payload */
public function cacheLivePricePayload(array $payload): void
{
Cache::put(self::LIVE_PRICE_CACHE_KEY, [
'price' => (int) ($payload['price'] ?? 0),
'buy' => (int) ($payload['buy'] ?? 0),
'sell' => (int) ($payload['sell'] ?? 0),
'price_last' => (int) ($payload['price_last'] ?? 0),
], now()->addSeconds(self::LIVE_PRICE_CACHE_SECONDS));
}
public function resolveAppBasePrice(): int
{
return $this->extractBaseFromPayload($this->livePricePayload());
}
/** @param array<string, mixed> $payload */
private function extractBaseFromPayload(array $payload): int
{
$price = (int) ($payload['price'] ?? 0);
if ($price > 0) {
return $price;
}
$buy = (int) ($payload['buy'] ?? 0);
$sell = (int) ($payload['sell'] ?? 0);
if ($buy > 0 && $sell > 0) {
return (int) round(($buy + $sell) / 2);
}
$priceLast = (int) ($payload['price_last'] ?? 0);
if ($priceLast > 0) {
return $priceLast;
}
return 0;
}
/** @return array{price: int, buy: int, sell: int, price_last: int} */
private function livePricePayload(): array
{
$cached = Cache::get(self::LIVE_PRICE_CACHE_KEY);
if (is_array($cached)) {
return $cached;
}
return $this->resolveFallbackPayload();
}
/** @return array{price: int, buy: int, sell: int, price_last: int} */
private function resolveFallbackPayload(): array
{
$config = app(ConfigService::class);
$provider = $config->priceProvider();
$price = 0;
if ($provider === 'montzar') {
$price = $config->getInt('montzar_live_price', 0);
} elseif ($provider === 'zaarbox') {
$price = $this->resolveZaarboxBoardPriceFromCache($config);
} elseif ($provider === 'main_product') {
$main = $config->mainProduct();
if ($main) {
$price = (int) (($main->price ?? 0) + ($main->price_profit ?? 0));
}
} elseif ($provider === 'taban_gohar') {
$price = $config->getInt('taban_gohar_live_price', 0);
}
if ($price <= 0 && $config->getBool('show_last_price', false)) {
$last = Price::orderByDesc('id')->first();
if ($last && (float) $last->price > 0) {
$price = (int) $last->price;
}
}
$diffBuy = (int) $config->displayBuySpreadRial();
$diffSell = (int) $config->displaySellSpreadRial();
$payload = [
'price' => $price,
'buy' => $price > 0 ? $price + $diffBuy : 0,
'sell' => $price > 0 ? max(0, $price - $diffSell) : 0,
'price_last' => $price > 0 ? $price + $diffBuy : 0,
];
if ($this->extractBaseFromPayload($payload) > 0) {
$this->cacheLivePricePayload($payload);
}
return $payload;
}
private function resolveZaarboxBoardPriceFromCache(ConfigService $config): int
{
$identifier = trim($config->getString('zaarbox_price_key', ''));
if ($identifier === '') {
return 0;
}
$feed = app(ZaarboxPriceService::class)->getCachedPriceListOnly();
if (!is_array($feed)) {
return 0;
}
$row = app(ZaarboxPriceService::class)->findFeedRow($feed, $identifier);
if ($row === null) {
return 0;
}
$row = app(ZaarboxPriceService::class)->normalizeFeedRow($row);
return (int) round((float) ($row['price'] ?? 0) + (float) ($row['profit'] ?? 0));
}
private function isMeltedTradeProduct(Product $product): bool
{
$group = trim((string) ($product->group_type ?? ''));
return in_array($group, ['آبشده', 'کاتد مس'], true);
}
/**
* آیا بعد از enrich قیمت پایه برای نمایش در اپ کاربر وجود دارد؟
* کارمزد (cash_fee) به‌تنهایی قیمت محسوب نمی‌شود.
*/
public function hasCustomerDisplayPrice(Product $product): bool
{
if ((int) ($product->price ?? 0) > 0) {
return true;
}
if (Schema::hasColumn($product->getTable(), 'price_profit')) {
if ((int) ($product->price_profit ?? 0) > 0) {
return true;
}
}
return false;
}
}