= price) * - tenants.frozen_by_balance_at — PreflightBalanceService (NULL = активен) * - projects.is_active — SnapshotRebuildCommand eligibility * - projects.delivered_today — LeadRouter остаток лимита * - projects.delivery_days_mask — LeadRouter / SnapshotRebuildCommand * - projects.regions — LeadRouter regional cascade * - projects.preflight_blocked_at — SnapshotRebuildCommand eligibility * * Имена колонок подтверждены чтением db/schema.sql и прод-кода: * - balance_rub: tenants, DECIMAL(12,2) DEFAULT 0 * - frozen_by_balance_at: tenants, TIMESTAMPTZ NULL (NULL = не заморожен) * - regions: projects, INT[] NOT NULL DEFAULT '{}' (порядковые коды 1..89, НЕ ГИБДД) * - delivery_days_mask: projects, INT NOT NULL DEFAULT 127 (bit 0=Пн..bit 6=Вс) * - daily_limit_target: projects, INT NOT NULL DEFAULT 10 * - delivered_today: projects, INT (остаток лимита) * - preflight_blocked_at: projects, TIMESTAMPTZ NULL * * Коды субъектов — ПОРЯДКОВЫЕ 1..89 (конституционный порядок), НЕ коды ГИБДД. * Использовать только через App\Support\RussianRegions::CODE_TO_NAME / nameToCode(). * Например: Москва = 82, Санкт-Петербург = 83. * * Task 3 — Phase 1 Portal Client Imitation Harness. * Spec: docs/superpowers/specs/2026-06-03-portal-client-imitation-phase1-design.md */ final class ConditionLevers { /** * Установить баланс тенанта (в рублях, как DECIMAL(12,2)). * * @param int|float|string $rub Сумма в рублях (например 500.00 или 0). */ public static function setBalance(Tenant $tenant, int|float|string $rub): void { DB::table('tenants') ->where('id', $tenant->id) ->update(['balance_rub' => $rub]); } /** * Обнулить баланс тенанта до 0 (лид не пройдёт LedgerService::chargeForDelivery). */ public static function drainBalance(Tenant $tenant): void { self::setBalance($tenant, 0); } /** * Выставить delivered_today = daily_limit_target у проекта, * чтобы LeadRouter не считал его eligible (лимит исчерпан). * * LeadRouter: `projects.delivered_today < snap.daily_limit` — равенство = не eligible. */ public static function fillToLimit(Project $project): void { $limit = (int) DB::table('projects') ->where('id', $project->id) ->value('daily_limit_target'); DB::table('projects') ->where('id', $project->id) ->update(['delivered_today' => $limit]); } /** * Приостановить проект: is_active = false + paused_at = NOW(). * * SnapshotRebuildCommand исключает проекты с is_active = false из нового snapshot. * (Проверено: команда WHERE p.is_active = true.) */ public static function pause(Project $project): void { DB::table('projects') ->where('id', $project->id) ->update([ 'is_active' => false, 'paused_at' => now(), ]); } /** * Заморозить тенанта по балансу: frozen_by_balance_at = NOW(). * * LeadRouter: WHERE tenants.frozen_by_balance_at IS NULL — заморожен = не eligible. * SnapshotRebuildCommand: WHERE t.frozen_by_balance_at IS NULL — не попадёт в snapshot. */ public static function freeze(Tenant $tenant): void { DB::table('tenants') ->where('id', $tenant->id) ->update(['frozen_by_balance_at' => now()]); } /** * Установить регионы проекта (порядковые коды 1..89, НЕ коды ГИБДД). * * LeadRouter Фаза 1: exact — ?::int = ANY(snap.regions). * Пустой массив = «вся РФ» (LeadRouter Фаза 2, regions = '{}'). * * Пример: [82] = только Москва, [82, 83] = Москва + СПб, [] = вся РФ. * Коды через App\Support\RussianRegions::CODE_TO_NAME / nameToCode(). * * @param array $codes Порядковые коды субъектов (1..89). */ public static function setRegions(Project $project, array $codes): void { // PostgreSQL int[] литерал: '{82,83}' или '{}'. $pgArray = '{'.implode(',', array_map('intval', $codes)).'}'; DB::table('projects') ->where('id', $project->id) ->update(['regions' => $pgArray]); } /** * Установить битмаску дней приёма лидов. * * Бит 0 (1) = Понедельник, бит 6 (64) = Воскресенье. * 127 = все 7 дней; 31 = Пн–Пт; 96 = Сб+Вс. * SnapshotRebuildCommand: WHERE (p.delivery_days_mask & weekdayBit) <> 0. * * @param int $mask Битмаска 0..127. */ public static function setDays(Project $project, int $mask): void { DB::table('projects') ->where('id', $project->id) ->update(['delivery_days_mask' => $mask]); } }