cascade: show all doc gaps; export: add order_number, hcpcs, recommended_next_step_code, diagnosis/pecos; supabase: confirm pro plan

- doc_state_machine: remove early returns from _build_cascade, surface all 5 doc checks, pecos gated to medicare/MA, pa denied routes to appeal alert
- main.py: add _recommended_next_step_code(), extend RecordOut and export CSV with new columns
- normalizer: accept order_number and hcpcs as pass-through fields
- coverage_calculator: add order_number/hcpcs to ShipmentRecord
- CLAUDE.md: supabase pro plan confirmed 2026-06-10

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Kisa 2026-06-10 22:53:22 -04:00
parent 63093990d2
commit ff657f4765
5 changed files with 85 additions and 10 deletions

View file

@ -118,7 +118,7 @@ This checklist tracks compliance gates separately from build readiness. Update i
|---|---|
| Privacy Policy published at stable public URL | PASS — privacy-policy.md written 2026-06-07; Privacy.jsx deployed to https://signal-ui-xi.vercel.app/privacy |
| FDA CDS exemption memo written and attorney-reviewed | NOT DONE |
| Supabase plan tier confirmed (Pro vs Team) and PHI posture documented | NOT VERIFIED |
| Supabase plan tier confirmed (Pro vs Team) and PHI posture documented | PASS — Pro plan confirmed 2026-06-10 ($25/mo). HIPAA available as paid add-on on Team ($599/mo) — deferred to post-funding per Real PHI gate. Pro is correct posture for pilot. |
| Pilot LOI reviewed by attorney | PARTIAL — template ready; Bittinger/Nixon review pending |
| NDA template reviewed by attorney | PARTIAL — template ready; Bittinger/Nixon review pending |
| Pilot LOI executed with supplier | NOT DONE |

View file

@ -167,6 +167,9 @@ class RecordOut(BaseModel):
is_transfer: bool = False
doc_state: Optional[DocStateOut] = None
cascade: list[str] = []
order_number: Optional[str] = None
hcpcs: Optional[str] = None
recommended_next_step_code: str = "NO_ACTION_NEEDED"
class UploadResponse(BaseModel):
@ -226,6 +229,24 @@ def _normalize_payer_type(payer: str) -> str:
return normalized
def _recommended_next_step_code(flag_val: str, doc: Optional[DocStateOut]) -> str:
if doc and doc.pa == "Denied":
return "PA_APPEAL"
if doc and doc.pa == "Required — Not Started":
return "INITIATE_PA"
if doc and "Not Confirmed" in (doc.visit or ""):
return "AWAITING_VISIT_CONFIRMATION"
if doc and doc.swo in ("Pending", "Expired"):
return "REQUEST_SWO_UPDATE"
if doc and doc.pecos == "Not Verified":
return "VERIFY_PRESCRIBER_PECOS"
if doc and doc.pa == "Pending":
return "PA_FOLLOW_UP"
if flag_val in ("RESUPPLY_READY", "ACTIVE") and (not doc or not doc.cascade if hasattr(doc, "cascade") else True):
return "READY_TO_BILL"
return "NO_ACTION_NEEDED"
def _to_record_out(r, record=None) -> RecordOut:
flag_val = r.flag.value if hasattr(r.flag, "value") else str(r.flag)
@ -269,6 +290,9 @@ def _to_record_out(r, record=None) -> RecordOut:
is_transfer=getattr(r, "is_transfer", False),
doc_state=doc_state_out,
cascade=cascade,
order_number=getattr(record, "order_number", None) if record else None,
hcpcs=getattr(record, "hcpcs", None) if record else None,
recommended_next_step_code=_recommended_next_step_code(flag_val, doc_state_out),
)
@ -393,10 +417,13 @@ async def export_work_queue(
output = io.StringIO()
writer = csv.writer(output)
writer.writerow([
"Order Number",
"HCPCS",
"Patient ID",
"Device",
"Payer",
"Status",
"Recommended Next Step",
"Priority Score",
"Days Until Resupply End",
"Next Visit Due",
@ -407,14 +434,19 @@ async def export_work_queue(
"SWO Status",
"Visit Status",
"PA Status",
"Diagnosis Status",
"PECOS Status",
])
for r in records:
doc = r.doc_state
writer.writerow([
r.order_number or "",
r.hcpcs or "",
r.patient_id,
r.device_display,
r.payer,
r.status_label,
r.recommended_next_step_code,
r.priority_score,
r.days_until_coverage_end,
r.next_visit_due_date or "",
@ -425,6 +457,8 @@ async def export_work_queue(
doc.swo if doc else "",
doc.visit if doc else "",
doc.pa if doc else "",
doc.diagnosis if doc else "",
doc.pecos if doc else "",
])
output.seek(0)

View file

@ -76,6 +76,15 @@ HEADER_MAP: dict[str, list[str]] = {
"transfer_from", "transfer from", "previous_supplier", "previous supplier",
"prior_supplier", "prior supplier", "transfer_status", "transferred_from",
],
"order_number": [
"order_number", "order number", "order_no", "order no", "order#",
"claim_number", "claim number", "brightree_order", "order_id",
"rx_number", "rx number", "rx#", "dispense_number",
],
"hcpcs": [
"hcpcs", "hcpcs_code", "hcpcs code", "procedure_code", "procedure code",
"billing_code", "billing code", "item_code", "item code", "hcpc",
],
}
DEVICE_MAP: dict[str, str] = {
@ -284,6 +293,8 @@ def normalize_csv(text: str) -> tuple[list[ShipmentRecord], list[str], dict]:
csv_diagnosis_on_file = mapped.get("csv_diagnosis_on_file") or None
transfer_raw = mapped.get("csv_transfer_from", "").strip()
csv_transfer_from = transfer_raw if transfer_raw else None
order_number = mapped.get("order_number") or None
hcpcs = mapped.get("hcpcs") or None
records.append(ShipmentRecord(
patient_id=patient_id,
@ -298,6 +309,8 @@ def normalize_csv(text: str) -> tuple[list[ShipmentRecord], list[str], dict]:
csv_pa_status=csv_pa_status,
csv_diagnosis_on_file=csv_diagnosis_on_file,
csv_transfer_from=csv_transfer_from,
order_number=order_number,
hcpcs=hcpcs,
))
return records, skipped, mapping_summary

View file

@ -74,6 +74,8 @@ class ShipmentRecord:
csv_pa_status: Optional[str] = None # "Approved" | "Pending" | "Denied" | "Not Required"
csv_diagnosis_on_file: Optional[str] = None # "Yes" | "No"
csv_transfer_from: Optional[str] = None # prior supplier name if transfer
order_number: Optional[str] = None # pass-through — billing system order/claim number
hcpcs: Optional[str] = None # pass-through — HCPCS code from supplier CSV
@dataclass

View file

@ -60,23 +60,49 @@ def _build_cascade(
payer_type: str,
) -> list:
"""
Build the downstream consequence chain for missing/at-risk doc items.
Only add cascade steps that are actually blocked.
Build the full consequence chain for all documentation gaps.
No early returns every unfulfilled requirement is surfaced.
Type A = doc gaps Signal can help close.
Type B = route-to-action alerts for denials.
"""
cascade = []
# Type A: Documentation gaps
if not visit_confirmed:
cascade.append("Visit date not on file")
cascade.append("Device shipment at risk")
return cascade
cascade.append(
"Qualifying visit not on file — confirm with prescriber before scheduling shipment"
)
if swo in ("Pending", "Expired"):
cascade.append("SWO not on file")
cascade.append("Device shipment at risk")
return cascade
label = "not on file" if swo == "Pending" else "expired"
cascade.append(
f"Written order {label} — obtain from prescriber before claim submission"
)
if diagnosis == "Missing":
cascade.append("Diagnosis not on file")
cascade.append(
"Diagnosis code not on file — obtain from prescriber records before billing"
)
if payer_type in _PECOS_REQUIRED and pecos == "Not Verified":
cascade.append(
"Ordering provider PECOS enrollment not verified — confirm NPI is active before claim submission"
)
if payer_type not in _PA_NOT_REQUIRED and pa == "Required — Not Started":
cascade.append(
"Prior authorization not started — obtain before scheduling shipment"
)
# Type B: Route-to-action alert
if pa == "Denied":
cascade.append(
"Prior authorization denied — file Level 1 appeal within 65 days. "
"Approximately 80% of first-level Medicare Advantage appeals succeed."
)
# Consequence line — only appended when at least one gap exists
if cascade:
cascade.append("Device shipment at risk")
return cascade