Resubmit {pendingResubmitIds.length} claims and download a 837 bundle?
)}
```
> **Aesthetic:** the existing inbox uses the Ticker Tape palette.
> Adapt the modal styling to match (`var(--tt-amber)` etc.). Not a
> focus of this task — the test asserts behavior, not pixels.
- [ ] **Step 6: Run, confirm PASS**
- [ ] **Step 7: Commit**
```bash
git add src/index.html src/lib/api.ts src/pages/Inbox.tsx src/pages/Inbox.test.tsx
git commit -m "feat(sp8): Inbox — resubmit bundle modal + JSZip download"
```
---
## Phase 5 — Prodfile round-trip + Docs
### Task 11: Prodfile round-trip smoke test
**Files:**
- Modify: `backend/tests/test_prodfiles_smoke.py`
- [ ] **Step 1: Add failing parametrized test**
Append to `backend/tests/test_prodfiles_smoke.py`:
```python
import pytest
from pathlib import Path
from cyclone.parsers.parse_837 import parse_837_text
from cyclone.parsers.serialize_837 import serialize_837
PRODFILES_DIR = Path("docs/prodfiles/claims")
@pytest.mark.parametrize(
"fixture_path",
sorted(PRODFILES_DIR.glob("*.x12")),
ids=lambda p: p.name,
)
def test_claims_prodfile_round_trip(fixture_path: Path):
"""Every prodfile in docs/prodfiles/claims/ round-trips through
serialize_837 → parse_837_text with deep-equal ClaimOutput (modulo
validation, which is recomputed by the parser)."""
text = fixture_path.read_text()
source = parse_837_text(text)
assert source.claims, f"{fixture_path.name}: no claims parsed"
for claim in source.claims:
out = serialize_837(claim)
reparsed = parse_837_text(out).claims[0]
# Compare canonical fields (not raw_segments — those are rebuilt).
assert reparsed.claim.claim_id == claim.claim.claim_id
assert reparsed.claim.total_charge == claim.claim.total_charge
assert reparsed.claim.frequency_code == claim.claim.frequency_code
assert reparsed.claim.place_of_service == claim.claim.place_of_service
assert [d.code for d in reparsed.diagnoses] == [d.code for d in claim.diagnoses]
assert len(reparsed.service_lines) == len(claim.service_lines)
```
- [ ] **Step 2: Run, confirm pass (or FAIL with diagnostics)**
```bash
cd backend && .venv/bin/pytest tests/test_prodfiles_smoke.py -v
```
Expected: as many tests as there are files in `docs/prodfiles/claims/`
(currently 113). One parametrized test, parametrized over the glob —
counts as 1 in the test suite's totals.
If failures occur: check the failing claim's structure (most likely a
segment the serializer doesn't handle yet — `K3` notes, conditional
segments, etc.). Document the gap in the plan and add the segment to
`_EDITABLE_SEGMENT_KINDS` or the stable pass-through as needed.
- [ ] **Step 3: Commit**
```bash
git add backend/tests/test_prodfiles_smoke.py
git commit -m "test(sp8): prodfile round-trip smoke — every claims/*.x12 serializes back"
```
---
### Task 12: README — Outbound 837 Serializer section
**Files:**
- Modify: `README.md`
- [ ] **Step 1: Add the section**
Insert after the existing "## Per-Line Adjustment Audit" section (around line 213):
```markdown
## Outbound 837 Serializer
Any parsed claim can be regenerated as a complete, round-trippable
X12 837P file. The serializer uses a hybrid approach: the envelope
(ISA/GS/ST/SE/GE/IEA) and editable claim-level segments (CLM, REF*G1,
HI, SV1, DTP*472) are freshly built from the canonical `ClaimOutput`
fields; the stable provider / subscriber / payer hierarchy segments
are passed through byte-identical from the parser's `raw_segments`.
### Where to find it
- **ClaimDrawerHeader → "Download 837"** — single-claim export to
`claim-{id}.x12`.
- **Inbox → Resubmit (multi)** — when resubmitting 2+ rejected claims,
a modal asks whether to also download an X12 bundle; on confirm,
the frontend zips the regenerated files in-browser (JSZip via CDN)
and triggers a download.
### Endpoints
| Method | Path | Returns |
|--------|------|---------|
| GET | `/api/claims/{id}/serialize-837` | `text/x12` attachment. 404 if claim missing. 422 if no raw_segments. |
| POST | `/api/inbox/rejected/resubmit?download=true` | existing JSON, plus `files: [{claim_id, filename, x12_text}]` array when `download=true`. |
### Edit propagation
After parse, edits to `claim.claim.{total_charge, frequency_code,
prior_auth}`, `claim.diagnoses[]`, or any `service_lines[i]` field
flow through to the regenerated X12 on the next call — the serializer
rebuilds those segments from the canonical fields rather than from
`raw_segments`. The stable hierarchies (billing provider, subscriber,
payer, submitter, receiver) come through verbatim from the original
file.
### Round-trip guarantee
Every file in `docs/prodfiles/claims/` (113 files at last count)
round-trips through `serialize_837` → `parse_837_text` with a
deep-equal `ClaimOutput` (modulo validation, which is recomputed).
This is enforced by a parametrized smoke test.
```
- [ ] **Step 2: Commit**
```bash
git add README.md
git commit -m "docs(sp8): README — Outbound 837 Serializer section"
```
---
### Task 13: Final smoke + full-suite run
**Files:** none
- [ ] **Step 1: Backend suite**
```bash
cd backend && .venv/bin/pytest -q
```
Expected: ≥ 528 backend passing (528 prior + 15 new — 9 serializer + 3
endpoint + 2 inbox + 1 prodfile). The prodfile test counts as 1 even
though it's parametrized over 113 files.
- [ ] **Step 2: Frontend suite**
```bash
cd .. && npx vitest run --reporter=dot
```
Expected: ≥ 342 frontend passing (342 prior + 2 new — 1 drawer button +
1 inbox modal).
- [ ] **Step 3: Typecheck**
```bash
npx tsc --noEmit
```
Expected: 0 errors.
- [ ] **Step 4: Manual curl smoke**
```bash
cd backend && CYCLONE_DB_URL=sqlite:///$(mktemp -d)/sp8-smoke.db .venv/bin/python -m cyclone serve &
curl -X POST -F "file=@tests/fixtures/co_medicaid_837p.txt" http://127.0.0.1:8000/api/parse-837
# Take the returned claim id, then:
curl -i http://127.0.0.1:8000/api/claims/