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:
Claude Agent
2026-06-29 06:01:05 +00:00
parent 9eccb9f6fa
commit c9f9a1ca0e
37 changed files with 3433 additions and 449 deletions

View File

@@ -544,3 +544,316 @@ def test_repune_select_afiseaza_denumirea(client):
assert "AAA — Schimb ulei motor" in html, (
f"Optiunea select nu randeaza 'cod — denumire': {html[html.find('AAA'):html.find('AAA')+60]}"
)
# ============================================================================= #
# Teste noi 5.16: US-004 (denumiri picker), US-005 (add_extra), #
# US-006 (save picker fara buton), T-E3 (by-index), T-D1/T-E5, T-C1/T-E4 #
# ============================================================================= #
def test_picker_flat_arata_cod_si_denumire(client):
"""US-004 (5.16): picker plat afiseaza 'cod — denumire', nu doar codul.
RED: _chips_prestatii.html:147 afiseaza doar {{ n.cod_prestatie }};
modul operatii (:101) afiseaza deja 'cod — nume'. Fix: uniformizare.
"""
acct = _create_account_user("picker.flat.denu@test.com")
_login(client, "picker.flat.denu@test.com")
_seed_cod("FRN1", "Sistem de franare")
# Submission flat: fara cod_op_service (mod plat)
sid = _insert(acct, status="needs_mapping", payload={
"vin": "WVWZZZ1JZXW0US4001",
"nr_inmatriculare": "B100AAA",
"data_prestatie": "2026-06-10",
"odometru_final": "50000",
"prestatii": [], # mod plat: fara operatii cu cod_op_service
})
resp = client.get(f"/_fragments/trimitere/{sid}")
assert resp.status_code == 200
# Optiunea trebuie sa arate 'FRN1 — Sistem de franare', nu doar 'FRN1'
assert "FRN1 — Sistem de franare" in resp.text, (
f"Picker plat nu arata denumirea: "
f"{resp.text[resp.text.find('FRN1'):resp.text.find('FRN1')+80] if 'FRN1' in resp.text else 'FRN1 absent'}"
)
def test_adauga_cod_extra_in_mod_operatii(client):
"""US-005 (5.16): in mod operatii, actiunea add_extra adauga un cod RAR liber.
RED: post_form_chips nu are actiunea 'add_extra' -> chips_action ignorata.
"""
acct = _create_account_user("add.extra.ops@test.com")
_login(client, "add.extra.ops@test.com")
_seed_cod("OE-1", "Schimb ulei motor")
_seed_cod("FRN1", "Sistem de franare")
csrf = _csrf(client)
# Chips stare: 1 operatie deja mapata (mod ops) → _has_ops = True
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1"], # chip existent (op mapata)
"chip_op_service": ["SchimbUlei"],
"chip_denumire": ["Schimb ulei motor"],
"chips_action": "add_extra",
"chips_add_cod_flat": "FRN1", # codul extra de adaugat
},
)
assert resp.status_code == 200, resp.text[:300]
# FRN1 trebuie sa apara in raspuns (chip extra adaugat)
assert "FRN1" in resp.text, (
f"Codul extra FRN1 nu a fost adaugat in mod operatii: {resp.text[:300]}"
)
# OE-1 trebuie sa ramana (chip original neatins)
assert "OE-1" in resp.text, f"Chip original OE-1 disparut: {resp.text[:300]}"
def test_extra_cod_persistat_la_salvare(client):
"""US-005 (5.16): codul extra adaugat via form-chips e salvat la /corecteaza.
Simulam starea form dupa add_extra: hidden inputs pentru op mapata (OE-1)
+ hidden inputs pentru chip extra flat (FRN1, fara op_service).
"""
acct = _create_account_user("extra.persist@test.com")
_login(client, "extra.persist@test.com")
_seed_cod("OE-1", "Schimb ulei")
_seed_cod("FRN1", "Sistem de franare")
sid = _insert(acct, status="needs_mapping", payload=_payload_cu_ops(
"WVWZZZ1JZXW0XP001",
[("SchimbUlei", "Schimb ulei motor")],
))
csrf = _csrf(client)
# Form state dupa add_extra: op mapata (idx=0, OE-1) + chip extra flat (idx=1, FRN1)
resp = client.post(
f"/trimitere/{sid}/corecteaza",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1", "FRN1"], # OE-1 pt op, FRN1 chip extra
"chip_op_service": ["SchimbUlei", ""], # idx 0 are op_service, idx 1 nu
"chip_denumire": ["Schimb ulei motor", ""],
},
)
assert resp.status_code == 200, resp.text[:300]
r = _row(sid)
assert r["status"] == "queued", f"status asteptat queued, got {r['status']}"
prestatii = _payload_json(sid)["prestatii"]
coduri = [p.get("cod_prestatie") for p in prestatii]
assert "OE-1" in coduri, f"OE-1 (op mapata) lipsa: {prestatii}"
assert "FRN1" in coduri, f"FRN1 (chip extra) lipsa: {prestatii}"
def test_extra_cod_validat_nomenclator(client):
"""US-005 (5.16): add_extra respinge cod necunoscut in nomenclator (invariant ORA-12899).
RED: actiunea add_extra nu exista; dupa fix, cod invalid nu se adauga.
"""
acct = _create_account_user("extra.valid@test.com")
_login(client, "extra.valid@test.com")
_seed_cod("OE-1", "Schimb ulei")
csrf = _csrf(client)
# add_extra cu cod INVALID (XX-99 nu e in nomenclator)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1"],
"chip_op_service": ["SchimbUlei"],
"chip_denumire": ["Schimb ulei"],
"chips_action": "add_extra",
"chips_add_cod_flat": "XX-99", # cod necunoscut
},
)
assert resp.status_code == 200
html = resp.text
# XX-99 NU trebuie sa apara ca chip valid (hidden input cu valoarea XX-99)
import re as _re
hidden_xx99 = _re.search(r'<input[^>]+name="cod_prestatie"[^>]+value="XX-99"', html)
assert hidden_xx99 is None, (
f"Codul invalid XX-99 a fost adaugat ca chip! HTML: {html[:500]}"
)
def test_cod_ales_in_picker_se_salveaza_fara_buton_add(client):
"""US-006 (5.16): codul ales in picker flat se aplica la /corecteaza fara a apasa '+'.
RED: post_corectie_trimitere citeste form.getlist('cod_prestatie') (hidden inputs)
dar ignora 'chips_add_cod_flat' (picker neselectat ca chip) → submission ramane
needs_mapping desi codul e ales.
"""
acct = _create_account_user("picker.save.nobutton@test.com")
_login(client, "picker.save.nobutton@test.com")
_seed_cod("OE-1", "Schimb ulei motor")
# Submission flat fara prestatii
sid = _insert(acct, status="needs_mapping", payload={
"vin": "WVWZZZ1JZXW0PS001",
"nr_inmatriculare": "B100AAA",
"data_prestatie": "2026-06-10",
"odometru_final": "50000",
"prestatii": [],
})
csrf = _csrf(client)
# Browser trimite chips_add_cod_flat=OE-1 (ales in picker) dar FARA hidden cod_prestatie
# (userul nu a apasat '+' sa promoveze selectia intr-un chip).
resp = client.post(
f"/trimitere/{sid}/corecteaza",
data={
"csrf_token": csrf,
"chips_add_cod_flat": "OE-1", # ales in picker, ne-aprobat prin '+'
# NU exista 'cod_prestatie' in form (zero hidden chips)
},
)
assert resp.status_code == 200, resp.text[:300]
r = _row(sid)
assert r["status"] == "queued", (
f"Codul ales in picker trebuia sa se aplice la salvare fara '+': status={r['status']}"
)
prestatii = _payload_json(sid)["prestatii"]
coduri = [p.get("cod_prestatie") for p in prestatii]
assert "OE-1" in coduri, f"OE-1 (ales in picker) lipsa din prestatii: {prestatii}"
def test_salvare_fara_chip_explicit_nu_e_no_op(client):
"""US-006 (5.16): o trimitere needs_mapping cu cod ales in picker nu ramane no-op.
Complementar cu test_cod_ales_in_picker_se_salveaza_fara_buton_add: verifica
explicit ca statusul se schimba (nu ramane needs_mapping).
"""
acct = _create_account_user("noop.previne@test.com")
_login(client, "noop.previne@test.com")
_seed_cod("FRN1", "Sistem de franare")
sid = _insert(acct, status="needs_mapping", payload={
"vin": "WVWZZZ1JZXW0NP001",
"nr_inmatriculare": "B100AAA",
"data_prestatie": "2026-06-10",
"odometru_final": "50000",
"prestatii": [],
})
old_status = _row(sid)["status"]
assert old_status == "needs_mapping"
csrf = _csrf(client)
resp = client.post(
f"/trimitere/{sid}/corecteaza",
data={
"csrf_token": csrf,
"chips_add_cod_flat": "FRN1",
},
)
assert resp.status_code == 200
new_status = _row(sid)["status"]
assert new_status != "needs_mapping", (
f"Salvarea cu cod ales in picker trebuia sa nu fie no-op: status ramas {new_status}"
)
assert new_status == "queued", f"status asteptat queued, got {new_status}"
def test_picker_by_index_op2_nu_op1(client):
"""T-E3 (5.16): codul ales pe picker-ul op#2 aterizeaza pe op#2, NU pe op#1.
Verifica alinierea by-index in modul operatii: chips_add_op_index=1 + chips_add_cod_1
actualizeaza chips[1] (op#2), nu chips[0] (op#1).
"""
acct = _create_account_user("byindex.op2@test.com")
_login(client, "byindex.op2@test.com")
_seed_cod("OE-1", "Schimb ulei")
_seed_cod("FRN1", "Sistem de franare")
csrf = _csrf(client)
# Chips: op#1 (idx=0) deja mapata cu OE-1, op#2 (idx=1) nemapata (cod gol)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1", ""], # idx 0=OE-1 (mapata), idx 1="" (nemapata)
"chip_op_service": ["Op-A", "Op-B"],
"chip_denumire": ["Prima", "A doua"],
"chips_action": "add",
"chips_add_op_index": "1", # adauga pe op#2 (idx=1)
"chips_add_cod_1": "FRN1", # picker-ul op#2 contine FRN1
},
)
assert resp.status_code == 200, resp.text[:300]
html = resp.text
import re as _re
hidden_vals = _re.findall(r'<input[^>]+name="cod_prestatie"[^>]+value="([^"]*)"', html)
assert "OE-1" in hidden_vals, f"OE-1 (op#1) a disparut dupa adaugare pe op#2: {hidden_vals}"
assert "FRN1" in hidden_vals, f"FRN1 nu a aterizat pe op#2: {hidden_vals}"
# By-index: OE-1 trebuie sa fie INAINTE de FRN1 (idx 0 < idx 1)
oe1_pos = hidden_vals.index("OE-1") if "OE-1" in hidden_vals else -1
frn1_pos = hidden_vals.index("FRN1") if "FRN1" in hidden_vals else -1
assert oe1_pos < frn1_pos, (
f"FRN1 (op#2, idx=1) trebuie dupa OE-1 (op#1, idx=0) by-index: {hidden_vals}"
)
def test_empty_state_picker_nomenclator_gol(client):
"""T-D1/T-E5 (5.16): empty-state vizibil cand nomenclatorul e gol.
RED: {% if nomenclator_rar %} fara {% else %} -> silentios; un rand needs_mapping
fara nomenclator nu are nicio cale de a adauga cod (nereparabil silentios).
GREEN: div.chips-nom-gol vizibil.
"""
acct = _create_account_user("empty.nom@test.com")
_login(client, "empty.nom@test.com")
# Golim nomenclatorul: seed_nomenclator_if_empty populeaza la initializare DB;
# testul simuleaza cazul extrem cand tabla e goala (post-update, inainte de re-seed).
from app.db import get_connection as _gconn
_c = _gconn()
_c.execute("DELETE FROM nomenclator_rar")
_c.commit()
_c.close()
sid = _insert(acct, status="needs_mapping", payload={
"vin": "WVWZZZ1JZXW0EN001",
"nr_inmatriculare": "B100AAA",
"data_prestatie": "2026-06-10",
"odometru_final": "50000",
"prestatii": [],
})
resp = client.get(f"/_fragments/trimitere/{sid}")
assert resp.status_code == 200
assert "chips-nom-gol" in resp.text, (
f"Empty state 'chips-nom-gol' lipsa cand nomenclatorul e gol: {resp.text[resp.text.find('chips'):resp.text.find('chips')+200] if 'chips' in resp.text else resp.text[:500]}"
)
def test_add_extra_semnal_vizibil_cod_invalid(client):
"""T-C1/T-E4 (5.16): add_extra cu cod invalid da semnal vizibil (nu esua silentios).
RED: actiunea add_extra nu exista → nu exista niciun semnal.
GREEN: div.chips-extra-error vizibil cand codul e invalid sau selectul e gol.
"""
acct = _create_account_user("extra.err.signal@test.com")
_login(client, "extra.err.signal@test.com")
_seed_cod("OE-1", "Schimb ulei")
csrf = _csrf(client)
# add_extra cu cod necunoscut in nomenclator
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1"],
"chip_op_service": ["SchimbUlei"],
"chip_denumire": ["Schimb ulei"],
"chips_action": "add_extra",
"chips_add_cod_flat": "XX-99", # cod inexistent
},
)
assert resp.status_code == 200
assert "chips-extra-error" in resp.text, (
f"Semnalul 'chips-extra-error' lipsa pentru cod invalid: {resp.text[:300]}"
)