$vid, 'project' => 'B2_Caranga', // префикс должен обрезаться до 'Caranga' 'tag' => 'Caranga', 'phone' => '79001234567', 'phones' => ['79001234567'], 'time' => $time ?? time(), ]; } test('новая сделка: INSERT в deals + INSERT в webhook_dedup_keys, баланс -1', function () { $tenant = Tenant::factory()->create(['balance_leads' => 10]); (new ProcessWebhookJob($tenant->id, makePayload(vid: 100)))->handle(); $tenant->refresh(); expect($tenant->balance_leads)->toBe(9); $deal = Deal::query()->where('tenant_id', $tenant->id)->first(); expect($deal)->not->toBeNull(); expect($deal->source_crm_id)->toBe(100); expect($deal->phone)->toBe('79001234567'); expect($deal->status)->toBe('new'); expect($deal->project->name)->toBe('Caranga'); // префикс B2_ обрезан $dedup = WebhookDedupKey::query() ->where('tenant_id', $tenant->id) ->where('source_crm_id', 100) ->first(); expect($dedup)->not->toBeNull(); expect($dedup->deal_id)->toBe($deal->id); }); test('дубль vid: UPDATE существующей сделки, баланс НЕ списывается второй раз', function () { $tenant = Tenant::factory()->create(['balance_leads' => 10]); $vid = 200; // Первый webhook (new ProcessWebhookJob($tenant->id, makePayload(vid: $vid)))->handle(); $tenant->refresh(); expect($tenant->balance_leads)->toBe(9); $dealsAfterFirst = Deal::query()->where('tenant_id', $tenant->id)->count(); // Второй webhook с тем же vid (но новым phone — будет UPDATE) $payload2 = makePayload(vid: $vid); $payload2['phone'] = '79009999999'; (new ProcessWebhookJob($tenant->id, $payload2))->handle(); $tenant->refresh(); expect($tenant->balance_leads)->toBe(9); // баланс не изменился expect(Deal::query()->where('tenant_id', $tenant->id)->count())->toBe($dealsAfterFirst); $deal = Deal::query()->where('tenant_id', $tenant->id)->where('source_crm_id', $vid)->first(); expect($deal->phone)->toBe('79009999999'); // обновлён phone // dedup-ключ всё ещё ровно один expect(WebhookDedupKey::query()->where('tenant_id', $tenant->id)->where('source_crm_id', $vid)->count())->toBe(1); }); test('баланс=0: запись в лог, без INSERT в deals и dedup_keys', function () { $tenant = Tenant::factory()->create(['balance_leads' => 0]); (new ProcessWebhookJob($tenant->id, makePayload(vid: 300)))->handle(); $tenant->refresh(); expect($tenant->balance_leads)->toBe(0); expect(Deal::query()->where('tenant_id', $tenant->id)->count())->toBe(0); expect(WebhookDedupKey::query()->where('tenant_id', $tenant->id)->count())->toBe(0); }); test('изоляция тенантов: одинаковый vid у разных тенантов = разные сделки', function () { $tenantA = Tenant::factory()->create(['balance_leads' => 10]); $tenantB = Tenant::factory()->create(['balance_leads' => 10]); (new ProcessWebhookJob($tenantA->id, makePayload(vid: 555)))->handle(); (new ProcessWebhookJob($tenantB->id, makePayload(vid: 555)))->handle(); expect(Deal::query()->where('tenant_id', $tenantA->id)->count())->toBe(1); expect(Deal::query()->where('tenant_id', $tenantB->id)->count())->toBe(1); expect(WebhookDedupKey::query()->count())->toBeGreaterThanOrEqual(2); $tenantA->refresh(); $tenantB->refresh(); expect($tenantA->balance_leads)->toBe(9); expect($tenantB->balance_leads)->toBe(9); }); test('findOrCreate проекта: повторный webhook с тем же project не создаёт дубля', function () { $tenant = Tenant::factory()->create(['balance_leads' => 10]); (new ProcessWebhookJob($tenant->id, makePayload(vid: 401)))->handle(); (new ProcessWebhookJob($tenant->id, makePayload(vid: 402)))->handle(); expect(Project::query()->where('tenant_id', $tenant->id)->count())->toBe(1); }); test('ON DELETE CASCADE: удаление сделки очищает webhook_dedup_keys', function () { $tenant = Tenant::factory()->create(['balance_leads' => 10]); (new ProcessWebhookJob($tenant->id, makePayload(vid: 700)))->handle(); $deal = Deal::query()->where('tenant_id', $tenant->id)->first(); DB::table('deals') ->where('id', $deal->id) ->where('received_at', $deal->received_at) ->delete(); expect(WebhookDedupKey::query() ->where('tenant_id', $tenant->id) ->where('source_crm_id', 700) ->count())->toBe(0); });