seed(PricingTierSeeder::class); SystemSetting::query()->where('key', 'supplier_webhook_secret') ->update(['value' => 'test-secret-32chars-aaaaaaaaaaaaaa']); }); it('end-to-end: 1 webhook → 3 deal copies for 3 active tenants', function (): void { $supplier = SupplierProject::factory()->create([ 'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'vashinvestor.ru', ]); $tenants = collect(); $projects = collect(); for ($i = 0; $i < 3; $i++) { $t = Tenant::factory()->create(['balance_leads' => 0, 'balance_rub' => '1000.00']); $tenants->push($t); $project = Project::factory()->create([ 'tenant_id' => $t->id, 'supplier_b1_project_id' => $supplier->id, 'signal_type' => 'site', 'signal_identifier' => 'vashinvestor.ru', 'is_active' => true, 'delivered_today' => 0, 'delivered_in_month' => 0, 'delivery_days_mask' => 127, 'region_mask' => 255, 'region_mode' => 'include', 'daily_limit_target' => 10, 'effective_daily_limit_today' => null, ]); $projects->push($project); // v8.26 (Plan 1-2): LeadRouter eligibility — через pivot project_supplier_links, // не legacy supplier_b1_project_id. Без pivot-связи проект не eligible → 0 сделок. linkProjectToSupplier($project, $supplier); createRoutingSnapshotFromProject($project, null, 'site', 'vashinvestor.ru'); } // 4-й tenant — paused (is_active=false). Связь в pivot есть, чтобы проверялся // именно фильтр is_active, а не отсутствие связи. $pausedTenant = Tenant::factory()->create(['balance_leads' => 0, 'balance_rub' => '1000.00']); $pausedProject = Project::factory()->create([ 'tenant_id' => $pausedTenant->id, 'supplier_b1_project_id' => $supplier->id, 'signal_type' => 'site', 'signal_identifier' => 'vashinvestor.ru', 'is_active' => false, ]); linkProjectToSupplier($pausedProject, $supplier); // NB: snapshot для paused-проекта НЕ создаём — SnapshotBackfillCommand в prod // фильтрует `WHERE p.is_active = true`. Это и есть Task 2.5 R-01 защита от // обратного case'а: paused after slepok всё равно НЕ получит лиды (потому что // snapshot fix'нут до его paused). $vid = 432176649; $response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [ 'vid' => $vid, 'project' => 'B1_vashinvestor.ru', 'tag' => 'Ваш инвестор', 'phone' => '79991234567', 'phones' => ['79991234567'], 'time' => time(), ]); $response->assertStatus(202); foreach ($projects as $i => $project) { $tenant = $tenants[$i]; DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'"); $deals = Deal::query() ->where('tenant_id', $tenant->id) ->where('source_crm_id', $vid) ->get(); expect($deals)->toHaveCount(1, "tenant {$tenant->id} expected 1 deal copy"); $deal = $deals->first(); expect($deal->phone)->toBe('79991234567'); expect($deal->project_id)->toBe($project->id); expect($deal->duplicate_of_id)->toBeNull(); expect((string) $tenant->fresh()->balance_rub)->toBe('500.00'); expect($project->fresh()->delivered_today)->toBe(1); expect($project->fresh()->delivered_in_month)->toBe(1); } DB::statement("SET LOCAL app.current_tenant_id = '{$pausedTenant->id}'"); expect(Deal::query() ->where('tenant_id', $pausedTenant->id) ->where('source_crm_id', $vid) ->count())->toBe(0); }); it('end-to-end: lead orphan (no matching projects) — 0 deals, lead stored', function (): void { $vid = 999_888_777; $response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [ 'vid' => $vid, 'project' => 'B1_orphan.ru', 'phone' => '79991234567', 'time' => time(), ]); $response->assertStatus(202); $lead = SupplierLead::where('vid', $vid)->firstOrFail(); expect($lead->processed_at)->not->toBeNull(); expect($lead->deals_created_count)->toBe(0); expect($lead->supplier_project_id)->not->toBeNull(); // resolveOrStub stub });