From 21066ad0bfa21b52c9268457faf0462d3fc3b05b Mon Sep 17 00:00:00 2001 From: Nora Date: Mon, 6 Jul 2026 13:40:09 -0600 Subject: [PATCH] feat(sp36): absorb 277ca-acks endpoints into api_routers/acks.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves GET /api/277ca-acks and GET /api/277ca-acks/{ack_id} out of api.py (formerly lines 1278-1318) into the existing api_routers/acks.py, which already handles 999 ACK list/detail/ stream. 277CA ACKs share the same shape-of-life contract (persisted by a parse endpoint, listed newest-first, detail returns raw_json) so the consolidation lands in the router that already owns the adjacent surface — no new router file. While here: - collapsed the inline batch-fetch in list_277ca_acks_endpoint to the existing _find_linked_claim_ids_for_acks(ack_ids, kind='277ca') helper that the 999 list endpoint already uses (and ta1_acks.py uses too). Eliminates the copy-pasted SQL; same N+1-avoidance one-SELECT contract. - pruned ClaimAck / to_ui_two77ca_ack imports from api.py. Behaviour-preserving: pytest post-cut = 20 failed / 1253 passed / 10 skipped = baseline match. pytest -k '277ca or ack' = 235 passed, 1 skipped. --- backend/src/cyclone/api.py | 46 +-------------------- backend/src/cyclone/api_routers/acks.py | 55 ++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/backend/src/cyclone/api.py b/backend/src/cyclone/api.py index 4786028..b84bd33 100644 --- a/backend/src/cyclone/api.py +++ b/backend/src/cyclone/api.py @@ -38,7 +38,7 @@ from pydantic import ValidationError from cyclone import __version__, db from cyclone.auth.deps import matrix_gate from cyclone.clearhouse import InboundFile -from cyclone.db import Batch, Claim, ClaimAck, ClaimState, Remittance +from cyclone.db import Batch, Claim, ClaimState, Remittance from sqlalchemy import desc, or_ from sqlalchemy.exc import IntegrityError from cyclone.inbox_state import apply_999_rejections @@ -96,7 +96,6 @@ from cyclone.store import ( InvalidStateError, dashboard_kpis, store, - to_ui_two77ca_ack, utcnow, ) @@ -1275,49 +1274,6 @@ async def parse_277ca_endpoint( }) -@app.get("/api/277ca-acks", dependencies=[Depends(matrix_gate)]) -def list_277ca_acks_endpoint( - limit: int = Query(100, ge=1, le=5000), -) -> Any: - """Return the list of persisted 277CA ACKs, newest first. - - SP28: each item gains ``linked_claim_ids`` (batch-fetched in - one query to avoid N+1) so the Acks page row can render the - "🔗 N claims" badge inline. - """ - rows = store.list_277ca_acks() - items = [to_ui_two77ca_ack(r) for r in rows[:limit]] - ack_ids = [r.id for r in rows] - linked_map: dict[int, list[str]] = {aid: [] for aid in ack_ids} - if ack_ids: - with db.SessionLocal()() as s: - link_rows = ( - s.query(ClaimAck.ack_id, ClaimAck.claim_id) - .filter( - ClaimAck.ack_kind == "277ca", - ClaimAck.ack_id.in_(ack_ids), - ClaimAck.claim_id.isnot(None), - ) - .all() - ) - for ack_id, claim_id in link_rows: - linked_map[ack_id].append(claim_id) - for item, aid in zip(items, ack_ids[:limit]): - item["linked_claim_ids"] = linked_map.get(aid, []) - return {"total": len(rows), "items": items} - - -@app.get("/api/277ca-acks/{ack_id}", dependencies=[Depends(matrix_gate)]) -def get_277ca_ack_endpoint(ack_id: int) -> dict: - """Return one persisted 277CA ACK row with its parsed detail.""" - row = store.get_277ca_ack(ack_id) - if row is None: - raise HTTPException(status_code=404, detail=f"277CA ACK {ack_id} not found") - body = to_ui_two77ca_ack(row) - body["raw_json"] = row.raw_json - return body - - def _serialize_ta1(result) -> str: """Render a TA1 file from a ParseResultTa1 for the ``raw_ta1_text`` field. diff --git a/backend/src/cyclone/api_routers/acks.py b/backend/src/cyclone/api_routers/acks.py index c5e61a9..9dbb7af 100644 --- a/backend/src/cyclone/api_routers/acks.py +++ b/backend/src/cyclone/api_routers/acks.py @@ -1,12 +1,17 @@ -"""``/api/acks`` — list, detail, and live-tail stream for 999 ACKs. +"""``/api/acks`` and ``/api/277ca-acks`` — list, detail, and live-tail. -These are the persisted acknowledgment rows produced by +999 ACKs are the persisted acknowledgment rows produced by ``POST /api/parse-999``. The frontend ``useAcks`` hook re-shapes the list payload to its ``Ack`` interface in ``src/types/index.ts``. -The detail endpoint returns the full ``raw_json`` payload plus the -regenerated ``raw_999_text`` so the UI can show "view source" without a -second round-trip. +277CA ACKs are the persisted Claim Acknowledgment rows produced by +``POST /api/parse-277ca``. The frontend ``useAcks`` hook treats them +as a second source alongside 999 — the row shape is normalised in +``cyclone.store.ui.to_ui_two77ca_ack``. + +The 999 detail endpoint returns the full ``raw_json`` payload plus +the regenerated ``raw_999_text`` so the UI can show "view source" +without a second round-trip. SP25: ``/api/acks/stream`` joins the live-tail triplet — the Acks page mounts ``useTailStream("acks")`` and ``useMergedTail("acks", …)`` @@ -32,7 +37,7 @@ from cyclone.auth.deps import matrix_gate from cyclone.parsers.models_999 import ParseResult999 from cyclone.parsers.serialize_999 import serialize_999 from cyclone.pubsub import EventBus -from cyclone.store import store, to_ui_ack +from cyclone.store import store, to_ui_ack, to_ui_two77ca_ack router = APIRouter(dependencies=[Depends(matrix_gate)]) @@ -178,3 +183,41 @@ def get_ack_endpoint(ack_id: int) -> dict: else: body["raw_999_text"] = None return body + + +# -- 277CA ACKs ------------------------------------------------------------- +# SP36 Task 2: these two endpoints moved here from api.py (lines 1278-1318). +# 277CA ACKs share the same shape-of-life contract as 999 ACKs: persisted by +# a parse endpoint, listed newest-first, detail returns raw_json so the UI +# can show "view source" without a round-trip. + + +@router.get("/api/277ca-acks") +def list_277ca_acks_endpoint( + limit: int = Query(100, ge=1, le=5000), +) -> Any: + """Return the list of persisted 277CA ACKs, newest first. + + SP28: each item gains ``linked_claim_ids`` (batch-fetched via + the shared ``_find_linked_claim_ids_for_acks`` helper — one + SELECT keyed on ``ack_kind``, no N+1) so the Acks page row + can render the "🔗 N claims" badge inline. + """ + rows = store.list_277ca_acks() + items = [to_ui_two77ca_ack(r) for r in rows[:limit]] + ack_ids = [r.id for r in rows] + linked_map = _find_linked_claim_ids_for_acks(ack_ids, kind="277ca") + for item, aid in zip(items, ack_ids[:limit]): + item["linked_claim_ids"] = linked_map.get(aid, []) + return {"total": len(rows), "items": items} + + +@router.get("/api/277ca-acks/{ack_id}") +def get_277ca_ack_endpoint(ack_id: int) -> dict: + """Return one persisted 277CA ACK row with its parsed detail.""" + row = store.get_277ca_ack(ack_id) + if row is None: + raise HTTPException(status_code=404, detail=f"277CA ACK {ack_id} not found") + body = to_ui_two77ca_ack(row) + body["raw_json"] = row.raw_json + return body