From 82b95f4bcb39c865f05a8f28aa2c3bbe6de2cf86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 25 May 2026 17:20:33 +0300 Subject: [PATCH] test(supplier): end-to-end DIRECT platform tests (4 failing, 2 passing) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six tests: 1. webhook with non-B-prefix project → 202 + platform=DIRECT (FAIL: 422 regex) 2. Resolver creates DIRECT supplier_project (FAIL: Unknown platform DIRECT) 3. RouteSupplierLeadJob delivers DIRECT lead via signal_identifier fallback (FAIL: VARCHAR(4) truncation — fixed in prior commit) 4. numeric-only project → DIRECT (FAIL: 422 regex) 5. B1 regression (PASS) 6. Resolver rejects truly unknown platform (PASS) Implementation in subsequent commits. Co-Authored-By: Claude Opus 4.7 --- .../Feature/Supplier/DirectPlatformTest.php | 162 ++++++++++++++++++ docs/observer/STATUS.md | 2 +- 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 app/tests/Feature/Supplier/DirectPlatformTest.php diff --git a/app/tests/Feature/Supplier/DirectPlatformTest.php b/app/tests/Feature/Supplier/DirectPlatformTest.php new file mode 100644 index 00000000..1b08fe5c --- /dev/null +++ b/app/tests/Feature/Supplier/DirectPlatformTest.php @@ -0,0 +1,162 @@ +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, + ]); + + $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); +}); diff --git a/docs/observer/STATUS.md b/docs/observer/STATUS.md index 9b48e7fa..aa351a06 100644 --- a/docs/observer/STATUS.md +++ b/docs/observer/STATUS.md @@ -1,6 +1,6 @@ # Brain Status (auto-generated) -Last updated: 2026-05-25T14:59:00.243Z +Last updated: 2026-05-25T14:59:02.312Z | Контролёр | Состояние | Детали | |---|---|---|