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']); });