diff --git a/app/Console/Commands/SettingsRestoreFromBackupCommand.php b/app/Console/Commands/SettingsRestoreFromBackupCommand.php index 18cae5b..542e0ba 100644 --- a/app/Console/Commands/SettingsRestoreFromBackupCommand.php +++ b/app/Console/Commands/SettingsRestoreFromBackupCommand.php @@ -3,7 +3,6 @@ namespace App\Console\Commands; use App\Models\Setting; -use App\Services\SuperAdminProgramSettingsClient; use App\Services\SuperAdminProgramSettingsOverlay; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; @@ -16,7 +15,8 @@ use Illuminate\Support\Facades\Schema; * قواعد سخت: * 1) value غیرخالی لوکال هرگز عوض نمی‌شود (مگر --force-overwrite). * 2) پیش‌فرض‌های seed/deploy (مثل limangold.ir) هرگز به‌عنوان مقدار واقعی نوشته نمی‌شوند. - * 3) با --fill-from-superadmin برای خالی/غایب: اول SA واقعی، بعد بکاپ واقعی. + * 3) --fill-from-superadmin دیگر کلیدهای غیر دسترسی را از سوپرادمین نمی‌خواند + * (سوپرادمین فقط ۱۴ دسترسی را مالک است). * * کلیدهای گروه «دسترسی مشتری به قابلیت‌ها» و runtime رد می‌شوند. */ @@ -78,7 +78,10 @@ class SettingsRestoreFromBackupCommand extends Command } $backupRows = $this->readBackup($table); - $superAdminValues = $fillFromSuperAdmin ? $this->superAdminValues() : []; + $superAdminValues = []; + if ($fillFromSuperAdmin) { + $this->warn('--fill-from-superadmin نادیده گرفته شد: سوپرادمین فقط دسترسی‌ها را دارد، نه بقیهٔ تنظیمات.'); + } if ($backupRows === [] && $superAdminValues === []) { $this->warn('هیچ کلید قابل بازگردانی پیدا نشد.'); @@ -332,37 +335,6 @@ class SettingsRestoreFromBackupCommand extends Command return $rows; } - /** - * @return array - */ - 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 { if (SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) { diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php index aa851dd..07131ec 100644 --- a/app/Http/Controllers/Admin/SettingsController.php +++ b/app/Http/Controllers/Admin/SettingsController.php @@ -214,11 +214,12 @@ class SettingsController extends Controller 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) { @@ -236,10 +237,7 @@ class SettingsController extends Controller if (! is_array($item)) { continue; } - if ( - SuperAdminProgramSettingsOverlay::isRuntime((string) $key) - || SuperAdminProgramSettingsOverlay::isLocalOwned((string) $key) - ) { + if (! SuperAdminProgramSettingsOverlay::isSuperAdminOwned((string) $key)) { continue; } $value = $item['value'] ?? null; @@ -292,7 +290,8 @@ class SettingsController extends Controller $key = (string) ($row->key ?? ''); return $this->isEditableCanEdit($row->can_edit ?? null) - || SuperAdminProgramSettingsOverlay::isRuntime($key); + || SuperAdminProgramSettingsOverlay::isRuntime($key) + || SuperAdminProgramSettingsOverlay::isLocalOwned($key); }); return $rows @@ -303,8 +302,8 @@ class SettingsController extends Controller /** * get_settings ادمین (و پایهٔ publicIndex قبل از فیلتر show_to_user): - * can_edit=1 (+ runtime) از DB لوکال - * can_edit=0 از سوپرادمین؛ اگر کلید در SA نبود یا value خالی بود → کپی لوکال + * can_edit=1 (+ runtime + local-owned) از DB لوکال + * فقط ۱۴ دسترسی سوپرادمین از overlay؛ بقیهٔ can_edit=0 از کپی لوکال * اگر سوپرادمین در دسترس نباشد کل جدول لوکال برمی‌گردد. */ private function composeGetSettings($localRows) @@ -338,10 +337,7 @@ class SettingsController extends Controller continue; } $key = (string) $key; - if ( - SuperAdminProgramSettingsOverlay::isRuntime($key) - || SuperAdminProgramSettingsOverlay::isLocalOwned($key) - ) { + if (! SuperAdminProgramSettingsOverlay::isSuperAdminOwned($key)) { continue; } $value = $item['value'] ?? null; diff --git a/app/Http/Controllers/Site/ReportController.php b/app/Http/Controllers/Site/ReportController.php index ef821ad..cf65a15 100644 --- a/app/Http/Controllers/Site/ReportController.php +++ b/app/Http/Controllers/Site/ReportController.php @@ -99,9 +99,11 @@ class ReportController extends Controller { // status: open | closed (خارج از ساعت کاری) | no_quote (مظنه در دسترس نیست) $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['gold_ounce'] = $this->resolveGoldOuncePayload(); app(ProductDisplayPriceService::class)->cacheLivePricePayload($payload); @@ -476,6 +478,10 @@ class ReportController extends Controller /** اختلاف پکیج ویژه — فقط روی پاسخ مشتری؛ کش مشترک را آلوده نمی‌کند */ protected function personalizeLastPriceForCustomer(array $payload): array { + // تنظیمات ساعت کاری همیشه تازه از DB — اگر داخل کش قیمت بماند، + // پولینگ بعدی مقدار قبلی را برمی‌گرداند و تغییر پنل عقب برمی‌گردد. + $payload = app(ConfigService::class)->attachTradingHoursSettingsPatch($payload); + $customerId = (int) (auth('customer')->id() ?? 0); if ($customerId <= 0) { return $payload; diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index ad29a9a..25aef4b 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -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 $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 $payload + * @return array + */ + 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); diff --git a/app/Services/LivePriceCacheService.php b/app/Services/LivePriceCacheService.php index baec03a..fad52bf 100644 --- a/app/Services/LivePriceCacheService.php +++ b/app/Services/LivePriceCacheService.php @@ -115,6 +115,7 @@ class LivePriceCacheService 'zaarbox' => 0, 'closed' => false, 'status' => 'no_quote', + 'price_source' => '', 'time' => now(), ]; } else { @@ -151,20 +152,25 @@ class LivePriceCacheService 'zaarbox' => $zaarboxSource, 'closed' => false, 'status' => 'open', + 'price_source' => $priceSource, '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 = $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); } } diff --git a/app/Services/SuperAdminProgramSettingsClient.php b/app/Services/SuperAdminProgramSettingsClient.php index bf28fe6..61ed3a3 100644 --- a/app/Services/SuperAdminProgramSettingsClient.php +++ b/app/Services/SuperAdminProgramSettingsClient.php @@ -6,13 +6,13 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; /** - * خواندن تنظیمات پیشرفته از سوپرادمین (منبع حقیقت can_edit=0). + * خواندن ۱۴ دسترسی سوپرادمین (منبع حقیقت can_edit=0 برای قابلیت‌ها). * * Public اول (دامنه‌محور، بدون وابستگی به HMAC): GET {origin}/api/public/program-settings?domain= * Full (با HMAC/Bearer): GET {origin}/api/remote/program-settings * - * Public برای همین ۱۴ کلید مالکیت سوپرادمین کافی است؛ اگر remote اول باشد و replica عقب بماند، - * سوییچ‌هایی مثل enable_automation روی false کهنه می‌مانند. + * Overlay فقط کلیدهای SUPERADMIN_OWNED_KEYS را نگه می‌دارد؛ پاسخ remote ممکن است + * کلیدهای دیگری هم برگرداند ولی آن‌ها روی tenant اعمال نمی‌شوند. */ class SuperAdminProgramSettingsClient { diff --git a/app/Services/SuperAdminProgramSettingsOverlay.php b/app/Services/SuperAdminProgramSettingsOverlay.php index 32062ad..b90a2dd 100644 --- a/app/Services/SuperAdminProgramSettingsOverlay.php +++ b/app/Services/SuperAdminProgramSettingsOverlay.php @@ -11,21 +11,16 @@ use Illuminate\Support\Facades\Cache; class SuperAdminProgramSettingsOverlay { /** - * آخرین پاسخ سالم سوپرادمین. - * - * کلیدهای can_edit=0 مالکشان سوپرادمین است و کپی‌شان در جدول settings لوکال - * ممکن است ماندهٔ deploy باشد. بدون این کش، یک timeout لحظه‌ای باعث می‌شود - * ConfigService به آن کپی سقوط کند — مثلاً price_provider از zaarbox به montzar - * می‌پرید و مظنهٔ قدیمی montzar_live_price به‌جای قیمت زنده نمایش داده می‌شد. + * آخرین پاسخ سالم سوپرادمین — فقط کلیدهای دسترسی (SUPERADMIN_OWNED_KEYS). + * پاسخ remote ممکن است کلیدهای دیگری هم داشته باشد؛ آن‌ها ذخیره و اعمال نمی‌شوند. */ private const LAST_GOOD_CACHE_KEY = 'superadmin:program_settings:last_good'; private const LAST_GOOD_CACHE_MINUTES = 1440; /** - * کلیدهایی که فقط سوپرادمین ویرایش می‌کند (پنل خانگی 403). - * این لیست فیلتر overlay نیست — برای load از blacklist زیر استفاده می‌شود - * تا مثلاً price_provider از سوپرادمین/کش last-good حذف نشود. + * فقط این ۱۴ دسترسی از سوپرادمین خوانده می‌شود. + * هر کلید دیگری (قیمت، ساعت کاری، برندینگ، …) مال پنل خانگی است. */ public const SUPERADMIN_OWNED_KEYS = [ 'customer_direct_sms', @@ -185,32 +180,47 @@ class SuperAdminProgramSettingsOverlay return null; } + $map = self::ownedItemsMap($items); + self::$memo = $map; + + return $map; + } + + /** + * فقط کلیدهای دسترسی سوپرادمین — بقیهٔ پاسخ remote دور ریخته می‌شود. + * + * @param array> $items + * @return array> + */ + public static function ownedItemsMap(array $items): array + { $map = []; foreach ($items as $row) { if (! is_array($row)) { continue; } $key = trim((string) ($row['key'] ?? '')); - // blacklist: فقط کلیدهای لوکال/runtime را دور بریز — نه whitelist قابلیت‌ها - // (whitelist قبلی price_provider را حذف می‌کرد و با قطعی SA به montzar لوکال می‌پرید) - if ($key === '' || self::isLocalOwned($key) || self::isRuntime($key)) { + if ($key === '' || ! self::isSuperAdminOwned($key)) { continue; } $map[$key] = $row; } - self::$memo = $map; - return $map; } /** @param array> $items */ private function rememberLastGood(array $items): void { + $owned = array_values(self::ownedItemsMap($items)); + if ($owned === []) { + return; + } + try { Cache::put( self::LAST_GOOD_CACHE_KEY, - $items, + $owned, now()->addMinutes(self::LAST_GOOD_CACHE_MINUTES) ); } catch (\Throwable $e) { @@ -255,7 +265,7 @@ class SuperAdminProgramSettingsOverlay return null; } $key = trim($key); - if ($key === '' || ! array_key_exists($key, $map)) { + if ($key === '' || ! self::isSuperAdminOwned($key) || ! array_key_exists($key, $map)) { return null; } @@ -267,9 +277,14 @@ class SuperAdminProgramSettingsOverlay return in_array($key, self::RUNTIME_KEYS, true); } + /** + * کلیدهای صفحهٔ «ساعت کاری» هم مالکشان پنل خانگی است — اگر از overlay خوانده شوند + * مقدار ذخیره‌شده در پنل هیچ‌وقت روی قیمت کاربر اعمال نمی‌شود. + */ 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 diff --git a/tests/Unit/ConfigServiceTradingHoursTest.php b/tests/Unit/ConfigServiceTradingHoursTest.php index 6dc5b0f..f9f1bf1 100644 --- a/tests/Unit/ConfigServiceTradingHoursTest.php +++ b/tests/Unit/ConfigServiceTradingHoursTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit; use App\Services\ConfigService; +use App\Services\SuperAdminProgramSettingsOverlay; use Carbon\Carbon; use Mockery; use Tests\TestCase; @@ -119,6 +120,9 @@ class ConfigServiceTradingHoursTest extends TestCase $this->assertTrue($result['closed']); $this->assertSame('closed', $result['status']); + $this->assertSame(0, $result['buy']); + $this->assertSame(0, $result['sell']); + $this->assertSame(0, $result['price']); Carbon::setTestNow(); } @@ -189,14 +193,87 @@ class ConfigServiceTradingHoursTest extends TestCase 'sell' => 100, 'buy' => 110, 'price' => 105, + 'price_source' => 'montzar', + 'montzar' => 105, ]); $this->assertFalse($result['closed']); $this->assertSame('open', $result['status']); + $this->assertSame(110, $result['buy']); 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 { $cfg = new ConfigService(); @@ -206,4 +283,19 @@ class ConfigServiceTradingHoursTest extends TestCase $this->assertTrue($cfg->isTradingHoursSettingKey('show_home_price_when_closed')); $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} نباید سوپرادمین-اونر باشد" + ); + } + } } diff --git a/tests/Unit/SuperAdminProgramSettingsOverlayTest.php b/tests/Unit/SuperAdminProgramSettingsOverlayTest.php new file mode 100644 index 0000000..a687d2b --- /dev/null +++ b/tests/Unit/SuperAdminProgramSettingsOverlayTest.php @@ -0,0 +1,79 @@ +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} نباید از سوپرادمین خوانده شود" + ); + } + } +}