80c9ca7e67
Кусок 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>
95 lines
4.3 KiB
PHP
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();
|
|
});
|