Files
rar-autopass/tests/test_import_review.py
Claude Agent 8d4ff3400e feat(5.13): carduri compacte mobil/tableta + fix editare preview (OOB tr) + toast
Dogfood pe import + Trimiteri (mobil/tableta <1024px), pur CSS + markup, backend
trimitere neatins:

- Card compact real pentru .tabel-trimiteri (preview + Trimiteri): vehicul=titlu,
  stare=pill dreapta-sus, operatie+cod, meta data/km muted, nota mica. Inlocuieste
  stiva generica eticheta+valoare (carduri de ~450px -> ~135px). Anuleaza regula
  desktop tr.trimitere-row > td{padding:11px} in blocul compact.
- FIX editare preview: OOB swap pe <tr> esua tacit in htmx 1.9 (un <tr> brut se
  pierde la parsarea unui fragment fara context de tabel) -> randul ramanea cu
  starea veche dupa salvare. Inlocuit cu reload complet al preview-ului prin
  HX-Trigger:reincarcaPreview + detalii randSalvat. /editeaza si /confirma-review
  folosesc helper-ul _raspuns_rand_salvat.
- Feedback post-salvare: toast global "Randul N actualizat · <stare>" + scroll +
  flash pe randul actualizat (base.html window.arataToast + listener randSalvat).
- Modal editare: Salveaza + Anuleaza pe acelasi rand (sistem .act): desktop text,
  mobil doua iconite Lucide 44px alaturate (save/x). Macro icon('x') + .act-primary.
- Randuri deja-trimise/duplicate colapsate implicit in preview + toggle "Arata N".
- Select "Operatii de mapat" full-width pe mobil (nu mai iese din viewport).
- Bara de filtre Trimiteri adaptata mobil: pills pe banda cu scroll orizontal,
  cautare vehicul proeminenta (nu 8 butoane full-width stivuite).
- Nota preview = culoarea camp-fix (accent) ca sa atraga atentia; hint-urile
  camp-fix per-camp scoase (campul Note e self-explanatory).
- Confirmare trimitere: scos campul email (Declarant); text mai clar
  ("Confirma numarul din N gata de trimis"). Backend confirmed_by ramane optional.

Teste: contractul OOB (rupt in browser) inlocuit cu noul contract
(reincarcaPreview + randSalvat) in test_web_preview_edit / test_preview_edit_ui /
test_import_review. Suita: 992 passed (exclus live).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 23:34:33 +00:00

609 lines
23 KiB
Python

"""Teste US-007 (PRD 5.12): gate review in modal — import_rows.reviewed + confirmare.
TDD RED: testele sunt scrise inainte de implementare.
Scenarii:
1. Migrare: import_rows.reviewed INTEGER DEFAULT 0, idempotent.
2. Rand needs_review exclus din "gata de trimis" pana la confirmare explicita.
3. POST confirma-review seteaza reviewed=1 → randul devine ok la recalcul.
4. `reviewed` NU intra in payload/idempotency (marcaj separat).
5. Editarea unei valori pe un rand confirmat reseteaza reviewed=0 (D#9).
"""
from __future__ import annotations
import io
import os
import re
import tempfile
import pytest
from fastapi.testclient import TestClient
# --------------------------------------------------------------------------- #
# Fixtures #
# --------------------------------------------------------------------------- #
@pytest.fixture()
def client(monkeypatch):
"""Client fara autentificare web obligatorie (cont 1 implicit)."""
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "ir.db"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.crypto import reset_cache
reset_cache()
from app.main import app
with TestClient(app) as c:
yield c
get_settings.cache_clear()
reset_cache()
# --------------------------------------------------------------------------- #
# Helpere #
# --------------------------------------------------------------------------- #
def _seed_op1(account_id: int = 1) -> None:
"""Semeaza nomenclator + mapare OP-1 → R-FRANE."""
from app.db import get_connection
conn = get_connection()
try:
conn.execute(
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) "
"VALUES ('R-FRANE','Reparatie frane')"
)
conn.execute(
"INSERT OR IGNORE INTO operations_mapping "
"(account_id, cod_op_service, cod_prestatie, auto_send) "
"VALUES (?, 'OP-1', 'R-FRANE', 1)",
(account_id,),
)
conn.commit()
finally:
conn.close()
def _csv_bytes(rows: list[dict], sep: str = ";") -> bytes:
import csv as _csv
buf = io.StringIO()
writer = _csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=sep)
writer.writeheader()
writer.writerows(rows)
return buf.getvalue().encode("utf-8")
# Rand care declanseaza needs_review: data in format ambiguu (DD.MM.YYYY cu zi<=12)
# si format_data=None -> col_fmt="ambiguous" -> is_ambiguous_date=True
_ROWS_NEEDS_REVIEW = [
{
"VIN": "WVWZZZ1KZAW000123",
"Nr": "B001TST",
"Data": "05.06.2026", # Format ambiguu: zi=5 <= 12, luna=6 <= 12
"KM": "123456",
"Operatie": "OP-1",
},
]
_MAP_COLS = {
"VIN": "vin",
"Nr": "nr_inmatriculare",
"Data": "data_prestatie",
"KM": "odometru_final",
"Operatie": "operatie",
}
def _get_csrf(client: TestClient) -> str:
r = client.get("/")
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', r.text) or \
re.search(r'value="([^"]+)"\s+name="csrf_token"', r.text)
return m.group(1) if m else ""
def _upload_and_preview_needs_review(client: TestClient) -> int:
"""Upload CSV cu data ambigua + salveaza mapare fara format_data → preview.
format_data=None → col_fmt='ambiguous' → data '05.06.2026' → is_ambiguous=True
→ flag needs_review in _resolve_row_for_preview.
Intoarce import_id.
"""
rows = _ROWS_NEEDS_REVIEW
csv_data = _csv_bytes(rows)
csrf = _get_csrf(client)
r = client.post(
"/_import/upload",
files={"file": ("test.csv", io.BytesIO(csv_data), "text/csv")},
data={"csrf_token": csrf},
)
assert r.status_code == 200, r.text
m = re.search(r"/_import/(\d+)/mapare-coloane", r.text)
assert m, f"import_id negasit in raspuns: {r.text[:300]}"
iid = int(m.group(1))
colnames = list(rows[0].keys())
canons = [_MAP_COLS[c] for c in colnames]
csrf2 = _get_csrf(client)
# IMPORTANT: format_data="" (gol/None) → date_col_format={} → col_fmt="ambiguous"
r2 = client.post(f"/_import/{iid}/mapare-coloane", data={
"colname": colnames,
"canon": canons,
"format_data": "", # fara format -> ambiguous
"csrf_token": csrf2,
})
assert r2.status_code == 200, r2.text
return iid
def _get_reviewed(import_id: int, row_index: int) -> int:
"""Citeste valoarea reviewed din DB pentru un rand."""
from app.db import get_connection
conn = get_connection()
try:
row = conn.execute(
"SELECT reviewed FROM import_rows WHERE batch_id=? AND row_index=?",
(import_id, row_index),
).fetchone()
return int(row["reviewed"]) if row else -1
finally:
conn.close()
def _get_idempotency_key(import_id: int, row_index: int) -> str | None:
"""Citeste cheia de idempotenta a unui rand prin endpoint-ul de preview."""
from app.db import get_connection
from app.crypto import decrypt_creds
from app.api.v1.import_router import (
_resolve_row_for_preview, _signature, _build_idempotency_key,
)
import json
from app.mapping import account_or_default, load_mapping_meta, load_nomenclator_codes, load_text_rules
from app.import_parse import parse_date_value
conn = get_connection()
try:
acct = account_or_default(1)
row_db = conn.execute(
"SELECT raw_json, override_json, reviewed FROM import_rows "
"WHERE batch_id=? AND row_index=?",
(import_id, row_index),
).fetchone()
if not row_db:
return None
raw = decrypt_creds(row_db["raw_json"]) or {}
ov = decrypt_creds(row_db["override_json"]) if row_db["override_json"] else None
col_names = list(raw.keys())
sig = _signature(col_names)
mapping_row = conn.execute(
"SELECT json_mapare, format_data FROM column_mappings "
"WHERE account_id=? AND signature_coloane=?",
(acct, sig),
).fetchone()
if not mapping_row:
return None
json_mapare = json.loads(mapping_row["json_mapare"])
format_data = mapping_row["format_data"]
date_col_format: dict[str, str] = {}
if format_data:
for col_f, camp_c in json_mapare.items():
if camp_c == "data_prestatie":
date_col_format[col_f] = format_data
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
info = _resolve_row_for_preview(
raw_row=raw,
json_mapare=json_mapare,
date_col_format=date_col_format,
coercion_flags=[],
mapping=mapping,
mapping_meta=mapping_meta,
formula_columns=[],
override=ov or None,
valid_codes=valid_codes,
text_rules=text_rules,
reviewed=bool(row_db["reviewed"]),
)
try:
return _build_idempotency_key(1, info["resolved"])
except Exception:
return None
finally:
conn.close()
# --------------------------------------------------------------------------- #
# Teste #
# --------------------------------------------------------------------------- #
def test_migrare_adauga_coloana_reviewed_idempotent():
"""_migrate adauga coloana import_rows.reviewed INTEGER DEFAULT 0.
Idempotent: a doua invocare nu ridica eroare.
"""
tmp = tempfile.mkdtemp()
import os
os.environ["AUTOPASS_DB_PATH"] = os.path.join(tmp, "m.db")
from app.config import get_settings
get_settings.cache_clear()
from app.crypto import reset_cache
reset_cache()
try:
from app.db import get_connection, _migrate
# Initializare DB (inclusiv _migrate)
from app.db import init_db
init_db()
conn = get_connection()
try:
cols = {r["name"] for r in conn.execute("PRAGMA table_info(import_rows)").fetchall()}
assert "reviewed" in cols, \
"import_rows trebuie sa aiba coloana 'reviewed' dupa init_db()"
# Verifica DEFAULT 0
row_info = {r["name"]: r for r in conn.execute("PRAGMA table_info(import_rows)").fetchall()}
reviewed_col = row_info.get("reviewed")
assert reviewed_col is not None
# dflt_value poate fi "0" sau 0 in functie de SQLite versiune
assert str(reviewed_col["dflt_value"]) == "0", \
f"import_rows.reviewed trebuie sa aiba DEFAULT 0, got: {reviewed_col['dflt_value']}"
# Idempotenta: a doua invocare a _migrate nu ridica eroare
_migrate(conn) # no exception
finally:
conn.close()
finally:
del os.environ["AUTOPASS_DB_PATH"]
get_settings.cache_clear()
reset_cache()
def test_needs_review_exclus_din_gata_pana_la_confirmare(client):
"""Un rand needs_review nu intra in 'gata de trimis' (n_confirmat = 0) pana la confirmare.
US-007 Q1: randul cu data ambigua apare cu pill 'Verifica valori' si este
EXCLUS din n_confirmat. Bannerul de discoverability (T1) trebuie sa fie prezent.
"""
_seed_op1()
iid = _upload_and_preview_needs_review(client)
# Verifica preview via GET /_import/{id}/preview
r = client.get(f"/_import/{iid}/preview")
assert r.status_code == 200, r.text
html = r.text
# Randul are starea needs_review
assert "needs_review" in html, \
"Randul cu data ambigua trebuie sa apara cu starea needs_review in preview"
# Pill-ul cu eticheta "Verifica valori"
assert "Verifica valori" in html or "verifica valori" in html.lower(), \
"Pill-ul 'Verifica valori' trebuie sa apara pentru randul needs_review"
# n_confirmat = 0 (randul NU e in ok)
# Cautam valoarea campului n_confirmat (value="0")
n_match = re.search(r'id="n-confirmat"[^>]*value="(\d+)"', html) or \
re.search(r'name="n_confirmat"[^>]*value="(\d+)"', html)
if n_match:
n_val = int(n_match.group(1))
assert n_val == 0, \
f"n_confirmat trebuie sa fie 0 cand randul e needs_review (negasit), got {n_val}"
# Bannerul de discoverability (T1) — prezent cand summary.needs_review > 0
# Trebuie sa contina un mesaj despre faptul ca randurile nu pleaca pana la confirmare
# (ex. 'nu pleaca la RAR' sau 'confirmi in modal' sau 'Verifica valori')
banner_present = (
"nu pleaca" in html.lower() or
"confirmi in modal" in html.lower() or
"preview-needs-review-banner" in html
)
assert banner_present, \
"Bannerul de discoverability (T1) trebuie sa fie prezent cand exista randuri needs_review"
def test_confirmare_in_modal_seteaza_reviewed_si_devine_ok(client):
"""POST /_import/{id}/rand/0/confirma-review seteaza reviewed=1.
US-007 T2: operatorul apasa 'Confirma valorile' in modal →
reviewed=1 in DB → randul devine ok la recalcul preview.
Verifica:
- Raspuns 200
- reviewed=1 in DB
- HX-Trigger: randSalvat cu noua stare 'Gata de trimis' (pentru toast)
- HX-Trigger: reincarcaPreview + HX-Trigger-After-Settle: inchideModal
"""
import json as _json
_seed_op1()
iid = _upload_and_preview_needs_review(client)
# Verifica ca randul e needs_review inainte de confirmare
assert _get_reviewed(iid, 0) == 0, "reviewed trebuie sa fie 0 inainte de confirmare"
# POST confirma-review
csrf = _get_csrf(client)
r = client.post(f"/_import/{iid}/rand/0/confirma-review", data={"csrf_token": csrf})
assert r.status_code == 200, r.text
# reviewed=1 in DB
assert _get_reviewed(iid, 0) == 1, \
"reviewed trebuie sa fie 1 in DB dupa confirmare"
# Contractul nou: reload preview + randSalvat cu noua stare (nu OOB pe <tr>).
trig = _json.loads(r.headers.get("HX-Trigger", "{}"))
assert trig.get("reincarcaPreview") is True, "confirma-review trebuie sa ceara reincarcaPreview"
assert trig.get("randSalvat", {}).get("stare") == "Gata de trimis", \
"Dupa confirmare, randSalvat.stare trebuie sa fie 'Gata de trimis' (pentru toast)"
# Modal se inchide
trigger = r.headers.get("HX-Trigger-After-Settle", "")
assert "inchideModal" in trigger, \
f"confirma-review trebuie sa emita inchideModal, got: '{trigger}'"
def test_reviewed_nu_intra_in_payload_sau_idempotency(client):
"""reviewed NU intra in payload, override_json sau cheia de idempotenta.
US-007 marcaj separat (D#8): reviewed e DOAR un flag de confirmare umana,
nu un camp de continut. Cheia de idempotenta trebuie sa fie identica inainte
si dupa confirmare.
"""
_seed_op1()
iid = _upload_and_preview_needs_review(client)
# Cheia inainte de confirmare
key_before = _get_idempotency_key(iid, 0)
assert key_before is not None, "Cheia de idempotenta trebuie sa existe inainte de confirmare"
# Confirma
csrf = _get_csrf(client)
r = client.post(f"/_import/{iid}/rand/0/confirma-review", data={"csrf_token": csrf})
assert r.status_code == 200, r.text
# Cheia dupa confirmare
key_after = _get_idempotency_key(iid, 0)
assert key_after is not None, "Cheia de idempotenta trebuie sa existe dupa confirmare"
assert key_before == key_after, \
f"Cheia de idempotenta NU trebuie sa se schimbe la confirmare! " \
f"Inainte: {key_before}, dupa: {key_after}. " \
"'reviewed' NU trebuie sa intre in payload sau cheia de idempotenta."
# Verifica ca 'reviewed' nu e in override_json
from app.db import get_connection
from app.crypto import decrypt_creds
conn = get_connection()
try:
row = conn.execute(
"SELECT override_json FROM import_rows WHERE batch_id=? AND row_index=?",
(iid, 0),
).fetchone()
if row and row["override_json"]:
ov = decrypt_creds(row["override_json"]) or {}
assert "reviewed" not in ov, \
"'reviewed' NU trebuie sa fie in override_json (camp separat, nu camp de continut)"
finally:
conn.close()
def test_editare_valoare_pe_needs_review_reseteaza_reviewed(client):
"""Editarea unei valori pe un rand confirmat reseteaza reviewed=0 (D#9).
Cand operatorul SCHIMBA o valoare (via POST editeaza) pe un rand deja confirmat
(reviewed=1), apply_row_override trebuie sa reseteze reviewed=0.
→ Randul se intoarce in starea needs_review si cere re-confirmare.
"""
_seed_op1()
iid = _upload_and_preview_needs_review(client)
# Confirma randul
csrf = _get_csrf(client)
r = client.post(f"/_import/{iid}/rand/0/confirma-review", data={"csrf_token": csrf})
assert r.status_code == 200, r.text
assert _get_reviewed(iid, 0) == 1, "reviewed trebuie sa fie 1 dupa confirmare"
# Editeaza o valoare (odometru_final)
csrf2 = _get_csrf(client)
r2 = client.post(f"/_import/{iid}/rand/0/editeaza", data={
"odometru_final": "200000",
"csrf_token": csrf2,
})
assert r2.status_code == 200, r2.text
# reviewed trebuie sa fie 0 (resetat de apply_row_override)
assert _get_reviewed(iid, 0) == 0, \
"reviewed trebuie sa fie resetat la 0 dupa editarea unei valori (D#9 — re-cere confirmare)"
def test_confirma_review_guard_committed_409(client):
"""POST confirma-review pe batch deja comis → 409 (guard committed)."""
_seed_op1()
iid = _upload_and_preview_needs_review(client)
# Marcheaza batch ca committed
from app.db import get_connection
conn = get_connection()
try:
conn.execute("UPDATE import_batches SET status='committed' WHERE id=?", (iid,))
conn.commit()
finally:
conn.close()
csrf = _get_csrf(client)
r = client.post(f"/_import/{iid}/rand/0/confirma-review", data={"csrf_token": csrf})
assert r.status_code == 409, \
f"confirma-review pe batch committed trebuie sa returneze 409, got {r.status_code}"
def test_confirma_review_scoped_404_alt_cont():
"""POST confirma-review pe un rand al altui cont → 404 (scoping JOIN)."""
tmp = tempfile.mkdtemp()
env_patch = {
"AUTOPASS_DB_PATH": os.path.join(tmp, "scope.db"),
"AUTOPASS_WEB_AUTH_REQUIRED": "true",
}
for k, v in env_patch.items():
os.environ[k] = v
from app.config import get_settings
get_settings.cache_clear()
from app.crypto import reset_cache
reset_cache()
from app.web import ratelimit
ratelimit._hits.clear()
from app.main import app
try:
with TestClient(app, follow_redirects=False) as c:
from app.db import get_connection
from app.accounts import create_account
from app.users import create_user
conn = get_connection()
try:
acct1 = create_account(conn, "Firma A", active=True)
create_user(conn, acct1, "userA@test.com", "parola123secure")
acct2 = create_account(conn, "Firma B", active=True)
create_user(conn, acct2, "userB@test.com", "parola123secure")
finally:
conn.close()
# Semeaza operatii pentru acct1
from app.db import get_connection as gcn
conn2 = gcn()
try:
conn2.execute(
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) "
"VALUES ('R-FRANE','Reparatie frane')"
)
conn2.execute(
"INSERT OR IGNORE INTO operations_mapping "
"(account_id, cod_op_service, cod_prestatie, auto_send) "
"VALUES (?, 'OP-1', 'R-FRANE', 1)", (acct1,)
)
conn2.commit()
finally:
conn2.close()
# Login ca userA, creeaza batch
def _login(client, email, pwd="parola123secure"):
resp = client.get("/login")
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \
re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
assert m
client.post("/login", data={
"email": email, "parola": pwd, "csrf_token": m.group(1),
})
_login(c, "userA@test.com")
rows = _ROWS_NEEDS_REVIEW
csv_data = _csv_bytes(rows)
def _csrf():
r = c.get("/")
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', r.text) or \
re.search(r'value="([^"]+)"\s+name="csrf_token"', r.text)
return m.group(1) if m else ""
csrf = _csrf()
r = c.post(
"/_import/upload",
files={"file": ("test.csv", io.BytesIO(csv_data), "text/csv")},
data={"csrf_token": csrf},
)
assert r.status_code == 200
m = re.search(r"/_import/(\d+)/mapare-coloane", r.text)
assert m
iid = int(m.group(1))
colnames = list(rows[0].keys())
canons = [_MAP_COLS[c] for c in colnames]
csrf2 = _csrf()
c.post(f"/_import/{iid}/mapare-coloane", data={
"colname": colnames, "canon": canons,
"format_data": "", "csrf_token": csrf2,
})
# Login ca userB si incearca confirma-review pe batch-ul lui A
_login(c, "userB@test.com")
csrf3 = _csrf()
r2 = c.post(f"/_import/{iid}/rand/0/confirma-review", data={"csrf_token": csrf3})
assert r2.status_code == 404, \
f"confirma-review cross-account trebuie sa returneze 404, got {r2.status_code}"
finally:
for k in env_patch:
if k in os.environ:
del os.environ[k]
ratelimit._hits.clear()
get_settings.cache_clear()
reset_cache()
# --------------------------------------------------------------------------- #
# Teste markup Bug 1: confirma-review form swap (B1) #
# --------------------------------------------------------------------------- #
def test_confirma_review_form_nu_foloseste_hx_swap_none():
"""Bug B1 (markup): formularul confirma-review NU trebuie sa foloseasca
hx-swap='none' — cu none, htmx aplica doar OOB-urile dar NU executa scriptul
din continutul principal → updateN() nu ruleaza → n_confirmat stale → 422.
Forma corecta: hx-target='#detaliu-modal-body' + hx-swap='innerHTML'.
"""
import pathlib
template = (
pathlib.Path(__file__).parent.parent
/ "app/web/templates/_editare_preview_modal.html"
)
html = template.read_text(encoding="utf-8")
# Gasim sectiunea formularului confirma-review (dupa marcajul T2)
idx = html.find("confirma-review")
assert idx >= 0, "Formularul confirma-review nu a fost gasit in template"
form_section = html[idx:]
# NU trebuie sa existe hx-swap="none" pe formularul confirma-review
assert 'hx-swap="none"' not in form_section, (
"Formularul confirma-review NU trebuie sa foloseasca hx-swap='none'. "
"Cu none, scriptul updateN() nu ruleaza → n_confirmat stale → gate 422."
)
# TREBUIE sa tinteasca #detaliu-modal-body cu innerHTML
assert 'hx-target="#detaliu-modal-body"' in form_section, (
"Formularul confirma-review trebuie sa aiba hx-target='#detaliu-modal-body' "
"ca scriptul updateN sa fie executat (identic cu formularul editeaza)."
)
assert 'hx-swap="innerHTML"' in form_section, (
"Formularul confirma-review trebuie sa aiba hx-swap='innerHTML' "
"ca scriptul updateN sa fie executat."
)
def test_confirma_review_cere_reincarcarea_preview(client):
"""Contractul nou (dogfood 5.13): confirma-review NU mai depinde de scriptul updateN
din payload (care, cu OOB pe <tr> rupt, lasa randul stale). Acum cere reincarcaPreview,
iar preview-ul reincarcat re-randeaza contorul si butonul de confirmare cu n_confirmat
corect server-side — deci problema B1 (n_confirmat stale -> 422) dispare structural.
Verifica:
- Raspuns 200
- HX-Trigger contine reincarcaPreview (reincarca contorul/confirmarea, fresh)
"""
import json as _json
_seed_op1()
iid = _upload_and_preview_needs_review(client)
csrf = _get_csrf(client)
r = client.post(f"/_import/{iid}/rand/0/confirma-review", data={"csrf_token": csrf})
assert r.status_code == 200, r.text
trig = _json.loads(r.headers.get("HX-Trigger", "{}"))
assert trig.get("reincarcaPreview") is True, (
"confirma-review trebuie sa ceara reincarcaPreview — preview-ul reincarcat aduce "
"n_confirmat corect server-side (fara dependenta de scriptul updateN din payload)."
)