'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 дня'); }); // ── регистрация (Этап 3): email → tenant + привязка ─────────────────────────── test('регистрация: e-mail не найден → 422', function () { $mgr = pr_user('manager'); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'negotiation']); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", ['action' => 'registered', 'email' => 'nobody@nowhere.tld']) ->assertStatus(422); expect(SalesProspect::find($p->id)->stage)->toBe('negotiation'); }); test('регистрация: найден+свободен → stage=registered, привязка со снимком тарифа', function () { $tariff = SalesTariff::create([ 'name' => 'Т '.uniqid(), 'kind' => 'topup_step', 'params' => ['threshold' => 30000, 'reward' => 500, 'periods' => []], 'is_active' => true, ]); $mgr = pr_user('manager'); $mgr->current_tariff_id = $tariff->id; $mgr->save(); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'negotiation']); $tenant = Tenant::factory()->create(['contact_email' => 'reg'.uniqid().'@example.com']); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", ['action' => 'registered', 'email' => $tenant->contact_email]) ->assertOk() ->assertJsonPath('prospect.stage', 'registered'); $fresh = SalesProspect::find($p->id); expect($fresh->linked_tenant_id)->toBe($tenant->id); expect($fresh->registered_email)->toBe($tenant->contact_email); $a = SalesClientAssignment::where('tenant_id', $tenant->id)->first(); expect($a)->not->toBeNull(); expect($a->sales_user_id)->toBe($mgr->id); expect($a->tariff_id)->toBe($tariff->id); }); test('регистрация: клиент занят другим менеджером → 422, стадия не меняется', function () { $mgr = pr_user('manager'); $other = pr_user('manager'); $p = SalesProspect::factory()->for($mgr, 'salesUser')->create(['stage' => 'negotiation']); $tenant = Tenant::factory()->create(['contact_email' => 'busy'.uniqid().'@example.com']); SalesClientAssignment::create([ 'sales_user_id' => $other->id, 'tenant_id' => $tenant->id, 'tariff_id' => null, 'tariff_kind' => null, 'tariff_params' => [], 'assigned_at' => now(), ]); $this->actingAs($mgr, 'sales') ->patchJson("/api/sales/prospects/{$p->id}", ['action' => 'registered', 'email' => $tenant->contact_email]) ->assertStatus(422); expect(SalesProspect::find($p->id)->stage)->toBe('negotiation'); }); 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'); });