bc573a99eb
Доработки экрана «Моя база» клиентской СМС-рассылки по просьбе владельца: - Кривые номера больше не выбрасываются молча: uploadContacts/uploadContactsFile возвращают rejected + rejected_samples, экран показывает «Добавлено: N, не распознали: M (например: …) — проверьте формат». - Таблица базы — только «Номер» и «Оператор» (колонку «Имя» убрал). - Загрузка базы Excel-файлом: сервис ClientSmsPhoneFileReader (phpspreadsheet) читает первую колонку, пропускает заголовок, номера-как-числа не уходят в научную нотацию; endpoint POST /api/sms/contacts/file; на экране — выбор файла и кнопка «Загрузить файл». - «Скачать пример файла»: ClientSmsBaseExampleWriter + GET /api/sms/contacts/example — готовый .xlsx с колонкой «Телефон» и образцами номеров. - Оператор через ДаДату: EnrichClientSmsContactsOperatorJob обогащает добавленные номера (DaDataPhoneClient.provider) в фоне, под tenant-контекстом (RLS), с защитой дневного бюджета (DaDataBudgetGuard); сбой ДаДаты не роняет прогон, оператор остаётся пустым; уже известного оператора не перезапрашиваем. Тесты: бэкенд ClientSms 130/130, фронт СМС-спеки 63/63 — зелёные; pint чист; портал пересобран. Синтетические номера 7999… в тестах. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
3.7 KiB
PHP
88 lines
3.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)]);
|
|
|
|
$c = ClientSmsContact::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000002', 'operator' => 'mts']);
|
|
|
|
(new EnrichClientSmsContactsOperatorJob($this->tenant->id))->handle(
|
|
app(DaDataPhoneClient::class),
|
|
app(DaDataBudgetGuard::class),
|
|
);
|
|
|
|
expect($c->fresh()->operator)->toBe('mts'); // не перезаписан
|
|
Http::assertNothingSent(); // ДаДату не дёргали
|
|
});
|
|
|
|
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();
|
|
});
|