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

85 lines
4.0 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Mail\BalanceFrozenFinalMail;
use App\Mail\BalanceFrozenMail;
use App\Mail\BalanceFrozenReminderMail;
use App\Mail\BalanceUnfrozenMail;
use App\Mail\ZeroBalancePausedMail;
use App\Models\Project;
use App\Models\Tenant;
use App\Services\Billing\PreflightResult;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
uses(DatabaseTransactions::class);
/**
* Регрессия прод-инцидента 14.07.2026: письма о заморазке баланса падали в очереди
* с ModelNotFoundException [App\Models\Tenant].
*
* Причина: письмо кладётся в очередь с Eloquent-моделью Tenant, SerializesModels
* заменяет её на ModelIdentifier(id), а воркер восстанавливает модель ЗАНОВО
* запросом в БД — под ролью crm_app_user, где RLS-policy tenants_self_isolation
* без app.current_tenant_id отдаёт 0 строк → firstOrFail() → падение. Ни одно
* письмо о заморозке до клиента не доходило.
*
* Здесь недоступность строки для воркера эмулируется удалением записи между
* постановкой в очередь и восстановлением — точный аналог «RLS не отдал строку».
* Правильное поведение: письмо несёт снимок данных и отправляется без похода в БД.
*/
it('renders queued balance mail after tenant row is invisible to the worker', function (string $mailClass, string $expectedText) {
$tenant = Tenant::factory()->create([
'organization_name' => 'ООО Заморозка',
'contact_email' => 'frozen@example.test',
]);
$result = new PreflightResult(false, 30, 20, 10);
// Что уходит в очередь (Redis) — PHP-сериализация письма.
$payload = serialize(new $mailClass($tenant, $result));
// Воркер строку не видит (на проде — RLS; здесь — записи уже нет).
DB::table('tenants')->where('id', $tenant->id)->delete();
$rendered = unserialize($payload)->render();
expect($rendered)->toContain('ООО Заморозка');
expect($rendered)->toContain($expectedText);
})->with([
'заморозка' => [BalanceFrozenMail::class, 'приостановлен'],
'напоминание' => [BalanceFrozenReminderMail::class, 'всё ещё приостановлен'],
'последнее напоминание' => [BalanceFrozenFinalMail::class, 'последнее напоминание'],
'разморозка' => [BalanceUnfrozenMail::class, 'возобновлён'],
]);
it('addresses queued balance mail to the tenant after the row is invisible', function () {
$tenant = Tenant::factory()->create(['contact_email' => 'frozen@example.test']);
$payload = serialize(new BalanceFrozenMail($tenant, new PreflightResult(false, 30, 20, 10)));
DB::table('tenants')->where('id', $tenant->id)->delete();
$envelope = unserialize($payload)->envelope();
expect($envelope->to[0]->address)->toBe('frozen@example.test');
});
it('renders queued zero-balance mail after tenant and project rows are invisible', function () {
$tenant = Tenant::factory()->create([
'organization_name' => 'ООО Ноль',
'contact_email' => 'zero@example.test',
'balance_rub' => '0.00',
]);
$project = Project::factory()->for($tenant)->create(['name' => 'Стоматология Москва']);
$payload = serialize(new ZeroBalancePausedMail($tenant, $project, 5000));
DB::table('projects')->where('id', $project->id)->delete();
DB::table('tenants')->where('id', $tenant->id)->delete();
$rendered = unserialize($payload)->render();
expect($rendered)->toContain('Стоматология Москва');
expect($rendered)->toContain('приостановлен');
});