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:
@@ -3,7 +3,6 @@
|
|||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use App\Services\SuperAdminProgramSettingsClient;
|
|
||||||
use App\Services\SuperAdminProgramSettingsOverlay;
|
use App\Services\SuperAdminProgramSettingsOverlay;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -16,7 +15,8 @@ use Illuminate\Support\Facades\Schema;
|
|||||||
* قواعد سخت:
|
* قواعد سخت:
|
||||||
* 1) value غیرخالی لوکال هرگز عوض نمیشود (مگر --force-overwrite).
|
* 1) value غیرخالی لوکال هرگز عوض نمیشود (مگر --force-overwrite).
|
||||||
* 2) پیشفرضهای seed/deploy (مثل limangold.ir) هرگز بهعنوان مقدار واقعی نوشته نمیشوند.
|
* 2) پیشفرضهای seed/deploy (مثل limangold.ir) هرگز بهعنوان مقدار واقعی نوشته نمیشوند.
|
||||||
* 3) با --fill-from-superadmin برای خالی/غایب: اول SA واقعی، بعد بکاپ واقعی.
|
* 3) --fill-from-superadmin دیگر کلیدهای غیر دسترسی را از سوپرادمین نمیخواند
|
||||||
|
* (سوپرادمین فقط ۱۴ دسترسی را مالک است).
|
||||||
*
|
*
|
||||||
* کلیدهای گروه «دسترسی مشتری به قابلیتها» و runtime رد میشوند.
|
* کلیدهای گروه «دسترسی مشتری به قابلیتها» و runtime رد میشوند.
|
||||||
*/
|
*/
|
||||||
@@ -78,7 +78,10 @@ class SettingsRestoreFromBackupCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
$backupRows = $this->readBackup($table);
|
$backupRows = $this->readBackup($table);
|
||||||
$superAdminValues = $fillFromSuperAdmin ? $this->superAdminValues() : [];
|
$superAdminValues = [];
|
||||||
|
if ($fillFromSuperAdmin) {
|
||||||
|
$this->warn('--fill-from-superadmin نادیده گرفته شد: سوپرادمین فقط دسترسیها را دارد، نه بقیهٔ تنظیمات.');
|
||||||
|
}
|
||||||
|
|
||||||
if ($backupRows === [] && $superAdminValues === []) {
|
if ($backupRows === [] && $superAdminValues === []) {
|
||||||
$this->warn('هیچ کلید قابل بازگردانی پیدا نشد.');
|
$this->warn('هیچ کلید قابل بازگردانی پیدا نشد.');
|
||||||
@@ -332,37 +335,6 @@ class SettingsRestoreFromBackupCommand extends Command
|
|||||||
return $rows;
|
return $rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
private function superAdminValues(): array
|
|
||||||
{
|
|
||||||
$items = app(SuperAdminProgramSettingsClient::class)->fetchItems();
|
|
||||||
if (! is_array($items)) {
|
|
||||||
$this->warn('سوپرادمین در دسترس نبود — فالبک نادیده گرفته شد.');
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$values = [];
|
|
||||||
foreach ($items as $item) {
|
|
||||||
if (! is_array($item)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$key = trim((string) ($item['key'] ?? ''));
|
|
||||||
if ($key === '' || ! $this->isRestorable($key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$value = $item['value'] ?? null;
|
|
||||||
if ($value === null || trim((string) $value) === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$values[$key] = trim((string) $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $values;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isRestorable(string $key): bool
|
private function isRestorable(string $key): bool
|
||||||
{
|
{
|
||||||
if (SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) {
|
if (SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) {
|
||||||
|
|||||||
@@ -214,11 +214,12 @@ class SettingsController extends Controller
|
|||||||
|
|
||||||
private function isPanelManagedKey(string $key): bool
|
private function isPanelManagedKey(string $key): bool
|
||||||
{
|
{
|
||||||
return in_array($key, self::PANEL_MANAGED_KEYS, true);
|
return in_array($key, self::PANEL_MANAGED_KEYS, true)
|
||||||
|
|| app(ConfigService::class)->isTradingHoursSettingKey($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* تنظیمات پیشرفته: can_edit=0 (سوپرادمین) + کلیدهای can_edit=1 که کد مستقیم مینویسد.
|
* تنظیمات پیشرفته: ۱۴ دسترسی از سوپرادمین + بقیهٔ can_edit=0 از DB لوکال.
|
||||||
*/
|
*/
|
||||||
private function composeSystemSettings($localRows)
|
private function composeSystemSettings($localRows)
|
||||||
{
|
{
|
||||||
@@ -236,10 +237,7 @@ class SettingsController extends Controller
|
|||||||
if (! is_array($item)) {
|
if (! is_array($item)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (
|
if (! SuperAdminProgramSettingsOverlay::isSuperAdminOwned((string) $key)) {
|
||||||
SuperAdminProgramSettingsOverlay::isRuntime((string) $key)
|
|
||||||
|| SuperAdminProgramSettingsOverlay::isLocalOwned((string) $key)
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$value = $item['value'] ?? null;
|
$value = $item['value'] ?? null;
|
||||||
@@ -292,7 +290,8 @@ class SettingsController extends Controller
|
|||||||
$key = (string) ($row->key ?? '');
|
$key = (string) ($row->key ?? '');
|
||||||
|
|
||||||
return $this->isEditableCanEdit($row->can_edit ?? null)
|
return $this->isEditableCanEdit($row->can_edit ?? null)
|
||||||
|| SuperAdminProgramSettingsOverlay::isRuntime($key);
|
|| SuperAdminProgramSettingsOverlay::isRuntime($key)
|
||||||
|
|| SuperAdminProgramSettingsOverlay::isLocalOwned($key);
|
||||||
});
|
});
|
||||||
|
|
||||||
return $rows
|
return $rows
|
||||||
@@ -303,8 +302,8 @@ class SettingsController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* get_settings ادمین (و پایهٔ publicIndex قبل از فیلتر show_to_user):
|
* get_settings ادمین (و پایهٔ publicIndex قبل از فیلتر show_to_user):
|
||||||
* can_edit=1 (+ runtime) از DB لوکال
|
* can_edit=1 (+ runtime + local-owned) از DB لوکال
|
||||||
* can_edit=0 از سوپرادمین؛ اگر کلید در SA نبود یا value خالی بود → کپی لوکال
|
* فقط ۱۴ دسترسی سوپرادمین از overlay؛ بقیهٔ can_edit=0 از کپی لوکال
|
||||||
* اگر سوپرادمین در دسترس نباشد کل جدول لوکال برمیگردد.
|
* اگر سوپرادمین در دسترس نباشد کل جدول لوکال برمیگردد.
|
||||||
*/
|
*/
|
||||||
private function composeGetSettings($localRows)
|
private function composeGetSettings($localRows)
|
||||||
@@ -338,10 +337,7 @@ class SettingsController extends Controller
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$key = (string) $key;
|
$key = (string) $key;
|
||||||
if (
|
if (! SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) {
|
||||||
SuperAdminProgramSettingsOverlay::isRuntime($key)
|
|
||||||
|| SuperAdminProgramSettingsOverlay::isLocalOwned($key)
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$value = $item['value'] ?? null;
|
$value = $item['value'] ?? null;
|
||||||
|
|||||||
@@ -99,9 +99,11 @@ class ReportController extends Controller
|
|||||||
{
|
{
|
||||||
// status: open | closed (خارج از ساعت کاری) | no_quote (مظنه در دسترس نیست)
|
// status: open | closed (خارج از ساعت کاری) | no_quote (مظنه در دسترس نیست)
|
||||||
$payload = app(ConfigService::class)->applyHomePriceDisplayState($payload);
|
$payload = app(ConfigService::class)->applyHomePriceDisplayState($payload);
|
||||||
$payload = app(ConfigService::class)->attachTradingHoursSettingsPatch($payload);
|
|
||||||
|
|
||||||
$payload['up'] = $this->buildUpDownPrices();
|
// سقف/کف روز هم وقتی قیمت نباید نمایش داده شود صفر میماند
|
||||||
|
$payload['up'] = ($payload['status'] ?? 'open') === 'open'
|
||||||
|
? $this->buildUpDownPrices()
|
||||||
|
: ['sell' => 0, 'buy' => 0];
|
||||||
$payload['last_historical'] = app(ProductDisplayPriceService::class)->resolveHistoricalBasePrice();
|
$payload['last_historical'] = app(ProductDisplayPriceService::class)->resolveHistoricalBasePrice();
|
||||||
$payload['gold_ounce'] = $this->resolveGoldOuncePayload();
|
$payload['gold_ounce'] = $this->resolveGoldOuncePayload();
|
||||||
app(ProductDisplayPriceService::class)->cacheLivePricePayload($payload);
|
app(ProductDisplayPriceService::class)->cacheLivePricePayload($payload);
|
||||||
@@ -476,6 +478,10 @@ class ReportController extends Controller
|
|||||||
/** اختلاف پکیج ویژه — فقط روی پاسخ مشتری؛ کش مشترک را آلوده نمیکند */
|
/** اختلاف پکیج ویژه — فقط روی پاسخ مشتری؛ کش مشترک را آلوده نمیکند */
|
||||||
protected function personalizeLastPriceForCustomer(array $payload): array
|
protected function personalizeLastPriceForCustomer(array $payload): array
|
||||||
{
|
{
|
||||||
|
// تنظیمات ساعت کاری همیشه تازه از DB — اگر داخل کش قیمت بماند،
|
||||||
|
// پولینگ بعدی مقدار قبلی را برمیگرداند و تغییر پنل عقب برمیگردد.
|
||||||
|
$payload = app(ConfigService::class)->attachTradingHoursSettingsPatch($payload);
|
||||||
|
|
||||||
$customerId = (int) (auth('customer')->id() ?? 0);
|
$customerId = (int) (auth('customer')->id() ?? 0);
|
||||||
if ($customerId <= 0) {
|
if ($customerId <= 0) {
|
||||||
return $payload;
|
return $payload;
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ class ConfigService
|
|||||||
'trading_hours_by_day',
|
'trading_hours_by_day',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فقط این منابع «آخرین قیمت ذخیرهشده» هستند؛ باقی price_source (montzar، zaarbox،
|
||||||
|
* main_product، tala_ir و ...) مظنهٔ زنده حساب میشود.
|
||||||
|
*/
|
||||||
|
public const STORED_PRICE_SOURCES = ['database_last'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* حداکثر انحراف مجاز مظنه نسبت به آخرین قیمت معتبر DB.
|
* حداکثر انحراف مجاز مظنه نسبت به آخرین قیمت معتبر DB.
|
||||||
* پرش اشتباه provider (مثل ۱۶۸M ماندهٔ montzar روی مظنهٔ ~۲۲۰M) را رد میکند.
|
* پرش اشتباه provider (مثل ۱۶۸M ماندهٔ montzar روی مظنهٔ ~۲۲۰M) را رد میکند.
|
||||||
@@ -64,14 +70,15 @@ class ConfigService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* مقدار setting؛ اول سوپرادمین، بعد DB لوکال، بعد price_providers.php.
|
* مقدار setting: فقط دسترسیهای سوپرادمین از overlay؛ بقیه از DB لوکال / فایل.
|
||||||
* برای کلیدهای can_edit=0 همیشه از این مسیر (یا ConfigService) بخوانید — نه Setting::where مستقیم.
|
|
||||||
*/
|
*/
|
||||||
public function get(string $key, $default = null)
|
public function get(string $key, $default = null)
|
||||||
{
|
{
|
||||||
$fromSa = $this->valueFromSuperAdmin($key, $default);
|
if (SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) {
|
||||||
if ($fromSa !== '__sa_miss__') {
|
$fromSa = $this->valueFromSuperAdmin($key, $default);
|
||||||
return $fromSa;
|
if ($fromSa !== '__sa_miss__') {
|
||||||
|
return $fromSa;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -784,6 +791,46 @@ class ConfigService
|
|||||||
|| (int) ($payload['price'] ?? 0) > 0;
|
|| (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 روشن است.
|
* آخرین قیمت جدول prices را روی payload میگذارد — وقتی مظنه زنده نیست و show_last_price روشن است.
|
||||||
*
|
*
|
||||||
@@ -807,7 +854,8 @@ class ConfigService
|
|||||||
$payload['buy'] = $trade['buy'];
|
$payload['buy'] = $trade['buy'];
|
||||||
$payload['price_last'] = $trade['priceLast'];
|
$payload['price_last'] = $trade['priceLast'];
|
||||||
$payload['price'] = (int) $priceValue;
|
$payload['price'] = (int) $priceValue;
|
||||||
$payload['price_source'] = $payload['price_source'] ?? 'database_last';
|
// قیمت از DB آمد — برچسب منبع قبلی باید پاک شود وگرنه دفعهٔ بعد «زنده» حساب میشود
|
||||||
|
$payload['price_source'] = 'database_last';
|
||||||
|
|
||||||
return $payload;
|
return $payload;
|
||||||
}
|
}
|
||||||
@@ -823,29 +871,39 @@ class ConfigService
|
|||||||
*/
|
*/
|
||||||
public function applyHomePriceDisplayState(array $payload, ?Carbon $now = null): array
|
public function applyHomePriceDisplayState(array $payload, ?Carbon $now = null): array
|
||||||
{
|
{
|
||||||
$outside = $this->isOutsideTradingHours($now);
|
if ($this->isOutsideTradingHours($now)) {
|
||||||
$showWhenClosed = $this->shouldShowHomePriceWhenClosed();
|
|
||||||
$showLast = $this->shouldShowLastSavedPrice();
|
|
||||||
|
|
||||||
if ($outside) {
|
|
||||||
$payload['closed'] = true;
|
$payload['closed'] = true;
|
||||||
if ($showWhenClosed) {
|
|
||||||
$payload = $this->fillPayloadFromLastStoredPrice($payload);
|
if (! $this->shouldShowHomePriceWhenClosed()) {
|
||||||
$payload['status'] = $this->payloadHasDisplayablePrice($payload) ? 'open' : 'no_quote';
|
$payload = $this->clearPayloadDisplayPrices($payload);
|
||||||
|
$payload['status'] = 'closed';
|
||||||
|
|
||||||
return $payload;
|
return $payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload['status'] = 'closed';
|
$payload = $this->fillPayloadFromLastStoredPrice($payload);
|
||||||
|
$payload['status'] = $this->payloadHasDisplayablePrice($payload) ? 'open' : 'no_quote';
|
||||||
|
|
||||||
return $payload;
|
return $payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload['closed'] = false;
|
$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;
|
return $payload;
|
||||||
}
|
}
|
||||||
@@ -911,16 +969,22 @@ class ConfigService
|
|||||||
|
|
||||||
$patch = $this->clientTradingHoursSettings();
|
$patch = $this->clientTradingHoursSettings();
|
||||||
$cache = app(LivePriceCacheService::class);
|
$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 = $this->applyHomePriceDisplayState($payload);
|
||||||
$payload = app(ZaarboxPriceService::class)->attachGoldOunceToPayload($payload);
|
$payload = app(ZaarboxPriceService::class)->attachGoldOunceToPayload($payload);
|
||||||
$payload['settings_patch'] = $patch;
|
// settings_patch وارد کش نمیشود؛ رویداد settings مقدار تازه را میبرد
|
||||||
|
unset($payload['settings_patch']);
|
||||||
|
|
||||||
$cache->replaceBody($payload);
|
$cache->replaceBody($payload);
|
||||||
|
|
||||||
$stream = app(PriceStreamService::class);
|
|
||||||
if ($patch !== []) {
|
if ($patch !== []) {
|
||||||
$stream->publish('settings', $patch);
|
app(PriceStreamService::class)->publish('settings', $patch);
|
||||||
}
|
}
|
||||||
|
|
||||||
app(PriceBroadcastService::class)->broadcastAfterPriceUpdate($payload, true);
|
app(PriceBroadcastService::class)->broadcastAfterPriceUpdate($payload, true);
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class LivePriceCacheService
|
|||||||
'zaarbox' => 0,
|
'zaarbox' => 0,
|
||||||
'closed' => false,
|
'closed' => false,
|
||||||
'status' => 'no_quote',
|
'status' => 'no_quote',
|
||||||
|
'price_source' => '',
|
||||||
'time' => now(),
|
'time' => now(),
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
@@ -151,20 +152,25 @@ class LivePriceCacheService
|
|||||||
'zaarbox' => $zaarboxSource,
|
'zaarbox' => $zaarboxSource,
|
||||||
'closed' => false,
|
'closed' => false,
|
||||||
'status' => 'open',
|
'status' => 'open',
|
||||||
|
'price_source' => $priceSource,
|
||||||
'time' => now(),
|
'time' => now(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload['up'] = $config->resolvedDailyBaseExtremes()['up'] > 0
|
|
||||||
? [
|
|
||||||
'sell' => max(0, (int) ceil($config->resolvedDailyBaseExtremes()['down'] - $config->displaySellPriceDiffRial())),
|
|
||||||
'buy' => (int) ceil($config->resolvedDailyBaseExtremes()['up'] + $config->displayBuyPriceDiffRial()),
|
|
||||||
]
|
|
||||||
: ['sell' => $payload['sell'] ?? 0, 'buy' => $payload['buy'] ?? 0];
|
|
||||||
$payload['last_historical'] = $display->resolveHistoricalBasePrice();
|
$payload['last_historical'] = $display->resolveHistoricalBasePrice();
|
||||||
|
|
||||||
$payload = $config->applyGlobalTradingHoursToPayload($payload);
|
$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);
|
return app(ZaarboxPriceService::class)->attachGoldOunceToPayload($payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ use Illuminate\Support\Facades\Http;
|
|||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* خواندن تنظیمات پیشرفته از سوپرادمین (منبع حقیقت can_edit=0).
|
* خواندن ۱۴ دسترسی سوپرادمین (منبع حقیقت can_edit=0 برای قابلیتها).
|
||||||
*
|
*
|
||||||
* Public اول (دامنهمحور، بدون وابستگی به HMAC): GET {origin}/api/public/program-settings?domain=
|
* Public اول (دامنهمحور، بدون وابستگی به HMAC): GET {origin}/api/public/program-settings?domain=
|
||||||
* Full (با HMAC/Bearer): GET {origin}/api/remote/program-settings
|
* Full (با HMAC/Bearer): GET {origin}/api/remote/program-settings
|
||||||
*
|
*
|
||||||
* Public برای همین ۱۴ کلید مالکیت سوپرادمین کافی است؛ اگر remote اول باشد و replica عقب بماند،
|
* Overlay فقط کلیدهای SUPERADMIN_OWNED_KEYS را نگه میدارد؛ پاسخ remote ممکن است
|
||||||
* سوییچهایی مثل enable_automation روی false کهنه میمانند.
|
* کلیدهای دیگری هم برگرداند ولی آنها روی tenant اعمال نمیشوند.
|
||||||
*/
|
*/
|
||||||
class SuperAdminProgramSettingsClient
|
class SuperAdminProgramSettingsClient
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,21 +11,16 @@ use Illuminate\Support\Facades\Cache;
|
|||||||
class SuperAdminProgramSettingsOverlay
|
class SuperAdminProgramSettingsOverlay
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* آخرین پاسخ سالم سوپرادمین.
|
* آخرین پاسخ سالم سوپرادمین — فقط کلیدهای دسترسی (SUPERADMIN_OWNED_KEYS).
|
||||||
*
|
* پاسخ remote ممکن است کلیدهای دیگری هم داشته باشد؛ آنها ذخیره و اعمال نمیشوند.
|
||||||
* کلیدهای can_edit=0 مالکشان سوپرادمین است و کپیشان در جدول settings لوکال
|
|
||||||
* ممکن است ماندهٔ deploy باشد. بدون این کش، یک timeout لحظهای باعث میشود
|
|
||||||
* ConfigService به آن کپی سقوط کند — مثلاً price_provider از zaarbox به montzar
|
|
||||||
* میپرید و مظنهٔ قدیمی montzar_live_price بهجای قیمت زنده نمایش داده میشد.
|
|
||||||
*/
|
*/
|
||||||
private const LAST_GOOD_CACHE_KEY = 'superadmin:program_settings:last_good';
|
private const LAST_GOOD_CACHE_KEY = 'superadmin:program_settings:last_good';
|
||||||
|
|
||||||
private const LAST_GOOD_CACHE_MINUTES = 1440;
|
private const LAST_GOOD_CACHE_MINUTES = 1440;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* کلیدهایی که فقط سوپرادمین ویرایش میکند (پنل خانگی 403).
|
* فقط این ۱۴ دسترسی از سوپرادمین خوانده میشود.
|
||||||
* این لیست فیلتر overlay نیست — برای load از blacklist زیر استفاده میشود
|
* هر کلید دیگری (قیمت، ساعت کاری، برندینگ، …) مال پنل خانگی است.
|
||||||
* تا مثلاً price_provider از سوپرادمین/کش last-good حذف نشود.
|
|
||||||
*/
|
*/
|
||||||
public const SUPERADMIN_OWNED_KEYS = [
|
public const SUPERADMIN_OWNED_KEYS = [
|
||||||
'customer_direct_sms',
|
'customer_direct_sms',
|
||||||
@@ -185,32 +180,47 @@ class SuperAdminProgramSettingsOverlay
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$map = self::ownedItemsMap($items);
|
||||||
|
self::$memo = $map;
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فقط کلیدهای دسترسی سوپرادمین — بقیهٔ پاسخ remote دور ریخته میشود.
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>> $items
|
||||||
|
* @return array<string, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public static function ownedItemsMap(array $items): array
|
||||||
|
{
|
||||||
$map = [];
|
$map = [];
|
||||||
foreach ($items as $row) {
|
foreach ($items as $row) {
|
||||||
if (! is_array($row)) {
|
if (! is_array($row)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$key = trim((string) ($row['key'] ?? ''));
|
$key = trim((string) ($row['key'] ?? ''));
|
||||||
// blacklist: فقط کلیدهای لوکال/runtime را دور بریز — نه whitelist قابلیتها
|
if ($key === '' || ! self::isSuperAdminOwned($key)) {
|
||||||
// (whitelist قبلی price_provider را حذف میکرد و با قطعی SA به montzar لوکال میپرید)
|
|
||||||
if ($key === '' || self::isLocalOwned($key) || self::isRuntime($key)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$map[$key] = $row;
|
$map[$key] = $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
self::$memo = $map;
|
|
||||||
|
|
||||||
return $map;
|
return $map;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<int, array<string, mixed>> $items */
|
/** @param array<int, array<string, mixed>> $items */
|
||||||
private function rememberLastGood(array $items): void
|
private function rememberLastGood(array $items): void
|
||||||
{
|
{
|
||||||
|
$owned = array_values(self::ownedItemsMap($items));
|
||||||
|
if ($owned === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Cache::put(
|
Cache::put(
|
||||||
self::LAST_GOOD_CACHE_KEY,
|
self::LAST_GOOD_CACHE_KEY,
|
||||||
$items,
|
$owned,
|
||||||
now()->addMinutes(self::LAST_GOOD_CACHE_MINUTES)
|
now()->addMinutes(self::LAST_GOOD_CACHE_MINUTES)
|
||||||
);
|
);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
@@ -255,7 +265,7 @@ class SuperAdminProgramSettingsOverlay
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$key = trim($key);
|
$key = trim($key);
|
||||||
if ($key === '' || ! array_key_exists($key, $map)) {
|
if ($key === '' || ! self::isSuperAdminOwned($key) || ! array_key_exists($key, $map)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,9 +277,14 @@ class SuperAdminProgramSettingsOverlay
|
|||||||
return in_array($key, self::RUNTIME_KEYS, true);
|
return in_array($key, self::RUNTIME_KEYS, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* کلیدهای صفحهٔ «ساعت کاری» هم مالکشان پنل خانگی است — اگر از overlay خوانده شوند
|
||||||
|
* مقدار ذخیرهشده در پنل هیچوقت روی قیمت کاربر اعمال نمیشود.
|
||||||
|
*/
|
||||||
public static function isLocalOwned(string $key): bool
|
public static function isLocalOwned(string $key): bool
|
||||||
{
|
{
|
||||||
return in_array($key, self::LOCAL_OWNED_KEYS, true);
|
return in_array($key, self::LOCAL_OWNED_KEYS, true)
|
||||||
|
|| in_array($key, ConfigService::TRADING_HOURS_CLIENT_KEYS, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function isSuperAdminOwned(string $key): bool
|
public static function isSuperAdminOwned(string $key): bool
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Tests\Unit;
|
namespace Tests\Unit;
|
||||||
|
|
||||||
use App\Services\ConfigService;
|
use App\Services\ConfigService;
|
||||||
|
use App\Services\SuperAdminProgramSettingsOverlay;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Mockery;
|
use Mockery;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
@@ -119,6 +120,9 @@ class ConfigServiceTradingHoursTest extends TestCase
|
|||||||
|
|
||||||
$this->assertTrue($result['closed']);
|
$this->assertTrue($result['closed']);
|
||||||
$this->assertSame('closed', $result['status']);
|
$this->assertSame('closed', $result['status']);
|
||||||
|
$this->assertSame(0, $result['buy']);
|
||||||
|
$this->assertSame(0, $result['sell']);
|
||||||
|
$this->assertSame(0, $result['price']);
|
||||||
|
|
||||||
Carbon::setTestNow();
|
Carbon::setTestNow();
|
||||||
}
|
}
|
||||||
@@ -189,14 +193,87 @@ class ConfigServiceTradingHoursTest extends TestCase
|
|||||||
'sell' => 100,
|
'sell' => 100,
|
||||||
'buy' => 110,
|
'buy' => 110,
|
||||||
'price' => 105,
|
'price' => 105,
|
||||||
|
'price_source' => 'montzar',
|
||||||
|
'montzar' => 105,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->assertFalse($result['closed']);
|
$this->assertFalse($result['closed']);
|
||||||
$this->assertSame('open', $result['status']);
|
$this->assertSame('open', $result['status']);
|
||||||
|
$this->assertSame(110, $result['buy']);
|
||||||
|
|
||||||
Carbon::setTestNow();
|
Carbon::setTestNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* سناریوی گزارششده: مظنه زنده قطع است و قیمت از آخرین ردیف prices آمده،
|
||||||
|
* ولی «نمایش آخرین قیمت ذخیرهشده» خاموش است → نباید قیمت نشان داده شود.
|
||||||
|
*/
|
||||||
|
public function test_stored_price_is_hidden_when_show_last_price_is_off(): void
|
||||||
|
{
|
||||||
|
$cfg = $this->cfg([
|
||||||
|
'close_price' => 'false',
|
||||||
|
'show_last_price' => 'false',
|
||||||
|
'show_home_price_when_closed' => 'false',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $cfg->applyHomePriceDisplayState([
|
||||||
|
'sell' => 168336410,
|
||||||
|
'buy' => 168798112,
|
||||||
|
'price' => 168798112,
|
||||||
|
'price_last' => 168798112,
|
||||||
|
'price_source' => 'database_last',
|
||||||
|
'montzar' => 0,
|
||||||
|
'zaarbox' => 0,
|
||||||
|
'main_product' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertFalse($result['closed']);
|
||||||
|
$this->assertSame('no_quote', $result['status']);
|
||||||
|
$this->assertSame(0, $result['buy']);
|
||||||
|
$this->assertSame(0, $result['sell']);
|
||||||
|
$this->assertSame(0, $result['price']);
|
||||||
|
$this->assertSame(0, $result['price_last']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_live_quote_detection_ignores_database_fallback(): void
|
||||||
|
{
|
||||||
|
$cfg = $this->cfg([]);
|
||||||
|
|
||||||
|
$this->assertTrue($cfg->payloadHasLiveQuote([
|
||||||
|
'buy' => 110,
|
||||||
|
'price_source' => 'montzar',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertFalse($cfg->payloadHasLiveQuote([
|
||||||
|
'buy' => 110,
|
||||||
|
'price_source' => 'database_last',
|
||||||
|
]));
|
||||||
|
|
||||||
|
// منابع زندهٔ دیگر مثل tala_ir نباید «ذخیرهشده» حساب شوند
|
||||||
|
$this->assertTrue($cfg->payloadHasLiveQuote([
|
||||||
|
'buy' => 110,
|
||||||
|
'price_source' => 'tala_ir',
|
||||||
|
]));
|
||||||
|
|
||||||
|
// بدون برچسب منبع، عددِ مرجع زنده تصمیم میگیرد
|
||||||
|
$this->assertTrue($cfg->payloadHasLiveQuote([
|
||||||
|
'buy' => 110,
|
||||||
|
'zaarbox' => 105,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertFalse($cfg->payloadHasLiveQuote([
|
||||||
|
'buy' => 110,
|
||||||
|
'montzar' => 0,
|
||||||
|
'zaarbox' => 0,
|
||||||
|
'main_product' => 0,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertFalse($cfg->payloadHasLiveQuote([
|
||||||
|
'buy' => 0,
|
||||||
|
'price_source' => 'montzar',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
public function test_trading_hours_setting_keys(): void
|
public function test_trading_hours_setting_keys(): void
|
||||||
{
|
{
|
||||||
$cfg = new ConfigService();
|
$cfg = new ConfigService();
|
||||||
@@ -206,4 +283,19 @@ class ConfigServiceTradingHoursTest extends TestCase
|
|||||||
$this->assertTrue($cfg->isTradingHoursSettingKey('show_home_price_when_closed'));
|
$this->assertTrue($cfg->isTradingHoursSettingKey('show_home_price_when_closed'));
|
||||||
$this->assertFalse($cfg->isTradingHoursSettingKey('name_fa'));
|
$this->assertFalse($cfg->isTradingHoursSettingKey('name_fa'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** کلیدهای ساعت کاری باید مالکیت tenant داشته باشند وگرنه پنل اثری روی قیمت ندارد */
|
||||||
|
public function test_trading_hours_keys_are_tenant_owned(): void
|
||||||
|
{
|
||||||
|
foreach (ConfigService::TRADING_HOURS_CLIENT_KEYS as $key) {
|
||||||
|
$this->assertTrue(
|
||||||
|
SuperAdminProgramSettingsOverlay::isLocalOwned($key),
|
||||||
|
"{$key} باید local-owned باشد"
|
||||||
|
);
|
||||||
|
$this->assertFalse(
|
||||||
|
SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key),
|
||||||
|
"{$key} نباید سوپرادمین-اونر باشد"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Services\ConfigService;
|
||||||
|
use App\Services\SuperAdminProgramSettingsOverlay;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class SuperAdminProgramSettingsOverlayTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_owned_keys_match_the_access_flag_list(): void
|
||||||
|
{
|
||||||
|
$expected = [
|
||||||
|
'customer_direct_sms',
|
||||||
|
'customer_special_pricing',
|
||||||
|
'enable_automation',
|
||||||
|
'enable_conditional_order',
|
||||||
|
'enable_customer_level_profit',
|
||||||
|
'enable_product_funds',
|
||||||
|
'group_sms',
|
||||||
|
'has_page_earn_money',
|
||||||
|
'has_page_gift_card',
|
||||||
|
'has_page_notification',
|
||||||
|
'has_page_price_trigger',
|
||||||
|
'jibit_enabled',
|
||||||
|
'permision__enabled',
|
||||||
|
'show_to_users_chat',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertSame($expected, SuperAdminProgramSettingsOverlay::SUPERADMIN_OWNED_KEYS);
|
||||||
|
$this->assertCount(14, SuperAdminProgramSettingsOverlay::SUPERADMIN_OWNED_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_overlay_keeps_only_access_keys(): void
|
||||||
|
{
|
||||||
|
$map = SuperAdminProgramSettingsOverlay::ownedItemsMap([
|
||||||
|
['key' => 'enable_automation', 'value' => 'true'],
|
||||||
|
['key' => 'jibit_enabled', 'value' => 'false'],
|
||||||
|
['key' => 'price_provider', 'value' => 'zaarbox'],
|
||||||
|
['key' => 'show_last_price', 'value' => 'false'],
|
||||||
|
['key' => 'close_price', 'value' => 'true'],
|
||||||
|
['key' => 'startDay', 'value' => '11:00'],
|
||||||
|
['key' => 'endDay', 'value' => '20:30'],
|
||||||
|
['key' => 'name_fa', 'value' => 'طلا'],
|
||||||
|
['key' => 'montzar_live_price', 'value' => '168000000'],
|
||||||
|
['key' => 'tracking_systems', 'value' => '[]'],
|
||||||
|
['key' => '', 'value' => 'x'],
|
||||||
|
'not-an-array',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(['enable_automation', 'jibit_enabled'], array_keys($map));
|
||||||
|
$this->assertSame('true', $map['enable_automation']['value']);
|
||||||
|
$this->assertArrayNotHasKey('price_provider', $map);
|
||||||
|
$this->assertArrayNotHasKey('show_last_price', $map);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_tenant_keys_are_never_superadmin_owned(): void
|
||||||
|
{
|
||||||
|
$tenantKeys = array_merge(
|
||||||
|
ConfigService::TRADING_HOURS_CLIENT_KEYS,
|
||||||
|
SuperAdminProgramSettingsOverlay::RUNTIME_KEYS,
|
||||||
|
SuperAdminProgramSettingsOverlay::LOCAL_OWNED_KEYS,
|
||||||
|
[
|
||||||
|
'price_provider',
|
||||||
|
'zaarbox_price_key',
|
||||||
|
'montzar_base_url',
|
||||||
|
'name_fa',
|
||||||
|
'name_en',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($tenantKeys as $key) {
|
||||||
|
$this->assertFalse(
|
||||||
|
SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key),
|
||||||
|
"{$key} نباید از سوپرادمین خوانده شود"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user