102 lines
4.2 KiB
PHP
102 lines
4.2 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
use App\Jobs\Billing\BalanceFrozenReminderJob;
|
||
|
|
use App\Mail\BalanceFrozenFinalMail;
|
||
|
|
use App\Mail\BalanceFrozenReminderMail;
|
||
|
|
use App\Models\PricingTier;
|
||
|
|
use App\Models\Project;
|
||
|
|
use App\Models\Tenant;
|
||
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
|
|
use Illuminate\Support\Carbon;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
use Illuminate\Support\Facades\Mail;
|
||
|
|
use Tests\Concerns\SharesSupplierPdo;
|
||
|
|
|
||
|
|
// Изоляция: liderra_testing persistent — DatabaseTransactions откатывает default-pgsql,
|
||
|
|
// SharesSupplierPdo делает pgsql_supplier общим PDO (паттерн Спека B / Task 1.4).
|
||
|
|
// Per-tenant Mail-фильтры обязательны — DemoSeeder тенанты могут попасть в sweep
|
||
|
|
// (прецедент idempotent-fixup 55a1bc05).
|
||
|
|
uses(DatabaseTransactions::class);
|
||
|
|
uses(SharesSupplierPdo::class);
|
||
|
|
|
||
|
|
beforeEach(function () {
|
||
|
|
// RLS-контекст (системный tenant 0).
|
||
|
|
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
|
||
|
|
PricingTier::query()->create([
|
||
|
|
'tier_no' => 1,
|
||
|
|
'leads_in_tier' => null,
|
||
|
|
'price_per_lead_kopecks' => 5000,
|
||
|
|
'is_active' => true,
|
||
|
|
'effective_from' => now(),
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('sends reminder ~1 day after freeze', function () {
|
||
|
|
Mail::fake();
|
||
|
|
// frozen 25h назад — попадает в окно reminder (24-48h).
|
||
|
|
Carbon::setTestNow('2026-05-25 12:00:00');
|
||
|
|
$tenant = Tenant::factory()->create([
|
||
|
|
'balance_rub' => '0.00',
|
||
|
|
'frozen_by_balance_at' => Carbon::now()->subHours(25),
|
||
|
|
]);
|
||
|
|
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 25]);
|
||
|
|
|
||
|
|
(new BalanceFrozenReminderJob)->handle();
|
||
|
|
|
||
|
|
Mail::assertQueued(BalanceFrozenReminderMail::class, fn ($mail) => $mail->tenant->id === $tenant->id);
|
||
|
|
Mail::assertNotQueued(BalanceFrozenFinalMail::class, fn ($mail) => $mail->tenant->id === $tenant->id);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('sends final ~3 days after freeze', function () {
|
||
|
|
Mail::fake();
|
||
|
|
// frozen 73h назад — попадает в окно final (72-96h).
|
||
|
|
Carbon::setTestNow('2026-05-25 12:00:00');
|
||
|
|
$tenant = Tenant::factory()->create([
|
||
|
|
'balance_rub' => '0.00',
|
||
|
|
'frozen_by_balance_at' => Carbon::now()->subHours(73),
|
||
|
|
]);
|
||
|
|
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 25]);
|
||
|
|
|
||
|
|
(new BalanceFrozenReminderJob)->handle();
|
||
|
|
|
||
|
|
Mail::assertQueued(BalanceFrozenFinalMail::class, fn ($mail) => $mail->tenant->id === $tenant->id);
|
||
|
|
Mail::assertNotQueued(BalanceFrozenReminderMail::class, fn ($mail) => $mail->tenant->id === $tenant->id);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('sends nothing for freshly frozen tenant', function () {
|
||
|
|
Mail::fake();
|
||
|
|
// frozen 2h назад — окно ещё не открылось.
|
||
|
|
Carbon::setTestNow('2026-05-25 12:00:00');
|
||
|
|
$tenant = Tenant::factory()->create([
|
||
|
|
'balance_rub' => '0.00',
|
||
|
|
'frozen_by_balance_at' => Carbon::now()->subHours(2),
|
||
|
|
]);
|
||
|
|
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 25]);
|
||
|
|
|
||
|
|
(new BalanceFrozenReminderJob)->handle();
|
||
|
|
|
||
|
|
Mail::assertNotQueued(BalanceFrozenReminderMail::class, fn ($mail) => $mail->tenant->id === $tenant->id);
|
||
|
|
Mail::assertNotQueued(BalanceFrozenFinalMail::class, fn ($mail) => $mail->tenant->id === $tenant->id);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is throttled — does not re-send reminder for same tenant in window', function () {
|
||
|
|
Mail::fake();
|
||
|
|
Carbon::setTestNow('2026-05-25 12:00:00');
|
||
|
|
$tenant = Tenant::factory()->create([
|
||
|
|
'balance_rub' => '0.00',
|
||
|
|
'frozen_by_balance_at' => Carbon::now()->subHours(25),
|
||
|
|
]);
|
||
|
|
Project::factory()->for($tenant)->create(['is_active' => true, 'daily_limit_target' => 25]);
|
||
|
|
|
||
|
|
// Первый прогон — отправляет reminder.
|
||
|
|
(new BalanceFrozenReminderJob)->handle();
|
||
|
|
Mail::assertQueued(BalanceFrozenReminderMail::class, fn ($mail) => $mail->tenant->id === $tenant->id);
|
||
|
|
|
||
|
|
// Сброс fake и второй прогон в том же окне — повторного письма быть не должно.
|
||
|
|
Mail::fake();
|
||
|
|
(new BalanceFrozenReminderJob)->handle();
|
||
|
|
Mail::assertNotQueued(BalanceFrozenReminderMail::class, fn ($mail) => $mail->tenant->id === $tenant->id);
|
||
|
|
});
|