fix(lead-router): R-03 — exclude frozen tenants from eligible matches
Stage 3 Task 3.0. Add 'AND tenants.frozen_by_balance_at IS NULL' to both EXISTS-on-tenants subqueries in matchEligibleProjects (DIRECT path + B path). Without this filter, a tenant frozen by BalancePreflightSweepJob continues to receive leads from the existing slepok, getting charged for deliveries they explicitly cannot fund. Spec §4.3.1 R-03.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -76,6 +76,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,
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\SupplierProject;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\LeadRouter;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
uses(SharesSupplierPdo::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
Carbon::setTestNow('2026-05-28 12:00:00', 'Europe/Moscow'); // pre-21:00 MSK window
|
||||
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
|
||||
});
|
||||
|
||||
afterEach(function (): void {
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Case 1 — B-platform: frozen tenant must NOT receive leads (R-03 §4.3.1)
|
||||
// ---------------------------------------------------------------------------
|
||||
it('does not match B-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,
|
||||
]);
|
||||
$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
|
||||
});
|
||||
Reference in New Issue
Block a user