gendesign/tradein-mvp/backend/app/services/account_quota.py
lekss361 bd3e5c8216
All checks were successful
Deploy / changes (push) Successful in 5s
Deploy Trade-In / changes (push) Successful in 7s
Deploy / build-backend (push) Successful in 29s
Deploy / build-frontend (push) Successful in 28s
Deploy / build-worker (push) Successful in 29s
Deploy Trade-In / build-backend (push) Successful in 46s
Deploy / deploy (push) Successful in 58s
Deploy Trade-In / build-frontend (push) Successful in 1m40s
Deploy Trade-In / deploy (push) Successful in 39s
feat(tradein): аккаунт praktika + лимит 15 оценок/мес, чистка PRINZIP в UI (#635)
2026-05-28 22:04:58 +00:00

157 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Сервис квоты оценок trade-in — 15 успешных оценок в месяц на аккаунт.
Правила:
- Лимит = 15 успешных оценок за календарный месяц (UTC, период 'YYYY-MM').
- Без лимита (unlimited): роль admin ИЛИ username == 'kopylov'.
- Учитываются ТОЛЬКО успешные оценки (инкремент ПОСЛЕ estimate_quality).
- Если заголовок X-Authenticated-User отсутствует (dev без Caddy) → unlimited,
лимит не применяется (fail-open).
- При исчерпании лимита поднимается HTTPException(429).
"""
from __future__ import annotations
import logging
from datetime import UTC, datetime
from fastapi import HTTPException
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.auth import get_role
logger = logging.getLogger(__name__)
MONTHLY_LIMIT = 15
LIMIT_EXHAUSTED_MESSAGE = (
"Лимит из 15 оценок в этом месяце исчерпан. Свяжитесь с Артёмом Копыловым."
)
def current_period() -> str:
"""Возвращает текущий период в формате 'YYYY-MM' (UTC)."""
return datetime.now(UTC).strftime("%Y-%m")
def is_unlimited(username: str) -> bool:
"""True если пользователь не ограничен квотой.
Unlimited: роль admin ИЛИ username == 'kopylov'.
KeyError (неизвестный пользователь) → трактуется как limited (False).
"""
if username == "kopylov":
return True
try:
role = get_role(username)
return role == "admin"
except KeyError:
return False
def get_status(db: Session, username: str | None) -> dict:
"""Возвращает статус квоты для пользователя.
Если username is None → unlimited True, used 0, remaining 15.
Если unlimited → used = фактический или 0, remaining = limit.
"""
if username is None:
return {
"limit": MONTHLY_LIMIT,
"used": 0,
"remaining": MONTHLY_LIMIT,
"unlimited": True,
}
unlimited = is_unlimited(username)
period = current_period()
row = db.execute(
text(
"""
SELECT used FROM account_estimate_usage
WHERE username = :u AND period_month = :p
"""
),
{"u": username, "p": period},
).fetchone()
used = row.used if row is not None else 0
if unlimited:
return {
"limit": MONTHLY_LIMIT,
"used": used,
"remaining": MONTHLY_LIMIT,
"unlimited": True,
}
remaining = max(0, MONTHLY_LIMIT - used)
return {
"limit": MONTHLY_LIMIT,
"used": used,
"remaining": remaining,
"unlimited": False,
}
def check_and_raise(db: Session, username: str | None) -> None:
"""Проверяет лимит квоты и поднимает 429 если исчерпан.
Если username is None или пользователь unlimited → no-op.
"""
if username is None:
return
if is_unlimited(username):
return
period = current_period()
row = db.execute(
text(
"""
SELECT used FROM account_estimate_usage
WHERE username = :u AND period_month = :p
"""
),
{"u": username, "p": period},
).fetchone()
used = row.used if row is not None else 0
if used >= MONTHLY_LIMIT:
logger.warning(
"quota exhausted: username=%r period=%s used=%d limit=%d",
username, period, used, MONTHLY_LIMIT,
)
raise HTTPException(status_code=429, detail=LIMIT_EXHAUSTED_MESSAGE)
def increment(db: Session, username: str | None) -> None:
"""Инкрементирует счётчик успешных оценок для пользователя.
Если username is None или пользователь unlimited → no-op.
UPSERT: INSERT ... ON CONFLICT DO UPDATE SET used = used + 1.
"""
if username is None:
return
if is_unlimited(username):
return
period = current_period()
db.execute(
text(
"""
INSERT INTO account_estimate_usage (username, period_month, used, updated_at)
VALUES (:u, :p, 1, NOW())
ON CONFLICT (username, period_month)
DO UPDATE SET
used = account_estimate_usage.used + 1,
updated_at = NOW()
"""
),
{"u": username, "p": period},
)
db.commit()
logger.debug(
"quota incremented: username=%r period=%s", username, period
)