docs(prd): 5.16 tipografie+bugfix editare + 5.17 tipuri cont + mockup-uri

PRD 5.16 (draft) — propagare design uniform peste aplicatie:
- fonturi standard web (system font stack), scala uniforma --fs-* (carduri aerisite)
- RAR online = dot in antet (datetime pe hover) + meniu burger; banda doar cand e blocat
- antet branded "ROMFAST AUTOPASS" + nume service + badge plan (gate is_authenticated)
- /login profesional (antet minimal pre-login), selector tema stil landing
- bug-uri editare: denumiri in picker, adaugare operatie extra, fix save no-op, fix Renunta
- dashboard compact: strip-less, contoare separate (mobil = bara numere), import colapsat,
  ordine carduri->import->tab-uri->lista, meniu cu separatoare
- wizard import (4 pasi) + editare/corectie aliniate la design

PRD 5.17 (draft) — tipuri de cont (Gratuit/Standard/Pro/Premium) + trial Pro 30 zile:
- model accounts.tier + trial_until, app/plans.py sursa unica
- enforcement DUR: limita Gratuit 60/luna (era 100) + API doar Pro+
- downgrade automat la expirare trial; aliniere landing (60, "Pro gratuit 30 zile")

Mockup-uri vizuale (docs/mockups/prd-5.16-*.html): fonturi, header+login+tema,
dashboard desktop+mobil, wizard import. Doar documentatie + mockup-uri; fara cod aplicatie.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-28 21:20:20 +00:00
parent 3fc53534e2
commit 8dd0e1678c
7 changed files with 1912 additions and 0 deletions

View File

@@ -0,0 +1,498 @@
# PRD 5.16 — Tipografie uniforma (fonturi standard web) + bug-fix formular editare + E2E
**Stare**: draft
> Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`.
> Sistemul de design: `DESIGN.md` + `app/web/templates/base.html`. Landing: `app/web/templates/landing.html`.
> Mockup tipografie (REFERINTA VIZUALA): `docs/mockups/prd-5.16-fonturi-system-stack.html`
> — system font stack, scala uniforma, DOT pentru RAR online, denumiri in picker, buton Renunta.
> Mockup header + login + selector tema (REFERINTA VIZUALA): `docs/mockups/prd-5.16-header-login-tema.html`
> — antet branded "ROMFAST AUTOPASS" + nume service, pagina /login profesionala, selector tema stil landing.
> Mockup dashboard COMPLET (REFERINTA VIZUALA): `docs/mockups/prd-5.16-dashboard.html`
> — pagina Acasa cu antet branded, selector tema landing, strip DOT, contoare SEPARATE
> (Total/Luna/Azi/In coada/De corectat), lista slim, modal editare cu picker+denumiri+Renunta.
> Mockup dashboard MOBIL 390px (REFERINTA VIZUALA): `docs/mockups/prd-5.16-dashboard-mobil.html`
> — Acasa + editare full-screen pe telefon: antet compact (tema doar iconita <=560px ca pe landing),
> contoare separate (Total prominent + 2x2), strip DOT cu text pe 2 linii, butoane full-width.
> Mockup WIZARD import + editare/corectie (REFERINTA VIZUALA): `docs/mockups/prd-5.16-import-wizard.html`
> — cei 4 pasi (1 Incarca · 2 Potriveste coloanele · 3 Verifica · 4 Confirma), import colapsat,
> preview cu stari (Cod RAR lipsa/Date incomplete/Duplicat/Deja trimis) + editare/corectie rand
> inline (picker cod+denumire, + adauga operatie, salveaza regula, Renunta).
> Continua si finalizeaza propagarea de design inceputa in 5.15 (`docs/prd/prd-5.15-propagare-design-dashboard-editare.md`).
> Starea trece: `draft -> aprobat -> in-executie -> verify-pass -> inchis`.
## 1. Introducere
PRD 5.15 a propagat sistemul de design al landing-ului in aplicatie (carduri-contor, lista slim,
formular slim cu chips, 7 teme). La folosire reala userul a constatat ca rezultatul NU respecta inca
claritatea exemplelor din landing: **fonturile sunt prea mici si neuniforme** intre pagini si
formulare, **cardurile au textul inghesuit**, iar in formularul de editare au ramas patru
**bug-uri functionale** care fac corectia trimiterilor frustranta sau imposibila. In plus userul vrea
**fonturi standard web** (fara fisiere de font instalate), aceleasi in aplicatie SI in landing.
5.16 finalizeaza propagarea de design pe doua planuri:
1. **Tipografie**: o singura scala uniforma, lizibila, mai mare, pe fonturi de sistem (system font
stack) — atat in aplicatie cat si in landing — eliminand IBM Plex self-hostat.
2. **Bug-fix + ergonomie formular editare**: cele 4 probleme reale + indicatorul RAR online ca DOT.
Plus o trecere E2E in browser pe toate paginile, conforme cu `DESIGN.md` si spiritul landing-ului.
## 2. Obiective
### Obiectiv principal
Aplicatia sa para acelasi produs ca landing-ul comercial: tipografie clara, uniforma, lizibila, fara
text inghesuit; iar formularul de editare sa functioneze corect (salveaza, se inchide, permite
adaugarea de operatii cu denumiri citibile).
### Obiective secundare
- Zero fisiere de font in runtime (gateway intern) — fonturi 100% native.
- Sursa unica de adevar pentru dimensiunile de text (tokeni de scala), nu valori ad-hoc per template.
- Indicator de stare RAR online consistent cu landing-ul (dot pulsant, nu bifa).
### Metrici de succes
- Un esantion de 5 pagini (Acasa, Trimiteri, detaliu/editare, Mapari, Integrare) folosesc ACELEASI
dimensiuni de text pentru acelasi rol (label, body, cifra) — verificabil prin tokeni.
- Cele 4 bug-uri din §3 (US-004..US-007) reproductibile inainte, ne-reproductibile dupa (teste lock).
- E2E browser: zero overflow orizontal, fonturi native incarcate (fara request la `/static/fonts/`).
## 3. User Stories
> Backend + UI pentru acelasi comportament = stories separate. `base.html` e fisier FIERBINTE
> (serializat — un singur autor pe val). Toate UI verificate pe un esantion de teme (o luminoasa +
> una intunecata) si pe 390/1280.
### US-001: Fonturi standard web (system font stack) — eliminam IBM Plex self-hostat
**Ca** operator **vreau** fonturi web standard, lizibile, fara sa instalez nimic **pentru ca** vreau
text clar si uniform, fara dependente de fisiere de font.
- **Depinde de**: —
- **Fisiere**: `app/web/templates/base.html` (`--font-ui`/`--font-mono` + `font-family` body),
`app/web/templates/landing.html` (sterge cele 9 `@font-face` IBM Plex + `font:... 'IBM Plex Sans'`),
`DESIGN.md` (sectiunea Tipografie rescrisa), `tests/test_tema.py` / `tests/test_web_responsive.py`
(~4 fisiere)
- **Test intai (RED)**: `test_font_stack_system_in_base`, `test_landing_fara_font_face_ibm_plex`,
`test_zero_referinte_static_fonts`
- **Acceptance criteria**:
- [ ] `base.html` defineste doi tokeni sursa-de-adevar in `:root`:
`--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;`
si `--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Segoe UI Mono", "Roboto Mono",
Menlo, Consolas, monospace;`. `body` foloseste `var(--font-ui)`; codurile/VIN folosesc
`var(--font-mono)` prin clasa existenta (`.camp-mono`/`.slim-vin` etc.).
- [ ] `landing.html`: cele 9 reguli `@font-face` IBM Plex Sans/Mono ELIMINATE; toate aparitiile
`font: ... 'IBM Plex Sans'` / `'IBM Plex Mono'` inlocuite cu `var(--font-ui)` / `var(--font-mono)`
(sau un fallback explicit identic cu app-ul). Landing si app folosesc ACELASI stack.
- [ ] **Zero referinte la `/static/fonts/`** raman in template-uri (grep negativ in test). Fisierele
woff2 din `app/web/static/fonts/` pot ramane pe disc (curatare = follow-up optional, non-blocant),
dar nu mai sunt referite.
- [ ] Diacriticele romanesti (ă/â/î/ș/ț) se randeaza corect pe stack-ul nativ (acoperite de fonturile
de sistem — verificat vizual in E2E).
- [ ] `DESIGN.md` §Tipografie rescrisa: motiveaza system font stack (zero dependente runtime,
nativ pe fiecare OS), documenteaza cele doua stive, noteaza tradeoff-ul (aspect usor diferit
Windows/Mac/Linux) ca decizie acceptata de user.
- **Verificare E2E**: DevTools Network pe `/` si pe landing — niciun request catre `/static/fonts/`;
text lizibil pe Windows (Segoe UI) verificat de operator la deploy.
### US-002: Scala tipografica uniforma (tokeni `--fs-*`) + carduri aerisite
**Ca** operator **vreau** dimensiuni de text uniforme si mai mari, fara text inghesuit **pentru ca**
azi fonturile sunt ad-hoc (10/11/13px) si difera de la pagina la pagina.
- **Depinde de**: US-001 (acelasi val, `base.html`)
- **Fisiere**: `app/web/templates/base.html` (tokeni `--fs-*` + clasele slim/contor/camp consumandu-i),
`DESIGN.md`, `tests/test_web_responsive.py` (~3 fisiere)
- **Test intai (RED)**: `test_tokeni_scala_fs_definiti`, `test_componente_slim_folosesc_fs_tokeni`,
`test_fara_font_size_sub_12px_in_componente_noi`
- **Acceptance criteria**:
- [ ] `base.html` defineste o scala unica in `:root` (sursa de adevar): `--fs-xs:12px`, `--fs-sm:13.5px`,
`--fs-base:15px`, `--fs-md:16px`, `--fs-lg:18px`, `--fs-xl:20px`, `--fs-2xl:28px`, `--fs-3xl:34px`
(+ `--lh-tight`/`--lh-body`). Valorile finale pot fi ajustate la executie, dar DEFINITE ca tokeni.
Referinta vizuala: `docs/mockups/prd-5.16-fonturi-system-stack.html`.
- [ ] Componentele slim din 5.15 (`.contor-cifra`, `.contor-label`, `.contor-sub`, `.slim-vin`,
`.slim-meta`, `.camp-slim label/input`, `.chip`, `.op-row-name`, pill-uri) sunt RECABLATE pe
tokenii `--fs-*` (NU mai au px hardcodat sub 12px). Minim crestere fata de azi:
label-uri 11→13.5px, sub-linii 10→12px, cifra contor 22→28px, input 13→16px.
- [ ] **Carduri aerisite**: `.contor-card` si randurile slim cresc padding-ul (contor ~18px,
rand slim ~14px) ca textul mai mare sa nu para inghesuit; fara overflow pe 390px.
- [ ] **Contoare separate (NU inghesuite)**: statisticile "Trimise" se afiseaza ca **carduri
distincte** — `Total`, `Luna asta`, `Azi` fiecare cu eticheta proprie, pe langa `In coada` si
`De corectat` (5 in total). NU se mai inghesuie `luna`/`azi` ca sub-linie mono sub cifra "Total"
(asa cum era in 5.15/D4). Pe **desktop**: 5 carduri pe un rand, FARA titlu/eticheta de grup
deasupra (minimalist — userul nu vrea subtitlu de sectiune). Referinta:
`docs/mockups/prd-5.16-dashboard.html`.
**Nota**: REVIZUIESTE framing-ul D4/US-003 din 5.15 (cifra mare all-time + sub-linie `luna · azi`).
Sursa de timp/bucketare RO ramane cea din 5.15/US-003 (`sent_today`/`sent_month`, timp local RO
E7); doar PREZENTAREA se schimba.
- [ ] **Mobil: contoare COMPACTE (doar numere)**: pe 390px contoarele NU mai sunt 5 carduri mari, ci
o **bara compacta de statistici pe un singur rand** — cifra mare + eticheta scurta (`Total`/`Lună`/
`Azi`/`Coadă`/`Corectat`), separatoare subtiri, inaltime mica, fara a ocupa jumatate de ecran.
Referinta: `docs/mockups/prd-5.16-dashboard-mobil.html`.
- [ ] **Ordine dashboard (compact, minimalist)**: pe Acasa, ordinea verticala e **(1) carduri-contor
→ (2) import colapsat → (3) tab-uri Trimiteri/Mapari → (4) lista**. Indicatorul RAR online NU mai
ocupa o banda in corp — sta ca dot in antet (US-003). Tab-urile Trimiteri/Mapari stau imediat
deasupra listei (nu sub antet). Atat pe desktop cat si pe mobil.
- [ ] **Fara duplicare / fara subtitlu de sectiune**: numele service-ului apare DOAR in antet
(US-010), NU se mai repeta intr-un meta "Service Auto … · data" deasupra listei. Se ELIMINA
titlul de sectiune "Trimiteri RAR AUTOPASS" + linia meta de sub el (ocupau spatiu pe desktop si
mobil) — lista incepe direct sub tab-uri/filtre.
- [ ] `body` are `font-size:var(--fs-base)` si `line-height:var(--lh-body)` (azi implicit de browser).
- [ ] Zero regresie vizuala pe componentele existente non-slim (`.card/.pill/.act/.tabel-trimiteri`):
crestem unde e prea mic, fara a sparge layout-ul tabelului desktop.
- [ ] `DESIGN.md` §Tipografie + §Componente slim actualizat cu scala in tokeni.
- **Verificare E2E**: browser pe Acasa + Trimiteri + editare, esantion de teme + 390/1280 — text mai
mare, uniform, fara inghesuire, fara overflow.
### US-003: RAR online — dot compact in antet (nu banda) + in meniul burger; banda DOAR cand e blocat
**Ca** operator **vreau** ca "RAR online" sa fie un dot mic in antet (cu data/ora pe hover) si in meniul
burger, NU o banda care ocupa un rand intreg **pentru ca** cand totul e ok nu vreau sa-mi fure spatiu —
dar cand e blocat trebuie sa fie imposibil de ratat.
- **Depinde de**: US-002, US-011 (antet/selector tema)
- **Fisiere**: `app/web/templates/base.html` (antet + meniu burger), `app/web/templates/_status.html`
(banda doar pe blocat + dot), `app/web/routes.py` (context sanatate in layout, nu doar fragment),
`DESIGN.md`, `tests/test_web_status.py`, `tests/test_web_status_fragment.py` (~6 fisiere)
- **Test intai (RED)**: `test_rar_dot_in_antet_ok`, `test_rar_in_meniu_burger`,
`test_banda_apare_doar_cand_blocat`, `test_dot_title_datetime`, `test_blocat_text_accesibil`
- **Acceptance criteria**:
- [ ] **Stare OK (online)**: indicatorul RAR e un **dot/pill compact in antet**, langa selectorul de
tema (ex. dot verde + "RAR online"), stilat ca pill-ul "Live" din landing (`landing.html:117`).
Data/ora ultimei autentificari RAR apare pe **`title`/hover** (`title="Ultima autentificare RAR:
28.06.2026 09:41"`), NU pe un rand separat. NU mai exista banda full-width in corpul paginii in
starea OK. Referinta: `docs/mockups/prd-5.16-dashboard.html`.
- [ ] **Si in meniul burger**: starea RAR (dot + "RAR online" + ora ultimei autentificari) apare ca
prima intrare in meniul de cont, pentru cazul in care antetul e ingust (mobil) — vezi
`docs/mockups/prd-5.16-dashboard-mobil.html`.
- [ ] **Stare BLOCAT (worker oprit / RAR inaccesibil)**: ATUNCI (si numai atunci) reapare **banda
rosie full-width**, loud, in corp ("Blocat: RAR inaccesibil — declaratiile NU pleaca"), iar
dot-ul din antet devine rosu. Invariant zero-silent-failures (D6/5.15) PASTRAT: blocajul e
imposibil de ratat; doar starea OK se comprima la un dot.
- [ ] **Accesibilitate**: sensul NU depinde de culoare — `title`/`aria-label` pe dot ("RAR online" /
"RAR blocat") + textul explicit al benzii pe blocat poarta sensul pentru screen-reader/daltonisti.
Se inlocuieste glifa bifa/X (`✓/✗`) cu dot + text.
- [ ] `DESIGN.md` §Header & branding + §Componente actualizat: dot de stare in antet + banda doar pe
blocat documentate.
- **Verificare E2E**: browser pe `/` — worker viu → dot verde in antet (+ in meniu), fara banda, ora pe
hover; worker oprit → dot rosu + banda rosie in corp; screen-reader confirma sensul fara culoare.
### US-004: Bug — picker prestatii arata DENUMIREA, nu doar codul
**Ca** operator **vreau** sa vad denumirea operatiei/codului RAR cand aleg din picker **pentru ca**
nu imi dau seama ce e doar din cod (ex. "REV2").
- **Depinde de**: — (independent; UI-only in template chips)
- **Fisiere**: `app/web/templates/_chips_prestatii.html`, `tests/test_web_mapare_din_chip.py` SAU
`tests/test_web_corectie_prestatii.py` (~2 fisiere)
- **Test intai (RED)**: `test_picker_flat_arata_cod_si_denumire`, `test_picker_op_arata_denumire`
- **Acceptance criteria**:
- [ ] In **modul plat** (corectie pura, fara `op_service`), picker-ul `chips_add_cod_flat`
(`_chips_prestatii.html:146-148`) afiseaza `{{ cod }} — {{ nume_prestatie }}` pentru fiecare
optiune (azi arata DOAR `cod_prestatie`). Acelasi format ca modul operatii (`:101`).
- [ ] Optiunea placeholder ramane lizibila ("+ cod RAR" sau "— alege cod RAR —"); latimea
selectului creste cat sa incapa denumirea fara a sparge layout-ul chips.
- [ ] Consistenta: ORICE select de cod RAR din formularul de editare (plat + per-operatie) arata
cod + denumire. Niciun loc nu mai arata cod gol.
- **Verificare E2E**: editez o trimitere fara cod (corectie pura) → deschid picker-ul → optiunile arata
"FRN1 — Sistem de franare", nu doar "FRN1".
### US-005: Bug — adaugare de operatii/coduri RAR suplimentare la editare
**Ca** operator **vreau** sa pot adauga ALTE operatii/coduri RAR pe o trimitere cu probleme **pentru ca**
azi, cand trimiterea are deja operatii, nu pot adauga un cod RAR in plus.
- **Depinde de**: US-004 (acelasi template)
- **Fisiere**: `app/web/templates/_chips_prestatii.html`, `app/web/routes.py` (`post_form_chips`,
`routes.py:1867-1957` — actiune noua `add_extra`), `tests/test_web_corectie_prestatii.py` (~3 fisiere)
- **Test intai (RED)**: `test_adauga_cod_extra_in_mod_operatii`, `test_extra_cod_persistat_la_salvare`,
`test_extra_cod_validat_nomenclator`
- **Acceptance criteria**:
- [ ] In **modul operatii** (`_has_ops`), sub lista de operatii apare un control "**+ Adauga alta
operatie / cod RAR**" (picker cod+denumire + buton), care adauga un chip de cod RAR liber
(fara `op_service`) la lista — pe langa codurile per-operatie existente. Azi acest control
exista DOAR in modul plat; il aducem si in modul operatii.
- [ ] `post_form_chips` primeste o actiune noua (`chips_action=add_extra`) care valideaza codul fata
de nomenclator (invariant ORA-12899) si il adauga ca item `{cod_prestatie, cod_op_service:"",
denumire:""}`. Re-randeaza partial-ul chips.
- [ ] Codul extra adaugat SE PERSISTA la salvare: e emis ca hidden `cod_prestatie` (paralel cu
`chip_op_service`/`chip_denumire` goale) si cules de `post_corectie_trimitere`
(`routes.py:1334 getlist`). Lista finala `prestatii` contine si codurile per-operatie SI
codurile extra (dedup per-item E4 din 5.15 pastrat).
- [ ] Stergere simetrica: chip-ul extra are `×` (reuse `remove`/`remove_flat`).
- **Verificare E2E**: editez o trimitere cu o operatie mapata → adaug un cod RAR extra → salvez →
`prestatii` contine ambele coduri (verificat in detaliu / payload).
### US-006: Bug — salvarea codului ales pe o trimitere fara cod nu facea nimic
**Ca** operator **vreau** ca atunci cand aleg un cod RAR si salvez, sa se aplice **pentru ca** azi,
la o trimitere care nu avea cod operatie, aleg codul dar la salvare nu se intampla nimic.
- **Depinde de**: US-004, US-005
- **Fisiere**: `app/web/templates/_chips_prestatii.html`, `app/web/routes.py`
(`post_corectie_trimitere` / `post_form_chips`), `tests/test_web_corectie_prestatii.py` (~3 fisiere)
- **Test intai (RED)**: `test_cod_ales_in_picker_se_salveaza_fara_buton_add`,
`test_salvare_fara_chip_explicit_nu_e_no_op`
- **Acceptance criteria**:
- [ ] **Reproducere (RED)**: o trimitere fara `cod_prestatie` (mod plat, ZERO chips). Userul alege
un cod in picker-ul `chips_add_cod_flat` si apasa direct "Salveaza" (FARA sa apese "+" intai).
Azi: `getlist("cod_prestatie")` e gol → submission ramane `needs_mapping` → "nu se intampla
nimic". Cauza confirmata: selectul `chips_add_cod_flat` NU e citit de `post_corectie_trimitere`.
- [ ] **Fix**: `post_corectie_trimitere` (si `/repune`, `/editeaza`) culeg si codul nesubmis din
picker (`chips_add_cod_flat` + `chips_add_cod_{i}` per-operatie) ca un add implicit la salvare,
SAU formularul promoveaza automat selectia in curs intr-un chip inainte de submit (progressive
enhancement JS + fallback server). Rezultat: codul ales se aplica chiar fara click pe "+".
Decizie de implementare (la executie): preferinta pe calea SERVER (citeste picker-ul la submit),
ca sa functioneze si fara JS — aliniat cu E6 server-driven din 5.15.
- [ ] Codul ales e validat fata de nomenclator; cod necunoscut → mesaj, nu trimitere raw.
- [ ] Nu rupe fluxul existent "+ apoi Salveaza" (ramane valid; nu se dubleaza codul — dedup per-item).
- **Verificare E2E**: editez o trimitere needs_mapping fara cod → aleg un cod in picker → Salveaza
(fara "+") → submission devine `queued` cu codul ales.
### US-007: Bug — butonul Renunta/Anuleaza inchide formularul
**Ca** operator **vreau** ca butonul Renunta sa inchida modalul **pentru ca** azi nu il inchide si
raman blocat in formular.
- **Depinde de**: —
- **Fisiere**: `app/web/templates/base.html` (handler `data-modal-close`, `base.html:1131-1133`),
`tests/test_web_corectie.py` SAU un test JS/markup nou (~2 fisiere)
- **Test intai (RED)**: `test_anuleaza_are_data_modal_close`,
`test_modal_close_pe_element_interior` (markup: butonul Anuleaza contine `<span>`/icon → tinta de
click e copilul)
- **Acceptance criteria**:
- [ ] **Cauza confirmata**: handler-ul `overlay.addEventListener('click', ...)` (`base.html:1132`)
verifica `e.target.hasAttribute('data-modal-close')`. Butonul Anuleaza/Renunta
(`_form_editare.html:106`) are atributul pe `<button>`, dar contine `<span class="act-tx">` +
un SVG icon → la click pe text/icon, `e.target` e copilul fara atribut → `close()` NU se apeleaza.
(X-ul `.modal-close` merge doar fiindca e `&times;` fara copii.)
- [ ] **Fix**: handler-ul foloseste `e.target.closest('[data-modal-close]')` in loc de
`hasAttribute` direct, ca un click pe orice descendent al unui element cu `data-modal-close`
sa inchida modalul. Pastreaza inchiderea pe backdrop + Esc.
- [ ] Butonul de Renunta e prezent in AMBELE modale unde se editeaza (detaliu + preview import):
verifica `with_cancel` / eticheta. Daca in modalul de detaliu nu exista buton de inchidere
in form, se adauga unul (text "Renunta"), nu doar X-ul din colt.
- [ ] Focus-ul revine logic dupa inchidere (comportament `close()` existent pastrat).
- **Verificare E2E**: deschid editarea → apas Renunta (pe text si pe icon) → modalul se inchide,
focus revine pe rand; Esc si backdrop inchid in continuare.
### US-008: Landing — aliniere copy si fonturi cu aplicatia (fara redesign)
**Ca** vizitator **vreau** ca landing-ul sa foloseasca aceleasi fonturi ca aplicatia **pentru ca**
userul a cerut explicit fonturi web uniforme intre cele doua.
- **Depinde de**: US-001
- **Fisiere**: `app/web/templates/landing.html`, `tests/test_web_*` (~2 fisiere)
- **Test intai (RED)**: `test_landing_foloseste_font_stack_app`
- **Acceptance criteria**:
- [ ] Landing-ul foloseste `var(--font-ui)`/`var(--font-mono)` (sau acelasi literal de stack) —
consecinta directa a US-001, fara redesign de layout.
- [ ] Mockup-ul intern din landing (cardul "prestatie noua", `landing.html:155-171`) — care arata
"Confirma Vin" ca al doilea camp VIN — ramane DOAR ilustrativ; NU il aliniem la formularul real
(VIN unic) in acest PRD (non-goal: redesign landing). Notat ca debt vizual minor.
- [ ] Fara alte modificari de continut in 5.16 (copy-ul de plan/limita 60 e in PRD-ul de tipuri-cont).
- **Verificare E2E**: landing in browser — fonturi identice cu app, fara request la `/static/fonts/`.
### US-010: Antet branded "ROMFAST AUTOPASS" + nume service + /login profesional
**Ca** operator **vreau** un antet profesional cu "ROMFAST AUTOPASS" si numele service-ului meu, si o
pagina /login pe masura **pentru ca** azi antetul arata generic ("Gateway RAR AUTOPASS") iar /login e
schelet — nu inspira incredere.
- **Depinde de**: US-001, US-002 (fonturi + scala)
- **Fisiere**: `app/web/templates/base.html` (header), `app/web/templates/login.html`,
`app/web/auth_routes.py` (`_base_ctx`/`login_get` — paseaza `account_name` cand e logat),
`app/web/routes.py` (context layout cu `account_name`), `DESIGN.md` (§Header & branding),
`tests/test_web_responsive.py` / un test de header (~5 fisiere)
- **Test intai (RED)**: `test_titlu_romfast_autopass`, `test_header_arata_nume_service_logat`,
`test_login_branded_nu_schelet`
- **Acceptance criteria**:
- [ ] Titlul din antet devine **"ROMFAST AUTOPASS"** (azi "Gateway RAR AUTOPASS",
`base.html:769`), pastrand badge-ul de mediu (test/prod) si link-ul la `/`.
- [ ] Cand userul e autentificat, sub titlu apare **numele service-ului auto** (din `accounts.name`,
ex. "Service auto: Service Auto Vâlcea SRL"), pasat in context (`account_name`). Pe paginile
neautentificate (login/signup) NU apare (nu exista cont logat).
- [ ] **Gate strict pe `is_authenticated` (NU pe /login/signup)**: dot-ul RAR online (US-003), numele
service-ului, badge-ul de plan/tier (US-010 + 5.17) si meniul burger se randeaza DOAR cand
`is_authenticated` e adevarat. Pe `/login` si `/signup` antetul e **minimal**: logo ROMFAST +
titlu "ROMFAST AUTOPASS" + selectorul de tema — atat. Fara RAR, fara nume service, fara tier,
fara meniu de cont (utilizatorul nu e logat). Referinta: `docs/mockups/prd-5.16-header-login-tema.html`
(blocurile "LOGAT" vs "/login NEAUTENTIFICAT").
- [ ] **/login profesional, nu schelet**: pagina capata un layout brandeit pe doua coloane (panou
stanga cu logo ROMFAST + "ROMFAST AUTOPASS" + tagline conformitate/criptare; dreapta formularul
de autentificare existent), folosind tokenii de fonturi/scala (US-001/002) si paleta temelor.
Functionalitatea formularului (`POST /login`, CSRF, link signup/parola) ramane neschimbata.
Referinta: `docs/mockups/prd-5.16-header-login-tema.html`. Degradeaza la o coloana pe mobil.
- [ ] `<title>` actualizat coerent ("ROMFAST AUTOPASS" in loc de "Gateway RAR AUTOPASS").
- [ ] **Tip cont (plan) in titlu**: langa titlu/badge-ul de mediu apare un **badge de plan**
(`Gratuit`/`Standard`/`Pro`/`Premium`), citit din `accounts.tier` (model definit in PRD 5.17;
in 5.16 e doar AFISARE — daca 5.17 nu e livrat inca, badge-ul citeste `tier` cu fallback
`Gratuit`). Referinta: `docs/mockups/prd-5.16-dashboard.html` / `...-mobil.html`.
- [ ] **Meniul burger — structura cu separatoare + plan + RAR**: meniul de cont primeste, in ordine:
(a) starea RAR online (US-003) + (b) linia "Plan: <tier> [· trial N zile]", apoi intrarile de
navigare **grupate cu separatoare (`<hr>`) intre sectiuni**: [Trimiteri · Mapari] | [Nomenclator]
| [Cont · Integrare · Jurnal] | [Iesi din cont]. Userul a cerut explicit separatoare clare intre
sectiuni. (Detaliile planului/trial vin din 5.17; in 5.16 = afisare.)
- [ ] `DESIGN.md` §Header & branding actualizat (titlu nou + nume service sub titlu + badge plan;
meniu burger cu separatoare).
- **Verificare E2E**: browser logat → antet "ROMFAST AUTOPASS" + nume service + badge plan; meniul
burger arata RAR + plan + sectiuni separate; `/login` brandeit pe desktop + o coloana pe 390px.
### US-011: Selector de tema stil landing (icon + eticheta temei curente)
**Ca** operator **vreau** ca selectorul de teme din aplicatie sa arate ca cel din landing **pentru ca**
azi e doar o iconita (☀) fara eticheta si nu stiu pe ce tema sunt.
- **Depinde de**: US-002 (acelasi val pe `base.html`)
- **Fisiere**: `app/web/templates/base.html` (butonul `#tema-toggle` + scriptul ciclic),
`DESIGN.md` (§Selector de tema), `tests/test_tema.py` (~3 fisiere)
- **Test intai (RED)**: `test_selector_tema_are_eticheta`, `test_eticheta_reflecta_tema_curenta`
- **Acceptance criteria**:
- [ ] Butonul de tema devine un **pill cu icon + eticheta** (numele temei curente vizibil, ex.
"Grafit"), ca pe landing (`landing.html:78-81`), nu doar iconita. Reuse structura `THEMES`
existenta (DRY E2 din 5.15) pentru a deriva eticheta — fara a doua sursa de adevar.
- [ ] Ciclarea prin toate temele (5.15) ramane; eticheta se actualizeaza la fiecare click; aria-label
si `#tema-live` (anunt screen-reader) reflecta tema curenta.
- [ ] Pe mobil (390px), daca spatiul e strans, eticheta se poate ascunde (ca pe landing,
`#theme-label{display:none}` la <=560px) pastrand iconita — degradare eleganta, fara overflow.
- [ ] Stil aliniat header-ului (US-010): bordura `--line`, hover `--accent`, fonturi system.
- [ ] `DESIGN.md` §Selector de tema actualizat (pill icon+eticheta, nu doar iconita).
- **Verificare E2E**: browser → butonul arata "Grafit"/"Hârtie" etc.; click cicleaza si eticheta se
schimba; pe 390px iconita ramane fara a sparge antetul.
### US-012: Landing — butonul "Autentificare" duce la /login
**Ca** vizitator **vreau** ca "Autentificare" sa ma duca la pagina reala de login **pentru ca** azi
deschide formularul de inregistrare din landing (tab login), nu pagina dedicata.
- **Depinde de**: US-010 (pagina /login brandeita e tinta)
- **Fisiere**: `app/web/templates/landing.html` (butonul `data-act="auth" data-tab="login"` +
scriptul de auth), `tests/test_web_*` (~2 fisiere)
- **Test intai (RED)**: `test_landing_autentificare_link_login`
- **Acceptance criteria**:
- [ ] Butonul "Autentificare" din header-ul landing (`landing.html:82`) devine un link/redirect real
catre **`/login`** (un `<a href="/login">` stilat ca butonul actual, sau handler care face
`location.href='/login'`), NU mai deschide modalul de auth din landing pe tab-ul login.
- [ ] "Creează cont" ramane neschimbat (deschide modalul de register din landing SAU duce la `/signup`
— pastram comportamentul actual; userul a cerut sa schimbam DOAR Autentificare).
- [ ] Daca dupa scoaterea tab-ului login modalul de auth ramane doar pe register, codul mort de
comutare login/register din landing se curata (fara a rupe register). Non-blocant daca modalul
ramane bi-tab; minim: butonul Autentificare nu mai deschide modalul.
- [ ] Link-ul "Autentifică-te" din interiorul modalului de register (`landing.html:370`) — decizie la
executie: poate ramane (tab intern) sau redirect la `/login`; implicit pastrat.
- **Verificare E2E**: click pe "Autentificare" in landing → navigheaza la `/login` (pagina brandeita
US-010), nu deschide modalul.
### US-013: Wizard import + preview/corectie aliniate la designul 5.16 (import colapsat)
**Ca** operator **vreau** ca paginile de import (cei 4 pasi + verificarea) sa arate la fel de
profesional ca restul aplicatiei **pentru ca** acolo petrec efectiv timpul, iar azi importul ocupa
spatiu pe Acasa chiar cand nu il folosesc.
- **Depinde de**: US-002 (scala/tokeni), US-004/005/006 (chips in editarea din preview)
- **Fisiere**: `app/web/templates/_stepper.html`, `app/web/templates/_upload.html`,
`app/web/templates/_mapcoloane.html`, `app/web/templates/_preview_import.html`,
`app/web/templates/_preview_rand.html`, `app/web/templates/_acasa.html` (import colapsat),
`tests/test_web_import_stepper.py` / `tests/test_web_responsive.py` (~6 fisiere)
- **Test intai (RED)**: `test_import_colapsat_implicit`, `test_wizard_foloseste_scala_tokeni`,
`test_preview_stari_pill_dot`
- **Acceptance criteria**:
- [ ] **Import colapsat pe Acasa**: zona de upload nu mai ocupa spatiu implicit — devine o bara slim
("+ Importa fisier (XLSX / CSV)") care se extinde la click (`<details>` nativ, fara JS custom),
ca lista de trimiteri sa fie primul lucru vizibil. Referinta: `docs/mockups/prd-5.16-dashboard.html`.
- [ ] **Cei 4 pasi** (`_stepper.html`, `_upload.html`, `_mapcoloane.html`, `_preview_import.html`)
consuma tokenii de fonturi/scala (US-001/002): stepper slim lizibil, tabel de mapare cu nume
coloana + exemplu + select "camp RAR", preview cu pill-uri de stare folosind **dot** (consistent
cu lista slim si stripul) — fara hex hardcodat, pe toate temele. Referinta:
`docs/mockups/prd-5.16-import-wizard.html`.
- [ ] **Editare/corectie rand in preview** foloseste ACELASI `_form_editare.html`/`_chips_prestatii.html`
ca modalul de detaliu — deci mostenesc automat fix-urile US-004 (denumiri in picker), US-005
(adaugare operatie), US-006 (salvare cod ales) si US-007 (Renunta inchide). Nu se duplica logica.
- [ ] Pill-urile de stare din preview pastreaza maparea din `labels.py` (`STARI_PREVIEW`:
"Cod RAR lipsa"/"Date incomplete"/"Verifica valori"/"Deja trimis"/"Duplicat in fisier") — zero
etichete noi; doar stilul (dot + scala) se aliniaza.
- [ ] Responsive: pe mobil stepperul ramane forma colapsata "Pasul N din 4" (5.13) si tabelele de
mapare/preview nu produc overflow orizontal.
- **Verificare E2E**: import xlsx → pas 1..4 in design 5.16; pe Acasa importul e colapsat implicit;
in preview, un rand needs_mapping editat cu picker (denumiri) + Renunta → comportament identic cu
modalul de detaliu.
### US-009: E2E browser final pe toate paginile + regresie
**Ca** dezvoltator **vreau** o trecere E2E completa **pentru ca** 5.16 atinge `base.html` (fierbinte)
si formularul de editare, si nu vreau regresii.
- **Depinde de**: US-002, US-003, US-006, US-007, US-008, US-010, US-011, US-012, US-013
- **Fisiere**: `tests/test_web_responsive.py`, `tests/test_tema.py`, `tests/test_web_corectie.py`
(~3 fisiere)
- **Test intai (RED)**: completare scenarii lipsa (scala pe toate componentele; bug-urile lock).
- **Acceptance criteria**:
- [ ] `python3 -m pytest -q -m "not live"` verde (fara regresii fata de baseline 5.15).
- [ ] E2E Playwright (sau headless screenshot ca fallback in sandbox) pe 390/1280, pe o tema
intunecata (grafit) + una luminoasa (hartie): Acasa (antet "ROMFAST AUTOPASS" + nume service,
selector tema cu eticheta, strip dot + contoare aerisite), Trimiteri (lista slim text mai mare),
editare (denumiri in picker, adaugare operatie extra, salvare cod ales, Renunta inchide),
wizard import (4 pasi + import colapsat pe Acasa + preview cu pill-uri dot + corectie rand),
`/login` brandeit, Mapari, Integrare, landing (Autentificare → /login) — fara overflow orizontal,
fonturi native (zero `/static/fonts/`), text uniform.
- [ ] Raport VERIFY documenteaza cele 4 bug-uri ca reproduse-inainte / reparate-dupa.
- **Verificare E2E**: rulare completa documentata in Raportul VERIFY.
## 4. Cerinte functionale (rezumat)
1. [REQ-001] Toate template-urile folosesc `var(--font-ui)`/`var(--font-mono)` (system stack); zero
`@font-face` si zero referinte `/static/fonts/`.
2. [REQ-002] Toate dimensiunile de text din componentele de design folosesc tokenii `--fs-*`.
3. [REQ-003] Indicatorul RAR online = dot colorat + text (nu bifa), accesibil fara culoare.
4. [REQ-004] Picker-ele de cod RAR arata cod + denumire peste tot.
5. [REQ-005] Se pot adauga operatii/coduri RAR suplimentare la editare, in modul operatii si plat.
6. [REQ-006] Codul ales in picker se aplica la salvare chiar si fara pasul intermediar "+".
7. [REQ-007] Butonul Renunta inchide modalul (click pe orice descendent).
8. [REQ-008] Antet branded "ROMFAST AUTOPASS" + nume service logat; `/login` profesional (nu schelet).
9. [REQ-009] Selector de tema stil landing (icon + eticheta temei curente).
10. [REQ-010] Butonul "Autentificare" din landing duce la `/login`.
## 5. Non-Goals (anti scope-creep)
- Fara redesign al layout-ului landing (doar fonturi); mockup-ul intern "Confirma Vin" ramane ilustrativ.
- Fara modificari pe backend-ul de trimitere (worker, masina de stari, idempotenta-logica `build_key`,
reconciliere, contract RAR). US-005/006 ating DOAR handler-ele de editare/`form-chips`, nu logica de
trimitere.
- Fara migrare de schema.
- Fara tipuri de cont / planuri / limite — sunt in PRD 5.17 (separat, la cererea userului).
- Fara stergerea fizica a fisierelor woff2 din `static/fonts/` (curatare optionala, follow-up).
- Fara a schimba cele 7 teme sau tokenii cromatici (5.15 ramane); 5.16 atinge doar tipografia.
## 6. Consideratii tehnice
- **Stack**: FastAPI + Jinja2 + HTMX, CSS pe variabile in `base.html` (fara build de CSS).
- **Patterns de urmat**: tokeni CSS sursa-unica (ca `THEMES`/`--card2`/`--line2` din 5.15);
server-driven HTMX (E6); `e.target.closest` pentru delegare de evenimente (deja folosit la
`data-modal-retry`, `base.html:1166`).
- **Fisier fierbinte**: `base.html` atins de US-001/002/003/007 → un singur autor pe val, serializat.
- **Riscuri**:
- System font stack arata diferit pe Windows/Mac/Linux — acceptat de user (tradeoff "no install").
Mitigare: scala in px (nu em-uri dependente de font) ramane uniforma indiferent de familie.
- Diacritice RO pe fonturi de sistem — toate stack-urile tinta le acopera; verificat in E2E.
- US-006 (save no-op) e un bug de cale dubla picker/chip — riscul e sa "reparam" doar JS-ul si sa
ramana rupt fara JS. Mitigare: fix pe calea server (citeste picker-ul la submit).
- `base.html` fierbinte + 7 teme = suprafata de test; recablarea pe `--fs-*` trebuie sa nu strice
tabelul desktop. Mitigare: US-002 AC de non-regresie + test ancorat pe sentinel.
## 7. Consideratii UI/UX
- Referinta vizuala obligatorie: `docs/mockups/prd-5.16-fonturi-system-stack.html`.
- Stari: strip verde (online) / rosu (blocat); picker gol vs cu denumiri; chip extra adaugabil/stergibil;
modal cu Renunta functional.
- Tinte 44px pe mobil pastrate; contrast AA in toate temele (text mai mare ajuta lizibilitatea).
## 8. Open Questions
- [ ] Valorile exacte ale scalei `--fs-*` (12/13.5/15/16/28…) — propuse in mockup; de validat la
executie pe ecran real, in limita "mai mare + uniform".
- [ ] Dot-ul RAR online: puls discret (animatie) DA/NU — implicit DA (ca "Live" din landing), dezactivabil
daca deranjeaza.
- [ ] Curatarea fizica a `static/fonts/*.woff2` — in 5.16 sau follow-up separat? (implicit: follow-up).
## 9. Valuri de executie
```
Val 1: [US-001] -> [US-002] -> [US-003] -> [US-010] -> [US-011] -> [US-007]
base.html: fonturi, scala, dot, antet branded,
selector tema, fix Renunta — SECVENTIAL (autor unic base.html) ||
[US-008] landing fonturi (dupa US-001 fixeaza stack-ul)
Val 2: [US-004] -> [US-005] -> [US-006] chips: denumiri, adaugare extra, save-no-op —
SECVENTIAL (acelasi _chips_prestatii.html + routes)
Val 3: [US-012] landing Autentificare → /login (dupa US-010 livreaza /login)
Val 4: [US-009] E2E final + regresie (dupa toate)
```
> Regula autor-unic: `base.html` (US-001/002/003/007/010/011) + `login.html`/`auth_routes.py` (US-010)
> si `routes.py`/`_chips_prestatii.html` (US-004/005/006) sunt fisiere fierbinti — serializate, nu
> paralele. US-010 atinge si `login.html` (separat), poate merge in paralel cu chips-urile din Val 2 daca
> autorul base.html e liber.
---
> Acest PRD nu a fost inca trecut prin `/plan-ceo-review` / `/plan-eng-review`. Recomandat inainte de
> executie (atinge `base.html` fierbinte + 4 bug-uri reale in calea de editare).

View File

@@ -0,0 +1,317 @@
# 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: <tier> [· 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).