feat(healthcare): prove revalidation is real via official CMS data + self-verify
Skepticism ("is this even real?") is the top objection. The data IS accurate
(verified our subscribers' NPIs match the official CMS Revalidation Due Date List
exactly), so this is a credibility-presentation fix:
1. Email: replace the plain detail row with an "Official record - CMS Medicare
Revalidation Due Date List" card (NPI, legal name, due date, days overdue)
plus a "Verify on CMS.gov" button. Clearly labeled as our presentation of
public CMS data, not a CMS screenshot (no impersonation).
2. API: npi/lookup now pulls the revalidation due date LIVE from the public CMS
dataset (data.cms.gov) instead of the empty local table, and returns a
revalidation{ due_date, source, cms_legal_name, verify_url } proof object.
3. Tool: /tools/npi-compliance-check shows a live "official record" card with a
self-verify link when CMS returns a due date.
Builder now stores reval_due_date/days_overdue as separate attribs for the card
(existing 194 subscribers backfilled from their detail string).
This commit is contained in:
parent
a732423f04
commit
483f185861
4 changed files with 143 additions and 8 deletions
|
|
@ -35,6 +35,56 @@ async function nppesFetch(params: Record<string, string>): Promise<any> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Live, authoritative CMS "Revalidation Due Date List" lookup by NPI. This is
|
||||||
|
// the same public .gov dataset the provider can check themselves at
|
||||||
|
// data.cms.gov/tools/medicare-revalidation-list, so it is the strongest proof
|
||||||
|
// the due date is real (not from our database). Returns null on miss/error so
|
||||||
|
// callers fall back to the local companion table.
|
||||||
|
const CMS_REVAL_API =
|
||||||
|
"https://data.cms.gov/data-api/v1/dataset/3746498e-874d-45d8-9c69-68603cafea60/data";
|
||||||
|
|
||||||
|
interface CmsRevalRecord {
|
||||||
|
revalidation_due_date: string | null;
|
||||||
|
adjusted_due_date: string | null;
|
||||||
|
enrollment_type: string | null;
|
||||||
|
specialty: string | null;
|
||||||
|
enrollment_state: string | null;
|
||||||
|
legal_name: string | null;
|
||||||
|
source: "cms_live";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cmsRevalFetch(npi: string): Promise<CmsRevalRecord | null> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), 6000);
|
||||||
|
try {
|
||||||
|
const qs = `filter[National Provider Identifier]=${encodeURIComponent(npi)}`;
|
||||||
|
const resp = await fetch(`${CMS_REVAL_API}?${qs}`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
});
|
||||||
|
clearTimeout(timer);
|
||||||
|
if (!resp.ok) return null;
|
||||||
|
const rows = (await resp.json()) as any[];
|
||||||
|
if (!Array.isArray(rows) || rows.length === 0) return null;
|
||||||
|
// Prefer the row with a concrete (non-TBD) due date.
|
||||||
|
const norm = (v: any) => (typeof v === "string" && v && v.toUpperCase() !== "TBD" ? v : null);
|
||||||
|
const row =
|
||||||
|
rows.find((r) => norm(r["Adjusted Due Date"]) || norm(r["Revalidation Due Date"])) || rows[0];
|
||||||
|
return {
|
||||||
|
revalidation_due_date: norm(row["Revalidation Due Date"]),
|
||||||
|
adjusted_due_date: norm(row["Adjusted Due Date"]),
|
||||||
|
enrollment_type: row["Enrollment Type"] || null,
|
||||||
|
specialty: row["Enrollment Specialty"] || null,
|
||||||
|
enrollment_state: row["Enrollment State Code"] || null,
|
||||||
|
legal_name: row["Organization Name"] || [row["First Name"], row["Last Name"]].filter(Boolean).join(" ") || null,
|
||||||
|
source: "cms_live",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
clearTimeout(timer);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type CheckStatus = "green" | "yellow" | "red" | "unknown";
|
type CheckStatus = "green" | "yellow" | "red" | "unknown";
|
||||||
|
|
||||||
interface ComplianceCheck {
|
interface ComplianceCheck {
|
||||||
|
|
@ -98,8 +148,11 @@ router.get("/api/v1/npi/lookup", async (req, res) => {
|
||||||
const locationAddr = (result.addresses || []).find((a: any) => a.address_purpose === "LOCATION") || (result.addresses || [])[0] || null;
|
const locationAddr = (result.addresses || []).find((a: any) => a.address_purpose === "LOCATION") || (result.addresses || [])[0] || null;
|
||||||
const practiceState = locationAddr?.state || null;
|
const practiceState = locationAddr?.state || null;
|
||||||
|
|
||||||
// 2) Companion data joins (best-effort; tables may be empty pre-load)
|
// 2) Companion data joins. Revalidation comes LIVE from the public CMS
|
||||||
const [revalRes, exclRes, optoutRes] = await Promise.all([
|
// Revalidation Due Date List (authoritative + provider-verifiable); the
|
||||||
|
// local table is a fallback only (it may be empty pre-load).
|
||||||
|
const [cmsReval, revalRes, exclRes, optoutRes] = await Promise.all([
|
||||||
|
cmsRevalFetch(rawNpi),
|
||||||
pool.query(
|
pool.query(
|
||||||
`SELECT revalidation_due_date, adjusted_due_date, enrollment_type, specialty, enrollment_state
|
`SELECT revalidation_due_date, adjusted_due_date, enrollment_type, specialty, enrollment_state
|
||||||
FROM npi_revalidation_due WHERE npi = $1 ORDER BY id LIMIT 1`,
|
FROM npi_revalidation_due WHERE npi = $1 ORDER BY id LIMIT 1`,
|
||||||
|
|
@ -119,7 +172,13 @@ router.get("/api/v1/npi/lookup", async (req, res) => {
|
||||||
).catch(() => ({ rows: [] as any[] })),
|
).catch(() => ({ rows: [] as any[] })),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const reval = revalRes.rows[0] || null;
|
// Prefer live CMS data; fall back to the local companion table.
|
||||||
|
const reval = cmsReval || revalRes.rows[0] || null;
|
||||||
|
const revalSource: "cms_live" | "local" | null = cmsReval
|
||||||
|
? "cms_live"
|
||||||
|
: revalRes.rows[0]
|
||||||
|
? "local"
|
||||||
|
: null;
|
||||||
const excl = exclRes.rows[0] || null;
|
const excl = exclRes.rows[0] || null;
|
||||||
const optout = optoutRes.rows[0] || null;
|
const optout = optoutRes.rows[0] || null;
|
||||||
|
|
||||||
|
|
@ -289,6 +348,16 @@ router.get("/api/v1/npi/lookup", async (req, res) => {
|
||||||
enumeration_date: enumerationDate ? fmtDate(enumerationDate) : null,
|
enumeration_date: enumerationDate ? fmtDate(enumerationDate) : null,
|
||||||
last_updated: lastUpdated ? fmtDate(lastUpdated) : null,
|
last_updated: lastUpdated ? fmtDate(lastUpdated) : null,
|
||||||
checks,
|
checks,
|
||||||
|
// Verification proof: the revalidation due date (when present) is from the
|
||||||
|
// live public CMS dataset the provider can check themselves.
|
||||||
|
revalidation: reval
|
||||||
|
? {
|
||||||
|
due_date: (reval.adjusted_due_date || reval.revalidation_due_date) ?? null,
|
||||||
|
source: revalSource,
|
||||||
|
cms_legal_name: (reval as any).legal_name ?? null,
|
||||||
|
verify_url: "https://data.cms.gov/tools/medicare-revalidation-list",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
summary: {
|
summary: {
|
||||||
red: redCount,
|
red: redCount,
|
||||||
yellow: yellowCount,
|
yellow: yellowCount,
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,38 @@
|
||||||
<div style="font-size:13px;color:#065f46;line-height:1.7;">If you do not revalidate, CMS will <strong>deactivate your Medicare billing privileges</strong> — claims stop paying and you must re-enroll from scratch, losing your effective date and any retroactive billing.</div>
|
<div style="font-size:13px;color:#065f46;line-height:1.7;">If you do not revalidate, CMS will <strong>deactivate your Medicare billing privileges</strong> — claims stop paying and you must re-enroll from scratch, losing your effective date and any retroactive billing.</div>
|
||||||
</td></tr></table>
|
</td></tr></table>
|
||||||
|
|
||||||
<!-- Detail row -->
|
<!-- Official CMS record card: the data is straight from the CMS Revalidation
|
||||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:18px 0;font-size:13px;">
|
Due Date List (verified to match by NPI), presented as a clearly-labeled
|
||||||
<tr style="border-bottom:1px solid #e5e7eb;"><td style="padding:10px 0;color:#6b7280;">NPI</td><td style="padding:10px 0;font-weight:600;text-align:right;">{{ .Subscriber.Attribs.npi }}</td></tr>
|
data readout. NOT a CMS screenshot / not impersonating CMS. -->
|
||||||
<tr style="border-bottom:1px solid #e5e7eb;"><td style="padding:10px 0;color:#6b7280;">Revalidation due</td><td style="padding:10px 0;font-weight:600;text-align:right;">{{ .Subscriber.Attribs.detail }}</td></tr>
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:22px 0;">
|
||||||
<tr><td style="padding:10px 0;color:#6b7280;">Our service fee</td><td style="padding:10px 0;font-weight:700;text-align:right;color:#047857;">$599</td></tr>
|
<tr><td style="border:1px solid #cbd5e1;border-radius:10px;overflow:hidden;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr><td style="background:#1e293b;padding:12px 16px;">
|
||||||
|
<p style="margin:0;font-size:11px;letter-spacing:.4px;text-transform:uppercase;color:#94a3b8;font-weight:700;">Official record · CMS Medicare Revalidation Due Date List</p>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="background:#f8fafc;padding:6px 16px 14px;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="font-size:13px;">
|
||||||
|
<tr style="border-bottom:1px solid #e5e7eb;"><td style="padding:9px 0;color:#64748b;">Provider / NPI</td><td style="padding:9px 0;font-weight:700;text-align:right;color:#0f172a;">{{ .Subscriber.Attribs.npi }}</td></tr>
|
||||||
|
<tr style="border-bottom:1px solid #e5e7eb;"><td style="padding:9px 0;color:#64748b;">Enrolled as</td><td style="padding:9px 0;font-weight:600;text-align:right;color:#0f172a;">{{ .Subscriber.Attribs.practice }}</td></tr>
|
||||||
|
<tr style="border-bottom:1px solid #e5e7eb;"><td style="padding:9px 0;color:#64748b;">Revalidation due date</td><td style="padding:9px 0;font-weight:700;text-align:right;color:#b91c1c;">{{ .Subscriber.Attribs.reval_due_date }}</td></tr>
|
||||||
|
<tr><td style="padding:9px 0;color:#64748b;">Status</td><td style="padding:9px 0;font-weight:700;text-align:right;color:#b91c1c;">PAST DUE · {{ .Subscriber.Attribs.days_overdue }} days overdue</td></tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin:10px 0 0;font-size:11px;color:#94a3b8;line-height:1.5;">Source: CMS Revalidation Due Date List (data.cms.gov), refreshed monthly. Performance West is an independent compliance firm, not affiliated with CMS or Medicare.</p>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Verify-it-yourself: nothing is more convincing than the government's own site -->
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:14px 0 22px;"><tr><td style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:16px;">
|
||||||
|
<p style="margin:0 0 6px;font-size:13px;color:#1e3a8a;font-weight:700;">Don’t take our word for it — check the official CMS record.</p>
|
||||||
|
<p style="margin:0 0 12px;font-size:13px;color:#1e40af;line-height:1.6;">Look up your NPI <strong>{{ .Subscriber.Attribs.npi }}</strong> on the U.S. government’s public Medicare Revalidation List and you’ll see the same due date above.</p>
|
||||||
|
<a href="https://data.cms.gov/tools/medicare-revalidation-list" style="display:inline-block;padding:10px 22px;background:#fff;border:1px solid #1d4ed8;color:#1d4ed8;font-weight:700;border-radius:8px;text-decoration:none;font-size:13px;">Verify on CMS.gov ↗</a>
|
||||||
|
</td></tr></table>
|
||||||
|
|
||||||
|
<!-- Service fee -->
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:0 0 8px;font-size:13px;">
|
||||||
|
<tr><td style="padding:8px 0;color:#6b7280;">Our service fee to file it for you</td><td style="padding:8px 0;font-weight:700;text-align:right;color:#047857;">$599</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- CTA -->
|
<!-- CTA -->
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,11 @@ def main():
|
||||||
"practice": r.get("name", ""),
|
"practice": r.get("name", ""),
|
||||||
"specialty": r.get("specialty", ""),
|
"specialty": r.get("specialty", ""),
|
||||||
"state": r.get("state", ""),
|
"state": r.get("state", ""),
|
||||||
|
# Separate fields so the email's "official CMS record" card can render
|
||||||
|
# the due date and overdue count cleanly (these mirror the authoritative
|
||||||
|
# CMS Revalidation Due Date List, verified to match by NPI).
|
||||||
|
"reval_due_date": r.get("reval_due_date", ""),
|
||||||
|
"days_overdue": str(r.get("days_overdue", "")),
|
||||||
"detail": (f"{r.get('reval_due_date','')} ({r.get('days_overdue','')} days overdue)"
|
"detail": (f"{r.get('reval_due_date','')} ({r.get('days_overdue','')} days overdue)"
|
||||||
if r.get("reval_status") == "overdue" else r.get("reval_due_date", "")),
|
if r.get("reval_status") == "overdue" else r.get("reval_due_date", "")),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,9 @@ import Base from "../../layouts/Base.astro";
|
||||||
|
|
||||||
<div id="checks-container" class="space-y-4"></div>
|
<div id="checks-container" class="space-y-4"></div>
|
||||||
|
|
||||||
|
<!-- CMS verification proof (shown when a revalidation due date is found) -->
|
||||||
|
<div id="cms-proof" class="hidden"></div>
|
||||||
|
|
||||||
<!-- CTA -->
|
<!-- CTA -->
|
||||||
<div id="cta-section" class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
<div id="cta-section" class="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
||||||
<div id="cta-content"></div>
|
<div id="cta-content"></div>
|
||||||
|
|
@ -267,6 +270,37 @@ import Base from "../../layouts/Base.astro";
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCta(data);
|
renderCta(data);
|
||||||
|
|
||||||
|
// CMS verification proof: when the revalidation due date came from the
|
||||||
|
// live public CMS dataset, show it as an "official record" the provider
|
||||||
|
// can independently verify on CMS.gov. This is what converts skeptics.
|
||||||
|
const proof = document.getElementById("cms-proof");
|
||||||
|
const rv = data.revalidation;
|
||||||
|
if (rv && rv.due_date && rv.source === "cms_live") {
|
||||||
|
proof.innerHTML = `
|
||||||
|
<div class="border border-slate-300 rounded-xl overflow-hidden shadow-sm">
|
||||||
|
<div class="bg-slate-800 px-4 py-2">
|
||||||
|
<p class="text-[11px] font-bold tracking-wide uppercase text-slate-300">Official record · CMS Medicare Revalidation Due Date List</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-slate-50 px-4 py-4">
|
||||||
|
<dl class="text-sm divide-y divide-gray-200">
|
||||||
|
<div class="flex justify-between py-2"><dt class="text-gray-500">Provider / NPI</dt><dd class="font-semibold text-gray-900">${data.npi}</dd></div>
|
||||||
|
${rv.cms_legal_name ? `<div class="flex justify-between py-2"><dt class="text-gray-500">Enrolled as</dt><dd class="font-medium text-gray-900 text-right">${rv.cms_legal_name}</dd></div>` : ""}
|
||||||
|
<div class="flex justify-between py-2"><dt class="text-gray-500">Revalidation due date</dt><dd class="font-bold text-red-700">${rv.due_date}</dd></div>
|
||||||
|
</dl>
|
||||||
|
<p class="mt-3 text-xs text-gray-400 leading-relaxed">Pulled live from the U.S. government’s public CMS dataset just now — this is not our data. Performance West is an independent compliance firm, not affiliated with CMS or Medicare.</p>
|
||||||
|
<a href="${rv.verify_url}" target="_blank" rel="noopener" class="inline-flex items-center gap-1 mt-3 px-4 py-2 bg-white border border-blue-600 text-blue-700 text-sm font-semibold rounded-lg hover:bg-blue-50 transition">
|
||||||
|
Verify this yourself on CMS.gov
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
proof.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
proof.classList.add("hidden");
|
||||||
|
proof.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById("checked-at").textContent = "Checked at " + new Date().toLocaleString();
|
document.getElementById("checked-at").textContent = "Checked at " + new Date().toLocaleString();
|
||||||
resultsEl.classList.remove("hidden");
|
resultsEl.classList.remove("hidden");
|
||||||
document.getElementById("results").scrollIntoView({ behavior: "smooth", block: "start" });
|
document.getElementById("results").scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue