feat(signup+admin): aliniere formular signup la landing + plan cerut, GDPR, control tier/trial in panou
Signup: - /signup aliniat ca format la formularul din landing (campuri, etichete, placeholder-uri, select plan, checkbox GDPR, buton). Eticheta `name` = "Companie" (corecta: backendul salveaza nume de firma), uniform si in landing. - Consimtamant GDPR validat server-side (functional, nu doar client-side) + salvat cu marca temporala (accounts.consent_at). - Plan ales la signup salvat in accounts.requested_plan (intentie, NU drept): tier ramane sursa de adevar pentru gate-ul API; coloana pregateste integrarea platilor. - landing: valorile `plan` = coduri tier (free/standard/pro/premium), data-plan sincronizat pe butoanele de pret; checkbox consimtamant primeste name. Schema/DB: - accounts: coloane noi requested_plan + consent_at (cu migrare aditiva in db.py). Panou admin: - Coloane noi: Plan curent (plan EFECTIV acum + zile trial ramase) si Plan cerut. - Buton "Aplica" (POST /admin/set-tier): aloca plan real si INCHEIE trial-ul (efect imediat; altfel trial-ul Pro universal de 30z masca alegerea). - Control "Trial Pro N zile" (POST /admin/set-trial via accounts.set_trial): acorda/prelungeste trial fara a schimba tier-ul de baza. Teste: signup (consent obligatoriu, requested_plan persistat, tier ramane free), panou admin (set-tier incheie trial, free opreste Pro imediat, set-trial, validari + CSRF). Call-site-urile existente POST /signup actualizate cu consent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,8 @@ def create_account(
|
||||
cui: str | None = None,
|
||||
email: str | None = None,
|
||||
active: bool = True,
|
||||
requested_plan: str | None = None,
|
||||
consent_at: str | None = None,
|
||||
) -> int:
|
||||
"""Insereaza un cont si intoarce id-ul nou (AUTOINCREMENT, deci >=2 — nu atinge default id=1).
|
||||
|
||||
@@ -51,12 +53,19 @@ def create_account(
|
||||
`email` se normalizeaza (trim+lower); sir gol -> ValueError.
|
||||
Un CUI deja folosit -> ValueError cu cauza+fix. Unicitatea e impusa de indexul partial
|
||||
`ux_accounts_cui` (nu de un check separat), deci e sigura la concurenta.
|
||||
|
||||
`requested_plan`: planul CERUT la signup (separat de `tier`). NU acorda drepturi — `tier`
|
||||
ramane mereu 'free' la creare; planul cerut e doar o intentie pentru integrarea platilor.
|
||||
Valoare invalida (nu e in VALID_TIERS) -> ignorata (stocata NULL), nu arunca.
|
||||
`consent_at`: marca temporala consimtamant Termeni+GDPR (proba); None = fara flux consimtamant.
|
||||
"""
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
raise ValueError("name gol (un cont are nevoie de nume)")
|
||||
cui = _norm_cui(cui)
|
||||
email = _norm_email(email)
|
||||
# Planul cerut: pastram doar valori valide; orice altceva -> NULL (defensiv).
|
||||
req_plan = requested_plan if requested_plan in VALID_TIERS else None
|
||||
try:
|
||||
# Trial Pro automat la creare (PRD 5.17 US-001): tier='free' + trial_until=now+30z.
|
||||
trial_until = (
|
||||
@@ -64,10 +73,11 @@ def create_account(
|
||||
)
|
||||
# Invariant (5.5): active=1 <=> status='active'; cont creat inactiv = 'pending'.
|
||||
cur = conn.execute(
|
||||
"INSERT INTO accounts (name, cui, email, active, status, tier, trial_until) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO accounts (name, cui, email, active, status, tier, trial_until, "
|
||||
"requested_plan, consent_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(name, cui, email, 1 if active else 0, "active" if active else "pending",
|
||||
"free", trial_until),
|
||||
"free", trial_until, req_plan, consent_at),
|
||||
)
|
||||
except sqlite3.IntegrityError:
|
||||
existing = conn.execute("SELECT id FROM accounts WHERE cui=?", (cui,)).fetchone()
|
||||
@@ -185,6 +195,38 @@ def set_tier(
|
||||
pass
|
||||
|
||||
|
||||
def set_trial(conn: sqlite3.Connection, account_id: int, trial_until: str | None) -> None:
|
||||
"""Seteaza DOAR `trial_until` (acorda/prelungeste/sterge trial Pro), fara a atinge `tier`.
|
||||
|
||||
Trial Pro activ (trial_until in viitor) ridica planul efectiv la 'pro' (vezi
|
||||
plans.effective_tier), indiferent de tier-ul de baza. Folosit din panoul admin ca sa
|
||||
acorzi un trial fara a schimba tier-ul de baza (post-trial).
|
||||
|
||||
Contul de sistem id=1 e protejat. Cont inexistent -> ValueError.
|
||||
trial_until: string ISO UTC ("YYYY-MM-DD HH:MM:SS") sau None (sterge trial-ul).
|
||||
"""
|
||||
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 primi trial (cont de sistem).")
|
||||
conn.execute(
|
||||
"UPDATE accounts SET trial_until=? WHERE id=?", (trial_until, account_id)
|
||||
)
|
||||
# Audit in app_events (best-effort, fara PII nou — ca set_tier).
|
||||
try:
|
||||
from .observ import log_event
|
||||
log_event(
|
||||
"plan_trial_setat",
|
||||
account_id=account_id,
|
||||
mesaj=f"trial_until -> {trial_until or 'NULL'}",
|
||||
context={"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
|
||||
@@ -208,7 +250,8 @@ 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, tier, trial_until, created_at FROM accounts "
|
||||
"SELECT id, name, cui, email, active, status, tier, trial_until, "
|
||||
"requested_plan, consent_at, 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