fix(billing-v2-c): RLS-контекст в BalancePreflightSweepJob (jobs/CLI hotfix)

CLI и queue не проходят через SetTenantContext → app.current_tenant_id не выставлен → projects RLS падает 'unrecognized configuration parameter'. Зеркалим SetTenantContext: DB::transaction + SET LOCAL (PgBouncer-safe). Затрагивает initial-sweep + ночной cron @18:00 MSK.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-25 08:53:12 +03:00
parent 42ebe2e7c6
commit 05938df4f2
@@ -50,36 +50,44 @@ final class BalancePreflightSweepJob implements ShouldQueue
*/
private function evaluateTenant(Tenant $tenant, BalancePreflightService $service, Collection $tiers): void
{
$required = $tenant->requiredLeadsForTomorrow();
$result = $service->evaluate(
balanceRub: (string) $tenant->balance_rub,
deliveredInMonth: (int) $tenant->delivered_in_month,
requiredLeads: $required,
tiers: $tiers,
);
// Spec C deploy hotfix (25.05.2026): CLI-команды и фоновые джобы не проходят
// через SetTenantContext middleware → app.current_tenant_id не выставлен →
// RLS-policy на projects падает с "unrecognized configuration parameter".
// Зеркалим mechanic SetTenantContext: SET LOCAL внутри транзакции (PgBouncer-safe).
DB::transaction(function () use ($tenant, $service, $tiers): void {
DB::statement('SET LOCAL app.current_tenant_id = '.(int) $tenant->id);
$isFrozen = $tenant->frozen_by_balance_at !== null;
$required = $tenant->requiredLeadsForTomorrow();
$result = $service->evaluate(
balanceRub: (string) $tenant->balance_rub,
deliveredInMonth: (int) $tenant->delivered_in_month,
requiredLeads: $required,
tiers: $tiers,
);
// Переход active → frozen.
if (! $result->passes && ! $isFrozen) {
$tenant->frozen_by_balance_at = now();
$tenant->save();
$this->logEvent($tenant, 'frozen', 'cutoff_18msk', $result);
Mail::queue(new BalanceFrozenMail($tenant, $result));
$isFrozen = $tenant->frozen_by_balance_at !== null;
return;
}
// Переход active → frozen.
if (! $result->passes && ! $isFrozen) {
$tenant->frozen_by_balance_at = now();
$tenant->save();
$this->logEvent($tenant, 'frozen', 'cutoff_18msk', $result);
Mail::queue(new BalanceFrozenMail($tenant, $result));
// Переход frozen → active.
if ($result->passes && $isFrozen) {
$tenant->frozen_by_balance_at = null;
$tenant->save();
$this->logEvent($tenant, 'unfrozen', 'cutoff_18msk', $result);
Mail::queue(new BalanceUnfrozenMail($tenant, $result));
return;
}
return;
}
// Иначе состояние не изменилось — ничего не делаем (идемпотентность).
// Переход frozen → active.
if ($result->passes && $isFrozen) {
$tenant->frozen_by_balance_at = null;
$tenant->save();
$this->logEvent($tenant, 'unfrozen', 'cutoff_18msk', $result);
Mail::queue(new BalanceUnfrozenMail($tenant, $result));
return;
}
// Иначе состояние не изменилось — ничего не делаем (идемпотентность).
});
}
private function logEvent(Tenant $tenant, string $event, string $trigger, PreflightResult $result): void