fix(settings): honor per-day trading hours and refresh SuperAdmin overlay
Use trading_hours_by_day for close_price gates, prefer public SuperAdmin fetch, and add refresh endpoint/command so stale last-good cache no longer keeps switches like enable_automation off. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\SuperAdminProgramSettingsOverlay;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class SuperAdminProgramSettingsRefreshCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'superadmin:refresh-program-settings
|
||||||
|
{--clear-only : فقط کش last-good را پاک کن}';
|
||||||
|
|
||||||
|
protected $description = 'کش overlay تنظیمات سوپرادمین را پاک/تازه میکند (مثلاً بعد از تغییر enable_automation)';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$overlay = app(SuperAdminProgramSettingsOverlay::class);
|
||||||
|
|
||||||
|
if ($this->option('clear-only')) {
|
||||||
|
$overlay->flushCache();
|
||||||
|
$this->line('cleared SuperAdmin program-settings cache');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $overlay->refreshFromSuperAdmin();
|
||||||
|
if (! ($result['ok'] ?? false)) {
|
||||||
|
$this->error('fetch from SuperAdmin failed — cache cleared; next HTTP request will retry');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('fetched '.(int) ($result['count'] ?? 0).' setting(s) from SuperAdmin');
|
||||||
|
if (($result['enable_automation'] ?? null) !== null) {
|
||||||
|
$this->line('enable_automation='.$result['enable_automation']);
|
||||||
|
} else {
|
||||||
|
$this->warn('enable_automation missing in SuperAdmin response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,31 @@ class SettingsController extends Controller
|
|||||||
return response()->json($rows);
|
return response()->json($rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* پاک کردن کش overlay و خواندن دوباره از سوپرادمین
|
||||||
|
* (سوییچهایی مثل enable_automation بعد از تغییر در پنل سوپرادمین).
|
||||||
|
*/
|
||||||
|
public function refreshSuperAdminOverlay()
|
||||||
|
{
|
||||||
|
$result = app(SuperAdminProgramSettingsOverlay::class)->refreshFromSuperAdmin();
|
||||||
|
|
||||||
|
if (! ($result['ok'] ?? false)) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'ارتباط با سوپرادمین برقرار نشد؛ کش پاک شد و در درخواست بعدی دوباره تلاش میشود',
|
||||||
|
'ok' => false,
|
||||||
|
'count' => 0,
|
||||||
|
], 502);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'تنظیمات پیشرفته از سوپرادمین همگام شد',
|
||||||
|
'ok' => true,
|
||||||
|
'count' => (int) ($result['count'] ?? 0),
|
||||||
|
'enable_automation' => $result['enable_automation'] ?? null,
|
||||||
|
'source' => $result['source'] ?? 'superadmin',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function edit(Request $request)
|
public function edit(Request $request)
|
||||||
{
|
{
|
||||||
$key = trim((string) $request->key);
|
$key = trim((string) $request->key);
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ class ReportController extends Controller
|
|||||||
$cached = $cache->get();
|
$cached = $cache->get();
|
||||||
if ($cached !== null) {
|
if ($cached !== null) {
|
||||||
$cached['gold_ounce'] = $this->resolveGoldOuncePayload();
|
$cached['gold_ounce'] = $this->resolveGoldOuncePayload();
|
||||||
|
$cached = $config->applyGlobalTradingHoursToPayload($cached);
|
||||||
|
|
||||||
return $this->personalizeLastPriceForCustomer($cached);
|
return $this->personalizeLastPriceForCustomer($cached);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -638,6 +638,86 @@ class ConfigService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{mode: 'uniform'|'per_day', days: array<string, array{enabled: bool, start: string, end: string}>}|null
|
||||||
|
*/
|
||||||
|
public function tradingHoursByDay(): ?array
|
||||||
|
{
|
||||||
|
$raw = $this->getString('trading_hours_by_day', '');
|
||||||
|
if ($raw === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$parsed = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_array($parsed) || ! isset($parsed['days']) || ! is_array($parsed['days'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = ($parsed['mode'] ?? 'uniform') === 'per_day' ? 'per_day' : 'uniform';
|
||||||
|
$defaults = $this->tradingHourBounds();
|
||||||
|
$dayKeys = ['sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri'];
|
||||||
|
$days = [];
|
||||||
|
|
||||||
|
foreach ($dayKeys as $key) {
|
||||||
|
$row = is_array($parsed['days'][$key] ?? null) ? $parsed['days'][$key] : [];
|
||||||
|
$days[$key] = [
|
||||||
|
'enabled' => ($row['enabled'] ?? true) !== false,
|
||||||
|
'start' => $this->normalizeTradingTime((string) ($row['start'] ?? $defaults['start']), $defaults['start']),
|
||||||
|
'end' => $this->normalizeTradingTime((string) ($row['end'] ?? $defaults['end']), $defaults['end']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'mode' => $mode,
|
||||||
|
'days' => $days,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function weekdayKey(Carbon $now): string
|
||||||
|
{
|
||||||
|
return match ($now->dayOfWeek) {
|
||||||
|
Carbon::SATURDAY => 'sat',
|
||||||
|
Carbon::SUNDAY => 'sun',
|
||||||
|
Carbon::MONDAY => 'mon',
|
||||||
|
Carbon::TUESDAY => 'tue',
|
||||||
|
Carbon::WEDNESDAY => 'wed',
|
||||||
|
Carbon::THURSDAY => 'thu',
|
||||||
|
Carbon::FRIDAY => 'fri',
|
||||||
|
default => 'sat',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* بازهٔ مؤثر ساعت کاری برای لحظهٔ فعلی.
|
||||||
|
*
|
||||||
|
* @return array{enabled: bool, start: string, end: string}
|
||||||
|
*/
|
||||||
|
public function effectiveTradingBounds(?Carbon $now = null): array
|
||||||
|
{
|
||||||
|
$now = $now ?? Carbon::now();
|
||||||
|
$weekly = $this->tradingHoursByDay();
|
||||||
|
|
||||||
|
if ($weekly !== null && $weekly['mode'] === 'per_day') {
|
||||||
|
$day = $weekly['days'][$this->weekdayKey($now)] ?? null;
|
||||||
|
if ($day !== null) {
|
||||||
|
return $day;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bounds = $this->tradingHourBounds();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => true,
|
||||||
|
'start' => $bounds['start'],
|
||||||
|
'end' => $bounds['end'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeTradingTime(string $value, string $default): string
|
private function normalizeTradingTime(string $value, string $default): string
|
||||||
{
|
{
|
||||||
$value = trim($value);
|
$value = trim($value);
|
||||||
@@ -659,7 +739,12 @@ class ConfigService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$now = $now ?? Carbon::now();
|
$now = $now ?? Carbon::now();
|
||||||
$bounds = $this->tradingHourBounds();
|
$bounds = $this->effectiveTradingBounds($now);
|
||||||
|
|
||||||
|
if (!$bounds['enabled']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[$startHour, $startMinute] = array_map('intval', explode(':', $bounds['start']));
|
[$startHour, $startMinute] = array_map('intval', explode(':', $bounds['start']));
|
||||||
[$endHour, $endMinute] = array_map('intval', explode(':', $bounds['end']));
|
[$endHour, $endMinute] = array_map('intval', explode(':', $bounds['end']));
|
||||||
|
|
||||||
@@ -669,6 +754,39 @@ class ConfigService
|
|||||||
return !$now->between($start, $end);
|
return !$now->between($start, $end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* گیت ساعت کاری روی payload کش/SSE — وقتی close_price روشن است.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function applyGlobalTradingHoursToPayload(array $payload): array
|
||||||
|
{
|
||||||
|
if (!$this->isClosePriceEnabled() || !$this->isOutsideTradingHours()) {
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->shouldShowHomePriceWhenClosed()) {
|
||||||
|
$payload['closed'] = true;
|
||||||
|
$payload['status'] = 'closed';
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function syncAppClosedFlag(?Carbon $now = null): bool
|
public function syncAppClosedFlag(?Carbon $now = null): bool
|
||||||
{
|
{
|
||||||
$closed = $this->isOutsideTradingHours($now);
|
$closed = $this->isOutsideTradingHours($now);
|
||||||
@@ -710,7 +828,13 @@ class ConfigService
|
|||||||
|
|
||||||
public function tradingClosedMessage(?Carbon $now = null): string
|
public function tradingClosedMessage(?Carbon $now = null): string
|
||||||
{
|
{
|
||||||
$bounds = $this->tradingHourBounds();
|
$now = $now ?? Carbon::now();
|
||||||
|
$bounds = $this->effectiveTradingBounds($now);
|
||||||
|
|
||||||
|
if (!$bounds['enabled']) {
|
||||||
|
return 'فروشگاه امروز تعطیل است.';
|
||||||
|
}
|
||||||
|
|
||||||
$start = $this->formatTradingTimeForDisplay($bounds['start']);
|
$start = $this->formatTradingTimeForDisplay($bounds['start']);
|
||||||
$end = $this->formatTradingTimeForDisplay($bounds['end']);
|
$end = $this->formatTradingTimeForDisplay($bounds['end']);
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ class LivePriceCacheService
|
|||||||
: ['sell' => $payload['sell'] ?? 0, 'buy' => $payload['buy'] ?? 0];
|
: ['sell' => $payload['sell'] ?? 0, 'buy' => $payload['buy'] ?? 0];
|
||||||
$payload['last_historical'] = $display->resolveHistoricalBasePrice();
|
$payload['last_historical'] = $display->resolveHistoricalBasePrice();
|
||||||
|
|
||||||
|
$payload = $config->applyGlobalTradingHoursToPayload($payload);
|
||||||
|
|
||||||
return app(ZaarboxPriceService::class)->attachGoldOunceToPayload($payload);
|
return app(ZaarboxPriceService::class)->attachGoldOunceToPayload($payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ 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
|
* Full (با HMAC/Bearer): GET {origin}/api/remote/program-settings
|
||||||
* Public: GET {origin}/api/public/program-settings?domain=
|
*
|
||||||
|
* Public برای همین ۱۴ کلید مالکیت سوپرادمین کافی است؛ اگر remote اول باشد و replica عقب بماند،
|
||||||
|
* سوییچهایی مثل enable_automation روی false کهنه میمانند.
|
||||||
*/
|
*/
|
||||||
class SuperAdminProgramSettingsClient
|
class SuperAdminProgramSettingsClient
|
||||||
{
|
{
|
||||||
@@ -21,6 +24,11 @@ class SuperAdminProgramSettingsClient
|
|||||||
|
|
||||||
public function fetchItems(): ?array
|
public function fetchItems(): ?array
|
||||||
{
|
{
|
||||||
|
$public = $this->fetchPublic();
|
||||||
|
if ($public !== null) {
|
||||||
|
return $public;
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->canAuth()) {
|
if ($this->canAuth()) {
|
||||||
$items = $this->fetchRemoteFull();
|
$items = $this->fetchRemoteFull();
|
||||||
if ($items !== null) {
|
if ($items !== null) {
|
||||||
@@ -28,7 +36,7 @@ class SuperAdminProgramSettingsClient
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->fetchPublic();
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function canAuth(): bool
|
private function canAuth(): bool
|
||||||
@@ -79,31 +87,34 @@ class SuperAdminProgramSettingsClient
|
|||||||
*/
|
*/
|
||||||
private function fetchPublic(): ?array
|
private function fetchPublic(): ?array
|
||||||
{
|
{
|
||||||
$domain = $this->domain();
|
$domains = $this->domainCandidates();
|
||||||
if ($domain === '') {
|
if ($domains === []) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->attemptOrigins() as $origin) {
|
foreach ($domains as $domain) {
|
||||||
try {
|
foreach ($this->attemptOrigins() as $origin) {
|
||||||
$response = Http::acceptJson()
|
try {
|
||||||
->withHeaders(['Cache-Control' => 'no-store'])
|
$response = Http::acceptJson()
|
||||||
->timeout(6)
|
->withHeaders(['Cache-Control' => 'no-store'])
|
||||||
->get($origin.self::PUBLIC_PATH, ['domain' => $domain]);
|
->timeout(6)
|
||||||
|
->get($origin.self::PUBLIC_PATH, ['domain' => $domain]);
|
||||||
|
|
||||||
if (! $response->successful() || ! $this->isJsonResponse($response)) {
|
if (! $response->successful() || ! $this->isJsonResponse($response)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$items = $this->extractItems($response->json());
|
$items = $this->extractItems($response->json());
|
||||||
if ($items !== null) {
|
if ($items !== null) {
|
||||||
return $items;
|
return $items;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('SuperAdmin public program-settings exception', [
|
||||||
|
'origin' => $origin,
|
||||||
|
'domain' => $domain,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::warning('SuperAdmin public program-settings exception', [
|
|
||||||
'origin' => $origin,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +200,7 @@ class SuperAdminProgramSettingsClient
|
|||||||
{
|
{
|
||||||
$secret = $this->sharedSecret();
|
$secret = $this->sharedSecret();
|
||||||
if ($secret !== '') {
|
if ($secret !== '') {
|
||||||
$domain = $this->domain();
|
$domain = $this->domainCandidates()[0] ?? '';
|
||||||
$timestamp = (string) time();
|
$timestamp = (string) time();
|
||||||
$payload = $timestamp.'.'.strtoupper($method).'.'.$canonicalPath.'.'.$domain;
|
$payload = $timestamp.'.'.strtoupper($method).'.'.$canonicalPath.'.'.$domain;
|
||||||
$signature = hash_hmac('sha256', $payload, $secret);
|
$signature = hash_hmac('sha256', $payload, $secret);
|
||||||
@@ -221,16 +232,50 @@ class SuperAdminProgramSettingsClient
|
|||||||
return trim((string) config('services.superadmin.remote_token', ''));
|
return trim((string) config('services.superadmin.remote_token', ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function domain(): string
|
/**
|
||||||
|
* دامنههای کاندید برای resolve نسخه در سوپرادمین.
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function domainCandidates(): array
|
||||||
{
|
{
|
||||||
|
$candidates = [];
|
||||||
|
|
||||||
$override = trim((string) config('services.superadmin.sms_domain', ''));
|
$override = trim((string) config('services.superadmin.sms_domain', ''));
|
||||||
if ($override !== '') {
|
if ($override !== '') {
|
||||||
return $this->normalizeHost($override);
|
$candidates[] = $this->normalizeHost($override);
|
||||||
}
|
}
|
||||||
|
|
||||||
$fromApp = parse_url((string) config('app.url'), PHP_URL_HOST);
|
$fromApp = parse_url((string) config('app.url'), PHP_URL_HOST);
|
||||||
|
if (is_string($fromApp) && $fromApp !== '') {
|
||||||
|
$candidates[] = $this->normalizeHost($fromApp);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->normalizeHost(is_string($fromApp) ? $fromApp : '');
|
try {
|
||||||
|
$request = request();
|
||||||
|
if ($request) {
|
||||||
|
$candidates[] = $this->normalizeHost((string) $request->getHost());
|
||||||
|
$forwarded = (string) $request->headers->get('X-Forwarded-Host', '');
|
||||||
|
if ($forwarded !== '') {
|
||||||
|
$candidates[] = $this->normalizeHost(explode(',', $forwarded)[0] ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// CLI / بدون request
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach ($candidates as $host) {
|
||||||
|
if ($host === '' || in_array($host, $out, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (in_array($host, ['localhost', '127.0.0.1', '::1'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeHost(string $value): string
|
private function normalizeHost(string $value): string
|
||||||
|
|||||||
@@ -71,6 +71,46 @@ class SuperAdminProgramSettingsOverlay
|
|||||||
|
|
||||||
private static bool $loaded = false;
|
private static bool $loaded = false;
|
||||||
|
|
||||||
|
/** پاک کردن کش last-good و memo درخواست — برای دکمهٔ همگامسازی تنظیمات پیشرفته */
|
||||||
|
public function flushCache(): void
|
||||||
|
{
|
||||||
|
self::$memo = null;
|
||||||
|
self::$loaded = false;
|
||||||
|
try {
|
||||||
|
Cache::forget(self::LAST_GOOD_CACHE_KEY);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// کش در دسترس نیست
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* کش را پاک میکند و دوباره از سوپرادمین میخواند.
|
||||||
|
*
|
||||||
|
* @return array{ok: bool, count: int, enable_automation: string|null, source: string}
|
||||||
|
*/
|
||||||
|
public function refreshFromSuperAdmin(): array
|
||||||
|
{
|
||||||
|
$this->flushCache();
|
||||||
|
$map = $this->catalogMap();
|
||||||
|
if ($map === null) {
|
||||||
|
return [
|
||||||
|
'ok' => false,
|
||||||
|
'count' => 0,
|
||||||
|
'enable_automation' => null,
|
||||||
|
'source' => 'unavailable',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$auto = $map['enable_automation']['value'] ?? null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'count' => count($map),
|
||||||
|
'enable_automation' => $auto === null || $auto === '' ? null : (string) $auto,
|
||||||
|
'source' => 'superadmin',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* همهٔ کلیدهای سوپرادمین (حتی value خالی). null = سوپرادمین در دسترس نبود.
|
* همهٔ کلیدهای سوپرادمین (حتی value خالی). null = سوپرادمین در دسترس نبود.
|
||||||
*
|
*
|
||||||
@@ -122,6 +162,11 @@ class SuperAdminProgramSettingsOverlay
|
|||||||
$this->rememberLastGood($items);
|
$this->rememberLastGood($items);
|
||||||
} else {
|
} else {
|
||||||
$items = $this->lastGoodItems();
|
$items = $this->lastGoodItems();
|
||||||
|
if ($items !== null) {
|
||||||
|
\Illuminate\Support\Facades\Log::warning('SuperAdmin program-settings using last-good cache', [
|
||||||
|
'count' => count($items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($items === null) {
|
if ($items === null) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Route::middleware('auth:user')->group(function (){
|
|||||||
Route::get('get_settings',[SettingsController::class, 'index']);
|
Route::get('get_settings',[SettingsController::class, 'index']);
|
||||||
Route::post('get_settings',[SettingsController::class, 'edit']);
|
Route::post('get_settings',[SettingsController::class, 'edit']);
|
||||||
Route::get('settings/table',[SettingsController::class, 'localTable']);
|
Route::get('settings/table',[SettingsController::class, 'localTable']);
|
||||||
|
Route::post('settings/refresh-superadmin', [SettingsController::class, 'refreshSuperAdminOverlay']);
|
||||||
|
|
||||||
//------------balence
|
//------------balence
|
||||||
Route::post('getBalance', [DashboardController::class, 'getBalance']);
|
Route::post('getBalance', [DashboardController::class, 'getBalance']);
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Services\ConfigService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Mockery;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ConfigServiceTradingHoursTest extends TestCase
|
||||||
|
{
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
Mockery::close();
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $values */
|
||||||
|
private function cfg(array $values): ConfigService
|
||||||
|
{
|
||||||
|
$cfg = Mockery::mock(ConfigService::class)->makePartial();
|
||||||
|
$cfg->shouldReceive('get')->andReturnUsing(function (string $key, $default = null) use ($values) {
|
||||||
|
return array_key_exists($key, $values) ? $values[$key] : $default;
|
||||||
|
});
|
||||||
|
$cfg->shouldReceive('getString')->andReturnUsing(function (string $key, string $default = '') use ($values) {
|
||||||
|
$value = $values[$key] ?? $default;
|
||||||
|
|
||||||
|
return trim((string) $value);
|
||||||
|
});
|
||||||
|
$cfg->shouldReceive('getBool')->andReturnUsing(function (string $key, bool $default = false) use ($values) {
|
||||||
|
if (! array_key_exists($key, $values)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter_var($values[$key], FILTER_VALIDATE_BOOLEAN);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_per_day_schedule_closes_before_global_end_day(): void
|
||||||
|
{
|
||||||
|
$weekly = json_encode([
|
||||||
|
'mode' => 'per_day',
|
||||||
|
'days' => [
|
||||||
|
'sat' => ['enabled' => true, 'start' => '08:30:00', 'end' => '21:30:00'],
|
||||||
|
'sun' => ['enabled' => true, 'start' => '08:30:00', 'end' => '21:30:00'],
|
||||||
|
'mon' => ['enabled' => true, 'start' => '08:30:00', 'end' => '21:00:00'],
|
||||||
|
'tue' => ['enabled' => true, 'start' => '08:30:00', 'end' => '21:30:00'],
|
||||||
|
'wed' => ['enabled' => true, 'start' => '08:30:00', 'end' => '21:30:00'],
|
||||||
|
'thu' => ['enabled' => true, 'start' => '08:30:00', 'end' => '21:30:00'],
|
||||||
|
'fri' => ['enabled' => true, 'start' => '08:30:00', 'end' => '21:30:00'],
|
||||||
|
],
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$cfg = $this->cfg([
|
||||||
|
'close_price' => 'true',
|
||||||
|
'startDay' => '08:30',
|
||||||
|
'endDay' => '21:30',
|
||||||
|
'trading_hours_by_day' => $weekly,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$monday2115 = Carbon::create(2026, 8, 24, 21, 15, 0);
|
||||||
|
$monday2059 = Carbon::create(2026, 8, 24, 20, 59, 0);
|
||||||
|
|
||||||
|
$this->assertTrue($cfg->isOutsideTradingHours($monday2115));
|
||||||
|
$this->assertFalse($cfg->isOutsideTradingHours($monday2059));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_disabled_day_is_closed_all_day(): void
|
||||||
|
{
|
||||||
|
$weekly = json_encode([
|
||||||
|
'mode' => 'per_day',
|
||||||
|
'days' => [
|
||||||
|
'fri' => ['enabled' => false, 'start' => '08:30:00', 'end' => '21:30:00'],
|
||||||
|
],
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$cfg = $this->cfg([
|
||||||
|
'close_price' => 'true',
|
||||||
|
'startDay' => '08:30',
|
||||||
|
'endDay' => '21:30',
|
||||||
|
'trading_hours_by_day' => $weekly,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fridayNoon = Carbon::create(2026, 8, 28, 12, 0, 0);
|
||||||
|
|
||||||
|
$this->assertTrue($cfg->isOutsideTradingHours($fridayNoon));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_apply_global_trading_hours_zeros_payload_when_closed(): void
|
||||||
|
{
|
||||||
|
$weekly = json_encode([
|
||||||
|
'mode' => 'per_day',
|
||||||
|
'days' => [
|
||||||
|
'mon' => ['enabled' => true, 'start' => '08:30:00', 'end' => '21:00:00'],
|
||||||
|
],
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
$cfg = $this->cfg([
|
||||||
|
'close_price' => 'true',
|
||||||
|
'show_home_price_when_closed' => 'false',
|
||||||
|
'startDay' => '08:30',
|
||||||
|
'endDay' => '21:30',
|
||||||
|
'trading_hours_by_day' => $weekly,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$now = Carbon::create(2026, 8, 24, 21, 15, 0);
|
||||||
|
Carbon::setTestNow($now);
|
||||||
|
|
||||||
|
$result = $cfg->applyGlobalTradingHoursToPayload([
|
||||||
|
'sell' => 100,
|
||||||
|
'buy' => 110,
|
||||||
|
'price' => 105,
|
||||||
|
'closed' => false,
|
||||||
|
'status' => 'open',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertTrue($result['closed']);
|
||||||
|
$this->assertSame('closed', $result['status']);
|
||||||
|
$this->assertSame(0, $result['price']);
|
||||||
|
|
||||||
|
Carbon::setTestNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user