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

@ -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;