Files
portal/app/tests/Feature/Sales/WarmingEpisodesWiringTest.php
T
Дмитрий 80c9ca7e67 feat(витрина B1,B2,B4,B5): летопись эпизодов прогрева + значки в воронке; уборка мёртвых скоупов
Кусок B «витрина прогрева» (спека 2026-07-21 §6), решение владельца — полная летопись:
- B1: таблица sales_ad_audience_warming_episodes + модель + WarmingEpisodeRecorder
  (open/close/record идемпотентно). Схема v8.85, бэкфилл из firm_channels(warming)
  и боевых СМС, guard по источнику. rls-reviewer OK 8/8.
- B2: «Греть»/«Убрать» на площадке открывают/закрывают эпизоды канала.
- B4: warmingByProspect считает значки из летописи (live/count вместо массива каналов),
  тип WarmingBadgeState в sales.ts.
- B5: единый компонент WarmingBadges.vue (идёт/грели раньше/×N) в канбане;
  осиротевший WarmingChannelIcons удалён.
Уборка: убраны мёртвые скоупы forYandex/Vk/Mts + их импорт + тест (боевых вызовов нет).
cspell: +5 пре-существующих слов CHANGELOG в словарь (apk/cvtjpq/hgq/sar/sca).

Проверено: Sales 427/427, composer stan 0, pint/prettier чисто, весь Vue-набор зелёный.
B3 (СМС→эпизод) — отдельным коммитом (СМС-блок правит и параллельная сессия).

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

95 lines
4.3 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\SalesAdAudienceFirm;
use App\Models\SalesAdAudienceWarmingEpisode;
use App\Models\SalesUser;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\Concerns\SharesSupplierPdo;
/**
* B2: «Греть»/«Убрать» на экране площадки пишут летопись эпизодов прогрева.
*
* open при loaded→warming (toggle on / warm), close при warming→удалено (toggle off /
* unassign). open идемпотентен — повторное «Греть» уже греющейся фирмы не плодит
* второй открытый эпизод (защита в WarmingEpisodeRecorder). Хелперы локальные
* (wiring_*) — тест самодостаточен и падает по нужной причине при запуске в одиночку.
*/
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
function wiringFirm(): SalesAdAudienceFirm
{
return SalesAdAudienceFirm::create(['firm_name' => 'Тест', 'warmup_started_at' => now()]);
}
function wiringHead(): SalesUser
{
return SalesUser::create([
'name' => 'Начальник '.uniqid(),
'email' => 'wiringhead'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'head',
'is_active' => true,
]);
}
it('«Греть» открывает эпизод канала', function () {
$firm = wiringFirm();
$this->actingAs(wiringHead(), 'sales')
->postJson('/api/sales/warming/yandex/warm', ['firm_ids' => [$firm->id]])
->assertOk();
$ep = SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->where('channel', 'yandex')->sole();
expect($ep->ended_at)->toBeNull();
});
it('повторное «Греть» уже греющейся фирмы второго открытого эпизода не создаёт', function () {
$firm = wiringFirm();
$head = wiringHead();
$this->actingAs($head, 'sales')->postJson('/api/sales/warming/yandex/warm', ['firm_ids' => [$firm->id]])->assertOk();
$this->actingAs($head, 'sales')->postJson('/api/sales/warming/yandex/warm', ['firm_ids' => [$firm->id]])->assertOk();
expect(SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->where('channel', 'yandex')->count())->toBe(1);
});
it('«Убрать» закрывает открытый эпизод канала', function () {
$firm = wiringFirm();
$head = wiringHead();
$this->actingAs($head, 'sales')->postJson('/api/sales/warming/yandex/warm', ['firm_ids' => [$firm->id]])->assertOk();
$this->actingAs($head, 'sales')->postJson('/api/sales/warming/yandex/unassign', ['firm_ids' => [$firm->id]])->assertOk();
$ep = SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->where('channel', 'yandex')->sole();
expect($ep->ended_at)->not->toBeNull();
});
it('тумблер on=true открывает, on=false закрывает эпизод', function () {
$firm = wiringFirm();
$head = wiringHead();
$this->actingAs($head, 'sales')
->postJson("/api/sales/warming/yandex/firms/{$firm->id}/toggle", ['on' => true])->assertOk();
expect(SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->whereNull('ended_at')->count())->toBe(1);
$this->actingAs($head, 'sales')
->postJson("/api/sales/warming/yandex/firms/{$firm->id}/toggle", ['on' => false])->assertOk();
expect(SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->whereNull('ended_at')->count())->toBe(0);
});
it('«Убрать» не трогает открытый эпизод другого канала той же фирмы', function () {
$firm = wiringFirm();
$head = wiringHead();
$this->actingAs($head, 'sales')->postJson('/api/sales/warming/yandex/warm', ['firm_ids' => [$firm->id]])->assertOk();
$this->actingAs($head, 'sales')->postJson('/api/sales/warming/vk/warm', ['firm_ids' => [$firm->id]])->assertOk();
$this->actingAs($head, 'sales')->postJson('/api/sales/warming/yandex/unassign', ['firm_ids' => [$firm->id]])->assertOk();
$vk = SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->where('channel', 'vk')->sole();
$yandex = SalesAdAudienceWarmingEpisode::where('firm_id', $firm->id)->where('channel', 'yandex')->sole();
expect($vk->ended_at)->toBeNull()
->and($yandex->ended_at)->not->toBeNull();
});