All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 56s
praktika (ГК «Практика») role expired -> pilot: доступ восстановлен по решению
владельца продукта. Добавлен expiredtest QA-аккаунт, чтобы test_rbac.py сохранил
regression-покрытие role=expired (praktika больше не годится как fixture).
account_quota.is_unlimited() больше не хардкодит username == 'kopylov': грант
безлимитных оценок теперь читается из account_quota_overrides.unlimited (migration
191) — единый источник правды для всех non-admin unlimited-аккаунтов. kopylov
перенесён в данные той же миграцией (поведение не меняется, деплой применяет SQL
раньше кода); praktika получил такой же грант.
unlimited выбран как явная boolean-колонка, а не sentinel monthly_limit (-1/0),
т.к. 0 неоднозначен ("ноль оценок" vs "без лимита").
68 lines
4.7 KiB
PL/PgSQL
68 lines
4.7 KiB
PL/PgSQL
-- 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;
|