feat(5.20): US-004/005/006/009 ingestie+API+worker+import pe mediu RAR

US-004: rezolva_rar_env (cerere>default cont>ancora globala) + MediuIndisponibil
+ cod RAR_MEDIU_INDISPONIBIL.
US-005: camp rar_env pe POST /v1/prezentari + /valideaza (Literal), echo in
SubmissionResult/ValidareResult/GET, build_key + INSERT env-aware.
US-006: AccountSessions re-cheiat (account_id, rar_env); RarClient base_url per
env; creds din slotul env; purge + recover_orphans scoped pe env (E1/1a, 1b/E6);
claim_one propaga rar_env (1c/E8); keepalive pe ancora globala (M2).
US-009: selector mediu la import (>=2 medii), eticheta la 1, banner la 0; commit
seteaza rar_env pe submissions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-29 20:30:11 +00:00
parent d5ce0e2e2b
commit 19d8aaa7aa
19 changed files with 1451 additions and 130 deletions

View File

@@ -55,6 +55,7 @@ from ...mapping import (
resolve_prestatii,
)
from ...validation import validate_prezentare
from ...rar_env import MediuIndisponibil, rar_env_efectiv_cont, rezolva_rar_env
router = APIRouter(prefix="/v1/import", tags=["import"])
@@ -260,10 +261,10 @@ def _resolve_row_for_preview(
}
def _build_idempotency_key(account_id: int | None, resolved: dict[str, Any]) -> str:
def _build_idempotency_key(account_id: int | None, resolved: dict[str, Any], rar_env: str = "test") -> str:
"""Construieste cheia de idempotenta pentru un rand rezolvat."""
canon = canonicalize_row(resolved)
return build_key(account_id, canon)
return build_key(account_id, canon, rar_env)
# Campuri de continut editabile in preview. Operatia/codul RAR NU se editeaza
@@ -767,6 +768,11 @@ def preview_import(
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(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).
from ...config import get_settings as _get_settings_env
preview_env = rar_env_efectiv_cont(conn, account_id) or _get_settings_env().rar_env or "test"
# Recalculam coercion_flags din valorile stocate (nu sunt persistate separat):
# detectie simpla de VIN numeric.
coercion_flags_map: dict[int, list[str]] = {}
@@ -822,7 +828,7 @@ def preview_import(
key = None
if resolved_info["resolved_status"] in ("ok", "needs_review", "needs_data"):
try:
key = _build_idempotency_key(account_id, resolved_info["resolved"])
key = _build_idempotency_key(account_id, resolved_info["resolved"], preview_env)
keys_for_lookup.append(key)
if key not in key_to_index:
key_to_index[key] = []
@@ -930,6 +936,7 @@ class CommitIn(BaseModel):
description="Indecsi de rand needs_review bifate explicit de utilizator",
)
confirmed_by: str | None = Field(None, description="Email/identifier utilizator (log atestare)")
rar_env: str | None = Field(None, description="Mediu RAR tinta ('test'|'prod'). None = default cont.")
@router.post("/{import_id}/commit")
@@ -1024,6 +1031,18 @@ def commit_import(
if n_total_ok == 0:
raise HTTPException(status_code=422, detail="Niciun rand ok de confirmat.")
# Rezolva mediul RAR tinta al lotului (US-009): cerut > default cont > ancora globala.
try:
env = rezolva_rar_env(conn, account_id, req.rar_env)
except ValueError as e:
raise HTTPException(status_code=422, detail={"error": "mediu_invalid", "message": str(e)})
except MediuIndisponibil as e:
raise HTTPException(status_code=422, detail={
"error": "mediu_indisponibil",
"message": str(e),
"disponibile": e.disponibile,
})
# T3 (PRD 5.17): enforce volum plan — INAINTE de enqueue (invariant idempotenta).
# Decizie #21: respingere TOTALA a lotului (nu enqueue partial tacut).
from ...config import get_settings as _get_settings
@@ -1172,8 +1191,8 @@ def commit_import(
"odometru_final": canon["odometru_final"],
})
# Cheia de idempotenta (identica cu cheia din preview — aceeasi ordine)
key = build_key(account_id, canon)
# Cheia de idempotenta (identica cu cheia din preview — aceeasi ordine + env)
key = build_key(account_id, canon, env)
# Hash row pentru atestare (valori rezolvate)
rows_for_hash.append(json.dumps({
@@ -1189,9 +1208,9 @@ def commit_import(
# INSERT ON CONFLICT DO NOTHING (TOCTOU)
cur = conn.execute(
"INSERT OR IGNORE INTO submissions "
"(idempotency_key, account_id, status, payload_json, batch_id, row_index, purge_after) "
"VALUES (?, ?, 'queued', ?, ?, ?, " + purge_after_sql + ")",
(key, acct, payload_json, import_id, row_index),
"(idempotency_key, account_id, status, payload_json, batch_id, row_index, purge_after, rar_env) "
"VALUES (?, ?, 'queued', ?, ?, ?, " + purge_after_sql + ", ?)",
(key, acct, payload_json, import_id, row_index, env),
)
if cur.rowcount == 0: