seed(PricingTierSeeder::class); DB::statement("SELECT set_config('app.current_tenant_id', '0', true)"); SystemSetting::query() ->where('key', 'supplier_webhook_secret') ->update(['value' => 'test-secret-32chars-aaaaaaaaaaaaaa']); SystemSetting::query() ->where('key', 'supplier_ip_allowlist') ->update(['value' => '[]']); }); function directDispatchJob(int $supplierLeadId): void { (new RouteSupplierLeadJob($supplierLeadId))->handle( app(LeadRouter::class), app(SupplierProjectResolver::class), app(NotificationService::class), app(LedgerService::class), app(LeadDistributor::class), app(RegionTagResolver::class), ); } it('webhook with non-B-prefix project is accepted (202) and platform=DIRECT', function (): void { $response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [ 'vid' => 9999001, 'project' => 'client.carmoney.ru', 'phone' => '79991234567', 'time' => time(), ]); $response->assertStatus(202); $lead = SupplierLead::where('vid', 9999001)->first(); expect($lead)->not->toBeNull(); expect($lead->platform)->toBe('DIRECT'); }); it('SupplierProjectResolver creates DIRECT supplier_project for non-B project', function (): void { $resolver = app(SupplierProjectResolver::class); $sp = $resolver->resolveOrStub('DIRECT', 'site', 'client.carmoney.ru'); expect($sp->platform)->toBe('DIRECT'); expect($sp->unique_key)->toBe('client.carmoney.ru'); expect($sp->signal_type)->toBe('site'); }); it('RouteSupplierLeadJob delivers DIRECT lead to matching project via signal_identifier fallback', function (): void { // Создаём Лидерра-проект с тем же signal_identifier, что и DIRECT-supplier_project. // ВАЖНО: НЕ создаём project_supplier_links — Phase 3 fallback должен матчить // только по signal_type+signal_identifier. $tenant = Tenant::factory()->create([ 'balance_leads' => 0, 'balance_rub' => '1000.00', 'delivered_in_month' => 0, ]); $project = Project::factory()->create([ 'tenant_id' => $tenant->id, 'signal_type' => 'site', 'signal_identifier' => 'client.carmoney.ru', 'is_active' => true, 'daily_limit_target' => 10, 'effective_daily_limit_today' => 10, 'delivered_today' => 0, 'delivery_days_mask' => 127, 'region_mask' => 255, ]); createRoutingSnapshotFromProject($project, signalType: 'site', signalIdentifier: 'client.carmoney.ru'); $lead = SupplierLead::factory()->create([ 'platform' => 'DIRECT', 'phone' => '79991234567', 'vid' => 9999002, 'raw_payload' => ['vid' => 9999002, 'project' => 'client.carmoney.ru', 'phone' => '79991234567', 'time' => time()], 'received_at' => now(), ]); directDispatchJob($lead->id); $deal = Deal::where('tenant_id', $tenant->id) ->where('phone', '79991234567') ->first(); expect($deal)->not->toBeNull(); expect($deal->project_id)->toBe($project->id); expect($deal->source_crm_id)->toBe(9999002); }); it('numeric-only project (e.g. 79135191264 callback) accepted as DIRECT', function (): void { // Поставщик иногда шлёт project=телефонный номер для callback-проектов. $response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [ 'vid' => 9999003, 'project' => '79135191264', 'phone' => '79991234567', 'time' => time(), ]); $response->assertStatus(202); $lead = SupplierLead::where('vid', 9999003)->first(); expect($lead->platform)->toBe('DIRECT'); }); it('existing B1 webhooks still work as platform=B1 (regression)', function (): void { $response = $this->postJson('/api/webhook/supplier/test-secret-32chars-aaaaaaaaaaaaaa', [ 'vid' => 9999004, 'project' => 'B1_krk-finance.ru', 'phone' => '79991234567', 'time' => time(), ]); $response->assertStatus(202); expect(SupplierLead::where('vid', 9999004)->first()->platform)->toBe('B1'); }); it('SupplierProjectResolver still rejects unknown platforms other than DIRECT', function (): void { $resolver = app(SupplierProjectResolver::class); expect(fn () => $resolver->resolveOrStub('UNKNOWN', 'site', 'foo.ru')) ->toThrow(InvalidArgumentException::class); });