feat(npi): healthcare marketing pages, nav dropdown, NPI lookup API + free tool + companion data migration/loader

This commit is contained in:
justin 2026-06-05 01:33:36 -05:00
parent f349d519c6
commit 4b0155542e
9 changed files with 1176 additions and 2 deletions

View file

@ -0,0 +1,69 @@
-- ─────────────────────────────────────────────────────────────────────
-- NPI / Healthcare provider compliance lookup data.
--
-- Powers /tools/npi-compliance-check. The live identity/status comes from
-- the free public NPPES Registry API at query time; these tables hold the
-- *dateable hooks* and exclusion data that the public NPI API does NOT
-- expose, all keyed by NPI:
--
-- npi_revalidation_due — CMS Medicare revalidation due dates (the flagship
-- dateable hook; ~218k providers are PAST DUE).
-- npi_exclusions — OIG LEIE exclusion list (matched by NPI when present).
-- npi_optout — Medicare opt-out affidavits (end dates).
--
-- Source files (free, public):
-- data.cms.gov Revalidation Due List, Medicare Opt Out
-- oig.hhs.gov LEIE downloadable database
-- ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS npi_revalidation_due (
id SERIAL PRIMARY KEY,
npi TEXT NOT NULL,
enrollment_id TEXT,
first_name TEXT,
last_name TEXT,
organization_name TEXT,
enrollment_state TEXT,
enrollment_type TEXT, -- CMS enrollment type code
provider_type TEXT,
specialty TEXT,
revalidation_due_date DATE, -- NULL = "TBD" in CMS data
adjusted_due_date DATE,
reassign_to TEXT,
receiving_reassignment TEXT,
loaded_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_npi_reval_npi ON npi_revalidation_due (npi);
CREATE TABLE IF NOT EXISTS npi_exclusions (
id SERIAL PRIMARY KEY,
npi TEXT, -- '0000000000' / blank when OIG has none
last_name TEXT,
first_name TEXT,
middle_name TEXT,
business_name TEXT,
general_category TEXT,
specialty TEXT,
state TEXT,
exclusion_type TEXT, -- e.g. 1128a1
exclusion_date DATE,
reinstatement_date DATE, -- NULL when still excluded
loaded_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_npi_excl_npi ON npi_exclusions (npi) WHERE npi IS NOT NULL AND npi <> '0000000000';
CREATE TABLE IF NOT EXISTS npi_optout (
id SERIAL PRIMARY KEY,
npi TEXT,
first_name TEXT,
last_name TEXT,
specialty TEXT,
optout_effective_date DATE,
optout_end_date DATE,
state TEXT,
loaded_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_npi_optout_npi ON npi_optout (npi);
-- Reuse the existing free-tool logging table (compliance_check_log) for NPI
-- checks; no schema change needed there.

View file

@ -47,6 +47,7 @@ import portalEsignGenericRouter from "./routes/portal-esign-generic.js";
import pucRouter from "./routes/puc.js";
import fccCarrierRegRouter from "./routes/fcc-carrier-registration.js";
import dotLookupRouter from "./routes/dot-lookup.js";
import npiLookupRouter from "./routes/npi-lookup.js";
import surveyRouter from "./routes/survey.js";
import orderTimelineRouter from "./routes/order-timeline.js";
@ -122,6 +123,7 @@ app.use(foreignQualRouter);
app.use(pucRouter);
app.use(fccCarrierRegRouter);
app.use(dotLookupRouter);
app.use(npiLookupRouter);
app.use(surveyRouter);
app.use(orderTimelineRouter);
app.use(adminCryptoRouter);

View file

@ -0,0 +1,361 @@
/**
* NPI / Healthcare provider compliance lookup.
*
* GET /api/v1/npi/lookup?npi=1234567893
* GET /api/v1/npi/search?name=Acme+Clinic&state=CA
*
* Combines the free public NPPES Registry API (live identity + status) with
* local CMS/OIG companion tables (revalidation due dates, exclusions, opt-out)
* to produce a green/yellow/red compliance summary, mirroring dot-lookup.ts.
*/
import { Router } from "express";
import { pool } from "../db.js";
const router = Router();
const NPPES_BASE = "https://npiregistry.cms.hhs.gov/api/";
// ── Helpers ─────────────────────────────────────────────────────────
async function nppesFetch(params: Record<string, string>): Promise<any> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 6000);
try {
const qs = new URLSearchParams({ version: "2.1", ...params }).toString();
const resp = await fetch(`${NPPES_BASE}?${qs}`, {
signal: controller.signal,
headers: { Accept: "application/json" },
});
clearTimeout(timer);
if (!resp.ok) return null;
return await resp.json();
} catch {
clearTimeout(timer);
return null;
}
}
type CheckStatus = "green" | "yellow" | "red" | "unknown";
interface ComplianceCheck {
id: string;
label: string;
status: CheckStatus;
detail: string;
action_url?: string | null;
}
function daysBetween(a: Date, b: Date): number {
return Math.round((a.getTime() - b.getTime()) / 86400000);
}
function fmtDate(d: Date | null): string {
if (!d) return "—";
return d.toISOString().slice(0, 10);
}
function nppesName(basic: any): string {
if (!basic) return "Unknown provider";
if (basic.organization_name) return basic.organization_name;
const parts = [basic.first_name, basic.middle_name, basic.last_name].filter(Boolean);
const name = parts.join(" ").trim();
return name || basic.name || "Unknown provider";
}
// ── Lookup by NPI ───────────────────────────────────────────────────
router.get("/api/v1/npi/lookup", async (req, res) => {
const rawNpi = String(req.query.npi || "").replace(/\D/g, "");
if (!/^\d{10}$/.test(rawNpi)) {
res.status(400).json({ error: "Provide a valid 10-digit NPI." });
return;
}
const startedAt = Date.now();
try {
// 1) Live NPPES identity + status
const nppes = await nppesFetch({ number: rawNpi });
const result = nppes?.results?.[0] || null;
if (!result) {
res.status(404).json({
error: "NPI not found in NPPES.",
npi: rawNpi,
});
return;
}
const basic = result.basic || {};
const name = nppesName(basic);
const status = basic.status || null; // "A" active, "D"/null = deactivated
const enumType = result.enumeration_type || null; // NPI-1 individual, NPI-2 org
const lastUpdated = basic.last_updated ? new Date(basic.last_updated) : null;
const enumerationDate = basic.enumeration_date ? new Date(basic.enumeration_date) : null;
const deactivationDate = basic.deactivation_date ? new Date(basic.deactivation_date) : null;
const primaryTaxonomy = (result.taxonomies || []).find((t: any) => t.primary) || (result.taxonomies || [])[0] || null;
const locationAddr = (result.addresses || []).find((a: any) => a.address_purpose === "LOCATION") || (result.addresses || [])[0] || null;
const practiceState = locationAddr?.state || null;
// 2) Companion data joins (best-effort; tables may be empty pre-load)
const [revalRes, exclRes, optoutRes] = await Promise.all([
pool.query(
`SELECT revalidation_due_date, adjusted_due_date, enrollment_type, specialty, enrollment_state
FROM npi_revalidation_due WHERE npi = $1 ORDER BY id LIMIT 1`,
[rawNpi]
).catch(() => ({ rows: [] as any[] })),
pool.query(
`SELECT exclusion_type, exclusion_date, reinstatement_date, general_category
FROM npi_exclusions
WHERE npi = $1 AND npi <> '0000000000'
ORDER BY exclusion_date DESC NULLS LAST LIMIT 1`,
[rawNpi]
).catch(() => ({ rows: [] as any[] })),
pool.query(
`SELECT optout_effective_date, optout_end_date, specialty
FROM npi_optout WHERE npi = $1 ORDER BY optout_end_date DESC NULLS LAST LIMIT 1`,
[rawNpi]
).catch(() => ({ rows: [] as any[] })),
]);
const reval = revalRes.rows[0] || null;
const excl = exclRes.rows[0] || null;
const optout = optoutRes.rows[0] || null;
const now = new Date();
const checks: ComplianceCheck[] = [];
// ── Check 1: NPI active status ──────────────────────────────────
if (status === "A" && !deactivationDate) {
checks.push({
id: "npi-active",
label: "NPI registration",
status: "green",
detail: "Your NPI is active in NPPES.",
action_url: null,
});
} else {
checks.push({
id: "npi-active",
label: "NPI registration",
status: "red",
detail: deactivationDate
? `Your NPI was deactivated on ${fmtDate(deactivationDate)}. You cannot bill until it is reactivated.`
: "Your NPI is not active in NPPES. Reactivation is required before you can bill.",
action_url: "/order/npi-reactivation",
});
}
// ── Check 2: Medicare revalidation (the dateable hook) ──────────
if (reval) {
const due = reval.adjusted_due_date || reval.revalidation_due_date;
if (!due) {
checks.push({
id: "revalidation",
label: "Medicare revalidation",
status: "yellow",
detail: "CMS lists you as enrolled but has not yet set a revalidation due date (\"TBD\"). We can monitor it for you.",
action_url: "/services/healthcare/npi-revalidation",
});
} else {
const dueDate = new Date(due);
const daysToD = daysBetween(dueDate, now);
if (daysToD < 0) {
checks.push({
id: "revalidation",
label: "Medicare revalidation",
status: "red",
detail: `Your Medicare revalidation was due ${fmtDate(dueDate)}${Math.abs(daysToD)} days ago. CMS may deactivate your billing privileges. File now.`,
action_url: "/order/npi-revalidation",
});
} else if (daysToD <= 180) {
checks.push({
id: "revalidation",
label: "Medicare revalidation",
status: "yellow",
detail: `Your Medicare revalidation is due ${fmtDate(dueDate)}${daysToD} days away. File before the deadline to avoid deactivation.`,
action_url: "/order/npi-revalidation",
});
} else {
checks.push({
id: "revalidation",
label: "Medicare revalidation",
status: "green",
detail: `Your next Medicare revalidation is due ${fmtDate(dueDate)}.`,
action_url: null,
});
}
}
} else {
checks.push({
id: "revalidation",
label: "Medicare revalidation",
status: "unknown",
detail: "We did not find an active Medicare revalidation record for this NPI. If you bill Medicare, confirm your enrollment status in PECOS.",
action_url: "/services/healthcare/medicare-enrollment",
});
}
// ── Check 3: OIG / SAM exclusion ────────────────────────────────
if (excl && !excl.reinstatement_date) {
checks.push({
id: "exclusion",
label: "OIG exclusion screening",
status: "red",
detail: `This NPI matches an active OIG exclusion (${excl.exclusion_type || "type unknown"}, excluded ${fmtDate(excl.exclusion_date ? new Date(excl.exclusion_date) : null)}). Excluded providers cannot bill federal healthcare programs.`,
action_url: "/order/oig-sam-screening",
});
} else {
checks.push({
id: "exclusion",
label: "OIG exclusion screening",
status: "green",
detail: "No active OIG exclusion found for this NPI. Annual screening of your staff is still recommended.",
action_url: "/order/oig-sam-screening",
});
}
// ── Check 4: NPPES record freshness ─────────────────────────────
if (lastUpdated) {
const staleDays = daysBetween(now, lastUpdated);
if (staleDays > 365 * 2) {
checks.push({
id: "nppes-fresh",
label: "NPPES record currency",
status: "yellow",
detail: `Your NPPES record was last updated ${fmtDate(lastUpdated)} — over 2 years ago. CMS requires updates within 30 days of any change. Re-attest to keep it current.`,
action_url: "/order/nppes-update",
});
} else {
checks.push({
id: "nppes-fresh",
label: "NPPES record currency",
status: "green",
detail: `Your NPPES record was last updated ${fmtDate(lastUpdated)}.`,
action_url: null,
});
}
}
// ── Check 5: Medicare opt-out ───────────────────────────────────
if (optout && optout.optout_end_date) {
const endDate = new Date(optout.optout_end_date);
const daysToEnd = daysBetween(endDate, now);
if (daysToEnd >= 0 && daysToEnd <= 365) {
checks.push({
id: "optout",
label: "Medicare opt-out",
status: "yellow",
detail: `Your Medicare opt-out affidavit ends ${fmtDate(endDate)}. Opt-out auto-renews every 2 years unless you act; if you want to re-enroll, plan ahead.`,
action_url: "/services/healthcare/medicare-enrollment",
});
}
}
const redCount = checks.filter((c) => c.status === "red").length;
const yellowCount = checks.filter((c) => c.status === "yellow").length;
const greenCount = checks.filter((c) => c.status === "green").length;
const severity = redCount > 0 ? "critical" : yellowCount > 0 ? "major" : "clean";
// Fire-and-forget logging (reuse compliance_check_log; NPI stored in frn col)
pool.query(
`INSERT INTO compliance_check_log
(frn, entity_name, ip_address, user_agent, referrer, total_checks, issues_found, severity, check_slugs, response_ms)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
[
rawNpi,
name,
(req.headers["x-forwarded-for"] as string)?.split(",")[0]?.trim() || req.ip || null,
req.headers["user-agent"] || null,
req.headers["referer"] || null,
checks.length,
redCount + yellowCount,
severity,
checks.filter((c) => c.status === "red" || c.status === "yellow").map((c) => c.id),
Date.now() - startedAt,
]
).catch(() => {});
res.json({
npi: rawNpi,
name,
enumeration_type: enumType,
status,
taxonomy: primaryTaxonomy
? { code: primaryTaxonomy.code, desc: primaryTaxonomy.desc, state: primaryTaxonomy.state, license: primaryTaxonomy.license }
: null,
practice_state: practiceState,
enumeration_date: enumerationDate ? fmtDate(enumerationDate) : null,
last_updated: lastUpdated ? fmtDate(lastUpdated) : null,
checks,
summary: {
red: redCount,
yellow: yellowCount,
green: greenCount,
total: checks.length,
},
severity,
checked_at: new Date().toISOString(),
});
} catch (err) {
console.error("[npi-lookup] Error:", err);
if (!res.headersSent) res.status(500).json({ error: "NPI lookup failed." });
}
});
// ── Name search ─────────────────────────────────────────────────────
router.get("/api/v1/npi/search", async (req, res) => {
const name = String(req.query.name || "").trim();
const state = String(req.query.state || "").trim().toUpperCase();
if (name.length < 2) {
res.status(400).json({ error: "Provide a name (at least 2 characters)." });
return;
}
try {
// NPPES supports org name and last name search; try organization first,
// then individual last name. Wildcard with trailing * for partials.
const params: Record<string, string> = { limit: "15" };
if (state && state.length === 2) params.state = state;
// Heuristic: single token -> last_name; otherwise org name.
const tokens = name.split(/\s+/);
if (tokens.length === 1) {
params.last_name = name + "*";
} else {
params.organization_name = name + "*";
}
let nppes = await nppesFetch(params);
// Fallback: if org search empty, try last name
if (!nppes?.results?.length && !params.last_name) {
delete params.organization_name;
params.last_name = tokens[tokens.length - 1] + "*";
nppes = await nppesFetch(params);
}
const results = (nppes?.results || []).map((r: any) => {
const b = r.basic || {};
const loc = (r.addresses || []).find((a: any) => a.address_purpose === "LOCATION") || (r.addresses || [])[0] || {};
const tax = (r.taxonomies || []).find((t: any) => t.primary) || (r.taxonomies || [])[0] || {};
return {
npi: r.number,
name: nppesName(b),
enumeration_type: r.enumeration_type,
status: b.status,
taxonomy: tax.desc || null,
city: loc.city || null,
state: loc.state || null,
};
});
res.json({ query: name, count: results.length, results });
} catch (err) {
console.error("[npi-search] Error:", err);
if (!res.headersSent) res.status(500).json({ error: "NPI search failed." });
}
});
export default router;

View file

@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""
Load CMS/OIG NPI companion data into Postgres for the NPI compliance check.
Populates:
npi_revalidation_due <- CMS Revalidation Due List
npi_exclusions <- OIG LEIE
npi_optout <- CMS Medicare Opt Out
Usage:
DATABASE_URL=postgresql://... python3 scripts/load_npi_companion_data.py \
--dir /tmp/npi_companion
Source CSVs (free/public):
revalidation_due.csv data.cms.gov Medicare Revalidation Due List
leie.csv oig.hhs.gov LEIE downloadable database
optout.csv data.cms.gov Medicare Opt Out
"""
import argparse
import csv
import os
import sys
from datetime import datetime
import psycopg2
from psycopg2.extras import execute_values
DATABASE_URL = os.environ.get("DATABASE_URL", "postgresql://pw:pw@localhost:5432/performancewest")
def parse_date(s):
if not s:
return None
s = s.strip()
if not s or s in ("00000000", "TBD"):
return None
for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%Y%m%d", "%m/%d/%y"):
try:
return datetime.strptime(s, fmt).date()
except ValueError:
continue
return None
def clean_npi(s):
s = (s or "").strip()
return s if s and s != "0000000000" and len(s) == 10 and s.isdigit() else (s or None)
def load_revalidation(conn, path):
rows = []
with open(path, newline="", encoding="utf-8-sig") as f:
for r in csv.DictReader(f):
npi = (r.get("National Provider Identifier") or "").strip()
if not (npi.isdigit() and len(npi) == 10):
continue
rows.append((
npi,
(r.get("Enrollment ID") or "").strip() or None,
(r.get("First Name") or "").strip() or None,
(r.get("Last Name") or "").strip() or None,
(r.get("Organization Name") or "").strip() or None,
(r.get("Enrollment State Code") or "").strip() or None,
(r.get("Enrollment Type") or "").strip() or None,
(r.get("Provider Type Text") or "").strip() or None,
(r.get("Enrollment Specialty") or "").strip() or None,
parse_date(r.get("Revalidation Due Date")),
parse_date(r.get("Adjusted Due Date")),
(r.get("Individual Total Reassign To") or "").strip() or None,
(r.get("Receiving Benefits Reassignment") or "").strip() or None,
))
with conn.cursor() as cur:
cur.execute("TRUNCATE npi_revalidation_due RESTART IDENTITY")
execute_values(cur, """
INSERT INTO npi_revalidation_due
(npi, enrollment_id, first_name, last_name, organization_name,
enrollment_state, enrollment_type, provider_type, specialty,
revalidation_due_date, adjusted_due_date, reassign_to, receiving_reassignment)
VALUES %s
""", rows, page_size=5000)
conn.commit()
return len(rows)
def load_exclusions(conn, path):
rows = []
with open(path, newline="", encoding="utf-8-sig") as f:
for r in csv.DictReader(f):
rows.append((
clean_npi(r.get("NPI")),
(r.get("LASTNAME") or "").strip() or None,
(r.get("FIRSTNAME") or "").strip() or None,
(r.get("MIDNAME") or "").strip() or None,
(r.get("BUSNAME") or "").strip() or None,
(r.get("GENERAL") or "").strip() or None,
(r.get("SPECIALTY") or "").strip() or None,
(r.get("STATE") or "").strip() or None,
(r.get("EXCLTYPE") or "").strip() or None,
parse_date(r.get("EXCLDATE")),
parse_date(r.get("REINDATE")),
))
with conn.cursor() as cur:
cur.execute("TRUNCATE npi_exclusions RESTART IDENTITY")
execute_values(cur, """
INSERT INTO npi_exclusions
(npi, last_name, first_name, middle_name, business_name,
general_category, specialty, state, exclusion_type,
exclusion_date, reinstatement_date)
VALUES %s
""", rows, page_size=5000)
conn.commit()
return len(rows)
def load_optout(conn, path):
rows = []
with open(path, newline="", encoding="utf-8-sig") as f:
for r in csv.DictReader(f):
npi = (r.get("npi") or r.get("NPI") or "").strip()
if not (npi.isdigit() and len(npi) == 10):
continue
rows.append((
npi,
(r.get("First Name") or "").strip() or None,
(r.get("Last Name") or "").strip() or None,
(r.get("Specialty") or "").strip() or None,
parse_date(r.get("Optout Effective Date")),
parse_date(r.get("Optout End Date")),
(r.get("State Code") or "").strip() or None,
))
with conn.cursor() as cur:
cur.execute("TRUNCATE npi_optout RESTART IDENTITY")
execute_values(cur, """
INSERT INTO npi_optout
(npi, first_name, last_name, specialty,
optout_effective_date, optout_end_date, state)
VALUES %s
""", rows, page_size=5000)
conn.commit()
return len(rows)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--dir", default="/tmp/npi_companion")
args = ap.parse_args()
conn = psycopg2.connect(DATABASE_URL)
try:
jobs = [
("revalidation_due.csv", load_revalidation),
("leie.csv", load_exclusions),
("optout.csv", load_optout),
]
for fname, fn in jobs:
path = os.path.join(args.dir, fname)
if not os.path.exists(path):
print(f" SKIP {fname} (not found at {path})")
continue
n = fn(conn, path)
print(f" loaded {n:,} rows from {fname}")
finally:
conn.close()
print("Done.")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,94 @@
---
import Base from "../../../layouts/Base.astro";
const title = "Healthcare Provider Compliance — NPI, NPPES & Medicare Filings";
const description = "We handle the federal paperwork that keeps providers billable: Medicare revalidation (PECOS), NPI reactivation, NPPES updates, enrollment, and OIG/SAM exclusion screening. HIPAA not included.";
---
<Base title={title} description={description}>
<main>
<section class="pw-hero">
<h1>Healthcare Provider Compliance</h1>
<p style="font-size: 1.1rem; max-width: 46rem;">
Your NPI is public. Your Medicare enrollment has a clock on it. We
handle the federal filings that keep you billable so a missed
deadline never deactivates your privileges.
</p>
</section>
<section>
<h2>The obligations we cover</h2>
<div class="pw-card-grid">
<a class="pw-card" href="/services/healthcare/npi-revalidation">
<h3>Medicare Revalidation (PECOS)</h3>
<p class="pw-card-price">$399 per filing</p>
<p>CMS requires every enrolled provider to revalidate every 5 years. Miss it and your billing privileges deactivate. We prepare and file it in PECOS.</p>
</a>
<a class="pw-card" href="/services/healthcare/medicare-enrollment">
<h3>Medicare Enrollment (PECOS)</h3>
<p class="pw-card-price">$499 per filing</p>
<p>New CMS-855 enrollment: taxonomy, practice location, authorized official, EFT. We assemble the package and file it.</p>
</a>
<a class="pw-card" href="/order/npi-reactivation">
<h3>NPI Reactivation</h3>
<p class="pw-card-price">$249 per filing</p>
<p>Reactivate a deactivated NPI in NPPES and re-certify the record so you can resume billing.</p>
</a>
<a class="pw-card" href="/order/nppes-update">
<h3>NPPES Data Update</h3>
<p class="pw-card-price">$149 per filing</p>
<p>CMS requires NPPES updates within 30 days of any change. We update and re-attest your record.</p>
</a>
<a class="pw-card" href="/order/oig-sam-screening">
<h3>OIG / SAM Exclusion Screening</h3>
<p class="pw-card-price">$99 / year</p>
<p>Annual OIG LEIE and SAM exclusion screening for you and your staff, with a compliance certificate.</p>
</a>
<a class="pw-card" href="/order/provider-compliance-bundle">
<h3>Provider Compliance Bundle</h3>
<p class="pw-card-price">$699 / year</p>
<p>Revalidation monitoring, OIG/SAM screening, and NPPES upkeep in one annual package.</p>
</a>
</div>
</section>
<section>
<h2>Not sure where you stand?</h2>
<p>
Run our free NPI compliance check. Enter your NPI and we&apos;ll tell you
whether your record is active, whether your Medicare revalidation is due
or past due, and whether anything in the public registries needs attention.
</p>
<p>
<a class="pw-cta-inline" href="/tools/npi-compliance-check">Run the free NPI compliance check →</a>
</p>
</section>
<section class="pw-note">
<p>
<strong>Note:</strong> We handle federal registry and enrollment
compliance. We do <em>not</em> provide HIPAA privacy/security program
services. That&apos;s a separate specialty.
</p>
</section>
<section class="pw-cta-section">
<a class="pw-cta" href="/services/healthcare/npi-revalidation">Check your revalidation status →</a>
</section>
</main>
</Base>
<style>
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
.pw-hero { padding: 2.5rem 1.5rem; background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%); color: #fff; border-radius: 14px; margin-bottom: 2rem; }
.pw-hero h1 { color: #fff; margin: 0 0 0.5rem; }
.pw-card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; margin: 1rem 0; }
.pw-card { display: block; padding: 1.25rem; border: 1px solid #e2e8f0; border-radius: 10px; text-decoration: none; color: inherit; transition: box-shadow 0.15s; }
.pw-card:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); border-color: #14b8a6; }
.pw-card h3 { margin: 0 0 0.3rem; color: #134e4a; font-size: 1.05rem; }
.pw-card-price { font-weight: 700; color: #0f766e; margin: 0 0 0.5rem; font-size: 1rem; }
.pw-note { border-left: 4px solid #14b8a6; background: #f0fdfa; padding: 0.9rem 1.1rem; border-radius: 0 6px 6px 0; margin: 1.5rem 0; }
.pw-cta-inline { color: #0f766e; font-weight: 600; }
.pw-cta-section { margin: 3rem 0; text-align: center; }
.pw-cta { display: inline-block; padding: 1rem 2rem; background: #0f766e; color: #fff; text-decoration: none; font-weight: 600; border-radius: 8px; font-size: 1.05rem; }
.pw-cta:hover { background: #115e59; }
</style>

View file

@ -0,0 +1,69 @@
---
import Base from "../../../layouts/Base.astro";
const title = "Medicare Enrollment (PECOS) — Get Enrolled and Billable";
const description = "New to Medicare or adding a practice location? We assemble and file your CMS-855 enrollment in PECOS: taxonomy, practice location, authorized official, EFT, and reassignments.";
---
<Base title={title} description={description}>
<main>
<section class="pw-hero">
<h1>Medicare Enrollment (PECOS)</h1>
<p style="font-size: 1.1rem; max-width: 44rem;">
Before you can bill Medicare, you have to be enrolled in PECOS with a
clean CMS-855. We build the package, file it, and shepherd it through
your MAC to approval.
</p>
</section>
<section>
<h2>Who needs to enroll</h2>
<ul>
<li><strong>New providers</strong> just out of training or newly licensed and ready to bill Medicare.</li>
<li><strong>Providers joining a group</strong> who need a reassignment (855R) to the group&apos;s billing.</li>
<li><strong>Groups and suppliers</strong> establishing a new billing entity (855B).</li>
<li><strong>Providers adding a practice location</strong> or changing how they bill.</li>
</ul>
</section>
<section>
<h2>What we file</h2>
<ul>
<li><strong>CMS-855I</strong> — individual physician/non-physician practitioner enrollment.</li>
<li><strong>CMS-855B</strong> — group practice / supplier enrollment.</li>
<li><strong>CMS-855R</strong> — reassignment of benefits to a group.</li>
<li><strong>CMS-588 (EFT)</strong> — electronic funds transfer setup so you get paid.</li>
<li><strong>CMS-460</strong> — participation agreement, if you&apos;re enrolling as participating.</li>
</ul>
<div class="pw-callout">
We confirm your <strong>taxonomy code</strong>, <strong>practice
location</strong>, and <strong>authorized official</strong> match
across NPPES and PECOS before we file, because a mismatch is the #1
cause of enrollment rejections.
</div>
</section>
<section>
<h2>How it works</h2>
<ol>
<li>You place the order and give us your NPI and basic practice details.</li>
<li>We pull your NPPES record and identify exactly which 855 forms apply.</li>
<li>We assemble the package and send it to you for signature/attestation.</li>
<li>We submit in PECOS and track it through your MAC, resolving any development requests.</li>
</ol>
</section>
<section class="pw-cta-section">
<a class="pw-cta" href="/order/medicare-enrollment">Start my Medicare enrollment — $499 →</a>
</section>
</main>
</Base>
<style>
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
.pw-hero { padding: 2.5rem 1.5rem; background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%); color: #fff; border-radius: 14px; margin-bottom: 2rem; }
.pw-hero h1 { color: #fff; margin: 0 0 0.5rem; }
.pw-callout { border-left: 4px solid #14b8a6; background: #f0fdfa; padding: 0.9rem 1.1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; }
.pw-cta-section { margin: 3rem 0; text-align: center; }
.pw-cta { display: inline-block; padding: 1rem 2rem; background: #0f766e; color: #fff; text-decoration: none; font-weight: 600; border-radius: 8px; font-size: 1.05rem; }
.pw-cta:hover { background: #115e59; }
</style>

View file

@ -0,0 +1,84 @@
---
import Base from "../../../layouts/Base.astro";
const title = "Medicare Revalidation Filing (PECOS) — Don't Let Your Billing Privileges Lapse";
const description = "CMS requires every enrolled provider and supplier to revalidate their Medicare enrollment every 5 years. Miss your deadline and your billing privileges are deactivated. We prepare and file your revalidation in PECOS.";
---
<Base title={title} description={description}>
<main>
<section class="pw-hero">
<h1>Medicare Revalidation Filing</h1>
<p style="font-size: 1.1rem; max-width: 44rem;">
Every Medicare-enrolled provider has to revalidate every 5 years. If
you miss your due date, CMS deactivates your billing privileges and
every claim after that bounces. We file your revalidation for you.
</p>
</section>
<section>
<h2>What revalidation is</h2>
<p>
Under <strong>42 CFR § 424.515</strong>, CMS re-verifies the
information of every enrolled provider and supplier on a recurring
cycle (every 5 years for most providers, every 3 years for DMEPOS
suppliers). You revalidate your entire enrollment record in
<strong>PECOS</strong> (the Provider Enrollment, Chain, and Ownership
System) using the CMS-855 family of forms.
</p>
<div class="pw-callout">
<strong>The deadline is real.</strong> CMS publishes a revalidation due
date for every provider. Submit late or not at all and your Medicare
billing privileges are <em>deactivated</em>. Reactivating means a gap
in payments and a fresh round of paperwork.
</div>
</section>
<section>
<h2>How do I know if I&apos;m due?</h2>
<p>
CMS posts a due date for every enrolled provider. Many providers are
already <strong>past due</strong> and don&apos;t know it because the
notification letter went to an old address. Run our free check and
we&apos;ll look up your status:
</p>
<p>
<a class="pw-cta-inline" href="/tools/npi-compliance-check">Check my revalidation status (free) →</a>
</p>
</section>
<section>
<h2>What we do</h2>
<ul>
<li>Pull your current PECOS enrollment record and confirm what CMS has on file.</li>
<li>Reconcile your practice locations, reassignments, ownership, and authorized officials.</li>
<li>Complete the correct CMS-855 (855I for individuals, 855B for groups/suppliers, 855R for reassignments).</li>
<li>Submit the revalidation in PECOS and track it through to approval.</li>
<li>Flag and resolve any development requests from your MAC before they cause a deactivation.</li>
</ul>
</section>
<section>
<h2>Why not do it yourself</h2>
<ul>
<li><strong>PECOS is unforgiving.</strong> One wrong reassignment or a stale practice location triggers a development request, and the clock keeps ticking.</li>
<li><strong>The forms branch.</strong> Whether you file 855I, 855B, 855R, or a combination depends on how you bill. Pick wrong and you start over.</li>
<li><strong>Deactivation is retroactive.</strong> If you miss the date, the gap isn&apos;t forgiven; claims in the gap stay denied.</li>
</ul>
</section>
<section class="pw-cta-section">
<a class="pw-cta" href="/order/npi-revalidation">File my Medicare revalidation — $399 →</a>
</section>
</main>
</Base>
<style>
main { max-width: 900px; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
.pw-hero { padding: 2.5rem 1.5rem; background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%); color: #fff; border-radius: 14px; margin-bottom: 2rem; }
.pw-hero h1 { color: #fff; margin: 0 0 0.5rem; }
.pw-callout { border-left: 4px solid #14b8a6; background: #f0fdfa; padding: 0.9rem 1.1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; }
.pw-cta-inline { color: #0f766e; font-weight: 600; }
.pw-cta-section { margin: 3rem 0; text-align: center; }
.pw-cta { display: inline-block; padding: 1rem 2rem; background: #0f766e; color: #fff; text-decoration: none; font-weight: 600; border-radius: 8px; font-size: 1.05rem; }
.pw-cta:hover { background: #115e59; }
</style>

View file

@ -0,0 +1,327 @@
---
import Base from "../../layouts/Base.astro";
---
<Base
title="NPI Compliance Check — Free Medicare Revalidation & NPPES Status"
description="Enter your 10-digit NPI to instantly check your NPPES status, Medicare revalidation due date, OIG exclusion, and record currency. Free tool from Performance West."
>
<main class="max-w-3xl mx-auto px-4 py-10">
<!-- Breadcrumb -->
<nav class="text-sm text-gray-500 mb-6" aria-label="Breadcrumb">
<a href="/" class="hover:text-teal-600">Home</a>
<span class="mx-1">/</span>
<a href="/tools" class="hover:text-teal-600">Free Tools</a>
<span class="mx-1">/</span>
<span class="text-gray-800 font-medium">NPI Compliance Check</span>
</nav>
<h1 class="text-3xl font-bold text-gray-900 mb-2">NPI Compliance Check</h1>
<p class="text-gray-600 mb-6">
Enter your provider name or 10-digit NPI to instantly check your NPPES status,
Medicare revalidation due date, OIG exclusion screening, and record currency.
</p>
<div class="bg-gray-100 border border-gray-200 rounded-lg p-4 text-sm text-gray-600 mb-8">
This tool queries publicly available NPPES, CMS, and OIG data for informational
purposes only. Always confirm critical compliance matters directly with CMS.
This is not legal advice.
</div>
<!-- Search box -->
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm mb-8">
<!-- Name search -->
<label for="name-input" class="block text-sm font-medium text-gray-700 mb-1">Provider or Organization Name</label>
<div class="flex gap-2 mb-2">
<input type="text" id="name-input" placeholder="e.g. Smith, or Acme Medical Group"
class="flex-1 border border-gray-300 rounded-lg px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-teal-400" />
<input type="text" id="state-input" placeholder="State" maxlength="2"
class="w-20 border border-gray-300 rounded-lg px-3 py-2 text-sm uppercase focus:outline-none focus:ring-2 focus:ring-teal-400" />
<button id="name-search-btn"
class="bg-teal-600 hover:bg-teal-700 text-white font-semibold px-5 py-2 rounded-lg text-sm transition">Search</button>
</div>
<div id="name-results" class="hidden mt-3"></div>
<div class="flex items-center gap-3 my-5">
<div class="flex-1 border-t border-gray-200"></div>
<span class="text-sm text-gray-400">or enter NPI directly</span>
<div class="flex-1 border-t border-gray-200"></div>
</div>
<!-- NPI search -->
<label for="npi-input" class="block text-sm font-medium text-gray-700 mb-1">National Provider Identifier (NPI)</label>
<div class="flex gap-2 mb-2">
<input type="text" id="npi-input" placeholder="1234567893" maxlength="10" inputmode="numeric"
class="flex-1 border border-gray-300 rounded-lg px-4 py-2 font-mono text-lg focus:outline-none focus:ring-2 focus:ring-teal-400" />
<button id="npi-check-btn"
class="bg-gray-700 hover:bg-gray-800 text-white font-semibold px-5 py-2 rounded-lg text-sm transition">Check Compliance</button>
</div>
<p class="text-xs text-gray-400">
Don't know your NPI?
<a href="https://npiregistry.cms.hhs.gov/search" target="_blank" rel="noopener" class="text-teal-600 hover:underline">Look it up in NPPES</a>
</p>
</div>
<!-- Loading -->
<div id="loading" class="hidden text-center py-10">
<svg class="animate-spin h-8 w-8 text-teal-600 mx-auto mb-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
</svg>
<p class="text-gray-700 text-sm font-medium" id="loading-status">Checking NPPES &amp; CMS databases&hellip;</p>
</div>
<!-- Error -->
<div id="error-box" class="hidden bg-red-50 border border-red-200 rounded-lg p-4 text-red-700 text-sm mb-6">
<p id="error-message"></p>
</div>
<!-- Results -->
<div id="results" class="hidden space-y-6">
<div class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<h2 id="provider-name" class="text-xl font-bold text-gray-900"></h2>
<p class="mt-1 text-sm text-gray-600">
NPI: <span id="provider-npi" class="font-mono font-semibold text-gray-800"></span>
</p>
<div id="provider-details" class="mt-2 space-y-1 text-sm text-gray-600"></div>
</div>
<div id="checks-container" class="space-y-4"></div>
<!-- CTA -->
<div id="cta-section" class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<div id="cta-content"></div>
</div>
<p id="checked-at" class="text-xs text-gray-400 text-center"></p>
</div>
</main>
<script type="module" is:inline>
const API = window.__PW_API || "https://api.performancewest.net";
const nameInput = document.getElementById("name-input");
const stateInput = document.getElementById("state-input");
const nameSearchBtn = document.getElementById("name-search-btn");
const nameResults = document.getElementById("name-results");
const npiInput = document.getElementById("npi-input");
const npiCheckBtn = document.getElementById("npi-check-btn");
const loadingEl = document.getElementById("loading");
const errorBox = document.getElementById("error-box");
const errorMessage = document.getElementById("error-message");
const resultsEl = document.getElementById("results");
let currentController = null;
// Service map: each check id -> the order page + price for the CTA.
const SERVICE_MAP = {
"npi-active": { name: "NPI Reactivation", price: 249, url: "/order/npi-reactivation" },
"revalidation": { name: "Medicare Revalidation Filing", price: 399, url: "/order/npi-revalidation" },
"exclusion": { name: "OIG/SAM Exclusion Screening", price: 99, url: "/order/oig-sam-screening" },
"nppes-fresh": { name: "NPPES Data Update", price: 149, url: "/order/nppes-update" },
"optout": { name: "Medicare Enrollment (PECOS)", price: 499, url: "/order/medicare-enrollment" },
};
// --- Name search ---
nameSearchBtn?.addEventListener("click", runNameSearch);
nameInput?.addEventListener("keydown", (e) => { if (e.key === "Enter") runNameSearch(); });
async function runNameSearch() {
const name = nameInput.value.trim();
if (!name) return;
const state = stateInput.value.trim().toUpperCase();
nameResults.classList.remove("hidden");
nameResults.innerHTML = '<p class="text-sm text-gray-500">Searching&hellip;</p>';
try {
const qs = `name=${encodeURIComponent(name)}${state ? "&state=" + encodeURIComponent(state) : ""}`;
const res = await fetch(`${API}/api/v1/npi/search?${qs}`);
if (!res.ok) throw new Error("Search failed");
const data = await res.json();
const items = data.results || [];
if (items.length === 0) {
nameResults.innerHTML = '<p class="text-sm text-gray-500">No results found.</p>';
return;
}
let html = `<p class="text-xs text-gray-400 mb-2">${items.length} result${items.length !== 1 ? "s" : ""} — click to check compliance</p>`;
html += '<div class="space-y-2 max-h-72 overflow-y-auto border border-gray-200 rounded-lg divide-y divide-gray-100">';
for (const item of items) {
html += `<button data-npi="${item.npi}" class="name-result-btn w-full text-left px-4 py-3 flex items-center justify-between gap-3 hover:bg-teal-50 transition cursor-pointer" style="display:flex;">
<div style="min-width:0;flex:1;">
<p style="font-weight:600;font-size:14px;color:#111827;margin:0;">${item.name || "Unknown"}</p>
<p style="font-size:12px;color:#6b7280;margin:2px 0 0;">${item.taxonomy || ""}${item.city ? " — " + item.city + ", " + (item.state || "") : ""}</p>
</div>
<div style="text-align:right;flex-shrink:0;">
<p style="font-family:monospace;font-size:12px;color:#0f766e;font-weight:600;margin:0;">${item.npi}</p>
<p style="font-size:11px;color:${item.status === "A" ? "#059669" : "#dc2626"};margin:2px 0 0;">${item.status === "A" ? "Active" : "Inactive"}</p>
</div>
</button>`;
}
html += "</div>";
nameResults.innerHTML = html;
nameResults.querySelectorAll(".name-result-btn").forEach((btn) => {
btn.addEventListener("click", () => {
npiInput.value = btn.getAttribute("data-npi");
runCheck();
});
});
} catch (err) {
nameResults.innerHTML = `<p class="text-sm text-red-600">Search error: ${err.message || "Unknown error"}</p>`;
}
}
// --- NPI compliance check ---
npiCheckBtn.addEventListener("click", runCheck);
npiInput.addEventListener("keydown", (e) => { if (e.key === "Enter") runCheck(); });
async function runCheck() {
const raw = npiInput.value.trim().replace(/\D/g, "");
if (raw.length !== 10) {
showError("NPI must be exactly 10 digits (e.g. 1234567893). You entered " + raw.length + " digits.");
return;
}
npiInput.value = raw;
const url = new URL(window.location);
url.searchParams.set("npi", raw);
history.replaceState(null, "", url);
resultsEl.classList.add("hidden");
errorBox.classList.add("hidden");
loadingEl.classList.remove("hidden");
if (currentController) currentController.abort();
const controller = new AbortController();
currentController = controller;
try {
const timeout = setTimeout(() => controller.abort(), 30000);
const res = await fetch(`${API}/api/v1/npi/lookup?npi=${raw}`, { signal: controller.signal });
clearTimeout(timeout);
if (currentController !== controller) return;
if (!res.ok) {
const body = await res.json().catch(() => ({}));
if (res.status === 404) {
showError(`NPI ${raw} was not found in NPPES. Verify your number at <a href="https://npiregistry.cms.hhs.gov/search" target="_blank" style="color:#0f766e;text-decoration:underline;">NPPES Registry</a>.`, true);
return;
}
throw new Error(body.error || "NPI lookup failed. Please try again.");
}
renderResults(await res.json());
} catch (err) {
if (err.name === "AbortError") {
showError("The CMS databases are taking too long to respond. Please try again in a moment.");
} else {
showError(err.message || "Could not reach the CMS databases right now. Please try again.");
}
} finally {
loadingEl.classList.add("hidden");
}
}
function showError(msg, allowHtml) {
if (allowHtml) errorMessage.innerHTML = msg;
else errorMessage.textContent = msg;
errorBox.classList.remove("hidden");
}
const colorMap = {
green: { bg: "bg-green-50", border: "border-green-200", iconColor: "text-green-600", textColor: "text-green-800" },
yellow: { bg: "bg-amber-50", border: "border-amber-200", iconColor: "text-amber-600", textColor: "text-amber-800" },
red: { bg: "bg-red-50", border: "border-red-200", iconColor: "text-red-600", textColor: "text-red-800" },
unknown: { bg: "bg-gray-50", border: "border-gray-200", iconColor: "text-gray-500", textColor: "text-gray-700" },
};
const icons = {
green: `<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>`,
yellow: `<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01M10.29 3.86l-8.8 15.32A1 1 0 002.36 21h19.28a1 1 0 00.87-1.5l-9.64-16.64a1 1 0 00-1.74 0z"/></svg>`,
red: `<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>`,
unknown: `<svg class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01"/></svg>`,
};
function renderResults(data) {
errorBox.classList.add("hidden");
document.getElementById("provider-name").textContent = data.name || "Unknown Provider";
document.getElementById("provider-npi").textContent = data.npi;
const details = document.getElementById("provider-details");
const bits = [];
if (data.enumeration_type) bits.push(`<span class="font-medium text-gray-700">Type:</span> ${data.enumeration_type === "NPI-2" ? "Organization" : "Individual"}`);
if (data.taxonomy?.desc) bits.push(`<span class="font-medium text-gray-700">Taxonomy:</span> ${data.taxonomy.desc}`);
if (data.practice_state) bits.push(`<span class="font-medium text-gray-700">Practice State:</span> ${data.practice_state}`);
if (data.enumeration_date) bits.push(`<span class="font-medium text-gray-700">Enumerated:</span> ${data.enumeration_date}`);
details.innerHTML = bits.map((b) => `<p>${b}</p>`).join("");
const container = document.getElementById("checks-container");
container.innerHTML = "";
for (const check of (data.checks || [])) {
const status = check.status || "unknown";
const c = colorMap[status] || colorMap.unknown;
const card = document.createElement("div");
card.className = `${c.bg} ${c.border} border rounded-xl p-4 flex items-start gap-3`;
card.innerHTML = `<div class="${c.iconColor} mt-0.5 flex-shrink-0">${icons[status] || icons.unknown}</div>
<div class="flex-1">
<p class="font-semibold ${c.textColor}">${check.label || check.id}</p>
${check.detail ? `<p class="text-sm ${c.textColor} mt-1">${check.detail}</p>` : ""}
</div>`;
container.appendChild(card);
}
renderCta(data);
document.getElementById("checked-at").textContent = "Checked at " + new Date().toLocaleString();
resultsEl.classList.remove("hidden");
document.getElementById("results").scrollIntoView({ behavior: "smooth", block: "start" });
}
function renderCta(data) {
const checks = data.checks || [];
const npi = data.npi;
const services = [];
const seen = new Set();
for (const check of checks) {
if (check.status === "green") continue;
const svc = SERVICE_MAP[check.id];
if (svc && !seen.has(check.id)) { services.push({ ...svc, id: check.id }); seen.add(check.id); }
}
const ctaContent = document.getElementById("cta-content");
if (services.length > 0) {
let html = `<div style="background:#fef2f2;border:2px solid #fca5a5;border-radius:10px;padding:16px;margin-bottom:16px;">
<p style="font-size:14px;color:#991b1b;font-weight:700;margin:0 0 6px;">&#9888; ${services.length} item${services.length > 1 ? "s need" : " needs"} attention</p>
<p style="font-size:13px;color:#7f1d1d;margin:0;">We can prepare and file these with CMS on your behalf so your billing privileges stay intact.</p>
</div>`;
html += `<h3 class="text-lg font-bold text-gray-900 mb-1">Recommended filings</h3>`;
html += `<p class="text-sm text-gray-600 mb-4">Based on your NPPES, CMS, and OIG records.</p>`;
html += `<div class="space-y-2">`;
for (const svc of services) {
html += `<a href="${svc.url}?npi=${npi}" class="flex items-center justify-between gap-3 p-3 rounded-lg border border-gray-200 hover:bg-teal-50 hover:border-teal-300 transition cursor-pointer" style="text-decoration:none;color:inherit;">
<span class="font-medium text-gray-900">${svc.name}</span>
<span class="font-semibold text-teal-700 whitespace-nowrap">$${svc.price} &rarr;</span>
</a>`;
}
html += `</div>`;
html += `<div class="mt-5 text-center">
<a href="/order/provider-compliance-bundle?npi=${npi}" style="background:#0f766e;color:#fff;font-weight:700;padding:12px 28px;border-radius:8px;display:inline-block;font-size:15px;text-decoration:none;">
Get the Provider Compliance Bundle ($699/yr) &rarr;
</a>
<p style="font-size:11px;color:#6b7280;margin-top:8px;">Revalidation monitoring, screening, and NPPES upkeep in one package.</p>
</div>`;
ctaContent.innerHTML = html;
} else {
ctaContent.innerHTML = `<h3 class="text-lg font-bold text-green-800 mb-2">Looking good &mdash; all checks passed</h3>
<p class="text-sm text-gray-600 mb-4">Your NPI and Medicare records appear current. No action is needed right now.</p>
<a href="/order/oig-sam-screening?npi=${data.npi}" class="inline-block bg-teal-600 hover:bg-teal-700 text-white font-semibold px-5 py-2 rounded-lg text-sm transition" style="text-decoration:none;">
Add annual OIG/SAM screening &mdash; $99/yr
</a>`;
}
}
// --- Auto-fill from URL ---
(function init() {
const params = new URLSearchParams(window.location.search);
const npi = params.get("npi");
const name = params.get("name");
if (npi) { npiInput.value = npi; runCheck(); }
else if (name) { nameInput.value = name; runNameSearch(); }
})();
</script>
</Base>

File diff suppressed because one or more lines are too long