- app/validation.py: reguli de continut (VIN ^[A-HJ-NPR-Z0-9]{17}$ fara O/I/Q,
nrInm ^[A-Z0-9]{1,10}$, dataPrestatie ∈ [2024-12-01, azi] TZ Bucuresti,
R-ODO/I-ODO -> odometruInitial obligatoriu, odometruInitial<=odometruFinal,
odometruFinal numeric, prestatii nevide, b64Image base64 valid)
- erori structurate {field, message} (aceeasi forma ca raspunsul RAR), fara exceptii
- modele Pydantic: normalizare strip/upper pe vin/nrInm/coduri
- router /v1/prezentari: validare inainte de enqueue; esec continut -> needs_data
(tinut, vizibil in dashboard cu motiv), NU 422; JSON malformat -> 422 (shape)
- tests/: 29 teste (per regula + rutare API + idempotenta)
Verify: pytest 29 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
"""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()
|