Files
rar-autopass/app/plans.py
Claude Agent 44f261e269 refactor: comentarii strict functionale, fara referinte PRD/stories
Curatare globala a comentariilor si docstring-urilor (app, tools, teste,
scripturi): eliminate referintele la PRD-uri, US-xxx, task-uri istorice si
review-uri; pastrata doar informatia functionala, formulata scurt. Regula
adaugata in CLAUDE.md (sectiunea Stil). Fara modificari de cod sau comportament.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:48:38 +00:00

130 lines
4.9 KiB
Python

"""Definitia planurilor de cont (sursa unica de adevar). Modul PUR, fara import DB/HTTP.
Pattern ca app/errors.py: catalog + helperi. Consumat de rutele de ingestie si dashboard.
Nu importa DB, HTTP, sau orice alt modul intern cu efecte secundare.
Note implementare:
- FREE_MONTHLY_LIMIT: constanta unica, tunabila fara arheologie de cod.
- CONSUMED_STATUSES: prestatie consumata = acceptata in coada.
- effective_tier: `now` injectabil pentru teste deterministe.
- monthly_usage: strftime localtime, `now` injectabil.
"""
from __future__ import annotations
import sqlite3
from datetime import datetime, timezone
# Limita lunara pentru planul Gratuit.
# O singura constanta, referita din PLANS. Tunabila fara a modifica logica de enforcement.
FREE_MONTHLY_LIMIT: int = 60
# Statusurile care consuma din cota lunara.
# Prestatie consumata = acceptata in coada (queued/sending/sent), nu cele respinse/blocate.
# Rationale: limita e pe ce trimitem la RAR, nu pe incercari esuate sau blocate.
CONSUMED_STATUSES: tuple[str, ...] = ("queued", "sending", "sent")
# Sursa unica de adevar pentru planuri. Fiecare plan are:
# label -- eticheta afisata in RO (UI, mesaje)
# monthly_limit -- None = nelimitat; int = limita prestatii/luna
# api_access -- True = acces import prin API (/v1/*); False = doar web dashboard
#
# Aliniat landing-ului comercial:
# Gratuit: 60/luna, fara API
# Standard: nelimitat, fara API
# Pro: nelimitat, cu API
# Premium: nelimitat, cu API (suport dedicat)
PLANS: dict[str, dict] = {
"free": {
"label": "Gratuit",
"monthly_limit": FREE_MONTHLY_LIMIT,
"api_access": False,
},
"standard": {
"label": "Standard",
"monthly_limit": None,
"api_access": False,
},
"pro": {
"label": "Pro",
"monthly_limit": None,
"api_access": True,
},
"premium": {
"label": "Premium",
"monthly_limit": None,
"api_access": True,
},
}
def effective_tier(account_row, now: datetime) -> str:
"""Returneaza tier-ul efectiv al contului la momentul `now` (injectabil pentru determinism).
Daca `trial_until` e in viitor -> 'pro' (trial Pro activ).
Altfel -> `tier`-ul de baza al contului.
trial_until malformat/NULL -> fallback defensiv la tier de baza (nu arunca niciodata).
`now` TREBUIE injectat explicit (nu datetime.now() intern) — decizie #2 din autoplan.
Suporta sqlite3.Row si dict.
"""
# Citire robusta: suporta sqlite3.Row (IndexError pe key absent) si dict (KeyError)
try:
tier = account_row["tier"]
except (KeyError, IndexError, TypeError):
tier = "free"
try:
trial_until_str = account_row["trial_until"]
except (KeyError, IndexError, TypeError):
trial_until_str = None
# Fallback defensiv la 'free' daca tier e None/gol
if not tier:
tier = "free"
if not trial_until_str:
return tier
try:
# Parseaza trial_until; stocam ca "YYYY-MM-DD HH:MM:SS" (UTC implicit) sau ISO
tu = datetime.fromisoformat(trial_until_str.strip().replace(" ", "T"))
# Daca fara timezone -> assume UTC (cum stocam in DB)
if tu.tzinfo is None:
tu = tu.replace(tzinfo=timezone.utc)
# Normalizeaza `now` la aware daca e naive
now_cmp = now
if now_cmp.tzinfo is None:
now_cmp = now_cmp.replace(tzinfo=timezone.utc)
if tu > now_cmp:
return "pro"
except (ValueError, AttributeError, TypeError):
pass # malformat -> fallback defensiv la tier de baza
return tier
def monthly_usage(conn: sqlite3.Connection, account_id: int, now: datetime) -> int:
"""Numara prestatiile contului acceptate in coada in luna calendaristica curenta.
Definitia 'luna curenta': strftime('%Y-%m', created_at, 'localtime') corespunde
lunii lui `now` (acelasi pattern ca E7/5.15 din routes.py — consistent cu 'localtime').
`now` injectabil pentru teste deterministe. Scoped strict pe account_id.
created_at NULL/malformat -> exclus defensiv (nu arunca niciodata).
NOTA: containerul are /etc/localtime=UTC, deci 'localtime' = UTC in mediul de test.
Testele de granita construiesc timestamp-uri relative la luna curenta calculata cu
acelasi 'localtime', nu valori absolute care presupun +2/+3h.
"""
# Formatam `now` ca string SQLite si folosim acelasi modificator 'localtime' ca routes.py
now_str = now.strftime("%Y-%m-%d %H:%M:%S")
placeholders = ",".join("?" * len(CONSUMED_STATUSES))
row = conn.execute(
f"SELECT COUNT(*) AS n FROM submissions "
f"WHERE account_id = ? "
f" AND status IN ({placeholders}) "
f" AND created_at IS NOT NULL "
f" AND strftime('%Y-%m', created_at, 'localtime') = strftime('%Y-%m', ?, 'localtime')",
(account_id, *CONSUMED_STATUSES, now_str),
).fetchone()
return int(row["n"]) if row else 0