From 8984899042b1a3385e671648548ef9aaa3b21aef Mon Sep 17 00:00:00 2001 From: mohammad sedighi Date: Tue, 25 Aug 2026 02:29:00 +0330 Subject: [PATCH] fix(settings): push trading-hours changes to live user prices immediately Recompute home price display on save, broadcast settings over SSE, and keep last-price status in sync without a user refresh. Co-authored-by: Cursor --- .../Controllers/Admin/SettingsController.php | 9 + .../Site/PriceStreamController.php | 5 + .../Controllers/Site/ReportController.php | 11 +- app/Services/ConfigService.php | 177 ++++++++++++++++-- app/Services/LivePriceCacheService.php | 33 +++- app/Services/PriceBroadcastService.php | 5 +- database/sql/deploy_settings_const.sql | 6 +- ...ngs_const_preserve_value_sync_can_edit.sql | 6 +- .../sql/settings_menu_and_show_to_user.sql | 2 + tests/Unit/ConfigServiceTradingHoursTest.php | 90 ++++++++- 10 files changed, 300 insertions(+), 44 deletions(-) diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php index 4e2e119..aa851dd 100644 --- a/app/Http/Controllers/Admin/SettingsController.php +++ b/app/Http/Controllers/Admin/SettingsController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Setting; +use App\Services\ConfigService; use App\Services\PriceChannelBroadcastService; use App\Services\SuperAdminProgramSettingsOverlay; use Illuminate\Http\Request; @@ -167,6 +168,14 @@ class SettingsController extends Controller })->afterResponse(); } + if (app(ConfigService::class)->isTradingHoursSettingKey($key)) { + try { + app(ConfigService::class)->notifyTradingHoursChanged(); + } catch (\Throwable $e) { + report($e); + } + } + if ($isNew) { $projectName = Setting::where('key', 'name_fa')->value('value') ?: 'پروژه'; $settingTitle = trim($request->title_fa ?? '') ?: $request->key; diff --git a/app/Http/Controllers/Site/PriceStreamController.php b/app/Http/Controllers/Site/PriceStreamController.php index 145d487..4fc177a 100644 --- a/app/Http/Controllers/Site/PriceStreamController.php +++ b/app/Http/Controllers/Site/PriceStreamController.php @@ -61,6 +61,11 @@ class PriceStreamController extends Controller $send('last', app(ZaarboxPriceService::class)->attachGoldOunceToPayload($last)); } + $hoursSettings = app(\App\Services\ConfigService::class)->clientTradingHoursSettings(); + if ($hoursSettings !== []) { + $send('settings', $hoursSettings); + } + $list = app(PriceBroadcastService::class)->buildListPayload(false); if (is_array($list) && $list !== []) { $send('list', $list); diff --git a/app/Http/Controllers/Site/ReportController.php b/app/Http/Controllers/Site/ReportController.php index 8c91234..ef821ad 100644 --- a/app/Http/Controllers/Site/ReportController.php +++ b/app/Http/Controllers/Site/ReportController.php @@ -98,15 +98,8 @@ class ReportController extends Controller protected function appendUpPrices(array $payload, bool $saveFullCache = false): array { // status: open | closed (خارج از ساعت کاری) | no_quote (مظنه در دسترس نیست) - if (!isset($payload['status'])) { - if (!empty($payload['closed'])) { - $payload['status'] = 'closed'; - } elseif ((int) ($payload['price'] ?? 0) <= 0 && (int) ($payload['buy'] ?? 0) <= 0) { - $payload['status'] = 'no_quote'; - } else { - $payload['status'] = 'open'; - } - } + $payload = app(ConfigService::class)->applyHomePriceDisplayState($payload); + $payload = app(ConfigService::class)->attachTradingHoursSettingsPatch($payload); $payload['up'] = $this->buildUpDownPrices(); $payload['last_historical'] = app(ProductDisplayPriceService::class)->resolveHistoricalBasePrice(); diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index 4c4bf3c..ad29a9a 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -14,6 +14,17 @@ class ConfigService { public const PRICE_PROVIDERS = ['montzar', 'main_product', 'zaarbox']; + /** کلیدهای صفحه «ساعت کاری» پنل — هر تغییر باید بلافاصله روی قیمت کاربر پخش شود */ + public const TRADING_HOURS_CLIENT_KEYS = [ + 'close_price', + 'startDay', + 'endDay', + 'show_last_price', + 'show_home_price_when_closed', + 'show_gold_ounce_price', + 'trading_hours_by_day', + ]; + /** * حداکثر انحراف مجاز مظنه نسبت به آخرین قیمت معتبر DB. * پرش اشتباه provider (مثل ۱۶۸M ماندهٔ montzar روی مظنهٔ ~۲۲۰M) را رد می‌کند. @@ -751,40 +762,168 @@ class ConfigService $start = $now->copy()->startOfDay()->setTime($startHour, $startMinute); $end = $now->copy()->startOfDay()->setTime($endHour, $endMinute); + // بازهٔ شبانه (مثلاً ۲۲:۰۰ تا ۰۶:۰۰) + if ($end->lte($start)) { + return $now->lt($start) && $now->gt($end); + } + return !$now->between($start, $end); } + public function shouldShowLastSavedPrice(): bool + { + return $this->getBool('show_last_price', false); + } + + /** @param array $payload */ + public function payloadHasDisplayablePrice(array $payload): bool + { + return (int) ($payload['buy'] ?? 0) > 0 + || (int) ($payload['sell'] ?? 0) > 0 + || (int) ($payload['price_last'] ?? 0) > 0 + || (int) ($payload['price'] ?? 0) > 0; + } + /** - * گیت ساعت کاری روی payload کش/SSE — وقتی close_price روشن است. + * آخرین قیمت جدول prices را روی payload می‌گذارد — وقتی مظنه زنده نیست و show_last_price روشن است. + * + * @param array $payload + * @return array + */ + public function fillPayloadFromLastStoredPrice(array $payload): array + { + if ($this->payloadHasDisplayablePrice($payload)) { + return $payload; + } + + $stored = Price::query()->orderByDesc('id')->value('price'); + $priceValue = $stored !== null ? (float) $stored : 0.0; + if ($priceValue <= 0) { + return $payload; + } + + $trade = $this->buildTradeDisplayPrices($priceValue, 'database_last'); + $payload['sell'] = $trade['sell']; + $payload['buy'] = $trade['buy']; + $payload['price_last'] = $trade['priceLast']; + $payload['price'] = (int) $priceValue; + $payload['price_source'] = $payload['price_source'] ?? 'database_last'; + + return $payload; + } + + /** + * وضعیت نمایش صفحه اصلی + پرچم معامله: + * - close_price خاموش / داخل ساعت: قیمت هست → open ؛ وگرنه با show_last_price آخرین قیمت؛ وگرنه no_quote + * - close_price روشن و خارج ساعت: show_home_price_when_closed روشن → آخرین قیمت (open) ؛ خاموش → closed + * closed=true یعنی خرید/فروش بسته است؛ status فقط برای نمایش خانه است. + * + * @param array $payload + * @return array + */ + public function applyHomePriceDisplayState(array $payload, ?Carbon $now = null): array + { + $outside = $this->isOutsideTradingHours($now); + $showWhenClosed = $this->shouldShowHomePriceWhenClosed(); + $showLast = $this->shouldShowLastSavedPrice(); + + if ($outside) { + $payload['closed'] = true; + if ($showWhenClosed) { + $payload = $this->fillPayloadFromLastStoredPrice($payload); + $payload['status'] = $this->payloadHasDisplayablePrice($payload) ? 'open' : 'no_quote'; + + return $payload; + } + + $payload['status'] = 'closed'; + + return $payload; + } + + $payload['closed'] = false; + if ($showLast) { + $payload = $this->fillPayloadFromLastStoredPrice($payload); + } + $payload['status'] = $this->payloadHasDisplayablePrice($payload) ? 'open' : 'no_quote'; + + return $payload; + } + + /** + * گیت ساعت کاری روی payload کش/SSE. * * @param array $payload * @return array */ public function applyGlobalTradingHoursToPayload(array $payload): array { - if (!$this->isClosePriceEnabled() || !$this->isOutsideTradingHours()) { - return $payload; + return $this->applyHomePriceDisplayState($payload); + } + + public function isTradingHoursSettingKey(string $key): bool + { + return in_array($key, self::TRADING_HOURS_CLIENT_KEYS, true); + } + + /** + * تنظیمات ساعت کاری برای اپ کاربر (بدون فیلتر show_to_user — secret نیستند). + * + * @return list + */ + public function clientTradingHoursSettings(): array + { + $rows = Setting::query() + ->whereIn('key', self::TRADING_HOURS_CLIENT_KEYS) + ->get() + ->keyBy('key'); + + $out = []; + foreach (self::TRADING_HOURS_CLIENT_KEYS as $key) { + $row = $rows->get($key); + $out[] = [ + 'key' => $key, + 'value' => $row->value ?? '', + 'is_active' => $row->is_active ?? 1, + ]; } - if ($this->shouldShowHomePriceWhenClosed()) { - $payload['closed'] = true; - $payload['status'] = 'closed'; + return $out; + } - return $payload; + /** + * @param array $payload + * @return array + */ + public function attachTradingHoursSettingsPatch(array $payload): array + { + $payload['settings_patch'] = $this->clientTradingHoursSettings(); + + return $payload; + } + + /** + * بعد از ذخیره ساعت کاری در پنل: کش last، SSE تنظیمات، و قیمت را همان لحظه برای کاربر بفرست. + */ + public function notifyTradingHoursChanged(): void + { + $this->syncAppClosedFlag(); + + $patch = $this->clientTradingHoursSettings(); + $cache = app(LivePriceCacheService::class); + $payload = $cache->get() ?? $cache->buildFromDatabase(); + $payload = $this->applyHomePriceDisplayState($payload); + $payload = app(ZaarboxPriceService::class)->attachGoldOunceToPayload($payload); + $payload['settings_patch'] = $patch; + + $cache->replaceBody($payload); + + $stream = app(PriceStreamService::class); + if ($patch !== []) { + $stream->publish('settings', $patch); } - return [ - 'sell' => 0, - 'buy' => 0, - 'price_last' => 0, - 'price' => 0, - 'main_product' => (int) ($payload['main_product'] ?? 0), - 'montzar' => (int) ($payload['montzar'] ?? 0), - 'zaarbox' => (int) ($payload['zaarbox'] ?? 0), - 'closed' => true, - 'status' => 'closed', - 'time' => now(), - ]; + app(PriceBroadcastService::class)->broadcastAfterPriceUpdate($payload, true); } public function syncAppClosedFlag(?Carbon $now = null): bool diff --git a/app/Services/LivePriceCacheService.php b/app/Services/LivePriceCacheService.php index 5c61b9d..baec03a 100644 --- a/app/Services/LivePriceCacheService.php +++ b/app/Services/LivePriceCacheService.php @@ -45,12 +45,7 @@ class LivePriceCacheService return $cachedBody; } - Cache::put(self::FULL_RESPONSE_KEY, [ - 'body' => $body, - 'stored_at' => now(), - ], now()->addSeconds(self::TTL_SECONDS + 5)); - - app(ProductDisplayPriceService::class)->cacheLivePricePayload($body); + $this->storeBody($body); app(PriceBroadcastService::class)->broadcastLastIfChanged($body); @@ -59,6 +54,32 @@ class LivePriceCacheService return $body; } + /** + * بازنویسی کش last بدون گیت هویت — برای اعمال فوری ساعت کاری. + * + * @param array $body + * @return array + */ + public function replaceBody(array $body): array + { + $this->storeBody($body); + + return $body; + } + + /** + * @param array $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)); diff --git a/app/Services/PriceBroadcastService.php b/app/Services/PriceBroadcastService.php index 142b2b1..54fcfb4 100644 --- a/app/Services/PriceBroadcastService.php +++ b/app/Services/PriceBroadcastService.php @@ -34,6 +34,7 @@ class PriceBroadcastService } if (is_array($lastPayload) && $lastPayload !== []) { + $lastPayload = app(ConfigService::class)->applyHomePriceDisplayState($lastPayload); $lastPayload = app(ZaarboxPriceService::class)->attachGoldOunceToPayload($lastPayload); } @@ -63,7 +64,7 @@ class PriceBroadcastService /** * @param array $payload - * @return array + * @return array */ public static function lastPriceIdentity(array $payload): array { @@ -75,6 +76,7 @@ class PriceBroadcastService 'price_last' => (int) ($payload['price_last'] ?? 0), 'price' => (int) ($payload['price'] ?? 0), 'closed' => (bool) ($payload['closed'] ?? false), + 'status' => (string) ($payload['status'] ?? ''), 'main_product' => (int) ($payload['main_product'] ?? 0), 'montzar' => (int) ($payload['montzar'] ?? 0), 'zaarbox' => (int) ($payload['zaarbox'] ?? 0), @@ -93,6 +95,7 @@ class PriceBroadcastService return; } + $lastPayload = app(ConfigService::class)->applyHomePriceDisplayState($lastPayload); $lastPayload = app(ZaarboxPriceService::class)->attachGoldOunceToPayload($lastPayload); if (! $this->rememberHashIfChanged(self::LAST_HASH_KEY, self::lastPriceIdentity($lastPayload))) { diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 58655a7..5947a3c 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -1653,16 +1653,16 @@ UPDATE `settings` SET `description` = 'روشن: نمودار قیمت از تا -- [super-admin can_edit=0] -- UPDATE `settings` SET `description` = 'آدرس سرور(های) فید قیمت (/api/free). تا ۵ آدرس (JSON آرایه) پشتیبانی می‌شود؛ اگر آدرس اول پاسخ ندهد به‌ترتیب سراغ بعدی می‌رود. مقدار تکی قبلی حفظ می‌شود.' WHERE `key` = 'zaarbox_api_url'; UPDATE `settings` SET `description` = 'کلید یا UUID ردیف قیمت در پاسخ API تابلو. باید با محصول/ردیف موردنظر در سرویس خارجی یکسان باشد.' WHERE `key` = 'zaarbox_price_key'; -UPDATE `settings` SET `description` = 'روشن: اگر API قیمت در دسترس نباشد، آخرین قیمت ذخیره‌شده در دیتابیس به کاربر نشان داده می‌شود. خاموش: در قطعی API ممکن است قیمت صفر نمایش داده شود.' WHERE `key` = 'show_last_price'; +UPDATE `settings` SET `description` = 'وقتی مظنه زنده نیست (و فروشگاه از نظر ساعت بسته حساب نمی‌شود): روشن = آخرین قیمت ذخیره‌شده نشان داده شود. خاموش = پیام «مظنه در دسترس نیست».' WHERE `key` = 'show_last_price'; UPDATE `settings` SET `description` = 'روشن: اگر آدرس API تابلو (zaarbox_api_url) تنظیم شده باشد، قیمت انس طلا در اپ مشتری نمایش داده می‌شود. خاموش: نمایش داده نمی‌شود.' WHERE `key` = 'show_gold_ounce_price'; -UPDATE `settings` SET `description` = 'روشن: وقتی فروشگاه خارج از ساعت کاری است، قیمت صفحه اصلی اپ همچنان نمایش داده می‌شود و پیام «فروشگاه بسته است» نشان داده نمی‌شود. خرید/فروش همچنان بسته می‌ماند. خاموش: مثل قبل، خارج از ساعت پیام بسته بودن و قیمت صفر نشان داده می‌شود.' WHERE `key` = 'show_home_price_when_closed'; +UPDATE `settings` SET `description` = 'فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن است و الان خارج از بازه هستید. روشن: آخرین قیمت روی صفحه اصلی نشان داده می‌شود (معامله بسته می‌ماند). خاموش: پیام «فروشگاه بسته است».' WHERE `key` = 'show_home_price_when_closed'; UPDATE `settings` SET `description` = 'روشن: بعد از انتخاب محصول در صفحه خرید/فروش، متن توضیح محصول نمایش داده می‌شود. خاموش: توضیح نشان داده نمی‌شود.' WHERE `key` = 'show_product_details'; UPDATE `settings` SET `description` = 'روشن: در صفحه خرید/فروش (بالای تب دسته‌ها) یک دکمه تمام‌عرض نشان داده می‌شود که با کلیک، لینک تنظیم‌شده را در تب جدید مرورگر باز می‌کند. خاموش: دکمه مخفی است.' WHERE `key` = 'products_site_link_enabled'; UPDATE `settings` SET `description` = 'آدرس کامل سایت یا صفحه خارجی (مثلاً https://example.com). با کلیک روی دکمه در اپ کاربر، همین آدرس در تب جدید مرورگر باز می‌شود. اگر خالی باشد، با وجود روشن بودن سوییچ، دکمه کاری انجام نمی‌دهد.' WHERE `key` = 'products_site_link_url'; UPDATE `settings` SET `description` = 'متنی که روی دکمه لینک خارجی در صفحه خرید/فروش نمایش داده می‌شود. اگر خالی باشد، پیش‌فرض «ورود به سایت» همراه آیکون کره زمین نشان داده می‌شود.' WHERE `key` = 'products_site_link_text'; UPDATE `settings` SET `description` = 'روشن: روی کارت محصول برچسب خرید باز/بسته و فروش باز/بسته نمایش داده می‌شود. خاموش: مخفی است.' WHERE `key` = 'show_product_trade_status'; UPDATE `settings` SET `description` = 'روشن: روی کارت محصول دسته‌بندی، وضعیت تحویل فیزیکی و مقدار/وزن نمایش داده می‌شود. خاموش: مخفی است.' WHERE `key` = 'show_product_card_meta'; -UPDATE `settings` SET `description` = 'روشن: خارج از بازه «ساعت شروع» تا «ساعت پایان» معامله و نمایش قیمت بسته می‌شود. خاموش: محدودیت ساعتی سراسری اعمال نمی‌شود.' WHERE `key` = 'close_price'; +UPDATE `settings` SET `description` = 'سوییچ اصلی ساعت کاری. خاموش: محدودیت ساعت نیست — مظنه زنده یا با «نمایش آخرین قیمت ذخیره‌شده» آخرین قیمت، وگرنه «مظنه در دسترس نیست». روشن: داخل بازه همان منطق مظنه؛ خارج از بازه با «نمایش قیمت صفحه اصلی خارج از ساعت کاری» یا پیام «فروشگاه بسته است».' WHERE `key` = 'close_price'; UPDATE `settings` SET `description` = 'ساعت شروع فعالیت روزانه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال می‌شود (مثلاً ۰۹:۰۰).' WHERE `key` = 'startDay'; UPDATE `settings` SET `description` = 'ساعت پایان فعالیت روزانه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال می‌شود (مثلاً ۲۲:۰۰).' WHERE `key` = 'endDay'; UPDATE `settings` SET `description` = 'مبلغ اضافه (ریال) روی قیمت پایه برای سمت خرید. خالی یا صفر = بدون اختلاف.' WHERE `key` = 'buy_fee'; diff --git a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql index 82ae880..0588cd5 100644 --- a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql +++ b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql @@ -1359,8 +1359,8 @@ UPDATE `settings` SET `description` = 'روشن: نمودار قیمت از تا -- [super-admin can_edit=0] -- UPDATE `settings` SET `description` = 'آدرس پایه API سرویس تابلو آنلاین (بدون مسیر انتهایی). مثال: https://example.com/api' WHERE `key` = 'zaarbox_api_url'; UPDATE `settings` SET `description` = 'کلید یا UUID ردیف قیمت در پاسخ API تابلو. باید با محصول/ردیف موردنظر در سرویس خارجی یکسان باشد.' WHERE `key` = 'zaarbox_price_key'; -UPDATE `settings` SET `description` = 'روشن: اگر API قیمت در دسترس نباشد، آخرین قیمت ذخیره‌شده در دیتابیس به کاربر نشان داده می‌شود. خاموش: در قطعی API ممکن است قیمت صفر نمایش داده شود.' WHERE `key` = 'show_last_price'; -UPDATE `settings` SET `description` = 'روشن: وقتی فروشگاه خارج از ساعت کاری است، قیمت صفحه اصلی اپ همچنان نمایش داده می‌شود و پیام «فروشگاه بسته است» نشان داده نمی‌شود. خرید/فروش همچنان بسته می‌ماند. خاموش: مثل قبل، خارج از ساعت پیام بسته بودن و قیمت صفر نشان داده می‌شود.' WHERE `key` = 'show_home_price_when_closed'; +UPDATE `settings` SET `description` = 'وقتی مظنه زنده نیست (و فروشگاه از نظر ساعت بسته حساب نمی‌شود): روشن = آخرین قیمت ذخیره‌شده نشان داده شود. خاموش = پیام «مظنه در دسترس نیست».' WHERE `key` = 'show_last_price'; +UPDATE `settings` SET `description` = 'فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن است و الان خارج از بازه هستید. روشن: آخرین قیمت روی صفحه اصلی نشان داده می‌شود (معامله بسته می‌ماند). خاموش: پیام «فروشگاه بسته است».' WHERE `key` = 'show_home_price_when_closed'; UPDATE `settings` SET `description` = 'روشن: بعد از انتخاب محصول در صفحه خرید/فروش، متن توضیح محصول نمایش داده می‌شود. خاموش: توضیح نشان داده نمی‌شود.' WHERE `key` = 'show_product_details'; UPDATE `settings` SET `description` = 'روشن: در صفحه خرید/فروش (بالای تب دسته‌ها) یک دکمه تمام‌عرض نشان داده می‌شود که با کلیک، لینک تنظیم‌شده را در تب جدید مرورگر باز می‌کند. خاموش: دکمه مخفی است.' WHERE `key` = 'products_site_link_enabled'; UPDATE `settings` SET `description` = 'آدرس کامل سایت یا صفحه خارجی (مثلاً https://example.com). با کلیک روی دکمه در اپ کاربر، همین آدرس در تب جدید مرورگر باز می‌شود. اگر خالی باشد، با وجود روشن بودن سوییچ، دکمه کاری انجام نمی‌دهد.' WHERE `key` = 'products_site_link_url'; @@ -1376,7 +1376,7 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` = 'show_product_trade_status UPDATE `settings` SET `description` = 'روشن: روی کارت محصول دسته‌بندی، وضعیت تحویل فیزیکی و مقدار/وزن نمایش داده می‌شود. خاموش: مخفی است.' WHERE `key` = 'show_product_card_meta'; UPDATE `settings` SET `title_fa` = 'نمایش دسته و تحویل و مقدار روی کارت محصول' WHERE `key` = 'show_product_card_meta'; UPDATE `settings` SET `type` = 'switch' WHERE `key` = 'show_product_card_meta'; -UPDATE `settings` SET `description` = 'روشن: خارج از بازه «ساعت شروع» تا «ساعت پایان» معامله و نمایش قیمت بسته می‌شود. خاموش: محدودیت ساعتی سراسری اعمال نمی‌شود.' WHERE `key` = 'close_price'; +UPDATE `settings` SET `description` = 'سوییچ اصلی ساعت کاری. خاموش: محدودیت ساعت نیست — مظنه زنده یا با «نمایش آخرین قیمت ذخیره‌شده» آخرین قیمت، وگرنه «مظنه در دسترس نیست». روشن: داخل بازه همان منطق مظنه؛ خارج از بازه با «نمایش قیمت صفحه اصلی خارج از ساعت کاری» یا پیام «فروشگاه بسته است».' WHERE `key` = 'close_price'; UPDATE `settings` SET `description` = 'ساعت شروع فعالیت روزانه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال می‌شود (مثلاً ۰۹:۰۰).' WHERE `key` = 'startDay'; UPDATE `settings` SET `description` = 'ساعت پایان فعالیت روزانه. فقط وقتی «بستن خودکار خارج از ساعت کاری» روشن باشد اعمال می‌شود (مثلاً ۲۲:۰۰).' WHERE `key` = 'endDay'; UPDATE `settings` SET `description` = 'مبلغ اضافه (ریال) روی قیمت پایه برای سمت خرید. خالی یا صفر = بدون اختلاف.' WHERE `key` = 'buy_fee'; diff --git a/database/sql/settings_menu_and_show_to_user.sql b/database/sql/settings_menu_and_show_to_user.sql index f680d12..e24a24c 100644 --- a/database/sql/settings_menu_and_show_to_user.sql +++ b/database/sql/settings_menu_and_show_to_user.sql @@ -123,6 +123,7 @@ UPDATE `settings` SET `show_to_user` = 1 WHERE `key` IN ( 'referral_show', 'show_gold_ounce_price', 'show_home_price_when_closed', + 'show_last_price', 'show_product_card_meta', 'show_product_details', 'show_product_trade_status', @@ -137,6 +138,7 @@ UPDATE `settings` SET `show_to_user` = 1 WHERE `key` IN ( 'text_rule', 'tracking_systems', 'tracking_systems_enabled', + 'trading_hours_by_day', 'unit', 'wss_enabled', 'wss_path', diff --git a/tests/Unit/ConfigServiceTradingHoursTest.php b/tests/Unit/ConfigServiceTradingHoursTest.php index ff585c0..6dc5b0f 100644 --- a/tests/Unit/ConfigServiceTradingHoursTest.php +++ b/tests/Unit/ConfigServiceTradingHoursTest.php @@ -88,7 +88,7 @@ class ConfigServiceTradingHoursTest extends TestCase $this->assertTrue($cfg->isOutsideTradingHours($fridayNoon)); } - public function test_apply_global_trading_hours_zeros_payload_when_closed(): void + public function test_apply_global_trading_hours_marks_closed_without_hiding_status_when_home_price_off(): void { $weekly = json_encode([ 'mode' => 'per_day', @@ -100,6 +100,7 @@ class ConfigServiceTradingHoursTest extends TestCase $cfg = $this->cfg([ 'close_price' => 'true', 'show_home_price_when_closed' => 'false', + 'show_last_price' => 'false', 'startDay' => '08:30', 'endDay' => '21:30', 'trading_hours_by_day' => $weekly, @@ -108,7 +109,7 @@ class ConfigServiceTradingHoursTest extends TestCase $now = Carbon::create(2026, 8, 24, 21, 15, 0); Carbon::setTestNow($now); - $result = $cfg->applyGlobalTradingHoursToPayload([ + $result = $cfg->applyHomePriceDisplayState([ 'sell' => 100, 'buy' => 110, 'price' => 105, @@ -118,8 +119,91 @@ class ConfigServiceTradingHoursTest extends TestCase $this->assertTrue($result['closed']); $this->assertSame('closed', $result['status']); - $this->assertSame(0, $result['price']); Carbon::setTestNow(); } + + public function test_outside_hours_shows_last_price_when_home_price_when_closed_is_on(): void + { + $cfg = $this->cfg([ + 'close_price' => 'true', + 'show_home_price_when_closed' => 'true', + 'show_last_price' => 'false', + 'startDay' => '11:00', + 'endDay' => '20:30', + ]); + + $now = Carbon::create(2026, 8, 25, 2, 3, 0); + Carbon::setTestNow($now); + + $result = $cfg->applyHomePriceDisplayState([ + 'sell' => 168000000, + 'buy' => 168798112, + 'price' => 168798112, + 'closed' => false, + 'status' => 'open', + ]); + + $this->assertTrue($result['closed']); + $this->assertSame('open', $result['status']); + $this->assertSame(168798112, $result['buy']); + + Carbon::setTestNow(); + } + + public function test_close_price_off_with_no_quote_is_no_quote(): void + { + $cfg = $this->cfg([ + 'close_price' => 'false', + 'show_last_price' => 'false', + 'show_home_price_when_closed' => 'false', + 'startDay' => '11:00', + 'endDay' => '20:30', + ]); + + $result = $cfg->applyHomePriceDisplayState([ + 'sell' => 0, + 'buy' => 0, + 'price' => 0, + 'closed' => false, + ]); + + $this->assertFalse($result['closed']); + $this->assertSame('no_quote', $result['status']); + } + + public function test_inside_hours_with_quote_is_open(): void + { + $cfg = $this->cfg([ + 'close_price' => 'true', + 'show_last_price' => 'false', + 'show_home_price_when_closed' => 'false', + 'startDay' => '11:00', + 'endDay' => '20:30', + ]); + + $now = Carbon::create(2026, 8, 25, 12, 0, 0); + Carbon::setTestNow($now); + + $result = $cfg->applyHomePriceDisplayState([ + 'sell' => 100, + 'buy' => 110, + 'price' => 105, + ]); + + $this->assertFalse($result['closed']); + $this->assertSame('open', $result['status']); + + Carbon::setTestNow(); + } + + public function test_trading_hours_setting_keys(): void + { + $cfg = new ConfigService(); + + $this->assertTrue($cfg->isTradingHoursSettingKey('close_price')); + $this->assertTrue($cfg->isTradingHoursSettingKey('trading_hours_by_day')); + $this->assertTrue($cfg->isTradingHoursSettingKey('show_home_price_when_closed')); + $this->assertFalse($cfg->isTradingHoursSettingKey('name_fa')); + } }