# Gateway RAR AUTOPASS Gateway web (Python / FastAPI) care preia prezentarile de service si le declara la **RAR AUTOPASS** (Legea 142/2023, OM 210/2024). Inlocuieste integrarea Visual FoxPro existenta (ROAAUTO). Sursa de adevar pentru contractul RAR este [`docs/api-rar-contract.md`](docs/api-rar-contract.md). ## Arhitectura pe scurt Doua procese peste acelasi SQLite persistent: | Proces | Rol | Pornire | |--------|-----|---------| | **API** (`app.main:app`) | API v1 (`/v1/*`), dashboard web (`/`), `/healthz`, `/metrics`, import fisiere (`/v1/import/*`) | `uvicorn app.main:app` | | **Worker** (`app.worker`) | login RAR + JWT, refresh nomenclator, trimite prezentarile din coada, retry/backoff, heartbeat | `python3 -m app.worker` | Worker-ul ruleaza ca **proces separat** (nu task in API) — un worker mort nu trebuie sa lase containerul "sanatos". Comunicarea API <-> worker se face exclusiv prin tabela `submissions` din SQLite. Send-ul catre RAR este **dezactivat implicit** (`AUTOPASS_WORKER_SEND_ENABLED=false`) — sigur pentru probe. ## Cerinte - Python 3.12+ - Dependintele din `requirements.txt` ```bash pip3 install -r requirements.txt ``` (Optional, pentru deploy: Docker + Docker Compose — vezi sectiunea Docker.) ## Configurare Variabilele de mediu folosesc prefixul `AUTOPASS_`. Pentru dev local valorile implicite sunt suficiente — **nu** ai nevoie de `.env` sau de credentiale RAR ca sa testezi UI-ul si API-ul. Copiaza `.env.example` -> `.env` doar cand vrei sa rulezi end-to-end. | Variabila | Implicit | Rol | |-----------|----------|-----| | `AUTOPASS_DB_PATH` | `./data/autopass.db` | calea fisierului SQLite | | `AUTOPASS_RAR_ENV` | `test` | `test` sau `prod` | | `AUTOPASS_REQUIRE_API_KEY` | `false` | `false` = dev (fara cheie -> cont id=1); `true` = prod (cere cheie) | | `AUTOPASS_CREDS_KEY` | (efemera) | cheie Fernet pt criptarea creds RAR. **Trebuie partajata intre API si worker.** Genereaza: `python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` | | `AUTOPASS_WORKER_SEND_ENABLED` | `false` | `true` = trimite efectiv la RAR (proba end-to-end) | | `AUTOPASS_WORKER_USE_TEST_CREDS` | `false` | dev: foloseste blocul `` din `settings.xml` pt login worker | Pentru proba reala cu RAR: copiaza `settings.xml.example` -> `settings.xml` si completeaza credentialele de test (fisierul **nu** se comite). ## Rulare locala (dezvoltare) ### 1. Porneste API-ul ```bash uvicorn app.main:app --reload --port 8010 # sau, daca uvicorn nu e pe PATH: python3 -m uvicorn app.main:app --reload --port 8010 ``` La prima pornire se creeaza schema SQLite si se face seed la nomenclatorul RAR (18 coduri din contract), astfel incat dashboard-ul si maparile functioneaza imediat, offline. ### 2. (Optional) Porneste worker-ul Necesar doar pentru a procesa coada / a trimite la RAR. Pentru testarea UI-ului si a import-ului **nu** e necesar. ```bash python3 -m app.worker ``` ### Pornire rapida cu `start.sh` `start.sh` ambaleaza pornirea pe mediu (`test` / `prod`) si rol (`api` / `worker` / `both`): ```bash ./start.sh test api # API pe :8010, mediu test ./start.sh test worker --send # worker care TRIMITE la RAR test ./start.sh test both --send # API + worker impreuna (dev end-to-end, loguri in .run/) ./start.sh prod api --port 8010 # API mediu prod ./start.sh prod worker --send # worker prod (NU foloseste creds de test) ./start.sh status # stare procese + /healthz ./start.sh stop # opreste procesele pornite cu "both" ./start.sh test finalizate # ce prezentari sunt inregistrate la RAR (vezi mai jos) ``` Optiuni: `--port N`, `--host H`, `--reload` (dev), `--send` (activeaza trimiterea la RAR), `--test-creds` / `--no-test-creds` (forteaza folosirea creds `` din `settings.xml`). Pe `test` cu `--send`, creds `` se folosesc automat. Pentru productie reala foloseste `docker compose` (vezi sectiunea Docker). Doua wrappere fixeaza mediul si forwardeaza rolul + optiunile: ```bash ./start-test.sh # = start.sh test both --send (API + worker, trimite la RAR test) ./start-test.sh worker --send # = start.sh test worker --send ./start-test.sh finalizate # = start.sh test finalizate ./start-prod.sh both --send # = start.sh prod both --send ./start-prod.sh api # = start.sh prod api ``` Pe test, `./start-test.sh` fara argumente porneste end-to-end (sandbox RAR e sigur). Pe prod, `./start-prod.sh` cere rolul explicit si trimiterea trebuie ceruta cu `--send` (evita trimiteri accidentale in productie). ## Testare in browser Cu API-ul pornit, deschide in browser: | URL | Ce vezi | |-----|---------| | `http://localhost:8010/` | **Dashboard** — stare coada, banner prezentari blocate, stare worker / ultim login RAR, editor mapari operatii, browser nomenclator, sectiune **import fisier** | | `http://localhost:8010/docs` | **Swagger UI** — API v1 interactiv (incearca endpointurile direct din browser) | | `http://localhost:8010/healthz` | JSON sanatate: worker viu, ultim login RAR, adancime coada | | `http://localhost:8010/metrics` | metrici text (submissions pe status) | ### Fluxul de import fisier (xlsx / csv) din browser Pe dashboard, in sectiunea de import: 1. **Incarca** un fisier `.xlsx` sau `.csv` (drag & drop sau selectare). 2. **Mapeaza coloanele** — gateway-ul sugereaza automat (fuzzy) maparea coloana fisier -> camp canonic (VIN, data prestatie, odometru, operatie etc.). Maparea se retine pe semnatura coloanelor: la urmatorul fisier cu aceleasi coloane se aplica automat. 3. **Preview** — fiecare rand primeste o stare: `ok`, `needs_mapping`, `needs_data`, `needs_review`, `already_sent`, `duplicate_in_file`. 4. **Confirma** — gate dur: retastezi numarul exact de randuri `ok` de trimis. Randurile confirmate intra in coada (`submissions`), apoi le urmaresti in tabelul de jos. Coloane recunoscute (cu sinonime): `VIN`, `Nr inmatriculare`, `Data prestatie`, `Odometru final`, `Odometru initial`, `Operatie`, `Observatii`. ### Genereaza un fisier de test pentru import Repo-ul nu contine fisiere sample. Creeaza unul rapid: ```bash python3 - <<'PY' import openpyxl wb = openpyxl.Workbook() ws = wb.active ws.append(["VIN", "Nr inmatriculare", "Data prestatie", "Odometru final", "Operatie"]) ws.append(["WAUZZZ8K0AA000001", "B123ABC", "2026-06-15", 120000, "REVIZIE PERIODICA"]) ws.append(["WAUZZZ8K0AA000002", "B456DEF", "2026-06-16", 85000, "REPARATIE"]) wb.save("sample_import.xlsx") print("scris sample_import.xlsx") PY ``` Sau un CSV echivalent: ```bash printf 'VIN,Nr inmatriculare,Data prestatie,Odometru final,Operatie\nWAUZZZ8K0AA000001,B123ABC,2026-06-15,120000,REVIZIE PERIODICA\n' > sample_import.csv ``` Incarca apoi fisierul prin sectiunea de import a dashboard-ului. ## Proba trimitere la RAR (mediu test) + verificare ca au ajuns Implicit worker-ul **nu** trimite (`AUTOPASS_WORKER_SEND_ENABLED=false`). Pentru proba end-to-end pe contul de test RAR: 1. Pune credentialele de test in `settings.xml` (copiaza din `settings.xml.example`, completeaza blocul ``). Acestea **nu** se comit. 2. Bag-a prezentari in coada — fie prin import fisier din dashboard, fie prin API (`POST /v1/prezentari`, vezi mai jos). 3. Porneste worker-ul cu trimiterea activa: ```bash ./start.sh test worker --send ``` Worker-ul face login la RAR test, ia randurile `queued`, trimite si trece fiecare rand in `sent` cu `id_prezentare` (id-ul intors de RAR — dovada ca a ajuns) sau in `needs_data` / `error` cu motivul. 4. **Vizualizeaza prezentarile trimise** — trei feluri: - **Dashboard** (`http://localhost:8010/`) — tabelul de jos arata fiecare submission cu status (`sent`/`error`/...), `id_prezentare`, cod RAR si eroare. Se actualizeaza singur. - **API**: `curl -s http://localhost:8010/v1/prezentari` — coada locala cu statusuri. - **Direct de la RAR** (confirmare independenta ca au ajuns): ```bash ./start.sh test finalizate ``` Face login la RAR test si listeaza prezentarile inregistrate acolo (id, VIN, data, odometru). Compari `id`-urile cu `id_prezentare` din coada locala: daca se regasesc, prezentarea a ajuns la RAR. > Status `sent` + `id_prezentare` completat = RAR a acceptat prezentarea. Worker-ul are si > reconciliere anti-duplicat: daca raspunsul RAR se pierde, la urmatorul ciclu cauta > prezentarea in finalizate si o marcheaza `sent` fara a o re-trimite. ## Import fisier pentru mai multi utilizatori (service-uri) cu formate diferite Da — fiecare service auto poate avea propriul format de fisier (alte denumiri de coloane, alta ordine, alt format de data). Sistemul **tine minte maparea per cont**, deci nu o refaci la fiecare upload: - **Cont (`account_id`)** — fiecare service e un cont. In productie contul se identifica prin **cheia API** (`X-API-Key`) trimisa la upload/cerere (`AUTOPASS_REQUIRE_API_KEY=true`). In dev, fara cheie, totul merge pe contul implicit `id=1`. - **Semnatura coloanelor** — la upload, gateway-ul calculeaza o semnatura din lista (sortata) a denumirilor de coloane din fisier. Maparea coloana-fisier -> camp-canonic se salveaza in tabela `column_mappings`, cheie unica `(account_id, signature_coloane)`, impreuna cu formatul de data. - **Re-aplicare automata** — la urmatorul fisier cu **aceleasi coloane** (aceeasi semnatura), pentru **acelasi cont**, maparea retinuta se aplica automat si sari direct la preview. Daca un service schimba formatul (alte coloane) se creeaza o semnatura noua, deci o mapare noua — fara sa o strice pe cea veche. Astfel un cont poate avea mai multe formate memorate simultan. Pe scurt: **cine** = `account_id` (din cheia API), **care format** = `signature_coloane` (setul de coloane al fisierului). Combinatia lor selecteaza maparea corecta. ## Conturi (service-uri) si chei API Un **cont** (`accounts`) = un service auto care foloseste gateway-ul. Cererile `/v1/*` se autentifica printr-o **cheie API** (header `X-API-Key: ` sau `Authorization: Bearer `) care identifica contul. Cheia e separata de credentialele RAR ale service-ului. Enforcement-ul e controlat de `AUTOPASS_REQUIRE_API_KEY`: - `false` (dev/test, implicit): cerere fara cheie -> contul implicit `id=1`; o cheie prezenta dar invalida -> `401`. - `true` (productie): orice `/v1/*` **protejat** cere o cheie valida, altfel `401`. Auth-ul se aplica pe endpointurile care scriu/sunt legate de cont (au dependinta de cheie): `POST /v1/prezentari`, `POST /v1/mapari`, `POST|DELETE /v1/conturi/rar-creds` si toate rutele de import (`POST /v1/import`, `.../column-mapping`, `.../preview`, `.../commit`, `.../export-failed`) — acestea ruleaza pe `account_id`-ul cheii. GET-urile de **monitorizare** (`/v1/prezentari`, `/v1/prezentari/{id}`, `/v1/nomenclator`, `/v1/mapari`, `/v1/audit/export`) sunt momentan **neprotejate si globale** (nu filtreaza pe cont). Filtrarea pe cont a listarilor + protejarea lor raman de adaugat (vezi tabelul de mai jos). ### Stare implementare | Capabilitate | Stare | Cum | |--------------|-------|-----| | Emitere / rotire / revocare / listare chei API | **Implementat** | CLI `python3 -m tools.apikey` | | Auth pe cheie (X-API-Key / Bearer) pe POST-uri + import | **Implementat** | `app/auth.py` + flag `AUTOPASS_REQUIRE_API_KEY` | | Ingestie + import account-scoped (din cheie) | **Implementat** | `POST /v1/prezentari`, `POST /v1/import` | | Creds RAR durabile per cont | **Implementat** | `POST /v1/conturi/rar-creds` | | Creare cont nou (service) | **De facut / manual** | momentan prin `INSERT` SQL (vezi mai jos); nu exista tool/endpoint dedicat | | Protejare + filtrare pe cont a GET-urilor de listare | **De facut** | `GET /v1/prezentari`, `/v1/nomenclator`, `/v1/audit/export` sunt globale acum | | Self-onboarding web (login email+parola -> emite cheie) | **De facut** | `docs/ROADMAP.md` (Etapa 3.3) — neimplementat | > Lifecycle-ul cheilor se face DOAR din CLI, pe masina gateway-ului (admin) — nu exista > suprafata HTTP de administrare de securizat. Cheia in clar se afiseaza **o singura data** > la creare/rotire; in DB se pastreaza doar hash-ul SHA-256. ### Creare cont + cheie pentru un service nou Pana la onboarding-ul web, un cont nou se creeaza direct in DB, apoi i se emite o cheie: ```bash # 1. Creeaza contul (numele + CUI sunt informative) python3 -c " from app.db import get_connection, init_db init_db() c = get_connection() cur = c.execute(\"INSERT INTO accounts (name, cui) VALUES ('Service Auto SRL', 'RO12345678')\") print('account_id nou =', cur.lastrowid); c.commit(); c.close() " # 2. Emite o cheie API pentru cont (afisata O SINGURA DATA) python3 -m tools.apikey create --account 2 # -> rfak_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Alte operatii python3 -m tools.apikey list # toate cheile python3 -m tools.apikey list --account 2 # cheile unui cont python3 -m tools.apikey rotate --account 2 # revoca cele active + emite una noua python3 -m tools.apikey revoke --key-id 3 # revoca o cheie dupa id ``` ### Creds RAR per cont Ca worker-ul sa poata trimite pentru un service fara ca fiecare cerere sa-i poarte parola RAR, seteaza credentialele RAR durabile pe cont (criptate Fernet at-rest): ```bash curl -s -X POST http://localhost:8010/v1/conturi/rar-creds \ -H 'X-API-Key: rfak_...' -H 'Content-Type: application/json' \ -d '{"email": "service@exemplu.ro", "password": "parola-rar"}' ``` ## Testare prin API (curl) Exemplele de mai jos arata atat varianta **dev** (fara cheie -> cont id=1), cat si varianta **service cu cheie API** (header `X-API-Key`). Cand `AUTOPASS_REQUIRE_API_KEY=true`, cheia e obligatorie. ```bash # Sanatate (neprotejat) curl -s http://localhost:8010/healthz | python3 -m json.tool # Nomenclator RAR (cache local) curl -s http://localhost:8010/v1/nomenclator # Coada de prezentari (monitorizare; momentan globala + neprotejata, vezi nota de mai sus) curl -s http://localhost:8010/v1/prezentari # Trimite o prezentare -- dev (fara cheie API -> cont id=1) curl -s -X POST http://localhost:8010/v1/prezentari \ -H 'Content-Type: application/json' \ -d '{ "rar_credentials": {"email": "test@example.ro", "password": "secret"}, "prezentari": [{ "vin": "WAUZZZ8K0AA000001", "nr_inmatriculare": "B123ABC", "data_prestatie": "2026-06-15", "odometru_final": "120000", "prestatii": [{"cod_op_service": "REVIZIE PERIODICA", "denumire": "REVIZIE PERIODICA"}] }] }' # Trimite o prezentare -- service cu cheie API (account_id curge din cheie) curl -s -X POST http://localhost:8010/v1/prezentari \ -H 'X-API-Key: rfak_...' -H 'Content-Type: application/json' \ -d '{ "rar_credentials": {"email": "service@exemplu.ro", "password": "parola-rar"}, "prezentari": [{ "vin": "WAUZZZ8K0AA000002", "nr_inmatriculare": "B456DEF", "data_prestatie": "2026-06-16", "odometru_final": "85000", "prestatii": [{"cod_op_service": "REPARATIE", "denumire": "REPARATIE"}] }] }' # Import fisier prin API pentru un service (multi-tenant: contul vine din cheie) curl -s -X POST http://localhost:8010/v1/import \ -H 'X-API-Key: rfak_...' -F 'file=@sample_import.xlsx' ``` Endpointurile complete sunt vizibile si testabile in `/docs` (Swagger UI). In Swagger, pune cheia prin butonul "Authorize" sau adauga header-ul `X-API-Key`. ```bash # Sanatate curl -s http://localhost:8010/healthz | python3 -m json.tool # Nomenclator RAR (cache local) curl -s http://localhost:8010/v1/nomenclator # Coada de prezentari curl -s http://localhost:8010/v1/prezentari # Trimite o prezentare (dev: fara cheie API -> cont id=1) curl -s -X POST http://localhost:8010/v1/prezentari \ -H 'Content-Type: application/json' \ -d '{ "rar_credentials": {"email": "test@example.ro", "password": "secret"}, "prezentari": [{ "vin": "WAUZZZ8K0AA000001", "nr_inmatriculare": "B123ABC", "data_prestatie": "2026-06-15", "odometru_final": "120000", "prestatii": [{"cod_op_service": "REVIZIE PERIODICA", "denumire": "REVIZIE PERIODICA"}] }] }' ``` Endpointurile complete sunt vizibile si testabile in `/docs` (Swagger UI). ## Rularea testelor ```bash python3 -m pytest -q ``` Suita acopera fundatia, securitatea, validarea, parserul de import, masina de stari a worker-ului si fluxul UI de import (E2E cu RAR mock). ## Docker / deploy ```bash # 1. Pregateste .env (CRITIC: AUTOPASS_CREDS_KEY partajata intre api si worker) cp .env.example .env # completeaza AUTOPASS_CREDS_KEY (vezi comanda de generare de mai sus) # 2. Porneste API + worker + autoheal docker compose up --build ``` `docker-compose.yml` porneste trei containere: `api` (port 8010), `worker` si `autoheal` (restarteaza worker-ul cand heartbeat-ul devine invechit). Ambele servicii folosesc acelasi image si acelasi volum SQLite persistent. ## Structura ``` app/ main.py # FastAPI: API v1 + dashboard + /healthz + /metrics api/v1/ # router.py (prezentari, nomenclator, mapari) + import_router.py web/ # routes.py (dashboard + import UI HTMX) + templates/ + static/ worker/ # proces separat: login RAR, send, retry, heartbeat rar_client.py # client HTTP RAR (login/JWT, postPrezentare, nomenclator) validation.py # validare continut (T3) mapping.py # mapare operatie -> cod prestatie + fuzzy lookup crypto.py # criptare Fernet creds RAR efemere (zero-storage at rest) schema.sql # schema SQLite docs/ # contract RAR (sursa de adevar) + ROADMAP (progres + proces) tests/ # suita pytest legacy-vfp/ # arhiva Visual FoxPro ROAAUTO (legacy, doar referinta/migrare) ``` Contract RAR (sursa de adevar): [`docs/api-rar-contract.md`](docs/api-rar-contract.md). Roadmap + proces de dezvoltare: [`docs/ROADMAP.md`](docs/ROADMAP.md).