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:
2026-08-24 21:22:44 +03:30
co-authored by Cursor
parent d2469dc253
commit 7e8979d78d
9 changed files with 437 additions and 27 deletions
@@ -8,8 +8,11 @@ use Illuminate\Support\Facades\Log;
/**
* خواندن تنظیمات پیشرفته از سوپرادمین (منبع حقیقت can_edit=0).
*
* Public اول (دامنه‌محور، بدون وابستگی به HMAC): GET {origin}/api/public/program-settings?domain=
* 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
{
@@ -21,6 +24,11 @@ class SuperAdminProgramSettingsClient
public function fetchItems(): ?array
{
$public = $this->fetchPublic();
if ($public !== null) {
return $public;
}
if ($this->canAuth()) {
$items = $this->fetchRemoteFull();
if ($items !== null) {
@@ -28,7 +36,7 @@ class SuperAdminProgramSettingsClient
}
}
return $this->fetchPublic();
return null;
}
private function canAuth(): bool
@@ -79,31 +87,34 @@ class SuperAdminProgramSettingsClient
*/
private function fetchPublic(): ?array
{
$domain = $this->domain();
if ($domain === '') {
$domains = $this->domainCandidates();
if ($domains === []) {
return null;
}
foreach ($this->attemptOrigins() as $origin) {
try {
$response = Http::acceptJson()
->withHeaders(['Cache-Control' => 'no-store'])
->timeout(6)
->get($origin.self::PUBLIC_PATH, ['domain' => $domain]);
foreach ($domains as $domain) {
foreach ($this->attemptOrigins() as $origin) {
try {
$response = Http::acceptJson()
->withHeaders(['Cache-Control' => 'no-store'])
->timeout(6)
->get($origin.self::PUBLIC_PATH, ['domain' => $domain]);
if (! $response->successful() || ! $this->isJsonResponse($response)) {
continue;
}
if (! $response->successful() || ! $this->isJsonResponse($response)) {
continue;
}
$items = $this->extractItems($response->json());
if ($items !== null) {
return $items;
$items = $this->extractItems($response->json());
if ($items !== null) {
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();
if ($secret !== '') {
$domain = $this->domain();
$domain = $this->domainCandidates()[0] ?? '';
$timestamp = (string) time();
$payload = $timestamp.'.'.strtoupper($method).'.'.$canonicalPath.'.'.$domain;
$signature = hash_hmac('sha256', $payload, $secret);
@@ -221,16 +232,50 @@ class SuperAdminProgramSettingsClient
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', ''));
if ($override !== '') {
return $this->normalizeHost($override);
$candidates[] = $this->normalizeHost($override);
}
$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