From e1fdb5ca8e88d90220d43ca7d2b237cfa8f40c81 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: Sat, 23 May 2026 20:38:56 +0300 Subject: [PATCH] =?UTF-8?q?refactor(billing-v2):=20remove=20DuplicateDetec?= =?UTF-8?q?tor=20=E2=80=94=20trust=20supplier=20dedup=20(Spec=20B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- app/app/Jobs/ProcessWebhookJob.php | 54 +------- app/app/Jobs/RouteSupplierLeadJob.php | 49 +------ app/app/Services/DuplicateDetector.php | 54 -------- .../Feature/Jobs/RouteSupplierLeadJobTest.php | 84 +++--------- app/tests/Feature/Pd/DealCreatePdLogTest.php | 2 - app/tests/Feature/ProcessWebhookJobTest.php | 123 ++++-------------- .../Feature/Supplier/AutoPauseFlowTest.php | 2 - .../RouteSupplierLeadJobBillingTest.php | 2 - .../SupplierLeadDeliveryGuardTest.php | 2 - 9 files changed, 45 insertions(+), 327 deletions(-) delete mode 100644 app/app/Services/DuplicateDetector.php diff --git a/app/app/Jobs/ProcessWebhookJob.php b/app/app/Jobs/ProcessWebhookJob.php index a7fd198f..16786459 100644 --- a/app/app/Jobs/ProcessWebhookJob.php +++ b/app/app/Jobs/ProcessWebhookJob.php @@ -13,7 +13,6 @@ use App\Models\RejectedDealsLog; use App\Models\SupplierLeadCost; use App\Models\SystemSetting; use App\Models\Tenant; -use App\Services\DuplicateDetector; use App\Services\NotificationService; use App\Services\Pd\PdAuditLogger; use App\Services\SupplierResolver; @@ -39,11 +38,6 @@ use Throwable; * 5. Для НОВОЙ сделки: списание баланса + BalanceTransaction + * SupplierLeadCost (Ю-2) + ActivityLog(deal.created). * - * Антифрод-дедуп Биз-19 (§10.8.1): при создании НОВОЙ сделки `DuplicateDetector` - * ищет master по `(tenant_id, phone)` в окне 24 ч. Если master найден — новой - * сделке проставляется `duplicate_of_id`, баланс НЕ списывается, SupplierLeadCost - * НЕ создаётся. ActivityLog пишется с context.duplicate_of=master.id. - * * Уведомления (ТЗ §18.5, событие new_lead): после успешного chargeNewLead * вызывается NotificationService::notifyNewLead, который рассылает email * всем активным user'ам тенанта с включённым каналом email для new_lead. @@ -76,9 +70,7 @@ class ProcessWebhookJob implements ShouldQueue public function handle(): void { - $duplicateDetector = app(DuplicateDetector::class); - - DB::transaction(function () use ($duplicateDetector): void { + DB::transaction(function (): void { DB::statement('SET LOCAL app.current_tenant_id = '.$this->tenantId); $tenant = Tenant::query() @@ -116,54 +108,10 @@ class ProcessWebhookJob implements ShouldQueue return; } - // Биз-19: master-сделка по phone в окне 24 ч → дубль, без charge. - $master = $duplicateDetector->findMaster( - tenantId: $tenant->id, - phone: (string) $this->data['phone'], - now: $receivedAt, - ); - - // Сам только что созданный $deal попадает в выборку DuplicateDetector - // (он уже в БД к моменту lookup'а), поэтому master может ===$deal. - // Дубль — только если master найден И это НЕ сам deal. - if ($master !== null && $master->id !== $deal->id) { - $this->markAsDuplicate($tenant, $deal, $master); - - return; - } - $this->chargeNewLead($tenant, $project, $deal); }); } - /** - * Биз-19: помечаем сделку как дубль master'а. БЕЗ списания баланса - * и БЕЗ SupplierLeadCost (не наша закупка). ActivityLog пишется с - * `context.duplicate_of=master.id` для аудита. - */ - private function markAsDuplicate(Tenant $tenant, Deal $deal, Deal $master): void - { - $deal->update(['duplicate_of_id' => $master->id]); - - ActivityLog::create([ - 'tenant_id' => $tenant->id, - 'user_id' => null, - 'deal_id' => $deal->id, - 'event' => ActivityLog::EVENT_DEAL_CREATED, - 'context' => [ - 'source' => 'webhook', - 'duplicate_of' => $master->id, - ], - 'created_at' => now(), - ]); - - app(PdAuditLogger::class)->record( - action: 'created', subjectType: 'lead', subjectId: $deal->id, - purpose: 'lead_create_webhook', tenantId: (int) $deal->tenant_id, - actorTenantUserId: null, actorAdminUserId: null, ip: null, - ); - } - private function logRejection(Tenant $tenant, string $reason): void { $rejected = RejectedDealsLog::create([ diff --git a/app/app/Jobs/RouteSupplierLeadJob.php b/app/app/Jobs/RouteSupplierLeadJob.php index 111bef74..550fa010 100644 --- a/app/app/Jobs/RouteSupplierLeadJob.php +++ b/app/app/Jobs/RouteSupplierLeadJob.php @@ -11,7 +11,6 @@ use App\Models\Project; use App\Models\SupplierLead; use App\Models\Tenant; use App\Services\Billing\LedgerService; -use App\Services\DuplicateDetector; use App\Services\LeadDistributor; use App\Services\LeadRouter; use App\Services\NotificationService; @@ -44,9 +43,7 @@ use Throwable; * 5. Для каждого Project — DB::transaction с SET LOCAL app.current_tenant_id: * - lockForUpdate Tenant. * - Создать Deal (source_crm_id=vid). - * - DuplicateDetector::findMaster — если найден master !== deal, mark - * duplicate_of_id (без charge/counter/notify, ActivityLog с duplicate_of). - * - Иначе: LedgerService::chargeForDelivery(tenant, deal, lead) — dual-balance + * - LedgerService::chargeForDelivery(tenant, deal, lead) — dual-balance * списание (prepaid balance_leads-- ИЛИ rub balance_rub-=tier_price), INSERT * lead_charges + balance_transactions + supplier_lead_costs внутри той же * транзакции. На InsufficientBalanceException — Log::warning + rethrow @@ -86,7 +83,6 @@ class RouteSupplierLeadJob implements ShouldQueue public function handle( LeadRouter $router, SupplierProjectResolver $resolver, - DuplicateDetector $duplicateDetector, NotificationService $notifier, LedgerService $ledger, LeadDistributor $distributor, @@ -135,7 +131,7 @@ class RouteSupplierLeadJob implements ShouldQueue $failures = []; foreach ($selected as $project) { try { - if ($this->createDealCopyForProject($lead, $project, $duplicateDetector, $notifier, $ledger, $subjectCode)) { + if ($this->createDealCopyForProject($lead, $project, $notifier, $ledger, $subjectCode)) { $createdCount++; } } catch (Throwable $e) { @@ -205,19 +201,18 @@ class RouteSupplierLeadJob implements ShouldQueue /** * Создаёт deal-копию в одной транзакции для конкретного Project. - * Возвращает true — если копия не дубль (баланс списан, счётчики выросли). - * false — если копия помечена дублем (без списания). + * Возвращает true — если deal создан и баланс списан, счётчики выросли. + * false — если лимит исчерпан под блокировкой (deal не создаётся). */ private function createDealCopyForProject( SupplierLead $lead, Project $project, - DuplicateDetector $duplicateDetector, NotificationService $notifier, LedgerService $ledger, ?int $subjectCode, ): bool { try { - return DB::transaction(function () use ($lead, $project, $duplicateDetector, $notifier, $ledger, $subjectCode): bool { + return DB::transaction(function () use ($lead, $project, $notifier, $ledger, $subjectCode): bool { DB::statement("SET LOCAL app.current_tenant_id = '{$project->tenant_id}'"); /** @var Tenant $tenant */ @@ -271,40 +266,6 @@ class RouteSupplierLeadJob implements ShouldQueue 'subject_code' => $subjectCode, ]); - $master = $duplicateDetector->findMaster( - tenantId: (int) $tenant->id, - phone: (string) $lead->phone, - now: $receivedAt, - ); - - // Только что созданный $deal сам попадает в выборку DuplicateDetector - // (он уже в БД к моменту lookup'а), поэтому master может ===$deal. - // Дубль — только если master найден И это НЕ сам deal. - if ($master !== null && $master->id !== $deal->id) { - $deal->update(['duplicate_of_id' => $master->id]); - - ActivityLog::create([ - 'tenant_id' => $tenant->id, - 'user_id' => null, - 'deal_id' => $deal->id, - 'event' => ActivityLog::EVENT_DEAL_CREATED, - 'context' => [ - 'source' => 'supplier_webhook', - 'duplicate_of' => $master->id, - 'supplier_lead_id' => $lead->id, - ], - 'created_at' => now(), - ]); - - app(PdAuditLogger::class)->record( - action: 'created', subjectType: 'lead', subjectId: $deal->id, - purpose: 'lead_create_supplier', tenantId: (int) $deal->tenant_id, - actorTenantUserId: null, actorAdminUserId: null, ip: null, - ); - - return false; - } - // Task 6: $ledger->chargeForDelivery бросит InsufficientBalanceException — // транзакция откатится, и outer catch ниже отловит для auto-pause flow. $ledger->chargeForDelivery($tenant, $deal, $lead); diff --git a/app/app/Services/DuplicateDetector.php b/app/app/Services/DuplicateDetector.php deleted file mode 100644 index 16eda380..00000000 --- a/app/app/Services/DuplicateDetector.php +++ /dev/null @@ -1,54 +0,0 @@ -= NOW() - INTERVAL '24 hours'`. - * Если найдена — новая сделка получает `duplicate_of_id = master.id` и - * НЕ списывает с баланса. - * - * Окно фиксированное 24 ч (не настраивается на MVP) — компромисс между - * антифродом и легитимными повторными интересами. - * - * Цепочки не строятся: дубль ссылается ТОЛЬКО на master (запись без - * `duplicate_of_id`), не на другой дубль. Если master найден среди дублей — - * берётся его собственный `duplicate_of_id` (root master). - * - * Performance: существующий индекс `(tenant_id, phone)` достаточен, см. §10.8.1. - */ -class DuplicateDetector -{ - public const WINDOW_HOURS = 24; - - /** - * Поиск master-сделки для (tenantId, phone) в окне 24 ч. - * - * Возвращает Deal-объект master'а либо null если master не найден. - * Текущий момент `now` параметризуется для тестируемости — в production - * по умолчанию `Carbon::now()`. - */ - public function findMaster(int $tenantId, string $phone, ?Carbon $now = null): ?Deal - { - $now ??= Carbon::now(); - $windowStart = $now->copy()->subHours(self::WINDOW_HOURS); - - return Deal::query() - ->where('tenant_id', $tenantId) - ->where('phone', $phone) - ->where('received_at', '>=', $windowStart) - ->whereNull('duplicate_of_id') - ->orderBy('received_at') - ->first(); - } -} diff --git a/app/tests/Feature/Jobs/RouteSupplierLeadJobTest.php b/app/tests/Feature/Jobs/RouteSupplierLeadJobTest.php index 4d53327b..e7bb0011 100644 --- a/app/tests/Feature/Jobs/RouteSupplierLeadJobTest.php +++ b/app/tests/Feature/Jobs/RouteSupplierLeadJobTest.php @@ -9,7 +9,6 @@ use App\Models\SupplierLead; use App\Models\SupplierProject; use App\Models\Tenant; use App\Services\Billing\LedgerService; -use App\Services\DuplicateDetector; use App\Services\LeadDistributor; use App\Services\LeadRouter; use App\Services\NotificationService; @@ -38,7 +37,6 @@ function runRouteJob(int $supplierLeadId): void (new RouteSupplierLeadJob($supplierLeadId))->handle( app(LeadRouter::class), app(SupplierProjectResolver::class), - app(DuplicateDetector::class), app(NotificationService::class), app(LedgerService::class), app(LeadDistributor::class), @@ -157,54 +155,6 @@ it('charges balance_rub for tenant after routing', function (): void { expect((string) $tenant->fresh()->balance_rub)->toBe('99500.00'); }); -it('marks duplicate via DuplicateDetector — no charge, no counter increment', function (): void { - $supplier = SupplierProject::factory()->create([ - 'platform' => 'B1', - 'signal_type' => 'site', - 'unique_key' => 'test.ru', - ]); - $tenant = Tenant::factory()->create(['balance_rub' => '100000.00']); - $project = Project::factory()->create([ - 'tenant_id' => $tenant->id, - 'supplier_b1_project_id' => $supplier->id, - 'signal_type' => 'site', - 'signal_identifier' => 'test.ru', - 'is_active' => true, - 'delivered_today' => 0, - ]); - linkProjectToSupplier($project, $supplier); - - DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'"); - $master = Deal::create([ - 'tenant_id' => $tenant->id, - 'source_crm_id' => 999, - 'project_id' => $project->id, - 'phone' => '79991234567', - 'phones' => ['79991234567'], - 'status' => 'new', - 'received_at' => now()->subHours(2), - ]); - - $vid = 1000; - $lead = SupplierLead::factory()->create([ - 'supplier_project_id' => null, - 'platform' => 'B1', - 'vid' => $vid, - 'phone' => '79991234567', - 'raw_payload' => ['vid' => $vid, 'project' => 'B1_test.ru', 'phone' => '79991234567', 'time' => now()->getTimestamp()], - ]); - - runRouteJob($lead->id); - - expect((string) $tenant->fresh()->balance_rub)->toBe('100000.00'); - expect($project->fresh()->delivered_today)->toBe(0); - - DB::statement("SET LOCAL app.current_tenant_id = '{$tenant->id}'"); - $duplicate = Deal::where('source_crm_id', $vid)->first(); - expect($duplicate)->not->toBeNull(); - expect($duplicate->duplicate_of_id)->toBe($master->id); -}); - it('throws DomainException when payload encodes B1+SMS combo', function (): void { $vid = 1; $lead = SupplierLead::factory()->create([ @@ -236,7 +186,7 @@ it('handles orphan supplier_project (no matching liderra-projects) — 0 deals, expect($lead->supplier_project_id)->not->toBeNull(); }); -it('handles mixed routing: 3 projects, 1 with pre-existing master (dup), 2 clean', function (): void { +it('same phone pre-existing does not suppress new delivery (Spec B)', function (): void { $supplier = SupplierProject::factory()->create([ 'platform' => 'B1', 'signal_type' => 'site', @@ -260,14 +210,14 @@ it('handles mixed routing: 3 projects, 1 with pre-existing master (dup), 2 clean linkProjectToSupplier($projects->last(), $supplier); } - // Tenant #0 имеет master deal с тем же phone в окне 24 ч — будет дубль. - $masterTenant = $tenants[0]; - $masterProject = $projects[0]; - DB::statement("SET LOCAL app.current_tenant_id = '{$masterTenant->id}'"); - $master = Deal::create([ - 'tenant_id' => $masterTenant->id, + // Tenant #0 имеет pre-existing deal с тем же phone — под новым правилом НЕ подавляет. + $firstTenant = $tenants[0]; + $firstProject = $projects[0]; + DB::statement("SET LOCAL app.current_tenant_id = '{$firstTenant->id}'"); + Deal::create([ + 'tenant_id' => $firstTenant->id, 'source_crm_id' => 555, - 'project_id' => $masterProject->id, + 'project_id' => $firstProject->id, 'phone' => '79991234567', 'phones' => ['79991234567'], 'status' => 'new', @@ -287,22 +237,19 @@ it('handles mixed routing: 3 projects, 1 with pre-existing master (dup), 2 clean $lead->refresh(); expect($lead->processed_at)->not->toBeNull(); - expect($lead->deals_created_count)->toBe(2); // 2 чистых, 1 дубль не считается + // Spec B: pre-existing master does NOT suppress — all 3 charged. + expect($lead->deals_created_count)->toBe(3); - // Tenant #0: deal помечен duplicate_of_id, balance НЕ списан, delivered_today = 0 - expect((string) $masterTenant->fresh()->balance_rub)->toBe('100000.00'); - expect($masterProject->fresh()->delivered_today)->toBe(0); - DB::statement("SET LOCAL app.current_tenant_id = '{$masterTenant->id}'"); - $dupDeal = Deal::query()->where('source_crm_id', $vid)->first(); - expect($dupDeal->duplicate_of_id)->toBe($master->id); - - // Tenant #1, #2: balance списан, delivered_today инкрементирован - foreach ([1, 2] as $i) { + // All 3 tenants: balance decremented, delivered_today incremented. + foreach (range(0, 2) as $i) { $t = $tenants[$i]; $p = $projects[$i]; expect((string) $t->fresh()->balance_rub)->toBe('99500.00'); expect($p->fresh()->delivered_today)->toBe(1); } + + // 3 deal rows exist for this vid (one per tenant). + expect(Deal::query()->where('source_crm_id', $vid)->count())->toBe(3); }); it('idempotent on retry — second handle() returns early, no ghost duplicate deals (Plan 2.5 fix #3)', function (): void { @@ -575,7 +522,6 @@ it('caps deal creation at 3 recipients and tags deal with subject from payload', (new RouteSupplierLeadJob($lead->id))->handle( app(LeadRouter::class), app(SupplierProjectResolver::class), - app(DuplicateDetector::class), app(NotificationService::class), app(LedgerService::class), app(LeadDistributor::class), diff --git a/app/tests/Feature/Pd/DealCreatePdLogTest.php b/app/tests/Feature/Pd/DealCreatePdLogTest.php index 1049986a..390fe42c 100644 --- a/app/tests/Feature/Pd/DealCreatePdLogTest.php +++ b/app/tests/Feature/Pd/DealCreatePdLogTest.php @@ -17,7 +17,6 @@ use App\Models\SupplierProject; use App\Models\Tenant; use App\Models\User; use App\Services\Billing\LedgerService; -use App\Services\DuplicateDetector; use App\Services\LeadDistributor; use App\Services\LeadRouter; use App\Services\NotificationService; @@ -107,7 +106,6 @@ it('writes pd_processing_log created (supplier) when deal created via RouteSuppl (new RouteSupplierLeadJob($lead->id))->handle( app(LeadRouter::class), app(SupplierProjectResolver::class), - app(DuplicateDetector::class), app(NotificationService::class), app(LedgerService::class), app(LeadDistributor::class), diff --git a/app/tests/Feature/ProcessWebhookJobTest.php b/app/tests/Feature/ProcessWebhookJobTest.php index 73f24dbf..919d1f93 100644 --- a/app/tests/Feature/ProcessWebhookJobTest.php +++ b/app/tests/Feature/ProcessWebhookJobTest.php @@ -275,112 +275,37 @@ test('SupplierLeadCost НЕ создаётся если у проекта нет }); // ============================================================================= -// Биз-19: антифрод-дедуп по phone в окне 24 ч (DuplicateDetector, §10.8.1) +// Spec B: no phone dedup — supplier owns dedup, Лидерра charges everything delivered // ============================================================================= -test('Биз-19: master в окне 24ч → дубль, баланс НЕ списывается', function () { - $tenant = Tenant::factory()->create(['balance_leads' => 10]); - $phone = '79007770001'; +test('charges both leads with same phone but different vid (no phone dedup, Spec B)', function () { + $tenant = Tenant::factory()->create(['balance_leads' => 5]); + $phone = '79007770010'; - // Master: пришёл вчера в 12:00. - $masterPayload = makePayload(vid: 901, time: now()->subHours(12)->timestamp); - $masterPayload['phone'] = $phone; - $masterPayload['phones'] = [$phone]; - (new ProcessWebhookJob($tenant->id, $masterPayload))->handle(); + // First webhook — distinct vid + $payload1 = makePayload(vid: 951); + $payload1['phone'] = $phone; + $payload1['phones'] = [$phone]; + (new ProcessWebhookJob($tenant->id, $payload1))->handle(); + + // Second webhook — same phone, different vid + $payload2 = makePayload(vid: 952); + $payload2['phone'] = $phone; + $payload2['phones'] = [$phone]; + (new ProcessWebhookJob($tenant->id, $payload2))->handle(); $tenant->refresh(); - expect($tenant->balance_leads)->toBe(9); + // Both charged — balance_leads decremented twice. + expect($tenant->balance_leads)->toBe(3); - // Дубль: пришёл сейчас, в окне 24 ч. - $dupPayload = makePayload(vid: 902, time: now()->timestamp); - $dupPayload['phone'] = $phone; - $dupPayload['phones'] = [$phone]; - (new ProcessWebhookJob($tenant->id, $dupPayload))->handle(); + // Two distinct deals exist for this tenant. + $deals = Deal::query()->where('tenant_id', $tenant->id)->get(); + expect($deals)->toHaveCount(2); - $master = Deal::query()->where('tenant_id', $tenant->id)->where('source_crm_id', 901)->first(); - $dup = Deal::query()->where('tenant_id', $tenant->id)->where('source_crm_id', 902)->first(); - - expect($master->duplicate_of_id)->toBeNull(); - expect($dup->duplicate_of_id)->toBe($master->id); - - $tenant->refresh(); - expect($tenant->balance_leads)->toBe(9); // только master списан, дубль — нет - expect(BalanceTransaction::query()->where('tenant_id', $tenant->id)->count())->toBe(1); - expect(SupplierLeadCost::query()->where('deal_id', $dup->id)->count())->toBe(0); -}); - -test('Биз-19: master старше 24ч → НЕ дубль, баланс списывается дважды', function () { - $tenant = Tenant::factory()->create(['balance_leads' => 10]); - $phone = '79007770002'; - - // Master: пришёл 25 часов назад — за окном. - $masterPayload = makePayload(vid: 911, time: now()->subHours(25)->timestamp); - $masterPayload['phone'] = $phone; - $masterPayload['phones'] = [$phone]; - (new ProcessWebhookJob($tenant->id, $masterPayload))->handle(); - - // Новая сделка с тем же phone — master уже за окном. - $newPayload = makePayload(vid: 912, time: now()->timestamp); - $newPayload['phone'] = $phone; - $newPayload['phones'] = [$phone]; - (new ProcessWebhookJob($tenant->id, $newPayload))->handle(); - - $deal911 = Deal::query()->where('tenant_id', $tenant->id)->where('source_crm_id', 911)->first(); - $deal912 = Deal::query()->where('tenant_id', $tenant->id)->where('source_crm_id', 912)->first(); - - expect($deal911->duplicate_of_id)->toBeNull(); - expect($deal912->duplicate_of_id)->toBeNull(); - - $tenant->refresh(); - expect($tenant->balance_leads)->toBe(8); // оба списаны - expect(BalanceTransaction::query()->where('tenant_id', $tenant->id)->count())->toBe(2); -}); - -test('Биз-19: дубли изолированы по tenant_id', function () { - $tenantA = Tenant::factory()->create(['balance_leads' => 10]); - $tenantB = Tenant::factory()->create(['balance_leads' => 10]); - $phone = '79007770003'; - - $payloadA = makePayload(vid: 921); - $payloadA['phone'] = $phone; - $payloadA['phones'] = [$phone]; - (new ProcessWebhookJob($tenantA->id, $payloadA))->handle(); - - // Тот же phone у tenantB — НЕ должен считаться дублем. - $payloadB = makePayload(vid: 922); - $payloadB['phone'] = $phone; - $payloadB['phones'] = [$phone]; - (new ProcessWebhookJob($tenantB->id, $payloadB))->handle(); - - $dealA = Deal::query()->where('tenant_id', $tenantA->id)->first(); - $dealB = Deal::query()->where('tenant_id', $tenantB->id)->first(); - - expect($dealA->duplicate_of_id)->toBeNull(); - expect($dealB->duplicate_of_id)->toBeNull(); -}); - -test('Биз-19: ActivityLog для дубля содержит context.duplicate_of', function () { - $tenant = Tenant::factory()->create(['balance_leads' => 10]); - $phone = '79007770004'; - - $masterPayload = makePayload(vid: 931, time: now()->subHours(2)->timestamp); - $masterPayload['phone'] = $phone; - $masterPayload['phones'] = [$phone]; - (new ProcessWebhookJob($tenant->id, $masterPayload))->handle(); - - $dupPayload = makePayload(vid: 932, time: now()->timestamp); - $dupPayload['phone'] = $phone; - $dupPayload['phones'] = [$phone]; - (new ProcessWebhookJob($tenant->id, $dupPayload))->handle(); - - $master = Deal::query()->where('source_crm_id', 931)->first(); - $dup = Deal::query()->where('source_crm_id', 932)->first(); - - $masterLog = ActivityLog::query()->where('deal_id', $master->id)->first(); - $dupLog = ActivityLog::query()->where('deal_id', $dup->id)->first(); - - expect($masterLog->context)->toBe(['source' => 'webhook']); - expect($dupLog->context)->toBe(['source' => 'webhook', 'duplicate_of' => $master->id]); + // Neither deal has duplicate_of_id set. + foreach ($deals as $deal) { + expect($deal->duplicate_of_id)->toBeNull(); + } }); // ============================================================================= diff --git a/app/tests/Feature/Supplier/AutoPauseFlowTest.php b/app/tests/Feature/Supplier/AutoPauseFlowTest.php index 192a6661..c0d2f31e 100644 --- a/app/tests/Feature/Supplier/AutoPauseFlowTest.php +++ b/app/tests/Feature/Supplier/AutoPauseFlowTest.php @@ -10,7 +10,6 @@ use App\Models\SupplierLead; use App\Models\SupplierProject; use App\Models\Tenant; use App\Services\Billing\LedgerService; -use App\Services\DuplicateDetector; use App\Services\LeadDistributor; use App\Services\LeadRouter; use App\Services\NotificationService; @@ -66,7 +65,6 @@ function runJob(int $leadId): void (new RouteSupplierLeadJob($leadId))->handle( app(LeadRouter::class), app(SupplierProjectResolver::class), - app(DuplicateDetector::class), app(NotificationService::class), app(LedgerService::class), app(LeadDistributor::class), diff --git a/app/tests/Feature/Supplier/RouteSupplierLeadJobBillingTest.php b/app/tests/Feature/Supplier/RouteSupplierLeadJobBillingTest.php index 1e91d406..23e1a480 100644 --- a/app/tests/Feature/Supplier/RouteSupplierLeadJobBillingTest.php +++ b/app/tests/Feature/Supplier/RouteSupplierLeadJobBillingTest.php @@ -12,7 +12,6 @@ use App\Models\SupplierLead; use App\Models\SupplierProject; use App\Models\Tenant; use App\Services\Billing\LedgerService; -use App\Services\DuplicateDetector; use App\Services\LeadDistributor; use App\Services\LeadRouter; use App\Services\NotificationService; @@ -90,7 +89,6 @@ function dispatchJob(int $supplierLeadId): void (new RouteSupplierLeadJob($supplierLeadId))->handle( app(LeadRouter::class), app(SupplierProjectResolver::class), - app(DuplicateDetector::class), app(NotificationService::class), app(LedgerService::class), app(LeadDistributor::class), diff --git a/app/tests/Feature/Supplier/SupplierLeadDeliveryGuardTest.php b/app/tests/Feature/Supplier/SupplierLeadDeliveryGuardTest.php index c890b7a1..04e33ffa 100644 --- a/app/tests/Feature/Supplier/SupplierLeadDeliveryGuardTest.php +++ b/app/tests/Feature/Supplier/SupplierLeadDeliveryGuardTest.php @@ -10,7 +10,6 @@ use App\Models\SupplierLead; use App\Models\SupplierProject; use App\Models\Tenant; use App\Services\Billing\LedgerService; -use App\Services\DuplicateDetector; use App\Services\LeadDistributor; use App\Services\LeadRouter; use App\Services\NotificationService; @@ -29,7 +28,6 @@ function runRouteJobB(int $id): void (new RouteSupplierLeadJob($id))->handle( app(LeadRouter::class), app(SupplierProjectResolver::class), - app(DuplicateDetector::class), app(NotificationService::class), app(LedgerService::class), app(LeadDistributor::class),