Verified firsthand against the live CMS-10114 (Rev. 02/25, OMB 0938-0931): - Section 1A confirms paper is valid for Change of Information (#2) AND Reactivation (#4), not just initial enumeration. Resolves the UNCERTAIN flag. - Current mailing address is CMS NPI Enumerator Services, Mail Stop DO-01-51, 7500 Security Blvd, Baltimore MD 21244. The old Fargo PO Box 6059 is retired; corrected in mac_routing.NPI_ENUMERATOR + all docs. - No electronic no-login equivalent exists for CMS (NPI Registry API is read-only; PECOS/NPPES-IA require login), unlike FMCSA's ask.fmcsa ticket form. So tiers stay: Standard=paper CMS-10114 (no login), Expedited=NPPES surrogate. New: cms10114_pdf_filler.py fills the flat official form via text overlay (reason checkbox + NPI + Section 2A identity + Section 4A cert name + signature anchor); wired into npi_provider._generate_10114_for_signing for nppes-update. Signed forms route to the NPI Enumerator via the existing daily batch. Tests: test_cms10114.py 27/27, test_paper_batch.py 15/15, Astro build 58 pages.
128 lines
5 KiB
Python
128 lines
5 KiB
Python
"""Tests for the CMS-10114 NPI Application/Update filler (Standard no-login path).
|
|
|
|
Verifies the overlay-based filler:
|
|
- selects the correct Reason-for-Submittal checkbox per reason/slug
|
|
- places the NPI on the correct line for each reason
|
|
- writes the provider name into Section 2A and the Section 4A certification
|
|
- produces a signature anchor on the certification page
|
|
- surfaces the right "manual completion" notes
|
|
- mails to the verified Baltimore NPI Enumerator address (via mac_routing)
|
|
|
|
Run: python scripts/tests/test_cms10114.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
|
|
import importlib.util # noqa: E402
|
|
|
|
import pdfplumber # noqa: E402
|
|
from workers import mac_routing as mr # noqa: E402
|
|
|
|
|
|
def _load_module(name, rel_path):
|
|
spec = importlib.util.spec_from_file_location(name, ROOT / rel_path)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
# Load the filler directly to avoid document_gen/__init__ side-effects (python-docx).
|
|
f = _load_module("cms10114_pdf_filler",
|
|
"scripts/document_gen/templates/cms10114_pdf_filler.py")
|
|
|
|
H = 792.0
|
|
_fails = 0
|
|
|
|
|
|
def check(name, cond):
|
|
global _fails
|
|
status = "PASS" if cond else "FAIL"
|
|
if not cond:
|
|
_fails += 1
|
|
print(f" {status} {name}")
|
|
|
|
|
|
def _tokens(pdf_bytes, page_index):
|
|
import io
|
|
pdf = pdfplumber.open(io.BytesIO(pdf_bytes))
|
|
page = pdf.pages[page_index]
|
|
return {w["text"]: (w["x0"], H - w["bottom"]) for w in page.extract_words()}
|
|
|
|
|
|
SAMPLE = {
|
|
"provider_name": "Jane Q Smith",
|
|
"npi": "1234567893",
|
|
"practice_state": "CA",
|
|
"enumeration_type": "NPI-1",
|
|
}
|
|
|
|
|
|
def main():
|
|
print("reason normalization")
|
|
check("slug nppes-update -> change", f.normalize_reason("nppes-update") == "change")
|
|
check("slug npi-reactivation -> reactivation", f.normalize_reason("npi-reactivation") == "reactivation")
|
|
check("explicit 'deactivation' kept", f.normalize_reason("deactivation") == "deactivation")
|
|
check("unknown -> change (safe default)", f.normalize_reason("zzz") == "change")
|
|
|
|
print("checkbox + NPI placement per reason (page 2)")
|
|
# Expected checkbox X positions (lower-left) per reason, from the form layout.
|
|
expect = {
|
|
"change": {"X": (48, 582), "NPI": (132, 570)},
|
|
"reactivation": {"X": (325, 506), "NPI": (408, 529)},
|
|
"deactivation": {"X": (325, 608), "NPI": (408, 599)},
|
|
}
|
|
for reason, exp in expect.items():
|
|
pdf, anchors, missing = f.fill_cms10114(SAMPLE, reason=reason)
|
|
toks = _tokens(pdf, f.PAGE_BASIC)
|
|
x = toks.get("X")
|
|
npi = toks.get("1234567893")
|
|
check(f"{reason}: X near {exp['X']}",
|
|
x and abs(x[0] - exp["X"][0]) < 6 and abs(x[1] - exp["X"][1]) < 6)
|
|
check(f"{reason}: NPI near {exp['NPI']}",
|
|
npi and abs(npi[0] - exp["NPI"][0]) < 6 and abs(npi[1] - exp["NPI"][1]) < 6)
|
|
|
|
print("identity + certification name (Section 2A / 4A)")
|
|
pdf, anchors, missing = f.fill_cms10114(SAMPLE, reason="change")
|
|
p2 = _tokens(pdf, f.PAGE_BASIC)
|
|
p4 = _tokens(pdf, f.PAGE_CERT)
|
|
check("Section 2A first name placed", "Jane" in p2)
|
|
check("Section 2A last name placed", "Smith" in p2)
|
|
check("Section 4A cert first name placed", "Jane" in p4)
|
|
check("Section 4A cert last name placed", "Smith" in p4)
|
|
|
|
print("signature anchor")
|
|
check("exactly one signature anchor", len(anchors) == 1)
|
|
check("anchor on certification page", anchors and anchors[0]["page"] == f.PAGE_CERT)
|
|
check("anchor field is 'signer'", anchors and anchors[0]["field"] == "signer")
|
|
|
|
print("manual-completion notes")
|
|
_, _, m_change = f.fill_cms10114(SAMPLE, reason="change")
|
|
_, _, m_react = f.fill_cms10114(SAMPLE, reason="reactivation")
|
|
_, _, m_deact = f.fill_cms10114(SAMPLE, reason="deactivation")
|
|
check("change asks to mark changing fields", any("changing" in x.lower() for x in m_change))
|
|
check("reactivation asks for reason text", any("reactivation reason" in x.lower() for x in m_react))
|
|
check("deactivation asks for reason box", any("deactivation reason" in x.lower() for x in m_deact))
|
|
_, _, m_noni = f.fill_cms10114({"provider_name": "No NPI"}, reason="change")
|
|
check("missing NPI flagged", any("npi is required" in x.lower() for x in m_noni))
|
|
_, _, m_org = f.fill_cms10114({**SAMPLE, "enumeration_type": "NPI-2"}, reason="change")
|
|
check("org (NPI-2) flagged for 4B path", any("organization" in x.lower() for x in m_org))
|
|
|
|
print("routing destination = verified Baltimore Enumerator")
|
|
addr = " ".join(mr.NPI_ENUMERATOR.address_lines).lower()
|
|
check("destination is Baltimore (not Fargo)", "baltimore" in addr and "fargo" not in addr)
|
|
check("destination has the Mail Stop", "do-01-51" in addr)
|
|
|
|
print()
|
|
if _fails:
|
|
print(f"FAILED: {_fails} checks")
|
|
sys.exit(1)
|
|
print("all CMS-10114 checks passed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|