"""Notification intake for informed-decision delivery ids. Intentionally absent from the 14-day write-idempotency allowlist: retention is this receipt row, and a conflicting reuse must stay readable by GET. """ from __future__ import annotations import json from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse from sqlalchemy.ext.asyncio import AsyncSession from api.database import get_session from api.models.memo_notification import MemoNotificationReceipt from api.services.informed_decision_contract import ( MAX_NOTIFICATION_BYTES, ContractError, parse_delivery_id, parse_notification, ) from api.services.memo_notification import NotificationConflict, accept_notification router = APIRouter(prefix="/informed-decision", tags=["informed-decision-notifications"]) def _error(status: int, code: str) -> JSONResponse: return JSONResponse( status_code=status, content={"error": code}, headers={"Cache-Control": "no-store"}, ) def _payload(receipt: MemoNotificationReceipt, *, status: int, replay: bool = False) -> JSONResponse: return JSONResponse( status_code=status, content={ "delivery_id": receipt.delivery_id, "memo_id": receipt.memo_id, "question": receipt.question, "memo_class": receipt.memo_class, "submitter": receipt.submitter, "message_id": str(receipt.message_id), "status": "accepted", "authoritative_review": receipt.authoritative_review, }, headers={ "Cache-Control": "no-store", **({"X-StateHub-Idempotency-Replay": "true"} if replay else {}), }, ) @router.post("/notifications") async def post_notification( request: Request, session: AsyncSession = Depends(get_session) ) -> JSONResponse: raw = await request.body() if len(raw) > MAX_NOTIFICATION_BYTES: return _error(413, "notification_too_large") content_type = request.headers.get("content-type", "") if content_type.split(";")[0].strip().lower() != "application/json": return _error(415, "json_required") try: delivery_id = parse_delivery_id(request.headers.get("idempotency-key")) notice = parse_notification(json.loads(raw)) except ContractError as exc: return _error(exc.status, exc.code) except (json.JSONDecodeError, UnicodeError): return _error(400, "invalid_json") try: receipt, replay = await accept_notification(session, delivery_id, notice) except NotificationConflict as exc: return JSONResponse( status_code=409, content={ "error": "notification_conflict", "delivery_id": exc.receipt.delivery_id, }, headers={"Cache-Control": "no-store"}, ) return _payload(receipt, status=200 if replay else 201, replay=replay) @router.get("/notifications/{delivery_id}") async def get_notification( delivery_id: str, session: AsyncSession = Depends(get_session) ) -> JSONResponse: try: checked = parse_delivery_id(delivery_id) except ContractError as exc: return _error(exc.status, exc.code) receipt = await session.get(MemoNotificationReceipt, checked) if receipt is None: return _error(404, "notification_not_found") return _payload(receipt, status=200)