d61b552532
Этап 3 «Время и цена», Task 5. Строка листа 3.3 закрыта целиком. Номер, про который мы ничего не знаем, перестал быть тупиком: теперь его судьбу решает ДаДата — и решает ОДНИМ платным вопросом, потому что пояс и оператор приезжают в одном ответе. Входов обогащения три, и все три ведут в одно и то же место. Своя база: джоб обогащения контактов кладёт пояс рядом с оператором и догоняет номера, загруженные до Этапа 3, — берёт контакт, у которого пусто хоть что-то одно. Без догона такой номер после выката не получил бы СМС никогда. Рассылка: новый EnrichClientSmsSnapshotRegionJob спрашивает про строки снимка без пояса и пишет туда и пояс, и оператора. У номеров, вписанных руками, оператора не было вовсе — это и есть закрытие старой жалобы В-69. Ставится из контроллера сразу после снимка и только если такие строки есть; узнав регион, сам будит рассылку, чтобы созревшие номера не ждали четверть часа зря. Будит ТОЛЬКО ждущую утра: у остановленной клиентом рассылки статус другой, и повторный запуск затёр бы ей «остановлена» на «готово». Авто-СМС: сделка без региона спрашивает ДаДату по номеру лида (В-100) — иначе заведённый руками лид не получил бы СМС и после этой задачи. За номер платим один раз: узнанный пояс несётся в самом отложенном задании, а «ответила, но пояса нет» помечается отметкой; заново спрашиваем только если вопрос не состоялся — сбой сети или выбранный дневной лимит (В-104). Дозревание: команда добора ставит окончательную причину строкам, которые ждут региона дольше суток, и будит рассылку, чтобы та дописала причину в журнал и закончилась — деньги размораживаются. В журнал пишет джоб отправки, он это и так умеет для всех пропущенных; второго места, пишущего в журнал, не завёл (В-107). Кого НЕ спрашиваем (В-105): строку, у которой пояс есть, а оператор пуст. Она и так уйдёт универсальным каналом, вопрос был бы ради экономии на канале, а ДаДата — наши деньги: рассылка на 20 000 номеров это до 12 000 ₽ при ещё не назначенном потолке расходов (В-90). Платим только там, где без ответа сообщение не уйдёт вовсе. Прогоны: СМС 217/217 (было 202, +15 новых), приём лидов 17/17, фронт 1663 зелёных, phpstan 0, pint чисто. Миграций нет — колонки пояса завёл ещё Task 2. Вырезанием проверено пятью вырезами: убрал запись пояса — покраснел «узнав регион, номер уходит»; убрал суточный срок — покраснел «не узнали за сутки»; убрал вторую половину условия пробуждения — покраснел денежный тест про заморозку; убрал вопрос ДаДате в авто-СМС — покраснели три теста третьего входа; убрал догон старых номеров базы — покраснел «догоняет уже загруженный номер». Живьём на локальной базе (19:19 МСК, ДаДата заглушена, живой ключ не тронут): вписанные руками номера узнали пояс и оператора, московский ушёл сразу, камчатский отложен до 10 утра его времени; номер, про который ДаДата молчит, через сутки получил причину и рассылка стала «готово»; сделка без региона получила авто-СМС по ответу ДаДаты, а сделка, про которую ДаДата молчит, ждёт дальше и в журнал не пишет. Стенд возвращён как был. 🪤 Урок В-106: повторный Http::fake() прежнюю заглушку не заменяет — отвечает первая, и тест «а теперь ДаДата отвечает» молча проверяет старый ответ. 🟢 Запрет на выкат ветки СНЯТ (был из-за В-93): ни один источник больше не отправляет ноль. Выкатывать всё равно рано — Этап 3 не закончен, цена считается по-старому.
147 lines
6.7 KiB
PHP
147 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\EnrichClientSmsContactsOperatorJob;
|
|
use App\Models\ClientSmsContact;
|
|
use App\Models\Tenant;
|
|
use App\Services\DaData\DaDataBudgetGuard;
|
|
use App\Services\DaData\DaDataPhoneClient;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
/**
|
|
* Каждый добавленный в базу номер прогоняем через ДаДату, чтобы вытащить оператора
|
|
* (нужен для маршрутизации СМС по нужному провайдеру). ДаДата платная — расход
|
|
* ограничен DaDataBudgetGuard; сбой ДаДаты не должен ронять обогащение (оператор
|
|
* просто останется пустым).
|
|
*
|
|
* Синтетические номера 7999… — реальные НИКОГДА.
|
|
*/
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::factory()->create();
|
|
// ДаДата включена (ключ задан) — иначе обогащение не стартует.
|
|
config()->set('services.dadata.api_key', 'test-key');
|
|
config()->set('services.dadata.secret', 'test-secret');
|
|
Cache::flush(); // чистый дневной бюджет
|
|
});
|
|
|
|
it('проставляет оператора контактам без оператора', function () {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 0, 'provider' => 'МТС']], 200)]);
|
|
|
|
$c = ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000001']);
|
|
|
|
(new EnrichClientSmsContactsOperatorJob($this->tenant->id))->handle(
|
|
app(DaDataPhoneClient::class),
|
|
app(DaDataBudgetGuard::class),
|
|
);
|
|
|
|
expect($c->fresh()->operator)->toBe('МТС');
|
|
});
|
|
|
|
it('не трогает контакты, про которых уже всё известно (экономим ДаДату)', function () {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 0, 'provider' => 'Билайн']], 200)]);
|
|
|
|
// «Всё известно» = И оператор, И часовой пояс. С Этапа 3 одного оператора мало:
|
|
// без пояса номеру СМС не уходит вовсе (В-85), и такой контакт мы обязаны догнать.
|
|
$c = ClientSmsContact::create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'phone' => '79990000002',
|
|
'operator' => 'mts',
|
|
'tz_offset_minutes' => 180,
|
|
]);
|
|
|
|
(new EnrichClientSmsContactsOperatorJob($this->tenant->id))->handle(
|
|
app(DaDataPhoneClient::class),
|
|
app(DaDataBudgetGuard::class),
|
|
);
|
|
|
|
expect($c->fresh()->operator)->toBe('mts'); // не перезаписан
|
|
Http::assertNothingSent(); // ДаДату не дёргали
|
|
});
|
|
|
|
it('3.3 — сохраняет часовой пояс из того же ответа, за который уже заплатили', function () {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 0, 'provider' => 'МТС', 'timezone' => 'UTC+9']], 200)]);
|
|
|
|
$c = ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000005']);
|
|
|
|
(new EnrichClientSmsContactsOperatorJob($this->tenant->id))->handle(
|
|
app(DaDataPhoneClient::class),
|
|
app(DaDataBudgetGuard::class),
|
|
);
|
|
|
|
// UTC+9 — это Якутск: девять часов от Гринвича, то есть 540 минут.
|
|
expect($c->fresh()->tz_offset_minutes)->toBe(540)
|
|
->and($c->fresh()->operator)->toBe('МТС');
|
|
// Второго обращения к ДаДате быть не должно: пояс приезжает тем же ответом.
|
|
Http::assertSentCount(1);
|
|
});
|
|
|
|
it('3.3 — непонятный пояс не мешает сохранить оператора', function () {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 0, 'provider' => 'МТС', 'timezone' => 'ерунда']], 200)]);
|
|
|
|
$c = ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000006']);
|
|
|
|
(new EnrichClientSmsContactsOperatorJob($this->tenant->id))->handle(
|
|
app(DaDataPhoneClient::class),
|
|
app(DaDataBudgetGuard::class),
|
|
);
|
|
|
|
// Ради оператора джоб и ходит — непонятный пояс не должен ронять главное.
|
|
expect($c->fresh()->operator)->toBe('МТС')
|
|
->and($c->fresh()->tz_offset_minutes)->toBeNull();
|
|
});
|
|
|
|
it('3.3 — догоняет уже загруженный номер: оператор есть, пояса нет', function () {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 0, 'provider' => 'МТС', 'timezone' => 'UTC+2']], 200)]);
|
|
|
|
// Номера, загруженные ДО Этапа 3: оператора им проставили, а пояса тогда не было
|
|
// вовсе. Без догона такой номер после выката не получил бы СМС никогда (В-88).
|
|
$c = ClientSmsContact::create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'phone' => '79990000007',
|
|
'operator' => 'МТС',
|
|
]);
|
|
|
|
(new EnrichClientSmsContactsOperatorJob($this->tenant->id))->handle(
|
|
app(DaDataPhoneClient::class),
|
|
app(DaDataBudgetGuard::class),
|
|
);
|
|
|
|
expect($c->fresh()->tz_offset_minutes)->toBe(120)
|
|
->and($c->fresh()->operator)->toBe('МТС');
|
|
});
|
|
|
|
it('сбой ДаДаты не роняет обогащение — оператор остаётся пустым', function () {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response('boom', 500)]);
|
|
|
|
$c = ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000003']);
|
|
|
|
(new EnrichClientSmsContactsOperatorJob($this->tenant->id))->handle(
|
|
app(DaDataPhoneClient::class),
|
|
app(DaDataBudgetGuard::class),
|
|
);
|
|
|
|
expect($c->fresh()->operator)->toBeNull();
|
|
});
|
|
|
|
it('при исчерпанном дневном бюджете ДаДату не дёргает', function () {
|
|
Http::fake(['cleaner.dadata.ru/*' => Http::response([['qc' => 0, 'provider' => 'МТС']], 200)]);
|
|
// выбираем весь дневной лимит
|
|
config()->set('services.dadata.daily_cap_rub', 1);
|
|
app(DaDataBudgetGuard::class)->recordSpend(200); // 2 ₽ > 1 ₽
|
|
|
|
$c = ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000004']);
|
|
|
|
(new EnrichClientSmsContactsOperatorJob($this->tenant->id))->handle(
|
|
app(DaDataPhoneClient::class),
|
|
app(DaDataBudgetGuard::class),
|
|
);
|
|
|
|
expect($c->fresh()->operator)->toBeNull();
|
|
Http::assertNothingSent();
|
|
});
|