Files
portal/app/tests/Feature/Billing/TenantPreflightTest.php
T
Дмитрий 8696b5e27f
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
feat/billing: F/J — единый расчёт замков + пополнение/пересчёт снимают оба замка
- D2: requiredLeadsForTomorrow переведён на полный лимит, откат share-aware R-19 [решение владельца]
- B/D3: пополнение снимает клиентскую заморозку И блоки проектов вместе, политика всё-или-ничего
- F/J/D6: вечерний пересчёт 18:00 снимает блоки проектов у незаморожённых; общий ProjectBlockReleaseService; иерархия заморозка > блок
- fix: balance_freeze_log INSERT переведён на главное соединение — межсессионный self-lock с FOR UPDATE топапа [найден живым прогоном, pg_blocking подтвердил; в тестах маскировался SharesSupplierPdo]
- spec + plan в docs/superpowers

138/138 биллинг-тестов GREEN. Pint чисто. Живьём B+F подтверждены на докалке. На прод НЕ катилось.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:58:44 +03:00

74 lines
3.8 KiB
PHP

<?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);
});
// ---------------------------------------------------------------------------
// D2 (23.06.2026, спека balance-lock-unify-FJ): единый расчёт required по ПОЛНОМУ
// лимиту активных проектов — откат share-aware (R-19) по решению владельца.
// requiredLeadsForTomorrow = SUM(daily_limit_target where is_active=true), без
// учёта доли группового заказа поставщика. Один расчёт во всех точках (заморозка,
// разморозка, снятие проектного блока), чтобы замки вели себя предсказуемо.
// ---------------------------------------------------------------------------
it('returns full daily_limit_target for a single call project', 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,
]);
expect($tenant->fresh()->requiredLeadsForTomorrow())->toBe(10);
});
it('returns full daily_limit_target even when 3 tenants share one call source (full-limit policy)', function () {
$sharedPhone = '7929'.Str::random(7); // unique shared identifier per run
// 3 tenants, same call source, each daily_limit_target=10.
// Единый расчёт по полному лимиту (D2): доля группы НЕ учитывается → 10, а не 4.
$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(10);
});
it('sums full limit for legacy webhook projects (signal_type=null)', 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);
});