# PRD 5.17 — Tipuri de cont (planuri) + trial Pro 30 zile + enforcement **Stare**: draft > Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`. > Landing comercial cu planurile: `app/web/templates/landing.html` (sectiunea PRICING). > Lifecycle cont existent: `app/accounts.py`, `app/schema.sql` (tabela `accounts`, coloana `status`). > Signup: `app/web/auth_routes.py` (`signup_post`, butoanele landing trimit `data-plan`). > Starea trece: `draft -> aprobat -> in-executie -> verify-pass -> inchis`. ## 1. Introducere Landing-ul comercial promite patru planuri — **Gratuit**, **Standard (39 lei)**, **Pro (59 lei, cu API)**, **Premium (la cerere)** — si afirma ca **fiecare cont incepe cu acces gratuit 30 de zile** la un plan superior. In aplicatie insa **nu exista deloc conceptul de tip de cont**: tabela `accounts` are doar `status` (pending/active/blocked/archived/deleted) si `on_unmapped_error_default`. Nimic nu diferentiaza un cont gratuit de unul platit, nimic nu aplica limita de volum sau gate-ul de API, si nu exista niciun trial. In plus, userul a decis doua corectii fata de landing-ul actual: 1. Trial-ul de 30 de zile e pe **Pro**, NU pe Premium (landing-ul scrie azi "Premium gratuit 30 de zile" — gresit; trebuie "Pro 30 de zile"). 2. Limita planului **Gratuit** scade de la **100** la **60 de prestatii/luna** — actualizata si in landing si in aplicatie. 5.17 introduce modelul de tipuri de cont, trial-ul Pro de 30 de zile, **enforcement DUR** al diferentelor (volum lunar + acces API), si downgrade automat la expirarea trial-ului. NU include integrare de plata (nu exista inca sistem de facturare) — alocarea planului platit ramane manuala (admin), iar trial-ul porneste automat la creare cont. ## 2. Obiective ### Obiectiv principal Aplicatia sa sustina real diferentele dintre planuri pe care landing-ul le promite: cont nou → trial Pro 30 zile → la expirare downgrade pe Gratuit (60/luna, fara API), cu enforcement efectiv. ### Obiective secundare - Sursa unica de adevar pentru definitia planurilor (limite + capabilitati), consumata de backend si UI. - Mesaje oneste cand un cont atinge limita sau cere o capabilitate neinclusa (3 niveluri, ca 5.4). - Vizibilitate in dashboard: planul curent + zile ramase din trial + consum lunar. ### Metrici de succes - Un cont Gratuit care depaseste 60 prestatii/luna primeste un raspuns clar de respingere (API + web), iar contoarele lunare se reseteaza corect la inceput de luna (timp local RO). - Un cont fara plan Pro+ primeste 403 onest pe `/v1/*` de import API. - Un cont nou are trial Pro activ; dupa 30 zile (sau setand `trial_until` in trecut in test) trece automat pe Gratuit, cu enforcement-ul aferent. - Landing + app afiseaza coerent "60 prestatii/luna" si "Pro gratuit 30 de zile". ## 3. User Stories > Database → backend → API → UI (ordinea dependentelor). Un singur autor pe `accounts.py`/`schema.sql` > in valul de model. ### US-001: Schema — `accounts.tier` + `trial_until` + definitia planurilor **Ca** sistem **vreau** sa stiu planul fiecarui cont si pana cand e in trial **pentru ca** restul logicii depinde de asta. - **Depinde de**: — - **Fisiere**: `app/schema.sql` (coloane noi + migrare defensiva), `app/accounts.py` (helperi), `app/plans.py` (NOU — definitia planurilor, sursa de adevar), `tests/test_accounts.py` / `tests/test_plans.py` (~4 fisiere) - **Test intai (RED)**: `test_migrare_tier_trial_defensiva`, `test_plan_definitii`, `test_cont_nou_trial_pro_30z` - **Acceptance criteria**: - [ ] `accounts` capata (migrare aditiva defensiva, ca `email`/`status` in 5.5/5.12): `tier TEXT NOT NULL DEFAULT 'free' CHECK (tier IN ('free','standard','pro','premium'))` si `trial_until TEXT` (nullable; ISO datetime UTC sau NULL daca nu e in trial). - [ ] `app/plans.py` = SINGURA sursa de adevar: dict `PLANS` cu, per plan, `{label, monthly_limit, api_access, ...}`. Valori: `free` → `monthly_limit=60`, `api_access=False`; `standard` → `monthly_limit=None` (nelimitat), `api_access=False`; `pro` → `monthly_limit=None`, `api_access=True`; `premium` → `monthly_limit=None`, `api_access=True`. (Aliniat landing-ului, cu limita Gratuit 60.) - [ ] Helper `effective_tier(account)`: daca `trial_until` e in viitor → randeaza ca `pro` (trial); altfel `tier`. (Trial-ul = acces Pro temporar peste tier-ul de baza `free`.) - [ ] `create_account` seteaza `tier='free'` si `trial_until = now + 30 zile` (trial Pro automat la creare). Contul implicit id=1 (dev) e exceptat / setat coerent (nu blocheaza dev-ul). - [ ] Migrare idempotenta (re-rulabila); conturile legacy fara `tier` primesc `free` + fara trial (sau trial calculat din `created_at` — decizie la executie; implicit: legacy → free fara trial). - **Verificare E2E**: creez cont nou → `tier=free`, `trial_until ≈ now+30z`, `effective_tier=pro`. ### US-002: Numarator de consum lunar (prestatii/luna pe cont) **Ca** sistem **vreau** sa stiu cate prestatii a trimis un cont in luna curenta **pentru ca** limita Gratuit (60/luna) se aplica pe acest numar. - **Depinde de**: US-001 - **Fisiere**: `app/accounts.py` SAU `app/plans.py` (`monthly_usage(conn, account_id)`), `tests/test_plans.py` (~2 fisiere) - **Test intai (RED)**: `test_consum_lunar_numara_sent_si_queued`, `test_consum_lunar_timp_local_ro`, `test_consum_lunar_resetare_luna_noua` - **Acceptance criteria**: - [ ] `monthly_usage(conn, account_id)` numara prestatiile contului in luna calendaristica curenta. **Definitia "prestatie consumata"** (de fixat la executie, propus): randuri `submissions` ale contului cu `status` in (`queued`,`sending`,`sent`) cu `created_at` in luna curenta — adica prestatiile ACCEPTATE in coada, nu cele respinse/blocate. (Justificare: limita e pe ce trimitem la RAR, nu pe incercari esuate.) Alternativ doar `sent` — de decis; implicit: acceptate-in-coada. - [ ] **Timp local RO** (ca E7 din 5.15): bucketarea lunii foloseste offset RO (`created_at,'+3 hours'` sau echivalent), nu UTC pur, ca prestatiile de la granita de luna sa cada corect. Test la granita. - [ ] Scoped strict pe cont (nu numara cross-account). - [ ] Fara coloana noua daca `submissions.created_at` ajunge (respecta non-goal migrare minima). - **Verificare E2E**: cont cu N trimiteri in luna → `monthly_usage == N`; luna urmatoare → reset la 0. ### US-003: Enforcement DUR — limita lunara Gratuit (60) pe ambele canale **Ca** owner **vreau** ca un cont Gratuit care depaseste 60 prestatii/luna sa fie oprit **pentru ca** asa sustinem diferenta de plan promisa. - **Depinde de**: US-001, US-002 - **Fisiere**: `app/api/v1/router.py` (`create_prezentari`), `app/api/v1/import_router.py` (commit import), `app/errors.py` (cod nou `PLAN_LIMITA_LUNARA`), `app/web/routes.py` (commit web), `tests/test_api_scope.py` / `tests/test_web_*` / `tests/test_plans.py` (~6 fisiere) - **Test intai (RED)**: `test_free_peste_60_respins_api`, `test_free_peste_60_respins_import_web`, `test_pro_si_trial_nelimitat`, `test_eroare_3_niveluri_plan_limita` - **Acceptance criteria**: - [ ] La enqueue (API `POST /v1/prezentari` + commit import web + commit import API), daca `effective_tier` are `monthly_limit` si `monthly_usage + nr_cerut > monthly_limit` → cererea e respinsa (sau respinsa partial, la limita) cu eroare 3 niveluri (`app/errors.py`, cod `PLAN_LIMITA_LUNARA`: problema "Ai atins limita planului Gratuit (60/luna)", cauza, fix "Treci pe Standard/Pro sau astepti luna viitoare"). NU se face enqueue peste limita. - [ ] `standard`/`pro`/`premium` si conturile in **trial Pro** → fara limita de volum. - [ ] Comportament la cerere de lot care depaseste partial limita (ex. 50 folosite, vin 20): decizie la executie — implicit RESPINGERE clara a intregului lot cu mesaj cat mai e disponibil ("mai poti trimite 10 luna asta"), NU enqueue partial tacut (evita surprize). De confirmat. - [ ] Enforcement aliniat cu `AUTOPASS_REQUIRE_API_KEY` (dev vs prod): in dev, contul id=1 nu e blocat artificial (trial/standard coerent), ca dogfooding-ul sa nu se loveasca de limita. - [ ] **Idempotenta neatinsa**: respingerea pe limita se face INAINTE de `build_key`/enqueue; un retry idempotent al unei prestatii deja acceptate nu consuma din nou cota. - **Verificare E2E**: cont free cu 60 trimise → a 61-a respinsa cu mesaj 3 niveluri (API si import web); cont pro → trece. ### US-004: Enforcement DUR — gate API doar pe Pro/Premium **Ca** owner **vreau** ca importul prin API sa fie disponibil doar pe Pro+ **pentru ca** landing-ul spune ca API-ul e o capabilitate Pro. - **Depinde de**: US-001 - **Fisiere**: `app/auth.py` (sau dependinta de ruta), `app/api/v1/router.py`, `app/api/v1/import_router.py`, `app/errors.py` (cod `PLAN_FARA_API`), `tests/test_api_scope.py` (~5 fisiere) - **Test intai (RED)**: `test_free_fara_api_403`, `test_standard_fara_api_403`, `test_pro_api_ok`, `test_trial_pro_api_ok`, `test_dry_run_valideaza_ramane_permis` - **Acceptance criteria**: - [ ] Rutele de **import/ingestie prin API** (`POST /v1/prezentari`, `POST /v1/import`, etc.) cer `effective_tier.api_access == True` (pro/premium sau trial Pro). Altfel 403 cu eroare 3 niveluri (`PLAN_FARA_API`: "Importul prin API e disponibil pe planul Pro", fix). - [ ] **Canalul web ramane neafectat** — operatorii pe plan gratuit pot folosi import xlsx/csv prin dashboard (asa promite landing-ul: Gratuit are import manual, NU API). Doar suprafata API e gated. - [ ] `GET /v1/nomenclator` ramane public (coduri RAR, fara PII) — invariant CLAUDE.md. - [ ] `POST /v1/prezentari/valideaza` (dry-run) — decizie: ramane permis pe orice plan (read-only, ajuta integrarea inainte de upgrade) SAU gated ca restul API. Implicit: PERMIS (read-only, fara enqueue). De confirmat. - [ ] In dev (`AUTOPASS_REQUIRE_API_KEY=false`), contul id=1 are acces API (tier coerent), ca testele API existente sa nu pice. - **Verificare E2E**: cheie API pe cont free → 403 onest pe import; cheie pe cont pro/trial → 200. ### US-005: Downgrade automat la expirarea trial-ului **Ca** owner **vreau** ca la expirarea celor 30 de zile contul sa treaca automat pe Gratuit **pentru ca** landing-ul spune "apoi trece automat pe Gratuit, fara plata". - **Depinde de**: US-001, US-003, US-004 - **Fisiere**: `app/plans.py` (`effective_tier` deja trateaza expirarea — lazy), optional `app/worker/__main__.py` SAU un job de intretinere (eager), `tests/test_plans.py` (~3 fisiere) - **Test intai (RED)**: `test_trial_expirat_efective_free`, `test_trial_expirat_aplica_limita_60`, `test_trial_expirat_pierde_api` - **Acceptance criteria**: - [ ] **Lazy-first**: `effective_tier` returneaza `tier` de baza (`free`) imediat ce `trial_until <= now` — fara job necesar pentru corectitudine (enforcement-ul US-003/004 se bazeaza pe `effective_tier`, deci downgrade-ul e automat la prima cerere dupa expirare). - [ ] Optional (eager, non-blocant): un pas in purjarea orara a worker-ului (T16 existent) poate normaliza `trial_until` expirat → NULL pentru igiena (NU obligatoriu pentru corectitudine). - [ ] Un cont cu `tier='standard'/'pro'/'premium'` setat de admin NU e downgradat de expirarea trial-ului (trial-ul e un BONUS peste `free`; un plan platit alocat persista). - [ ] Mesajele de limita/API dupa expirare sunt cele 3-niveluri din US-003/004. - **Verificare E2E**: setez `trial_until` in trecut → contul aplica limita 60 + pierde API, fara restart. ### US-006: UI dashboard — plan curent + zile ramase din trial + consum lunar **Ca** operator **vreau** sa vad pe ce plan sunt, cat mi-a mai ramas din trial si cat am consumat luna asta **pentru ca** vreau sa stiu cand ma apropii de limita. - **Depinde de**: US-001, US-002 - **Fisiere**: `app/web/routes.py` (context), `app/web/templates/_status.html` SAU `_cont.html` (afisaj plan), `tests/test_web_status.py` / `tests/test_dashboard.py` (~4 fisiere) - **Test intai (RED)**: `test_afisaj_plan_si_zile_trial`, `test_afisaj_consum_lunar`, `test_avertizare_aproape_de_limita` - **Acceptance criteria**: - [ ] Dashboard-ul afiseaza discret planul curent (ex. "Plan: Pro · trial 18 zile ramase" sau "Plan: Gratuit · 47/60 luna asta"). In trial → eticheta "trial" + zile ramase; pe Gratuit → consum `N/60`. - [ ] **Plasare (aliniat cu PRD 5.16)**: planul apare ca **badge in titlul din antet** (`Gratuit`/`Standard`/`Pro`/`Premium`) SI ca linie in **meniul burger** ("Plan: [· trial N zile]"), nu doar intr-un card pe Acasa. Vezi mockup-urile 5.16 (`docs/mockups/prd-5.16-dashboard.html` / `...-mobil.html`). 5.16 furnizeaza locul de afisare (antet + meniu); 5.17 furnizeaza datele (tier, trial, consum). - [ ] Avertizare vizuala cand consumul Gratuit se apropie de limita (ex. ≥80% → ton warn), fara a ingropa stripul de sanatate (zero-silent-failures pastrat). - [ ] Scoped pe cont; design conform 5.15/5.16 (tokeni, fonturi system, fara hex hardcodat). - [ ] Pagina "Cont" arata planul + (daca exista) o explicatie "cum trec pe alt plan" (contact, ca nu exista plata self-service inca). - **Verificare E2E**: cont trial → "trial N zile"; cont free aproape de 60 → avertizare; cont pro → fara contor de limita. ### US-007: Aliniere landing — limita 60 + trial pe Pro (nu Premium) **Ca** vizitator **vreau** ca landing-ul sa spuna adevarul **pentru ca** azi promite "100/luna" si "Premium gratuit 30 zile", dar realitatea va fi 60/luna si trial pe Pro. - **Depinde de**: — (copy-only; aliniaza cu modelul din US-001) - **Fisiere**: `app/web/templates/landing.html`, `tests/test_web_*` (~2 fisiere) - **Test intai (RED)**: `test_landing_limita_60`, `test_landing_trial_pro_nu_premium` - **Acceptance criteria**: - [ ] Toate aparitiile "100 de prestatii/luna" / "100/luna" / `meta description` (`landing.html:7,65,266` + oriunde apar) → **60**. Inclusiv cardul Gratuit din sectiunea PRICING. - [ ] Textul "Fiecare cont incepe cu **Premium gratuit 30 de zile**" (`landing.html:256`) → "**Pro gratuit 30 de zile**" (planul corect). Restul frazei ("Apoi trece automat pe Gratuit…") ramane. - [ ] Coerenta: orice alt loc care implica trial/limita reflecta 60 + Pro. - [ ] Fara alte schimbari de pret/continut (39/59 lei raman). - **Verificare E2E**: landing in browser — "60 prestatii/luna" peste tot, "Pro gratuit 30 de zile". ### US-008: Admin — alocare manuala de plan (fara plata self-service) **Ca** admin **vreau** sa pot seta planul unui cont **pentru ca** nu exista inca facturare automata, dar trebuie sa pot acorda Standard/Pro/Premium. - **Depinde de**: US-001 - **Fisiere**: `tools/account.py` (CLI `set-tier`), optional `app/web/routes.py` (`/admin` actiune), `tests/test_accounts.py` / `tests/test_web_admin*.py` (~3 fisiere) - **Test intai (RED)**: `test_cli_set_tier`, `test_admin_set_tier_scoped`, `test_tier_invalid_respins` - **Acceptance criteria**: - [ ] CLI `python3 -m tools.account set-tier --account N --tier pro [--trial-days 30|--no-trial]` seteaza `tier`/`trial_until`. Tier invalid → eroare clara. - [ ] Optional (la executie): actiune in panoul `/admin` pentru a seta planul unui cont (scoped, CSRF, ca bulk-ul de status din 5.5). Daca nu intra in 5.17, CLI e suficient (admin-only). - [ ] Alocarea unui plan platit de catre admin NU e suprascrisa de expirarea trial-ului (US-005). - [ ] Audit: schimbarea de plan se logheaza in `app_events` (reuse jurnalul din 5.6), fara PII nou. - **Verificare E2E**: `set-tier --account 2 --tier pro` → contul 2 are API + volum nelimitat. ### US-009: Teste de regresie + E2E plan/trial/enforcement **Ca** dezvoltator **vreau** acoperire completa **pentru ca** enforcement-ul atinge ambele canale de ingestie si nu vreau sa blochez gresit conturi legitime. - **Depinde de**: US-003, US-004, US-005, US-006, US-007 - **Fisiere**: `tests/test_plans.py`, `tests/test_api_scope.py`, `tests/test_web_*` (~3 fisiere) - **Test intai (RED)**: matricea plan × capabilitate (volum, API) × canal (API, web) × trial activ/expirat. - **Acceptance criteria**: - [ ] `python3 -m pytest -q -m "not live"` verde; regresia de aur (`POST /v1/prezentari` → queued pe un cont cu drept) ramane verde. - [ ] Matrice testata: free(volum-blocat/API-blocat), standard(volum-ok/API-blocat), pro(ok/ok), trial-pro(ok/ok), trial-expirat(=free). - [ ] Contoarele lunare resetate la luna noua (test la granita timp local RO). - [ ] Dev (id=1) nu e blocat de enforcement (dogfooding). - **Verificare E2E**: rulare completa documentata in Raportul VERIFY. ## 4. Cerinte functionale (rezumat) 1. [REQ-001] `accounts.tier` ∈ {free,standard,pro,premium} + `trial_until`; migrare aditiva defensiva. 2. [REQ-002] `app/plans.py` = sursa unica: limite (free=60/luna) + capabilitati (API doar Pro+). 3. [REQ-003] Cont nou → trial Pro 30 zile automat; `effective_tier` randeaza Pro in trial, free dupa. 4. [REQ-004] Enforcement DUR: free peste 60/luna respins (API + import web) cu eroare 3 niveluri. 5. [REQ-005] Enforcement DUR: import API gated pe Pro+ (403 onest); canalul web ramane liber. 6. [REQ-006] Downgrade automat la expirare trial (lazy via `effective_tier`). 7. [REQ-007] Dashboard arata plan + zile trial + consum lunar; landing aliniat (60, Pro). 8. [REQ-008] Admin aloca planuri manual (CLI `set-tier`), audit in `app_events`. ## 5. Non-Goals (anti scope-creep) - **Fara integrare de plata / facturare / abonamente** (Stripe etc.) — alocarea platita = manuala (admin). - Fara self-service upgrade din UI (doar afisare plan + "contacteaza-ne"); plata vine intr-un PRD viitor. - Fara modificari pe backend-ul de trimitere (worker, masina de stari, idempotenta `build_key`, reconciliere, contract RAR). Enforcement-ul se face la ingestie/enqueue, INAINTE de coada. - Fara schimbarea capabilitatilor de produs in sine (sugestii/mapare exista deja pe toate planurile in cod; diferentierea 5.17 e pe VOLUM + ACCES API, exact ce promite landing-ul ca diferentiator hard). - Fara modificari de design (tipografia/temele sunt 5.16/5.15); doar reuse-ul stilurilor existente. ## 6. Consideratii tehnice - **Stack**: SQLite (migrare aditiva defensiva ca 5.5/5.12), FastAPI, Jinja2/HTMX. - **Patterns de urmat**: sursa unica (`app/plans.py` ca `app/errors.py`); eroare 3 niveluri (5.4); scope pe cont (5.15/US-011); timp local RO la bucketare (5.15/E7); audit `app_events` (5.6). - **Riscuri**: - **Blocare gresita a unui cont legitim** (enforcement prea agresiv) — risc de business. Mitigare: dev id=1 exceptat; teste matrice; mesaje 3 niveluri cu cale de iesire; respingere INAINTE de enqueue (nu pierde date). - **Definitia "prestatie consumata"** (acceptate-in-coada vs sent) schimba cand musca limita. Mitigare: o decidem explicit (US-002 AC) + test; documentam. - **Granita de luna / fus orar** — off-by-a-day la reset. Mitigare: timp local RO + test la granita (lectia E7 din 5.15). - **Idempotenta vs cota** — un retry idempotent nu trebuie sa consume cota de doua ori. Mitigare: enforce inainte de `build_key`; testul de retry. - **Conturi legacy fara tier** — migrare le pune `free`; un cont real activ ar putea fi limitat brusc la 60. Mitigare: decizie de migrare (legacy activ → ce plan?) confirmata cu user inainte de deploy. ## 7. Consideratii UI/UX - Afisaj plan discret, conform 5.16 (fonturi system, tokeni `--fs-*`, fara hex). - Stari: trial activ (zile ramase) / free (consum N/60, warn la ≥80%) / platit (fara contor limita). - Mesaje de respingere oneste, actionabile (cum trec pe alt plan), nu doar "403". ## 8. Open Questions - [ ] "Prestatie consumata" = acceptate-in-coada (queued+sending+sent) sau doar `sent`? (implicit: acceptate) - [ ] Lot care depaseste partial limita → respingere totala sau enqueue partial? (implicit: respingere totala clara) - [ ] `POST /v1/prezentari/valideaza` (dry-run) — gated pe Pro sau permis tuturor? (implicit: permis) - [ ] Migrare conturi legacy active: raman `free` (risc limitare brusca) sau primesc un trial/plan? (de confirmat cu user) - [ ] Standard (39 lei) si Premium difera de Pro doar prin API + suport in landing — pastram exact maparea de capabilitati din landing in `plans.py`? (implicit: da) ## 9. Valuri de executie ``` Val 1: [US-001] schema tier+trial + app/plans.py (autor unic schema/accounts) Val 2: [US-002] numarator consum lunar (dupa model) || [US-007] landing copy 60 + Pro (independent, copy-only) Val 3: [US-003] [US-004] [US-005] enforcement volum + API + downgrade (consuma plans.py) Val 4: [US-006] [US-008] UI dashboard plan/consum || admin set-tier Val 5: [US-009] regresie + E2E matrice (dupa toate) ``` > Secventiere fata de 5.16: independent (5.16 = design/tipografie; 5.17 = model de cont). Pot rula in > paralel; doar US-006 (afisaj plan in `_status.html`) atinge un fisier pe care 5.16/US-003 il modifica > (dot RAR) — serializeaza acel template daca ambele PRD-uri sunt in executie simultan. --- > Acest PRD nu a fost inca trecut prin `/plan-ceo-review` / `/plan-eng-review`. Recomandat inainte de > executie (enforcement de business cu risc de blocare gresita + decizia de migrare a conturilor legacy).