Files
portal/app/tests/Feature/Sales/AdAudiencePlatformApiTest.php
T
Дмитрий e15a4d8a69 feat(прогрев ф2): тоггл «Греть здесь» и firmRow — на строки firm_channels
Фаза 2 Этап A, Task 8. toggle пишет строку firm_channels(channel, status=warming,
mode=funnel, warming_started_at=firm.warmup_started_at, warmed_times++) вместо
булева ch_*; off удаляет строку (инкремент двумя шагами, без raw). firmRow на
вкладке площадки считает state/warming_active по строке канала через
decideForChannelRow (нет строки или loaded → stopped/false). firms() eager-грузит
firmChannels (против N+1). Границы этапа A: warming_channels-бейджи, platformState,
channelColumn, allFirms и показ всех фирм — без изменений (ch_*, показ состава →
этап C). ch_* колонки не удаляются. Бамп baseline (+9 actingAs-ложняков).

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

162 lines
7.2 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 () {
SalesAdAudienceFirm::create(['firm_name' => 'Только Яндекс', 'ch_yandex' => true, 'ch_vk' => false]);
SalesAdAudienceFirm::create(['firm_name' => 'Только ВК', 'ch_yandex' => false, 'ch_vk' => true]);
// Состав одинаков на всех вкладках (синхронизирован по Яндексу): на вкладке ВК
// видны ОБЕ фирмы, а отметка ch_vk у каждой отражает подписку именно на ВК.
$resp = $this->actingAs(warmingHead(), 'sales')
->getJson('/api/sales/warming/vk/firms')
->assertOk()
->assertJsonCount(2, 'firms');
$firms = collect($resp->json('firms'))->keyBy('firm_name');
expect($firms['Только ВК']['ch_vk'])->toBeTrue()
->and($firms['Только Яндекс']['ch_vk'])->toBeFalse();
});
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);
});