6e771636ef
Начальник отдела продаж отмечает фирмы прогрева галочками, пишет текст, видит цену ДО отправки и журнал после. Отправки СМС в проекте не было вообще — ни у СМС-центра (только баланс и HLR), ни у МТС (только файл). Что сделано: - разъём провайдера SmsProvider: новый оператор подключается одним файлом - заглушка FakeSmsProvider — модуль работает и проверяется ДО согласования имени отправителя у операторов (это недели), иначе разработку не закончить - маршрутизация по оператору: билайновский номер уходит через Билайн за 4,75 ₽, прочие через МТС — без ручного выбора канала - стоп-лист: кто отписался, тому не шлём никогда, проверка перед списанием - отбор получателей с шестью причинами пропуска, все ДО траты денег - списание скопировано с AutopodborChargeService; пока клиента нет (tenant_id пуст) с баланса не берём — платим оператору напрямую - оператор номера доезжает из «Поиска клиентов» в прогрев (был известен и оплачен ДаДате, но терялся при передаче) Мультиклиентность в костях: колонка tenant_id во всех четырёх таблицах СМС с первого дня, NULL = «Лидерра сама». Клиент добавляется строкой, а не переделкой модуля. Найдено и закрыто при исполнении: - замок от двойного списания стоял не на том соединении: кампания на pgsql_supplier, деньги на pgsql, lockForUpdate по кампании отпускался сразу. На бою два запуска списали бы дважды, обрыв — оставил бы пометку «оплачено» при неушедших деньгах. Источник правды перенесён в balance_transactions под замок по тенанту. Доказано тестом: старый код списывал 700 вместо 850 - приём в портал требовал phones строкой по regex — словарь с оператором получал 422, в базу не доезжало ничего. Тесты были зелёные, потому что звали сервис МИМО контроллера. Проверка теперь принимает оба формата, тест идёт через HTTP - телефоны директоров в contacts остаются строками (договор SalesProspectController), словари — только в верхнем phones Заодно вылечена мигающая поломка 48 тестов доставки лидов: помощник createRoutingSnapshotFromProject клал снимок на сегодня, а LeadRouter после 21:00 МСК ищет завтрашний (вечерний переворот заливки) — вечерние прогоны падали, дневные проходили. Помощник теперь зеркалит активную дату роутера в любой час. Регрессия SnapshotHelperTimeOfDayTest замораживает 22:00 МСК и пинит инвариант. Боевой LeadRouter не тронут. Тесты: 84 бэкенд + фронт по экрану + 397 поисковика, весь набор 3226 зелёный, статанализ чист. Все защиты проверены вырезанием. План: docs/superpowers/plans/2026-07-20-sms-progrev-modul.md Спека: docs/superpowers/specs/2026-07-20-sms-progrev-modul-design.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
55 lines
2.5 KiB
PHP
55 lines
2.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\Project;
|
|
use App\Models\SupplierProject;
|
|
use App\Models\Tenant;
|
|
use App\Services\LeadRouter;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\Concerns\SharesSupplierPdo;
|
|
|
|
/**
|
|
* Регрессия на мигающие тесты доставки лидов.
|
|
*
|
|
* Роутер выбирает дату слепка по часам: после 21:00 МСК — завтра, до — сегодня
|
|
* (LeadRouter::activeSnapshotDate, вечерний переворот заливки). Помощник
|
|
* createRoutingSnapshotFromProject раньше клал снимок ВСЕГДА на сегодня, поэтому
|
|
* после 21:00 МСК роутер искал завтрашний снимок, не находил и возвращал 0 —
|
|
* весь пласт тестов доставки падал только по вечерам. Здесь фиксируем инвариант:
|
|
* помощник обязан класть снимок на ту же дату, что спросит роутер, в любой час.
|
|
*/
|
|
uses(DatabaseTransactions::class);
|
|
uses(SharesSupplierPdo::class);
|
|
|
|
beforeEach(function (): void {
|
|
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
|
|
});
|
|
|
|
afterEach(function (): void {
|
|
Carbon\Carbon::setTestNow();
|
|
});
|
|
|
|
it('находит проект по слепку и после 21:00 МСК (когда роутер смотрит на завтра)', function (): void {
|
|
// 22:00 МСК — роутер уже ищет завтрашний слепок. Именно этот час валил 48 тестов.
|
|
Carbon\Carbon::setTestNow(Carbon\Carbon::parse('2026-07-21 22:00', 'Europe/Moscow'));
|
|
|
|
$tenant = Tenant::factory()->create(['balance_leads' => 100, 'balance_rub' => '1000.00']);
|
|
$sp = SupplierProject::query()->create([
|
|
'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'r.ru',
|
|
'subject_code' => 82, 'current_limit' => 0, 'sync_status' => 'ok',
|
|
]);
|
|
$project = Project::factory()->create([
|
|
'tenant_id' => $tenant->id, 'is_active' => true,
|
|
'daily_limit_target' => 10, 'delivered_today' => 0, 'delivery_days_mask' => 127,
|
|
]);
|
|
linkProjectToSupplier($project, $sp);
|
|
createRoutingSnapshotFromProject($project);
|
|
|
|
$matched = app(LeadRouter::class)->matchEligibleProjects($sp);
|
|
|
|
expect($matched)->toHaveCount(1)
|
|
->and($matched->first()->id)->toBe($project->id);
|
|
});
|