689 Commits

Author SHA1 Message Date
Tyler b3de9c4d22 fix(sp7): mark ClaimDetail.lineReconciliation optional for back-compat
Existing test fixtures (ClaimDrawer, ClaimDrawerHeader, Claims pages)
construct partial ClaimDetail objects without the new slim
lineReconciliation projection. Other SP7 fields follow the optional
pattern (matchedLines?, serviceLinePayments?, claimLevelAdjustments?),
so apply the same to lineReconciliation.

Backend always emits it for matched claims; UI treats absent as
"no per-line audit yet".
2026-06-20 20:16:07 -06:00
Tyler d033ce85db feat(sp7): wire _reconcile_pair into manual_match (T21)
manual_match previously only flipped the claim↔remit FK and the
claim state — it never ran line-level reconciliation, so manually-
paired claims surfaced empty line-reconciliation rows to the UI
and skipped CLP-level CAS aggregate recompute.

Refactor reconcile.run() to call a new per-pair helper
_reconcile_pair(session, claim, remittance) that:
  - clears any existing LineReconciliation rows for the claim
    (idempotent re-run; safe across manual_unmatch + manual_rematch
    cycles that may pair the claim with a different remittance),
  - reads 837 SV1 lines from Claim.raw_json and 835 SVC rows from
    ServiceLinePayment,
  - runs match_service_lines() and persists a LineReconciliation row
    per side,
  - recomputes Remittance.claim_level_adjustment_amount (CLP-level
    CAS) and Remittance.adjustment_amount (total CAS).

manual_match now calls this helper after the FK is set and before
commit, mirroring the auto-match path. Reversals are skipped (they
don't have SV1↔SVC line pairs; per §7.3).

Tests (test_store_reconcile.py):
  - test_manual_match_populates_line_reconciliation_rows: end-to-end
    check that a manual pair writes the expected matched +
    unmatched_837_only rows plus zero CAS aggregates.
  - test_manual_match_idempotent_line_reconciliation: after
    manual_unmatch + manual_rematch + inserting a CLP-level CAS row
    directly, the claim has exactly two fresh LineReconciliation rows
    (no duplicates) and the remittance aggregate reflects the new CAS.

Smoke tested end-to-end via TestClient: parse co_medicaid_837p.txt
+ co_medicaid_835.txt, auto-matcher skips (PCNs differ), manual
match via POST /api/reconciliation/match, then
GET /api/claims/{id}/line-reconciliation returns 2 rows and
GET /api/inbox/lanes shows matched_remittance.total_lines=2.

Note: matched_lines may be 0 on real 835 fixtures due to a
pre-existing parser bug in _consume_service_payment that swaps
SVC04 (units) and SVC05 (unit-of-measure) when reading units from
the segment. The SP7 strict-match criterion requires units parity,
so the misread produces None on the SVC side and strict-match
never succeeds. Surfaced as a follow-up; the SP7 work itself is
correct (line counts are computed and persisted).
2026-06-20 20:13:16 -06:00
Tyler 27c1680830 docs: refresh README roadmap (SP4/6/7 shipped, add Next up) + commit SP6 plan
- 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.
2026-06-20 19:58:26 -06:00
Tyler ceeb8b664d docs(sp7): README — Per-Line Adjustment Audit section 2026-06-20 19:51:32 -06:00
Tyler 0b8f253186 feat(sp7): wire LineReconciliationTab into ClaimDrawer 2026-06-20 19:51:04 -06:00
Tyler 2569ff99a0 feat(sp7): LineReconciliationTab component 2026-06-20 19:47:49 -06:00
Tyler b538c0939b feat(sp7): MatchedRemitCard — line-count badge 2026-06-20 19:47:11 -06:00
Tyler 00cdd068fc feat(sp7): CasAdjustmentsPanel — per-line + claim-level sections 2026-06-20 19:46:30 -06:00
Tyler 77f61b675d feat(sp7): ServiceLinesTable — Paid + Adjustments columns 2026-06-20 19:45:35 -06:00
Tyler 2432ca0ec4 feat(sp7): inbox API types include matched/total line counts 2026-06-20 19:44:45 -06:00
Tyler 391a07f877 feat(sp7): add per-line reconciliation + matched-remittance types 2026-06-20 19:44:23 -06:00
Tyler b7bda0662f feat(sp7): inbox lanes matched_remittance includes matched/total line counts 2026-06-20 19:43:43 -06:00
Tyler 6ef59b5d1d feat(sp7): remit detail includes serviceLinePayments + claimLevelAdjustments 2026-06-20 19:39:45 -06:00
Tyler 217f14a52a feat(sp7): claim detail includes lineReconciliation slim projection 2026-06-20 19:37:38 -06:00
Tyler 48d4739864 feat(sp7): GET /api/claims/{id}/line-reconciliation endpoint 2026-06-20 19:35:17 -06:00
Tyler c1323c44d0 feat(sp7): wire match_service_lines into reconcile.run 2026-06-20 19:33:02 -06:00
Tyler 9626eae5f1 feat(sp7): match_service_lines() pure function + tests 2026-06-20 19:29:21 -06:00
Tyler 8442bf91ce feat(sp7): persist ServiceLinePayment + link CAS rows on 835 ingest 2026-06-20 19:27:24 -06:00
Tyler 0dd6d39424 test(sp7): update migration version assertion to 6 (SP7 + TA1 WIP) 2026-06-20 19:25:26 -06:00
Tyler e8b452d255 feat(sp7): migration 0006 — service_line_payments + line_reconciliations + FKs
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.
2026-06-20 19:23:14 -06:00
Tyler 1feeff94b9 feat(sp7): LineReconciliation ORM model 2026-06-20 19:19:56 -06:00
Tyler 4c7b82f05b feat(sp7): ServiceLinePayment ORM model 2026-06-20 19:19:48 -06:00
Tyler e4743e41b1 feat(sp7): migration 0005 — service_line_payments + line_reconciliations + FKs 2026-06-20 19:19:34 -06:00
Tyler e35c59bd6e docs(sp7): SP7 implementation plan — 21 tasks across 6 phases 2026-06-20 19:19:26 -06:00
Tyler c43160927b docs(sp7): SP7 design spec — per-service-line adjustment audit 2026-06-20 19:15:55 -06:00
Tyler a58f613bcd docs(sp7): outbound 837P serializer design spec
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.
2026-06-20 19:12:34 -06:00
Tyler 76278ec9f6 feat(ta1): TA1 interchange ACK parser, persistence, and API
Adds full TA1 (Interchange Acknowledgment, X12 envelope-level) support
to mirror the existing 999 transaction-set ACK pipeline.

Parser & models
- parsers/models_ta1.py: Pydantic Ta1Ack + ParseResultTa1 models
  (AckCode = Literal['A','E','R'], date serializer mirroring 999)
- parsers/parse_ta1.py: TA1 segment parser
  - Tolerant YYMMDD (6-digit) + CCYYMMDD (8-digit) date handling
    for CO Medicaid interchange_date / ack_generated_date
  - 106-char ISA validation (15-char sender/receiver IDs)
  - source_batch_id = 'TA1-<ISA13>'

Persistence
- migrations/0005_create_ta1_acks.sql: ta1_acks table + indexes
  on source_batch_id and ack_code
- db.py: Ta1Ack ORM model (flat columns + raw_json, mirrors Ack)
- store.py: add_ta1_ack, list_ta1_acks (returns all rows),
  get_ta1_ack
- db version bumped 4 -> 5 (test_acks.py updated)

API (api.py)
- POST /api/parse-ta1: text/file ingest, persists ta1_ack,
  returns detail-ready payload
- GET /api/ta1-acks: list with limit + total (mirrors 999 pattern)
- GET /api/ta1-acks/{ack_id}: detail
- _ta1_to_ui / _serialize_ta1 / _serialize_ta1_from_row helpers

Tests (17 new, 508 total passing)
- tests/test_parse_ta1.py (8): CCYYMMDD + YYMMDD acceptance,
  E/R codes, source_batch_id, missing ISA/TA1, short defaults
- tests/test_api_ta1.py (9): happy path, rejected persists,
  empty/malformed 400, missing file 422, detail regenerates
  segment, 404, empty list, newest-first
- tests/test_prodfiles_smoke.py: extended to smoke-test 352
  production TA1 files (A=0, R=352, E=0)
- tests/test_api_parse_persists.py: cross-pipeline reconciliation
  test asserting invariants across 837P + 835 prod files

Real-data finding: all 352 production TA1s are R (rejected);
operator follow-up warranted.
2026-06-20 18:58:56 -06:00
Tyler 1db33ef2fa docs(sp6): README — Inbox section 2026-06-20 18:46:24 -06:00
Tyler fb8eba9bba feat(sp6): useInboxLanes refetches on SP5 tail events 2026-06-20 18:46:08 -06:00
Tyler 3157c86340 feat(sp6): bulk action bar wired into lanes 2026-06-20 18:43:51 -06:00
Tyler b607e4c459 feat(sp6): Inbox page + useInboxLanes hook 2026-06-20 18:41:44 -06:00
Tyler 610e580844 feat(sp6): InboxHeader + CandidateBreakdown 2026-06-20 18:41:07 -06:00
Tyler 5f3339ac1f feat(sp6): Lane component with multi-select 2026-06-20 18:40:34 -06:00
Tyler 713f0932b2 feat(sp6): InboxRow with Ticker Tape accents 2026-06-20 18:38:52 -06:00
Tyler ab841653d4 feat(sp6): /inbox route + sidebar nav link 2026-06-20 18:37:24 -06:00
Tyler bc34d991a9 feat(sp6): Ticker Tape color tokens for Inbox 2026-06-20 18:36:38 -06:00
Tyler f3e5d800e1 feat(sp6): inbox api client + types 2026-06-20 18:36:14 -06:00
Tyler f4baceb574 feat(sp6): inbox endpoints — lanes, match, dismiss, resubmit, csv export
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.
2026-06-20 18:34:09 -06:00
Tyler cdffb3faf3 feat(sp6): compute_lanes — 4 lanes on read with scoring 2026-06-20 18:31:01 -06:00
Tyler 3432c76561 feat(sp6): 4-field weighted scoring module 2026-06-20 18:30:05 -06:00
Tyler f4910fd94d feat(sp6): wire 999 ingest → claim rejected state
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.
2026-06-20 18:29:28 -06:00
Tyler 78ed75a31d feat(sp6): apply_999_rejections state helper
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.
2026-06-20 18:25:25 -06:00
Tyler 1b1534d8d2 feat(sp6): migration 0004 — rejection columns + state-history index
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.
2026-06-20 18:21:29 -06:00
Tyler 756196e2a8 feat(sp6): add REJECTED to ClaimState enum
T1 of the SP6 workflow automation plan. Paired with the existing test
in test_db_models.py that asserts 8 ClaimState values.
2026-06-20 18:15:38 -06:00
Tyler de09959377 test(835): update prodfile test for PCN dedup behavior
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
2026-06-20 18:12:59 -06:00
Tyler b5e27927e0 fix(835): drop over-constraining UNIQUE constraints, add multi-BPR warning
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.
2026-06-20 18:10:07 -06:00
Tyler 66da69baa0 build(deps): regenerate uv.lock for SP5 pytest-asyncio
SP5 (commit e8dc8c1) added pytest-asyncio to pyproject.toml but didn't
regenerate uv.lock. This brings the lockfile in sync.
2026-06-20 17:51:13 -06:00
Tyler 6ee87174d3 fix(activity): tolerate unknown activity kinds in ActivityFeed
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).
2026-06-20 17:46:00 -06:00
Tyler f4a7cd99b3 merge: ui/batch-diff 2026-06-20 17:30:30 -06:00
Tyler 5baa0290f0 merge: ui/remit-drawer 2026-06-20 17:30:09 -06:00