diff --git a/app/app/Services/LeadRouter.php b/app/app/Services/LeadRouter.php index 3da56fa8..a8febfd2 100644 --- a/app/app/Services/LeadRouter.php +++ b/app/app/Services/LeadRouter.php @@ -1,4 +1,4 @@ - 0 + -- R-03: frozen tenant must not receive new leads (Stage 3 §4.3.1) + AND tenants.frozen_by_balance_at IS NULL ) ORDER BY snap.tenant_id, (snap.daily_limit - projects.delivered_today) DESC, @@ -110,6 +112,8 @@ class LeadRouter SELECT 1 FROM tenants WHERE tenants.id = snap.tenant_id AND tenants.balance_rub > 0 + -- R-03: frozen tenant must not receive new leads (Stage 3 §4.3.1) + AND tenants.frozen_by_balance_at IS NULL ) ORDER BY snap.tenant_id, (snap.daily_limit - projects.delivered_today) DESC, diff --git a/app/tests/Feature/LeadRouter/FrozenFilterTest.php b/app/tests/Feature/LeadRouter/FrozenFilterTest.php new file mode 100644 index 00000000..615b697d --- /dev/null +++ b/app/tests/Feature/LeadRouter/FrozenFilterTest.php @@ -0,0 +1,152 @@ +create([ + 'balance_rub' => '500.00', + 'frozen_by_balance_at' => now(), // frozen — R-03 + ]); + $project = Project::factory()->for($tenant)->create([ + 'is_active' => true, + 'delivery_days_mask' => 127, + 'daily_limit_target' => 10, + 'delivered_today' => 0, + ]); + $sp = SupplierProject::factory()->create(['platform' => 'B1']); + DB::table('project_supplier_links')->insert([ + 'project_id' => $project->id, + 'supplier_project_id' => $sp->id, + 'platform' => $sp->platform, + 'subject_code' => null, + ]); + DB::table('project_routing_snapshots')->insert([ + 'snapshot_date' => '2026-05-28', + 'project_id' => $project->id, + 'tenant_id' => $tenant->id, + 'daily_limit' => 10, + 'delivery_days_mask' => 127, + 'regions' => '{}', + 'signal_type' => 'call', + 'signal_identifier' => null, + 'sms_senders' => null, + 'sms_keyword' => null, + 'expected_volume' => 10, + 'delivered_count' => 0, + 'created_at' => now(), + ]); + + $matched = app(LeadRouter::class)->matchEligibleProjects($sp); + + expect($matched)->toHaveCount(0); // R-03: frozen tenant must not receive leads +}); + +// --------------------------------------------------------------------------- +// Case 2 — DIRECT-platform: frozen tenant must NOT receive leads +// --------------------------------------------------------------------------- +it('does not match DIRECT-platform project for frozen tenant (frozen_by_balance_at IS NOT NULL)', function () { + $tenant = Tenant::factory()->create([ + 'balance_rub' => '500.00', + 'frozen_by_balance_at' => now(), // frozen — R-03 + ]); + $project = Project::factory()->for($tenant)->create([ + 'is_active' => true, + 'delivery_days_mask' => 127, + 'daily_limit_target' => 10, + 'delivered_today' => 0, + ]); + + // DIRECT supplier_project matches via signal_type + unique_key + $sp = SupplierProject::factory()->create([ + 'platform' => 'DIRECT', + 'signal_type' => 'call', + 'unique_key' => 'direct-test-frozen-001', + ]); + + // Snapshot must carry signal_type + signal_identifier matching sp->unique_key + DB::table('project_routing_snapshots')->insert([ + 'snapshot_date' => '2026-05-28', + 'project_id' => $project->id, + 'tenant_id' => $tenant->id, + 'daily_limit' => 10, + 'delivery_days_mask' => 127, + 'regions' => '{}', + 'signal_type' => 'call', + 'signal_identifier' => 'direct-test-frozen-001', // matches sp->unique_key + 'sms_senders' => null, + 'sms_keyword' => null, + 'expected_volume' => 10, + 'delivered_count' => 0, + 'created_at' => now(), + ]); + + $matched = app(LeadRouter::class)->matchEligibleProjects($sp); + + expect($matched)->toHaveCount(0); // R-03: frozen tenant must not receive leads +}); + +// --------------------------------------------------------------------------- +// Case 3 (control) — B-platform, not frozen: MUST receive leads +// --------------------------------------------------------------------------- +it('matches B-platform project for non-frozen tenant (frozen_by_balance_at IS NULL)', function () { + $tenant = Tenant::factory()->create([ + 'balance_rub' => '500.00', + 'frozen_by_balance_at' => null, // NOT frozen — should match + ]); + $project = Project::factory()->for($tenant)->create([ + 'is_active' => true, + 'delivery_days_mask' => 127, + 'daily_limit_target' => 10, + 'delivered_today' => 0, + ]); + $sp = SupplierProject::factory()->create(['platform' => 'B1']); + DB::table('project_supplier_links')->insert([ + 'project_id' => $project->id, + 'supplier_project_id' => $sp->id, + 'platform' => $sp->platform, + 'subject_code' => null, + ]); + DB::table('project_routing_snapshots')->insert([ + 'snapshot_date' => '2026-05-28', + 'project_id' => $project->id, + 'tenant_id' => $tenant->id, + 'daily_limit' => 10, + 'delivery_days_mask' => 127, + 'regions' => '{}', + 'signal_type' => 'call', + 'signal_identifier' => null, + 'sms_senders' => null, + 'sms_keyword' => null, + 'expected_volume' => 10, + 'delivered_count' => 0, + 'created_at' => now(), + ]); + + $matched = app(LeadRouter::class)->matchEligibleProjects($sp); + + expect($matched)->toHaveCount(1); // control: non-frozen tenant with balance IS eligible +});