Files
portal/app/tests/Feature/Sales/AdAudienceScreenTest.php
T

293 lines
12 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\SalesAdAudienceFirm;
use App\Models\SalesAdAudiencePhone;
use App\Models\SalesAdAudienceState;
use App\Models\SalesProspect;
use App\Models\SalesUser;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\Concerns\SharesSupplierPdo;
/**
* Экран «Реклама на кандидатов» в кабинете начальника (Task 8, бэк).
*
* GET /api/sales/ad-audience — состояние: три числа, настройки, метка синхронизации
* PATCH /api/sales/ad-audience — рубильник и срок хранения номера
*
* Оба метода — ТОЛЬКО начальник (role=head), как у SalesManagersController. Менеджер → 403.
* Аутентификация: $this->actingAs($user, 'sales').
*
* SharesSupplierPdo обязателен: SalesAdAudiencePhone/State прибиты к pgsql_supplier
* (отдельный PDO), без шаринга DatabaseTransactions откатывал бы только соединение
* по умолчанию — записи текли бы из теста в тест.
*/
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
// Task 7: список фирм на прогреве и назначение менеджера — тестам нужны оба
// пользователя сразу, заводим тем же приёмом, что и остальные тесты файла.
beforeEach(function () {
$this->head = ads_head();
$this->manager = ads_manager();
});
// ── helpers (уникальный префикс ads_) ────────────────────────────────────────
function ads_head(): SalesUser
{
return SalesUser::create([
'name' => 'Начальник '.uniqid(),
'email' => 'adshead'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'head',
'is_active' => true,
]);
}
function ads_manager(): SalesUser
{
return SalesUser::create([
'name' => 'Менеджер '.uniqid(),
'email' => 'adsmgr'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'manager',
'is_active' => true,
]);
}
/** Номер в аудитории: живой, просроченный или не отправленный — по аргументам. */
function ads_phone(string $phone, array $attrs = []): SalesAdAudiencePhone
{
return SalesAdAudiencePhone::create(array_merge([
'phone' => $phone,
'added_at' => now(),
'expires_at' => now()->addDays(30),
], $attrs));
}
// ── доступ ────────────────────────────────────────────────────────────────────
test('менеджер не видит экран рекламы', function () {
$this->actingAs(ads_manager(), 'sales')
->getJson('/api/sales/ad-audience')
->assertForbidden();
});
test('менеджер не может тронуть настройки рекламы', function () {
SalesAdAudienceState::current()->update(['enabled' => false, 'days' => 30]);
$this->actingAs(ads_manager(), 'sales')
->patchJson('/api/sales/ad-audience', ['enabled' => true])
->assertForbidden();
expect(SalesAdAudienceState::current()->fresh()->enabled)->toBeFalse();
});
test('без входа экран закрыт', function () {
$this->getJson('/api/sales/ad-audience')->assertUnauthorized();
});
// ── состояние ─────────────────────────────────────────────────────────────────
test('начальник видит три числа по номерам', function () {
SalesAdAudienceState::current()->update([
'enabled' => true, 'days' => 30, 'yandex_segment_id' => 777, 'last_error' => null,
]);
// В рекламе сейчас: живые (не удалённые, срок не вышел) — 3 штуки.
ads_phone('79990000002', ['synced_at' => now(), 'expires_at' => now()->addDays(30)]);
ads_phone('79990000003', ['synced_at' => now(), 'expires_at' => now()->addDays(3)]); // выходит на неделе
ads_phone('79990000004', ['expires_at' => now()->addDays(20)]); // ждёт отправки
// Просроченный и удалённый в счёт не идут.
ads_phone('79990000005', ['synced_at' => now(), 'expires_at' => now()->subDay()]);
ads_phone('79990000006', ['synced_at' => now(), 'removed_at' => now()]);
$response = $this->actingAs(ads_head(), 'sales')
->getJson('/api/sales/ad-audience')
->assertOk();
$response->assertJsonPath('data.in_ads', 3)
->assertJsonPath('data.expiring_week', 1)
->assertJsonPath('data.waiting_sync', 1)
->assertJsonPath('data.enabled', true)
->assertJsonPath('data.days', 30)
->assertJsonPath('data.segment_id', 777);
});
test('пустая аудитория отдаёт нули, а не ошибку', function () {
$this->actingAs(ads_head(), 'sales')
->getJson('/api/sales/ad-audience')
->assertOk()
->assertJsonPath('data.in_ads', 0)
->assertJsonPath('data.expiring_week', 0)
->assertJsonPath('data.waiting_sync', 0);
});
test('ошибка последней синхронизации видна начальнику', function () {
SalesAdAudienceState::current()->update(['last_error' => 'не задан YANDEX_AUDIENCE_TOKEN']);
$this->actingAs(ads_head(), 'sales')
->getJson('/api/sales/ad-audience')
->assertOk()
->assertJsonPath('data.last_error', 'не задан YANDEX_AUDIENCE_TOKEN');
});
// ── настройки ─────────────────────────────────────────────────────────────────
test('начальник включает рекламу и меняет срок', function () {
SalesAdAudienceState::current()->update(['enabled' => false, 'days' => 30]);
$this->actingAs(ads_head(), 'sales')
->patchJson('/api/sales/ad-audience', ['enabled' => true, 'days' => 45])
->assertOk()
->assertJsonPath('data.enabled', true)
->assertJsonPath('data.days', 45);
$state = SalesAdAudienceState::current()->fresh();
expect($state->enabled)->toBeTrue()->and($state->days)->toBe(45);
});
test('можно менять только рубильник, срок остаётся прежним', function () {
SalesAdAudienceState::current()->update(['enabled' => true, 'days' => 15]);
$this->actingAs(ads_head(), 'sales')
->patchJson('/api/sales/ad-audience', ['enabled' => false])
->assertOk()
->assertJsonPath('data.enabled', false)
->assertJsonPath('data.days', 15);
});
test('бессмысленный срок отвергается', function () {
SalesAdAudienceState::current()->update(['days' => 30]);
foreach ([0, -5, 400, 'много'] as $bad) {
$this->actingAs(ads_head(), 'sales')
->patchJson('/api/sales/ad-audience', ['days' => $bad])
->assertStatus(422);
}
expect(SalesAdAudienceState::current()->fresh()->days)->toBe(30);
});
// ── Task 7: список прогрева и назначение менеджера ──────────────────────────
it('начальник видит список фирм на прогреве', function () {
$firm = SalesAdAudienceFirm::create([
'firm_name' => 'Тестовая стоматология',
'city' => 'Москва',
'warmup_started_at' => now()->subDays(2),
]);
SalesAdAudiencePhone::create([
'phone' => '79990000001',
'firm_id' => $firm->id,
'expires_at' => now()->addDay(),
'state' => 'active',
]);
$this->actingAs($this->head, 'sales')
->getJson('/api/sales/ad-audience/firms')
->assertOk()
->assertJsonPath('firms.0.firm_name', 'Тестовая стоматология')
->assertJsonPath('firms.0.state', 'active')
->assertJsonPath('firms.0.days_in_ads', 2);
});
it('менеджер список прогрева не видит', function () {
$this->actingAs($this->manager, 'sales')
->getJson('/api/sales/ad-audience/firms')
->assertStatus(403);
});
it('начальник назначает менеджера и получает карточку в воронке', function () {
$firm = SalesAdAudienceFirm::create([
'firm_name' => 'Тестовая стоматология',
'firm_inn' => '7700000001',
'city' => 'Москва',
'payload' => ['site' => 'example.test'],
'contacts' => [['name' => 'Директор', 'phones' => ['79990000001']]],
]);
$this->actingAs($this->head, 'sales')
->postJson("/api/sales/ad-audience/firms/{$firm->id}/assign", [
'sales_user_id' => $this->manager->id,
])
->assertOk();
$firm->refresh();
$prospect = SalesProspect::findOrFail($firm->prospect_id);
expect($prospect->sales_user_id)->toBe($this->manager->id)
->and($prospect->stage)->toBe('new')
->and($prospect->assigned_by)->toBe($this->head->id)
->and($prospect->contacts)->toHaveCount(1)
->and($firm->assigned_at)->not->toBeNull();
});
it('повторное назначение той же фирмы отвергается', function () {
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Тестовая стоматология', 'prospect_id' => 1]);
$this->actingAs($this->head, 'sales')
->postJson("/api/sales/ad-audience/firms/{$firm->id}/assign", ['sales_user_id' => $this->manager->id])
->assertStatus(422);
});
// ── Task 5: массовая смена площадки прогрева ────────────────────────────────
it('начальник меняет площадку сразу у нескольких фирм', function () {
$first = SalesAdAudienceFirm::create([
'firm_name' => 'Первая', 'warmup_started_at' => now(), 'channels' => 'yandex',
]);
$second = SalesAdAudienceFirm::create([
'firm_name' => 'Вторая', 'warmup_started_at' => now(), 'channels' => 'yandex',
]);
$this->actingAs($this->head, 'sales')
->postJson('/api/sales/ad-audience/channels', [
'firm_ids' => [$first->id, $second->id],
'channels' => 'both',
])
->assertOk()
->assertJson(['updated' => 2]);
expect($first->fresh()->channels)->toBe('both')
->and($second->fresh()->channels)->toBe('both');
});
it('менеджеру менять площадку нельзя', function () {
$firm = SalesAdAudienceFirm::create([
'firm_name' => 'Чужая', 'warmup_started_at' => now(),
]);
$this->actingAs($this->manager, 'sales')
->postJson('/api/sales/ad-audience/channels', [
'firm_ids' => [$firm->id], 'channels' => 'vk',
])
->assertStatus(403);
expect($firm->fresh()->channels)->toBe('yandex');
});
it('в списке фирм видно, где греем', function () {
SalesAdAudienceFirm::create([
'firm_name' => 'Показательная', 'warmup_started_at' => now(), 'channels' => 'vk',
]);
$this->actingAs($this->head, 'sales')
->getJson('/api/sales/ad-audience/firms')
->assertOk()
->assertJsonFragment(['channels' => 'vk']);
});
// ── Task 9: состояние канала ВК на экране начальника ────────────────────────
it('показывает начальнику состояние канала ВК', function () {
SalesAdAudienceState::current()->update(['vk_status' => 'waiting_volume']);
$this->actingAs(ads_head(), 'sales')
->getJson('/api/sales/ad-audience')
->assertOk()
->assertJsonPath('data.vk_status', 'waiting_volume');
});