Files
portal/app/tests/Feature/Services/LeadRouterTest.php
T

211 lines
8.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Services\LeadRouter;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class);
uses(SharesSupplierPdo::class);
beforeEach(function (): void {
// Clear tenant context — LeadRouter operates without it (sharing across tenants).
// Use set_config (session-scoped, rolls back via DatabaseTransactions).
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
});
// `linkProjectToSupplier` helper now lives in tests/Pest.php — single source.
it('returns project linked via pivot to the supplier_project', function (): void {
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
$sp = SupplierProject::query()->create([
'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'r.ru',
'subject_code' => 82, 'current_limit' => 0, 'sync_status' => 'ok',
]);
$project = Project::factory()->create([
'tenant_id' => $tenant->id, 'is_active' => true,
'daily_limit_target' => 10, 'delivered_today' => 0, 'delivery_days_mask' => 127,
]);
linkProjectToSupplier($project, $sp);
$matched = app(LeadRouter::class)->matchEligibleProjects($sp);
expect($matched)->toHaveCount(1)
->and($matched->first()->id)->toBe($project->id);
});
it('excludes project NOT linked to this supplier_project', function (): void {
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
$sp = SupplierProject::query()->create([
'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'r2.ru',
'subject_code' => 82, 'current_limit' => 0, 'sync_status' => 'ok',
]);
Project::factory()->create([
'tenant_id' => $tenant->id, 'is_active' => true,
'daily_limit_target' => 10, 'delivered_today' => 0, 'delivery_days_mask' => 127,
]); // не линкуем
expect(app(LeadRouter::class)->matchEligibleProjects($sp))->toHaveCount(0);
});
it('excludes inactive project, project at limit, and zero-balance tenant', function (): void {
$sp = SupplierProject::query()->create([
'platform' => 'B1', 'signal_type' => 'site', 'unique_key' => 'r3.ru',
'subject_code' => 82, 'current_limit' => 0, 'sync_status' => 'ok',
]);
$t1 = Tenant::factory()->create(['balance_leads' => 100]);
$inactive = Project::factory()->create(['tenant_id' => $t1->id, 'is_active' => false, 'daily_limit_target' => 10, 'delivered_today' => 0, 'delivery_days_mask' => 127]);
linkProjectToSupplier($inactive, $sp);
$atLimit = Project::factory()->create(['tenant_id' => $t1->id, 'is_active' => true, 'daily_limit_target' => 5, 'delivered_today' => 5, 'delivery_days_mask' => 127]);
linkProjectToSupplier($atLimit, $sp);
$t0 = Tenant::factory()->create(['balance_leads' => 0, 'balance_rub' => 0]);
$broke = Project::factory()->create(['tenant_id' => $t0->id, 'is_active' => true, 'daily_limit_target' => 10, 'delivered_today' => 0, 'delivery_days_mask' => 127]);
linkProjectToSupplier($broke, $sp);
expect(app(LeadRouter::class)->matchEligibleProjects($sp))->toHaveCount(0);
});
it('skips paused project (is_active=false)', function (): void {
$supplier = SupplierProject::factory()->create(['platform' => 'B1', 'signal_type' => 'site']);
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'signal_type' => 'site',
'signal_identifier' => 'example.com',
'is_active' => false,
]);
linkProjectToSupplier($project, $supplier);
$router = app(LeadRouter::class);
expect($router->matchEligibleProjects($supplier))->toHaveCount(0);
});
it('skips project where today is not in delivery_days_mask', function (): void {
// Mirror LeadRouter's МСК alignment to avoid off-by-one near midnight when
// process TZ (UTC) and Europe/Moscow disagree on ISO day-of-week.
$todayBit = 1 << (now('Europe/Moscow')->isoWeekday() - 1);
$maskWithoutToday = 127 & ~$todayBit;
$supplier = SupplierProject::factory()->create(['platform' => 'B1', 'signal_type' => 'site']);
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'signal_type' => 'site',
'signal_identifier' => 'example.com',
'is_active' => true,
'delivery_days_mask' => $maskWithoutToday,
]);
linkProjectToSupplier($project, $supplier);
$router = app(LeadRouter::class);
expect($router->matchEligibleProjects($supplier))->toHaveCount(0);
});
it('skips project where delivered_today >= effective_daily_limit_today', function (): void {
$supplier = SupplierProject::factory()->create(['platform' => 'B1', 'signal_type' => 'site']);
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'signal_type' => 'site',
'signal_identifier' => 'example.com',
'is_active' => true,
'effective_daily_limit_today' => 5,
'delivered_today' => 5,
]);
linkProjectToSupplier($project, $supplier);
$router = app(LeadRouter::class);
expect($router->matchEligibleProjects($supplier))->toHaveCount(0);
});
it('falls back to daily_limit_target when effective_daily_limit_today is null', function (): void {
$supplier = SupplierProject::factory()->create(['platform' => 'B1', 'signal_type' => 'site']);
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'signal_type' => 'site',
'signal_identifier' => 'example.com',
'is_active' => true,
'effective_daily_limit_today' => null,
'daily_limit_target' => 10,
'delivered_today' => 5,
]);
linkProjectToSupplier($project, $supplier);
$router = app(LeadRouter::class);
expect($router->matchEligibleProjects($supplier))->toHaveCount(1);
});
it('skips project where tenant has zero in BOTH balance_leads AND balance_rub (Plan 4 dual-balance)', function (): void {
// Plan 4 Task 4: фильтр расширен на (balance_leads > 0 OR balance_rub > 0).
// Tenant с обоими нулями — реально невыдачный, должен скипаться.
$supplier = SupplierProject::factory()->create(['platform' => 'B1', 'signal_type' => 'site']);
$tenant = Tenant::factory()->create(['balance_leads' => 0, 'balance_rub' => '0.00']);
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'signal_type' => 'site',
'signal_identifier' => 'example.com',
'is_active' => true,
]);
linkProjectToSupplier($project, $supplier);
$router = app(LeadRouter::class);
expect($router->matchEligibleProjects($supplier))->toHaveCount(0);
});
it('includes project when balance_leads=0 BUT balance_rub > 0 (Plan 4 dual-balance rub-only tenant)', function (): void {
// Plan 4 Task 4: rub-only tenant ДОЛЖЕН пройти LeadRouter; LedgerService
// сам резолвит prepaid/rub в transaction'е.
$supplier = SupplierProject::factory()->create(['platform' => 'B1', 'signal_type' => 'site']);
$tenant = Tenant::factory()->create(['balance_leads' => 0, 'balance_rub' => '1000.00']);
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'signal_type' => 'site',
'signal_identifier' => 'example.com',
'is_active' => true,
]);
linkProjectToSupplier($project, $supplier);
$router = app(LeadRouter::class);
$eligible = $router->matchEligibleProjects($supplier);
expect($eligible)->toHaveCount(1);
expect($eligible->first()->id)->toBe($project->id);
});
it('orders results by created_at ASC (deterministic, spec §6 step 4)', function (): void {
$supplier = SupplierProject::factory()->create(['platform' => 'B1', 'signal_type' => 'site']);
$projectsCreated = collect();
for ($i = 0; $i < 3; $i++) {
$tenant = Tenant::factory()->create(['balance_leads' => 100]);
$project = Project::factory()->create([
'tenant_id' => $tenant->id,
'signal_type' => 'site',
'signal_identifier' => 'example.com',
'is_active' => true,
'created_at' => now()->subDays(3 - $i),
]);
linkProjectToSupplier($project, $supplier);
$projectsCreated->push($project);
}
$router = app(LeadRouter::class);
$matched = $router->matchEligibleProjects($supplier);
expect($matched->pluck('id')->all())->toBe($projectsCreated->pluck('id')->all());
});