Inchide bucla de trimitere (plan.md sect. 4 worker, failure registry).
- app/reconcile.py: match_finalizata pe vin+dataPrestatie+odometruFinal (int),
alege id maxim la duplicate (RAR accepta duplicate, confirmat live)
- app/rar_client.get_finalizate: parseaza data.content (descoperit live ca
ruta = GET /prezentari/getAllPrezentariFinalizate; filtrele nu merg pe test)
- app/worker rescris:
- recuperare orfane (rand 'sending' peste lease = worker mort mid-POST)
- pe eroare tranzitorie/timeout: reconciliere INAINTE de re-send (anti-duplicat);
daca recordul exista la RAR -> sent fara re-POST
- retry/backoff exponential; peste worker_max_retries -> error + banner
- re-login la token expirat (JWT 30h)
- schema: coloana next_attempt_at (backoff) + migrare aditiva in init_db
- config: worker_sending_lease_s, worker_retry_base_s/max_s, worker_max_retries
- contract: documentata ruta+forma getAllPrezentariFinalizate (verificat live)
Verify: pytest 54 passed (15 noi T2) + validare live (reconciliere record 68514).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
1.9 KiB
Python
55 lines
1.9 KiB
Python
"""Reconciliere anti-duplicat pe raspuns pierdut (T2 — P1).
|
|
|
|
Daca un POST postPrezentare ajunge la RAR si RAR insereaza, dar raspunsul se
|
|
pierde (timeout/retea), randul ramane `sending`. Un re-send orb ar crea duplicat
|
|
(RAR ACCEPTA duplicate — confirmat live, vezi contract). Inainte de re-send,
|
|
interogam lista finalizate si cautam o potrivire pe `vin + dataPrestatie +
|
|
odometruFinal`. UNIQUE pe `submissions` NU acopera acest caz (e despre starea la RAR).
|
|
|
|
Functie pura, unit-testabila. odometruFinal e NUMAR in listarea RAR, string in
|
|
payload-ul nostru -> comparam ca int.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def _as_int(value: object) -> int | None:
|
|
s = str(value if value is not None else "").strip()
|
|
try:
|
|
return int(s)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def match_finalizata(
|
|
finalizate: list[dict[str, Any]],
|
|
*,
|
|
vin: str,
|
|
data_prestatie: str,
|
|
odometru_final: object,
|
|
) -> int | None:
|
|
"""Intoarce id-ul (data.id) unei prezentari finalizate care se potriveste, altfel None.
|
|
|
|
Match: vin (case-insensitive) + dataPrestatie (egal) + odometruFinal (egal ca int).
|
|
Daca exista mai multe potriviri (RAR accepta duplicate), intoarce id-ul MAXIM
|
|
(cel mai recent) — orice match dovedeste ca RAR are deja inregistrarea.
|
|
"""
|
|
want_vin = (vin or "").strip().upper()
|
|
want_odo = _as_int(odometru_final)
|
|
want_data = (data_prestatie or "").strip()
|
|
|
|
matches: list[int] = []
|
|
for item in finalizate:
|
|
if (item.get("vin") or "").strip().upper() != want_vin:
|
|
continue
|
|
if (str(item.get("dataPrestatie") or "").strip()) != want_data:
|
|
continue
|
|
if _as_int(item.get("odometruFinal")) != want_odo:
|
|
continue
|
|
item_id = _as_int(item.get("id"))
|
|
if item_id is not None:
|
|
matches.append(item_id)
|
|
return max(matches) if matches else None
|