Files
portal/app/tests/Feature/Sales/SalesProspectApiTest.php
T
Дмитрий 9d8fd8d48c feat(sales): API воронки — список (свои/все+фильтр) + результаты разговора
Этап 1 Task 3–7. GET /api/sales/prospects (менеджер видит свои; начальник —
все + ?manager_id). PATCH /prospects/{id} — переговоры (next_call_at),
недозвон (причина), отказ (причина; запрещён из stage=user). ownership 403.
8 тестов зелёные. Baseline Larastan под Pest-паттерны нового файла.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:33:29 +03:00

139 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\SalesProspect;
use App\Models\SalesUser;
use Illuminate\Foundation\Testing\DatabaseTransactions;
uses(DatabaseTransactions::class);
function pr_user(string $role = 'manager'): SalesUser
{
return SalesUser::create([
'name' => '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_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');
});