'U '.uniqid(), 'email' => 'pr'.uniqid().'@s.local', 'password' => bcrypt('secret'), 'role' => $role, 'is_active' => true, ]); } // ── index: менеджер ─────────────────────────────────────────────────────────── test('менеджер видит только свои карточки, сгруппированные по стадии', function () { $mgr = pr_user('manager'); $other = pr_user('manager'); SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'new', 'firm_name' => 'МОЯ']); SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'negotiation']); SalesProspect::factory()->for($other, 'salesUser')->create(['stage' => 'new', 'firm_name' => 'ЧУЖАЯ']); $res = $this->actingAs($mgr, 'sales')->getJson('/api/sales/prospects'); $res->assertOk(); $names = collect($res->json('prospects'))->pluck('firm_name'); expect($names)->toContain('МОЯ')->not->toContain('ЧУЖАЯ'); expect($res->json('by_stage'))->toHaveKeys(['new', 'negotiation']); }); test('неаутентифицированный GET /api/sales/prospects → 401', function () { $this->getJson('/api/sales/prospects')->assertUnauthorized(); }); // ── index: начальник + фильтр ───────────────────────────────────────────────── test('начальник видит карточки всех менеджеров', function () { $head = pr_user('head'); $m1 = pr_user('manager'); $m2 = pr_user('manager'); SalesProspect::factory()->for($m1, 'salesUser')->create(['firm_name' => 'A1']); SalesProspect::factory()->for($m2, 'salesUser')->create(['firm_name' => 'B2']); $res = $this->actingAs($head, 'sales')->getJson('/api/sales/prospects'); $names = collect($res->json('prospects'))->pluck('firm_name'); expect($names)->toContain('A1')->toContain('B2'); }); test('начальник фильтрует по ?manager_id', function () { $head = pr_user('head'); $m1 = pr_user('manager'); $m2 = pr_user('manager'); SalesProspect::factory()->for($m1, 'salesUser')->create(['firm_name' => 'A1']); SalesProspect::factory()->for($m2, 'salesUser')->create(['firm_name' => 'B2']); $res = $this->actingAs($head, 'sales')->getJson('/api/sales/prospects?manager_id='.$m1->id); $names = collect($res->json('prospects'))->pluck('firm_name'); expect($names)->toContain('A1')->not->toContain('B2'); }); test('начальник получает manager_counts по всем менеджерам (не зависит от фильтра)', function () { $head = pr_user('head'); $m1 = pr_user('manager'); $m2 = pr_user('manager'); SalesProspect::factory()->count(3)->for($m1, 'salesUser')->create(); SalesProspect::factory()->count(1)->for($m2, 'salesUser')->create(); // Даже при фильтре по m1 счётчики остаются полными по всем менеджерам. $res = $this->actingAs($head, 'sales')->getJson('/api/sales/prospects?manager_id='.$m1->id); $res->assertOk(); expect($res->json('manager_counts.'.$m1->id))->toBe(3); expect($res->json('manager_counts.'.$m2->id))->toBe(1); }); test('менеджеру manager_counts не отдаётся (пустой объект)', function () { $mgr = pr_user('manager'); SalesProspect::factory()->for($mgr, 'salesUser')->create(); $res = $this->actingAs($mgr, 'sales')->getJson('/api/sales/prospects'); $res->assertOk(); expect($res->json('manager_counts'))->toBe([]); }); test('менеджер не может через manager_id увидеть чужие', function () { $m1 = pr_user('manager'); $m2 = pr_user('manager'); SalesProspect::factory()->for($m2, 'salesUser')->create(['firm_name' => 'ЧУЖАЯ']); $res = $this->actingAs($m1, 'sales')->getJson('/api/sales/prospects?manager_id='.$m2->id); expect(collect($res->json('prospects'))->pluck('firm_name'))->not->toContain('ЧУЖАЯ'); }); // ── update: результаты разговора ────────────────────────────────────────────── test('переговоры: ставит stage + next_call_at; без времени → 422; чужую → 403', function () { $mgr = pr_user('manager'); $other = pr_user('manager'); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'new']); $foreign = SalesProspect::factory()->for($other, 'salesUser')->create(['stage' => 'new']); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", ['action' => 'negotiation']) ->assertStatus(422); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", [ 'action' => 'negotiation', 'next_call_at' => '2026-07-20T10:30:00+03:00', ]) ->assertOk() ->assertJsonPath('prospect.stage', 'negotiation'); expect(SalesProspect::find($p->id)->next_call_at)->not->toBeNull(); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$foreign->id}", [ 'action' => 'negotiation', 'next_call_at' => '2026-07-20T10:30:00+03:00', ]) ->assertStatus(403); }); test('недозвон: причина обязательна; со причиной → no_answer', function () { $mgr = pr_user('manager'); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'new']); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", ['action' => 'no_answer']) ->assertStatus(422); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", ['action' => 'no_answer', 'reason' => 'не берёт трубку 3 дня']) ->assertOk()->assertJsonPath('prospect.stage', 'no_answer'); expect(SalesProspect::find($p->id)->reason)->toBe('не берёт трубку 3 дня'); }); test('отказ: причина обязательна; из user запрещён', function () { $mgr = pr_user('manager'); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'negotiation']); $userStage = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'user']); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", ['action' => 'rejected']) ->assertStatus(422); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", ['action' => 'rejected', 'reason' => 'дорого']) ->assertOk()->assertJsonPath('prospect.stage', 'rejected'); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$userStage->id}", ['action' => 'rejected', 'reason' => 'ушёл']) ->assertStatus(422); expect(SalesProspect::find($userStage->id)->stage)->toBe('user'); });