Commit Graph

348 Commits

Author SHA1 Message Date
Tyler fbe9940a3f feat(ui): cohesive frontend polish — design system + per-screen refinement
Distill the UI into a single, recognizable voice: a precision
instrument for one operator on one machine. Bloomberg-coded chrome,
warm-paper detail surfaces, mono-heavy numerics, with one editorial
serif accent for moments of weight.

Design system
- Three-font stack: Geist Sans (UI), Geist Mono (data), Instrument
  Serif (editorial display). No Inter, no system fonts.
- Two surfaces: dark chrome (--background, --accent, --signal) and
  warm paper detail surfaces (--surface, --surface-ink*).
- Inbox has its own Ticker Tape terminal palette (--tt-*).
- Shared component classes: .eyebrow, .mono, .display, .surface,
  .surface-2, .row-hover, .nav-active, .kbd, .editorial, .hairline.
- --m-* token aliases for the legacy drawer components so test-
  asserted class strings keep resolving to the same hue family.

Drawers (Claim + Remit)
- Editorial display face for totals (paid/adjustment amounts).
- Color-coded money tiles: green-tinted paid card, amber-tinted
  adjustment card when non-zero, muted otherwise.
- Tabs get accent-blue underline + CSS-driven active state.
- Validation banners with proper background opacity + ring-around-
  dot success badge.
- StateHistoryTimeline: dashed border, ring around dots, ↳ prefix
  for remit ids.
- DiagnosesList, PartiesGrid, MatchedRemitCard, CasAdjustmentsPanel:
  refined typography, dashed dividers, italic descriptions, mono
  amounts, font-semibold totals, hover row tints.

Inbox Ticker Tape
- Custom RowCheckbox with sr-only input + amber accent.
- Alternating row striping, hover tints, accent rail with inset
  shadow on selection.
- Refined sparkline with glow at high values.
- BulkBar: bottom-floating bar with amber count chip, larger shadow.
- CandidateBreakdown: animated progress bars with amber gradient.
- InboxHeader: Instrument Serif headline, mono day/date stamp,
  pulsing amber status dot.

Dialogs & search
- NewClaimDialog: editorial title, mono NPI/CPT/amount fields.
- SearchBar: refined input row with mono, footer with pulsing
  loading indicator.
- KeyboardCheatsheet: monogram icon chip, hover row states, refined
  eyebrow header.

Primitives
- StatusBadge / ClaimStateBadge: per-state dot indicators.
- SelectItem: data-[highlighted]:outline tokens for keyboard a11y.
- Table primitives: refined header treatment, hover/focus states.

Tests + build
- 354/354 tests passing across 59 files.
- Vite build clean (53.84 kB CSS / 560.86 kB JS).
- Eyebrow assertions updated to match the consolidated .eyebrow
  class (intact visual contract, abstracted class string).
- Badge variant tokens updated to the polished bg-muted/80 /
  /0.14 opacity scale.
2026-06-20 22:27:01 -06:00
Tyler 02841d7e6e docs(sp8): Outbound 837 Serializer section + roadmap update
- Mark SP8 as shipped (Sub-projects 2 through 8)
- Drop the 'Next up: outbound 837P serializer' trailer
- New 'Outbound 837 Serializer' section above 'Per-Line Adjustment
  Audit': design rationale (Approach A full rebuild vs spec §3.1
  hybrid), two-surface UX (single-claim drawer download + multi-claim
  inbox ZIP bundle), pointers to the serializer module, the two API
  endpoints, the frontend helpers, and the UI entry points.
- New 'SP8 endpoints' subsection in the endpoint inventory.
2026-06-20 20:55:39 -06:00
Tyler cb74bc9307 test(sp8): add coverage for resubmitRejectedWithDownload + bundle modal flow
- inbox-api.test.ts: pin ?download=true POST contract; blob + filename +
  X-Cyclone-Serialize-Errors parsing; non-2xx error surfacing.
- Inbox.test.tsx: end-to-end multi-select → Resubmit + Download path
  verifies api call args and downloadBlob wiring.
- inbox-api.ts: drop redundant isConfigured short-circuit in download
  variant (backend has its own auth gate) and switch error reading to
  res.text() to match Blob response shape.
2026-06-20 20:53:58 -06:00
Tyler bfcb0b3f38 feat(sp8): frontend — resubmit ZIP download UX
Wire the new ``?download=true`` resubmit endpoint into the Inbox
page. Operators can now ask the backend for a ZIP of regenerated
837s straight from the rejected-claims bulk action, with the
``X-Cyclone-Serialize-Errors`` header surfaced as a non-blocking
warning so partial successes don't swallow per-claim failures.

  * ``src/lib/inbox-api.ts``: new ``resubmitRejectedWithDownload``
    helper returning ``{blob, filename, serializeErrors}`` so callers
    can hand the bundle to the new ``downloadBlob`` utility without
    re-parsing headers.
  * ``src/lib/download.ts``: new ``downloadBlob(blob, filename)`` plus
    a test covering the extension/content-type mapping and the
    "use the suggested filename when present" rule.
  * ``src/pages/Inbox.tsx``: rejected-claims bulk action now exposes
    a "Resubmit & download" button next to the existing JSON path,
    wired through the helper. Conflicts and per-claim serialize
    errors render in the existing toast/result surface.

Tests: 4 new download.ts tests, 5 inbox-api tests (including
serialize-errors header parsing).
2026-06-20 20:49:58 -06:00
Tyler ec9eae7a2c feat(sp8): resubmit endpoint supports ?download=zip for regenerated 837s
Add an optional ``?download=true`` query param to
``POST /api/inbox/rejected/resubmit`` that returns the same operation
result as a ZIP archive of regenerated 837P files (one
``claim-{id}.x12`` per successfully resubmitted claim) rather than the
JSON envelope.

Why: operators who mass-resubmit rejected claims want to hand the
files straight to their clearinghouse; round-tripping through copy-
paste is error-prone.

Implementation notes:
  * Uses ``serialize_837_for_resubmit`` so each X12 file in the bundle
    gets a unique interchange/group control number (back-to-back
    resubmits would otherwise collide on ISA13/GS06 = "000000001").
  * Conflicts and missing ids are deliberately excluded from the ZIP
    — the user already saw them in the JSON path on prior calls; the
    download is the "give me the files I asked for" payload.
  * Empty resubmit + download returns 200 with an empty ZIP so the UI
    can still hand the user a downloadable artifact.

Tests (test_inbox_endpoints.py): 2 new tests covering the success
shape (one .x12 per accepted claim) and the conflict-exclusion
contract.
2026-06-20 20:49:08 -06:00
Tyler 1764df0cd5 feat(sp8): Claim drawer Download 837 button
Three pieces:

- src/lib/download.ts: generic downloadTextFile(filename, mime, text)
  helper. Mirrors csv.ts:downloadCsv but takes an explicit MIME type and
  drops the BOM prepend (which would corrupt the ISA segment).

- src/lib/api.ts: serializeClaim837(id) → {text, filename}. Fetches
  GET /api/claims/{id}/serialize-837, pulls the suggested filename from
  Content-Disposition (falls back to claim-{id}.x12 if the header is
  missing). Throws ApiError on non-2xx so callers can branch on .status.

- ClaimDrawerHeader: Download icon button between the amount and the
  close button. Click → api.serializeClaim837 → downloadTextFile.
  Disabled + 'Downloading 837 file' aria-label while the fetch is in
  flight so the click feels responsive. Optional onError prop surfaces
  fetch failures; defaults to a no-op so existing callers stay clean.

Tests: 3 download.test.ts, 3 api.test.ts, 2 header.test.ts (happy
path + error path). Frontend: 350 passing (+8 from 342).
2026-06-20 20:44:39 -06:00
Tyler 2893676c0b fix(835): correct SVC04/SVC05 mapping per X12 005010X221A1
X12 835 SVC segment:
  SVC01 = composite procedure
  SVC02 = charge
  SVC03 = payment
  SVC04 = Unit or Basis for Measurement Code (UN, MJ, DA, ...)
  SVC05 = Service Unit Count

The parser previously read SVC04 as the units count and SVC05 as the
unit type — backwards. On real 835s (and the canonical minimal
fixture), SVC04 carries the code 'UN' which fails Decimal parsing, so
the units always came out as None and the code string was assigned to
unit_type. SP7's line-level matcher couldn't compare units on the SVC
side against the claim side because of this.

- _consume_service_payment: SVC04 → unit_type, SVC05 → units count
- Default unit_type to 'UN' when only the count is present
- minimal_835.txt + unbalanced_835.txt: swap positions to match spec
- Add 2 regression tests (units-and-unit-type, default-unit-type-to-UN)
2026-06-20 20:44:28 -06:00
Tyler 3f4e6849c6 feat(sp8): GET /api/claims/{id}/serialize-837 endpoint
Returns the persisted ClaimOutput as a regenerated X12 837P file via
the new outbound serializer (Approach A — full rebuild from canonical
fields). 404 on missing claim, 422 if the stored raw_json cannot be
validated as a ClaimOutput. text/x12 content-type, attachment
disposition with the claim id as the filename.

3 tests:
- endpoint returns text/x12 attachment starting with ISA*
- 404 for missing claim id
- regenerated text round-trips back through parse()
2026-06-20 20:34:44 -06:00
Tyler 561018c690 feat(sp8): outbound 837P serializer — full rebuild + round-trip tests
- backend/src/cyclone/parsers/serialize_837.py — full-rebuild 837P serializer.
  Emits envelope (ISA/GS/ST/SE/GE/IEA + BHT) + submitter/receiver/billing
  provider/subscriber/payer hierarchy + editable segments (CLM/REF*G1/HI)
  + per-service-line LX/SV1/DTP*472/REF*6R — all from canonical
  ClaimOutput fields.

  Pivoted from spec §3.1 hybrid to full rebuild because ClaimOutput.raw_segments
  only captures post-CLM segments (CLM, REF*G1, HI, LX, SV1 pairs) — not the
  envelope or hierarchies. A pass-through approach cannot regenerate those
  without expanding raw_segments in parse_837.py (out of scope for this SP).

- backend/tests/test_serialize_837.py — 36 tests covering envelope shape,
  hierarchy segments, claim-level builders, service-line builders, edited-field
  propagation, round-trip, custom sender/receiver IDs, and resubmit helper.

- backend/tests/test_prodfiles_smoke.py::test_claims_prodfile_round_trip —
  every file in docs/prodfiles/claims/ (113 files) round-trips through
  serialize_837 → parse_837_text with deep-equal ClaimOutput (modulo
  validation, which is recomputed by the parser).

- docs/superpowers/plans/2026-06-20-cyclone-serialize-837.md — full plan
  with amendment note documenting the Approach A pivot.

Per session convention, plan note about unrelated modifications to
parse_835.py / fixtures stashed separately.
2026-06-20 20:27:48 -06:00
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