Stored DB prices were treated as live quotes, and SuperAdmin overlay overwrote tenant keys like trading hours. Keep overlay to the 14 access flags and hide fallback prices when the panel says so. Co-authored-by: Cursor <cursoragent@cursor.com>
177 lines
5.7 KiB
PHP
177 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Price;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
class LivePriceCacheService
|
|
{
|
|
public const FULL_RESPONSE_KEY = 'live_price_full_response';
|
|
|
|
public const LAST_TICK_KEY = 'live_price_last_tick_at';
|
|
|
|
/** همتراز با کران ۱۰ ثانیهای */
|
|
public const TTL_SECONDS = 10;
|
|
|
|
/** اگر tick اخیر نباشد، buildFromDatabase قیمت را قفل میکند */
|
|
public const TICK_MAX_AGE_SECONDS = 20;
|
|
|
|
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
|
|
{
|
|
$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;
|
|
}
|
|
|
|
$this->storeBody($body);
|
|
|
|
app(PriceBroadcastService::class)->broadcastLastIfChanged($body);
|
|
|
|
app(PriceChannelBroadcastService::class)->scheduleBroadcastIfEligible();
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* بازنویسی کش 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);
|
|
}
|
|
|
|
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) */
|
|
public function buildFromDatabase(): array
|
|
{
|
|
$config = app(ConfigService::class);
|
|
$display = app(ProductDisplayPriceService::class);
|
|
$last = Price::orderByDesc('id')->first();
|
|
$base = $last ? (float) $last->price : 0.0;
|
|
$provider = $config->priceProvider();
|
|
|
|
if ($base <= 0) {
|
|
$payload = [
|
|
'sell' => 0,
|
|
'buy' => 0,
|
|
'price_last' => 0,
|
|
'price' => 0,
|
|
'main_product' => 0,
|
|
'montzar' => 0,
|
|
'zaarbox' => 0,
|
|
'closed' => false,
|
|
'status' => 'no_quote',
|
|
'price_source' => '',
|
|
'time' => now(),
|
|
];
|
|
} else {
|
|
$montzarSource = 0;
|
|
$zaarboxSource = 0;
|
|
$mainProductSource = 0;
|
|
$priceSource = 'database_last';
|
|
|
|
if ($provider === 'montzar') {
|
|
$liveMontzar = (float) $config->get('montzar_live_price', 0);
|
|
if ($liveMontzar > 0 && $config->isPlausibleQuotePrice($liveMontzar)) {
|
|
$base = $liveMontzar;
|
|
$montzarSource = (int) $liveMontzar;
|
|
$priceSource = 'montzar';
|
|
} else {
|
|
$montzarSource = 0;
|
|
$priceSource = 'database_last';
|
|
}
|
|
} elseif ($provider === 'zaarbox') {
|
|
$zaarboxSource = (int) $base;
|
|
} elseif ($provider === 'main_product') {
|
|
$mainProductSource = (int) $base;
|
|
}
|
|
|
|
$trade = $config->buildTradeDisplayPrices($base, $priceSource);
|
|
|
|
$payload = [
|
|
'sell' => $trade['sell'],
|
|
'buy' => $trade['buy'],
|
|
'price_last' => $trade['priceLast'],
|
|
'price' => (int) $base,
|
|
'main_product' => $mainProductSource,
|
|
'montzar' => $montzarSource,
|
|
'zaarbox' => $zaarboxSource,
|
|
'closed' => false,
|
|
'status' => 'open',
|
|
'price_source' => $priceSource,
|
|
'time' => now(),
|
|
];
|
|
}
|
|
|
|
$payload['last_historical'] = $display->resolveHistoricalBasePrice();
|
|
|
|
$payload = $config->applyGlobalTradingHoursToPayload($payload);
|
|
|
|
$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]);
|
|
|
|
return app(ZaarboxPriceService::class)->attachGoldOunceToPayload($payload);
|
|
}
|
|
}
|