feat(T2): reconciliere anti-duplicat + retry/backoff + recuperare orfane

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>
This commit is contained in:
Claude Agent
2026-06-15 18:20:32 +00:00
parent 36d1b916d5
commit 77088daf29
9 changed files with 495 additions and 67 deletions

View File

@@ -211,12 +211,34 @@ Aplicate deja pe ambele medii (test + producție):
- Corecția datelor eronate (după FINALIZATA) = solicitare la **suport.autopass@rarom.ro**
(pe test nu e cazul). **Nu există flux API de corecție/anulare pentru records-urile noastre.**
## Monitorizare (citire prezentări)
## Monitorizare (citire prezentări) — VERIFICAT LIVE 2026-06-15
- Pe **mediul de test**: la interogarea listei de prezentări finalizate **NU primești și `prestatii`** în răspuns.
- Pe **producție**: prestațiile sunt disponibile; lista poate fi filtrată după keyword / interval
de date și exportată în Excel.
- Implicație dashboard: nu te baza pe `prestatii` din listă pe test; le ai în `submissions` local.
**Rută:** `GET /prezentari/getAllPrezentariFinalizate` (Bearer). Confirmat live.
**Răspuns:** `{statusCode, message, data: {totalCount, content: [...]}}` listă în `data.content`.
Fiecare item din `content` (live):
```json
{
"id": 68514, "dataPrestatie": "2026-06-15", "vin": "WVWZZZ1KZAW000123",
"odometruFinal": 123456, "idAgent": 40, "tipPrestatie": null,
"odometruInitial": null, "idUser": 6766, "sistemReparat": null, "obs": "...",
"nrInmatriculare": "B999TST", "listaPrestatii": null, "status": "FINALIZATA",
"prestatii": null, "b64Image": null
}
```
- **`odometruFinal` e NUMĂR** (int) în listare (deși la `postPrezentare` se trimite string). Reconcilierea
compară ca int.
- Pe **test**: `prestatii` vine `null` (confirmă: nu te baza pe `prestatii` din listă le ai local în `submissions`).
- **Filtrele NU funcționează pe test**: `?vin=`, `?search=`, `?keyword=`, `?dataPrestatie=` sunt IGNORATE
(întorc tot setul). `?page=&size=` rup răspunsul (non-JSON). **fetch tot setul, filtrează client-side.**
Pe prod doc-ul promite filtrare keyword/dată + export Excel (de re-verificat pe prod).
- **RAR acceptă DUPLICATE**: live există 2 perechi de records identice pe `vin+dataPrestatie+odometruFinal`
(id 6362263625, 6362363626). De aceea reconcilierea pe răspuns pierdut e necesară, iar matcher-ul
alege **id-ul maxim** când există mai multe potriviri.
> Reconciliere (T2): înainte de re-send pe un rând `sending`, GET finalizate, match pe
> `vin + dataPrestatie + odometruFinal(int)`; dacă există → marchează `sent` cu id-ul găsit, NU re-trimite.
## Corecții față de `docs/plans/*` (citește înainte de a refolosi planurile)