6536c19c96
Экран «Лиды» (/admin/leads): серверный список с фильтрами (дата/канал/поставщик/
статус/поиск) + пагинация (масштаб 10⁴+ лидов). Карточка лида (/admin/leads/{id}):
полная цепочка — ОТКУДА (поставщик B1/B2/B3 + канал + источник + регион) → КОМУ
(сделки клиентов через deals.source_crm_id = supplier_leads.vid). Дашборд: drill
Лиды +топ-10 последних + «Открыть все лиды →». Nav-пункт «Лиды». ПДн-телефон
маскируется (152-ФЗ). Тесты: backend 3 + FE 5 (38 FE всего зелёные).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
4.3 KiB
PHP
90 lines
4.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
uses(DatabaseTransactions::class);
|
||
|
||
function seedLeadTenant(): int
|
||
{
|
||
return DB::table('tenants')->insertGetId([
|
||
'subdomain' => 'acme'.uniqid(), 'organization_name' => 'Acme', 'contact_email' => 'a@acme.ru',
|
||
'status' => 'active', 'is_trial' => false, 'balance_rub' => 0, 'balance_leads' => 0,
|
||
'chargeback_unrecovered_rub' => 0, 'created_at' => now(), 'updated_at' => now(),
|
||
]);
|
||
}
|
||
|
||
function seedSupplierProject(string $signal, string $key): int
|
||
{
|
||
return DB::table('supplier_projects')->insertGetId([
|
||
'platform' => 'B1', 'signal_type' => $signal, 'unique_key' => $key,
|
||
'current_limit' => 10, 'sync_status' => 'ok', 'created_at' => now(), 'updated_at' => now(),
|
||
]);
|
||
}
|
||
|
||
it('GET /api/admin/leads — пагинированный список с фильтром по каналу', function () {
|
||
$sp = seedSupplierProject('site', 'okna.ru');
|
||
DB::table('supplier_leads')->insert([
|
||
['supplier_project_id' => $sp, 'platform' => 'B1', 'raw_payload' => json_encode(['x' => 1]),
|
||
'phone' => '+79135397707', 'vid' => 1001, 'received_at' => now()->subHour(),
|
||
'processed_at' => now()->subHour(), 'deals_created_count' => 2],
|
||
['supplier_project_id' => null, 'platform' => 'B2', 'raw_payload' => json_encode(['x' => 2]),
|
||
'phone' => '+79990001122', 'vid' => 1002, 'received_at' => now()->subDays(2),
|
||
'processed_at' => null, 'deals_created_count' => 0],
|
||
]);
|
||
|
||
$res = $this->getJson('/api/admin/leads?per_page=10');
|
||
$res->assertOk();
|
||
$res->assertJsonStructure([
|
||
'data' => [['id', 'received_at', 'platform', 'channel', 'source', 'region_code', 'phone_masked', 'deals_created_count', 'status']],
|
||
'total', 'page', 'per_page',
|
||
]);
|
||
expect($res->json('total'))->toBeGreaterThanOrEqual(2);
|
||
|
||
// фильтр по каналу site → только лид с supplier_project site
|
||
$res2 = $this->getJson('/api/admin/leads?channel=site');
|
||
expect(collect($res2->json('data'))->pluck('channel')->unique()->all())->toBe(['site']);
|
||
});
|
||
|
||
it('телефон в списке маскируется', function () {
|
||
$sp = seedSupplierProject('call', '+74950000000');
|
||
DB::table('supplier_leads')->insert([
|
||
'supplier_project_id' => $sp, 'platform' => 'B1', 'raw_payload' => json_encode([]),
|
||
'phone' => '+79135397707', 'vid' => 2001, 'received_at' => now(), 'deals_created_count' => 0,
|
||
]);
|
||
$res = $this->getJson('/api/admin/leads?channel=call');
|
||
$masked = collect($res->json('data'))->firstWhere('id', '!=', null)['phone_masked'] ?? '';
|
||
expect($masked)->not->toContain('9135397'); // середина скрыта
|
||
expect($masked)->toContain('**');
|
||
});
|
||
|
||
it('GET /api/admin/leads/{id} — карточка с цепочкой: источник + сделки клиентов', function () {
|
||
$sp = seedSupplierProject('site', 'okna.ru');
|
||
$leadId = DB::table('supplier_leads')->insertGetId([
|
||
'supplier_project_id' => $sp, 'platform' => 'B1', 'raw_payload' => json_encode(['tag' => 77]),
|
||
'phone' => '+79135397707', 'vid' => 5005, 'received_at' => now()->subHour(),
|
||
'processed_at' => now()->subHour(), 'deals_created_count' => 1, 'resolved_subject_code' => 77,
|
||
]);
|
||
$tenant = seedLeadTenant();
|
||
$project = DB::table('projects')->insertGetId([
|
||
'tenant_id' => $tenant, 'name' => 'Проект', 'is_active' => true,
|
||
'created_at' => now(), 'updated_at' => now(),
|
||
]);
|
||
DB::table('deals')->insert([
|
||
'tenant_id' => $tenant, 'project_id' => $project, 'source_crm_id' => 5005, 'phone' => '+79135397707',
|
||
'status' => 'new', 'is_test' => false, 'received_at' => now()->subHour(), 'created_at' => now(), 'updated_at' => now(),
|
||
]);
|
||
|
||
$res = $this->getJson("/api/admin/leads/{$leadId}");
|
||
$res->assertOk();
|
||
$res->assertJsonStructure([
|
||
'lead' => ['id', 'platform', 'phone_masked', 'received_at', 'region_code', 'status'],
|
||
'source' => ['platform', 'channel', 'identifier'],
|
||
'deals' => [['tenant_id', 'tenant_name', 'status']],
|
||
]);
|
||
expect($res->json('source.identifier'))->toBe('okna.ru');
|
||
expect($res->json('deals.0.tenant_name'))->toBe('Acme');
|
||
});
|