feat(import): T9 canonicalize_row + build_key partajat (idempotency)

- canonicalize_row: VIN upper, odometru strip ".0" (Excel float coercion),
  data strip — INAINTE de validare si cheie (§3.4bis)
- build_key: aplica account_or_default(None->1) inainte de hash (OV-2):
  canal API (None) si canal import (1) produc aceeasi cheie
- build_key_legacy: helper dual-lookup pentru randuri DB vechi (pre-T9)
- router.py: POST /v1/prezentari foloseste build_key(account_id, canonicalize_row(content))
- 14 teste: canonicalizare, cross-canal, dedup float/int odometru, legacy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-16 20:15:59 +00:00
parent 80897ccbb1
commit 4ea21a034e
3 changed files with 293 additions and 5 deletions

View File

@@ -22,7 +22,7 @@ from pydantic import BaseModel, Field
from ...auth import resolve_account_id
from ...crypto import encrypt_creds
from ...db import get_connection
from ...idempotency import idempotency_key
from ...idempotency import build_key, canonicalize_row, idempotency_key
from ...mapping import (
account_or_default,
load_mapping,
@@ -63,7 +63,17 @@ def create_prezentari(
mapping = load_mapping(conn, acct)
for prez in req.prezentari:
content = prez.model_dump()
key = idempotency_key(account_id, content)
# T9/OV-2: canonicalize_row inaintea build_key (odometru strip ".0", VIN upper).
# build_key aplica account_or_default(account_id) inainte de hash:
# None si 1 colapseaza la aceeasi cheie (canal API + canal import).
canon = canonicalize_row(content)
key = build_key(account_id, canon)
# Aplica normalizarea si in content (odometru canonicalizat inainte de validare, §3.4bis)
content.update({
"vin": canon["vin"],
"nr_inmatriculare": canon["nr_inmatriculare"],
"odometru_final": canon["odometru_final"],
})
existing = conn.execute(
"SELECT id, status, id_prezentare FROM submissions WHERE idempotency_key=?",
(key,),