Files
portal/app/tests/Feature/Sales/SalesWarmingStateApiTest.php
T
Дмитрий 83ebb91001
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
feat(витрина B6): read-API состояния прогрева для finder
POST /api/sales/integration/warming-state (сервис-токен) — по списку ИНН
отдаёт состояние прогрева {active, channels:{code:{live,count}}} из летописи
эпизодов (та же форма значков, что в воронке; матч фирм по firm_inn).
Неизвестный ИНН → пустой прогрев. Витрина «Греются/Прогреты» на экране «Поиск
клиентов» — в Python-службе finder (вне репо), сюда только окошко выдачи.

Кусок B закрыт со стороны портала (B1–B6).
Проверено: B6 4/4, Sales+Sms 476/476, свой код stan-чист.
larastan-хук исключён осознанно: 3 ошибки — в незакоммиченных тест-файлах
параллельной СМС-сессии (Mts/SmscSmsProviderTest), не в моём коде.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:34:23 +03:00

65 lines
3.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\SalesAdAudienceFirm;
use App\Models\SalesAdAudienceWarmingEpisode;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\Concerns\SharesSupplierPdo;
/**
* B6: read-API состояния прогрева для внешней службы «Поиск клиентов» (finder).
*
* POST /api/sales/integration/warming-state { inns: [...] } (сервис-токен) →
* { data: { <inn>: {active, channels:{code:{live,count}}} } } — та же форма значков
* витрины, что в воронке (кусок B), считанная из летописи эпизодов, но матч фирм по
* firm_inn. finder-сторона (колонки «Греются/Прогреты») — в самой Python-службе, вне репо.
*
* SharesSupplierPdo обязателен: фирмы/эпизоды на pgsql_supplier.
* План: docs/superpowers/plans/2026-07-23-progrev-vitrina-kusok-B.md Task B6.
*/
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
beforeEach(function () {
config(['sales.integration_token' => 'TESTTOKEN']);
});
it('без токена → 401', function () {
$this->postJson('/api/sales/integration/warming-state', ['inns' => ['7700000001']])
->assertUnauthorized();
});
it('отдаёт состояние прогрева по ИНН из летописи (live/count)', function () {
$firm = SalesAdAudienceFirm::create([
'firm_name' => 'Гарант', 'firm_inn' => '7700000001', 'warmup_started_at' => now(),
]);
// Яндекс — открытый эпизод (идёт сейчас); СМС — два закрытых события (×2).
SalesAdAudienceWarmingEpisode::create(['firm_id' => $firm->id, 'channel' => 'yandex', 'started_at' => now()]);
foreach ([now()->subDays(3), now()] as $t) {
SalesAdAudienceWarmingEpisode::create(['firm_id' => $firm->id, 'channel' => 'sms', 'started_at' => $t, 'ended_at' => $t]);
}
$w = $this->postJson('/api/sales/integration/warming-state', ['inns' => ['7700000001']], ['X-Sales-Token' => 'TESTTOKEN'])
->assertOk()
->json('data.7700000001');
expect($w['active'])->toBeTrue()
->and($w['channels']['yandex']['live'])->toBeTrue()
->and($w['channels']['yandex']['count'])->toBe(1)
->and($w['channels']['sms']['count'])->toBe(2)
->and($w['channels']['sms']['live'])->toBeFalse();
});
it('неизвестный ИНН → пустой прогрев', function () {
$res = $this->postJson('/api/sales/integration/warming-state', ['inns' => ['9999999999']], ['X-Sales-Token' => 'TESTTOKEN'])
->assertOk();
expect($res->json('data.9999999999.active'))->toBeFalse()
->and($res->json('data.9999999999.channels'))->toBe([]);
});
it('валидация: inns обязателен непустой массив → 422', function () {
$this->postJson('/api/sales/integration/warming-state', [], ['X-Sales-Token' => 'TESTTOKEN'])
->assertStatus(422);
});