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>
61 lines
2.7 KiB
PHP
61 lines
2.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Illuminate\Database\QueryException;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Tests\Concerns\SharesSupplierPdo;
|
||
|
||
/**
|
||
* SharesSupplierPdo обязателен: таблицы sales_* сидят на отдельном соединении
|
||
* pgsql_supplier, и без шаринга PDO откат отменял бы только соединение по
|
||
* умолчанию — вставленные кампании копились бы от прогона к прогону.
|
||
*
|
||
* Здесь это до поры незаметно (уникальность по (campaign_id, phone), а номер
|
||
* кампании каждый раз новый), но осадок реален и рванёт при первой же проверке
|
||
* вида «в таблице ровно N строк».
|
||
*/
|
||
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
||
|
||
it('создаёт четыре таблицы СМС на соединении поставщика', function () {
|
||
$db = DB::connection('pgsql_supplier');
|
||
|
||
foreach (['sales_sms_senders', 'sales_sms_campaigns', 'sales_sms_messages', 'sales_sms_optouts'] as $table) {
|
||
expect($db->getSchemaBuilder()->hasTable($table))->toBeTrue("нет таблицы {$table}");
|
||
}
|
||
});
|
||
|
||
it('заводит tenant_id во всех таблицах СМС — задел под клиентов', function () {
|
||
$db = DB::connection('pgsql_supplier')->getSchemaBuilder();
|
||
|
||
foreach (['sales_sms_senders', 'sales_sms_campaigns', 'sales_sms_optouts'] as $table) {
|
||
expect($db->hasColumn($table, 'tenant_id'))->toBeTrue("нет tenant_id в {$table}");
|
||
}
|
||
});
|
||
|
||
it('добавляет оператора и тип номера в таблицу номеров прогрева', function () {
|
||
$db = DB::connection('pgsql_supplier')->getSchemaBuilder();
|
||
|
||
expect($db->hasColumn('sales_ad_audience_phones', 'operator'))->toBeTrue();
|
||
expect($db->hasColumn('sales_ad_audience_phones', 'phone_type'))->toBeTrue();
|
||
});
|
||
|
||
it('запрещает два одинаковых номера внутри одной рассылки', function () {
|
||
$db = DB::connection('pgsql_supplier');
|
||
|
||
$campaignId = $db->table('sales_sms_campaigns')->insertGetId([
|
||
'title' => 'Проверка', 'body' => 'текст', 'status' => 'draft', 'created_by' => 1,
|
||
]);
|
||
|
||
$row = [
|
||
'campaign_id' => $campaignId, 'phone' => '79990000001',
|
||
'status' => 'pending', 'segments' => 1, 'cost_kopecks' => 0,
|
||
];
|
||
|
||
$db->table('sales_sms_messages')->insert($row);
|
||
|
||
expect(fn () => $db->table('sales_sms_messages')->insert($row))
|
||
->toThrow(QueryException::class);
|
||
});
|