6775f58003
Фаза 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>
170 lines
7.4 KiB
PHP
170 lines
7.4 KiB
PHP
<?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);
|
||
});
|