2026-01-24 14:03:11 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers\Site;
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Admin\PriceController;
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
|
|
use App\Models\Cnt;
|
|
|
|
|
use App\Models\ConfigStatic;
|
2026-05-25 12:47:04 +03:30
|
|
|
use App\Services\MontzarService;
|
2026-06-04 14:40:02 +03:30
|
|
|
use App\Services\ZaarboxPriceService;
|
2026-01-24 14:03:11 +00:00
|
|
|
use App\Models\Price;
|
|
|
|
|
use App\Models\PriceCandle;
|
|
|
|
|
use App\Models\Product;
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
|
use Morilog\Jalali\Jalalian;
|
2026-05-25 12:47:04 +03:30
|
|
|
use App\Services\ConfigService;
|
2026-06-13 17:35:40 +03:30
|
|
|
use App\Services\LivePriceCacheService;
|
2026-06-24 11:34:23 +03:30
|
|
|
use App\Services\PriceSnapshotService;
|
2026-06-24 19:13:36 +03:30
|
|
|
use App\Services\PriceSyncLogService;
|
2026-06-11 16:41:28 +03:30
|
|
|
use App\Services\ProductDisplayPriceService;
|
2026-01-24 14:03:11 +00:00
|
|
|
|
2026-05-11 17:21:56 +00:00
|
|
|
|
2026-01-24 14:03:11 +00:00
|
|
|
class ReportController extends Controller
|
|
|
|
|
{
|
|
|
|
|
public function candleChart()
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
$candle = PriceCandle::select(["start",
|
|
|
|
|
"high",
|
|
|
|
|
"low",
|
|
|
|
|
"end",
|
|
|
|
|
"date_at"])->whereDate('date_at', '<=', date("Y-m-d"))
|
|
|
|
|
->whereDate('date_at', '>', Carbon::now()->subDays(30)->format("Y-m-d"))
|
|
|
|
|
->get()->toArray();
|
|
|
|
|
$a = [];
|
|
|
|
|
foreach ($candle as $c) {
|
|
|
|
|
$create['date'] = $c["date_at"];
|
|
|
|
|
unset($c["date_at"]);
|
|
|
|
|
$create['candle'] = array_values($c);
|
|
|
|
|
$a[] = array_values($create);
|
|
|
|
|
}
|
|
|
|
|
return response()->json($a);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function updatePrise()
|
|
|
|
|
{
|
2026-08-18 22:23:20 +03:30
|
|
|
app(\App\Services\PriceTriggerService::class)->evaluateActive();
|
2026-08-23 14:16:00 +03:30
|
|
|
try {
|
|
|
|
|
app(\App\Services\Automation\AutomationEngine::class)->evaluatePriceMove();
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
report($e);
|
|
|
|
|
}
|
2026-01-24 14:03:11 +00:00
|
|
|
|
|
|
|
|
return response()->json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function upDownPrice()
|
2026-06-03 21:52:37 +03:30
|
|
|
{
|
|
|
|
|
return $this->buildUpDownPrices();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function buildUpDownPrices(): array
|
2026-01-24 14:03:11 +00:00
|
|
|
{
|
2026-06-11 20:40:47 +03:30
|
|
|
$config = app(ConfigService::class);
|
2026-07-02 01:26:26 +03:30
|
|
|
$diffBuy = $config->displayBuyPriceDiffRial();
|
|
|
|
|
$diffSell = $config->displaySellPriceDiffRial();
|
2026-06-01 18:52:59 +03:30
|
|
|
|
2026-06-11 20:40:47 +03:30
|
|
|
$extremes = $config->resolvedDailyBaseExtremes();
|
|
|
|
|
$up = $extremes['up'];
|
|
|
|
|
$down = $extremes['down'];
|
2026-06-01 18:46:57 +03:30
|
|
|
|
2026-06-11 20:40:47 +03:30
|
|
|
if ($up > 0 && $down > 0) {
|
|
|
|
|
return [
|
|
|
|
|
'sell' => max(0, (int) ceil($down - $diffSell)),
|
|
|
|
|
'buy' => (int) ceil($up + $diffBuy),
|
|
|
|
|
];
|
2026-06-01 18:46:57 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-11 20:40:47 +03:30
|
|
|
if (!$config->getBool('show_last_price', false)) {
|
|
|
|
|
return ['sell' => 0, 'buy' => 0];
|
2026-01-24 14:03:11 +00:00
|
|
|
}
|
2026-06-01 18:46:57 +03:30
|
|
|
|
2026-06-11 20:40:47 +03:30
|
|
|
$price = Price::orderByDesc('id')->first();
|
|
|
|
|
$priceValue = $price ? (float) $price->price : 0.0;
|
|
|
|
|
|
|
|
|
|
if ($priceValue <= 0) {
|
|
|
|
|
return ['sell' => 0, 'buy' => 0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'sell' => max(0, (int) ceil($priceValue - $diffSell)),
|
|
|
|
|
'buy' => (int) ceil($priceValue + $diffBuy),
|
|
|
|
|
];
|
2026-01-24 14:03:11 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-14 12:33:29 +03:30
|
|
|
protected function appendUpPrices(array $payload, bool $saveFullCache = false): array
|
2026-06-03 21:52:37 +03:30
|
|
|
{
|
2026-06-28 15:16:39 +03:30
|
|
|
// status: open | closed (خارج از ساعت کاری) | no_quote (مظنه در دسترس نیست)
|
|
|
|
|
if (!isset($payload['status'])) {
|
|
|
|
|
if (!empty($payload['closed'])) {
|
|
|
|
|
$payload['status'] = 'closed';
|
|
|
|
|
} elseif ((int) ($payload['price'] ?? 0) <= 0 && (int) ($payload['buy'] ?? 0) <= 0) {
|
|
|
|
|
$payload['status'] = 'no_quote';
|
|
|
|
|
} else {
|
|
|
|
|
$payload['status'] = 'open';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:52:37 +03:30
|
|
|
$payload['up'] = $this->buildUpDownPrices();
|
2026-06-11 21:37:17 +03:30
|
|
|
$payload['last_historical'] = app(ProductDisplayPriceService::class)->resolveHistoricalBasePrice();
|
2026-07-12 14:08:16 +03:30
|
|
|
$payload['gold_ounce'] = $this->resolveGoldOuncePayload();
|
2026-06-11 16:41:28 +03:30
|
|
|
app(ProductDisplayPriceService::class)->cacheLivePricePayload($payload);
|
2026-06-03 21:52:37 +03:30
|
|
|
|
2026-07-07 18:21:36 +03:30
|
|
|
$isFreshPrice = $saveFullCache || request()->boolean('force_refresh');
|
|
|
|
|
|
|
|
|
|
// ارسال قیمت به کانال فقط روی «لحظهٔ تغییر قیمت» = مسیر force_refresh (همان چیزی که سرورِ
|
|
|
|
|
// حساس به قیمت صدا میزند و مبنای برادکست SSE است) — نه هر pulling عادی که کش برمیگرداند.
|
|
|
|
|
// بعد از پاسخ اجرا میشود تا تاخیری برای فراخوان نباشد. در کنسول صدا نمیزنیم چون tick()
|
|
|
|
|
// خودش مستقیماً broadcastIfEligible را اجرا میکند و afterResponse در دیمن اجرا نمیشود.
|
|
|
|
|
// خودِ سرویس با price_channel_min_seconds و تشخیص تغییر قیمت گیت میکند.
|
|
|
|
|
if ($isFreshPrice && (int) ($payload['price'] ?? 0) > 0 && ! app()->runningInConsole()) {
|
|
|
|
|
dispatch(function () {
|
|
|
|
|
app(\App\Services\PriceChannelBroadcastService::class)->broadcastIfEligible();
|
|
|
|
|
})->afterResponse();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 21:23:26 +03:30
|
|
|
if ($isFreshPrice && (int) ($payload['price'] ?? 0) > 0) {
|
|
|
|
|
$pricePayload = $payload;
|
|
|
|
|
if (app()->runningInConsole()) {
|
|
|
|
|
app(\App\Services\ConditionalOrderService::class)->evaluateFromLastPrice($pricePayload);
|
2026-08-18 22:23:20 +03:30
|
|
|
app(\App\Services\PriceTriggerService::class)->evaluateActive($pricePayload);
|
2026-08-23 14:16:00 +03:30
|
|
|
try {
|
|
|
|
|
app(\App\Services\Automation\AutomationEngine::class)->evaluatePriceMove($pricePayload);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
report($e);
|
|
|
|
|
}
|
2026-07-21 21:23:26 +03:30
|
|
|
} else {
|
|
|
|
|
dispatch(function () use ($pricePayload) {
|
|
|
|
|
app(\App\Services\ConditionalOrderService::class)->evaluateFromLastPrice($pricePayload);
|
2026-08-18 22:23:20 +03:30
|
|
|
app(\App\Services\PriceTriggerService::class)->evaluateActive($pricePayload);
|
2026-08-23 14:16:00 +03:30
|
|
|
try {
|
|
|
|
|
app(\App\Services\Automation\AutomationEngine::class)->evaluatePriceMove($pricePayload);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
report($e);
|
|
|
|
|
}
|
2026-07-21 21:23:26 +03:30
|
|
|
})->afterResponse();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-07 18:21:36 +03:30
|
|
|
if ($isFreshPrice) {
|
2026-06-14 13:15:45 +03:30
|
|
|
$cache = app(LivePriceCacheService::class);
|
|
|
|
|
$cache->markTick();
|
|
|
|
|
|
2026-06-18 19:32:28 +03:30
|
|
|
return $cache->put($payload);
|
2026-06-13 17:35:40 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:52:37 +03:30
|
|
|
return $payload;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 14:08:16 +03:30
|
|
|
protected function resolveGoldOuncePayload(): ?array
|
|
|
|
|
{
|
2026-07-12 20:58:34 +03:30
|
|
|
return app(ZaarboxPriceService::class)->clientGoldOuncePayload();
|
2026-07-12 14:08:16 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-13 17:35:40 +03:30
|
|
|
public function lastPrice(Request $request)
|
2026-01-24 14:03:11 +00:00
|
|
|
{
|
2026-05-25 12:47:04 +03:30
|
|
|
$config = app(ConfigService::class);
|
2026-06-13 17:35:40 +03:30
|
|
|
$cache = app(LivePriceCacheService::class);
|
2026-06-14 12:33:29 +03:30
|
|
|
$forceRefresh = $request->boolean('force_refresh');
|
2026-06-13 17:35:40 +03:30
|
|
|
|
2026-06-14 12:33:29 +03:30
|
|
|
if (! $forceRefresh) {
|
2026-06-13 17:35:40 +03:30
|
|
|
$cached = $cache->get();
|
|
|
|
|
if ($cached !== null) {
|
2026-07-12 20:58:34 +03:30
|
|
|
$cached['gold_ounce'] = $this->resolveGoldOuncePayload();
|
|
|
|
|
|
2026-08-16 22:00:23 +03:30
|
|
|
return $this->personalizeLastPriceForCustomer($cached);
|
2026-06-13 17:35:40 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-14 13:15:45 +03:30
|
|
|
// فقط وقتی کران اخیراً tick زده — وگرنه API زنده (جلوگیری از قفل قیمت)
|
|
|
|
|
if ($cache->hasRecentTick()) {
|
|
|
|
|
$dbBuilt = $cache->buildFromDatabase();
|
|
|
|
|
if (($dbBuilt['price'] ?? 0) > 0) {
|
2026-08-16 22:00:23 +03:30
|
|
|
return $this->personalizeLastPriceForCustomer($this->appendUpPrices($dbBuilt, false));
|
2026-06-14 13:15:45 +03:30
|
|
|
}
|
2026-06-13 17:35:40 +03:30
|
|
|
}
|
|
|
|
|
}
|
2026-05-25 12:47:04 +03:30
|
|
|
|
2026-06-12 19:49:53 +03:30
|
|
|
if ($config->isStore()) {
|
2026-06-24 11:34:23 +03:30
|
|
|
$storePayload = array_merge([
|
2026-06-13 17:35:40 +03:30
|
|
|
'closed' => false,
|
|
|
|
|
'time' => now(),
|
|
|
|
|
'main_product' => 0,
|
|
|
|
|
'zaarbox' => 0,
|
2026-06-24 11:34:23 +03:30
|
|
|
], $this->storeLastPrice($config));
|
|
|
|
|
|
2026-06-24 19:13:36 +03:30
|
|
|
if ($forceRefresh) {
|
|
|
|
|
$this->recordHalfHourPriceSnapshot(
|
|
|
|
|
(int) ($storePayload['buy'] ?? 0),
|
2026-07-06 13:39:05 +03:30
|
|
|
(string) ($storePayload['price_source'] ?? 'store'),
|
2026-06-24 19:13:36 +03:30
|
|
|
null,
|
|
|
|
|
(int) ($storePayload['price'] ?? $storePayload['buy'] ?? 0)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-06-24 11:34:23 +03:30
|
|
|
|
2026-08-16 22:00:23 +03:30
|
|
|
return $this->personalizeLastPriceForCustomer($this->appendUpPrices($storePayload, $forceRefresh));
|
2026-06-12 19:49:53 +03:30
|
|
|
}
|
|
|
|
|
|
2026-05-02 18:47:02 +00:00
|
|
|
$closed = false;
|
|
|
|
|
|
2026-07-27 21:01:18 +03:30
|
|
|
$montzarLivePrice = 0;
|
2026-06-04 14:40:02 +03:30
|
|
|
$zaarbox_price = 0;
|
2026-05-24 16:55:07 +03:30
|
|
|
$pr_price = 0;
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
$montzar_open_buy = false;
|
|
|
|
|
$montzar_open_sell = false;
|
2026-05-24 16:55:07 +03:30
|
|
|
|
|
|
|
|
$priceSource = null;
|
2026-05-24 22:29:43 +03:30
|
|
|
$priceValue = 0;
|
2026-05-24 16:55:07 +03:30
|
|
|
|
|
|
|
|
$toBool = function ($value) {
|
|
|
|
|
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-14 12:33:29 +03:30
|
|
|
$zeroResponse = function () use ($forceRefresh) {
|
2026-08-16 22:00:23 +03:30
|
|
|
return $this->personalizeLastPriceForCustomer($this->appendUpPrices([
|
2026-05-25 12:47:04 +03:30
|
|
|
'sell' => 0,
|
|
|
|
|
'buy' => 0,
|
|
|
|
|
'price_last' => 0,
|
|
|
|
|
'price' => 0,
|
|
|
|
|
'main_product' => 0,
|
|
|
|
|
'montzar' => 0,
|
2026-06-04 14:40:02 +03:30
|
|
|
'zaarbox' => 0,
|
2026-05-25 12:47:04 +03:30
|
|
|
'closed' => true,
|
|
|
|
|
'time' => now(),
|
2026-08-16 22:00:23 +03:30
|
|
|
], $forceRefresh));
|
2026-05-25 12:47:04 +03:30
|
|
|
};
|
|
|
|
|
|
2026-06-04 12:18:53 +03:30
|
|
|
$priceProvider = $config->priceProvider();
|
|
|
|
|
|
2026-05-24 16:55:07 +03:30
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
2026-05-24 22:29:43 +03:30
|
|
|
| بستن قیمت طبق منطق قبلی
|
2026-05-24 16:55:07 +03:30
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
*/
|
2026-05-25 12:47:04 +03:30
|
|
|
if ($toBool($config->Config('close_price'))) {
|
2026-06-14 21:42:21 +03:30
|
|
|
if ($config->isOutsideTradingHours()) {
|
|
|
|
|
$config->syncAppClosedFlag();
|
2026-05-25 12:47:04 +03:30
|
|
|
|
2026-07-24 16:49:42 +03:30
|
|
|
// نمایش قیمت صفحه اصلی حتی وقتی فروشگاه بسته است — معامله همچنان از سمت اپ بسته میماند
|
|
|
|
|
if (! $config->shouldShowHomePriceWhenClosed()) {
|
|
|
|
|
return $zeroResponse();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Cnt::where('key', 'app_closed')->update(['value' => 'false']);
|
2026-05-02 18:47:02 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-11 17:36:23 +00:00
|
|
|
|
2026-05-24 16:55:07 +03:30
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
2026-05-24 22:29:43 +03:30
|
|
|
| Settings
|
2026-05-24 16:55:07 +03:30
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
*/
|
2026-06-08 11:01:07 +03:30
|
|
|
$show_last_price = $config->getBool('show_last_price', false);
|
2026-08-22 18:06:35 +03:30
|
|
|
// main_product_price کلید سوپرادمین است — از ConfigService خوانده شود، نه جدول لوکال
|
|
|
|
|
$main_product_price = $config->getBool('main_product_price', false);
|
2026-01-24 14:03:11 +00:00
|
|
|
|
2026-05-24 16:55:07 +03:30
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
2026-06-04 12:18:53 +03:30
|
|
|
| Montzar — فقط وقتی price_provider=montzar
|
2026-05-24 16:55:07 +03:30
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
*/
|
2026-06-04 12:18:53 +03:30
|
|
|
if ($priceProvider === 'montzar' && !$closed) {
|
2026-05-24 16:55:07 +03:30
|
|
|
try {
|
2026-05-25 12:47:04 +03:30
|
|
|
$montzarBaseUrl = rtrim($config->Config('montzar_base_url') ?: 'https://montpar.ir', '/');
|
|
|
|
|
$montzarProductId = (int) ($config->Config('montzar_product_id') ?: 25);
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
$montzar = app(MontzarService::class)->productList();
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
if (isset($montzar['message'])) {
|
|
|
|
|
throw new \Exception('Provider message: ' . $montzar['message']);
|
2026-05-24 16:55:07 +03:30
|
|
|
}
|
|
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
$collection = collect($montzar);
|
|
|
|
|
$montzarProduct = $collection->firstWhere('id', $montzarProductId);
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
if (!$montzarProduct) {
|
|
|
|
|
throw new \Exception("Provider product not found. product_id={$montzarProductId}");
|
2026-05-24 22:29:43 +03:30
|
|
|
}
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
if (!isset($montzarProduct['sub_agent'])) {
|
|
|
|
|
throw new \Exception("Provider product has no sub_agent. product_id={$montzarProductId}");
|
2026-05-24 22:29:43 +03:30
|
|
|
}
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
$buy = (float) ($montzarProduct['sub_agent']['rate_buy_gram_price'] ?? 0);
|
|
|
|
|
$sell = (float) ($montzarProduct['sub_agent']['rate_sell_gram_price'] ?? 0);
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-24 22:29:43 +03:30
|
|
|
if ($buy <= 0 || $sell <= 0) {
|
|
|
|
|
throw new \Exception("Provider invalid gram price. buy={$buy}, sell={$sell}");
|
|
|
|
|
}
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-24 22:29:43 +03:30
|
|
|
$customPrice = [];
|
|
|
|
|
$customPrice['price'] = ($buy + $sell) / 2;
|
|
|
|
|
$customPrice['diff'] = ($buy - $sell) / 2;
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 16:17:24 +03:30
|
|
|
$config->UpdateConfig('montzar_live_price', (float) $customPrice['price']);
|
2026-05-25 12:47:04 +03:30
|
|
|
$config->UpdateConfig('montzar_diff', (float) $customPrice['diff']);
|
|
|
|
|
$config->UpdateConfig('montzar_mazaneh', ((float) $customPrice['price'] * 4.331802));
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
$config->UpdateConfig(
|
|
|
|
|
'montzar_truly_buy_mazaneh',
|
|
|
|
|
$montzarProduct['sub_agent']['rate_buy_price'] ?? 0
|
|
|
|
|
);
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
$config->UpdateConfig(
|
|
|
|
|
'montzar_truly_sell_mazaneh',
|
|
|
|
|
$montzarProduct['sub_agent']['rate_sell_price'] ?? 0
|
|
|
|
|
);
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
$config->UpdateConfig(
|
|
|
|
|
'montzar_last_product_log',
|
|
|
|
|
json_encode($montzarProduct, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
|
|
|
|
);
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-06-28 11:03:54 +03:30
|
|
|
$montzarFlags = $config->montzarOpenFlags();
|
2026-06-28 10:51:56 +03:30
|
|
|
$montzar_open_buy = $montzarFlags['open_buy'];
|
|
|
|
|
$montzar_open_sell = $montzarFlags['open_sell'];
|
2026-05-24 22:29:43 +03:30
|
|
|
|
2026-07-27 21:01:18 +03:30
|
|
|
$montzarLivePrice = (int) $customPrice['price'];
|
2026-05-24 22:29:43 +03:30
|
|
|
|
2026-06-28 10:51:56 +03:30
|
|
|
$config->UpdateConfig('montzar_closed', 'false');
|
2026-05-24 22:29:43 +03:30
|
|
|
} catch (\Throwable $e) {
|
2026-07-27 21:01:18 +03:30
|
|
|
$montzarLivePrice = 0;
|
2026-05-24 16:55:07 +03:30
|
|
|
|
2026-05-25 12:47:04 +03:30
|
|
|
\Log::error('MONZAR PRICE ERROR', [
|
2026-05-24 22:29:43 +03:30
|
|
|
'message' => $e->getMessage(),
|
2026-05-25 12:47:04 +03:30
|
|
|
'montzar_product_id' => $montzarProductId ?? null,
|
|
|
|
|
'montzar_response' => $montzar ?? null,
|
2026-05-24 22:29:43 +03:30
|
|
|
]);
|
|
|
|
|
|
2026-06-28 10:46:09 +03:30
|
|
|
$config->applyMontzarSyncFailure();
|
|
|
|
|
if (!$config->getBool('montzar_closed', true)) {
|
2026-08-24 16:15:52 +03:30
|
|
|
$stale = $config->getInt('montzar_live_price', 0);
|
|
|
|
|
$montzarLivePrice = $config->isPlausibleQuotePrice((float) $stale) ? $stale : 0;
|
2026-06-28 10:46:09 +03:30
|
|
|
}
|
2026-05-24 16:55:07 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 14:40:02 +03:30
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Zaarbox — price_provider=zaarbox + settings.zaarbox_price_key
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
if ($priceProvider === 'zaarbox' && !$closed) {
|
|
|
|
|
$zaarbox = app(ZaarboxPriceService::class)->fetchPrice();
|
|
|
|
|
$zaarbox_price = (int) ($zaarbox['order_price'] ?? 0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 16:55:07 +03:30
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
2026-06-04 14:19:54 +03:30
|
|
|
| Main Product — settings.main_product سپس const
|
2026-05-25 12:47:04 +03:30
|
|
|
|--------------------------------------------------------------------------
|
2026-05-24 16:55:07 +03:30
|
|
|
*/
|
2026-06-04 14:40:02 +03:30
|
|
|
$pr_product = null;
|
2026-08-22 18:06:35 +03:30
|
|
|
if ($priceProvider === 'main_product' || $main_product_price) {
|
2026-06-04 14:40:02 +03:30
|
|
|
$pr_product = $config->mainProduct();
|
|
|
|
|
}
|
|
|
|
|
if ($pr_product && !$closed) {
|
|
|
|
|
if ($priceProvider === 'main_product') {
|
2026-06-04 14:42:22 +03:30
|
|
|
if (Product::hasStoredPriceColumns()) {
|
2026-06-05 17:43:02 +03:30
|
|
|
app(ZaarboxPriceService::class)->syncProductFromApi($pr_product->fresh());
|
2026-06-04 14:42:22 +03:30
|
|
|
$pr_product->refresh();
|
|
|
|
|
$pr_price = (float) $pr_product->price + (float) ($pr_product->price_profit ?? 0);
|
|
|
|
|
} else {
|
2026-06-05 17:43:02 +03:30
|
|
|
$live = app(ZaarboxPriceService::class)->fetchLiveOrderPrice(
|
|
|
|
|
$pr_product->reading_method
|
2026-06-04 14:42:22 +03:30
|
|
|
);
|
|
|
|
|
$pr_price = (float) ($live['order_price'] ?? 0);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$pr_price = Product::hasStoredPriceColumns()
|
|
|
|
|
? (float) $pr_product->price + (float) ($pr_product->price_profit ?? 0)
|
|
|
|
|
: 0.0;
|
2026-05-24 16:55:07 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
2026-06-04 12:18:53 +03:30
|
|
|
| انتخاب قیمت اصلی (همان price_provider)
|
2026-05-24 16:55:07 +03:30
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
*/
|
2026-06-04 14:40:02 +03:30
|
|
|
if ($priceProvider === 'zaarbox' && $zaarbox_price > 0 && !$closed) {
|
|
|
|
|
$priceValue = $zaarbox_price;
|
|
|
|
|
$priceSource = 'zaarbox';
|
2026-08-24 16:15:52 +03:30
|
|
|
} elseif (
|
|
|
|
|
$priceProvider === 'montzar'
|
|
|
|
|
&& ($montzarLivePrice ?? 0) !== 0
|
|
|
|
|
&& !$closed
|
|
|
|
|
&& $config->isPlausibleQuotePrice((float) $montzarLivePrice)
|
|
|
|
|
) {
|
2026-07-27 21:01:18 +03:30
|
|
|
$priceValue = (int) $montzarLivePrice;
|
2026-05-25 12:47:04 +03:30
|
|
|
$priceSource = 'montzar';
|
2026-06-04 14:19:54 +03:30
|
|
|
} elseif ($priceProvider === 'main_product' && ($pr_price ?? 0) !== 0 && !$closed) {
|
2026-03-31 18:28:27 +03:30
|
|
|
$priceValue = $pr_price;
|
2026-05-24 16:55:07 +03:30
|
|
|
$priceSource = 'main_product';
|
|
|
|
|
} else {
|
2026-03-31 18:28:27 +03:30
|
|
|
$priceValue = 0;
|
2026-05-24 22:29:43 +03:30
|
|
|
$priceSource = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($priceValue <= 0) {
|
2026-05-25 12:47:04 +03:30
|
|
|
$lastPrice = Price::latest()->first();
|
2026-06-08 11:01:07 +03:30
|
|
|
$storedPrice = $lastPrice ? (float) $lastPrice->price : 0.0;
|
2026-05-25 12:47:04 +03:30
|
|
|
|
2026-06-29 11:27:23 +03:30
|
|
|
// fallback فقط با show_last_price تصمیمگیری میشود؛ force_refresh (تیک کران) نباید آن را override کند.
|
|
|
|
|
// آستانهٔ ثابت قبلی (۳۰ میلیون) برای طلا بود و محصولات ارزانتر مثل کاتد مس/نقره را اشتباهاً «بدون مظنه» میکرد.
|
|
|
|
|
if ($storedPrice > 0 && $show_last_price) {
|
2026-06-13 17:35:40 +03:30
|
|
|
$priceValue = $storedPrice;
|
|
|
|
|
$priceSource = 'database_last';
|
|
|
|
|
} else {
|
2026-06-28 15:16:39 +03:30
|
|
|
// مظنه زنده نداریم و show_last_price خاموش است → نه «بسته»، بلکه «مظنه در دسترس نیست»
|
2026-08-16 22:00:23 +03:30
|
|
|
return $this->personalizeLastPriceForCustomer($this->appendUpPrices([
|
2026-05-25 12:47:04 +03:30
|
|
|
'sell' => 0,
|
|
|
|
|
'buy' => 0,
|
|
|
|
|
'price_last' => 0,
|
|
|
|
|
'price' => 0,
|
|
|
|
|
'main_product' => $pr_price ?? 0,
|
2026-07-27 21:01:18 +03:30
|
|
|
'montzar' => $montzarLivePrice ?? 0,
|
2026-06-04 14:40:02 +03:30
|
|
|
'zaarbox' => $zaarbox_price ?? 0,
|
2026-06-28 15:16:39 +03:30
|
|
|
'closed' => false,
|
|
|
|
|
'status' => 'no_quote',
|
2026-05-25 12:47:04 +03:30
|
|
|
'time' => now(),
|
2026-08-16 22:00:23 +03:30
|
|
|
], $forceRefresh));
|
2026-05-25 12:47:04 +03:30
|
|
|
}
|
2026-03-10 15:39:30 +03:30
|
|
|
}
|
2026-05-24 16:55:07 +03:30
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
2026-05-25 12:47:04 +03:30
|
|
|
| محاسبه sell / buy
|
2026-05-24 16:55:07 +03:30
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
*/
|
2026-06-14 12:33:29 +03:30
|
|
|
$display = $config->buildTradeDisplayPrices(
|
|
|
|
|
(float) $priceValue,
|
|
|
|
|
$priceSource,
|
|
|
|
|
[
|
|
|
|
|
'montzar_open_buy' => $montzar_open_buy,
|
|
|
|
|
'montzar_open_sell' => $montzar_open_sell,
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
$sell = $display['sell'];
|
|
|
|
|
$buy = $display['buy'];
|
|
|
|
|
$priceLast = $display['priceLast'];
|
2026-06-04 13:48:37 +03:30
|
|
|
|
2026-06-01 18:46:57 +03:30
|
|
|
if (!$closed && $priceValue > 0) {
|
|
|
|
|
$config->trackDailyPriceExtreme((float) $priceValue);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 19:13:36 +03:30
|
|
|
if ($forceRefresh) {
|
|
|
|
|
$this->recordHalfHourPriceSnapshot(
|
|
|
|
|
(int) $buy,
|
|
|
|
|
(string) ($priceSource ?: ($priceProvider ?: 'legacy')),
|
|
|
|
|
$this->resolvePriceSourceUrl($priceSource, $config),
|
|
|
|
|
(int) $priceValue
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-06-24 11:34:23 +03:30
|
|
|
|
2026-08-16 22:00:23 +03:30
|
|
|
return $this->personalizeLastPriceForCustomer($this->appendUpPrices([
|
2026-05-24 16:55:07 +03:30
|
|
|
'sell' => $sell,
|
|
|
|
|
'buy' => $buy,
|
|
|
|
|
'price_last' => $priceLast,
|
|
|
|
|
'price' => $priceValue,
|
2026-07-06 13:39:05 +03:30
|
|
|
'price_source' => $priceSource,
|
2026-05-24 16:55:07 +03:30
|
|
|
'main_product' => $pr_price ?? 0,
|
2026-07-27 21:01:18 +03:30
|
|
|
'montzar' => $montzarLivePrice ?? 0,
|
2026-06-04 14:40:02 +03:30
|
|
|
'zaarbox' => $zaarbox_price ?? 0,
|
2026-05-24 22:29:43 +03:30
|
|
|
'closed' => false,
|
2026-05-24 16:55:07 +03:30
|
|
|
'time' => now(),
|
2026-08-16 22:00:23 +03:30
|
|
|
], $forceRefresh));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** اختلاف پکیج ویژه — فقط روی پاسخ مشتری؛ کش مشترک را آلوده نمیکند */
|
|
|
|
|
protected function personalizeLastPriceForCustomer(array $payload): array
|
|
|
|
|
{
|
|
|
|
|
$customerId = (int) (auth('customer')->id() ?? 0);
|
|
|
|
|
if ($customerId <= 0) {
|
|
|
|
|
return $payload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return app(\App\Services\CustomerSpecialPricingService::class)
|
|
|
|
|
->applyToLastPayload($payload, $customerId);
|
2026-01-24 14:03:11 +00:00
|
|
|
}
|
|
|
|
|
|
2026-06-24 19:13:36 +03:30
|
|
|
protected function recordHalfHourPriceSnapshot(
|
|
|
|
|
int $buyPrice,
|
|
|
|
|
string $source,
|
|
|
|
|
?string $sourceUrl,
|
|
|
|
|
int $referencePrice
|
|
|
|
|
): void {
|
2026-07-06 13:39:05 +03:30
|
|
|
if ($buyPrice <= 0 && $referencePrice <= 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// فقط fallback دیتابیس — مظنهٔ زنده نیست؛ لاگ/رکورد جدید نساز (همان قیمت دیشب تکرار میشود).
|
|
|
|
|
if ($source === 'database_last') {
|
2026-06-24 19:13:36 +03:30
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$reference = $referencePrice > 0 ? $referencePrice : $buyPrice;
|
|
|
|
|
|
|
|
|
|
app(PriceSyncLogService::class)->recordSnapshotIfDue(
|
|
|
|
|
$source,
|
|
|
|
|
$sourceUrl,
|
|
|
|
|
$reference,
|
|
|
|
|
PriceSnapshotService::HALF_HOUR_MINUTES
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
app(PriceSnapshotService::class)->recordHalfHourBuyPrice($buyPrice);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected function resolvePriceSourceUrl(?string $priceSource, ConfigService $config): ?string
|
|
|
|
|
{
|
|
|
|
|
if ($priceSource === 'zaarbox') {
|
|
|
|
|
return app(ZaarboxPriceService::class)->apiUrl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($priceSource === 'montzar') {
|
|
|
|
|
return rtrim($config->Config('montzar_base_url') ?: 'https://montpar.ir', '/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 19:49:53 +03:30
|
|
|
/**
|
2026-06-30 22:17:09 +03:30
|
|
|
* فروشگاه — منطق lastPrice بر اساس price_provider
|
2026-06-12 19:49:53 +03:30
|
|
|
*/
|
|
|
|
|
protected function storeLastPrice(ConfigService $config): array
|
|
|
|
|
{
|
|
|
|
|
$talaIr = 0;
|
|
|
|
|
$montzar = 0;
|
|
|
|
|
|
2026-06-30 22:17:09 +03:30
|
|
|
$montzarEnabled = $config->priceProvider() === 'montzar';
|
2026-06-12 19:49:53 +03:30
|
|
|
|
|
|
|
|
if ($montzarEnabled) {
|
|
|
|
|
try {
|
|
|
|
|
$montzarProductId = (int) ($config->Config('montzar_product_id') ?: 25);
|
|
|
|
|
$montzarProducts = app(MontzarService::class)->productList();
|
|
|
|
|
|
|
|
|
|
if (isset($montzarProducts['message'])) {
|
|
|
|
|
throw new \Exception('Montzar message: ' . $montzarProducts['message']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$montzarProduct = collect($montzarProducts)->firstWhere('id', $montzarProductId);
|
|
|
|
|
if (! $montzarProduct || ! isset($montzarProduct['sub_agent'])) {
|
|
|
|
|
throw new \Exception("Montzar product not found. product_id={$montzarProductId}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$buy = (float) ($montzarProduct['sub_agent']['rate_buy_gram_price'] ?? 0);
|
|
|
|
|
$sell = (float) ($montzarProduct['sub_agent']['rate_sell_gram_price'] ?? 0);
|
|
|
|
|
if ($buy <= 0 || $sell <= 0) {
|
|
|
|
|
throw new \Exception("Montzar invalid gram price. buy={$buy}, sell={$sell}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$customPrice = [
|
|
|
|
|
'price' => ($buy + $sell) / 2,
|
|
|
|
|
'diff' => ($buy - $sell) / 2,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$config->UpdateConfig('montzar_live_price', (float) $customPrice['price']);
|
|
|
|
|
$config->UpdateConfig('montzar_diff', (float) $customPrice['diff']);
|
|
|
|
|
$config->UpdateConfig('montzar_mazaneh', ((float) $customPrice['price'] * 4.331802));
|
|
|
|
|
$config->UpdateConfig('montzar_truly_buy_mazaneh', $montzarProduct['sub_agent']['rate_buy_price'] ?? 0);
|
|
|
|
|
$config->UpdateConfig('montzar_truly_sell_mazaneh', $montzarProduct['sub_agent']['rate_sell_price'] ?? 0);
|
|
|
|
|
$config->UpdateConfig(
|
|
|
|
|
'montzar_last_product_log',
|
|
|
|
|
json_encode($montzarProduct, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-28 11:03:54 +03:30
|
|
|
$montzarFlags = $config->montzarOpenFlags();
|
2026-06-28 10:51:56 +03:30
|
|
|
$openBuy = $montzarFlags['open_buy'];
|
|
|
|
|
$openSell = $montzarFlags['open_sell'];
|
2026-06-12 19:49:53 +03:30
|
|
|
|
2026-06-28 10:51:56 +03:30
|
|
|
$montzar = (int) $customPrice['price'];
|
2026-06-12 19:49:53 +03:30
|
|
|
|
2026-06-28 10:51:56 +03:30
|
|
|
$config->UpdateConfig('montzar_closed', 'false');
|
2026-06-12 19:49:53 +03:30
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$montzar = 0;
|
2026-06-28 10:46:09 +03:30
|
|
|
$config->applyMontzarSyncFailure();
|
2026-06-28 10:51:56 +03:30
|
|
|
if (!$config->getBool('montzar_closed', true)) {
|
2026-08-24 16:15:52 +03:30
|
|
|
$stale = $config->getInt('montzar_live_price', 0);
|
|
|
|
|
$montzar = $config->isPlausibleQuotePrice((float) $stale) ? $stale : 0;
|
2026-06-28 10:51:56 +03:30
|
|
|
}
|
2026-06-12 19:49:53 +03:30
|
|
|
\Log::error('STORE MONZAR PRICE ERROR', ['message' => $e->getMessage()]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 16:15:52 +03:30
|
|
|
if ($montzarEnabled && $montzar !== 0 && $config->isPlausibleQuotePrice((float) $montzar)) {
|
2026-06-12 19:49:53 +03:30
|
|
|
$priceValue = $montzar;
|
|
|
|
|
$priceSource = 'montzar';
|
|
|
|
|
} else {
|
|
|
|
|
$priceValue = $talaIr;
|
|
|
|
|
$priceSource = 'tala_ir';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$lastStored = Price::latest()->first();
|
2026-06-24 11:34:23 +03:30
|
|
|
if ($priceValue <= 0 && $lastStored && (float) $lastStored->price > 0) {
|
|
|
|
|
$priceValue = (float) $lastStored->price;
|
|
|
|
|
$priceSource = 'database_last';
|
2026-06-12 19:49:53 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-24 11:34:23 +03:30
|
|
|
if ($priceValue <= 0) {
|
2026-06-12 19:49:53 +03:30
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 11:34:23 +03:30
|
|
|
$display = $config->buildTradeDisplayPrices(
|
|
|
|
|
(float) $priceValue,
|
|
|
|
|
$priceSource
|
|
|
|
|
);
|
2026-06-12 19:49:53 +03:30
|
|
|
$response = [
|
2026-06-24 11:34:23 +03:30
|
|
|
'sell' => $display['sell'],
|
|
|
|
|
'buy' => $display['buy'],
|
|
|
|
|
'price_last' => $display['priceLast'],
|
2026-06-12 19:49:53 +03:30
|
|
|
'price' => $priceValue,
|
2026-07-06 13:39:05 +03:30
|
|
|
'price_source' => $priceSource,
|
2026-06-12 19:49:53 +03:30
|
|
|
'tala_ir' => $talaIr,
|
|
|
|
|
];
|
|
|
|
|
|
2026-06-30 22:17:09 +03:30
|
|
|
if ($montzarEnabled) {
|
2026-06-12 19:49:53 +03:30
|
|
|
$response['montzar'] = $montzar;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 14:03:11 +00:00
|
|
|
public function check()
|
|
|
|
|
{
|
|
|
|
|
$service = Http::
|
|
|
|
|
withHeaders([
|
|
|
|
|
'accept' => 'application/json',
|
|
|
|
|
'Authorization' => 'Bearer ohxsrbjagqvbt0glaygt'
|
|
|
|
|
])->get('https://studio.persianapi.com/index.php/web-service/common/gold-currency-coin?format=json&limit=30&page=1')
|
|
|
|
|
->json();
|
|
|
|
|
$list = [];
|
|
|
|
|
if (isset($service['result'])) {
|
|
|
|
|
foreach ($service['result'] as $item) {
|
|
|
|
|
if ($item['key'] == 137120 || $item['key'] == 137119) {
|
|
|
|
|
$list[] = $item;
|
|
|
|
|
if ($item['key'] == 137120) {
|
|
|
|
|
(new PriceController())->store(new Request(["price" => $item['price']]));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Cnt::where('key', 'price')->update(['value_json' => $list]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function test()
|
|
|
|
|
{
|
|
|
|
|
$a = '{{12, 23, {4, 0, {1}, {1}}}, 0, {1}}';
|
|
|
|
|
$b = str_split($a);
|
|
|
|
|
$c = [];
|
|
|
|
|
$str = "";
|
|
|
|
|
foreach ($b as $item) {
|
|
|
|
|
if ($item != " ") {
|
|
|
|
|
print_r($item);
|
|
|
|
|
print_r('++');
|
|
|
|
|
if ($item == ',' || $item == '}') {
|
|
|
|
|
if (!in_array($c[count($c) - 1], ['{', '}'])) {
|
|
|
|
|
$c[count($c) - 1] += (int)$str;
|
|
|
|
|
print_r((int)$str);
|
|
|
|
|
print_r('^^');
|
|
|
|
|
} else {
|
|
|
|
|
$c[] = $str;
|
|
|
|
|
print_r($c);
|
|
|
|
|
print_r('&&');
|
|
|
|
|
}
|
|
|
|
|
$str = "";
|
|
|
|
|
} elseif (in_array($item, ['{', '}'])) {
|
|
|
|
|
$c[] = $item;
|
|
|
|
|
print_r($c);
|
|
|
|
|
print_r('**');
|
|
|
|
|
} else {
|
|
|
|
|
$str .= $item;
|
|
|
|
|
print_r($str);
|
|
|
|
|
print_r('@@');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
dd($c);
|
|
|
|
|
$d = [];
|
|
|
|
|
$e = [];
|
|
|
|
|
foreach ($c as $item) {
|
|
|
|
|
if ($item == '}') {
|
|
|
|
|
print_r($d[count($d) - 1]);
|
|
|
|
|
print_r('***');
|
|
|
|
|
$e[] = $d[count($d) - 1];
|
|
|
|
|
} else {
|
|
|
|
|
$d[] = $item;
|
|
|
|
|
print_r($d);
|
|
|
|
|
print_r('-----');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
dd($e);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|