Files
portal/app/tests/Feature/Sales/AdAudiencePlatformApiTest.php
T
Дмитрий 6775f58003 feat(прогрев ф2 этап C): показ по каналам — firms/бейджи/числа из firm_channels (+ план этапа C)
Фаза 2 Этап C, Task C1. Вкладка канала показывает ТОЛЬКО свой состав: firms({platform})
фильтрует по наличию строки firm_channels(channel) (loaded или warming). Бейджи
warming_channels строятся из строк firm_channels (yandex/vk/mts по строкам, sms по
факту рассылки), а не из ch_*. Числа вкладки (in_ads/expiring_week/waiting_sync)
считаются по фирмам со строкой firm_channels(channel, status='warming') — loaded в
рекламе не участвует. Это чинит поломку после этапа B (заезд не пишет ch_* ⇒ старые
ch_*-подсчёты давали 0/пусто у новых фирм). channelColumn удалён (сирота).
allFirms/toggle/mtsFile/движок/заливки не тронуты. ch_* не удаляем (откат).
Тесты мигрированы. Bump baseline (actingAs-ложняки).

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

170 lines
7.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\SalesAdAudienceFirm;
use App\Models\SalesAdAudiencePlatform;
use App\Models\SalesAdAudienceState;
use App\Models\SalesUser;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\Concerns\SharesSupplierPdo;
/**
* API «Реклама на кандидатов» по площадкам (Task 4+5 плана трёх площадок).
*
* Одна общая ручка /api/sales/ad-audience разъезжается на три:
* /api/sales/warming/{yandex|vk|mts}. Настройки и статус — построчно
* в sales_ad_audience_platforms; days/сроки воронки пока общие
* (sales_ad_audience_state). Членство фирмы в площадке — булевы
* ch_yandex/ch_vk/ch_mts на sales_ad_audience_firms.
*
* Аутентификация — как в AdAudienceScreenTest: SalesUser::create() (у SalesUser
* нет HasFactory/factory()) + $this->actingAs(..., 'sales'). Паттерн $this->
* (не test()->) — чтобы larastan понимал тип TestCase.
*/
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
function warmingHead(): SalesUser
{
return SalesUser::create([
'name' => 'Начальник '.uniqid(),
'email' => 'warminghead'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'head',
'is_active' => true,
]);
}
function warmingManager(): SalesUser
{
return SalesUser::create([
'name' => 'Менеджер '.uniqid(),
'email' => 'warmingmgr'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'manager',
'is_active' => true,
]);
}
it('отдаёт настройки именно запрошенной площадки', function () {
SalesAdAudiencePlatform::forCode('vk')->update(['status' => 'waiting_volume']);
$this->actingAs(warmingHead(), 'sales')
->getJson('/api/sales/warming/vk')
->assertOk()
->assertJsonPath('data.platform', 'vk')
->assertJsonPath('data.min_phones', 2000)
->assertJsonPath('data.status', 'waiting_volume');
});
it('неизвестную площадку отвергает', function () {
$this->actingAs(warmingHead(), 'sales')
->getJson('/api/sales/warming/facebook')
->assertNotFound();
});
it('рубильник пишется в строку площадки', function () {
$this->actingAs(warmingHead(), 'sales')
->patchJson('/api/sales/warming/vk', ['enabled' => true])
->assertOk();
expect(SalesAdAudiencePlatform::forCode('vk')->enabled)->toBeTrue();
});
it('менеджеру доступ закрыт', function () {
$this->actingAs(warmingManager(), 'sales')
->getJson('/api/sales/warming/yandex')
->assertForbidden();
});
it('на вкладке площадки видны только фирмы со строкой этого канала', function () {
// Task C1: firms(vk) отдаёт только фирмы со строкой firm_channels(vk),
// а не весь синхронизированный состав. Фирма без vk-строки на вкладку не попадает.
$vkFirm = SalesAdAudienceFirm::create(['firm_name' => 'Только ВК', 'ch_vk' => true]);
$vkFirm->firmChannels()->create([
'channel' => 'vk', 'status' => 'loaded', 'mode' => 'funnel',
'warming_started_at' => now(), 'warmed_times' => 0,
]);
$yandexFirm = SalesAdAudienceFirm::create(['firm_name' => 'Только Яндекс', 'ch_yandex' => true]);
$yandexFirm->firmChannels()->create([
'channel' => 'yandex', 'status' => 'loaded', 'mode' => 'funnel',
'warming_started_at' => now(), 'warmed_times' => 0,
]);
$resp = $this->actingAs(warmingHead(), 'sales')
->getJson('/api/sales/warming/vk/firms')
->assertOk()
->assertJsonCount(1, 'firms');
expect($resp->json('firms.0.firm_name'))->toBe('Только ВК')
->and($resp->json('firms.0.ch_vk'))->toBeTrue();
});
it('переключатель добавляет и убирает фирму с площадки', function () {
// Task 8: членство площадки теперь — строка firm_channels(vk), а не флаг ch_vk.
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Плавающая', 'warmup_started_at' => now()->subDay()]);
$head = warmingHead();
$this->actingAs($head, 'sales')
->postJson("/api/sales/warming/vk/firms/{$firm->id}/toggle", ['on' => true])
->assertOk();
expect($firm->firmChannels()->where('channel', 'vk')->where('status', 'warming')->count())->toBe(1);
$this->actingAs($head, 'sales')
->postJson("/api/sales/warming/vk/firms/{$firm->id}/toggle", ['on' => false])
->assertOk();
expect($firm->firmChannels()->where('channel', 'vk')->count())->toBe(0);
});
it('отдаёт ВСЕ фирмы прогрева, без фильтра площадки (для модуля СМС)', function () {
SalesAdAudienceFirm::create(['firm_name' => 'Только Яндекс', 'ch_yandex' => true]);
SalesAdAudienceFirm::create(['firm_name' => 'Только ВК', 'ch_vk' => true]);
SalesAdAudienceFirm::create(['firm_name' => 'Нигде', 'ch_yandex' => false, 'ch_vk' => false, 'ch_mts' => false]);
$this->actingAs(warmingHead(), 'sales')
->getJson('/api/sales/ad-audience/firms')
->assertOk()
->assertJsonCount(3, 'firms');
});
it('список всех фирм закрыт для менеджера', function () {
$this->actingAs(warmingManager(), 'sales')
->getJson('/api/sales/ad-audience/firms')
->assertForbidden();
});
it('PATCH сроков пишет в строку площадки, не в state и не в другие площадки', function () {
$head = warmingHead();
$this->actingAs($head, 'sales')
->patchJson('/api/sales/warming/yandex', ['in_work_days' => 20])
->assertOk()
->assertJsonPath('data.durations.in_work_days', 20);
expect(SalesAdAudiencePlatform::forCode('yandex')->in_work_days)->toBe(20)
->and(SalesAdAudiencePlatform::forCode('vk')->in_work_days)->toBe(14) // default, не тронут
->and(SalesAdAudiencePlatform::forCode('mts')->in_work_days)->toBe(14)
->and(SalesAdAudienceState::current()->in_work_days)->toBe(14); // state не тронут
});
it('сроки площадок независимы: yandex=20, vk=5', function () {
$head = warmingHead();
$this->actingAs($head, 'sales')->patchJson('/api/sales/warming/yandex', ['in_work_days' => 20])->assertOk();
$this->actingAs($head, 'sales')->patchJson('/api/sales/warming/vk', ['in_work_days' => 5])->assertOk();
$this->actingAs($head, 'sales')->getJson('/api/sales/warming/yandex')->assertJsonPath('data.durations.in_work_days', 20);
$this->actingAs($head, 'sales')->getJson('/api/sales/warming/vk')->assertJsonPath('data.durations.in_work_days', 5);
expect(SalesAdAudiencePlatform::forCode('yandex')->in_work_days)->toBe(20);
});
it('PATCH days пишется в строку площадки', function () {
$head = warmingHead();
$this->actingAs($head, 'sales')
->patchJson('/api/sales/warming/mts', ['days' => 45])
->assertOk()
->assertJsonPath('data.days', 45);
expect(SalesAdAudiencePlatform::forCode('mts')->days)->toBe(45);
});