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 | | 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 | | 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 | | Pilot LOI reviewed by attorney | PARTIAL — template ready; Bittinger/Nixon review pending |
| NDA template 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 | | Pilot LOI executed with supplier | NOT DONE |

View file

@ -167,6 +167,9 @@ class RecordOut(BaseModel):
is_transfer: bool = False is_transfer: bool = False
doc_state: Optional[DocStateOut] = None doc_state: Optional[DocStateOut] = None
cascade: list[str] = [] cascade: list[str] = []
order_number: Optional[str] = None
hcpcs: Optional[str] = None
recommended_next_step_code: str = "NO_ACTION_NEEDED"
class UploadResponse(BaseModel): class UploadResponse(BaseModel):
@ -226,6 +229,24 @@ def _normalize_payer_type(payer: str) -> str:
return normalized 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: def _to_record_out(r, record=None) -> RecordOut:
flag_val = r.flag.value if hasattr(r.flag, "value") else str(r.flag) 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), is_transfer=getattr(r, "is_transfer", False),
doc_state=doc_state_out, doc_state=doc_state_out,
cascade=cascade, 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() output = io.StringIO()
writer = csv.writer(output) writer = csv.writer(output)
writer.writerow([ writer.writerow([
"Order Number",
"HCPCS",
"Patient ID", "Patient ID",
"Device", "Device",
"Payer", "Payer",
"Status", "Status",
"Recommended Next Step",
"Priority Score", "Priority Score",
"Days Until Resupply End", "Days Until Resupply End",
"Next Visit Due", "Next Visit Due",
@ -407,14 +434,19 @@ async def export_work_queue(
"SWO Status", "SWO Status",
"Visit Status", "Visit Status",
"PA Status", "PA Status",
"Diagnosis Status",
"PECOS Status",
]) ])
for r in records: for r in records:
doc = r.doc_state doc = r.doc_state
writer.writerow([ writer.writerow([
r.order_number or "",
r.hcpcs or "",
r.patient_id, r.patient_id,
r.device_display, r.device_display,
r.payer, r.payer,
r.status_label, r.status_label,
r.recommended_next_step_code,
r.priority_score, r.priority_score,
r.days_until_coverage_end, r.days_until_coverage_end,
r.next_visit_due_date or "", r.next_visit_due_date or "",
@ -425,6 +457,8 @@ async def export_work_queue(
doc.swo if doc else "", doc.swo if doc else "",
doc.visit if doc else "", doc.visit if doc else "",
doc.pa if doc else "", doc.pa if doc else "",
doc.diagnosis if doc else "",
doc.pecos if doc else "",
]) ])
output.seek(0) output.seek(0)

View file

@ -76,6 +76,15 @@ HEADER_MAP: dict[str, list[str]] = {
"transfer_from", "transfer from", "previous_supplier", "previous supplier", "transfer_from", "transfer from", "previous_supplier", "previous supplier",
"prior_supplier", "prior supplier", "transfer_status", "transferred_from", "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] = { 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 csv_diagnosis_on_file = mapped.get("csv_diagnosis_on_file") or None
transfer_raw = mapped.get("csv_transfer_from", "").strip() transfer_raw = mapped.get("csv_transfer_from", "").strip()
csv_transfer_from = transfer_raw if transfer_raw else None 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( records.append(ShipmentRecord(
patient_id=patient_id, 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_pa_status=csv_pa_status,
csv_diagnosis_on_file=csv_diagnosis_on_file, csv_diagnosis_on_file=csv_diagnosis_on_file,
csv_transfer_from=csv_transfer_from, csv_transfer_from=csv_transfer_from,
order_number=order_number,
hcpcs=hcpcs,
)) ))
return records, skipped, mapping_summary 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_pa_status: Optional[str] = None # "Approved" | "Pending" | "Denied" | "Not Required"
csv_diagnosis_on_file: Optional[str] = None # "Yes" | "No" csv_diagnosis_on_file: Optional[str] = None # "Yes" | "No"
csv_transfer_from: Optional[str] = None # prior supplier name if transfer 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 @dataclass

View file

@ -60,23 +60,49 @@ def _build_cascade(
payer_type: str, payer_type: str,
) -> list: ) -> list:
""" """
Build the downstream consequence chain for missing/at-risk doc items. Build the full consequence chain for all documentation gaps.
Only add cascade steps that are actually blocked. 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 = [] cascade = []
# Type A: Documentation gaps
if not visit_confirmed: if not visit_confirmed:
cascade.append("Visit date not on file") cascade.append(
cascade.append("Device shipment at risk") "Qualifying visit not on file — confirm with prescriber before scheduling shipment"
return cascade )
if swo in ("Pending", "Expired"): if swo in ("Pending", "Expired"):
cascade.append("SWO not on file") label = "not on file" if swo == "Pending" else "expired"
cascade.append("Device shipment at risk") cascade.append(
return cascade f"Written order {label} — obtain from prescriber before claim submission"
)
if diagnosis == "Missing": 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") cascade.append("Device shipment at risk")
return cascade return cascade