feat(5.6): observabilitate + jurnal aplicatie + lifecycle trimiteri blocate
Implementeaza PRD 5.6 complet (14 stories, TDD). Doua axe:
Lifecycle trimiteri blocate (Val A):
- submissions_admin.py: sterge/repune scoped (404 cross-account inaintea lui 409 stare)
- reactivare dedup peste `error` cu CAS (WHERE id=? AND status='error'), creds noi in
submissions + accounts.rar_creds_enc; worker invalideaza sesiunea RAR la creds proaspete
(JWT 30h vechi nu mai trimite cu parola gresita); camp aditiv `reactivated:true`
- retentie randuri blocate 30z; purge_expired exclude queued/sending; purge_after curatat
la reactivare/requeue
- API DELETE /v1/prezentari/{id} + /repune (200+JSON); UI butoane + bulk + banner actionabil
Observabilitate:
- app/observ.py log_event: dublu canal app_events (DB) + RotatingFileHandler per-proces,
redactare creds/PII la scriere (redact_pii/vin_partial)
- request_id middleware + X-Request-ID pe toate raspunsurile
- handler global excepții -> 500 envelope 6-chei + request_id (traceback doar in jurnal)
- audit cerere API (api_prezentari/api_auth_esuat) + audit worker (rar_login/tranzitii)
- tab "Jurnal" filtrabil scoped (non-admin doar contul sau); retentie jurnal 90z
- rar_error expus in GET /v1/prezentari/{id} (recovery observabil)
pytest -q: 741 passed, 0 failed. Docs: PRD raport VERIFY, contract endpointuri noi, ROADMAP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,14 @@ from ...models import (
|
||||
ValidareResponse,
|
||||
ValidareResult,
|
||||
)
|
||||
from ...observ import log_event
|
||||
from ...payload_view import prezentare_din_payload
|
||||
from ...submissions_admin import (
|
||||
SubmissionNotFound,
|
||||
SubmissionStateConflict,
|
||||
delete_submission,
|
||||
requeue_submission,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/v1", tags=["v1"])
|
||||
|
||||
@@ -93,6 +100,37 @@ def create_prezentari(
|
||||
(key,),
|
||||
).fetchone()
|
||||
if existing:
|
||||
# US-012: un rand `error` (ex. creds RAR gresite) NU mai blocheaza tacit
|
||||
# retrimiterea aceluiasi continut. Il RE-ACTIVAM (re-clasificam + actualizam
|
||||
# creds + reset), printr-un UPDATE compare-and-swap pe status='error'.
|
||||
if existing["status"] == "error":
|
||||
cl = classify_prezentare(content, mapping, mapping_meta)
|
||||
cur = conn.execute(
|
||||
"UPDATE submissions SET status=?, payload_json=?, rar_error=?, "
|
||||
"rar_creds_enc=COALESCE(?, rar_creds_enc), retry_count=0, "
|
||||
"next_attempt_at=NULL, sending_since=NULL, purge_after=NULL, "
|
||||
"updated_at=datetime('now') WHERE id=? AND status='error'",
|
||||
(cl["status"], json.dumps(cl["content"], ensure_ascii=False),
|
||||
cl["rar_error"], creds_enc, existing["id"]),
|
||||
)
|
||||
if cur.rowcount == 1:
|
||||
# Creds noi se propaga si in canalul durabil (accounts.rar_creds_enc,
|
||||
# decizie #17) — ambele canale converg pe parola corectata.
|
||||
if req.rar_credentials is not None:
|
||||
conn.execute(
|
||||
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
|
||||
(encrypt_creds(req.rar_credentials.model_dump()), acct),
|
||||
)
|
||||
results.append(SubmissionResult(
|
||||
submission_id=existing["id"], status=cl["status"], reactivated=True,
|
||||
))
|
||||
continue
|
||||
# Cursa: alt POST/requeue a schimbat starea intre SELECT si UPDATE
|
||||
# (rowcount==0) -> raspuns dedup pe starea CURENTA.
|
||||
existing = conn.execute(
|
||||
"SELECT id, status, id_prezentare FROM submissions WHERE id=?",
|
||||
(existing["id"],),
|
||||
).fetchone()
|
||||
results.append(
|
||||
SubmissionResult(
|
||||
submission_id=existing["id"],
|
||||
@@ -112,6 +150,25 @@ def create_prezentari(
|
||||
(key, acct, cl["status"], json.dumps(cl["content"], ensure_ascii=False), cl["rar_error"], creds_enc),
|
||||
)
|
||||
results.append(SubmissionResult(submission_id=int(cur.lastrowid), status=cl["status"]))
|
||||
|
||||
# US-004: audit cerere API per cont. Doar metadate (count + distributie status),
|
||||
# NICIUN camp de payload PII integral. Reuse conn (T4 — fara contentie WAL).
|
||||
dist: dict[str, int] = {}
|
||||
for r in results:
|
||||
if r.reactivated:
|
||||
cheie = "reactivated"
|
||||
elif r.deduped:
|
||||
cheie = "deduped"
|
||||
else:
|
||||
cheie = r.status
|
||||
dist[cheie] = dist.get(cheie, 0) + 1
|
||||
log_event(
|
||||
"api_prezentari",
|
||||
account_id=acct,
|
||||
mesaj=f"{len(results)} prezentari procesate",
|
||||
context={"count": len(results), "distributie": dist},
|
||||
conn=conn,
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
return PrezentariResponse(results=results)
|
||||
@@ -199,6 +256,10 @@ _PREZENTARE_FIELDS = frozenset({
|
||||
"id", "status", "id_prezentare", "rar_status_code", "retry_count",
|
||||
"next_attempt_at", "created_at", "updated_at", "account_id",
|
||||
"batch_id", "row_index", "purge_after",
|
||||
# T9: rar_error e SIGUR de expus — contine doar coduri/mesaje de validare RAR si
|
||||
# erori din catalog (niciodata creds, ex. RAR_CREDS_INVALIDE poarta doar cauza
|
||||
# "credentiale RAR invalide", fara parola). Face recovery-ul observabil prin API.
|
||||
"rar_error",
|
||||
})
|
||||
|
||||
|
||||
@@ -224,6 +285,59 @@ def get_prezentare(
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.delete("/prezentari/{submission_id}")
|
||||
def delete_prezentare(
|
||||
submission_id: int,
|
||||
account_id: int = Depends(resolve_account_id),
|
||||
) -> dict:
|
||||
"""Sterge o trimitere blocata a contului cheii API (US-010).
|
||||
|
||||
Raspuns 200 + body JSON (NU 204 — clienti VFP fac string-parse). Scope evaluat
|
||||
INAINTEA starii (decizie /autoplan #20): cross-account / inexistent -> 404 (acelasi
|
||||
mesaj, B3); own-account `sent`/`sending` -> 409 (conflict de stare).
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
try:
|
||||
res = delete_submission(conn, account_id, submission_id)
|
||||
except SubmissionNotFound:
|
||||
raise HTTPException(status_code=404, detail="submission inexistent")
|
||||
except SubmissionStateConflict as exc:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"trimiterea nu se poate sterge in starea '{exc.status}'",
|
||||
)
|
||||
return {"ok": True, **res}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/prezentari/{submission_id}/repune")
|
||||
def repune_prezentare(
|
||||
submission_id: int,
|
||||
account_id: int = Depends(resolve_account_id),
|
||||
) -> dict:
|
||||
"""Re-pune in coada o trimitere blocata a contului cheii API (US-010).
|
||||
|
||||
`error -> queued` (peste helper US-009), re-ruleaza classify. Acelasi oracol de
|
||||
scope/stare ca DELETE (404 cross-account/inexistent, 409 sent/sending).
|
||||
"""
|
||||
conn = get_connection()
|
||||
try:
|
||||
try:
|
||||
res = requeue_submission(conn, account_id, submission_id)
|
||||
except SubmissionNotFound:
|
||||
raise HTTPException(status_code=404, detail="submission inexistent")
|
||||
except SubmissionStateConflict as exc:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"trimiterea nu se poate re-pune in starea '{exc.status}'",
|
||||
)
|
||||
return {"ok": True, **res}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/nomenclator")
|
||||
def get_nomenclator() -> dict:
|
||||
conn = get_connection()
|
||||
|
||||
Reference in New Issue
Block a user