-- Migration 191: account_quota_overrides.unlimited — безлимит как данные, не хардкод -- -- WHY: -- app.services.account_quota.is_unlimited() до этой миграции проверял ровно два -- условия: роль admin ИЛИ literal `username == 'kopylov'` — захардкоженное сравнение -- строки прямо в коде. Восстановление пилота praktika (ГК «Практика», доступ вернул -- владелец продукта 2026-07-27 — см. auth/roles.yaml) с безлимитным грантом сделало -- бы это хардкодом ВТОРОГО имени: не масштабируется (каждый следующий безлимитный -- клиент требовал бы code-change + review + deploy вместо data-change) и плохо само -- по себе как паттерн (магическая строка вместо конфигурируемых данных). -- -- unlimited — отдельная boolean-колонка, а не sentinel-значение monthly_limit -- (-1 / 0): `0` неоднозначен («ноль оценок в месяц» vs «без лимита»), explicit -- boolean честнее и не требует специального парсинга в user_limit()/is_unlimited(). -- -- kopylov ПЕРЕНЕСЁН в данные этой же миграцией (хардкод в коде убран, не оставлен -- параллельно) — единый источник правды для non-admin unlimited-аккаунтов вместо -- двух параллельных механизмов (код-константа + таблица). Порядок деплоя -- (SQL-миграция применяется РАНЬШЕ, чем стартует новый код — см. -- .claude/rules/sql.md "Migration order") гарантирует, что строка kopylov уже в -- таблице к моменту, когда новый is_unlimited() (без хардкода) начинает работать — -- поведение kopylov не меняется ни на секунду простоя. -- -- WHAT: -- 1. account_quota_overrides.unlimited boolean NOT NULL DEFAULT false. -- 2. Seed: kopylov (перенос хардкода) + praktika (новый грант, пилот восстановлен -- 2026-07-27) — оба unlimited=true. monthly_limit=999999 — placeholder: get_status() -- безусловно читает monthly_limit через user_limit() даже для unlimited-аккаунтов -- (чтобы вернуть какое-то "limit" поле в /quota), а само enforcement для unlimited -- обходит этот лимит (is_unlimited() гейтит раньше в check_and_raise()/increment()). -- Значение просто не должно выглядеть абсурдным, если когда-либо surfaced напрямую. -- -- IDEMPOTENCY: -- ALTER TABLE ... ADD COLUMN IF NOT EXISTS (новая колонка) + INSERT ... ON CONFLICT -- DO UPDATE (повторный прогон сходится к тому же состоянию, не дублирует строки). -- -- Dependencies: 185_account_quota_overrides.sql (создаёт account_quota_overrides). BEGIN; ALTER TABLE account_quota_overrides ADD COLUMN IF NOT EXISTS unlimited boolean NOT NULL DEFAULT false; COMMENT ON COLUMN account_quota_overrides.unlimited IS 'Безлимитный грант (нет месячного лимита оценок) — читается ' 'app.services.account_quota.is_unlimited(). Заменяет прежний хардкод username в коде.'; INSERT INTO account_quota_overrides (username, monthly_limit, unlimited, note) VALUES ('kopylov', 999999, true, 'Личный аккаунт — безлимит перенесён из хардкода is_unlimited() в данные ' '(migration 191, 2026-07-27), поведение не изменилось') ON CONFLICT (username) DO UPDATE SET monthly_limit = EXCLUDED.monthly_limit, unlimited = EXCLUDED.unlimited, note = EXCLUDED.note, updated_at = now(); INSERT INTO account_quota_overrides (username, monthly_limit, unlimited, note) VALUES ('praktika', 999999, true, 'ГК «Практика» — пилот восстановлен 2026-07-27 (решение владельца продукта), ' 'безлимитный грант') ON CONFLICT (username) DO UPDATE SET monthly_limit = EXCLUDED.monthly_limit, unlimited = EXCLUDED.unlimited, note = EXCLUDED.note, updated_at = now(); COMMIT;