From ff657f476543503542f30d9ad9a7b698013c7f59 Mon Sep 17 00:00:00 2001 From: Kisa Date: Wed, 10 Jun 2026 22:53:22 -0400 Subject: [PATCH] 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 --- CLAUDE.md | 2 +- python-backend/api/main.py | 34 +++++++++++++++++ python-backend/api/normalizer.py | 13 +++++++ python-backend/core/coverage_calculator.py | 2 + python-backend/core/doc_state_machine.py | 44 +++++++++++++++++----- 5 files changed, 85 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c88ac8a..4a8500c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 | diff --git a/python-backend/api/main.py b/python-backend/api/main.py index a089ad9..00777bd 100644 --- a/python-backend/api/main.py +++ b/python-backend/api/main.py @@ -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) diff --git a/python-backend/api/normalizer.py b/python-backend/api/normalizer.py index b276fe0..467cd25 100644 --- a/python-backend/api/normalizer.py +++ b/python-backend/api/normalizer.py @@ -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 diff --git a/python-backend/core/coverage_calculator.py b/python-backend/core/coverage_calculator.py index 4cf2478..58920cb 100644 --- a/python-backend/core/coverage_calculator.py +++ b/python-backend/core/coverage_calculator.py @@ -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 diff --git a/python-backend/core/doc_state_machine.py b/python-backend/core/doc_state_machine.py index 8393bb9..c1352d2 100644 --- a/python-backend/core/doc_state_machine.py +++ b/python-backend/core/doc_state_machine.py @@ -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