# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Ce este

Gateway web (Python / FastAPI) care preia prezentari de service-auto si le declara la **RAR AUTOPASS** (Legea 142/2023, OM 210/2024). Inlocuieste integrarea Visual FoxPro `RarAutoPass` (ROAAUTO), arhivata in `legacy-vfp/` (doar referinta).

Limba proiectului este **romana**: cod, comentarii, commit-uri, documentatie. Fara emoji in raspunsuri sau fisiere noi (preferinta proiect).

## Surse de adevar (citeste-le inainte sa modifici comportament)

- `docs/api-rar-contract.md` — contractul RAR. **Unde un plan/cod difera de contract, contractul are dreptate.**
- `docs/ROADMAP.md` — singura sursa de progres + procesul de lucru. O sesiune noua porneste de aici. Doar sectiunea "Stadiu Implementare" se modifica pe parcurs; detaliile stau in PRD-uri (`docs/prd/`).

## Comenzi

```bash
pip3 install -r requirements.txt          # Python 3.12+

# Rulare locala (dev): API + worker sunt PROCESE SEPARATE
uvicorn app.main:app --reload --port 8010 # API: dashboard /, Swagger /docs, /healthz, /metrics
python3 -m app.worker                      # worker (necesar doar pentru a procesa coada)

# Wrapper-ul start.sh ambaleaza mediu (test/prod) + rol (api/worker/both/finalizate)
./start.sh test both --send     # API + worker, trimite efectiv la RAR test (loguri in .run/)
./start.sh test finalizate      # listeaza prezentarile inregistrate la RAR (verificare independenta)
./start.sh status               # stare procese + /healthz
./start.sh stop                 # opreste procesele pornite cu "both"
./start-test.sh / ./start-prod.sh   # fixeaza mediul, forwardeaza rolul

# Teste (pytest; folosesc FastAPI TestClient + SQLite temporar). Testele live RAR sunt
# skip implicit (marker `live`) — `pytest -q` nu atinge endpointul real.
python3 -m pytest -q
python3 -m pytest tests/test_worker_reconcile.py -q          # un fisier
python3 -m pytest tests/test_worker_reconcile.py::test_x -q  # un singur test
python3 -m pytest -q -m "not live"                           # exclude explicit testele live

# Test LIVE pe RAR test (opt-in, skip implicit; atinge endpointul real -> creeaza FINALIZATA):
# reproduce lantul mapare inline -> queued -> worker -> sent -> verificare in finalizate.
AUTOPASS_LIVE_RAR=1 python3 -m pytest tests/test_live_rar.py -q   # necesita settings.xml cu creds <test>

# Lifecycle chei API (admin, doar din CLI — nu exista suprafata HTTP)
python3 -m tools.apikey create --account 2   # cheie afisata O SINGURA DATA (rfak_...)
python3 -m tools.apikey list|rotate|revoke

# Docker (deploy): api + worker + autoheal, acelasi image + volum SQLite
docker compose up --build
```

## Arhitectura

**Doua procese peste acelasi SQLite (WAL) persistent**, care comunica EXCLUSIV prin tabela `submissions`:

- **API** (`app.main:app`) — API v1 (`app/api/v1/router.py` + `import_router.py`), dashboard web HTMX (`app/web/routes.py` + `templates/`), `/healthz`, `/metrics`. Worker-ul **nu** ruleaza ca task aici: un worker mort nu trebuie sa lase containerul "sanatos".
- **Worker** (`app/worker/__main__.py`) — bucla: heartbeat → recupereaza orfane → claim atomic (`BEGIN IMMEDIATE`) → login RAR (per cont) → `postPrezentare` → update. Retry/backoff exponential, reconciliere anti-duplicat, lease pe randuri `sending` orfane, re-login la JWT expirat (TTL 30h). Send DEZACTIVAT implicit (`AUTOPASS_WORKER_SEND_ENABLED=false`) — sigur pentru probe.

**Doua canale de intrare** convergent in coada `submissions`:
- Canal API (Treapta 1): `POST /v1/prezentari` pentru ROAAUTO / soft propriu.
- Import web (Treapta 2): upload xlsx/csv → mapare coloane → preview → commit (`app/import_parse.py`, `import_router.py`, dashboard).

Flux: validare (`validation.py`) → mapare operatie→cod (`mapping.py`) → enqueue cu PII criptat → worker trimite → dashboard monitorizeaza live.

### Invariante critice (usor de stricat)

- **`AUTOPASS_CREDS_KEY` trebuie sa fie ACEEASI intre API si worker.** API cripteaza creds RAR (Fernet), worker le decripteaza. Chei diferite → worker nu poate decripta → trimiterile esueaza. `start.sh both` genereaza o cheie efemera partajata; pentru prod pune una persistenta in `.env`. (`crypto.py`)
- **Idempotenta = hash de continut canonic** server-side (`idempotency.py`), pentru ca RAR accepta duplicate si nu are nr. comanda. `build_key` normalizeaza INTOTDEAUNA `account_id` la `account_or_default` (None == 1) INAINTE de hash — altfel acelasi rand logic primeste chei diferite pe canalele API vs import (OV-2). `canonicalize_row` normeaza VIN/nr/odometru (strip ".0" din coercion Excel) inainte de validare si de cheie.
- **`FINALIZATA` e terminal la RAR** — fara anulare/corectie prin API. De aceea reconcilierea anti-duplicat: pe eroare **ambigua** (timeout / TransportError / 502/503/504 / 429 / 408) sau rand `sending` orfan, worker-ul cauta in finalizate (match pe vin+dataPrestatie+odometruFinal) si marcheaza `sent` fara a re-trimite (`reconcile.py`). **EXCEPTIE: un RAR 500 cu mesaj** (`RarError.rar_message`, ex. `ORA-12899`) e un esec DEFINITIV (RAR a raspuns „am esuat", nu o pierdere de raspuns) → worker-ul NU reconciliaza si NU reincearca, marcheaza `error` cu mesajul RAR (`RAR_EROARE_SERVER`). Altfel ar marca fals `sent` pe un record PARTIAL pe care RAR (ne-tranzactional) il lasa la esec.
- **Creds RAR per cont**: durabile in `accounts.rar_creds_enc` (canal web, fallback re-login) SAU efemere in `submissions.rar_creds_enc` (canal API, sterse dupa primul login reusit). Worker incearca submission-ul intai, apoi fallback la cont. Purjarea sterge DOAR `submissions.rar_creds_enc`, NU `accounts.rar_creds_enc`.
- **Auth API-key** (`auth.py`): identifica CONTUL ROAAUTO, separat de credentialele RAR. Stocam doar SHA-256 al cheii. Enforcement prin `AUTOPASS_REQUIRE_API_KEY`: `false` (dev) → fara cheie merge pe cont id=1, cheie invalida → 401; `true` (prod) → cheie obligatorie pe `/v1/*` protejat. POST-urile + rutele de import sunt account-scoped; GET-urile de listare sunt si ele account-scoped (5.15/US-011: fragmentele `_fragments/submissions|trimitere|mapari|status|jurnal|nomenclator|trimiteri-versiune` sub `require_login` + scope, 404-before-leak pe id strain; `GET /v1/prezentari(/{id})`/`/v1/mapari`/`/v1/audit/export` filtrate pe cont). `GET /v1/nomenclator` ramane public intentionat (coduri RAR publice, fara PII).
- **Admin bootstrap fara hardcodare**: la `/signup`, primul user din TOATA baza (`count_admins()==0`, verificat in tranzactie) primeste `is_admin=1` automat; orice signup ulterior e neadmin. `is_admin` da acces la `/admin` (panou global, toate conturile) — verificat pe `account_id`-ul sesiunii, dar neconditionat DE care `account_id` a primit userul (semnul de "cine e admin" nu are legatura cu contul `id=1`). Contul `id=1` e doar bucket-ul de fallback pentru trafic API neautentificat in dev (`AUTOPASS_REQUIRE_API_KEY=false`) — separat de conceptul de admin. Fix manual daca primul signup s-a consumat deja gresit: `python3 -m tools.account set-admin --account N`.
- **Mapare coloane retinuta per `(account_id, signature_coloane)`** (`column_mappings`): la urmatorul fisier cu aceleasi coloane, pentru acelasi cont, maparea se reaplica automat. Un cont poate avea mai multe formate memorate simultan.
- **Mapare operatie→cod**: prestatie poate veni cu `cod_prestatie` (cod RAR direct) sau `cod_op_service` (cod intern) + `denumire`. Nerezolvat → submission `needs_mapping` (nu se trimite), apare in editorul web cu sugestie fuzzy; la salvarea maparii se re-rezolva automat submission-urile blocate.
- **`cod_prestatie` e VALIDAT fata de nomenclator la ingestie** (`resolve_prestatii(..., valid_codes)`): un cod direct NECUNOSCUT in nomenclator NU se mai trimite raw — e promovat la `cod_op_service` (denumire=cod) si tratat ca operatie de mapat. Motiv (confirmat live 2026-06-23): RAR accepta NUMAI coduri din nomenclator (coloana `COD_PRESTATIE` max 5 car.); un cod necunoscut da **HTTP 500** (`ORA-12899`), iar RAR **NU e tranzactional** → lasa un record PARTIAL `FINALIZATA` (terminal) chiar pe esec, pe care reconcilierea worker-ului l-ar marca fals `sent`. Comportamentul la cod necunoscut/nemapat: `on_unmapped_error` (camp boolean top-level pe `POST /v1/prezentari` + `/valideaza`) = `false` (intra in editor, `needs_mapping`) sau `true` (respinge fara enqueue → `submission_id=null` + `erori`). Default = `accounts.on_unmapped_error_default` (implicit `false`/`0`); precedenta cerere > cont > `false`.
- **WAF RAR da 403 fara User-Agent de browser** — toate apelurile httpx trimit `User-Agent: Mozilla/5.0` (`config.py`, confirmat live).
- **422 fara echo de credentiale**: handler-ul global de validare in `main.py` pastreaza type/loc/msg dar DROP-a `input`/`ctx` (altfel ar reflecta `rar_credentials.password`).
- **Retentie**: `submissions` sent + `import_batches` primesc `purge_after = now + 90 zile`; worker-ul purjeaza odata pe ora (T16, GDPR/L.142).

### Masina de stari submissions

`queued → sending → sent` (succes, cu `id_prezentare` de la RAR). Ramuri: `needs_mapping` (cod nerezolvat), `needs_data` (RAR 400, validare continut), `error` (max retries / 4xx nerecuperabil / **RAR 500 cu mesaj — esec definitiv** / creds invalide / login 401 — NU se face retry pe creds gresite). Backoff: `next_attempt_at = now + base*2^retry`, plafonat. Schema completa: `app/schema.sql`.

## Mod non-interactiv

Vezi `/workspace/CLAUDE.md` (workspace-level): cand esti lansat cu `claude -p`, creeaza fisiere noi DOAR in `/workspace/.claude-work/<task>/`, nu in `/workspace`. Modificarile la fisiere existente se fac in locatia originala.

## Skill routing

When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.

Key routing rules:
- Product ideas/brainstorming → invoke /office-hours
- Strategy/scope → invoke /plan-ceo-review
- Architecture → invoke /plan-eng-review
- Design system/plan review → invoke /design-consultation or /plan-design-review
- Full review pipeline → invoke /autoplan
- Bugs/errors → invoke /investigate
- QA/testing site behavior → invoke /qa or /qa-only
- Code review/diff check → invoke /review
- Visual polish → invoke /design-review
- Ship/deploy/PR → invoke /ship or /land-and-deploy
- Save progress → invoke /context-save
- Resume context → invoke /context-restore
- Author a backlog-ready spec/issue → invoke /spec
