/// import { type Cell, cell, Default, handler, lift, recipe, str, } from "commontools"; /** Pattern managing procurement approvals with derived spending summaries. */ export type StageStatus = "pending" | "approved" | "rejected"; export type RequestStatus = "routing" | "approved" | "rejected"; export interface ApprovalStageSeed { key?: string; label?: string; approver?: string; status?: string; } export interface ApprovalStage { key: StageKey; label: string; approver: string; status: StageStatus; } export interface ProcurementRequestSeed { id?: string; requester?: string; department?: string; vendor?: string; description?: string; amount?: number; currency?: string; stages?: ApprovalStageSeed[]; } export interface ProcurementRequest { id: string; requester: string; department: string; vendor: string; description: string; amount: number; currency: string; stages: readonly ApprovalStage[]; status: RequestStatus; } export interface RequestCounts { total: number; routing: number; approved: number; rejected: number; } export interface AmountTotals { requested: number; routing: number; approved: number; rejected: number; } export interface DepartmentTotals { department: string; requested: number; routing: number; approved: number; rejected: number; } export interface RoutingAssignment { id: string; vendor: string; department: string; stage: string; approver: string; amount: number; currency: string; } export interface ProcurementRequestArgs { requests: Default; } export interface ApprovalDecisionEvent { id?: string; stage?: string; decision?: string; note?: string; } export interface RerouteEvent { id?: string; stage?: string; approver?: string; note?: string; } type StageKey = "department" | "procurement" | "finance"; type StageTemplate = { key: StageKey; label: string; fallbackApprover: string; }; const stageCatalog: readonly StageTemplate[] = [ { key: "department", label: "Department Review", fallbackApprover: "Department Head", }, { key: "procurement", label: "Procurement Desk", fallbackApprover: "Procurement Lead", }, { key: "finance", label: "Finance Approval", fallbackApprover: "Finance Controller", }, ]; const defaultRequests: ProcurementRequestSeed[] = [ { id: "monitor-upgrade", requester: "Jordan Smith", department: "Engineering", vendor: "Display Hub", description: '24" workstation monitor replacements', amount: 3200, currency: "usd", stages: [ { label: "Department Review", approver: "Morgan Patel", status: "approved", }, { label: "Procurement Desk", approver: "Ariana Flores", status: "pending", }, { label: "Finance Approval", approver: "Casey Liu", status: "pending", }, ], }, { id: "ergonomic-chairs", requester: "Kendall Ortiz", department: "Operations", vendor: "Comfort Office", description: "Ergonomic chairs for support pod", amount: 1850, currency: "usd", stages: [ { label: "Department Review", approver: "Jamie Lynn", status: "pending", }, { label: "Procurement Desk", approver: "Samir Adey", status: "pending", }, { label: "Finance Approval", approver: "Wei Tan", status: "pending", }, ], }, { id: "software-renewal", requester: "Hayden Lee", department: "Product", vendor: "Workflow Soft", description: "Annual workflow suite renewal", amount: 2400, currency: "usd", stages: [ { label: "Department Review", approver: "Ally Ng", status: "approved", }, { label: "Procurement Desk", approver: "Asha Ray", status: "approved", }, { label: "Finance Approval", approver: "Jonas Pike", status: "approved", }, ], }, ]; const HISTORY_LIMIT = 8; const roundCurrency = (value: number): number => { return Math.round(value * 100) / 100; }; const sanitizeText = (value: unknown, fallback: string): string => { if (typeof value !== "string") return fallback; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : fallback; }; const sanitizeCurrency = (value: unknown, fallback: string): string => { if (typeof value !== "string") return fallback; const trimmed = value.trim().toUpperCase(); return /^[A-Z]{3}$/.test(trimmed) ? trimmed : fallback; }; const sanitizeAmount = (value: unknown, fallback: number): number => { if (typeof value !== "number" || !Number.isFinite(value)) { return roundCurrency(fallback); } return roundCurrency(value < 0 ? 0 : value); }; const slugify = (value: string): string => { return value .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)+/g, ""); }; const slugOrFallback = (value: unknown, fallback: string): string => { if (typeof value !== "string") return fallback; const trimmed = value.trim(); if (trimmed.length === 0) return fallback; const slug = slugify(trimmed); return slug.length > 0 ? slug : fallback; }; const ensureUniqueId = (base: string, used: Set): string => { let candidate = base; let index = 2; while (used.has(candidate)) { candidate = `${base}-${index}`; index += 1; } used.add(candidate); return candidate; }; const sanitizeStageStatus = ( value: unknown, fallback: StageStatus, ): StageStatus => { if (value === "pending" || value === "approved" || value === "rejected") { return value; } if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if ( normalized === "pending" || normalized === "approved" || normalized === "rejected" ) { return normalized as StageStatus; } } return fallback; }; const normalizeStageKey = (value: unknown): StageKey | undefined => { if (typeof value !== "string") return undefined; const normalized = value.trim().toLowerCase(); for (const stage of stageCatalog) { if (normalized === stage.key) return stage.key; if (normalized === stage.label.toLowerCase()) return stage.key; } return undefined; }; const defaultStageSeeds = (): ApprovalStageSeed[] => { return stageCatalog.map((stage) => ({ key: stage.key, label: stage.label, approver: stage.fallbackApprover, status: "pending", })); }; const sanitizeStageList = ( value: readonly ApprovalStageSeed[] | undefined, fallback: readonly ApprovalStageSeed[], ): ApprovalStage[] => { const base = Array.isArray(value) && value.length > 0 ? value : fallback; return stageCatalog.map((stage, index) => { const raw = base[index] ?? fallback[index] ?? {}; const fallbackApprover = sanitizeText( fallback[index]?.approver, stage.fallbackApprover, ); const fallbackStatus = sanitizeStageStatus( fallback[index]?.status, "pending", ); const approver = sanitizeText(raw?.approver, fallbackApprover); const label = sanitizeText(raw?.label, stage.label); const status = sanitizeStageStatus(raw?.status, fallbackStatus); return { key: stage.key, label, approver, status, }; }); }; const deriveRequestStatus = ( stages: readonly ApprovalStage[], ): RequestStatus => { if (stages.some((stage) => stage.status === "rejected")) { return "rejected"; } if (stages.every((stage) => stage.status === "approved")) { return "approved"; } return "routing"; }; const sanitizeRequests = ( value: readonly ProcurementRequestSeed[] | undefined, ): ProcurementRequest[] => { const base = Array.isArray(value) && value.length > 0 ? value : defaultRequests; const used = new Set(); const sanitized: ProcurementRequest[] = []; for (let index = 0; index < base.length; index++) { const raw = base[index] ?? {}; const defaults = defaultRequests[index] ?? {}; const fallbackId = slugOrFallback( defaults.id, `request-${index + 1}`, ); const idBase = slugOrFallback(raw.id, fallbackId); const id = ensureUniqueId(idBase, used); const requester = sanitizeText( raw.requester, sanitizeText(defaults.requester, `Requester ${index + 1}`), ); const department = sanitizeText( raw.department, sanitizeText(defaults.department, "Operations"), ); const vendor = sanitizeText( raw.vendor, sanitizeText(defaults.vendor, `Vendor ${index + 1}`), ); const description = sanitizeText( raw.description, sanitizeText(defaults.description, "Procurement request"), ); const amount = sanitizeAmount( raw.amount, sanitizeAmount(defaults.amount, 1000), ); const currency = sanitizeCurrency( raw.currency, sanitizeCurrency(defaults.currency, "USD"), ); const fallbackStages = Array.isArray(defaults.stages) && defaults.stages.length > 0 ? defaults.stages : defaultStageSeeds(); const stages = sanitizeStageList(raw.stages, fallbackStages); const status = deriveRequestStatus(stages); sanitized.push({ id, requester, department, vendor, description, amount, currency, stages, status, }); } if (sanitized.length === 0) { return sanitizeRequests(defaultRequests); } return sanitized; }; const serializeRequest = ( request: ProcurementRequest, ): ProcurementRequestSeed => { return { id: request.id, requester: request.requester, department: request.department, vendor: request.vendor, description: request.description, amount: request.amount, currency: request.currency, stages: request.stages.map((stage) => ({ key: stage.key, label: stage.label, approver: stage.approver, status: stage.status, })), }; }; const calculateTotals = ( requests: readonly ProcurementRequest[], ): AmountTotals => { let requested = 0; let routing = 0; let approved = 0; let rejected = 0; for (const request of requests) { const amount = roundCurrency(request.amount); requested += amount; if (request.status === "routing") { routing += amount; } else if (request.status === "approved") { approved += amount; } else { rejected += amount; } } return { requested: roundCurrency(requested), routing: roundCurrency(routing), approved: roundCurrency(approved), rejected: roundCurrency(rejected), }; }; const calculateCounts = ( requests: readonly ProcurementRequest[], ): RequestCounts => { let routing = 0; let approved = 0; let rejected = 0; for (const request of requests) { if (request.status === "routing") routing += 1; else if (request.status === "approved") approved += 1; else rejected += 1; } return { total: requests.length, routing, approved, rejected, }; }; const buildDepartmentTotals = ( requests: readonly ProcurementRequest[], ): DepartmentTotals[] => { const totals = new Map(); for (const request of requests) { const existing = totals.get(request.department) ?? { department: request.department, requested: 0, routing: 0, approved: 0, rejected: 0, }; existing.requested = roundCurrency(existing.requested + request.amount); if (request.status === "routing") { existing.routing = roundCurrency(existing.routing + request.amount); } else if (request.status === "approved") { existing.approved = roundCurrency(existing.approved + request.amount); } else { existing.rejected = roundCurrency(existing.rejected + request.amount); } totals.set(request.department, existing); } return Array.from(totals.values()).sort((left, right) => left.department.localeCompare(right.department) ); }; const buildAssignments = ( requests: readonly ProcurementRequest[], ): RoutingAssignment[] => { const assignments: RoutingAssignment[] = []; for (const request of requests) { if (request.status !== "routing") continue; const nextStage = request.stages.find((stage) => stage.status === "pending" ); if (!nextStage) continue; assignments.push({ id: request.id, vendor: request.vendor, department: request.department, stage: nextStage.label, approver: nextStage.approver, amount: request.amount, currency: request.currency, }); } return assignments.sort((left, right) => left.id.localeCompare(right.id)); }; const formatCurrency = (amount: number, currency: string): string => { return `${currency} ${roundCurrency(amount).toFixed(2)}`; }; const buildSummaryLine = ( requests: readonly ProcurementRequest[], ): string => { if (requests.length === 0) return "No procurement requests"; const totals = calculateTotals(requests); const counts = calculateCounts(requests); const currency = requests[0]?.currency ?? "USD"; const routingLabel = formatCurrency(totals.routing, currency); const approvedLabel = formatCurrency(totals.approved, currency); const rejectedLabel = formatCurrency(totals.rejected, currency); const parts = [ `${counts.routing} routing (${routingLabel})`, `${counts.approved} approved (${approvedLabel})`, `${counts.rejected} rejected (${rejectedLabel})`, ]; return `${counts.total} requests: ${parts.join(", ")}`; }; const normalizeId = (value: unknown): string | undefined => { if (typeof value !== "string") return undefined; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; }; const normalizeDecision = (value: unknown): StageStatus | undefined => { if (value === "approved" || value === "rejected") { return value; } if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (normalized === "approved" || normalized === "rejected") { return normalized as StageStatus; } } return undefined; }; const pushHistory = ( history: Cell, sequence: Cell, message: string, ) => { const current = sequence.get() ?? 1; const nextSequence = current + 1; sequence.set(nextSequence); const entry = `${nextSequence}. ${message}`; const previous = history.get() ?? []; const appended = [...previous, entry]; if (appended.length > HISTORY_LIMIT) { history.set(appended.slice(-HISTORY_LIMIT)); return; } history.set(appended); }; const buildDecisionMessage = ( request: ProcurementRequest, stage: ApprovalStage, decision: StageStatus, ): string => { const amount = formatCurrency(request.amount, request.currency); if (decision === "approved") { if (request.status === "approved") { return [ "Completed approval for", `${request.vendor} (${request.id})`, amount, ].join(" "); } return [ "Approved", stage.label, "for", request.id, `(${stage.approver})`, ].join(" "); } return [ "Rejected", request.id, "at", stage.label, `(${amount})`, ].join(" "); }; const buildRerouteMessage = ( request: ProcurementRequest, stage: ApprovalStage, ): string => { const amount = formatCurrency(request.amount, request.currency); return [ "Rerouted", request.id, "to", stage.approver, "for", stage.label, `(${amount})`, ].join(" "); }; const recordDecision = handler( ( event: ApprovalDecisionEvent | undefined, context: { requests: Cell; history: Cell; sequence: Cell; }, ) => { const id = normalizeId(event?.id); const decision = normalizeDecision(event?.decision); if (!id || !decision) return; const sanitized = sanitizeRequests(context.requests.get()); const index = sanitized.findIndex((request) => request.id === id); if (index === -1) return; const request = sanitized[index]; const stageKey = normalizeStageKey(event?.stage) ?? request.stages.find((stage) => stage.status === "pending")?.key; if (!stageKey) return; const stageIndex = request.stages.findIndex((stage) => stage.key === stageKey ); if (stageIndex === -1) return; const targetStage = request.stages[stageIndex]; if (decision === targetStage.status) return; const updatedStages = request.stages.map( (stage, stageIdx): ApprovalStage => { if (stageIdx === stageIndex) { return { ...stage, status: decision }; } if (decision === "rejected" && stageIdx > stageIndex) { return { ...stage, status: "pending" }; } return stage; }, ); const updatedRequest: ProcurementRequest = { ...request, stages: updatedStages, status: deriveRequestStatus(updatedStages), }; const nextRequests = sanitized.map((entry, entryIndex) => entryIndex === index ? updatedRequest : entry ); context.requests.set(nextRequests.map(serializeRequest)); const message = buildDecisionMessage( updatedRequest, updatedStages[stageIndex], decision, ); pushHistory(context.history, context.sequence, message); }, ); const rerouteStage = handler( ( event: RerouteEvent | undefined, context: { requests: Cell; history: Cell; sequence: Cell; }, ) => { const id = normalizeId(event?.id); if (!id) return; const sanitized = sanitizeRequests(context.requests.get()); const index = sanitized.findIndex((request) => request.id === id); if (index === -1) return; const request = sanitized[index]; const stageKey = normalizeStageKey(event?.stage) ?? request.stages.find((stage) => stage.status !== "approved")?.key; if (!stageKey) return; const stageIndex = request.stages.findIndex((stage) => stage.key === stageKey ); if (stageIndex === -1) return; const currentStage = request.stages[stageIndex]; const approver = sanitizeText( event?.approver, currentStage.approver, ); const updatedStages = request.stages.map( (stage, stageIdx): ApprovalStage => { if (stageIdx === stageIndex) { return { ...stage, approver, status: "pending" }; } if (stageIdx > stageIndex) { return { ...stage, status: "pending" }; } return stage; }, ); const updatedRequest: ProcurementRequest = { ...request, stages: updatedStages, status: deriveRequestStatus(updatedStages), }; const nextRequests = sanitized.map((entry, entryIndex) => entryIndex === index ? updatedRequest : entry ); context.requests.set(nextRequests.map(serializeRequest)); const message = buildRerouteMessage( updatedRequest, updatedStages[stageIndex], ); pushHistory(context.history, context.sequence, message); }, ); /** * Builds a procurement approval workflow that tracks routing progress while * deriving deterministic spending summaries for the harness. */ export const procurementRequest = recipe( "Procurement Request Workflow", ({ requests }) => { const history = cell(["1. Procurement queue initialized"]); const sequence = cell(1); const requestList = lift(sanitizeRequests)(requests); const totals = lift(calculateTotals)(requestList); const counts = lift(calculateCounts)(requestList); const departmentTotals = lift(buildDepartmentTotals)(requestList); const routingAssignments = lift(buildAssignments)(requestList); const summaryLine = lift(buildSummaryLine)(requestList); const stageHeadline = str`${counts.routing} requests awaiting review`; const handlerContext = { requests, history, sequence }; return { requests, requestList, totals, counts, departmentTotals, routingAssignments, summaryLine, stageHeadline, activityLog: history, recordDecision: recordDecision(handlerContext), rerouteStage: rerouteStage(handlerContext), }; }, );