Files
portal/app/tests/Feature/Billing/TenantPreflightTest.php
T

84 lines
4.2 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Models\Project;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
it('sums daily_limit_target of active projects for required leads', function () {
$tenant = Tenant::factory()->create(['balance_rub' => '1000.00']);
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 10]);
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 15]);
Project::factory()->for($tenant)->create(['is_active' => false, 'daily_limit_target' => 100]); // не считается
expect($tenant->requiredLeadsForTomorrow())->toBe(25);
});
it('casts frozen_by_balance_at to datetime', function () {
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => now()]);
expect($tenant->frozen_by_balance_at)->toBeInstanceOf(Carbon::class);
});
it('casts project preflight_blocked_at to datetime', function () {
$project = Project::factory()->create(['preflight_blocked_at' => now()]);
expect($project->preflight_blocked_at)->toBeInstanceOf(Carbon::class);
});
// ---------------------------------------------------------------------------
// Stage 4 / Task 4.4 — R-19 (spec §4.4.3): share-aware requiredLeadsForTomorrow.
// Before fix: simple SUM(daily_limit_target). Overcharges preflight when a tenant
// shares a call/site signal with other tenants — supplier order is capped at
// max(max(limits), ceil(Σ/3)) and split proportionally, so a single tenant's
// share is typically much smaller than its raw limit.
// Formula per project:
// group_limits = limits of all is_active projects sharing the same
// (signal_type, agnostic signal — phone/domain/sms-sender+keyword)
// group_order = max(max(group_limits), ceil(Σ group_limits / 3))
// tenant_share = ceil(group_order × (project_limit / Σ group_limits))
// Legacy projects (signal_type=null — webhook-only, no supplier share) → full limit.
// ---------------------------------------------------------------------------
it('R-19 single call project (no sharing) — returns full daily_limit_target', function () {
$phone = '7919'.Str::random(7); // unique per run to dodge any pre-existing leakage
$tenant = Tenant::factory()->create(['balance_rub' => '1000.00']);
Project::factory()->for($tenant)->asCallSignal($phone)->create([
'is_active' => true, 'daily_limit_target' => 10,
]);
// groupLimits = [10] (only this project) → sum=10, max=10, order=max(10, ceil(10/3))=10,
// share = ceil(10 × 10/10) = 10. Same as legacy.
expect($tenant->fresh()->requiredLeadsForTomorrow())->toBe(10);
});
it('R-19 3 tenants sharing same call source — each tenant gets proportional share, not full limit', function () {
$sharedPhone = '7929'.Str::random(7); // unique shared identifier per run
// 3 tenants, same call source $sharedPhone, each daily_limit_target=10.
// group_order = max(max([10,10,10]), ceil(30/3)) = max(10, 10) = 10.
// share per tenant = ceil(10 × 10/30) = ceil(3.33) = 4.
// Legacy formula would give 10 (4 vs 10 = the bug R-19 fixes).
$tenants = [];
foreach (range(1, 3) as $i) {
$t = Tenant::factory()->create(['balance_rub' => '1000.00']);
Project::factory()->for($t)->asCallSignal($sharedPhone)->create([
'is_active' => true,
'daily_limit_target' => 10,
]);
$tenants[] = $t;
}
expect($tenants[0]->fresh()->requiredLeadsForTomorrow())->toBe(4);
});
it('R-19 legacy webhook projects (signal_type=null) — still summed as full limit (no shared group)', function () {
// Regression-protection for existing TenantPreflightTest behavior.
// Webhook-only projects don't participate in supplier sharing — their full limit counts.
$tenant = Tenant::factory()->create(['balance_rub' => '1000.00']);
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 10]);
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 15]);
expect($tenant->fresh()->requiredLeadsForTomorrow())->toBe(25);
});