- Mark SP4 as fully shipped (batch diff, search, CSV, a11y all landed)
- Add SP6 (Inbox) and SP7 (Per-line reconciliation) sections
- Add SP6 + SP7 endpoint inventories
- Note next up: outbound 837P serializer
The workflow-automation plan was authored but never committed; the
features it specified have shipped, so commit it for the historical
record alongside this README refresh.
Migration renumbered from 0005 to 0006 because 0005_create_ta1_acks.sql
(another agent's WIP, unmerged) already claims version 5. Bumping our
migration to version 6 avoids stepping on it; user_version will land
at 6 once both migrations run.
Sub-project 7 adds serialize_837.py so a parsed ClaimOutput can be
regenerated as a complete X12 837P file (the shape of
docs/prodfiles/claims/). Unblocks the SP6 resubmit lane end-to-end
and adds a 'Download 837' affordance on the claim drawer.
Approach: hybrid (fresh envelope + selective body rebuild). Stable
segments (provider/subscriber/payer hierarchies) pass through from
claim.raw_segments; editable segments (CLM, REF*G1, HI, SV1, DTP*472)
are rebuilt from canonical ClaimOutput fields so post-parse edits
propagate to the output.
Out of scope: 997 ACK, claim <-> TA1/999 linking, in-app claim
editing UI, multi-CLM envelope batching, Inbox/drawer redesign.
T7-T10 combined (single edit: all endpoints share a section).
- GET /api/inbox/lanes — four lanes in one call
- POST /api/inbox/candidates/{remit_id}/match — 409 on conflict
- POST /api/inbox/candidates/dismiss — session-scoped dismissed set
- POST /api/inbox/rejected/resubmit — 200 with conflicts list
- GET /api/inbox/export.csv?lane=<lane> — streams CSV
Also adds module-level imports (db, Claim, ClaimState, Remittance,
csv, io, datetime) that the new endpoints need; cleans up the
duplicated local imports in the parse-999 SP6 T4 block.
T4 — calls apply_999_rejections inside /api/parse-999, then
publishes a claim.rejected event for each transitioned claim so
the Inbox live-tail refetches.
T3 — moves claims to ClaimState.REJECTED on 999 AK5 R/E/X.
- New module: src/cyclone/inbox_state.py
- New tests: tests/test_inbox_state.py (4 cases)
- Adds ORM mappings for rejection_reason / rejected_at / resubmit_count
/ state_changed_at on Claim (db.py) — these were added to the
schema by migration 0004 but not yet exposed to the ORM.
Adds:
- claims.rejection_reason, claims.rejected_at, claims.resubmit_count
- claims.state_changed_at (was missing, needed for Done-today lane)
- ix_claims_state_changed_at composite index
Also fixes the ClaimState count assertion in test_db_models.py
(7 → 8) to match the REJECTED enum value added in the previous
commit, and bumps the user_version expectation in test_acks.py
(3 → 4) for the same reason.
The store dedupes remittances by PCN. Refine the assertions:
- total_clps counts the raw CLP segments across all files (3374)
- unique_pcns counts distinct PCNs (persisted row count)
- Read claimId via the API response shape, not the SQLAlchemy attribute
Three related changes for real CO Medicaid data:
1. Drop UNIQUE(batch_id, patient_control_number) on claims and
UNIQUE(batch_id, payer_claim_control_number) on remittances. The X12
spec allows multiple CLM segments per 2000B subscriber loop and 835
ERAs can repeat a payer_claim_control_number for reversals. Claim/
remittance identity is provided by the primary key (claims.id = CLM01,
remittances.id = CLP01).
2. Add validator rule R835_MULTI_BPR warning for files with multiple BPR
segments (CO Medicaid split-payment pattern). The parser already sums
BPR02 paid_amounts; this surfaces the non-standard data to operators.
3. Skip R835_BAL_BPR_vs_CLP04 when BPR01='I' (Information Only 835).
In that mode BPR02 is informational and the per-claim CLP04 totals
are authoritative — a diff is expected, not an error.
Migration 0003 handles the drop with IF EXISTS so fresh DBs skip cleanly.
Updates affected tests to reflect new schema (no UNIQUE constraint on
batch_id + patient_control_number / payer_claim_control_number).
Fixes test_api_835::test_prodfile_round_trip_persists_separately which
was failing on real production data.
The live-tail stream (now reachable through the new Vite /api proxy)
started delivering activity rows with kind="manual_match", a kind the
ActivityFeed kindConfig map didn't know about. The map lookup returned
undefined, and reading .icon on it threw — taking the whole Activity
Log page down.
Two fixes:
1. Add "manual_match" to the ActivityKind union in src/types/index.ts
and to the kindConfig map in src/components/ActivityFeed.tsx (with
a Wrench icon + neutral tone), so the kind is properly typed and
rendered.
2. Add a FALLBACK_KIND guard so any future backend-emitted kind that
arrives before the frontend is updated no longer crashes the page.
kindConfig stays exhaustive for known kinds, so TypeScript still
catches missing entries at compile time.
Also adds src/components/ActivityFeed.test.tsx with three regression
tests (empty state, multi-item render, unknown-kind safety).