feat(5.16+5.17): tipografie/antet branded + tipuri cont, planuri si enforcement
PRD 5.16 — propagare design finalizata (system font stack, fara IBM Plex self-hostat): - US-001/002/008: tokeni --font-ui/--font-mono (system stack) + scala --fs-*; zero @font-face si zero /static/fonts/; landing aliniat la acelasi stack - US-003: RAR online = dot compact in antet + meniu burger; banda rosie DOAR pe blocat (invariant zero-silent-failures pastrat) - US-010: antet "ROMFAST AUTOPASS" + nume service + /login brandeit 2 coloane + badge plan; meniu burger cu separatoare; gate strict pe is_authenticated - US-011: selector tema pill icon+eticheta (reuse THEMES) - US-004/005/006/007: bug-fix editor prestatii (picker cod+denumire, add_extra in mod operatii, cod ales se salveaza fara "+", Renunta inchide via closest) - US-012/013: landing Autentificare->/login; wizard import colapsat + 4 pasi pe tokeni - fix VERIFY E2E: contoare duplicate pe 390px (inline display:flex batea @media) -> CSS + test-lock PRD 5.17 — tipuri de cont + trial Pro 30z + enforcement DUR: - US-001/002/008: accounts.tier + trial_until (migrare aditiva defensiva); app/plans.py sursa unica (PLANS, FREE_MONTHLY_LIMIT=60, effective_tier(now injectabil), monthly_usage, CONSUMED_STATUSES); create_account trial Pro 30z; CLI set-tier (protejat id=1, audit) - US-003/004/005: enforce volum 60/luna INAINTE de build_key pe ambele canale (PLAN_LIMITA_LUNARA, 3 niveluri + log_event); gate API Pro+ (PLAN_FARA_API 403 actionabil); valideaza/nomenclator raman permise; downgrade lazy; flag AUTOPASS_ENFORCE_PLANS (kill-switch) - US-006: badge plan antet + linie burger + consum N/60 + warn>=80% + 6 stari + copy RO pluralizat + banner one-time trial->Gratuit + pagina Cont Regresie: 1380 passed, 0 failed, 1 deselected (live). E2E browser pe 390/1280 confirmat. Backend trimitere (worker/masina stari/idempotenta/contract RAR) NEATINS. Lucrul 5.18 (corpus kNN) ramane separat, necomis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ inca fluxul de trimitere. (Addendum A2.)
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
def _norm_cui(cui: str | None) -> str | None:
|
||||
@@ -57,10 +58,16 @@ def create_account(
|
||||
cui = _norm_cui(cui)
|
||||
email = _norm_email(email)
|
||||
try:
|
||||
# Trial Pro automat la creare (PRD 5.17 US-001): tier='free' + trial_until=now+30z.
|
||||
trial_until = (
|
||||
(datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
# Invariant (5.5): active=1 <=> status='active'; cont creat inactiv = 'pending'.
|
||||
cur = conn.execute(
|
||||
"INSERT INTO accounts (name, cui, email, active, status) VALUES (?, ?, ?, ?, ?)",
|
||||
(name, cui, email, 1 if active else 0, "active" if active else "pending"),
|
||||
"INSERT INTO accounts (name, cui, email, active, status, tier, trial_until) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(name, cui, email, 1 if active else 0, "active" if active else "pending",
|
||||
"free", trial_until),
|
||||
)
|
||||
except sqlite3.IntegrityError:
|
||||
existing = conn.execute("SELECT id FROM accounts WHERE cui=?", (cui,)).fetchone()
|
||||
@@ -107,6 +114,8 @@ def set_active(conn: sqlite3.Connection, account_id: int, active: bool) -> None:
|
||||
# Stari de ciclu de viata gestionate explicit (5.5). 'deleted' = stergere soft (purjata de
|
||||
# retentie); restul sunt reversibile.
|
||||
VALID_STATUSES = ("pending", "active", "blocked", "archived", "deleted")
|
||||
# Tieruri de cont valide (5.17). Sursa de adevar: app/plans.py#PLANS (nu duplica valorile).
|
||||
VALID_TIERS = ("free", "standard", "pro", "premium")
|
||||
# Verbele care nu se pot aplica contului de sistem id=1 (protejat, ca la deactivate in 3.3b).
|
||||
_PROTECTED_ACCOUNT_ID = 1
|
||||
|
||||
@@ -131,6 +140,51 @@ def set_status(conn: sqlite3.Connection, account_id: int, status: str) -> None:
|
||||
)
|
||||
|
||||
|
||||
def set_tier(
|
||||
conn: sqlite3.Connection,
|
||||
account_id: int,
|
||||
tier: str,
|
||||
trial_until: str | None = None,
|
||||
) -> None:
|
||||
"""Seteaza planul unui cont (tier + trial_until).
|
||||
|
||||
tier invalid -> ValueError cu mesaj clar.
|
||||
Contul de sistem id=1 e protejat (ca set_status).
|
||||
Cont inexistent -> ValueError.
|
||||
Logheaza schimbarea in app_events (reuse observ.log_event, fara PII nou).
|
||||
|
||||
trial_until: string ISO UTC ("YYYY-MM-DD HH:MM:SS") sau None (sterge trial-ul).
|
||||
"""
|
||||
if tier not in VALID_TIERS:
|
||||
raise ValueError(
|
||||
f"tier invalid: {tier!r} (valid: {', '.join(VALID_TIERS)})"
|
||||
)
|
||||
row = conn.execute("SELECT 1 FROM accounts WHERE id=?", (account_id,)).fetchone()
|
||||
if not row:
|
||||
raise ValueError(f"cont inexistent: {account_id}")
|
||||
if account_id == _PROTECTED_ACCOUNT_ID:
|
||||
raise ValueError(
|
||||
"Contul default (id=1) nu poate fi mutat pe alt plan via CLI "
|
||||
"(cont de sistem, tratat coerent)."
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE accounts SET tier=?, trial_until=? WHERE id=?",
|
||||
(tier, trial_until, account_id),
|
||||
)
|
||||
# Audit in app_events (decizie PRD 5.17 US-008, fara PII nou)
|
||||
try:
|
||||
from .observ import log_event
|
||||
log_event(
|
||||
"plan_schimbare_tier",
|
||||
account_id=account_id,
|
||||
mesaj=f"tier -> {tier}",
|
||||
context={"tier": tier, "trial_until": trial_until},
|
||||
conn=conn,
|
||||
)
|
||||
except Exception: # noqa: BLE001 — jurnal best-effort (ca observ.log_event)
|
||||
pass
|
||||
|
||||
|
||||
def delete_account(conn: sqlite3.Connection, account_id: int) -> None:
|
||||
"""Stergere SOFT: randul ramane ca tombstone (status='deleted', scos din liste), DAR datele
|
||||
sensibile se purjeaza IMEDIAT (GDPR/L.142): credentialele RAR criptate sterse, cheile API
|
||||
@@ -154,7 +208,7 @@ def list_accounts(conn: sqlite3.Connection) -> list[dict]:
|
||||
"""Metadate conturi (FARA `rar_creds_enc`), ordonate dupa id. Exclude conturile 'deleted'
|
||||
(stergere soft -> invizibile in panou)."""
|
||||
rows = conn.execute(
|
||||
"SELECT id, name, cui, email, active, status, created_at FROM accounts "
|
||||
"SELECT id, name, cui, email, active, status, tier, trial_until, created_at FROM accounts "
|
||||
"WHERE status != 'deleted' ORDER BY id"
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
Reference in New Issue
Block a user