ucfirst($role).' '.uniqid(), 'email' => 'card-test-'.$role.uniqid().'@test.local', 'password' => Hash::make('secret'), 'role' => $role, 'is_active' => true, ]); } function card_getClientCard(SalesUser $user, int $tenantId, array $params = []): TestResponse { $token = $user->createToken('sales')->plainTextToken; $url = '/api/sales/clients/'.$tenantId; if ($params !== []) { $url .= '?'.http_build_query($params); } return test()->withHeader('Authorization', 'Bearer '.$token)->getJson($url); } /** * Создаёт тенанта с реквизитами. */ function card_makeTenant(string $orgName = 'Тест-Орг', float $balance = 1000.0): Tenant { $tenant = Tenant::factory()->create([ 'organization_name' => $orgName, 'status' => 'active', 'is_trial' => false, 'balance_rub' => (string) $balance, 'chargeback_unrecovered_rub' => '0.00', 'desired_daily_numbers' => 5, 'contact_email' => 'contact@test.local', 'last_activity_at' => now(), ]); DB::table('tenant_requisites')->insert([ 'tenant_id' => $tenant->id, 'subject_type' => 'legal_entity', 'contact_name' => 'Иванов Иван Иванович', 'contact_phone' => '+70001112233', 'inn' => '1234567890', 'legal_address' => 'г. Москва, ул. Тестовая, 1', 'created_at' => now(), 'updated_at' => now(), ]); return $tenant; } /** * Присваивает тенанта менеджеру. */ function card_assignTenant(SalesUser $manager, Tenant $tenant): SalesClientAssignment { return SalesClientAssignment::create([ 'sales_user_id' => $manager->id, 'tenant_id' => $tenant->id, 'tariff_id' => null, 'tariff_kind' => null, 'tariff_params' => [], 'assigned_at' => now(), ]); } /** * Создаёт проект для тенанта и возвращает его id. */ function card_makeProject(int $tenantId): int { // signal_type=NULL пропускает constraint chk_projects_signal_identifier_required // (constraint требует signal_identifier только для site/call) return (int) DB::table('projects')->insertGetId([ 'tenant_id' => $tenantId, 'name' => 'Тест-Проект '.uniqid(), 'tag' => 'tag-'.uniqid(), 'is_active' => true, 'signal_type' => null, 'daily_limit_target' => 10, 'delivery_days_mask' => 127, 'created_at' => now(), 'updated_at' => now(), ]); } /** * Вставляет сделку с конкретным телефоном и возвращает её deal_id. */ function card_insertDeal(int $tenantId, int $projectId, string $phone, string $receivedAt): int { return (int) DB::table('deals')->insertGetId([ 'tenant_id' => $tenantId, 'project_id' => $projectId, 'source_crm_id' => rand(100_000_000, 999_999_999), 'phone' => $phone, 'status' => 'new', 'is_test' => false, 'received_at' => $receivedAt, 'created_at' => now(), 'updated_at' => now(), ]); } /** * Вставляет запись lead_charges для тенанта. * * @param int $dealId — id сделки (NOT NULL в схеме) * @param string $dealReceivedAt — received_at сделки (NOT NULL, composite FK) */ function card_insertCharge(int $tenantId, int $priceKopecks, string $chargedAt, int $dealId, string $dealReceivedAt): void { DB::table('lead_charges')->insert([ 'tenant_id' => $tenantId, 'deal_id' => $dealId, 'deal_received_at' => $dealReceivedAt, 'tier_no' => 1, 'price_per_lead_kopecks' => $priceKopecks, 'charge_source' => 'rub', 'charged_at' => $chargedAt, ]); } /** * Вставляет баланс-транзакцию. */ function card_insertTransaction(int $tenantId, string $type, float $amountRub, string $description = ''): void { DB::table('balance_transactions')->insert([ 'tenant_id' => $tenantId, 'type' => $type, 'amount_rub' => $amountRub, 'description' => $description, 'created_at' => now(), ]); } // ── тесты ──────────────────────────────────────────────────────────────────── test('менеджер открывает своего клиента → 200, profile и kpi присутствуют', function () { $manager = card_makeSalesUser('manager'); $tenant = card_makeTenant('ООО Тест-Клиент', 5000.0); card_assignTenant($manager, $tenant); $response = card_getClientCard($manager, $tenant->id); $response->assertOk(); expect($response->json('profile.organization_name'))->toBe('ООО Тест-Клиент'); expect($response->json('kpi.balance_rub'))->not->toBeNull(); expect($response->json('kpi.earned_rub'))->toBeNull(); }); test('менеджер открывает чужого клиента → 403', function () { $manager = card_makeSalesUser('manager'); $manager2 = card_makeSalesUser('manager'); $tenant = card_makeTenant('Чужой Клиент'); card_assignTenant($manager2, $tenant); // назначен другому менеджеру $response = card_getClientCard($manager, $tenant->id); $response->assertForbidden(); }); test('начальник открывает любого клиента → 200', function () { $head = card_makeSalesUser('head'); $manager = card_makeSalesUser('manager'); $tenant = card_makeTenant('Клиент Начальника'); card_assignTenant($manager, $tenant); $response = card_getClientCard($head, $tenant->id); $response->assertOk(); expect($response->json('profile.organization_name'))->toBe('Клиент Начальника'); }); test('recent_leads — телефоны замаскированы', function () { $manager = card_makeSalesUser('manager'); $tenant = card_makeTenant('Клиент Маска'); card_assignTenant($manager, $tenant); $projectId = card_makeProject($tenant->id); $rawPhone = '79161234567'; $now = CarbonImmutable::now('Europe/Moscow')->format('Y-m-d H:i:s'); card_insertDeal($tenant->id, $projectId, $rawPhone, $now); $response = card_getClientCard($manager, $tenant->id); $response->assertOk(); $leads = $response->json('recent_leads'); expect($leads)->toBeArray()->not->toBeEmpty(); // Маска: «79** *** ** 67» (первые 2 цифры + ** *** ** + 2 последних) // Сырой телефон НЕ должен быть виден $phone = $leads[0]['phone_masked']; expect($phone)->not->toBe($rawPhone); expect($phone)->toContain('**'); }); test('kpi содержит leads_delivered, avg_lead_price_rub, earned_rub=null', function () { $manager = card_makeSalesUser('manager'); $tenant = card_makeTenant('Клиент KPI', 2000.0); card_assignTenant($manager, $tenant); $projectId = card_makeProject($tenant->id); $now = CarbonImmutable::now('Europe/Moscow'); $inPeriod = $now->format('Y-m-d H:i:s'); $dealId = card_insertDeal($tenant->id, $projectId, '79001112233', $inPeriod); card_insertCharge($tenant->id, 2500, $inPeriod, $dealId, $inPeriod); // 25.00 руб за лид $response = card_getClientCard($manager, $tenant->id); $response->assertOk(); expect($response->json('kpi.leads_delivered'))->toBe(1); // avg_lead_price_rub = 2500 kopecks / 100 / 1 лид = 25 руб expect((float) $response->json('kpi.avg_lead_price_rub'))->toBe(25.0); expect($response->json('kpi.earned_rub'))->toBeNull(); }); test('projects содержит проект тенанта', function () { $manager = card_makeSalesUser('manager'); $tenant = card_makeTenant('Клиент Проекты'); card_assignTenant($manager, $tenant); card_makeProject($tenant->id); $response = card_getClientCard($manager, $tenant->id); $response->assertOk(); $projects = $response->json('projects'); expect($projects)->toBeArray()->not->toBeEmpty(); expect($projects[0])->toHaveKey('id'); expect($projects[0])->toHaveKey('name'); expect($projects[0])->toHaveKey('signal_type'); expect($projects[0])->toHaveKey('daily_limit_target'); expect($projects[0])->toHaveKey('delivered_today'); expect($projects[0])->toHaveKey('status'); }); test('activity содержит balance_transactions', function () { $manager = card_makeSalesUser('manager'); $tenant = card_makeTenant('Клиент Актив', 1500.0); card_assignTenant($manager, $tenant); card_insertTransaction($tenant->id, 'topup', 500.0, 'Тестовое пополнение'); $response = card_getClientCard($manager, $tenant->id); $response->assertOk(); $activity = $response->json('activity'); expect($activity)->toBeArray()->not->toBeEmpty(); $tx = $activity[0]; expect($tx)->toHaveKey('type'); expect($tx)->toHaveKey('amount_rub'); expect($tx)->toHaveKey('description'); expect($tx)->toHaveKey('created_at'); }); test('запрос без токена → 401', function () { test()->getJson('/api/sales/clients/1') ->assertUnauthorized(); });