2026-07-28 18:23:15 +03:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
|
|
use App\Jobs\SendAutoSmsForDealJob;
|
|
|
|
|
|
use App\Models\ClientSmsAutoRule;
|
|
|
|
|
|
use App\Models\ClientSmsMessage;
|
|
|
|
|
|
use App\Models\ClientSmsOptout;
|
|
|
|
|
|
use App\Models\Deal;
|
|
|
|
|
|
use App\Models\Tenant;
|
|
|
|
|
|
use App\Services\Advertising\AdWalletService;
|
|
|
|
|
|
use App\Services\ClientSms\ClientSmsPricing;
|
|
|
|
|
|
use App\Services\ClientSms\ClientSmsRecipientSelector;
|
|
|
|
|
|
use App\Services\ClientSms\SmsQuietHours;
|
2026-07-28 19:28:14 +03:00
|
|
|
|
use App\Services\DaData\DaDataBudgetGuard;
|
|
|
|
|
|
use App\Services\DaData\DaDataPhoneClient;
|
2026-07-28 18:23:15 +03:00
|
|
|
|
use Carbon\CarbonImmutable;
|
|
|
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
|
|
use Illuminate\Support\Carbon;
|
2026-07-28 19:28:14 +03:00
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
|
|
use Illuminate\Support\Facades\Http;
|
2026-07-28 18:23:15 +03:00
|
|
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Строка листа 3.6: лид пришёл ночью — авто-СМС уходит не ночью, а утром, и ровно
|
|
|
|
|
|
* одно. Плюс вторая половина строки 3.1 (окно действует и для авто-СМС).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Пары тестов обязательны (урок В-54): прогон «ночью не ушло» сам по себе ничего не
|
|
|
|
|
|
* доказывает — ноль мог получиться от поломки. Рядом стоит прогон «а в полдень ушло»,
|
|
|
|
|
|
* где всё то же самое, кроме часа.
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 Регион у сделки может отсутствовать вовсе (сделка заведена руками). Москву при
|
|
|
|
|
|
* этом НЕ подставляем — владелец это отменил (В-85): пока не знаем, где живёт человек,
|
|
|
|
|
|
* СМС не уходит. Но и дверь не закрываем сразу (В-99): пишем причину только через
|
|
|
|
|
|
* сутки, до тех пор джоб просто переставляет себя.
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🪤 Код субъекта здесь НЕ автомобильный (конституционный порядок): Москва — 82,
|
|
|
|
|
|
* Камчатка — 27. Ошибиться легко, и тест тогда молча проверяет не тот пояс.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Помощники с префиксом autoWin* — имена функций в Pest ГЛОБАЛЬНЫЕ.
|
|
|
|
|
|
* Синтетические номера 7999… — реальные НИКОГДА.
|
|
|
|
|
|
*/
|
|
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
|
|
|
|
|
|
|
|
beforeEach(function () {
|
|
|
|
|
|
config(['services.sms.sandbox' => true]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** Правило авто-СМС тенанта (одно на тенанта — UNIQUE tenant_id). */
|
|
|
|
|
|
function autoWinRule(int $tenantId): ClientSmsAutoRule
|
|
|
|
|
|
{
|
|
|
|
|
|
return ClientSmsAutoRule::create([
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
|
'body' => 'Здравствуйте!',
|
|
|
|
|
|
'sender_name' => 'liderra.ru',
|
|
|
|
|
|
'updated_by' => null,
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Сделка без срабатывания DealSmsObserver — чтобы в тесте бежал РОВНО один явный
|
|
|
|
|
|
* вызов джоба. 🪤 Регион живёт в subject_code (1..89), НЕ в region_code (В-82);
|
2026-07-28 19:28:14 +03:00
|
|
|
|
* Москва — 82, Камчатка — 27.
|
2026-07-28 18:23:15 +03:00
|
|
|
|
*/
|
|
|
|
|
|
function autoWinDeal(int $tenantId, ?int $subjectCode, ?CarbonImmutable $receivedAt = null): Deal
|
|
|
|
|
|
{
|
|
|
|
|
|
return Deal::withoutEvents(fn () => Deal::factory()->create([
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'phone' => '79990000001',
|
|
|
|
|
|
'phone_operator' => 'МТС',
|
|
|
|
|
|
'subject_code' => $subjectCode,
|
|
|
|
|
|
'received_at' => $receivedAt ?? CarbonImmutable::now(),
|
|
|
|
|
|
]));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Прямой вызов джоба (как AutoSmsTest) — обходит afterCommit-отсрочку RefreshDatabase. */
|
2026-07-28 19:28:14 +03:00
|
|
|
|
function autoWinRunJob(int $dealId, int $tenantId, ?int $tz = null, bool $asked = false): void
|
2026-07-28 18:23:15 +03:00
|
|
|
|
{
|
2026-07-28 19:28:14 +03:00
|
|
|
|
(new SendAutoSmsForDealJob($dealId, $tenantId, $tz, $asked))->handle(
|
2026-07-28 18:23:15 +03:00
|
|
|
|
app(ClientSmsRecipientSelector::class),
|
|
|
|
|
|
app(ClientSmsPricing::class),
|
|
|
|
|
|
app(AdWalletService::class),
|
|
|
|
|
|
app(SmsQuietHours::class),
|
2026-07-28 19:28:14 +03:00
|
|
|
|
app(DaDataPhoneClient::class),
|
|
|
|
|
|
app(DaDataBudgetGuard::class),
|
2026-07-28 18:23:15 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 19:28:14 +03:00
|
|
|
|
/** Включает ДаДату и задаёт её ответ про пояс (заглушка — живой ключ не трогаем). */
|
|
|
|
|
|
function autoWinDaData(?string $timezone): void
|
|
|
|
|
|
{
|
|
|
|
|
|
config(['services.dadata.api_key' => 'test-key', 'services.dadata.secret' => 'test-secret']);
|
|
|
|
|
|
Cache::flush(); // чистый дневной бюджет
|
|
|
|
|
|
|
|
|
|
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([[
|
|
|
|
|
|
'qc' => 0,
|
|
|
|
|
|
'provider' => 'МТС',
|
|
|
|
|
|
'timezone' => $timezone,
|
|
|
|
|
|
]], 200)]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 18:23:15 +03:00
|
|
|
|
/** Момент UTC по человеческой записи. */
|
|
|
|
|
|
function autoWinUtc(string $iso): CarbonImmutable
|
|
|
|
|
|
{
|
|
|
|
|
|
return CarbonImmutable::parse($iso, 'UTC');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
it('ночной лид: в журнале пусто, а джоб переставлен на утро того же места', function () {
|
|
|
|
|
|
// 23:00 UTC = 02:00 ночи в Москве (пояс +3).
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-03 23:00:00'));
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: 82);
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
// Ничего не записано: с номером ещё НИЧЕГО не произошло, а запись «ждём утра»
|
|
|
|
|
|
// навсегда закрыла бы этому лиду дорогу (идемпотентность — по наличию записи).
|
|
|
|
|
|
expect(ClientSmsMessage::where('tenant_id', $tenant->id)->count())->toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 10:00 по Москве = 07:00 UTC того же дня.
|
|
|
|
|
|
Queue::assertPushed(
|
|
|
|
|
|
SendAutoSmsForDealJob::class,
|
|
|
|
|
|
fn (SendAutoSmsForDealJob $job) => $job->dealId === (int) $deal->id
|
|
|
|
|
|
&& $job->delay !== null
|
|
|
|
|
|
&& CarbonImmutable::parse($job->delay)->equalTo(autoWinUtc('2026-08-04 07:00:00')),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('КОНТРОЛЬ: тот же лид в полдень уходит сразу и никуда не переставляется', function () {
|
|
|
|
|
|
// 09:00 UTC = 12:00 дня в Москве — окно открыто.
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-04 09:00:00'));
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: 82);
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
expect(ClientSmsMessage::where('tenant_id', $tenant->id)->count())->toBe(1)
|
|
|
|
|
|
->and(ClientSmsMessage::where('tenant_id', $tenant->id)->first()->status)
|
|
|
|
|
|
->toBe(ClientSmsMessage::STATUS_FAKE_SENT);
|
|
|
|
|
|
|
|
|
|
|
|
Queue::assertNothingPushed();
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('утро наступило: отложенный джоб отрабатывает и шлёт РОВНО одно сообщение', function () {
|
|
|
|
|
|
// 🪤 Queue::fake ОБЯЗАТЕЛЕН: в тестах очередь синхронная, и «переставить себя на
|
|
|
|
|
|
// утро» она исполнит НЕМЕДЛЕННО, проглотив задержку, — джоб позовёт сам себя без
|
|
|
|
|
|
// конца. В бою очередь redis и задержка соблюдается (В-101).
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
// Ночью: лид пришёл, ничего не ушло.
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-03 23:00:00'));
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: 82);
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
expect(ClientSmsMessage::where('tenant_id', $tenant->id)->count())->toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
// Утро: отложенный джоб проснулся. И следом пришёл второй — задвоение
|
|
|
|
|
|
// (отложенный + повторная постановка). Сообщение обязано остаться одно.
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-04 07:00:00'));
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
$messages = ClientSmsMessage::where('tenant_id', $tenant->id)->get();
|
|
|
|
|
|
expect($messages)->toHaveCount(1)
|
|
|
|
|
|
->and($messages->first()->status)->toBe(ClientSmsMessage::STATUS_FAKE_SENT)
|
|
|
|
|
|
->and($messages->first()->deal_id)->toBe((int) $deal->id);
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('Камчатка: московский вечер — это камчатская ночь, СМС ждёт камчатского утра', function () {
|
|
|
|
|
|
// 17:00 UTC = 20:00 в Москве (окно уже закрылось) и 05:00 следующего дня на Камчатке (+12).
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-04 17:00:00'));
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: 27); // Камчатский край (+12)
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
expect(ClientSmsMessage::where('tenant_id', $tenant->id)->count())->toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
// 10:00 камчатского утра 05.08 = 22:00 UTC 04.08.
|
|
|
|
|
|
Queue::assertPushed(
|
|
|
|
|
|
SendAutoSmsForDealJob::class,
|
|
|
|
|
|
fn (SendAutoSmsForDealJob $job) => $job->delay !== null
|
|
|
|
|
|
&& CarbonImmutable::parse($job->delay)->equalTo(autoWinUtc('2026-08-04 22:00:00')),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('регион неизвестен: СМС не уходит и причина ещё НЕ записана — дверь открыта', function () {
|
|
|
|
|
|
// Полдень по Москве: дело не в часе, а в том, что мы не знаем, где живёт человек.
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-04 09:00:00'));
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: null); // сделка заведена руками
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
expect(ClientSmsMessage::where('tenant_id', $tenant->id)->count())->toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
Queue::assertPushed(
|
|
|
|
|
|
SendAutoSmsForDealJob::class,
|
|
|
|
|
|
fn (SendAutoSmsForDealJob $job) => $job->delay !== null
|
|
|
|
|
|
&& CarbonImmutable::parse($job->delay)->equalTo(autoWinUtc('2026-08-04 10:00:00')),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('регион не узнали за сутки: одна честная запись «не знаем регион», денег не берём', function () {
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-04 09:00:00'));
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: null, receivedAt: autoWinUtc('2026-08-03 08:00:00'));
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
$messages = ClientSmsMessage::where('tenant_id', $tenant->id)->get();
|
|
|
|
|
|
expect($messages)->toHaveCount(1);
|
|
|
|
|
|
expect($messages->first()->status)->toBe(ClientSmsMessage::SKIP_UNKNOWN_REGION)
|
|
|
|
|
|
->and($messages->first()->deal_id)->toBe((int) $deal->id)
|
|
|
|
|
|
->and($messages->first()->campaign_id)->toBeNull()
|
|
|
|
|
|
->and((string) $messages->first()->cost_rub)->toBe('0.00');
|
|
|
|
|
|
|
|
|
|
|
|
// Больше не возвращаемся: причина окончательная.
|
|
|
|
|
|
Queue::assertNothingPushed();
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('окончательная причина сильнее ожидания: стоп-лист называется сразу, а не «ждём региона»', function () {
|
|
|
|
|
|
// Номер в стоп-листе И без региона. Человеку важна причина, которая не изменится
|
|
|
|
|
|
// (урок В-67: сначала решаем «уйдёт или нет», и только потом — как это назвать).
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-03 23:00:00'));
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
ClientSmsOptout::create(['tenant_id' => $tenant->id, 'phone' => '79990000001']);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: null);
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
$messages = ClientSmsMessage::where('tenant_id', $tenant->id)->get();
|
|
|
|
|
|
expect($messages)->toHaveCount(1)
|
|
|
|
|
|
->and($messages->first()->status)->toBe(ClientSmsMessage::SKIP_OPTOUT);
|
|
|
|
|
|
|
|
|
|
|
|
Queue::assertNothingPushed();
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
2026-07-28 19:28:14 +03:00
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
|
| Третий вход обогащения (В-100): регион спрашиваем у ДаДаты по номеру лида.
|
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
|
| Сделка, заведённая в кабинете руками, региона не имеет вовсе — и без этого
|
|
|
|
|
|
| куска авто-СМС ей не уходила бы никогда. Спрашиваем тем же клиентом и под тем
|
|
|
|
|
|
| же дневным лимитом расходов, что и остальные обогащения (решение владельца В-88).
|
|
|
|
|
|
| Платим за номер ОДИН раз: ответ несём в самом задании (моё решение В-104).
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
it('3.3 — сделка без региона: спрашиваем ДаДату по номеру лида и шлём по её ответу', function () {
|
|
|
|
|
|
// 09:00 UTC = полдень в Москве: если ДаДата скажет «UTC+3», окно открыто.
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-04 09:00:00'));
|
|
|
|
|
|
autoWinDaData('UTC+3');
|
|
|
|
|
|
Queue::fake(); // страховка от В-101: если пояс не узнается, джоб зовёт себя без конца
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: null);
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
$messages = ClientSmsMessage::where('tenant_id', $tenant->id)->get();
|
|
|
|
|
|
expect($messages)->toHaveCount(1)
|
|
|
|
|
|
->and($messages->first()->status)->toBe(ClientSmsMessage::STATUS_FAKE_SENT);
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertSentCount(1); // один номер — один платный вопрос
|
|
|
|
|
|
Queue::assertNothingPushed();
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('контрольный: ДаДата не знает регион — лид ждёт дальше, в журнале пусто', function () {
|
|
|
|
|
|
// Без этого прогона предыдущий ничего не доказывает: «ушло» могло бы означать
|
|
|
|
|
|
// «уходит всегда, спросили мы или нет» (урок В-54).
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-04 09:00:00'));
|
|
|
|
|
|
autoWinDaData(null); // ответ есть, пояса в нём нет
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: null);
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
expect(ClientSmsMessage::where('tenant_id', $tenant->id)->count())->toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
// Ждём дальше — но с отметкой «уже спрашивали»: платить второй раз за тот же
|
|
|
|
|
|
// номер незачем, ДаДата ответит то же самое (В-104).
|
|
|
|
|
|
Queue::assertPushed(
|
|
|
|
|
|
SendAutoSmsForDealJob::class,
|
|
|
|
|
|
fn (SendAutoSmsForDealJob $job) => $job->daDataAsked === true && $job->tzOffsetMinutes === null,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('3.3 — за один номер платим один раз: с отметкой «уже спрашивали» ДаДату не дёргаем', function () {
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-04 09:00:00'));
|
|
|
|
|
|
autoWinDaData('UTC+3');
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: null);
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id, asked: true);
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertNothingSent();
|
|
|
|
|
|
expect(ClientSmsMessage::where('tenant_id', $tenant->id)->count())->toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('3.3 — узнанный пояс несётся в отложенное задание, а не спрашивается заново', function () {
|
|
|
|
|
|
// 17:00 UTC = 05:00 утра на Камчатке (UTC+12): окно ещё закрыто, ждём десяти утра.
|
|
|
|
|
|
Carbon::setTestNow(autoWinUtc('2026-08-03 17:00:00'));
|
|
|
|
|
|
autoWinDaData('UTC+12');
|
|
|
|
|
|
Queue::fake();
|
|
|
|
|
|
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
autoWinRule($tenant->id);
|
|
|
|
|
|
$deal = autoWinDeal($tenant->id, subjectCode: null);
|
|
|
|
|
|
|
|
|
|
|
|
autoWinRunJob((int) $deal->id, (int) $tenant->id);
|
|
|
|
|
|
|
|
|
|
|
|
Http::assertSentCount(1);
|
|
|
|
|
|
Queue::assertPushed(
|
|
|
|
|
|
SendAutoSmsForDealJob::class,
|
|
|
|
|
|
fn (SendAutoSmsForDealJob $job) => $job->tzOffsetMinutes === 720
|
|
|
|
|
|
&& $job->delay !== null
|
|
|
|
|
|
&& CarbonImmutable::parse($job->delay)->equalTo(autoWinUtc('2026-08-03 22:00:00')),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
Carbon::setTestNow();
|
|
|
|
|
|
});
|