2026-06-13 17:35:40 +03:30
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Models\Price;
|
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
|
|
|
|
|
|
class LivePriceCacheService
|
|
|
|
|
{
|
|
|
|
|
public const FULL_RESPONSE_KEY = 'live_price_full_response';
|
|
|
|
|
|
2026-06-14 13:15:45 +03:30
|
|
|
public const LAST_TICK_KEY = 'live_price_last_tick_at';
|
|
|
|
|
|
|
|
|
|
/** همتراز با کران ۱۰ ثانیهای */
|
|
|
|
|
public const TTL_SECONDS = 10;
|
|
|
|
|
|
|
|
|
|
/** اگر tick اخیر نباشد، buildFromDatabase قیمت را قفل میکند */
|
|
|
|
|
public const TICK_MAX_AGE_SECONDS = 20;
|
2026-06-13 17:35:40 +03:30
|
|
|
|
|
|
|
|
public function get(): ?array
|
|
|
|
|
{
|
|
|
|
|
$cached = Cache::get(self::FULL_RESPONSE_KEY);
|
|
|
|
|
if (! is_array($cached) || ! isset($cached['body'], $cached['stored_at'])) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (now()->diffInSeconds($cached['stored_at']) > self::TTL_SECONDS) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $cached['body'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function put(array $body): array
|
|
|
|
|
{
|
2026-06-18 19:44:23 +03:30
|
|
|
$cached = Cache::get(self::FULL_RESPONSE_KEY);
|
|
|
|
|
$cachedBody = is_array($cached['body'] ?? null) ? $cached['body'] : null;
|
|
|
|
|
|
|
|
|
|
if ($cachedBody !== null && PriceBroadcastService::lastPriceIdentity($cachedBody) === PriceBroadcastService::lastPriceIdentity($body)) {
|
|
|
|
|
Cache::put(self::FULL_RESPONSE_KEY, [
|
|
|
|
|
'body' => $cachedBody,
|
|
|
|
|
'stored_at' => now(),
|
|
|
|
|
], now()->addSeconds(self::TTL_SECONDS + 5));
|
|
|
|
|
|
|
|
|
|
return $cachedBody;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 02:29:00 +03:30
|
|
|
$this->storeBody($body);
|
2026-06-13 17:35:40 +03:30
|
|
|
|
2026-06-18 19:32:28 +03:30
|
|
|
app(PriceBroadcastService::class)->broadcastLastIfChanged($body);
|
|
|
|
|
|
2026-07-08 16:42:52 +03:30
|
|
|
app(PriceChannelBroadcastService::class)->scheduleBroadcastIfEligible();
|
|
|
|
|
|
2026-06-13 17:35:40 +03:30
|
|
|
return $body;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 02:29:00 +03:30
|
|
|
/**
|
|
|
|
|
* بازنویسی کش last بدون گیت هویت — برای اعمال فوری ساعت کاری.
|
|
|
|
|
*
|
|
|
|
|
* @param array<string, mixed> $body
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
public function replaceBody(array $body): array
|
|
|
|
|
{
|
|
|
|
|
$this->storeBody($body);
|
|
|
|
|
|
|
|
|
|
return $body;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string, mixed> $body
|
|
|
|
|
*/
|
|
|
|
|
private function storeBody(array $body): void
|
|
|
|
|
{
|
|
|
|
|
Cache::put(self::FULL_RESPONSE_KEY, [
|
|
|
|
|
'body' => $body,
|
|
|
|
|
'stored_at' => now(),
|
|
|
|
|
], now()->addSeconds(self::TTL_SECONDS + 5));
|
|
|
|
|
|
|
|
|
|
app(ProductDisplayPriceService::class)->cacheLivePricePayload($body);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-14 13:15:45 +03:30
|
|
|
public function markTick(): void
|
|
|
|
|
{
|
|
|
|
|
Cache::put(self::LAST_TICK_KEY, now(), now()->addMinutes(10));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function hasRecentTick(): bool
|
|
|
|
|
{
|
|
|
|
|
$lastTick = Cache::get(self::LAST_TICK_KEY);
|
|
|
|
|
if ($lastTick === null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return now()->diffInSeconds($lastTick) <= self::TICK_MAX_AGE_SECONDS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** پاسخ سریع بدون API خارجی — فقط وقتی کران اخیراً tick زده (همان فرمول lastPrice) */
|
2026-06-13 17:35:40 +03:30
|
|
|
public function buildFromDatabase(): array
|
|
|
|
|
{
|
|
|
|
|
$config = app(ConfigService::class);
|
|
|
|
|
$display = app(ProductDisplayPriceService::class);
|
|
|
|
|
$last = Price::orderByDesc('id')->first();
|
2026-06-14 12:33:29 +03:30
|
|
|
$base = $last ? (float) $last->price : 0.0;
|
2026-06-14 12:01:20 +03:30
|
|
|
$provider = $config->priceProvider();
|
|
|
|
|
|
2026-06-13 17:35:40 +03:30
|
|
|
if ($base <= 0) {
|
|
|
|
|
$payload = [
|
|
|
|
|
'sell' => 0,
|
|
|
|
|
'buy' => 0,
|
|
|
|
|
'price_last' => 0,
|
|
|
|
|
'price' => 0,
|
|
|
|
|
'main_product' => 0,
|
|
|
|
|
'montzar' => 0,
|
|
|
|
|
'zaarbox' => 0,
|
2026-06-28 15:16:39 +03:30
|
|
|
'closed' => false,
|
|
|
|
|
'status' => 'no_quote',
|
2026-08-25 03:01:07 +03:30
|
|
|
'price_source' => '',
|
2026-06-13 17:35:40 +03:30
|
|
|
'time' => now(),
|
|
|
|
|
];
|
|
|
|
|
} else {
|
2026-06-14 12:33:29 +03:30
|
|
|
$montzarSource = 0;
|
|
|
|
|
$zaarboxSource = 0;
|
|
|
|
|
$mainProductSource = 0;
|
|
|
|
|
$priceSource = 'database_last';
|
2026-06-14 12:01:20 +03:30
|
|
|
|
|
|
|
|
if ($provider === 'montzar') {
|
2026-06-14 12:33:29 +03:30
|
|
|
$liveMontzar = (float) $config->get('montzar_live_price', 0);
|
2026-08-24 16:15:52 +03:30
|
|
|
if ($liveMontzar > 0 && $config->isPlausibleQuotePrice($liveMontzar)) {
|
2026-06-14 13:15:45 +03:30
|
|
|
$base = $liveMontzar;
|
2026-08-24 16:15:52 +03:30
|
|
|
$montzarSource = (int) $liveMontzar;
|
|
|
|
|
$priceSource = 'montzar';
|
|
|
|
|
} else {
|
|
|
|
|
$montzarSource = 0;
|
|
|
|
|
$priceSource = 'database_last';
|
2026-06-14 13:15:45 +03:30
|
|
|
}
|
2026-06-14 12:01:20 +03:30
|
|
|
} elseif ($provider === 'zaarbox') {
|
2026-06-14 12:33:29 +03:30
|
|
|
$zaarboxSource = (int) $base;
|
2026-06-14 12:01:20 +03:30
|
|
|
} elseif ($provider === 'main_product') {
|
2026-06-14 12:33:29 +03:30
|
|
|
$mainProductSource = (int) $base;
|
2026-06-14 12:01:20 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-14 12:33:29 +03:30
|
|
|
$trade = $config->buildTradeDisplayPrices($base, $priceSource);
|
2026-06-13 17:35:40 +03:30
|
|
|
|
|
|
|
|
$payload = [
|
2026-06-14 12:33:29 +03:30
|
|
|
'sell' => $trade['sell'],
|
|
|
|
|
'buy' => $trade['buy'],
|
|
|
|
|
'price_last' => $trade['priceLast'],
|
|
|
|
|
'price' => (int) $base,
|
|
|
|
|
'main_product' => $mainProductSource,
|
|
|
|
|
'montzar' => $montzarSource,
|
|
|
|
|
'zaarbox' => $zaarboxSource,
|
2026-06-13 17:35:40 +03:30
|
|
|
'closed' => false,
|
2026-06-28 15:16:39 +03:30
|
|
|
'status' => 'open',
|
2026-08-25 03:01:07 +03:30
|
|
|
'price_source' => $priceSource,
|
2026-06-13 17:35:40 +03:30
|
|
|
'time' => now(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$payload['last_historical'] = $display->resolveHistoricalBasePrice();
|
|
|
|
|
|
2026-08-24 21:22:44 +03:30
|
|
|
$payload = $config->applyGlobalTradingHoursToPayload($payload);
|
|
|
|
|
|
2026-08-25 03:01:07 +03:30
|
|
|
$extremes = $config->resolvedDailyBaseExtremes();
|
|
|
|
|
$payload['up'] = ($payload['status'] ?? 'open') !== 'open'
|
|
|
|
|
? ['sell' => 0, 'buy' => 0]
|
|
|
|
|
: ($extremes['up'] > 0
|
|
|
|
|
? [
|
|
|
|
|
'sell' => max(0, (int) ceil($extremes['down'] - $config->displaySellPriceDiffRial())),
|
|
|
|
|
'buy' => (int) ceil($extremes['up'] + $config->displayBuyPriceDiffRial()),
|
|
|
|
|
]
|
|
|
|
|
: ['sell' => $payload['sell'] ?? 0, 'buy' => $payload['buy'] ?? 0]);
|
|
|
|
|
|
2026-07-12 20:58:34 +03:30
|
|
|
return app(ZaarboxPriceService::class)->attachGoldOunceToPayload($payload);
|
2026-06-13 17:35:40 +03:30
|
|
|
}
|
|
|
|
|
}
|