add order timeline API: step names + estimated business-day completion dates per service
GET /api/v1/order-timeline/:order_id — returns steps with dates: - Skips weekends + US federal holidays (matches business_days.py) - Per-service timelines: MCS-150 (2 days), BOC-3 (3 days), MC Auth (15 days), etc - Entity upgrade bundle shows full sequential pipeline (10 business days) - ETA emergency shows same-day processing
This commit is contained in:
parent
ad41de817c
commit
90a0f983ee
2 changed files with 162 additions and 0 deletions
|
|
@ -48,6 +48,7 @@ import pucRouter from "./routes/puc.js";
|
|||
import fccCarrierRegRouter from "./routes/fcc-carrier-registration.js";
|
||||
import dotLookupRouter from "./routes/dot-lookup.js";
|
||||
import surveyRouter from "./routes/survey.js";
|
||||
import orderTimelineRouter from "./routes/order-timeline.js";
|
||||
|
||||
const app = express();
|
||||
|
||||
|
|
@ -122,6 +123,7 @@ app.use(pucRouter);
|
|||
app.use(fccCarrierRegRouter);
|
||||
app.use(dotLookupRouter);
|
||||
app.use(surveyRouter);
|
||||
app.use(orderTimelineRouter);
|
||||
app.use(adminCryptoRouter);
|
||||
// Note: identityRouter mounted above express.json() for webhook route,
|
||||
// but also handles non-webhook routes (create-session, poll) which work fine with json()
|
||||
|
|
|
|||
160
api/src/routes/order-timeline.ts
Normal file
160
api/src/routes/order-timeline.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/**
|
||||
* 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";
|
||||
|
||||
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" },
|
||||
],
|
||||
};
|
||||
|
||||
// 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" },
|
||||
];
|
||||
|
||||
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
|
||||
let orders: Record<string, unknown>[] = [];
|
||||
const single = await pool.query(
|
||||
"SELECT order_number, service_slug, service_name, created_at, payment_status FROM compliance_orders WHERE order_number = $1",
|
||||
[orderId],
|
||||
);
|
||||
if (single.rows.length > 0) {
|
||||
orders = single.rows as Record<string, unknown>[];
|
||||
} else {
|
||||
// Try as batch_id
|
||||
const batch = await pool.query(
|
||||
"SELECT order_number, service_slug, service_name, created_at, payment_status FROM compliance_orders WHERE batch_id = $1 ORDER BY created_at",
|
||||
[orderId],
|
||||
);
|
||||
orders = batch.rows as Record<string, unknown>[];
|
||||
}
|
||||
|
||||
if (orders.length === 0) {
|
||||
res.status(404).json({ error: "Order not found." });
|
||||
return;
|
||||
}
|
||||
|
||||
const timelines = orders.map((order) => {
|
||||
const slug = order.service_slug as string;
|
||||
const startDate = new Date(order.created_at as string);
|
||||
const steps = (SERVICE_TIMELINES[slug] || DEFAULT_TIMELINE).map((step) => ({
|
||||
...step,
|
||||
estimated_date: addBusinessDays(startDate, step.business_days).toISOString().split("T")[0],
|
||||
}));
|
||||
|
||||
return {
|
||||
order_number: order.order_number,
|
||||
service_slug: slug,
|
||||
service_name: order.service_name,
|
||||
steps,
|
||||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue