Files
portal/app/tests/Feature/Sales/AdAudienceScreenTest.php
T
Дмитрий 44b892c771 feat(sales): экран «Реклама на кандидатов» в кабинете начальника
GET/PATCH /api/sales/ad-audience — три числа (в рекламе сейчас, выходят на
этой неделе, ждут отправки), рубильник и срок хранения номера. Доступ только
роли head — гейт denyIfNotHead зеркалит SalesManagersController.

Экран объясняет обычными словами, что список пополняется только кнопкой
«Отправить в рекламу» в Поиске клиентов, и предупреждает, когда номеров
меньше 100 (Яндекс не запустит рекламу).

Тесты: 9 в tests/Feature/Sales/AdAudienceScreenTest.php (доступ, три числа,
смена настроек, валидация срока). SharesSupplierPdo — модели на pgsql_supplier.

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

163 lines
7.1 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\SalesAdAudiencePhone;
use App\Models\SalesAdAudienceState;
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);
// ── 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('79990000009', ['synced_at' => now(), 'expires_at' => now()->addDays(30)]);
ads_phone('79990000009', ['synced_at' => now(), 'expires_at' => now()->addDays(3)]); // выходит на неделе
ads_phone('79990000009', ['expires_at' => now()->addDays(20)]); // ждёт отправки
// Просроченный и удалённый в счёт не идут.
ads_phone('79990000009', ['synced_at' => now(), 'expires_at' => now()->subDay()]);
ads_phone('79990000009', ['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);
});