408 lines
14 KiB
Markdown
408 lines
14 KiB
Markdown
# Sub-project 3 — Backend EDI Features: Implementation Plan
|
|
|
|
**Date:** 2026-06-20
|
|
**Status:** Approved
|
|
**Branch:** `edi-features`
|
|
**Spec:** `docs/superpowers/specs/2026-06-20-cyclone-edi-features-design.md`
|
|
|
|
30 tasks across 5 phases. Follows the same task pattern as the SP2 plan (`[ ] Step 1 → Step N → commit`).
|
|
|
|
## Phase 1 — 837P extra validation rules
|
|
|
|
> **Note on rule IDs:** R032 and R033 are already used in `validator.py`
|
|
> for CLM05-2 facility code qualifier and CLM05-1 place of service code
|
|
> respectively. The two new rules in this phase use **R034** (REF*G1
|
|
> enforcement) and **R035** (BHT06 transaction type code).
|
|
|
|
### Task 1: REF*G1 enforcement rule (R034)
|
|
|
|
**Files:**
|
|
- Modify: `backend/src/cyclone/parsers/validator.py`
|
|
|
|
**Detection mechanism:** `ClaimOutput.raw_segments` is already populated
|
|
by the parser, so R034 scans `raw_segments` for any segment whose first
|
|
two elements are `["REF", "G1", ...]`. No new model field is required.
|
|
|
|
- [x] **Step 1: Add failing tests**
|
|
|
|
Append to `backend/tests/test_validator.py`:
|
|
|
|
```python
|
|
def test_r034_ref_g1_required_when_freq_7_no_ref_g1_emits_error(strict_ref_g1_payer):
|
|
# ... claim with frequency_code=7, no REF*G1 in raw_segments
|
|
# expect validation error
|
|
|
|
def test_r034_ref_g1_required_when_freq_8_no_ref_g1_emits_error(strict_ref_g1_payer):
|
|
# ... claim with frequency_code=8, no REF*G1 in raw_segments
|
|
# expect validation error
|
|
|
|
def test_r034_ref_g1_required_when_freq_7_with_ref_g1_passes(strict_ref_g1_payer):
|
|
# ... claim with frequency_code=7, REF*G1 present in raw_segments
|
|
# expect no error
|
|
|
|
def test_r034_ref_g1_required_when_freq_1_no_ref_g1_passes(strict_ref_g1_payer):
|
|
# ... claim with frequency_code=1, no REF*G1
|
|
# expect no error (rule only fires on 7/8)
|
|
```
|
|
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Implement `_r034_ref_g1_required` in `validator.py`**
|
|
- Scan `claim.raw_segments` for `["REF", "G1", *]`; only check if claim
|
|
frequency is 7 or 8; only emit if `cfg.require_ref_g1_for_adjustments`.
|
|
- [x] **Step 4: Register in `_RULES` list**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 2: BHT06 transaction-type-code rule (R035)
|
|
|
|
**Files:**
|
|
- Modify: `backend/src/cyclone/parsers/validator.py`
|
|
- Modify: `backend/src/cyclone/parsers/models.py` (add `ClaimOutput.transaction_type_code: str | None = None`)
|
|
- Modify: `backend/src/cyclone/parsers/parse_837.py` (populate `ClaimOutput.transaction_type_code` from BHT06 in the per-claim builder)
|
|
|
|
> **Why on ClaimOutput, not Envelope?** The current `ClaimOutput` shape
|
|
> already mirrors a few envelope fields (`control_number`,
|
|
> `transaction_date`) because the per-claim flow is easier when the
|
|
> validator only needs to look at one object. Following the same
|
|
> convention keeps R035's lookup one-liner clean and avoids threading
|
|
> the Envelope into every validation rule signature.
|
|
|
|
> **Note on naming:** BHT06 in the 837P 005010X222A1 implementation is the
|
|
> "Transaction Type Code" element (qualifier 640) — not the "Transaction
|
|
> Set Purpose Code" (which is BHT02, qualifier 353). The plan originally
|
|
> conflated these. Use `transaction_type_code` for the new `Envelope` field.
|
|
|
|
- [x] **Step 1: Add failing tests** — frequency_code=7/8 with no REF*G1 in raw_segments should NOT fire R034 (R034 is gated on `cfg.require_ref_g1_for_adjustments`, which is False for CO). Test must use a "strict" config.
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Add `transaction_type_code: str | None = None` to `Envelope` model**
|
|
- [x] **Step 4: Populate it in `_build_envelope` from `seg[6]` when present**
|
|
- [x] **Step 5: Implement `_r035_bht06_allowed` in `validator.py`**
|
|
- If `claim.envelope.transaction_type_code` is set and not in
|
|
`cfg.allowed_bht06`, emit error.
|
|
- [x] **Step 6: Register in `_RULES` list**
|
|
- [x] **Step 7: Commit**
|
|
|
|
### Task 3: Verify existing R031 (REF*G1 optional) keeps no-op behavior
|
|
|
|
**Files:**
|
|
- Verify: `backend/src/cyclone/parsers/validator.py:54-57`
|
|
|
|
- [x] **Step 1: Read current `_r031_ref_g1_optional`**
|
|
- [x] **Step 2: Confirm it stays as a no-op (returns empty tuple)**
|
|
- [x] **Step 3: (No commit — verification task)**
|
|
|
|
### Task 4: Wire `require_ref_g1_for_adjustments` into CO Medicaid factory
|
|
|
|
**Files:**
|
|
- Verify: `backend/src/cyclone/parsers/payer.py:67`
|
|
|
|
- [x] **Step 1: Confirm factory sets the flag for CO (currently `False` for v1 leniency)**
|
|
- [x] **Step 2: Run existing co_medicaid fixture through new R034**
|
|
- [x] **Step 3: Adjust fixture or factory if false positives appear**
|
|
- [x] **Step 4: Commit if any change**
|
|
|
|
### Task 5: Phase 1 commit + full suite
|
|
|
|
```bash
|
|
.venv/bin/pytest -q
|
|
```
|
|
Expected: 205 + 6 = ~211 pass.
|
|
|
|
---
|
|
|
|
## Phase 2 — 835 CAS deep-parsing (reason-code lookup)
|
|
|
|
### Task 6: `parsers/cas_codes.py` with CARC dict
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/parsers/cas_codes.py`
|
|
- Create: `backend/tests/test_cas_codes.py`
|
|
|
|
- [x] **Step 1: Add failing tests** for known/unknown lookup, count > 100
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Implement `reason_label` + `all_known_codes` with ~150 CARC codes**
|
|
- [x] **Step 4: Run tests to verify they pass**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 7: Extend `GET /api/remittances/{id}` to surface `adjustments`
|
|
|
|
**Files:**
|
|
- Modify: `backend/src/cyclone/store.py`
|
|
- Modify: `backend/src/cyclone/api.py`
|
|
- Modify: `backend/tests/test_api_gets.py`
|
|
|
|
- [x] **Step 1: Add failing tests** (empty adjustments returns `[]`, populated returns `{group, reason, label, amount, quantity}`)
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Add a `get_remit_with_adjustments` method to `CycloneStore`**
|
|
- [x] **Step 4: Update `to_ui_remittance` (or add `to_ui_remittance_detail`) to include adjustments array**
|
|
- [x] **Step 5: Update endpoint to use the new mapper**
|
|
- [x] **Step 6: Run tests to verify they pass**
|
|
- [x] **Step 7: Commit**
|
|
|
|
### Task 8: Frontend Remit type + Remittances page adjustments expansion
|
|
|
|
**Files:**
|
|
- Modify: `src/types/index.ts`
|
|
- Modify: `src/pages/Remittances.tsx`
|
|
|
|
- [x] **Step 1: Add `adjustments?: { group, reason, label, amount, quantity }[]` to `Remittance`**
|
|
- [x] **Step 2: Update Remittances page to render adjustments expansion**
|
|
- [x] **Step 3: Add test** in Remittances.test.tsx
|
|
- [x] **Step 4: Verify build + tests pass**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 9: Phase 2 commit + full suite
|
|
|
|
```bash
|
|
.venv/bin/pytest -q && npm test -- --run
|
|
```
|
|
Expected: ~215 backend, ~9 frontend.
|
|
|
|
---
|
|
|
|
## Phase 3 — 999 ACK transaction set
|
|
|
|
### Task 10: New Pydantic models for 999
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/parsers/models_999.py`
|
|
- Modify: `backend/src/cyclone/parsers/__init__.py`
|
|
|
|
- [x] **Step 1: Implement model classes per spec §4.1**
|
|
- [x] **Step 2: Wire lazy re-exports**
|
|
- [x] **Step 3: Verify import + simple instantiation works** (1 sanity test)
|
|
- [x] **Step 4: Commit**
|
|
|
|
### Task 11: 999 parser
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/parsers/parse_999.py`
|
|
|
|
- [x] **Step 1: Add failing tests** with minimal_999.txt fixture (basic happy path)
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Implement orchestrator** following `parse_835.py` pattern
|
|
- [x] **Step 4: Run tests to verify they pass**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 12: 999 serializer (outbound)
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/parsers/serialize_999.py`
|
|
|
|
- [x] **Step 1: Add failing round-trip test** (parse → serialize → parse → assert equal)
|
|
- [x] **Step 2: Run test to verify it fails**
|
|
- [x] **Step 3: Implement `serialize_999`** (ISA/GS/ST/AK1/[AK2/AK3/AK4/AK5*]/AK9/SE/GE/IEA)
|
|
- [x] **Step 4: Run test to verify it passes**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 13: `acks` table + ORM + migration
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/migrations/0002_acks.sql`
|
|
- Modify: `backend/src/cyclone/db.py`
|
|
|
|
- [x] **Step 1: Add migration SQL** per spec §4.4
|
|
- [x] **Step 2: Add ORM model `Ack` to `db.py`**
|
|
- [x] **Step 3: Add `CycloneStore.add_ack(...)` method**
|
|
- [x] **Step 4: Run migration on a fresh DB; verify table exists**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 14: `POST /api/parse-999` endpoint
|
|
|
|
**Files:**
|
|
- Modify: `backend/src/cyclone/api.py`
|
|
|
|
- [x] **Step 1: Add failing tests** (parse + persist, returns summary)
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Add endpoint** mirroring `parse-835` JSON path
|
|
- [x] **Step 4: Run tests to verify they pass**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 15: Auto-generate 999 on 837 ingest via `?ack=true`
|
|
|
|
**Files:**
|
|
- Modify: `backend/src/cyclone/api.py`
|
|
- Modify: `backend/tests/test_api_parse_persists.py`
|
|
|
|
- [x] **Step 1: Add 3 failing tests** (default no ack, ack=true happy path, ack=true with rejected claims)
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Add `ack: bool = Query(False)` param to `parse_837_endpoint`**
|
|
- [x] **Step 4: Build `BatchAckBuilder` that converts validation report to 999**
|
|
- [x] **Step 5: Persist `Ack` row, attach to response body**
|
|
- [x] **Step 6: Run tests to verify they pass**
|
|
- [x] **Step 7: Commit**
|
|
|
|
### Task 16: Frontend `/acks` page + sidebar entry
|
|
|
|
**Files:**
|
|
- Modify: `src/lib/api.ts`
|
|
- Modify: `src/hooks/` (new `useAcks.ts`)
|
|
- Modify: `src/types/index.ts`
|
|
- Create: `src/pages/Acks.tsx`
|
|
- Create: `src/pages/Acks.test.tsx`
|
|
- Modify: `src/components/Sidebar.tsx`
|
|
|
|
- [x] **Step 1: Add `Ack` type to `src/types/index.ts`**
|
|
- [x] **Step 2: Add `api.listAcks()` + `api.getAck(id)` to `src/lib/api.ts`**
|
|
- [x] **Step 3: Create `useAcks` hook**
|
|
- [x] **Step 4: Create `Acks.tsx` page** with empty state, populated list, download button
|
|
- [x] **Step 5: Add sidebar nav entry with `CheckCircle2` icon**
|
|
- [x] **Step 6: Add tests** (1-2 cases: empty + populated)
|
|
- [x] **Step 7: Verify typecheck + build + tests pass**
|
|
- [x] **Step 8: Commit**
|
|
|
|
### Task 17: Phase 3 commit + full suite
|
|
|
|
```bash
|
|
.venv/bin/pytest -q && npm test -- --run
|
|
```
|
|
Expected: ~225 backend, ~10 frontend.
|
|
|
|
---
|
|
|
|
## Phase 4 — 270/271 eligibility
|
|
|
|
### Task 18: 270 request models
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/parsers/models_270.py`
|
|
|
|
- [x] **Step 1: Implement model classes per spec §4.2**
|
|
- [x] **Step 2: Wire lazy re-exports**
|
|
- [x] **Step 3: Sanity test**
|
|
- [x] **Step 4: Commit**
|
|
|
|
### Task 19: 271 response models + service-type-code dict
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/parsers/models_271.py`
|
|
|
|
- [x] **Step 1: Implement model classes per spec §4.3**
|
|
- [x] **Step 2: Add inline `SERVICE_TYPE_CODES` dict** (~50 codes)
|
|
- [x] **Step 3: Add `service_type_description(code)` helper**
|
|
- [x] **Step 4: Sanity test**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 20: 270 request parser
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/parsers/parse_270.py`
|
|
|
|
- [x] **Step 1: Add failing tests** with minimal_270.txt fixture
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Implement orchestrator**
|
|
- [x] **Step 4: Run tests to verify they pass**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 21: 271 response parser
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/parsers/parse_271.py`
|
|
|
|
- [x] **Step 1: Add failing tests** with minimal_271.txt fixture (2 EB segments)
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Implement orchestrator** with EB segment handling
|
|
- [x] **Step 4: Run tests to verify they pass**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 22: 270 request serializer (outbound)
|
|
|
|
**Files:**
|
|
- Create: `backend/src/cyclone/parsers/serialize_270.py`
|
|
|
|
- [x] **Step 1: Add failing round-trip test**
|
|
- [x] **Step 2: Run test to verify it fails**
|
|
- [x] **Step 3: Implement `serialize_270`**
|
|
- [x] **Step 4: Run test to verify it passes**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 23: `POST /api/eligibility/request` endpoint
|
|
|
|
**Files:**
|
|
- Modify: `backend/src/cyclone/api.py`
|
|
- Create: `backend/tests/test_api_eligibility.py`
|
|
|
|
- [x] **Step 1: Add failing test**
|
|
- [x] **Step 2: Run test to verify it fails**
|
|
- [x] **Step 3: Implement endpoint**
|
|
- [x] **Step 4: Run test to verify it passes**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 24: `POST /api/eligibility/parse-271` endpoint
|
|
|
|
**Files:**
|
|
- Modify: `backend/src/cyclone/api.py`
|
|
|
|
- [x] **Step 1: Add 2 failing tests** (happy path + parse error)
|
|
- [x] **Step 2: Run tests to verify they fail**
|
|
- [x] **Step 3: Implement endpoint**
|
|
- [x] **Step 4: Run tests to verify they pass**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 25: Fixtures + cross-parser round-trip test
|
|
|
|
**Files:**
|
|
- Create: `backend/tests/fixtures/minimal_270.txt`
|
|
- Create: `backend/tests/fixtures/minimal_271.txt`
|
|
|
|
- [x] **Step 1: Write minimal_270.txt fixture** (small valid 270 request)
|
|
- [x] **Step 2: Write minimal_271.txt fixture** (small valid 271 response with 2 EB segments)
|
|
- [x] **Step 3: Add round-trip integration test** (parse 270 → serialize → parse → assert equal)
|
|
- [x] **Step 4: Verify tests pass**
|
|
- [x] **Step 5: Commit**
|
|
|
|
### Task 26: Phase 4 commit + full suite
|
|
|
|
```bash
|
|
.venv/bin/pytest -q
|
|
```
|
|
Expected: ~229 backend, ~10 frontend.
|
|
|
|
---
|
|
|
|
## Phase 5 — Docs + smoke
|
|
|
|
### Task 27: Spec doc already committed (T0)
|
|
|
|
Verify `docs/superpowers/specs/2026-06-20-cyclone-edi-features-design.md` is on branch.
|
|
|
|
### Task 28: Update root README
|
|
|
|
**Files:**
|
|
- Modify: `README.md`
|
|
|
|
- [x] **Step 1: Update Roadmap section** to mark SP3 shipped and describe 4 areas
|
|
- [x] **Step 2: Commit**
|
|
|
|
### Task 29: End-to-end smoke
|
|
|
|
- [x] **Step 1: Clean DB**, start server, parse 837 with `?ack=true`
|
|
- [x] **Step 2: Confirm 999 in response + `acks` row in DB**
|
|
- [x] **Step 3: Parse 999 fixture**, confirm endpoint round-trips
|
|
- [x] **Step 4: Build 270 request** via API, paste into parser
|
|
- [x] **Step 5: Commit empty** `smoke: end-to-end SP3 flow passes`
|
|
|
|
### Task 30: Final cleanup + merge worktree to main
|
|
|
|
- [x] **Step 1: Verify worktree is clean**
|
|
- [x] **Step 2: Discard main's stale uncommitted changes** (if any)
|
|
- [x] **Step 3: Fast-forward merge `edi-features` into `main`**
|
|
- [x] **Step 4: Remove worktree + delete branch**
|
|
|
|
---
|
|
|
|
## Test count targets
|
|
|
|
| Phase | New backend | New frontend | Running total |
|
|
|-------|-------------|--------------|---------------|
|
|
| Start | 205 | 8 | — |
|
|
| P1 end | +6 | 0 | 211 / 8 |
|
|
| P2 end | +4 | +1 | 215 / 9 |
|
|
| P3 end | +10 | +2 | 225 / 11 |
|
|
| P4 end | +4 | 0 | 229 / 11 |
|
|
|
|
## Risk reminders
|
|
|
|
- 270/271 scope creep — keep tight to EQ/EB surface.
|
|
- 999 auto-gen semantics — "rejected" = validation errors.
|
|
- CARC codes drift — `LAST_UPDATED` constant for tracking.
|
|
- Migration ordering — `0002_acks.sql` after `0001_initial.sql`, idempotent. |