create(['tier_no' => 1, 'leads_in_tier' => null, 'price_per_lead_kopecks' => 5000, 'is_active' => true, 'effective_from' => now()]); }); it('freezes tenant whose balance no longer covers daily limit', function () { Mail::fake(); // 500₽ / 50₽ = 10 лидов; проекты хотят 25 → заморозка. $tenant = Tenant::factory()->create(['balance_rub' => '500.00', 'frozen_by_balance_at' => null]); Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 25]); (new BalancePreflightSweepJob)->handle(); expect($tenant->fresh()->frozen_by_balance_at)->not->toBeNull(); Mail::assertQueued(BalanceFrozenMail::class); }); it('unfreezes tenant whose balance now covers daily limit', function () { Mail::fake(); // 2000₽ / 50₽ = 40 лидов; хотят 25 → разморозка. $tenant = Tenant::factory()->create(['balance_rub' => '2000.00', 'frozen_by_balance_at' => now()->subDay()]); Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 25]); (new BalancePreflightSweepJob)->handle(); expect($tenant->fresh()->frozen_by_balance_at)->toBeNull(); }); it('is idempotent — does not re-freeze already frozen tenant', function () { Mail::fake(); $frozenAt = now()->subDay(); $tenant = Tenant::factory()->create(['balance_rub' => '0.00', 'frozen_by_balance_at' => $frozenAt]); Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 25]); (new BalancePreflightSweepJob)->handle(); // Дата заморозки не перезаписана; для ЭТОГО tenant повторного письма нет. // NB: per-tenant фильтр, т.к. liderra_testing persistent (DemoSeeder тенанты // могут попасть в sweep и тоже получить BalanceFrozenMail — не наш ответ). expect($tenant->fresh()->frozen_by_balance_at->timestamp)->toBe($frozenAt->timestamp); Mail::assertNotQueued(BalanceFrozenMail::class, fn ($mail) => $mail->tenant->id === $tenant->id); }); // Spec C extension (26.05.2026): freeze/unfreeze дёргают supplier sync в режиме 'online'. // Привязка к существующему админ-переключателю SupplierExportMode (system_settings.supplier_export_mode). // Online нужен сейчас для отладки (моментальный sync с поставщиком); batch будет рабочим режимом // при росте числа клиентов (накопленные изменения уезжают одним cut-off-cron'ом в 18:00 MSK). it('dispatches SyncSupplierProjectJob for each active project on freeze when supplier mode is online', function () { Mail::fake(); Queue::fake(); DB::table('system_settings')->updateOrInsert(['key' => 'supplier_export_mode'], ['value' => 'online']); // 500₽ / 50₽ = 10 лидов; 2 проекта по 15 = 30 → заморозка. $tenant = Tenant::factory()->create(['balance_rub' => '500.00', 'frozen_by_balance_at' => null]); $p1 = Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 15]); $p2 = Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 15]); (new BalancePreflightSweepJob)->handle(); expect($tenant->fresh()->frozen_by_balance_at)->not->toBeNull(); Queue::assertPushed(SyncSupplierProjectJob::class, fn (SyncSupplierProjectJob $job) => $job->projectId === $p1->id); Queue::assertPushed(SyncSupplierProjectJob::class, fn (SyncSupplierProjectJob $job) => $job->projectId === $p2->id); }); it('does NOT dispatch SyncSupplierProjectJob on freeze when supplier mode is batch', function () { Mail::fake(); Queue::fake(); DB::table('system_settings')->updateOrInsert(['key' => 'supplier_export_mode'], ['value' => 'batch']); $tenant = Tenant::factory()->create(['balance_rub' => '500.00', 'frozen_by_balance_at' => null]); Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 25]); (new BalancePreflightSweepJob)->handle(); expect($tenant->fresh()->frozen_by_balance_at)->not->toBeNull(); // batch-режим: sync с поставщиком отложен до cut-off 18:00 MSK через SyncSupplierProjectsJob (множественный). Queue::assertNotPushed(SyncSupplierProjectJob::class, fn (SyncSupplierProjectJob $job) => $job->projectId === Project::query()->where('tenant_id', $tenant->id)->value('id')); }); it('dispatches SyncSupplierProjectJob on unfreeze when supplier mode is online', function () { Mail::fake(); Queue::fake(); DB::table('system_settings')->updateOrInsert(['key' => 'supplier_export_mode'], ['value' => 'online']); // 2000₽ / 50₽ = 40 лидов; хотят 25 → разморозка. $tenant = Tenant::factory()->create(['balance_rub' => '2000.00', 'frozen_by_balance_at' => now()->subDay()]); $project = Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 25]); (new BalancePreflightSweepJob)->handle(); expect($tenant->fresh()->frozen_by_balance_at)->toBeNull(); Queue::assertPushed(SyncSupplierProjectJob::class, fn (SyncSupplierProjectJob $job) => $job->projectId === $project->id); });