Client-facing and website code now describes only a generic per-document signing authorization; nothing visible to signers or recorded in the website/API code or DB schema references ink, paper, reproduction, or any fulfillment mechanics. - rename esign-ink-consent.ts -> esign-sign-consent.ts; INK_CONSENT_TEXT -> SIGN_CONSENT_TEXT (generic: 'use my signature to complete and submit this single filing', no ink/paper/reproduce language); helpers ink* -> sign* - portal-esign-generic.ts: API field ink_reproduction -> require_sign_consent, ink_consent_text -> sign_consent_text, request field ink_consent -> sign_consent - signing page (site/public/portal/esign): all ids/vars/comments ink* -> sign*; no 'ink' string remains - npi_provider metadata flag ink_reproduction -> require_sign_consent - migration 090/092 + live DB column comments rewritten to drop ink/plotter wording (DB column names kept as ink_consent* for compat, internal only) - order-timeline.ts buffer comments neutralized - tests: 37 checks, consent text asserted to omit ink/plotter/paper/reproduce/etc DB columns ink_consent* retained (internal, never sent to clients) to avoid a risky rename of already-applied prod columns.
272 lines
15 KiB
TypeScript
272 lines
15 KiB
TypeScript
/**
|
|
* Order timeline estimator — shows step names + estimated completion dates.
|
|
*
|
|
* GET /api/v1/order-timeline/:order_id
|
|
*
|
|
* Returns a timeline of steps with estimated business-day completion dates,
|
|
* skipping weekends and US federal holidays.
|
|
*/
|
|
|
|
import { Router, type Request, type Response } from "express";
|
|
import { pool } from "../db.js";
|
|
import { applyLiveStatus } from "./order-timeline-status.js";
|
|
|
|
const router = Router();
|
|
|
|
// US Federal Holidays — hardcoded for 2026-2027 (matches scripts/workers/business_days.py)
|
|
const US_HOLIDAYS = new Set([
|
|
"2026-01-01","2026-01-19","2026-02-16","2026-05-25","2026-07-03",
|
|
"2026-09-07","2026-10-12","2026-11-11","2026-11-26","2026-12-25",
|
|
"2027-01-01","2027-01-18","2027-02-15","2027-05-31","2027-07-05",
|
|
"2027-09-06","2027-10-11","2027-11-11","2027-11-25","2027-12-25",
|
|
]);
|
|
|
|
function addBusinessDays(start: Date, days: number): Date {
|
|
if (days === 0) return new Date(start);
|
|
const result = new Date(start);
|
|
let added = 0;
|
|
while (added < days) {
|
|
result.setDate(result.getDate() + 1);
|
|
const dow = result.getDay();
|
|
const dateStr = result.toISOString().split("T")[0];
|
|
if (dow !== 0 && dow !== 6 && !US_HOLIDAYS.has(dateStr)) {
|
|
added++;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Service-specific step definitions with business day estimates
|
|
interface TimelineStep {
|
|
name: string;
|
|
description: string;
|
|
business_days: number; // days from order start
|
|
status: "pending" | "in_progress" | "completed";
|
|
}
|
|
|
|
const SERVICE_TIMELINES: Record<string, TimelineStep[]> = {
|
|
"mcs150-update": [
|
|
{ name: "Order Received", description: "Payment confirmed, order in queue", business_days: 0, status: "completed" },
|
|
{ name: "Document Preparation", description: "Filling official MCS-150 form with your information", business_days: 1, status: "pending" },
|
|
{ name: "E-Sign Required", description: "Review and sign the perjury declaration", business_days: 1, status: "pending" },
|
|
{ name: "Filed with FMCSA", description: "Submitted electronically to FMCSA", business_days: 2, status: "pending" },
|
|
{ name: "Certificate of Filing", description: "Attestation document delivered to you", business_days: 2, status: "pending" },
|
|
{ name: "FMCSA Confirmation", description: "FMCSA processes and reflects update (5-10 business days)", business_days: 10, status: "pending" },
|
|
],
|
|
"boc3-filing": [
|
|
{ name: "Order Received", description: "Payment confirmed", business_days: 0, status: "completed" },
|
|
{ name: "Process Agent Filing", description: "Filing BOC-3 with Registered Agents Inc", business_days: 1, status: "pending" },
|
|
{ name: "FMCSA Registration", description: "BOC-3 on file with FMCSA", business_days: 3, status: "pending" },
|
|
],
|
|
"ucr-registration": [
|
|
{ name: "Order Received", description: "Payment confirmed", business_days: 0, status: "completed" },
|
|
{ name: "UCR Filing", description: "Registering with Unified Carrier Registration", business_days: 2, status: "pending" },
|
|
{ name: "Confirmation", description: "UCR registration receipt delivered", business_days: 3, status: "pending" },
|
|
],
|
|
"dot-registration": [
|
|
{ name: "Order Received", description: "Payment confirmed", business_days: 0, status: "completed" },
|
|
{ name: "Application Preparation", description: "Preparing USDOT application", business_days: 1, status: "pending" },
|
|
{ name: "Filed with FMCSA", description: "Application submitted to FMCSA", business_days: 2, status: "pending" },
|
|
{ name: "USDOT Issued", description: "FMCSA issues your USDOT number", business_days: 5, status: "pending" },
|
|
],
|
|
"mc-authority": [
|
|
{ name: "Order Received", description: "Payment confirmed", business_days: 0, status: "completed" },
|
|
{ name: "Application Filed", description: "Operating authority application submitted", business_days: 2, status: "pending" },
|
|
{ name: "Insurance Filing Required", description: "Insurance must be on file before authority activates", business_days: 5, status: "pending" },
|
|
{ name: "Authority Active", description: "Operating authority becomes active", business_days: 15, status: "pending" },
|
|
],
|
|
"dot-drug-alcohol": [
|
|
{ name: "Order Received", description: "Payment confirmed", business_days: 0, status: "completed" },
|
|
{ name: "Program Setup", description: "Enrolling in DOT-compliant D&A testing program", business_days: 2, status: "pending" },
|
|
{ name: "Materials Delivered", description: "Policy manual and enrollment confirmation", business_days: 3, status: "pending" },
|
|
],
|
|
"usdot-reactivation": [
|
|
{ name: "Order Received", description: "Payment confirmed", business_days: 0, status: "completed" },
|
|
{ name: "Reactivation Filed", description: "Request submitted to FMCSA", business_days: 1, status: "pending" },
|
|
{ name: "USDOT Reactivated", description: "FMCSA reactivates your USDOT number", business_days: 5, status: "pending" },
|
|
],
|
|
"emergency-temporary-authority": [
|
|
{ name: "Order Received", description: "Payment confirmed — PRIORITY processing", business_days: 0, status: "completed" },
|
|
{ name: "ETA Filed", description: "Emergency request submitted to FMCSA", business_days: 0, status: "pending" },
|
|
{ name: "FMCSA Response", description: "Awaiting FMCSA emergency authorization", business_days: 2, status: "pending" },
|
|
],
|
|
"entity-upgrade-bundle": [
|
|
{ name: "Order Received", description: "Payment confirmed", business_days: 0, status: "completed" },
|
|
{ name: "LLC Formation", description: "Filing LLC in your chosen state", business_days: 3, status: "pending" },
|
|
{ name: "EIN Application", description: "Applying for employer ID number with IRS", business_days: 4, status: "pending" },
|
|
{ name: "MCS-150 Update", description: "Updating FMCSA registration with new entity", business_days: 5, status: "pending" },
|
|
{ name: "Authority Transfer", description: "Transferring operating authority to new entity", business_days: 7, status: "pending" },
|
|
{ name: "BOC-3 Update", description: "Updating process agent filing", business_days: 8, status: "pending" },
|
|
{ name: "Complete", description: "All filings updated under your new entity", business_days: 10, status: "pending" },
|
|
],
|
|
// ── Healthcare / CMS provider filings ────────────────────────────────────
|
|
// These submit a signed form to CMS; the WET_SIGNATURE_BUFFER is applied to
|
|
// the post-signature steps (see the buffer logic below).
|
|
"nppes-update": [
|
|
{ name: "Order Received", description: "Payment confirmed, order in queue", business_days: 0, status: "completed" },
|
|
{ name: "Document Preparation", description: "Preparing your NPI update for filing", business_days: 1, status: "pending" },
|
|
{ name: "Signature Required", description: "Review and sign the certification", business_days: 1, status: "pending" },
|
|
{ name: "Filed with CMS", description: "Your signed update is submitted to CMS", business_days: 2, status: "pending" },
|
|
{ name: "CMS Confirmation", description: "CMS processes the update (typically 1-2 weeks)", business_days: 10, status: "pending" },
|
|
],
|
|
"npi-reactivation": [
|
|
{ name: "Order Received", description: "Payment confirmed, order in queue", business_days: 0, status: "completed" },
|
|
{ name: "Document Preparation", description: "Preparing your NPI reactivation", business_days: 1, status: "pending" },
|
|
{ name: "Signature Required", description: "Review and sign the certification", business_days: 1, status: "pending" },
|
|
{ name: "Filed with CMS", description: "Your signed reactivation is submitted to CMS", business_days: 2, status: "pending" },
|
|
{ name: "CMS Confirmation", description: "CMS processes the reactivation (typically 1-2 weeks)", business_days: 10, status: "pending" },
|
|
],
|
|
"npi-revalidation": [
|
|
{ name: "Order Received", description: "Payment confirmed, order in queue", business_days: 0, status: "completed" },
|
|
{ name: "Document Preparation", description: "Preparing your Medicare revalidation", business_days: 1, status: "pending" },
|
|
{ name: "Signature Required", description: "Review and sign the certification", business_days: 1, status: "pending" },
|
|
{ name: "Filed with CMS", description: "Your signed revalidation is submitted to CMS", business_days: 2, status: "pending" },
|
|
{ name: "CMS Confirmation", description: "CMS/MAC processes the revalidation", business_days: 15, status: "pending" },
|
|
],
|
|
"medicare-enrollment": [
|
|
{ name: "Order Received", description: "Payment confirmed, order in queue", business_days: 0, status: "completed" },
|
|
{ name: "Document Preparation", description: "Preparing your Medicare enrollment", business_days: 1, status: "pending" },
|
|
{ name: "Signature Required", description: "Review and sign the certification", business_days: 1, status: "pending" },
|
|
{ name: "Filed with CMS", description: "Your signed enrollment is submitted to CMS", business_days: 2, status: "pending" },
|
|
{ name: "CMS Confirmation", description: "CMS/MAC processes the enrollment", business_days: 15, status: "pending" },
|
|
],
|
|
"provider-compliance-bundle": [
|
|
{ name: "Order Received", description: "Payment confirmed, order in queue", business_days: 0, status: "completed" },
|
|
{ name: "Document Preparation", description: "Preparing your provider compliance filings", business_days: 1, status: "pending" },
|
|
{ name: "Signature Required", description: "Review and sign the certification(s)", business_days: 1, status: "pending" },
|
|
{ name: "Filed with CMS", description: "Your signed filings are submitted to CMS", business_days: 2, status: "pending" },
|
|
{ name: "Screening Complete", description: "OIG/SAM screening completed (no action needed from you)", business_days: 2, status: "pending" },
|
|
{ name: "CMS Confirmation", description: "CMS/MAC processes your filings", business_days: 15, status: "pending" },
|
|
],
|
|
};
|
|
|
|
// Default timeline for services without a specific definition
|
|
const DEFAULT_TIMELINE: TimelineStep[] = [
|
|
{ name: "Order Received", description: "Payment confirmed, order in queue", business_days: 0, status: "completed" },
|
|
{ name: "Processing", description: "Our team is working on your filing", business_days: 2, status: "pending" },
|
|
{ name: "Complete", description: "Filing completed and delivered", business_days: 5, status: "pending" },
|
|
];
|
|
|
|
// Services whose Standard path requires extra fulfillment handling on a mailed
|
|
// CMS form and therefore needs a small ETA buffer so we always have time to
|
|
// produce and mail the completed original. Revisit the buffer once that
|
|
// fulfillment step is in steady-state.
|
|
const WET_SIGNATURE_BUFFER_DAYS = 2;
|
|
const WET_SIGNATURE_SLUGS = new Set<string>([
|
|
"nppes-update",
|
|
"npi-reactivation",
|
|
"npi-revalidation",
|
|
"medicare-enrollment",
|
|
"provider-compliance-bundle",
|
|
]);
|
|
|
|
// ── Live-progress derivation ──────────────────────────────────────────────
|
|
// applyLiveStatus (pure, DB-free, unit-tested in order-timeline-status.ts)
|
|
// overrides each step's static status from real signals (payment, e-signature,
|
|
// fulfillment_status) so the client portal shows true progress.
|
|
|
|
router.get("/api/v1/order-timeline/:order_id", async (req: Request, res: Response) => {
|
|
try {
|
|
const orderId = req.params.order_id;
|
|
|
|
// Try single order first, then batch. We select fulfillment_status when
|
|
// present (column may not exist on older DBs — fall back gracefully).
|
|
const COLS = "order_number, service_slug, service_name, created_at, payment_status, fulfillment_status";
|
|
const COLS_FALLBACK = "order_number, service_slug, service_name, created_at, payment_status";
|
|
|
|
async function loadOrders(): Promise<Record<string, unknown>[]> {
|
|
const trySelect = async (cols: string) => {
|
|
const single = await pool.query(
|
|
`SELECT ${cols} FROM compliance_orders WHERE order_number = $1`,
|
|
[orderId],
|
|
);
|
|
if (single.rows.length > 0) return single.rows as Record<string, unknown>[];
|
|
const batch = await pool.query(
|
|
`SELECT ${cols} FROM compliance_orders WHERE batch_id = $1 ORDER BY created_at`,
|
|
[orderId],
|
|
);
|
|
return batch.rows as Record<string, unknown>[];
|
|
};
|
|
try {
|
|
return await trySelect(COLS);
|
|
} catch {
|
|
// fulfillment_status column not present yet — retry without it.
|
|
return await trySelect(COLS_FALLBACK);
|
|
}
|
|
}
|
|
|
|
const orders = await loadOrders();
|
|
|
|
if (orders.length === 0) {
|
|
res.status(404).json({ error: "Order not found." });
|
|
return;
|
|
}
|
|
|
|
// Which of these orders have a signed e-signature on file? One query for
|
|
// all order_numbers in the batch. Defensive: a missing table must not break
|
|
// the timeline (clients still see estimated dates).
|
|
const orderNumbers = orders.map((o) => o.order_number as string);
|
|
const signedSet = new Set<string>();
|
|
try {
|
|
const sig = await pool.query(
|
|
"SELECT DISTINCT order_number FROM esign_records WHERE order_number = ANY($1) AND status = 'signed'",
|
|
[orderNumbers],
|
|
);
|
|
for (const r of sig.rows as Record<string, unknown>[]) {
|
|
signedSet.add(r.order_number as string);
|
|
}
|
|
} catch {
|
|
// esign_records absent — treat all as unsigned.
|
|
}
|
|
|
|
const timelines = orders.map((order) => {
|
|
const slug = order.service_slug as string;
|
|
const startDate = new Date(order.created_at as string);
|
|
// These services get a buffer on every step from the signature onward,
|
|
// so we always have time to produce and mail the completed original form.
|
|
const isWetSig = WET_SIGNATURE_SLUGS.has(slug);
|
|
let pastSignature = false;
|
|
const dated = (SERVICE_TIMELINES[slug] || DEFAULT_TIMELINE).map((step) => {
|
|
if (isWetSig && /signature/i.test(step.name)) {
|
|
pastSignature = true;
|
|
}
|
|
const buffer = isWetSig && pastSignature ? WET_SIGNATURE_BUFFER_DAYS : 0;
|
|
return {
|
|
...step,
|
|
estimated_date: addBusinessDays(startDate, step.business_days + buffer)
|
|
.toISOString()
|
|
.split("T")[0],
|
|
};
|
|
});
|
|
|
|
// Overlay live progress so the portal reflects reality, not just dates.
|
|
const paymentStatus = String(order.payment_status || "").toLowerCase();
|
|
const steps = applyLiveStatus(dated, {
|
|
paid: paymentStatus === "paid" || paymentStatus === "completed",
|
|
signed: signedSet.has(order.order_number as string),
|
|
fulfillmentStatus: (order.fulfillment_status as string) || null,
|
|
});
|
|
|
|
// The current step is the first in_progress one, else the last completed.
|
|
const inProgress = steps.find((s) => s.status === "in_progress");
|
|
const lastCompleted = [...steps].reverse().find((s) => s.status === "completed");
|
|
const currentStep = (inProgress || lastCompleted || steps[0]).name;
|
|
|
|
return {
|
|
order_number: order.order_number,
|
|
service_slug: slug,
|
|
service_name: order.service_name,
|
|
steps,
|
|
current_step: currentStep,
|
|
estimated_completion: steps[steps.length - 1].estimated_date,
|
|
};
|
|
});
|
|
|
|
res.json({ timelines });
|
|
} catch (err) {
|
|
console.error("[timeline] Error:", err);
|
|
res.status(500).json({ error: "Could not generate timeline." });
|
|
}
|
|
});
|
|
|
|
export default router;
|