From 05938df4f2673220fa6b859dc6a7429b35e7cb0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 25 May 2026 08:53:12 +0300 Subject: [PATCH] =?UTF-8?q?fix(billing-v2-c):=20RLS-=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=B2=20BalancePreflightSweep?= =?UTF-8?q?Job=20(jobs/CLI=20hotfix)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Jobs/Billing/BalancePreflightSweepJob.php | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/app/app/Jobs/Billing/BalancePreflightSweepJob.php b/app/app/Jobs/Billing/BalancePreflightSweepJob.php index b5aca412..b5dcde80 100644 --- a/app/app/Jobs/Billing/BalancePreflightSweepJob.php +++ b/app/app/Jobs/Billing/BalancePreflightSweepJob.php @@ -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