feat(mapari): regula 'exclude de la declarare' per operatie + stare preview Nedeclarat

Operatiile care nu se declara la RAR (ex. ITP facturat in service) primesc o
regula exclus=1 in operations_mapping, setabila din panoul de mapare al
preview-ului de import si din tab-ul Mapari (optiunea 'Nu se declara la RAR').

- resolve_prestatii(excluded_ops): item nemapat cu op exclusa -> adnotat exclus,
  nu mai e needs_mapping; precedenta: cod explicit > exclus > mapare > reguli text
- split_prestatii_excluse: itemii exclusi nu intra niciodata in payload/cheie
- preview import: rand cu toate operatiile excluse -> stare 'excluded'
  (eticheta Nedeclarat), necomis; operatia dispare din panoul de mapat
- reresolve/corectie/API: submission cu toate operatiile excluse -> needs_data
  cu motiv explicit; ingestia API trateaza excluderea la clasificare
- migrare: coloana operations_mapping.exclus + rebuild import_rows pentru
  CHECK-ul resolved_status cu 'excluded' (o singura data, gardat pe sqlite_master)
- fix flake: clamp similaritate embeddings la [-1,1] (float32 dadea 1.0000001)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-07 17:49:47 +00:00
parent c6e29aa65d
commit 0bc618763e
14 changed files with 616 additions and 37 deletions

View File

@@ -49,11 +49,13 @@ from ...mapping import (
_emite_text_rule_hits,
account_or_default,
has_no_auto_send,
load_excluded_ops,
load_mapping_meta,
load_nomenclator_codes,
load_text_rules,
normalize_for_match,
resolve_prestatii,
split_prestatii_excluse,
)
from ...validation import validate_prezentare
from ...rar_env import MediuIndisponibil, rar_env_efectiv_cont, rezolva_rar_env
@@ -135,11 +137,12 @@ def _resolve_row_for_preview(
valid_codes: set[str] | None = None,
text_rules: list[dict] | None = None,
reviewed: bool = False,
excluded_ops: set[str] | None = None,
) -> dict[str, Any]:
"""Rezolva un rand din import pentru preview: aplica mapare coloane + validare.
Intoarce un dict cu:
resolved_status: ok/needs_mapping/needs_data/needs_review
resolved_status: ok/needs_mapping/needs_data/needs_review/excluded
resolved: valorile finale rezolvate (VIN, data, km, prestatii)
errors: lista erori validare
flags: motive needs_review
@@ -218,7 +221,7 @@ def _resolve_row_for_preview(
# Rezolvare prestatii
prestatii = mapped.get("prestatii") or []
resolved, unmapped = resolve_prestatii(prestatii, mapping, valid_codes, text_rules)
resolved, unmapped = resolve_prestatii(prestatii, mapping, valid_codes, text_rules, excluded_ops)
mapped["prestatii"] = resolved
# Determinare stare
@@ -230,6 +233,19 @@ def _resolve_row_for_preview(
"flags": all_flags,
}
# Operatii excluse de la declarare: ies din prestatiile trimise (si din cheia
# de idempotenta). Toate excluse -> rand 'excluded' (nu se trimite, nu blocheaza).
declarabile, excluse = split_prestatii_excluse(resolved)
if excluse and not declarabile:
return {
"resolved_status": "excluded",
"resolved": mapped,
"errors": [],
"flags": all_flags,
}
if excluse:
mapped["prestatii"] = declarabile
# Validare continut
errors = validate_prezentare(mapped)
@@ -770,9 +786,10 @@ def preview_import(
# Incarca maparea de operatii o singura data
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# Validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
# Validare nomenclator + reguli text + excluderi incarcate O DATA, inainte de bucla.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
excluded_ops = load_excluded_ops(conn, acct)
# Mediul RAR efectiv al contului — folosit la calculul cheii de idempotenta
# la preview (trebuie sa coincida cu ce va folosi commit-ul fara rar_env explicit).
@@ -828,6 +845,7 @@ def preview_import(
override=overrides[i] or None,
valid_codes=valid_codes,
text_rules=text_rules,
excluded_ops=excluded_ops,
)
# Calculeaza cheia de idempotenta pentru randurile ok/needs_review
@@ -1118,9 +1136,10 @@ 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()}
# Validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
# Validare nomenclator + reguli text + excluderi incarcate O DATA, inainte de bucla.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
excluded_ops = load_excluded_ops(conn, acct)
# Construieste payload-urile submissions
enqueued: list[dict] = []
@@ -1184,7 +1203,12 @@ 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, valid_codes, text_rules)
resolved, _ = resolve_prestatii(prestatii, mapping, valid_codes, text_rules, excluded_ops)
# Prestatiile excluse de la declarare NU pleaca la RAR (ca in preview);
# rand fara nicio prestatie declarabila = 'excluded' in preview -> defensiv skip.
resolved, _excluse = split_prestatii_excluse(resolved)
if not resolved:
continue
mapped["prestatii"] = resolved
# Canonicalizare (dupa rezolvare prestatii -> cod_prestatie inclus in cheie;

View File

@@ -31,6 +31,7 @@ from ...mapping import (
account_or_default,
account_scope_clause,
classify_prezentare,
load_excluded_ops,
load_mapping_meta,
load_nomenclator_codes,
load_text_rules,
@@ -69,13 +70,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, text_rules=None) -> dict:
def _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules=None, excluded_ops=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, text_rules)
cl = classify_prezentare(content, mapping, mapping_meta, valid_codes, text_rules, excluded_ops)
cl["blocked_error"] = bool(cl["unmapped"]) and error_mode
return cl
@@ -186,6 +187,7 @@ def create_prezentari(
valid_codes = load_nomenclator_codes(conn) or None
# Reguli text incarcate o data per cerere (seam partajat cu dry-run).
text_rules = load_text_rules(conn, acct)
excluded_ops = load_excluded_ops(conn, acct)
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
# Rezolva mediul RAR tinta (cerut > default cont > ancora globala).
@@ -285,7 +287,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, text_rules)
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules, excluded_ops)
if cl["blocked_error"]:
# on_unmapped_error=True: nu reactivam; randul ramane 'error'.
results.append(_rezultat_respins(existing["id"], cl, rar_env=env))
@@ -337,7 +339,7 @@ def create_prezentari(
# Helper pur partajat cu dry-run: reproduce EXACT clasificarea
# (canonicalize + mapare op->cod + validare; fara gate auto_send).
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules, excluded_ops)
if cl["blocked_error"]:
# on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat).
results.append(_rezultat_respins(None, cl, rar_env=env))
@@ -397,6 +399,7 @@ def valideaza_prezentari(
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)
excluded_ops = load_excluded_ops(conn, acct)
error_mode = _effective_on_unmapped_error(conn, acct, req.on_unmapped_error)
# Rezolva env identic ca trimiterea reala si ecou-ieste in raspuns.
@@ -429,7 +432,7 @@ def valideaza_prezentari(
for i, prez in enumerate(req.prezentari):
content = prez.model_dump()
res = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
res = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules, excluded_ops)
if res["blocked_error"]:
res = {**res, "status": "error"}
# Imbogatim fiecare element nemapat cu 3 niveluri COD_NEMAPAT