All checks were successful
CI Trade-In / backend-tests (pull_request) Successful in 4m54s
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
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
Two bugs hit the first paying pilot (user2/Брусника):
1. Bonus attempts were granted via negative `used` hack
(account_estimate_usage.used = -35), which made /quota return
{limit:15, used:-35, remaining:50} -> frontend rendered the absurd
"Осталось 50 из 15". Replaced with a proper personal monthly limit:
account_quota_overrides table (migration 184) + account_quota.user_limit()
used across get_status/check_and_raise/increment instead of the hardcoded
global MONTHLY_LIMIT. get_status now clamps remaining = max(0, limit -
max(0, used)) so remaining can never exceed limit even if a negative
used leaks through again. Migration seeds user2 -> monthly_limit=50 and
resets the old hack (used<0 -> 0), so user2 now sees "50 из 50".
2. POST /estimate unconditionally called account_quota.increment() after
estimate_quality, even when the address failed to geocode and the
handler returned the _empty_estimate fallback (median=0, n_analogs=0,
insufficient_data=True, HTTP 200) -- burning a paid slot for a null
result. Now increment is skipped when result.insufficient_data is True;
the #747 TOCTOU-safe atomic increment/429 gate is unchanged for real
results.
59 lines
3.3 KiB
PL/PgSQL
59 lines
3.3 KiB
PL/PgSQL
-- Migration 184: account_quota_overrides — персональные месячные лимиты оценок
|
||
--
|
||
-- WHY:
|
||
-- Бонусные попытки для платящих пилотов раздавались хаком через SQL-runbook:
|
||
-- `UPDATE account_estimate_usage SET used = used - N` (negative used). Для user2
|
||
-- (Брусника) это дало used=-35 при глобальном limit=15 → GET /quota отдавал
|
||
-- {limit:15, used:-35, remaining:50} → фронт (EstimateForm.tsx) рендерил абсурд
|
||
-- «Осталось 50 из 15». Правильное решение — персональный лимит, а не отрицательный
|
||
-- счётчик использования.
|
||
--
|
||
-- WHAT:
|
||
-- 1. Таблица account_quota_overrides(username PK, monthly_limit, note, updated_at) —
|
||
-- per-user override глобального settings.estimate_quota_limit. Читается через
|
||
-- app.services.account_quota.user_limit().
|
||
-- 2. Seed: user2 (Брусника) → monthly_limit=50, пилотный грант 2026-07-13.
|
||
-- 3. Сброс хака: account_estimate_usage.used < 0 → 0 (для user2, единственного
|
||
-- затронутого аккаунта). После этой миграции user2: limit=50, used=0,
|
||
-- remaining=50 — корректно вместо «50 из 15».
|
||
--
|
||
-- IDEMPOTENCY:
|
||
-- CREATE TABLE IF NOT EXISTS + INSERT ... ON CONFLICT DO UPDATE (upsert, безопасен
|
||
-- при повторном применении) + UPDATE ... WHERE used < 0 (после первого прогона
|
||
-- used=0, условие больше не матчит — безопасный no-op при ре-apply).
|
||
--
|
||
-- Dependencies: 076_account_estimate_quota.sql (account_estimate_usage).
|
||
|
||
BEGIN;
|
||
|
||
CREATE TABLE IF NOT EXISTS account_quota_overrides (
|
||
username text PRIMARY KEY,
|
||
monthly_limit integer NOT NULL,
|
||
note text,
|
||
updated_at timestamptz NOT NULL DEFAULT now()
|
||
);
|
||
|
||
COMMENT ON TABLE account_quota_overrides IS
|
||
'Персональный override месячного лимита оценок trade-in (вместо глобального '
|
||
'settings.estimate_quota_limit). Читается app.services.account_quota.user_limit().';
|
||
|
||
COMMENT ON COLUMN account_quota_overrides.monthly_limit IS
|
||
'Персональный лимит успешных оценок в календарный месяц для username.';
|
||
|
||
COMMENT ON COLUMN account_quota_overrides.note IS
|
||
'Причина override (grant / pilot / manual bump) — для аудита ручных изменений.';
|
||
|
||
INSERT INTO account_quota_overrides (username, monthly_limit, note)
|
||
VALUES ('user2', 50, 'Брусника — пилот, грант 2026-07-13')
|
||
ON CONFLICT (username) DO UPDATE SET
|
||
monthly_limit = EXCLUDED.monthly_limit,
|
||
note = EXCLUDED.note,
|
||
updated_at = now();
|
||
|
||
-- Сбрасываем прежний хак бонусных попыток (negative used), который ломал /quota:
|
||
-- limit=15, used=-35 → remaining=50 → фронт рендерил «Осталось 50 из 15».
|
||
UPDATE account_estimate_usage
|
||
SET used = 0, updated_at = now()
|
||
WHERE username = 'user2' AND used < 0;
|
||
|
||
COMMIT;
|