createToken('sales')->plainTextToken. * * Spec: docs/superpowers/plans/2026-06-30-sales-portal.md (Task 1.3) */ uses(DatabaseTransactions::class); // ── helpers (prefixed с clients_ чтобы не конфликтовать с SalesModelsTest) ─── /** * Создаёт SalesUser для тестов clients-эндпоинта. */ function clients_makeSalesUser(string $role = 'manager'): SalesUser { return SalesUser::create([ 'name' => ucfirst($role).' '.uniqid(), 'email' => 'clients-test-'.$role.uniqid().'@test.local', 'password' => Hash::make('secret'), 'role' => $role, 'is_active' => true, ]); } /** * Выполняет GET /api/sales/clients с Bearer-токеном пользователя. * * @param array $params */ function clients_getClients(SalesUser $user, array $params = []): TestResponse { $token = $user->createToken('sales')->plainTextToken; $url = '/api/sales/clients'; if ($params !== []) { $url .= '?'.http_build_query($params); } return test()->withHeader('Authorization', 'Bearer '.$token)->getJson($url); } /** * Создаёт тенанта с tenant_requisites (inn). */ function clients_makeTenantWithInn(string $inn, string $orgName = ''): Tenant { $tenant = Tenant::factory()->create([ 'organization_name' => $orgName !== '' ? $orgName : 'Org '.$inn, 'status' => 'active', 'is_trial' => false, 'balance_rub' => '0.00', 'chargeback_unrecovered_rub' => '0.00', ]); DB::table('tenant_requisites')->insert([ 'tenant_id' => $tenant->id, 'subject_type' => 'legal_entity', 'contact_name' => 'Контакт', 'contact_phone' => '+70000000000', 'inn' => $inn, 'created_at' => now(), 'updated_at' => now(), ]); return $tenant; } /** * Присваивает тенанта менеджеру с опциональным снимком тарифа. */ function clients_assignTenant(SalesUser $manager, Tenant $tenant, ?SalesTariff $tariff = null): SalesClientAssignment { return SalesClientAssignment::create([ 'sales_user_id' => $manager->id, 'tenant_id' => $tenant->id, 'tariff_id' => $tariff?->id, 'tariff_kind' => $tariff?->kind, 'tariff_params' => $tariff !== null ? $tariff->params : [], 'assigned_at' => now(), ]); } /** * Вставляет deal для теста period-фильтра. */ function clients_insertDeal(int $tenantId, string $receivedAt): void { // Создаём проект если нет $projectId = (int) DB::table('projects') ->where('tenant_id', $tenantId) ->value('id'); if ($projectId === 0) { $projectId = (int) DB::table('projects')->insertGetId([ 'tenant_id' => $tenantId, 'name' => 'Period Test Project', 'tag' => 'period-test-'.uniqid(), 'is_active' => true, 'daily_limit_target' => 10, 'delivery_days_mask' => 127, 'created_at' => now(), 'updated_at' => now(), ]); } DB::table('deals')->insert([ 'tenant_id' => $tenantId, 'project_id' => $projectId, 'source_crm_id' => rand(100_000_000, 999_999_999), 'phone' => '7'.str_pad((string) rand(0, 9_999_999_999), 10, '0', STR_PAD_LEFT), 'status' => 'new', 'is_test' => false, 'received_at' => $receivedAt, 'created_at' => now(), 'updated_at' => now(), ]); } // ── тесты ──────────────────────────────────────────────────────────────────── test('менеджер видит только своих 2 клиентов из 3', function () { $manager = clients_makeSalesUser('manager'); $manager2 = clients_makeSalesUser('manager'); $t1 = clients_makeTenantWithInn('001', 'Клиент 1'); $t2 = clients_makeTenantWithInn('002', 'Клиент 2'); $t3 = clients_makeTenantWithInn('003', 'Клиент 3'); clients_assignTenant($manager, $t1); clients_assignTenant($manager, $t2); clients_assignTenant($manager2, $t3); $response = clients_getClients($manager); $response->assertOk(); $data = $response->json('data'); expect($data)->toBeArray()->toHaveCount(2); $tenantIds = array_column($data, 'tenant_id'); expect($tenantIds)->toContain($t1->id)->toContain($t2->id) ->not->toContain($t3->id); }); test('начальник видит всех 3 клиентов', function () { $head = clients_makeSalesUser('head'); $manager = clients_makeSalesUser('manager'); $t1 = clients_makeTenantWithInn('101'); $t2 = clients_makeTenantWithInn('102'); $t3 = clients_makeTenantWithInn('103'); clients_assignTenant($manager, $t1); clients_assignTenant($manager, $t2); clients_assignTenant($manager, $t3); $response = clients_getClients($head); $response->assertOk(); $data = $response->json('data'); $tenantIds = array_column($data, 'tenant_id'); expect($tenantIds)->toContain($t1->id)->toContain($t2->id)->toContain($t3->id); }); test('менеджер без назначений видит 0 клиентов', function () { $manager2 = clients_makeSalesUser('manager'); $manager1 = clients_makeSalesUser('manager'); $t1 = clients_makeTenantWithInn('201'); clients_assignTenant($manager1, $t1); $response = clients_getClients($manager2); $response->assertOk(); expect($response->json('data'))->toBeArray()->toHaveCount(0); }); test('строка ответа содержит нужные поля и earned_rub=null', function () { $manager = clients_makeSalesUser('manager'); $tariff = SalesTariff::create([ 'name' => 'Тариф Тест', 'kind' => 'percent_oborot', 'params' => ['percent' => 10], 'is_active' => true, ]); $t1 = clients_makeTenantWithInn('301', 'ООО Тест'); clients_assignTenant($manager, $t1, $tariff); $response = clients_getClients($manager); $response->assertOk(); $data = $response->json('data'); expect($data)->toHaveCount(1); $row = $data[0]; expect($row)->toHaveKey('tenant_id') ->toHaveKey('organization_name') ->toHaveKey('inn') ->toHaveKey('status') ->toHaveKey('tariff_name') ->toHaveKey('earned_rub') ->toHaveKey('leads_delivered') ->toHaveKey('oborot_rub') ->toHaveKey('runway_days') ->toHaveKey('projects_count'); expect($row['organization_name'])->toBe('ООО Тест'); expect($row['inn'])->toBe('301'); expect($row['tariff_name'])->toBe('Тариф Тест'); expect($row['earned_rub'])->toBeNull(); }); test('status derivation: is_trial → trial, suspended → suspended, balance_rub < 0 → overdue, active → active', function () { $head = clients_makeSalesUser('head'); $manager = clients_makeSalesUser('manager'); $tTrial = Tenant::factory()->create(['is_trial' => true, 'status' => 'active', 'balance_rub' => '0.00', 'chargeback_unrecovered_rub' => '0.00']); clients_assignTenant($manager, $tTrial); $tSuspended = Tenant::factory()->create(['is_trial' => false, 'status' => 'suspended', 'balance_rub' => '0.00', 'chargeback_unrecovered_rub' => '0.00']); clients_assignTenant($manager, $tSuspended); $tOverdue = Tenant::factory()->create(['is_trial' => false, 'status' => 'active', 'balance_rub' => '-100.00', 'chargeback_unrecovered_rub' => '0.00']); clients_assignTenant($manager, $tOverdue); $tActive = Tenant::factory()->create(['is_trial' => false, 'status' => 'active', 'balance_rub' => '500.00', 'chargeback_unrecovered_rub' => '0.00']); clients_assignTenant($manager, $tActive); $response = clients_getClients($head); $response->assertOk(); $data = $response->json('data'); $byId = []; foreach ($data as $row) { $byId[$row['tenant_id']] = $row['status']; } expect($byId[$tTrial->id])->toBe('trial'); expect($byId[$tSuspended->id])->toBe('suspended'); expect($byId[$tOverdue->id])->toBe('overdue'); expect($byId[$tActive->id])->toBe('active'); }); test('period=this включает лиды текущего месяца, period=prev — предыдущего', function () { $manager = clients_makeSalesUser('manager'); $tenant = clients_makeTenantWithInn('401', 'Тест период'); clients_assignTenant($manager, $tenant); $nowMsk = CarbonImmutable::now('Europe/Moscow'); // Лид в текущем месяце (сегодня) $inRange = $nowMsk->format('Y-m-d H:i:s'); // Лид в прошлом месяце (вне текущего периода) $prevMonth = $nowMsk->subMonth()->startOfMonth()->format('Y-m-d H:i:s'); clients_insertDeal($tenant->id, $inRange); clients_insertDeal($tenant->id, $prevMonth); // period=this — 1 лид (только текущий месяц) $respThis = clients_getClients($manager, ['period' => 'this']); $respThis->assertOk(); $dataThis = $respThis->json('data'); expect($dataThis)->toHaveCount(1); expect($dataThis[0]['leads_delivered'])->toBe(1); // period=prev — 1 лид (прошлый месяц) $respPrev = clients_getClients($manager, ['period' => 'prev']); $respPrev->assertOk(); $dataPrev = $respPrev->json('data'); expect($dataPrev)->toHaveCount(1); expect($dataPrev[0]['leads_delivered'])->toBe(1); }); test('запрос без токена → 401', function () { $this->getJson('/api/sales/clients') ->assertUnauthorized(); });