new-site/api/src/routes/order-timeline-status.ts
justin 28b1af341d Wire fulfillment alerts to Telegram + surface order progress in portal + even out ERPNext sync
Telegram notifications:
- Add shared scripts/workers/telegram_notify.py (send_telegram, notify_fulfillment_todo,
  create_admin_todo) so every worker alerts the operator the same way; fire-and-forget.
- Fire notify_fulfillment_todo after each admin_todos insert across all 8 service
  handlers (9 sites) so no fulfillment task waits unseen.
  (Orders + quotes + tickets already notified via checkout/quotes/tickets routes.)

Client portal order progress:
- order-timeline: derive real per-step status from live signals (payment paid,
  e-signature signed, fulfillment_status) instead of a static template; add
  current_step to the response.
- Extract pure applyLiveStatus into order-timeline-status.ts (DB-free) + unit test
  (api/test/test_timeline_status.ts, 8 cases).
- portal /me now returns compliance_orders.fulfillment_status.
- Dashboard renders a client-safe Progress badge (In progress / Action needed /
  Filed-awaiting-confirmation / Completed); batches show the most actionable status.
  No back-office mechanics exposed.

ERPNext sync parity:
- Create a Sales Order for formation and fcc_carrier_registration orders (previously
  only canada_crtc + compliance synced); write erpnext_sales_order back to each table.
  Non-blocking, matches existing pattern.

Verified: API tsc clean, timeline unit tests 8/8, Astro build 58 pages,
cms10114/ink/paper_batch Python tests still green, no mechanics leaks.
2026-06-07 03:17:46 -05:00

72 lines
2.8 KiB
TypeScript

/**
* Pure live-progress derivation for the order timeline.
*
* Kept free of DB/config imports so it can be unit-tested in isolation and
* reused. The route (order-timeline.ts) imports applyLiveStatus from here.
*
* We translate real signals (payment, e-signature, fulfillment_status) into
* per-step "completed | in_progress | pending" so the client portal shows true
* progress rather than a static template.
*/
/** Step-name matchers used to find phase boundaries within any timeline. */
export const SIGNATURE_STEP_RE = /signature|e-?sign/i;
export const FILED_STEP_RE = /filed|filing|submitted|application filed|registration/i;
// fulfillment_status values that mean "the filing has been submitted to the agency".
export const FILED_STATUSES = new Set<string>([
"filed_waiting_state",
"ready_to_file", // queued for ops to file — treat prep+signature as done
]);
// fulfillment_status values that mean "fully done".
export const COMPLETED_STATUSES = new Set<string>(["completed", "complete"]);
export interface ProgressSignals {
paid: boolean;
signed: boolean;
fulfillmentStatus: string | null;
}
/**
* Recompute each step's status from live signals. Returns a new step array.
*
* We resolve a "reached index" — the index of the furthest step we can prove is
* done — then mark everything up to it completed, the next in_progress, the
* rest pending. We never regress a step that the static definition already
* marked completed.
*/
export function applyLiveStatus<T extends { name: string; status: string }>(
steps: T[],
sig: ProgressSignals,
): T[] {
const sigIdx = steps.findIndex((s) => SIGNATURE_STEP_RE.test(s.name));
const filedIdx = steps.findIndex((s) => FILED_STEP_RE.test(s.name));
const confirmIdx = steps.length - 1; // last step is always the terminal one
let reached = -1;
// Payment confirmed → at minimum "Order Received" (index 0) is done.
if (sig.paid) reached = Math.max(reached, 0);
// Signature captured → the signature step (and everything before it) is done.
if (sig.signed && sigIdx >= 0) reached = Math.max(reached, sigIdx);
// Filing submitted to agency → the "Filed" step (and everything before) done.
if (sig.fulfillmentStatus && FILED_STATUSES.has(sig.fulfillmentStatus) && filedIdx >= 0) {
reached = Math.max(reached, filedIdx);
}
// Fully completed → everything done.
if (sig.fulfillmentStatus && COMPLETED_STATUSES.has(sig.fulfillmentStatus)) {
reached = confirmIdx;
}
return steps.map((step, i) => {
const staticallyDone = step.status === "completed";
let status: "pending" | "in_progress" | "completed";
if (i <= reached || staticallyDone) status = "completed";
else if (i === reached + 1) status = "in_progress";
else status = "pending";
return { ...step, status };
});
}