"""API v1 — suprafata gateway (schelet). Endpointuri din plan.md sect. 4. In schelet: - POST /v1/prezentari: enqueue cu idempotenta (dedup pe idempotency_key UNIQUE). - GET /v1/prezentari, /v1/prezentari/{id}: monitorizare coada. - GET /v1/nomenclator: cache local. - GET /v1/mapari: listare mapari cont. Validarea completa (T3), maparea op->cod, auth API-key, redactarea creds in middleware (CORE) si exportul CSV vin ulterior — marcate TODO unde lipsesc. """ from __future__ import annotations import json from fastapi import APIRouter, HTTPException from ...db import get_connection from ...idempotency import idempotency_key from ...models import PrezentareRequest, PrezentariResponse, SubmissionResult from ...validation import validate_prezentare router = APIRouter(prefix="/v1", tags=["v1"]) @router.post("/prezentari", response_model=PrezentariResponse) def create_prezentari(req: PrezentareRequest) -> PrezentariResponse: """Enqueue una/mai multe prezentari. Idempotent: continut identic -> acelasi submission. Validarea de continut (T3, app.validation) ruleaza inainte de enqueue: esecurile NU resping cererea, ci enqueue-aza cu status `needs_data` + motiv (plan.md sect. 3). JSON malformat -> 422 din Pydantic (validare de shape). TODO(auth): rezolva account_id din API key (acum None). Nota: rar_credentials NU se persista (zero-storage) — worker-ul le va primi pe alt canal (T2); in schelet enqueue-ul doar stocheaza prezentarea. """ account_id = None # TODO(auth): din API key conn = get_connection() results: list[SubmissionResult] = [] try: for prez in req.prezentari: content = prez.model_dump() key = idempotency_key(account_id, content) existing = conn.execute( "SELECT id, status, id_prezentare FROM submissions WHERE idempotency_key=?", (key,), ).fetchone() if existing: results.append( SubmissionResult( submission_id=existing["id"], status=existing["status"], id_prezentare=existing["id_prezentare"], deduped=True, ) ) continue # T3: validare de continut -> queued daca e curat, altfel needs_data + motiv. errors = validate_prezentare(content) if errors: status, rar_error = "needs_data", json.dumps(errors, ensure_ascii=False) else: status, rar_error = "queued", None cur = conn.execute( "INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error) " "VALUES (?, ?, ?, ?, ?)", (key, account_id, status, json.dumps(content, ensure_ascii=False), rar_error), ) results.append(SubmissionResult(submission_id=int(cur.lastrowid), status=status)) finally: conn.close() return PrezentariResponse(results=results) @router.get("/prezentari") def list_prezentari(status: str | None = None, limit: int = 100) -> dict: conn = get_connection() try: if status: rows = conn.execute( "SELECT id, status, id_prezentare, rar_status_code, retry_count, created_at, updated_at " "FROM submissions WHERE status=? ORDER BY id DESC LIMIT ?", (status, limit), ).fetchall() else: rows = conn.execute( "SELECT id, status, id_prezentare, rar_status_code, retry_count, created_at, updated_at " "FROM submissions ORDER BY id DESC LIMIT ?", (limit,), ).fetchall() return {"submissions": [dict(r) for r in rows]} finally: conn.close() @router.get("/prezentari/{submission_id}") def get_prezentare(submission_id: int) -> dict: conn = get_connection() try: row = conn.execute("SELECT * FROM submissions WHERE id=?", (submission_id,)).fetchone() if not row: raise HTTPException(status_code=404, detail="submission inexistent") out = dict(row) out.pop("payload_json", None) # nu expunem payload-ul brut (PII) in listare return out finally: conn.close() @router.get("/nomenclator") def get_nomenclator() -> dict: conn = get_connection() try: rows = conn.execute( "SELECT cod_prestatie, nume_prestatie, updated_at FROM nomenclator_rar ORDER BY cod_prestatie" ).fetchall() return {"nomenclator": [dict(r) for r in rows]} finally: conn.close() @router.get("/mapari") def get_mapari(account_id: int | None = None) -> dict: conn = get_connection() try: if account_id is not None: rows = conn.execute( "SELECT * FROM operations_mapping WHERE account_id=? ORDER BY cod_op_service", (account_id,), ).fetchall() else: rows = conn.execute("SELECT * FROM operations_mapping ORDER BY account_id, cod_op_service").fetchall() return {"mapari": [dict(r) for r in rows]} finally: conn.close()