Reframe healthcare filing as standard vs expedited; e2e test + bug fixes
Copy: drop paper/electronic/fax framing across the revalidation + enrollment marketing pages and the order-confirmation email; present two service tiers: - Standard filing (no CMS account; we prepare CMS-855, you sign, we submit to MAC) - Expedited filing (CMS I&A surrogate access; same-day PECOS filing + tracking) Internal worker todos + the _STANDARD_FILING_SLUGS identifier updated to match. New scripts/test_healthcare_e2e.py validates the whole order line (slug consistency x6 places, price agreement, intake field collection+enforcement, worker dispatch, handler execution producing CMS-855 PDF+anchor, free-tool action_urls). 45 checks. Bugs found + fixed by the test: - medicare-enrollment requires practice_state server-side but the wizard never enforced it -> orders could be paid then stall. Wizard now requires it. - determine_form_type defaulted org NPIs to the individual 855I because enumeration_type is never collected -> wrong form, CMS rejection. Now does a live NPPES lookup (safe 855I fallback).
This commit is contained in:
parent
5cfe9702e2
commit
695ace207c
7 changed files with 381 additions and 54 deletions
|
|
@ -2152,42 +2152,43 @@ export async function sendComplianceIntakeEmail(
|
||||||
</p>
|
</p>
|
||||||
</div>` : "";
|
</div>` : "";
|
||||||
|
|
||||||
// CMS filing-method section for PECOS / NPPES orders. We offer two paths and
|
// CMS filing-method section for PECOS / NPPES orders. We offer two service
|
||||||
// let the provider pick the one that's easiest for them:
|
// tiers and let the provider pick the one that's easiest for them:
|
||||||
// (1) Paper CMS-855 — they e-sign one page, we print + mail to their MAC.
|
// (1) Standard filing — they review + sign one certification, we submit to
|
||||||
// Zero account setup. Default for revalidation/enrollment.
|
// their MAC. Zero account setup. Default for reval/enrollment.
|
||||||
// (2) I&A surrogacy — faster/trackable, but needs a CMS I&A account.
|
// (2) Expedited filing — faster/same-day-trackable via CMS I&A surrogate access.
|
||||||
// NPPES-only services (reactivation, update) are web-only, so surrogacy is
|
// NPPES-only services (reactivation, update) are web-only, so surrogate access
|
||||||
// the only path for those. We never ask for the provider's password.
|
// is the only path for those. We never ask for the provider's password.
|
||||||
const npiConfirmUrl = `${SITE_DOMAIN}/order/success?action=ia_surrogacy&order_id=${orderId}`;
|
const npiConfirmUrl = `${SITE_DOMAIN}/order/success?action=ia_surrogacy&order_id=${orderId}`;
|
||||||
// Which ordered services can use the paper CMS-855 path (PECOS enrollment/reval)
|
// Which ordered services support the standard (no-account) filing path
|
||||||
// vs. are NPPES-web-only (surrogacy required).
|
// vs. are NPPES-web-only (surrogate access required).
|
||||||
const PAPER_855_SLUGS = new Set<string>(["npi-revalidation", "medicare-enrollment", "provider-compliance-bundle"]);
|
const STANDARD_FILING_SLUGS = new Set<string>(["npi-revalidation", "medicare-enrollment", "provider-compliance-bundle"]);
|
||||||
const NPPES_ONLY_SLUGS = new Set<string>(["npi-reactivation", "nppes-update"]);
|
const NPPES_ONLY_SLUGS = new Set<string>(["npi-reactivation", "nppes-update"]);
|
||||||
const hasPaper855 = npiAccessOrders.some(o => PAPER_855_SLUGS.has(o.service_slug as string));
|
const hasStandardFiling = npiAccessOrders.some(o => STANDARD_FILING_SLUGS.has(o.service_slug as string));
|
||||||
const hasNppesOnly = npiAccessOrders.some(o => NPPES_ONLY_SLUGS.has(o.service_slug as string));
|
const hasNppesOnly = npiAccessOrders.some(o => NPPES_ONLY_SLUGS.has(o.service_slug as string));
|
||||||
|
|
||||||
const paper855Block = hasPaper855 ? `
|
const standardFilingBlock = hasStandardFiling ? `
|
||||||
<p style="margin:0 0 6px;font-size:13px;font-weight:700;color:#115e59;">Option 1 (easiest): Paper CMS-855 — no account needed</p>
|
<p style="margin:0 0 6px;font-size:13px;font-weight:700;color:#115e59;">Standard filing — no account needed</p>
|
||||||
<p style="margin:0 0 12px;font-size:13px;color:#134e4a;line-height:1.5;">
|
<p style="margin:0 0 12px;font-size:13px;color:#134e4a;line-height:1.5;">
|
||||||
We complete the correct CMS-855 form for you. You e-sign the certification page
|
We complete the correct CMS-855 for you. You review and sign the certification
|
||||||
from a secure link we send (takes about a minute), and we print it and mail it to
|
from a secure link we send (takes about a minute), and we submit it to your
|
||||||
your Medicare Administrative Contractor (MAC). Nothing for you to set up.
|
Medicare Administrative Contractor (MAC) and track it to confirmation. Nothing
|
||||||
|
for you to set up.
|
||||||
</p>` : "";
|
</p>` : "";
|
||||||
|
|
||||||
const surrogacyHeading = hasPaper855
|
const surrogacyHeading = hasStandardFiling
|
||||||
? `Option 2 (faster): CMS I&A surrogate access`
|
? `Expedited filing — CMS I&A surrogate access`
|
||||||
: `Grant CMS I&A surrogate access`;
|
: `Grant CMS I&A surrogate access`;
|
||||||
|
|
||||||
const npiSection = hasNpiAccess ? `
|
const npiSection = hasNpiAccess ? `
|
||||||
<div style="background:#ccfbf1;border:1px solid #5eead4;border-radius:8px;padding:16px 20px;margin:20px 0;">
|
<div style="background:#ccfbf1;border:1px solid #5eead4;border-radius:8px;padding:16px 20px;margin:20px 0;">
|
||||||
<p style="margin:0 0 10px;font-size:14px;font-weight:700;color:#115e59;">Action Required: Choose How We File</p>
|
<p style="margin:0 0 10px;font-size:14px;font-weight:700;color:#115e59;">Action Required: Choose How We File</p>
|
||||||
${paper855Block}
|
${standardFilingBlock}
|
||||||
<p style="margin:0 0 6px;font-size:13px;font-weight:700;color:#115e59;">${surrogacyHeading}</p>
|
<p style="margin:0 0 6px;font-size:13px;font-weight:700;color:#115e59;">${surrogacyHeading}</p>
|
||||||
<p style="margin:0 0 12px;font-size:13px;color:#134e4a;line-height:1.5;">
|
<p style="margin:0 0 12px;font-size:13px;color:#134e4a;line-height:1.5;">
|
||||||
${hasNppesOnly ? "NPPES updates and reactivations are online-only, so this is required for those. " : ""}You add us as a
|
${hasNppesOnly ? "NPPES updates and reactivations are online-only, so this is required for those. " : ""}You add us as a
|
||||||
<strong>Surrogate</strong> in CMS Identity & Access (I&A) — you never share your password.
|
<strong>Surrogate</strong> in CMS Identity & Access (I&A) — you never share your password.
|
||||||
We then file in PECOS / NPPES under our own credentials, e-sign where permitted, and capture the tracking ID.
|
We then file in PECOS / NPPES under our own credentials and capture the tracking ID the same day.
|
||||||
</p>
|
</p>
|
||||||
<ol style="margin:0 0 12px;padding-left:20px;font-size:13px;color:#134e4a;line-height:1.7;">
|
<ol style="margin:0 0 12px;padding-left:20px;font-size:13px;color:#134e4a;line-height:1.7;">
|
||||||
<li>Log in to <a href="https://nppes.cms.hhs.gov/IAWeb/login.do" style="color:#0f766e;">CMS I&A (I&A System)</a></li>
|
<li>Log in to <a href="https://nppes.cms.hhs.gov/IAWeb/login.do" style="color:#0f766e;">CMS I&A (I&A System)</a></li>
|
||||||
|
|
@ -2201,7 +2202,7 @@ export async function sendComplianceIntakeEmail(
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p style="margin:8px 0 0;font-size:12px;color:#115e59;text-align:center;">
|
<p style="margin:8px 0 0;font-size:12px;color:#115e59;text-align:center;">
|
||||||
${hasPaper855 ? "Prefer the paper option? Just reply to this email and we'll send your CMS-855 to e-sign — no further action needed from you here." : "Clicking this notifies our team so we can begin your filing."}
|
${hasStandardFiling ? "Prefer standard filing? Just reply to this email and we'll send your CMS-855 to sign — no further action needed from you here." : "Clicking this notifies our team so we can begin your filing."}
|
||||||
</p>
|
</p>
|
||||||
</div>` : "";
|
</div>` : "";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
LOG = logging.getLogger("cms855_pdf_filler")
|
LOG = logging.getLogger("cms855_pdf_filler")
|
||||||
|
|
@ -93,15 +94,45 @@ def _split_name(full: str) -> tuple[str, str, str]:
|
||||||
return parts[0], parts[1][0], " ".join(parts[2:])
|
return parts[0], parts[1][0], " ".join(parts[2:])
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_enumeration_type(npi: str) -> str:
|
||||||
|
"""Best-effort NPPES lookup of an NPI's enumeration_type (NPI-1 / NPI-2).
|
||||||
|
|
||||||
|
Returns "" if the NPI is missing, malformed, or the lookup fails — callers
|
||||||
|
fall back to the individual form (855I), the safe default.
|
||||||
|
"""
|
||||||
|
npi = (npi or "").strip()
|
||||||
|
if not re.fullmatch(r"\d{10}", npi):
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
import json as _json
|
||||||
|
from urllib.request import urlopen
|
||||||
|
url = f"https://npiregistry.cms.hhs.gov/api/?version=2.1&number={npi}"
|
||||||
|
with urlopen(url, timeout=8) as resp: # nosec - public, read-only
|
||||||
|
data = _json.loads(resp.read().decode("utf-8"))
|
||||||
|
results = data.get("results") or []
|
||||||
|
if results:
|
||||||
|
return (results[0].get("enumeration_type") or "").upper()
|
||||||
|
except Exception:
|
||||||
|
LOG.warning("NPPES enumeration_type lookup failed for %s", npi)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def determine_form_type(slug: str, intake: dict) -> str:
|
def determine_form_type(slug: str, intake: dict) -> str:
|
||||||
"""Pick which 855 form applies for the order."""
|
"""Pick which 855 form applies for the order.
|
||||||
|
|
||||||
|
Org NPIs (Type 2 / NPI-2) revalidate/enroll on 855B; individuals on 855I.
|
||||||
|
The wizard does not collect enumeration_type, so when it is absent we look
|
||||||
|
it up live from NPPES rather than silently defaulting an organization to the
|
||||||
|
wrong (individual) form, which CMS would reject.
|
||||||
|
"""
|
||||||
|
if slug not in ("npi-revalidation", "medicare-enrollment"):
|
||||||
|
return "855i"
|
||||||
|
|
||||||
enum_type = (intake.get("enumeration_type") or "").upper()
|
enum_type = (intake.get("enumeration_type") or "").upper()
|
||||||
if slug == "npi-revalidation":
|
if not enum_type:
|
||||||
# Org NPIs (Type 2) revalidate on 855B; individuals on 855I.
|
enum_type = _lookup_enumeration_type(intake.get("npi", ""))
|
||||||
return "855b" if enum_type in ("NPI-2", "2", "ORGANIZATION") else "855i"
|
|
||||||
if slug == "medicare-enrollment":
|
return "855b" if enum_type in ("NPI-2", "2", "ORGANIZATION") else "855i"
|
||||||
return "855b" if enum_type in ("NPI-2", "2", "ORGANIZATION") else "855i"
|
|
||||||
return "855i"
|
|
||||||
|
|
||||||
|
|
||||||
def fill_cms855(form_type: str, intake: dict, order_number: str = "") -> tuple[bytes, list[dict], list[str]]:
|
def fill_cms855(form_type: str, intake: dict, order_number: str = "") -> tuple[bytes, list[dict], list[str]]:
|
||||||
|
|
|
||||||
284
scripts/test_healthcare_e2e.py
Normal file
284
scripts/test_healthcare_e2e.py
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""End-to-end consistency + flow test for the Healthcare / NPI ordering line.
|
||||||
|
|
||||||
|
Validates the full order path across all wiring points and runs the worker
|
||||||
|
handlers for real (PDF generation, todo creation), catching logical errors.
|
||||||
|
|
||||||
|
Checks:
|
||||||
|
1. Slug consistency across all 5 registration points.
|
||||||
|
2. Pricing agreement (catalog vs intake manifest vs ERPNext www/orders).
|
||||||
|
3. Intake manifest steps + the wizard collects every server-required field.
|
||||||
|
4. Worker dispatch maps every slug to a handler.
|
||||||
|
5. Each handler runs end-to-end (mocked DB/MinIO/esign) and produces the
|
||||||
|
right artifacts: CMS-855 PDF + signature anchor for filing slugs, todo
|
||||||
|
for all.
|
||||||
|
6. The free-tool action_urls point at real order/service slugs.
|
||||||
|
|
||||||
|
Run: python3 scripts/test_healthcare_e2e.py
|
||||||
|
Exit code 0 = all pass, 1 = failures.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "scripts"))
|
||||||
|
|
||||||
|
HEALTHCARE_SLUGS = [
|
||||||
|
"npi-revalidation", "npi-reactivation", "nppes-update",
|
||||||
|
"medicare-enrollment", "oig-sam-screening", "provider-compliance-bundle",
|
||||||
|
]
|
||||||
|
# Slugs that should generate a CMS-855 PDF + e-sign anchor.
|
||||||
|
FILING_SLUGS = {"npi-revalidation", "npi-reactivation", "medicare-enrollment"}
|
||||||
|
|
||||||
|
failures: list[str] = []
|
||||||
|
passes: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def ok(msg):
|
||||||
|
passes.append(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def fail(msg):
|
||||||
|
failures.append(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def read(p: Path) -> str:
|
||||||
|
return (ROOT / p).read_text()
|
||||||
|
|
||||||
|
|
||||||
|
# ── 1. Slug consistency across the 5 registration points ──────────────────
|
||||||
|
def check_slug_consistency():
|
||||||
|
sources = {
|
||||||
|
"compliance-orders.ts (catalog)": ("api/src/routes/compliance-orders.ts",
|
||||||
|
r'"(npi-[a-z-]+|nppes-update|medicare-enrollment|oig-sam-screening|provider-compliance-bundle)":\s*\{'),
|
||||||
|
"compliance-orders.ts (intake reqs)": ("api/src/routes/compliance-orders.ts",
|
||||||
|
r'"(npi-[a-z-]+|nppes-update|medicare-enrollment|oig-sam-screening|provider-compliance-bundle)":\s*\{ required'),
|
||||||
|
"intake_manifest.ts (steps)": ("site/src/lib/intake_manifest.ts",
|
||||||
|
r'"(npi-[a-z-]+|nppes-update|medicare-enrollment|oig-sam-screening|provider-compliance-bundle)":\s*\['),
|
||||||
|
"intake_manifest.ts (meta)": ("site/src/lib/intake_manifest.ts",
|
||||||
|
r'"(npi-[a-z-]+|nppes-update|medicare-enrollment|oig-sam-screening|provider-compliance-bundle)":\s*\{ name'),
|
||||||
|
"www/orders.py": ("performancewest_erpnext/performancewest_erpnext/www/orders.py",
|
||||||
|
r'"(npi-[a-z-]+|nppes-update|medicare-enrollment|oig-sam-screening|provider-compliance-bundle)":'),
|
||||||
|
"services/__init__.py (dispatch)": ("scripts/workers/services/__init__.py",
|
||||||
|
r'"(npi-[a-z-]+|nppes-update|medicare-enrollment|oig-sam-screening|provider-compliance-bundle)":\s*\w+Handler'),
|
||||||
|
}
|
||||||
|
want = set(HEALTHCARE_SLUGS)
|
||||||
|
for label, (path, pat) in sources.items():
|
||||||
|
try:
|
||||||
|
txt = read(Path(path))
|
||||||
|
except FileNotFoundError:
|
||||||
|
fail(f"[slugs] file missing: {path}")
|
||||||
|
continue
|
||||||
|
found = set(re.findall(pat, txt))
|
||||||
|
missing = want - found
|
||||||
|
extra = found - want
|
||||||
|
if missing:
|
||||||
|
fail(f"[slugs] {label}: MISSING {sorted(missing)}")
|
||||||
|
elif extra:
|
||||||
|
fail(f"[slugs] {label}: UNEXPECTED {sorted(extra)}")
|
||||||
|
else:
|
||||||
|
ok(f"[slugs] {label}: all 6 present")
|
||||||
|
|
||||||
|
|
||||||
|
# ── 2. Pricing agreement ──────────────────────────────────────────────────
|
||||||
|
def check_pricing():
|
||||||
|
cat = read(Path("api/src/routes/compliance-orders.ts"))
|
||||||
|
man = read(Path("site/src/lib/intake_manifest.ts"))
|
||||||
|
|
||||||
|
def prices(txt):
|
||||||
|
out = {}
|
||||||
|
for slug in HEALTHCARE_SLUGS:
|
||||||
|
m = re.search(rf'"{re.escape(slug)}":\s*\{{[^}}]*?price_cents:\s*(\d+)', txt, re.S)
|
||||||
|
if m:
|
||||||
|
out[slug] = int(m.group(1))
|
||||||
|
return out
|
||||||
|
|
||||||
|
cprices, mprices = prices(cat), prices(man)
|
||||||
|
for slug in HEALTHCARE_SLUGS:
|
||||||
|
c, m = cprices.get(slug), mprices.get(slug)
|
||||||
|
if c is None:
|
||||||
|
fail(f"[price] {slug}: no price in catalog")
|
||||||
|
elif m is None:
|
||||||
|
fail(f"[price] {slug}: no price in intake manifest")
|
||||||
|
elif c != m:
|
||||||
|
fail(f"[price] {slug}: catalog {c} != manifest {m}")
|
||||||
|
else:
|
||||||
|
ok(f"[price] {slug}: ${c/100:.0f} consistent")
|
||||||
|
|
||||||
|
|
||||||
|
# ── 3. Intake: wizard collects every server-required field ────────────────
|
||||||
|
def check_intake_fields():
|
||||||
|
cat = read(Path("api/src/routes/compliance-orders.ts"))
|
||||||
|
step = read(Path("site/src/components/intake/steps/NpiIntakeStep.astro"))
|
||||||
|
|
||||||
|
# Fields the wizard writes into intake_data (from PW.set({...}) block).
|
||||||
|
set_block = re.search(r"PW\.set\(\{[^}]*intake_data:\s*\{(.+?)\}\}\)", step, re.S)
|
||||||
|
collected = set(re.findall(r"(\w+):", set_block.group(1))) if set_block else set()
|
||||||
|
|
||||||
|
# Fields the wizard *enforces* as required (the `missing.push` validations).
|
||||||
|
enforced = set()
|
||||||
|
if "npi-provider-name" in step and "missing.push" in step:
|
||||||
|
if 'val("npi-provider-name")' in step: enforced.add("provider_name")
|
||||||
|
if 'val("npi-number")' in step or "val(\"npi-number\")" in step: enforced.add("npi")
|
||||||
|
if 'val("npi-email")' in step: enforced.add("email")
|
||||||
|
|
||||||
|
# Slug-conditional enforcement: e.g. practice_state is only required when
|
||||||
|
# the active service is medicare-enrollment. Map field -> {slugs}.
|
||||||
|
conditional_enforced = {}
|
||||||
|
if 'activeSlugs.includes("medicare-enrollment")' in step and 'val("npi-practice-state")' in step:
|
||||||
|
conditional_enforced["practice_state"] = {"medicare-enrollment"}
|
||||||
|
|
||||||
|
for slug in HEALTHCARE_SLUGS:
|
||||||
|
m = re.search(rf'"{re.escape(slug)}":\s*\{{ required:\s*\[([^\]]*)\]', cat)
|
||||||
|
if not m:
|
||||||
|
fail(f"[intake] {slug}: no required-fields entry in catalog")
|
||||||
|
continue
|
||||||
|
required = set(re.findall(r'"([a-z_]+)"', m.group(1)))
|
||||||
|
# every required field must be collectable by the wizard
|
||||||
|
not_collected = required - collected
|
||||||
|
if not_collected:
|
||||||
|
fail(f"[intake] {slug}: required {sorted(not_collected)} NOT collected by wizard")
|
||||||
|
# every required field must be *enforced* — either always, or
|
||||||
|
# conditionally for this slug.
|
||||||
|
not_enforced = set()
|
||||||
|
for fld in required - enforced:
|
||||||
|
if slug not in conditional_enforced.get(fld, set()):
|
||||||
|
not_enforced.add(fld)
|
||||||
|
if not_enforced:
|
||||||
|
fail(f"[intake] {slug}: required {sorted(not_enforced)} collected but NOT validated as required in wizard")
|
||||||
|
if not not_collected and not not_enforced:
|
||||||
|
ok(f"[intake] {slug}: all required fields collected + enforced")
|
||||||
|
|
||||||
|
|
||||||
|
# ── 4 & 5. Worker dispatch + handler execution ────────────────────────────
|
||||||
|
def check_handlers():
|
||||||
|
import asyncio
|
||||||
|
import workers.services.npi_provider as npi
|
||||||
|
from workers.services import SERVICE_HANDLERS
|
||||||
|
|
||||||
|
for slug in HEALTHCARE_SLUGS:
|
||||||
|
if slug not in SERVICE_HANDLERS:
|
||||||
|
fail(f"[dispatch] {slug}: no handler registered")
|
||||||
|
continue
|
||||||
|
ok(f"[dispatch] {slug}: -> {SERVICE_HANDLERS[slug].__name__}")
|
||||||
|
|
||||||
|
# Run each handler with mocked IO and verify artifacts.
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_todo(self, order_number, intake, title, description, priority="normal"):
|
||||||
|
captured.setdefault(self.SERVICE_SLUG, {})["todo"] = {"title": title, "desc": description}
|
||||||
|
|
||||||
|
esign_calls = {}
|
||||||
|
|
||||||
|
def fake_esign(self, order_number, intake, provider, customer_email, form_type, document_key, anchors):
|
||||||
|
esign_calls[self.SERVICE_SLUG] = {"form_type": form_type, "anchors": anchors, "key": document_key}
|
||||||
|
return True # pretend esign + email succeeded
|
||||||
|
|
||||||
|
# Stub MinIO upload inside the filler path by stubbing _generate_855... upload
|
||||||
|
orig_gen = npi._BaseNPIHandler._generate_855_for_signing
|
||||||
|
|
||||||
|
def gen_no_upload(self, order_number, intake, provider, customer_email):
|
||||||
|
from document_gen.templates.cms855_pdf_filler import determine_form_type, fill_cms855
|
||||||
|
ft = determine_form_type(self.SERVICE_SLUG, intake)
|
||||||
|
pdf, anchors, missing = fill_cms855(ft, intake, order_number)
|
||||||
|
captured.setdefault(self.SERVICE_SLUG, {})["pdf"] = {"len": len(pdf), "anchors": anchors, "missing": missing, "form_type": ft}
|
||||||
|
# exercise the esign record path (stubbed)
|
||||||
|
self._create_855_esign_record(order_number, intake, provider, customer_email, ft, f"compliance/{order_number}/cms{ft}.pdf", anchors)
|
||||||
|
return f"CMS-{ft.upper()} generated ({len(pdf)} bytes)"
|
||||||
|
|
||||||
|
npi._BaseNPIHandler._create_todo = fake_todo
|
||||||
|
npi._BaseNPIHandler._create_855_esign_record = fake_esign
|
||||||
|
npi._BaseNPIHandler._generate_855_for_signing = gen_no_upload
|
||||||
|
|
||||||
|
intake = {
|
||||||
|
"npi": "1234567893", "provider_name": "Jane Q Smith", "email": "jane@example.com",
|
||||||
|
"dob": "01011980", "practice_state": "CA", "enumeration_type": "NPI-1",
|
||||||
|
}
|
||||||
|
for slug in HEALTHCARE_SLUGS:
|
||||||
|
h = SERVICE_HANDLERS[slug]()
|
||||||
|
order = {"order_number": f"CO-T-{slug}", "customer_name": "Jane Q Smith",
|
||||||
|
"customer_email": "jane@example.com", "intake_data": dict(intake)}
|
||||||
|
try:
|
||||||
|
asyncio.run(h.process(order))
|
||||||
|
except Exception as e:
|
||||||
|
fail(f"[handler] {slug}: raised {type(e).__name__}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
c = captured.get(slug, {})
|
||||||
|
if "todo" not in c:
|
||||||
|
fail(f"[handler] {slug}: no admin todo created")
|
||||||
|
else:
|
||||||
|
ok(f"[handler] {slug}: admin todo created")
|
||||||
|
|
||||||
|
if slug in FILING_SLUGS:
|
||||||
|
if "pdf" not in c:
|
||||||
|
fail(f"[handler] {slug}: expected CMS-855 PDF, none generated")
|
||||||
|
else:
|
||||||
|
pdf = c["pdf"]
|
||||||
|
if pdf["len"] < 10000:
|
||||||
|
fail(f"[handler] {slug}: PDF suspiciously small ({pdf['len']} bytes)")
|
||||||
|
elif not pdf["anchors"]:
|
||||||
|
fail(f"[handler] {slug}: PDF has NO signature anchor (form={pdf['form_type']})")
|
||||||
|
else:
|
||||||
|
a = pdf["anchors"][0]
|
||||||
|
need = {"field", "page", "x", "y", "w", "h", "page_w", "page_h"}
|
||||||
|
if not need.issubset(a):
|
||||||
|
fail(f"[handler] {slug}: anchor missing keys {need - set(a)}")
|
||||||
|
else:
|
||||||
|
ok(f"[handler] {slug}: CMS-{pdf['form_type'].upper()} {pdf['len']}B + anchor on page {a['page']}")
|
||||||
|
if slug not in esign_calls:
|
||||||
|
fail(f"[handler] {slug}: esign record was not requested")
|
||||||
|
else:
|
||||||
|
ok(f"[handler] {slug}: esign record requested (form {esign_calls[slug]['form_type']})")
|
||||||
|
else:
|
||||||
|
if "pdf" in c:
|
||||||
|
fail(f"[handler] {slug}: unexpectedly generated a CMS-855 PDF (should not)")
|
||||||
|
else:
|
||||||
|
ok(f"[handler] {slug}: correctly skips CMS-855 generation")
|
||||||
|
|
||||||
|
|
||||||
|
# ── 6. Free-tool action_urls point at real slugs ──────────────────────────
|
||||||
|
def check_free_tool_links():
|
||||||
|
try:
|
||||||
|
lookup = read(Path("api/src/routes/npi-lookup.ts"))
|
||||||
|
except FileNotFoundError:
|
||||||
|
fail("[tool] npi-lookup.ts missing")
|
||||||
|
return
|
||||||
|
urls = set(re.findall(r'action_url:\s*"(/(?:order|services/healthcare)[^"]*)"', lookup))
|
||||||
|
order_pages = {p.stem for p in (ROOT / "site/src/pages/order").glob("*.astro")}
|
||||||
|
for u in sorted(urls):
|
||||||
|
if u.startswith("/order/"):
|
||||||
|
slug = u.split("/order/")[1].strip("/")
|
||||||
|
if slug not in order_pages:
|
||||||
|
fail(f"[tool] action_url {u} -> no order page /order/{slug}.astro")
|
||||||
|
else:
|
||||||
|
ok(f"[tool] action_url {u} resolves")
|
||||||
|
else:
|
||||||
|
# /services/healthcare[/x]
|
||||||
|
tail = u.replace("/services/healthcare", "").strip("/")
|
||||||
|
target = ROOT / "site/src/pages/services/healthcare" / (f"{tail}.astro" if tail else "index.astro")
|
||||||
|
if not target.exists():
|
||||||
|
fail(f"[tool] action_url {u} -> no page {target.relative_to(ROOT)}")
|
||||||
|
else:
|
||||||
|
ok(f"[tool] action_url {u} resolves")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
check_slug_consistency()
|
||||||
|
check_pricing()
|
||||||
|
check_intake_fields()
|
||||||
|
check_handlers()
|
||||||
|
check_free_tool_links()
|
||||||
|
|
||||||
|
print("\n".join(f" PASS {p}" for p in passes))
|
||||||
|
if failures:
|
||||||
|
print("\n".join(f" FAIL {f}" for f in failures))
|
||||||
|
print(f"\n{len(passes)} passed, {len(failures)} FAILED")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"\nALL {len(passes)} CHECKS PASSED")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -41,7 +41,7 @@ _SLUG_META = {
|
||||||
"enrollment record, and submit. Capture the PECOS tracking ID."
|
"enrollment record, and submit. Capture the PECOS tracking ID."
|
||||||
),
|
),
|
||||||
"access": (
|
"access": (
|
||||||
"PECOS via CMS I&A surrogacy (preferred). Fallback: paper CMS-855I/B/R, provider wet-signs cert page, mail to provider's MAC."
|
"Standard: prepare CMS-855I/B/R, provider signs cert, submit to MAC. Expedited: file in PECOS via CMS I&A surrogate access."
|
||||||
),
|
),
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
},
|
},
|
||||||
|
|
@ -53,7 +53,7 @@ _SLUG_META = {
|
||||||
"reason, correct any stale data, and re-certify the record."
|
"reason, correct any stale data, and re-certify the record."
|
||||||
),
|
),
|
||||||
"access": (
|
"access": (
|
||||||
"NPPES via CMS I&A surrogacy. No paper option (NPPES is web-only)."
|
"NPPES via CMS I&A surrogate access (online-only)."
|
||||||
),
|
),
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
},
|
},
|
||||||
|
|
@ -66,7 +66,7 @@ _SLUG_META = {
|
||||||
"changes and certify."
|
"changes and certify."
|
||||||
),
|
),
|
||||||
"access": (
|
"access": (
|
||||||
"NPPES via CMS I&A surrogacy. No paper option (NPPES is web-only)."
|
"NPPES via CMS I&A surrogate access (online-only)."
|
||||||
),
|
),
|
||||||
"priority": "normal",
|
"priority": "normal",
|
||||||
},
|
},
|
||||||
|
|
@ -78,7 +78,7 @@ _SLUG_META = {
|
||||||
"Confirm taxonomy, practice location, and authorized official."
|
"Confirm taxonomy, practice location, and authorized official."
|
||||||
),
|
),
|
||||||
"access": (
|
"access": (
|
||||||
"PECOS via CMS I&A surrogacy (preferred). Fallback: paper CMS-855, provider wet-signs, mail to MAC."
|
"Standard: prepare CMS-855, provider signs cert, submit to MAC. Expedited: file in PECOS via CMS I&A surrogate access."
|
||||||
),
|
),
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
},
|
},
|
||||||
|
|
@ -104,17 +104,17 @@ _SLUG_META = {
|
||||||
"record. Set the next revalidation reminder."
|
"record. Set the next revalidation reminder."
|
||||||
),
|
),
|
||||||
"access": (
|
"access": (
|
||||||
"PECOS/NPPES via CMS I&A surrogacy; screening is public. Paper CMS-855 fallback for the enrollment/revalidation piece."
|
"Standard CMS-855 filing for the enrollment/revalidation piece; NPPES + PECOS via CMS I&A surrogate access; screening is public."
|
||||||
),
|
),
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Slugs whose fulfilment includes a paper CMS-855 (auto-filled official form,
|
# Slugs whose fulfilment includes a CMS-855 (auto-filled official form, signed
|
||||||
# e-signed, then printed + USPS Priority-mailed to the provider's MAC). The
|
# via the secure e-sign link, then submitted to the provider's MAC). The
|
||||||
# bundle's revalidation piece is handled by the dedicated revalidation order it
|
# bundle's revalidation piece is handled by the dedicated revalidation order it
|
||||||
# spawns, so it is not listed here.
|
# spawns, so it is not listed here.
|
||||||
_PAPER_855_SLUGS = {
|
_STANDARD_FILING_SLUGS = {
|
||||||
"npi-revalidation",
|
"npi-revalidation",
|
||||||
"npi-reactivation",
|
"npi-reactivation",
|
||||||
"medicare-enrollment",
|
"medicare-enrollment",
|
||||||
|
|
@ -151,16 +151,16 @@ class _BaseNPIHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
# For PECOS enrollment/revalidation we generate the official CMS-855,
|
# For PECOS enrollment/revalidation we generate the official CMS-855,
|
||||||
# send it for e-signature, then a human prints + USPS-mails it to the MAC.
|
# send it for e-signature, then a human submits it to the MAC.
|
||||||
paper_note = ""
|
filing_note = ""
|
||||||
if self.SERVICE_SLUG in _PAPER_855_SLUGS:
|
if self.SERVICE_SLUG in _STANDARD_FILING_SLUGS:
|
||||||
try:
|
try:
|
||||||
paper_note = self._generate_855_for_signing(
|
filing_note = self._generate_855_for_signing(
|
||||||
order_number, intake, provider, customer_email
|
order_number, intake, provider, customer_email
|
||||||
)
|
)
|
||||||
except Exception as exc: # never block the admin todo on PDF issues
|
except Exception as exc: # never block the admin todo on PDF issues
|
||||||
LOG.error("[%s] CMS-855 generation failed: %s", order_number, exc)
|
LOG.error("[%s] CMS-855 generation failed: %s", order_number, exc)
|
||||||
paper_note = f"CMS-855 auto-generation FAILED ({exc}); prepare the form manually."
|
filing_note = f"CMS-855 auto-generation FAILED ({exc}); prepare the form manually."
|
||||||
|
|
||||||
description = (
|
description = (
|
||||||
f"{meta['action']}\n\n"
|
f"{meta['action']}\n\n"
|
||||||
|
|
@ -171,9 +171,9 @@ class _BaseNPIHandler:
|
||||||
f"Practice state: {practice_state or 'not provided'}\n"
|
f"Practice state: {practice_state or 'not provided'}\n"
|
||||||
f"Portal: {meta['portal']}\n"
|
f"Portal: {meta['portal']}\n"
|
||||||
f"Access model: {meta['access']}\n"
|
f"Access model: {meta['access']}\n"
|
||||||
+ (f"\n{paper_note}\n" if paper_note else "")
|
+ (f"\n{filing_note}\n" if filing_note else "")
|
||||||
+ "\nReview-staged: complete/verify the form, get it signed, then "
|
+ "\nReview-staged: complete/verify the form, get it signed, then "
|
||||||
"print and USPS Priority Mail it to the provider's MAC (or file in "
|
"submit it to the provider's MAC (standard), or file in "
|
||||||
"PECOS if surrogate access was granted). Mark this order complete."
|
"PECOS if surrogate access was granted). Mark this order complete."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -191,7 +191,7 @@ class _BaseNPIHandler:
|
||||||
|
|
||||||
Returns a human-readable note for the admin todo describing what was
|
Returns a human-readable note for the admin todo describing what was
|
||||||
generated and what still needs manual completion. The signed PDF is
|
generated and what still needs manual completion. The signed PDF is
|
||||||
printed and USPS Priority-mailed to the MAC by the fulfilment team.
|
submitted to the MAC by the fulfilment team.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from scripts.document_gen.templates.cms855_pdf_filler import (
|
from scripts.document_gen.templates.cms855_pdf_filler import (
|
||||||
|
|
@ -231,13 +231,13 @@ class _BaseNPIHandler:
|
||||||
)
|
)
|
||||||
|
|
||||||
note_lines = [
|
note_lines = [
|
||||||
f"PAPER CMS-{form_type.upper()} generated (official form, auto-filled where possible).",
|
f"CMS-{form_type.upper()} generated (official form, auto-filled where possible).",
|
||||||
f"Unsigned PDF: {document_key}",
|
f"Unsigned PDF: {document_key}",
|
||||||
]
|
]
|
||||||
if signed and customer_email:
|
if signed and customer_email:
|
||||||
note_lines.append(f"E-sign link emailed to {customer_email}. After signing, print + USPS Priority Mail to the MAC.")
|
note_lines.append(f"E-sign link emailed to {customer_email}. After signing, submit to the MAC (standard) or file via PECOS surrogate access (expedited).")
|
||||||
else:
|
else:
|
||||||
note_lines.append("No customer email or esign infra — send the form for wet signature manually.")
|
note_lines.append("No customer email or esign infra — send the form for signature manually.")
|
||||||
if missing:
|
if missing:
|
||||||
note_lines.append("MANUAL COMPLETION NEEDED:")
|
note_lines.append("MANUAL COMPLETION NEEDED:")
|
||||||
note_lines.extend(f" - {m}" for m in missing)
|
note_lines.extend(f" - {m}" for m in missing)
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,16 @@
|
||||||
if (!email) missing.push("Email");
|
if (!email) missing.push("Email");
|
||||||
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) missing.push("a valid Email");
|
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) missing.push("a valid Email");
|
||||||
|
|
||||||
|
// Medicare enrollment requires the practice/MAC routing state (server-side
|
||||||
|
// required field). Enforce it here so the order can't be submitted without
|
||||||
|
// it and then stall in fulfilment.
|
||||||
|
const activeSlugs = PW.get().batch_slugs
|
||||||
|
|| [document.querySelector(".pw-wizard[data-service]")?.getAttribute("data-service")
|
||||||
|
|| PW.get().service_slug || ""];
|
||||||
|
if (activeSlugs.includes("medicare-enrollment") && !val("npi-practice-state")) {
|
||||||
|
missing.push("Practice State (required for Medicare enrollment)");
|
||||||
|
}
|
||||||
|
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
errDiv.hidden = false;
|
errDiv.hidden = false;
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,15 @@ const description = "New to Medicare or adding a practice location? We assemble
|
||||||
<h2>How it works (without sharing your password)</h2>
|
<h2>How it works (without sharing your password)</h2>
|
||||||
<ol>
|
<ol>
|
||||||
<li>You place the order and give us your NPI and basic practice details.</li>
|
<li>You place the order and give us your NPI and basic practice details.</li>
|
||||||
<li>We send a one-time link to add us as a <strong>Surrogate</strong> in CMS I&A. You never share your login.</li>
|
|
||||||
<li>We pull your NPPES record and identify exactly which 855 forms apply.</li>
|
<li>We pull your NPPES record and identify exactly which 855 forms apply.</li>
|
||||||
<li>We file in <strong>Internet-based PECOS</strong> under our own CMS credentials and track it through your MAC.</li>
|
<li>We prepare your CMS-855 package and send a secure link for you to review and sign the certification.</li>
|
||||||
|
<li>We submit it to your Medicare Administrative Contractor (MAC) and track it through to approval.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<div class="pw-callout">
|
<div class="pw-callout">
|
||||||
<strong>Prefer paper?</strong> We can complete a paper CMS-855 instead,
|
<strong>Need it filed faster?</strong> Choose <strong>expedited</strong> at
|
||||||
send it for your wet signature, and mail it to your Medicare
|
checkout and add us as a <strong>Surrogate</strong> in CMS Identity &
|
||||||
Administrative Contractor (CMS does not accept faxed 855s).
|
Access. We then file directly in PECOS under our own credentials and
|
||||||
|
capture the tracking ID the same day. You never share your login.
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,8 @@ const description = "CMS requires every enrolled provider and supplier to revali
|
||||||
password.</strong>
|
password.</strong>
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Paper CMS-855 (easiest, nothing to set up).</strong> We complete the correct CMS-855 form, you e-sign the certification page from a secure link (about a minute), and we print it and mail it to your Medicare Administrative Contractor (MAC). CMS does not accept faxed 855s, so we handle the mailing.</li>
|
<li><strong>Standard filing (nothing to set up).</strong> We complete the correct CMS-855, you approve and sign the certification from a secure link in about a minute, and we submit it to your Medicare Administrative Contractor (MAC) and track it to confirmation.</li>
|
||||||
<li><strong>Internet-based PECOS (faster, needs a CMS account).</strong> You add us as a <strong>Surrogate</strong> in the CMS Identity & Access (I&A) system; we then file in PECOS under our own credentials and capture the tracking ID.</li>
|
<li><strong>Expedited filing (needs a CMS account).</strong> You add us as a <strong>Surrogate</strong> in the CMS Identity & Access (I&A) system; we then file directly in PECOS under our own credentials and capture the tracking ID the same day.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue