90 lines
5.8 KiB
Markdown
90 lines
5.8 KiB
Markdown
# SP29 — Inbox 999-rejected claim drill: inline ack evidence + per-row Resubmit
|
|
|
|
> **Status:** Approved, 2026-07-02.
|
|
> **Branch:** `sp29-rejected-row-ack-drill`
|
|
> **Spec → plan → implement → atomic merge into main.**
|
|
|
|
## Goal
|
|
|
|
Make the Inbox `rejected` lane (the 999 envelope rejects — the resubmittable kind) **actionable per claim**: the operator can see why a claim was rejected (which 999 AK2 set_response, what code, what ST02) without opening the drawer, and they can resubmit a single rejected claim with one click instead of having to select its checkbox + open the bulk modal.
|
|
|
|
The SP28 → SP29 chain: SP28 gave us the `claim_acks` join table so we can now surface the 999 evidence inline on the rejected row. SP29 is the operator-facing surface that makes the rebill workflow usable.
|
|
|
|
## Scope
|
|
|
|
### Backend — extend `/api/inbox/lanes`
|
|
|
|
`backend/src/cyclone/inbox_lanes.py` (`compute_lanes`): for each `rejected`-lane claim row, attach `claim_acks: {total, rejected, items}` summarizing the 999 acks linked to that claim by SP28.
|
|
|
|
New field on `InboxClaimRow`:
|
|
|
|
```python
|
|
{
|
|
"total": int, # total claim_acks rows linked to this claim (ack_kind='999')
|
|
"rejected": int, # subset where set_accept_reject_code in ('R', 'E', 'X')
|
|
"items": [ # up to 5 most recent, newest first; for UI chip rendering
|
|
{"ack_id": int, "set_control_number": str, "set_accept_reject_code": str, "ak2_index": int, "linked_at": iso8601}
|
|
],
|
|
}
|
|
```
|
|
|
|
The field is `null` when the claim has zero linked 999 acks. Filtered to `ack_kind='999'` — the 277CA ack evidence already lives on the `payer_rejected_*` fields on `payer_rejected` rows (out of scope for this SP; not touched).
|
|
|
|
Implementation: one batched query inside `compute_lanes`:
|
|
|
|
```sql
|
|
SELECT claim_id, ack_id, set_control_number, set_accept_reject_code, ak2_index, linked_at
|
|
FROM claim_acks
|
|
WHERE claim_id IN (...) AND ack_kind = '999'
|
|
ORDER BY linked_at DESC, id DESC
|
|
```
|
|
|
|
(Indexes sufficient — `ix_claim_acks_claim_id` already exists from `0018_claim_acks.sql`. The `(claim_id, linked_at)` composite for sorted fetch is not added in v1; the LIMIT-by-claim is done in Python after the batched fetch. If latency grows on the inbox endpoint we add a `(claim_id, linked_at)` composite index in a follow-up SP.)
|
|
|
|
### Frontend — render + per-row Resubmit
|
|
|
|
**`src/lib/inbox-api.ts`** — extend `InboxClaimRow` with the new `claim_acks` field (additive, non-breaking).
|
|
|
|
**`src/components/inbox/InboxRow.tsx`** — add an inline sub-row for `rejected`-state claim rows showing up to 3 AK2 chips + a `+N more` chip that opens the ClaimDrawer. Reject codes (`R`/`E`/`X`) get the oxblood tint; accept (`A`) gets a muted check tint. When `claim_acks` is null or empty, render `999 not linked` in muted text.
|
|
|
|
**`src/components/inbox/InboxRow.tsx`** + **`src/pages/Inbox.tsx`** — a per-row `Resubmit` button at the right edge of each `rejected` claim row. Click triggers `serializeClaim837(row.id)` → `downloadTextFile(...)` (operator gets the corrected 837 on disk; they reconcile via their own workflow or via the existing bulk Resubmit modal).
|
|
|
|
**Auth posture:** per-row Resubmit is visible to `admin` and `user` roles. Mirrors the bulk Resubmit gating in `pages/Inbox.tsx:529-538`. Manual-only SP24 posture unchanged.
|
|
|
|
### Out of scope (deferred)
|
|
|
|
- `payer_rejected` lane (277CA STC A4/A6/A7) — distinct drill, owns the Acknowledge gesture.
|
|
- A missing-999 alarm surfacing (claims SUBMITTED >24h with zero 999 acks).
|
|
- Hoisting ack counts into the ClaimDrawer header.
|
|
- Orphan-ack triage bulk actions.
|
|
- Audit-log entry on single-claim resubmit (the bulk path doesn't audit either; closing that gap is its own SP).
|
|
|
|
## Decisions
|
|
|
|
- **D1 — per-row Resubmit is download-only (not state-flipping).** Mirrors the operator's actual workflow (download, fix in their editor, resubmit via the bulk path or the existing single-claim modal flow). Keeps v1 small. v2 can add the "submit + flip state" path if needed.
|
|
- **D2 — lane surface, not a new page.** The Inbox already has the `rejected` lane with chrome cost paid. Adding the per-row affordance inside the existing lane is lower-friction than a new drill page.
|
|
- **D3 — `claim_acks.items` is 5 most recent, sorted DESC.** Matches what the eye scans (newest rejections first). `total` and `rejected` summary counts cover the rest of the story. No pagination.
|
|
- **D4 — per-row Resubmit visible to `admin` AND `user` roles.** Mirrors the existing bulk Resubmit gating.
|
|
|
|
## Risks
|
|
|
|
- **N+1 fetch via the inbox endpoint.** Could grow `/api/inbox/lanes` latency by O(N-rejected) without a batched query. Mitigation: one batched query inside `compute_lanes` (`claim_id IN (...)`).
|
|
- **Backwards compatibility of `InboxClaimRow`.** Adding a nullable `claim_acks` field is non-breaking. Existing UI (and the CLAUDE.md-documented "5 lanes" assumption) continues to work; only the rejected row gets new chips.
|
|
- **Per-row Resubmit file may not match the operator's edits.** Out of scope; the operator is responsible for editing. The download is the same `serialize_837` content the bulk path produces. Trade-off documented in D1.
|
|
|
|
## Implementation shape
|
|
|
|
- Branch: `sp29-rejected-row-ack-drill`
|
|
- Plan path: `docs/superpowers/plans/2026-07-02-cyclone-999-rejected-drill.md`
|
|
- Commit prefixes: `feat(sp29): …`, `docs(spec): …`, `docs(plan): …`, `merge: SP29 …`
|
|
- PR title: `SP29 Inbox 999-rejected drill`
|
|
- Merge shape: single atomic merge, no squash, no rebase.
|
|
|
|
## File footprint (per phase)
|
|
|
|
- Backend modified (1): `backend/src/cyclone/inbox_lanes.py`.
|
|
- Backend test added (1): `backend/tests/test_inbox_lanes.py` (extend or new).
|
|
- Frontend modified (3): `src/lib/inbox-api.ts`, `src/components/inbox/InboxRow.tsx`, `src/pages/Inbox.tsx`.
|
|
- Frontend test added (2): `src/components/inbox/InboxRow.test.tsx`, `src/pages/Inbox.test.tsx`.
|
|
- Estimated 70-100 LOC total.
|