fix(nrinmatriculare): normalizeaza spatii/cratime/puncte consecvent pe toate canalele
Doar 13.8% din numerele de inmatriculare istorice treceau validarea stricta
^[A-Z0-9]{1,10}$ (spike SQL), majoritatea din cauza formatelor reale cu
spatii/cratime ("B 99 XYZ", "B-99-XYZ") pe care normalizarea actuala
(strip+upper) nu le curata.
Adauga normalize_nr_inmatriculare() in app.idempotency (deja punctul comun
API+import pentru canonicalize_row) si o foloseste consecvent in:
- models.PrezentareIn (validare shape / normalizare API)
- validation.validate_prezentare (validare continut)
- idempotency.canonicalize_row (cheia de dedup)
Fara aceasta consecventa, validarea si idempotency ar diverge pe acelasi
numar de inmatriculare in formate echivalente.
This commit is contained in:
@@ -19,9 +19,25 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
NRINM_STRIP_RE = re.compile(r"[\s\-.]+")
|
||||
|
||||
|
||||
def normalize_nr_inmatriculare(value: object) -> str:
|
||||
"""Normalizare canonica nr. inmatriculare: strip, upper, elimina spatii/cratime/puncte.
|
||||
|
||||
Formate reale ("B 99 XYZ", "B-99-XYZ") nu treceau NRINM_RE (app.validation) fara
|
||||
aceasta curatare (spike SQL: doar 13.8% din numerele istorice treceau strict
|
||||
^[A-Z0-9]{1,10}$). Folosita consecvent in models.PrezentareIn, validation._norm
|
||||
si canonicalize_row de mai jos ca sa nu diverga validarea de cheia de dedup.
|
||||
"""
|
||||
s = str(value or "").strip().upper()
|
||||
return NRINM_STRIP_RE.sub("", s)
|
||||
|
||||
|
||||
def _op_identity(p: Any) -> str:
|
||||
"""Cod RAR (normalizat) daca exista, altfel codul intern ROAAUTO."""
|
||||
get = p.get if isinstance(p, dict) else (lambda k, d=None: getattr(p, k, d))
|
||||
@@ -41,7 +57,7 @@ def canonicalize_row(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
- prestatii: pastrate ca-atare (rezolvarea e in resolve_prestatii).
|
||||
"""
|
||||
vin = (raw.get("vin") or "").strip().upper()
|
||||
nr = (raw.get("nr_inmatriculare") or "").strip().upper()
|
||||
nr = normalize_nr_inmatriculare(raw.get("nr_inmatriculare"))
|
||||
|
||||
# Odometru: strip ".0" Excel float coercion
|
||||
odo_raw = raw.get("odometru_final")
|
||||
|
||||
@@ -11,6 +11,8 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
from app.idempotency import normalize_nr_inmatriculare
|
||||
|
||||
|
||||
class RarCredentials(BaseModel):
|
||||
"""Credentiale RAR per-cerere (vin de la ROAAUTO din Oracle). NU se stocheaza."""
|
||||
@@ -69,11 +71,16 @@ class PrezentareIn(BaseModel):
|
||||
obs: str | None = None
|
||||
b64_image: str | None = None
|
||||
|
||||
@field_validator("vin", "nr_inmatriculare")
|
||||
@field_validator("vin")
|
||||
@classmethod
|
||||
def _norm_upper(cls, v: str) -> str:
|
||||
return v.strip().upper()
|
||||
|
||||
@field_validator("nr_inmatriculare")
|
||||
@classmethod
|
||||
def _norm_nrinm(cls, v: str) -> str:
|
||||
return normalize_nr_inmatriculare(v)
|
||||
|
||||
@field_validator("data_prestatie", "odometru_final")
|
||||
@classmethod
|
||||
def _norm_strip(cls, v: str) -> str:
|
||||
|
||||
@@ -18,6 +18,7 @@ from datetime import date
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from app.errors import eroare as _eroare
|
||||
from app.idempotency import normalize_nr_inmatriculare
|
||||
|
||||
# VIN: 17 caractere, majuscule, fara O/I/Q (plan §2 + contract).
|
||||
VIN_RE = re.compile(r"^[A-HJ-NPR-Z0-9]{17}$")
|
||||
@@ -73,7 +74,7 @@ def validate_prezentare(content: dict) -> list[dict]:
|
||||
))
|
||||
|
||||
# --- nrInmatriculare ---
|
||||
nrinm = _norm(content.get("nr_inmatriculare"))
|
||||
nrinm = normalize_nr_inmatriculare(content.get("nr_inmatriculare"))
|
||||
if not NRINM_RE.match(nrinm):
|
||||
errors.append(_eroare(
|
||||
"NR_INMATRICULARE_FORMAT",
|
||||
|
||||
Reference in New Issue
Block a user