fix(settings): honor last-price switch and overlay only SuperAdmin access flags

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>
This commit is contained in:
2026-08-25 03:01:07 +03:30
co-authored by Cursor
parent 8984899042
commit b21b136711
9 changed files with 327 additions and 97 deletions
+86 -22
View File
@@ -25,6 +25,12 @@ class ConfigService
'trading_hours_by_day',
];
/**
* فقط این منابع «آخرین قیمت ذخیره‌شده» هستند؛ باقی price_source (montzar، zaarbox،
* main_product، tala_ir و ...) مظنهٔ زنده حساب می‌شود.
*/
public const STORED_PRICE_SOURCES = ['database_last'];
/**
* حداکثر انحراف مجاز مظنه نسبت به آخرین قیمت معتبر DB.
* پرش اشتباه provider (مثل ۱۶۸M ماندهٔ montzar روی مظنهٔ ~۲۲۰M) را رد می‌کند.
@@ -64,14 +70,15 @@ class ConfigService
}
/**
* مقدار setting؛ اول سوپرادمین، بعد DB لوکال، بعد price_providers.php.
* برای کلیدهای can_edit=0 همیشه از این مسیر (یا ConfigService) بخوانید — نه Setting::where مستقیم.
* مقدار setting: فقط دسترسی‌های سوپرادمین از overlay؛ بقیه از DB لوکال / فایل.
*/
public function get(string $key, $default = null)
{
$fromSa = $this->valueFromSuperAdmin($key, $default);
if ($fromSa !== '__sa_miss__') {
return $fromSa;
if (SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) {
$fromSa = $this->valueFromSuperAdmin($key, $default);
if ($fromSa !== '__sa_miss__') {
return $fromSa;
}
}
try {
@@ -784,6 +791,46 @@ class ConfigService
|| (int) ($payload['price'] ?? 0) > 0;
}
/**
* مظنهٔ زنده داریم یا قیمت از آخرین ردیف prices آمده؟
* بدون این تفکیک، fallback دیتابیس مثل مظنهٔ زنده دیده می‌شود و
* «نمایش آخرین قیمت ذخیره‌شده» خاموش هم قیمت نشان می‌دهد.
*
* @param array<string, mixed> $payload
*/
public function payloadHasLiveQuote(array $payload): bool
{
if (! $this->payloadHasDisplayablePrice($payload)) {
return false;
}
$source = strtolower(trim((string) ($payload['price_source'] ?? '')));
if ($source !== '') {
return ! in_array($source, self::STORED_PRICE_SOURCES, true);
}
// بدون برچسب منبع، فقط عددِ مرجع زنده قابل اتکاست
return (int) ($payload['montzar'] ?? 0) > 0
|| (int) ($payload['zaarbox'] ?? 0) > 0
|| (int) ($payload['main_product'] ?? 0) > 0;
}
/**
* قیمت‌ها را صفر می‌کند تا هیچ کلاینتی مقدار مانده را نشان ندهد.
*
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
public function clearPayloadDisplayPrices(array $payload): array
{
$payload['sell'] = 0;
$payload['buy'] = 0;
$payload['price_last'] = 0;
$payload['price'] = 0;
return $payload;
}
/**
* آخرین قیمت جدول prices را روی payload می‌گذارد — وقتی مظنه زنده نیست و show_last_price روشن است.
*
@@ -807,7 +854,8 @@ class ConfigService
$payload['buy'] = $trade['buy'];
$payload['price_last'] = $trade['priceLast'];
$payload['price'] = (int) $priceValue;
$payload['price_source'] = $payload['price_source'] ?? 'database_last';
// قیمت از DB آمد — برچسب منبع قبلی باید پاک شود وگرنه دفعهٔ بعد «زنده» حساب می‌شود
$payload['price_source'] = 'database_last';
return $payload;
}
@@ -823,29 +871,39 @@ class ConfigService
*/
public function applyHomePriceDisplayState(array $payload, ?Carbon $now = null): array
{
$outside = $this->isOutsideTradingHours($now);
$showWhenClosed = $this->shouldShowHomePriceWhenClosed();
$showLast = $this->shouldShowLastSavedPrice();
if ($outside) {
if ($this->isOutsideTradingHours($now)) {
$payload['closed'] = true;
if ($showWhenClosed) {
$payload = $this->fillPayloadFromLastStoredPrice($payload);
$payload['status'] = $this->payloadHasDisplayablePrice($payload) ? 'open' : 'no_quote';
if (! $this->shouldShowHomePriceWhenClosed()) {
$payload = $this->clearPayloadDisplayPrices($payload);
$payload['status'] = 'closed';
return $payload;
}
$payload['status'] = 'closed';
$payload = $this->fillPayloadFromLastStoredPrice($payload);
$payload['status'] = $this->payloadHasDisplayablePrice($payload) ? 'open' : 'no_quote';
return $payload;
}
$payload['closed'] = false;
if ($showLast) {
$payload = $this->fillPayloadFromLastStoredPrice($payload);
if ($this->payloadHasLiveQuote($payload)) {
$payload['status'] = 'open';
return $payload;
}
$payload['status'] = $this->payloadHasDisplayablePrice($payload) ? 'open' : 'no_quote';
if ($this->shouldShowLastSavedPrice()) {
$payload = $this->fillPayloadFromLastStoredPrice($payload);
$payload['status'] = $this->payloadHasDisplayablePrice($payload) ? 'open' : 'no_quote';
return $payload;
}
$payload = $this->clearPayloadDisplayPrices($payload);
$payload['status'] = 'no_quote';
return $payload;
}
@@ -911,16 +969,22 @@ class ConfigService
$patch = $this->clientTradingHoursSettings();
$cache = app(LivePriceCacheService::class);
$payload = $cache->get() ?? $cache->buildFromDatabase();
// کش ممکن است قیمت صفرشدهٔ حالت قبل باشد؛ فقط وقتی مظنهٔ زنده دارد قابل استفاده است
$cached = $cache->get();
$payload = is_array($cached) && $this->payloadHasLiveQuote($cached)
? $cached
: $cache->buildFromDatabase();
$payload = $this->applyHomePriceDisplayState($payload);
$payload = app(ZaarboxPriceService::class)->attachGoldOunceToPayload($payload);
$payload['settings_patch'] = $patch;
// settings_patch وارد کش نمی‌شود؛ رویداد settings مقدار تازه را می‌برد
unset($payload['settings_patch']);
$cache->replaceBody($payload);
$stream = app(PriceStreamService::class);
if ($patch !== []) {
$stream->publish('settings', $patch);
app(PriceStreamService::class)->publish('settings', $patch);
}
app(PriceBroadcastService::class)->broadcastAfterPriceUpdate($payload, true);