6ceec17439
Действие «Зарегистрировался» (PATCH action=registered): email→tenant, привязка SalesClientAssignment со снимком тарифа, linked_tenant_id+stage=registered; клиент занят другим → 422. Диалог карточки: пункт «Зарегистрировался» + поле e-mail. SalesProspectsAdvanceJob (каждые 15 мин, pgsql_admin): по balance_transactions считает стадию — Σtopup≥30000→user, >0→topped_up, есть расход при 0 topup→testing, иначе registered; ручные/отказные стадии не трогает. Схема НЕ меняется. Гейты: бэк 19/19, фронт 9/9, Larastan 0. 🪤 property $connection конфликтовал с трейтом Queueable → переименовал в $dbConnection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
221 lines
10 KiB
PHP
221 lines
10 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Models\SalesClientAssignment;
|
||
use App\Models\SalesProspect;
|
||
use App\Models\SalesTariff;
|
||
use App\Models\SalesUser;
|
||
use App\Models\Tenant;
|
||
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_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');
|
||
});
|