feat(5.8): reguli mapare pe text (substring/cont) + UX tabel trimiteri (detaliu inline, fara scroll, cod RAR)

Reguli text per cont (operation_text_rules), resolve_prestatii cu param aditiv
text_rules + precedenta stricta, threadat pe toate cele 6 callsite-uri + valid_codes
+ seam classify_prezentare. UI Mapari: sectiune reguli + preview pre-salvare + overlap
+ telemetrie text_rule_hit. UX tabel: cod_rar sub operatie, pill eticheta scurta, fara
scroll orizontal (scopat .tabel-trimiteri + carduri <768px), detaliu inline expandabil
(a11y + pauza poll). code-review: reparat regula auto_send=0 care trimitea automat la RAR
in loc sa tina randul pentru review. 814 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-24 12:47:37 +00:00
parent c80c79462c
commit 51dc504f1d
28 changed files with 3023 additions and 61 deletions

View File

@@ -44,9 +44,12 @@ from ...import_parse import (
parse_file,
)
from ...mapping import (
_emite_text_rule_hits,
account_or_default,
has_no_auto_send,
load_mapping_meta,
load_nomenclator_codes,
load_text_rules,
normalize_for_match,
resolve_prestatii,
)
@@ -126,6 +129,8 @@ def _resolve_row_for_preview(
mapping_meta: dict[str, dict],
formula_columns: list[str],
override: dict[str, Any] | None = None,
valid_codes: set[str] | None = None,
text_rules: list[dict] | None = None,
) -> dict[str, Any]:
"""Rezolva un rand din import pentru preview: aplica mapare coloane + validare.
@@ -201,7 +206,7 @@ def _resolve_row_for_preview(
# Rezolvare prestatii
prestatii = mapped.get("prestatii") or []
resolved, unmapped = resolve_prestatii(prestatii, mapping)
resolved, unmapped = resolve_prestatii(prestatii, mapping, valid_codes, text_rules)
mapped["prestatii"] = resolved
# Determinare stare
@@ -745,6 +750,9 @@ def preview_import(
# Incarca maparea de operatii o singura data (Eng#5: load_mapping o singura data)
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# T2: validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
# Reconstruieste parsed info (coercion_flags si date_col_format) din datele stocate
# Nota: import_rows stocheaza raw_json dupa coercion (din parse_file)
@@ -797,6 +805,8 @@ def preview_import(
mapping_meta=mapping_meta,
formula_columns=formula_columns,
override=overrides[i] or None,
valid_codes=valid_codes,
text_rules=text_rules,
)
# Calculeaza cheia de idempotenta pentru randurile ok/needs_review
@@ -1030,6 +1040,9 @@ def commit_import(
# Incarca maparea de operatii
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# T2: validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
# Construieste payload-urile submissions
enqueued: list[dict] = []
@@ -1076,7 +1089,7 @@ def commit_import(
# Rezolva prestatii INAINTE de canonicalizare (altfel cheia difera de cea din preview)
prestatii = mapped.get("prestatii") or []
resolved, _ = resolve_prestatii(prestatii, mapping)
resolved, _ = resolve_prestatii(prestatii, mapping, valid_codes, text_rules)
mapped["prestatii"] = resolved
# Canonicalizare (dupa rezolvare prestatii -> cod_prestatie inclus in cheie)
@@ -1127,6 +1140,8 @@ def commit_import(
toctou_collisions.append(row_index)
else:
sub_id = cur.lastrowid
# US-010: telemetrie pentru itemii rezolvati prin regula text.
_emite_text_rule_hits(conn, acct, int(sub_id), resolved)
enqueued.append({
"submission_id": sub_id,
"row_index": row_index,

View File

@@ -25,11 +25,13 @@ from ...db import get_connection
from ...errors import eroare as err_eroare
from ...idempotency import build_key, canonicalize_row
from ...mapping import (
_emite_text_rule_hits,
account_or_default,
account_scope_clause,
classify_prezentare,
load_mapping_meta,
load_nomenclator_codes,
load_text_rules,
pending_unmapped,
reresolve_account,
save_mapping,
@@ -65,13 +67,13 @@ def _effective_on_unmapped_error(conn, acct: int, req_value: bool | None) -> boo
return bool(row["on_unmapped_error_default"]) if row else False
def _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode) -> dict:
def _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules=None) -> dict:
"""classify_prezentare + aplicarea modului on_unmapped_error.
Cand exista coduri nemapate si error_mode=True, marcheaza outcome-ul ca respingere
(blocked_error=True): rutele NU mai fac enqueue, ci intorc o eroare per-element.
"""
cl = classify_prezentare(content, mapping, mapping_meta, valid_codes)
cl = classify_prezentare(content, mapping, mapping_meta, valid_codes, text_rules)
cl["blocked_error"] = bool(cl["unmapped"]) and error_mode
return cl
@@ -164,6 +166,8 @@ def create_prezentari(
# Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat.
# valid_codes gol (nomenclator nepopulat) -> None (nu validam, ca sa nu blocam tot).
valid_codes = load_nomenclator_codes(conn) or None
# Reguli text incarcate o data per cerere (seam partajat cu dry-run, invariant 5.2).
text_rules = load_text_rules(conn, acct)
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
for prez in req.prezentari:
content = prez.model_dump()
@@ -187,7 +191,7 @@ def create_prezentari(
# retrimiterea aceluiasi continut. Il RE-ACTIVAM (re-clasificam + actualizam
# creds + reset), printr-un UPDATE compare-and-swap pe status='error'.
if existing["status"] == "error":
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode)
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
if cl["blocked_error"]:
# on_unmapped_error=True: nu reactivam; randul ramane 'error'.
results.append(_rezultat_respins(existing["id"], cl))
@@ -208,6 +212,8 @@ def create_prezentari(
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
(encrypt_creds(req.rar_credentials.model_dump()), acct),
)
# US-010: telemetrie pentru itemii rezolvati prin regula text.
_emite_text_rule_hits(conn, acct, existing["id"], cl["resolved"])
# Raspuns onest si la reactivare (PRD 5.7): daca re-clasificarea
# cade pe needs_data/needs_mapping, expune motivul (nu doar status).
results.append(_rezultat_enqueue(existing["id"], cl, reactivated=True))
@@ -230,7 +236,7 @@ def create_prezentari(
# Helper pur partajat cu dry-run (PRD 5.2): reproduce EXACT clasificarea
# (canonicalize + mapare op->cod + validare + auto_send gate).
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode)
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
if cl["blocked_error"]:
# on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat).
results.append(_rezultat_respins(None, cl))
@@ -240,8 +246,11 @@ def create_prezentari(
"VALUES (?, ?, ?, ?, ?, ?)",
(key, acct, cl["status"], json.dumps(cl["content"], ensure_ascii=False), cl["rar_error"], creds_enc),
)
sub_id = int(cur.lastrowid)
# US-010: telemetrie pentru itemii rezolvati prin regula text.
_emite_text_rule_hits(conn, acct, sub_id, cl["resolved"])
# Raspuns onest (PRD 5.7): pe needs_data/needs_mapping expune erori/nemapate/motiv.
results.append(_rezultat_enqueue(int(cur.lastrowid), cl))
results.append(_rezultat_enqueue(sub_id, cl))
# US-004: audit cerere API per cont. Doar metadate (count + distributie status),
# NICIUN camp de payload PII integral. Reuse conn (T4 — fara contentie WAL).
@@ -284,10 +293,12 @@ def valideaza_prezentari(
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
# Acelasi seam ca trimiterea reala: dry-run trebuie sa vada aceleasi reguli text.
text_rules = load_text_rules(conn, acct)
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
for i, prez in enumerate(req.prezentari):
content = prez.model_dump()
res = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode)
res = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
if res["blocked_error"]:
res = {**res, "status": "error"}
# US-003: imbogatim fiecare element nemapat cu 3 niveluri COD_NEMAPAT