reading_method) { return false; } // پرچم صریح «قیمت اپ» برای هر دسته‌ای (شمش، سکه، …) — قیمت هر گرم × وزن if (Product::hasAppPriceColumn() && (bool) ($product->app_price ?? false)) { return true; } if ((int) ($product->direct_price ?? 0) === 1) { return false; } return $this->isMeltedTradeProduct($product) || $this->usesQuantityAppPrice($product); } /** محصولی که با پرچم صریح app_price قیمت اپ می‌گیرد (نه استنتاج آبشده/مقداری) */ public function usesFlaggedAppPrice(Product $product): bool { return Product::hasAppPriceColumn() && (bool) ($product->app_price ?? false) && ! $product->reading_method; } /** ضریب وزن برای محصول قیمت‌اپ: وزن واحد به گرم‌واحد محصول (amount) یا ۱ */ public function appPriceWeightFactor(Product $product): float { $amount = (float) ($product->amount ?? 0); return $amount > 0 ? $amount : 1.0; } /** معامله مقداری با قیمت اپ — نه قیمت دستی قدیمی با price پر در DB */ private function usesQuantityAppPrice(Product $product): bool { if (! ($product->trade_by_quantity ?? false)) { return false; } if ($product->reading_method || (int) ($product->direct_price ?? 0) === 1) { return false; } return (int) ($product->price ?? 0) <= 0; } /** * پر کردن price روی مدل‌ها برای پاسخ JSON — بدون UPDATE دیتابیس */ /** پر کردن کش مظنه و فید zaarbox اگر خالی باشند (قبل از enrich لیست مشتری) */ public function warmLivePriceCache(): void { if (! is_array(Cache::get(self::LIVE_PRICE_CACHE_KEY))) { $this->resolveFallbackPayload(); } // فروشگاه و خانگی هر دو ممکن است reading_method / قیمت اپ داشته باشند app(ZaarboxPriceService::class)->cachedPriceList(false); } public function enrichForApi(Collection|iterable $products): void { $collection = collect($products); $zaarbox = app(ZaarboxPriceService::class); // مثل ادمین — روی cache miss یک بار fetch؛ نه فقط getCachedPriceListOnly $zaarboxFeed = $zaarbox->cachedPriceList(false); foreach ($collection as $product) { if (!$product instanceof Product) { continue; } if (FormulaPriceService::isEnabled($product) && $product->reading_method && is_array($zaarboxFeed)) { $this->applyFormulaFeedPrice($product, $zaarboxFeed, $zaarbox); continue; } if ($this->usesAppPrice($product)) { $base = $this->productUsesLastPriceIfZero($product) ? $this->resolveAppBasePrice() : $this->resolveStrictLiveAppBasePrice(); $this->applyAppPrice($product, $base); continue; } if ($product->reading_method && is_array($zaarboxFeed)) { $this->applyReferenceFeedPrice($product, $zaarboxFeed, $zaarbox); continue; } if ($product->reading_method && ! $this->productUsesLastPriceIfZero($product)) { $product->setAttribute('reference_provider_active', false); $this->zeroProductPrice($product); continue; } if ($product->reading_method) { $product->setAttribute('reference_provider_active', true); continue; } if ($this->isWeightBasedStoreProduct($product)) { $this->enrichWeightBasedPrice($product); } } } /** شمش / سکه و … — قیمت واحد (هر گرم) از مظنه زنده یا آخرین قیمت */ private function isWeightBasedStoreProduct(Product $product): bool { if ($this->usesAppPrice($product) || $product->reading_method) { return false; } if ((int) ($product->direct_price ?? 0) === 1) { return false; } return (float) ($product->amount ?? 0) > 0; } private function enrichWeightBasedPrice(Product $product): void { $useLast = $this->productUsesLastPriceIfZero($product); $unitBuy = $useLast ? $this->resolveLiveBuyPriceWithFallback() : $this->resolveStrictLiveBuyPrice(); if ($unitBuy <= 0 && app(ConfigService::class)->getBool('show_last_price', false)) { $unitBuy = $this->resolveHistoricalBasePrice(); } $offset = ProductPriceService::offsetPercent($product); if ($unitBuy > 0 && $offset != 0.0) { $unitBuy = ProductPriceService::adjustedTotal($unitBuy, 0, $offset); } $this->applyPriceOrFallback($product, $unitBuy, $useLast); } private function resolveStrictLiveBuyPrice(): int { $cached = Cache::get(self::LIVE_PRICE_CACHE_KEY); if (! is_array($cached)) { return 0; } $buy = (int) ($cached['buy'] ?? 0); if ($buy > 0) { return $buy; } $priceLast = (int) ($cached['price_last'] ?? 0); if ($priceLast > 0) { return $priceLast; } return $this->extractBaseFromPayload($cached); } private function resolveLiveBuyPriceWithFallback(): int { $strict = $this->resolveStrictLiveBuyPrice(); if ($strict > 0) { return $strict; } $payload = $this->resolveFallbackPayload(); return (int) ($payload['buy'] ?? 0) ?: (int) ($payload['price_last'] ?? 0); } 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; } private function zeroProductPrice(Product $product): void { $product->setAttribute('price', 0); if (Schema::hasColumn($product->getTable(), 'price_profit')) { $product->setAttribute('price_profit', 0); } } /** * فقط قیمت زنده (کش /api/price/last) — بدون montzar_live_price قدیمی settings. * وقتی use_last_price_if_zero خاموش است. */ public function resolveStrictLiveAppBasePrice(): int { $cached = Cache::get(self::LIVE_PRICE_CACHE_KEY); if (!is_array($cached)) { return 0; } return $this->extractBaseFromPayload($cached); } /** @param array> $feed */ private function applyFormulaFeedPrice(Product $product, array $feed, ZaarboxPriceService $zaarbox): void { $useLast = $this->productUsesLastPriceIfZero($product); $row = $zaarbox->findFeedRow($feed, (string) $product->reading_method); if ($row === null) { $product->setAttribute('reference_provider_active', $useLast); $this->applyPriceOrFallback($product, 0, $useLast); return; } $parts = $zaarbox->resolveReferenceBaseProfit($row); $providerActive = $useLast || $parts['price'] > 0; $product->setAttribute('reference_provider_active', $providerActive); if (! $useLast && $parts['price'] <= 0) { $this->zeroProductPrice($product); return; } $computed = FormulaPriceService::calculateForProduct($product, [ 'price' => $parts['price'], 'active' => true, ]); if ($computed === null || $computed <= 0) { $this->applyPriceOrFallback($product, 0, $useLast); return; } $product->setAttribute('price', $computed); if (Schema::hasColumn($product->getTable(), 'price_profit')) { $product->setAttribute('price_profit', 0); } } /** @param array> $feed */ private function applyReferenceFeedPrice(Product $product, array $feed, ZaarboxPriceService $zaarbox): void { $useLast = $this->productUsesLastPriceIfZero($product); $row = $zaarbox->findFeedRow($feed, (string) $product->reading_method); if ($row === null) { $product->setAttribute('reference_provider_active', $useLast); $offset = ProductPriceService::offsetPercent($product); $historical = $useLast ? $this->resolveHistoricalBasePrice() : 0; $computed = $historical > 0 ? ProductPriceService::adjustedTotal($historical, 0, $offset) : 0; $this->applyPriceOrFallback($product, $computed, $useLast); return; } $providerRaw = $zaarbox->resolveReferenceProviderTotal($row); $providerActive = $useLast || $providerRaw > 0; $product->setAttribute('reference_provider_active', $providerActive); if (! $useLast && $providerRaw <= 0) { $this->zeroProductPrice($product); return; } $computed = $zaarbox->computeAdjustedProductPrice($product, $row); if ($computed <= 0) { 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); } } else { $this->zeroProductPrice($product); } } else { $this->zeroProductPrice($product); } return; } $product->setAttribute('price', $computed); if (Schema::hasColumn($product->getTable(), 'price_profit')) { $product->setAttribute('price_profit', 0); } } /** @param array $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()); } /** قیمت پایه سفارش — همان منبع نمایش اپ (کش / montzar_live_price / prices) */ public function resolveOrderBasePrice(?Product $product = null): int { $this->warmLivePriceCache(); if ($product instanceof Product) { if ($this->usesAppPrice($product)) { $base = $this->resolveAppBasePrice(); if ($base <= 0) { $base = $this->resolveHistoricalBasePrice(); } if ($base <= 0 && $this->productUsesLastPriceIfZero($product)) { $base = $this->resolveLastPriceFallback($product); } if ($base <= 0) { return 0; } $offset = Product::hasPriceOffsetColumn() ? (float) ($product->price_offset_percent ?? 0) : 0.0; return ProductPriceService::adjustedTotal($base, 0, $offset); } if ($product->reading_method) { $this->enrichForApi(collect([$product])); $unit = (int) (($product->price ?? 0) + ($product->price_profit ?? 0)); if ($unit > 0) { return $unit; } } if (FormulaPriceService::isEnabled($product)) { return (int) (($product->price ?? 0) + ($product->price_profit ?? 0)); } if ((int) ($product->direct_price ?? 0) === 1) { return (int) (($product->price ?? 0) + ($product->price_profit ?? 0)); } } $base = $this->resolveAppBasePrice(); if ($base > 0) { return $base; } return $this->resolveHistoricalBasePrice(); } /** * قیمت پایهٔ سفارش برای یک جهت — با اختلاف واقعی نرخ همکار. * پایه وسطِ نرخ خرید/فروش همکار است؛ سفارش باید با همان سمتی حساب شود که ما با همکار معامله می‌کنیم. */ public function resolveOrderTradePrice(?Product $product = null, ?int $direction = null): int { $base = (float) $this->resolveOrderBasePrice($product); return (int) $this->applyProviderTradeSpread($base, $product, $direction); } /** * خرید → پایه + اختلاف نرخ همکار، فروش → پایه − اختلاف. * فقط روی قیمتی که از مظنهٔ اپ آمده؛ محصول با قیمت دستی/فید/فرمول دست نمی‌خورد. * ceil مثل buildTradeDisplayPrices تا قیمت سفارش با قیمت نمایشی اپ یکی بماند. */ public function applyProviderTradeSpread( float $basePrice, ?Product $product = null, ?int $direction = null ): float { if ($basePrice <= 0 || $direction === null) { return $basePrice; } if ($product instanceof Product && ! $this->usesAppPrice($product)) { return $basePrice; } $spread = app(ConfigService::class)->providerTradeSpreadRial(); if ($spread <= 0) { return $basePrice; } return $direction === 2 ? max(0.0, ceil($basePrice - $spread)) : ceil($basePrice + $spread); } /** @param array $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)); } } 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->displayBuyPriceDiffRial(); $diffSell = (int) $config->displaySellPriceDiffRial(); $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)->cachedPriceList(false); 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 app(ZaarboxPriceService::class)->resolveReferenceProviderTotal($row); } 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; } /** * نمایش در اپ کاربر: * • خاموش (show=false در DB) → false * • قیمت نمایشی بعد از enrich → true * • قیمت صفر و show_last_price خاموش → false * • قیمت صفر و show_last_price روشن و fallback موجود → true */ public function resolveCustomerShow(Product $product): bool { if (! (bool) ($product->active ?? true)) { return false; } if (! app(ConfigService::class)->isProductWithinTradingHours($product)) { return false; } if ($this->hasCustomerDisplayPrice($product)) { return true; } if (! app(ConfigService::class)->getBool('show_last_price', false)) { return false; } return $this->resolveLastPriceFallback($product) > 0 || $this->resolveHistoricalBasePrice() > 0; } }