docs: surface SP14 (Payer-Rejected UI + acknowledge) + SP15 (key rotation) #3

Closed
tyler wants to merge 14 commits from docs/sp14-15-readme-sync into main

14 Commits

Author SHA1 Message Date
Tyler 81aebf54ed docs: SP16 Inbound MFT Scheduler section + project-layout updates
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)
2026-06-21 09:16:14 -06:00
Tyler 3b5e2af077 refactor(api): split eligibility + admin routers
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.
2026-06-21 09:03:46 -06:00
Tyler fc73075ef9 refactor(api): split inbox + reconciliation routers
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).
2026-06-21 08:34:24 -06:00
Tyler 27181144f2 docs: list remittances.py in api_routers/ Project layout
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.
2026-06-21 07:06:08 -06:00
Tyler eb674f890f refactor(api): split remittances router (list + stream + detail)
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).
2026-06-21 06:29:01 -06:00
Tyler 804e557a49 docs: list claims.py in api_routers/ Project layout
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.
2026-06-21 05:05:20 -06:00
Tyler ff43f90156 refactor(api): split claims router (list + stream + detail + serialize + line-recon)
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).
2026-06-21 04:28:11 -06:00
Tyler ea64e6e0f0 docs: list all 8 api_routers/ subpackages in Project layout
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.
2026-06-21 03:05:52 -06:00
Tyler 6ce638553b refactor(api): split batches router (list summary + detail)
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.
2026-06-21 02:45:47 -06:00
Tyler e63be87ba9 refactor(api): split activity router (list + live-tail stream)
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.
2026-06-21 02:41:20 -06:00
Tyler 6aa440d3e4 refactor(api): trailing newlines + hoist clearhouse.py parser imports
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.
2026-06-21 02:30:11 -06:00
Tyler 45cafbdb32 refactor(api): split providers + clearhouse + config routers
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.
2026-06-21 02:28:56 -06:00
Tyler bbf89c9dd8 docs: add Batches, Reconciliation, and Activity endpoint reference
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.
2026-06-21 02:07:27 -06:00
Tyler d25f00ac58 docs: surface SP14 (Payer-Rejected UI + acknowledge) + SP15 (key rotation)
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.
2026-06-21 01:07:44 -06:00