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); });