Documents the SP16 scheduler control surface (5 endpoints under
/api/admin/scheduler/*) and brings the Project layout tree in sync
with the latest api_routers/ split state:
- new 'Inbound MFT Scheduler (SP16)' section between SFTP Wire-Up
and Batches/Reconciliation/Activity, covering start/stop/status/
tick/processed-files with a forward-compat note that
cyclone.scheduler + cyclone.db.ProcessedInboundFile are in flight
- api_routers/ tree now lists all 14 routers, including the four
parse_* routers (parse_835/837/999/ta1) and admin.py carrying the
scheduler endpoints
- api.py comment updated to 'app + middleware; mounts api_routers/
sub-apps' (no inline route bodies remain)
Extracts 12 endpoints from api.py into two new routers:
cyclone.api_routers.eligibility (2 ep):
POST /api/eligibility/request
POST /api/eligibility/parse-271
cyclone.api_routers.admin (10 ep):
GET /api/admin/audit-log
GET /api/admin/audit-log/verify
POST /api/admin/db/rotate-key
POST /api/admin/reload-config
POST /api/admin/scheduler/start
POST /api/admin/scheduler/stop
GET /api/admin/scheduler/status
POST /api/admin/scheduler/tick
GET /api/admin/scheduler/processed-files
Module-level _db_rotate_lock moved with the rotate-key endpoint.
8+ dead imports removed from api.py.
No behaviour changes; routers wire identically to the inline versions.
api.py: 1349 -> 949 lines (-400, ~64% reduction from start of refactor).
eligibility.py: 220 lines, admin.py: 332 lines.
Extracts 10 endpoints from api.py into two new routers:
cyclone.api_routers.inbox (6 ep):
GET /api/inbox/lanes
POST /api/inbox/candidates/{remit_id}/match
POST /api/inbox/candidates/dismiss
POST /api/inbox/payer-rejected/acknowledge
POST /api/inbox/rejected/resubmit
GET /api/inbox/export.csv
cyclone.api_routers.reconciliation (4 ep):
GET /api/reconciliation/unmatched
GET /api/batch-diff
POST /api/reconciliation/match
POST /api/reconciliation/unmatch
The two reconciliation/match endpoints delegate to store.manual_match
/ store.manual_unmatch; the router translates the store's exception
hierarchy (AlreadyMatchedError, InvalidStateError, NotMatchedError,
LookupError) into the HTTP error contract.
State-access refactor: endpoints that previously read app.state.dismissed_pairs
directly (lanes / dismiss / export.csv) now take an explicit
request: Request parameter and read request.app.state.dismissed_pairs
— the FastAPI-idiomatic pattern. No behavior change.
Dead-import cleanup: 8 imports left dangling in api.py after extraction
(csv, io, ClaimOutput, ParseResult, serialize_837,
serialize_837_for_resubmit, SerializeError837, Response) — all removed.
api.py: 1753 -> 1342 (-411). Net diff: +443 / -439.
Pytest: 8 failed / 735 passed / 16 skipped — identical to baseline.
62/62 inbox + reconciliation tests pass.
Live smoke: all 10 endpoints return expected status codes (200 for
reads, 400 for invalid bodies, 404 for missing ids).
Another router extraction landed: api_routers/remittances.py now owns
the three /api/remittances[...] routes (list, stream, detail). Pure
code-organization move from api.py — no new endpoints, no new
functionality.
Update the Project layout block to list remittances.py alongside the
other 9 routers with the routes it owns.
Extracts the 3 /api/remittances endpoints from api.py into
cyclone.api_routers.remittances:
GET /api/remittances (paginated list with filters)
GET /api/remittances/stream (NDJSON live-tail)
GET /api/remittances/{remit_id} (detail with CAS adjustments)
Mirror of the claims router for the 835 / Remittance resource.
No private helpers — every endpoint is a thin wrapper over
store.iter_remittances() / store.get_remittance().
remittances_stream is re-exported at the cyclone.api module level
so test_api_stream_live.py's direct import keeps working.
Route ordering preserved: /stream is declared before /{remit_id} in
the router source so FastAPI's first-match routing doesn't swallow
the literal 'stream' segment as a remittance id.
api.py: 1841 -> 1753 (-88). Net diff: +170 / -96.
Pytest: 8 failed / 735 passed / 16 skipped — identical to baseline.
11/11 remittance-targeted tests pass.
Live smoke: all 3 routes return expected status codes (200 for
list/stream, 404 for missing-remit detail).
Another router extraction landed: api_routers/claims.py now owns the
five /api/claims[...] routes (list, stream, detail, serialize-837,
line-reconciliation). Pure code-organization move from api.py — no
new endpoints, no new functionality.
Update the Project layout block to list claims.py alongside the other
8 routers with the routes it owns.
Extracts the 5 /api/claims endpoints from api.py into
cyclone.api_routers.claims:
GET /api/claims (paginated list)
GET /api/claims/stream (NDJSON live-tail)
GET /api/claims/{claim_id} (drawer detail)
GET /api/claims/{claim_id}/serialize-837 (X12 regen)
GET /api/claims/{claim_id}/line-reconciliation (per-line view)
Plus the two private projection helpers _claim_line_dict and
_svc_to_dict which are only used by line-reconciliation.
claims_stream is re-exported at the cyclone.api module level so
test_api_stream_live.py's direct import keeps working (same pattern
as the activity_stream re-export).
Route ordering preserved: /stream is declared before /{claim_id} in
the router source so FastAPI's first-match routing doesn't swallow
the literal 'stream' segment as a claim id.
api.py: 2201 -> 1841 (-360). Net diff: +432 / -389.
Pytest: 8 failed / 735 passed / 16 skipped — identical to baseline.
57/57 claims-targeted tests pass.
Live smoke: all 5 routes return expected status codes (200 for
list/stream, 404 for missing-claim detail/serialize/line-recon).
Four additional routers landed since the last doc pass:
- activity.py (GET /api/activity, /api/activity/stream)
- batches.py (GET /api/batches, /api/batches/{id}, /api/batch-diff)
- providers.py (GET /api/providers)
- clearhouse.py (GET /api/clearhouse, POST /api/clearhouse/submit)
- config.py (/api/config/providers[/...], /api/config/payers[/...],
POST /api/admin/reload-config)
Update the Project layout block to list all 8 routers with the routes
they own. No new functionality is introduced — the refactor is purely
a code-organization move from api.py into the api_routers/ subpackage.
No code changes; no tests touched.
Extracts GET /api/batches and GET /api/batches/{batch_id} from api.py
into cyclone.api_routers.batches. The _batch_summary_claim_count
helper moves along (only used by list_batches).
api.py: 2257 -> 2201 (-56). Net diff: +93 / -64.
Pytest: 8 failed / 735 passed / 16 skipped — identical to baseline.
Live smoke: /api/batches 200 JSON; /api/batches/{id} 404 on missing;
NDJSON form returns the expected summary envelope.
Extracts GET /api/activity and GET /api/activity/stream from api.py
into cyclone.api_routers.activity. The activity router is small and
self-contained: store.recent_activity() for the snapshot half,
tail_events() (from api_helpers) for the live tail.
activity_stream is re-exported at the cyclone.api module level so
test_api_stream_live.py's direct import keeps working.
api.py: 2310 -> 2257 (-53). Net diff: +110 / -61.
Pytest: 8 failed / 735 passed / 16 skipped — identical to baseline.
Live smoke: /api/activity 200 JSON; /api/activity/stream 200
application/x-ndjson with the expected snapshot lines.
Self-review nits from Checkpoint 2b:
- All three new router files lacked a trailing newline (same nit as 2a).
- clearhouse.py had lazy imports of serialize_837 / parse /
db as _db inside helper bodies. Hoisted serialize_837 and
parse to module-level (no import cycle risk: clearhouse.py does
not import from cyclone.api). Dropped the redundant db as _db
alias — the top-level from cyclone import db is already in scope.
No behavior change. test_clearhouse_api: 10 / 10 passing.
Checkpoint 2b. Extracts 7 more endpoints (1 + 2 + 4) into three
new routers:
- providers.py: GET /api/providers (list distinct providers from
claim rows; npi/state filter; NDJSON or paginated JSON).
- clearhouse.py: GET /api/clearhouse + POST /api/clearhouse/submit.
Carries the two serialize-from-raw helpers (_serialize_claim_for_submit,
_serialize_claim_from_raw) since they're only used here.
- config.py: GET /api/config/providers + GET /api/config/providers/{npi}
+ GET /api/config/payers + GET /api/config/payers/{payer_id}/configs.
Payer configs endpoint merges YAML-loaded blocks with any DB-overridden
live blocks per payer.
api.py shrank from 2474 to 2310 lines (-164) and no longer has any
single resource's full request/response shape — every remaining route
now lives in a dedicated router module.
Verifies:
- Full pytest: 8 failed / 735 passed / 16 skipped — identical to
clean main baseline (the 8 are pre-existing env/secret/sqlcipher).
- Live smoke: /api/health, /api/providers, /api/clearhouse,
/api/config/payers, /api/config/payers/co_medicaid/configs all 200.
/api/config/providers/{npi-not-seeded} returns 404 as expected.
See /tmp/refactor-cyclone.md for the full plan and progress.
The README's SP-specific endpoint reference blocks (SP3-SP15 in the
Roadmap) cover the per-SP additions, but the pre-existing core operator
surface was never documented as a single block. This pass adds the
missing endpoints:
- GET /api/batches, GET /api/batches/{batch_id} — batch list + detail.
- GET /api/batch-diff?a=<id>&b=<id> — side-by-side diff between two
batches (added/removed/changed claims + envelope metadata).
- GET /api/reconciliation/unmatched — every claim with no paired
remit and every remit with no paired claim; powers the
reconciliation review UI.
- POST /api/reconciliation/match — manual pair (claim_id, remit_id);
400/404/409 contract.
- POST /api/reconciliation/unmatch — remove a match and reset the
claim to 'submitted'.
- GET /api/providers — distinct providers from the parsed claim
stream. Distinct from /api/config/providers/{npi} (the SP9 config
table endpoint).
- GET /api/activity — recent activity events. Powers the Activity
page; the streaming counterpart /api/activity/stream is already
documented under 'Live updates'.
These 7 routes are referenced by the UI (BatchesList, BatchDetail,
BatchDiffView, Reconciliation page, Providers page, Activity page) but
were missing from the README's route inventory. The new section sits
between 'SFTP Wire-Up' and 'Persistence', with a one-paragraph pointer
to the per-SP endpoint reference blocks for SP-specific routes.
No code changes. No tests touched.
The README's most recent merge was SP13. Since then two more sub-projects
landed in main:
- SP14 (5c9365e + 8a65baa) — 5-lane Inbox UI with the Payer-Rejected
lane rendered alongside Rejected/Candidates/Unmatched/Done today,
and a bulk Acknowledge action that drops claims from the working
surface without erasing the original 277CA rejection event. New
endpoint POST /api/inbox/payer-rejected/acknowledge (idempotent,
audit-logged). Migration 0010.
- SP15 (47902fd + ab00909) — SQLCipher key rotation in place via
PRAGMA rekey. New endpoint POST /api/admin/db/rotate-key, serialized
through a module-level threading.Lock. Switches the SQLAlchemy
engine to NullPool when SQLCipher is enabled (QueuePool breaks
SQLCipher thread affinity under FastAPI's per-request threadpool).
Writes a db.key_rotated audit event with old + new key
fingerprints and post-rotation table_count.
What this PR does:
- Inbox section + Inbox endpoint table: document the Payer-Rejected
acknowledge action.
- Encryption at Rest: add a 'Key rotation' sub-section that explains
the rotate-key handler, the NullPool choice, the threading.Lock,
and the error code mapping (409/400/503).
- Tamper-Evident Audit Log: note that key rotations + Payer-Rejected
acknowledgements are part of the audit-logged event set.
- Project layout: add api_routers/ (health.py, acks.py, ta1_acks.py)
as the FastAPI APIRouter subpackage extracted from api.py.
- Roadmap: bump 'shipped 2-13' to 'shipped 2-15'; add SP14 and SP15
entries to the shipped list; add SP14 + SP15 endpoint reference
sections at the end.
Verification:
- pytest --collect-only collects 759 tests (up from 733 at the
previous doc pass).
- git diff --check clean.
- No code changes; no tests touched.