diff --git a/backend/src/cyclone/api.py b/backend/src/cyclone/api.py index 03c6021..9a1b9e9 100644 --- a/backend/src/cyclone/api.py +++ b/backend/src/cyclone/api.py @@ -144,6 +144,7 @@ app.add_middleware( from cyclone.api_routers import ( # noqa: E402 acks, activity, + batches, clearhouse, config, health, @@ -158,6 +159,7 @@ app.include_router(providers.router) app.include_router(clearhouse.router) app.include_router(config.router) app.include_router(activity.router) +app.include_router(batches.router) # Backwards-compat re-exports: test_api_stream_live.py imports these # names directly from ``cyclone.api`` to invoke the endpoint coroutines @@ -1159,63 +1161,7 @@ def inbox_export_csv(lane: str): ) -# --------------------------------------------------------------------------- # -# GET endpoints (read views over the in-memory store) -# --------------------------------------------------------------------------- # - - -def _batch_summary_claim_count(rec: BatchRecord) -> int: - """Return the number of claims on a batch, handling both 837P and 835.""" - if rec.kind == "837p": - return len(rec.result.claims) # type: ignore[attr-defined] - if rec.kind == "835": - return len(rec.result.claims) # type: ignore[attr-defined] - return 0 - - -@app.get("/api/batches") -def list_batches( - request: Request, - limit: int = Query(100, ge=1, le=1000), -) -> Any: - """Summary of all parsed batches, newest first.""" - records = store.list(limit=limit) - items = [ - { - "id": r.id, - "kind": r.kind, - "inputFilename": r.input_filename, - "parsedAt": r.parsed_at.isoformat().replace("+00:00", "Z"), - "claimCount": _batch_summary_claim_count(r), - } - for r in records - ] - all_records = store.all() - total = len(all_records) - returned = len(items) - has_more = total > returned - if _wants_ndjson(request): - return StreamingResponse( - _ndjson_stream_list(items, total, returned, has_more), - media_type="application/x-ndjson", - ) - return { - "items": items, - "total": total, - "returned": returned, - "has_more": has_more, - } - - -@app.get("/api/batches/{batch_id}") -def get_batch(batch_id: str) -> Any: - rec = store.get(batch_id) - if rec is None: - raise HTTPException( - status_code=404, - detail={"error": "Not found", "detail": f"Batch {batch_id} not found"}, - ) - return json.loads(rec.result.model_dump_json()) +# --- /api/batches lives in cyclone.api_routers.batches --- @app.get("/api/claims") diff --git a/backend/src/cyclone/api_routers/batches.py b/backend/src/cyclone/api_routers/batches.py new file mode 100644 index 0000000..958ca6d --- /dev/null +++ b/backend/src/cyclone/api_routers/batches.py @@ -0,0 +1,75 @@ +"""``/api/batches`` — list & detail endpoints for parsed batches. + +Each ``store.add(...)`` call (837P, 835, 999, TA1, 277CA) produces a +``BatchRecord`` kept in memory by :class:`CycloneStore`. The list +endpoint returns a lightweight summary (id, kind, filename, parsedAt, +claim count) for the recent batches; the detail endpoint returns the +full parsed payload for one batch. +""" +from __future__ import annotations + +import json +from typing import Any + +from fastapi import APIRouter, HTTPException, Query, Request +from fastapi.responses import StreamingResponse + +from cyclone.api_helpers import ndjson_stream_list, wants_ndjson +from cyclone.store import BatchRecord, store + +router = APIRouter() + + +def _batch_summary_claim_count(rec: BatchRecord) -> int: + """Return the number of claims on a batch, handling both 837P and 835.""" + if rec.kind == "837p": + return len(rec.result.claims) # type: ignore[attr-defined] + if rec.kind == "835": + return len(rec.result.claims) # type: ignore[attr-defined] + return 0 + + +@router.get("/api/batches") +def list_batches_endpoint( + request: Request, + limit: int = Query(100, ge=1, le=1000), +) -> Any: + """Summary of all parsed batches, newest first.""" + records = store.list(limit=limit) + items = [ + { + "id": r.id, + "kind": r.kind, + "inputFilename": r.input_filename, + "parsedAt": r.parsed_at.isoformat().replace("+00:00", "Z"), + "claimCount": _batch_summary_claim_count(r), + } + for r in records + ] + all_records = store.all() + total = len(all_records) + returned = len(items) + has_more = total > returned + if wants_ndjson(request): + return StreamingResponse( + ndjson_stream_list(items, total, returned, has_more), + media_type="application/x-ndjson", + ) + return { + "items": items, + "total": total, + "returned": returned, + "has_more": has_more, + } + + +@router.get("/api/batches/{batch_id}") +def get_batch_endpoint(batch_id: str) -> Any: + """Return the full parsed payload for one batch.""" + rec = store.get(batch_id) + if rec is None: + raise HTTPException( + status_code=404, + detail={"error": "Not found", "detail": f"Batch {batch_id} not found"}, + ) + return json.loads(rec.result.model_dump_json())