2026-08-24 21:22:44 +03:30
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Tests\Unit;
|
|
|
|
|
|
|
|
|
|
use App\Services\ConfigService;
|
2026-08-25 03:01:07 +03:30
|
|
|
use App\Services\SuperAdminProgramSettingsOverlay;
|
2026-08-24 21:22:44 +03:30
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 02:29:00 +03:30
|
|
|
public function test_apply_global_trading_hours_marks_closed_without_hiding_status_when_home_price_off(): void
|
2026-08-24 21:22:44 +03:30
|
|
|
{
|
|
|
|
|
$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',
|
2026-08-25 02:29:00 +03:30
|
|
|
'show_last_price' => 'false',
|
2026-08-24 21:22:44 +03:30
|
|
|
'startDay' => '08:30',
|
|
|
|
|
'endDay' => '21:30',
|
|
|
|
|
'trading_hours_by_day' => $weekly,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$now = Carbon::create(2026, 8, 24, 21, 15, 0);
|
|
|
|
|
Carbon::setTestNow($now);
|
|
|
|
|
|
2026-08-25 02:29:00 +03:30
|
|
|
$result = $cfg->applyHomePriceDisplayState([
|
2026-08-24 21:22:44 +03:30
|
|
|
'sell' => 100,
|
|
|
|
|
'buy' => 110,
|
|
|
|
|
'price' => 105,
|
|
|
|
|
'closed' => false,
|
|
|
|
|
'status' => 'open',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertTrue($result['closed']);
|
|
|
|
|
$this->assertSame('closed', $result['status']);
|
2026-08-25 03:01:07 +03:30
|
|
|
$this->assertSame(0, $result['buy']);
|
|
|
|
|
$this->assertSame(0, $result['sell']);
|
|
|
|
|
$this->assertSame(0, $result['price']);
|
2026-08-24 21:22:44 +03:30
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
}
|
2026-08-25 02:29:00 +03:30
|
|
|
|
|
|
|
|
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,
|
2026-08-25 03:01:07 +03:30
|
|
|
'price_source' => 'montzar',
|
|
|
|
|
'montzar' => 105,
|
2026-08-25 02:29:00 +03:30
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$this->assertFalse($result['closed']);
|
|
|
|
|
$this->assertSame('open', $result['status']);
|
2026-08-25 03:01:07 +03:30
|
|
|
$this->assertSame(110, $result['buy']);
|
2026-08-25 02:29:00 +03:30
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 03:01:07 +03:30
|
|
|
/**
|
|
|
|
|
* سناریوی گزارششده: مظنه زنده قطع است و قیمت از آخرین ردیف 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',
|
|
|
|
|
]));
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-25 02:29:00 +03:30
|
|
|
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'));
|
|
|
|
|
}
|
2026-08-25 03:01:07 +03:30
|
|
|
|
|
|
|
|
/** کلیدهای ساعت کاری باید مالکیت 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} نباید سوپرادمین-اونر باشد"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-24 21:22:44 +03:30
|
|
|
}
|