feat(sp36): absorb 277ca-acks endpoints into api_routers/acks.py

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.
This commit is contained in:
Nora
2026-07-06 13:40:09 -06:00
parent a963d3ce25
commit 21066ad0bf
2 changed files with 50 additions and 51 deletions
+1 -45
View File
@@ -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.
+49 -6
View File
@@ -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