Files
portal/app/tests/Feature/Sales/AdAudienceIntakeTest.php
T
Дмитрий 54ffba360d fix(sales): AdAudienceIntake молча ронял ch_yandex/ch_vk/ch_mts
'channels' убрали из $fillable в Task 2 плана tri-ploshadki-i-mts, но
AdAudienceIntake остался класть его в атрибуты — Laravel тихо
выбрасывал ключ при mass assignment. Итог: каждая новая фирма из
«Поиска клиентов» получала ch_yandex=false/ch_vk=false/ch_mts=false —
не грелась нигде, без единой ошибки в логах.

Контракт принимает оба формата: новые булевы поля ch_yandex/ch_vk/
ch_mts (приоритет) и старую строку channels ('yandex'|'vk'|'both')
для совместимости с ещё не переехавшей Python-службой. Если не
пришло ни то, ни другое — ch_yandex=true (фирма отправлена в прогрев
осознанно, разумное умолчание — основная площадка).

Регресс-тест ловил баг до фикса: 3/3 новых теста падали с
"Failed asserting that false is true" на ch_yandex/ch_vk.
2026-07-20 08:31:50 +03:00

160 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\SalesAdAudienceFirm;
use App\Models\SalesAdAudiencePhone;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\Concerns\SharesSupplierPdo;
/**
* Приём фирм в прогрев рекламой сервис-каналом «Поиск → Портал» (контракт v2).
*
* Контракт сменился: приходит фирма со снимком и списком номеров, а не голый
* номер (v1). Фирма опознаётся по ИНН — повторная отправка не плодит дубль,
* а начинает прогрев заново (начальник осознанно вернул фирму в рекламу).
*
* Токен канала — config('sales.integration_token') (middleware SalesIntegrationToken).
* В тестовом окружении env пуст, поэтому задаём его здесь, как в SalesIntegrationApiTest.
*
* SharesSupplierPdo обязателен: обе модели прибиты к соединению pgsql_supplier
* (отдельный PDO), и без шаринга DatabaseTransactions откатывал бы только
* соединение по умолчанию — записи текли бы из теста в тест.
*/
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
beforeEach(function () {
config(['sales.integration_token' => 'test-token']);
});
function warmupPayload(array $override = []): array
{
return ['firms' => [array_merge([
'firm_name' => 'Тестовая стоматология',
'firm_inn' => '7700000001',
'city' => 'Москва',
'rubric' => 'Стоматологии',
'phones' => ['79990000001', '79990000002'],
'payload' => ['site' => 'example.test'],
'contacts' => [['name' => 'Директор', 'phones' => ['79990000001']]],
], $override)]];
}
it('без токена канал закрыт', function () {
$this->postJson('/api/sales/integration/ad-audience', warmupPayload())
->assertStatus(401);
});
it('заводит фирму на прогрев вместе с её номерами', function () {
$this->postJson('/api/sales/integration/ad-audience', warmupPayload(), ['X-Sales-Token' => 'test-token'])
->assertOk()
->assertJson(['added' => 1]);
$firm = SalesAdAudienceFirm::where('firm_inn', '7700000001')->firstOrFail();
expect($firm->phones()->count())->toBe(2)
->and($firm->prospect_id)->toBeNull()
->and($firm->warmup_started_at)->not->toBeNull();
});
it('повторная отправка не плодит дубль, а начинает прогрев заново', function () {
$headers = ['X-Sales-Token' => 'test-token'];
$this->postJson('/api/sales/integration/ad-audience', warmupPayload(), $headers)->assertOk();
SalesAdAudienceFirm::where('firm_inn', '7700000001')
->update(['warmup_started_at' => now()->subDays(10), 'stopped_at' => now()]);
$this->postJson('/api/sales/integration/ad-audience', warmupPayload(), $headers)
->assertOk()
->assertJson(['added' => 0, 'renewed' => 1]);
$firm = SalesAdAudienceFirm::where('firm_inn', '7700000001')->firstOrFail();
expect(SalesAdAudienceFirm::count())->toBe(1)
->and($firm->stopped_at)->toBeNull()
->and($firm->warmup_started_at->isToday())->toBeTrue();
});
it('добавляет новый номер к уже заведённой фирме', function () {
$headers = ['X-Sales-Token' => 'test-token'];
$this->postJson('/api/sales/integration/ad-audience', warmupPayload(), $headers)->assertOk();
$this->postJson(
'/api/sales/integration/ad-audience',
warmupPayload(['phones' => ['79990000001', '79990000003']]),
$headers
)->assertOk();
expect(SalesAdAudiencePhone::count())->toBe(3);
});
it('отвергает номер не в формате Яндекса', function () {
$this->postJson(
'/api/sales/integration/ad-audience',
warmupPayload(['phones' => ['+7 (999) 000-00-01']]),
['X-Sales-Token' => 'test-token']
)->assertStatus(422);
});
it('фирму без единого номера в прогрев не берём', function () {
$this->postJson(
'/api/sales/integration/ad-audience',
warmupPayload(['phones' => []]),
['X-Sales-Token' => 'test-token']
)->assertStatus(422);
});
it('старый формат channels=both включает обе галочки ch_yandex и ch_vk', function () {
$this->postJson(
'/api/sales/integration/ad-audience',
warmupPayload(['channels' => 'both']),
['X-Sales-Token' => 'test-token']
)->assertOk();
$firm = SalesAdAudienceFirm::where('firm_inn', '7700000001')->firstOrFail();
expect($firm->ch_yandex)->toBeTrue()
->and($firm->ch_vk)->toBeTrue()
->and($firm->ch_mts)->toBeFalse();
});
it('без указания площадки греет в Яндексе (ch_yandex=true по умолчанию)', function () {
$payload = warmupPayload();
unset($payload['firms'][0]['channels']);
$this->postJson('/api/sales/integration/ad-audience', $payload, ['X-Sales-Token' => 'test-token'])
->assertOk();
$firm = SalesAdAudienceFirm::where('firm_inn', '7700000001')->firstOrFail();
expect($firm->ch_yandex)->toBeTrue()
->and($firm->ch_vk)->toBeFalse()
->and($firm->ch_mts)->toBeFalse();
});
it('новый формат ch_yandex/ch_vk/ch_mts проставляет площадки явно', function () {
$payload = warmupPayload();
unset($payload['firms'][0]['channels']);
$payload['firms'][0]['ch_yandex'] = false;
$payload['firms'][0]['ch_vk'] = true;
$payload['firms'][0]['ch_mts'] = true;
$this->postJson('/api/sales/integration/ad-audience', $payload, ['X-Sales-Token' => 'test-token'])
->assertOk();
$firm = SalesAdAudienceFirm::where('firm_inn', '7700000001')->firstOrFail();
expect($firm->ch_yandex)->toBeFalse()
->and($firm->ch_vk)->toBeTrue()
->and($firm->ch_mts)->toBeTrue();
});
it('отвергает выдуманную площадку', function () {
$this->postJson(
'/api/sales/integration/ad-audience',
warmupPayload(['channels' => 'telegram']),
['X-Sales-Token' => 'test-token']
)->assertStatus(422);
});