/** * ERP business-query helpers for the chatbot. * Each function fetches ERP data and returns a concise, LLM-friendly string * (or structured object) that can be injected into the system prompt context. * * Sensitive fields (cost prices, internal IDs, etc.) are intentionally omitted. */ import { fetchCatalog, fetchContacts, fetchOrderDetail, fetchOrdersList, fetchStock, fetchProductImages, type OrderDetail, type OrderListItem, type UserCtx, } from "./erpClient"; export type { UserCtx }; // ─── Order lookup ───────────────────────────────────────────────────────────── /** * Fetch a single sales order by SO-ID and return a chatbot-friendly summary. */ export async function lookupOrder(soId: string, userCtx?: UserCtx): Promise { const order = await fetchOrderDetail(soId.trim().toUpperCase(), userCtx); if (!order) return `Sales order "${soId}" was not found in the system.`; return formatOrderDetail(order); } /** * Fetch the N most recent orders for a customer (by CID or name). */ export async function lookupOrdersByCustomer( customerCid: string, limit = 5, userCtx?: UserCtx ): Promise { const rows = await fetchOrdersList({ customer_cid: customerCid, limit }, userCtx); if (!rows.length) return `No orders found for customer "${customerCid}".`; return formatOrderList(rows); } /** * Fetch recent orders for a customer and return tracking-focused summary. */ export async function lookupTrackingByCustomer( customerCid: string, limit = 5, userCtx?: UserCtx ): Promise { const rows = await fetchOrdersList({ customer_cid: customerCid, limit }, userCtx); if (!rows.length) return `No recent orders found for customer "${customerCid}".`; return formatTrackingList(rows); } /** * Search orders by PO number or partial SO-ID. */ export async function lookupOrdersByPO(poId: string, userCtx?: UserCtx): Promise { const rows = await fetchOrdersList({ po_id: poId, limit: 10 }, userCtx); if (!rows.length) return `No orders found with PO number "${poId}".`; return formatOrderList(rows); } // ─── Catalog / product lookup ───────────────────────────────────────────────── export async function lookupCatalog(params: { model?: string; description?: string; category?: string; manufacturer?: string; limit?: number; }, userCtx?: UserCtx): Promise { const items = await fetchCatalog({ ...params, limit: params.limit ?? 10 }, userCtx); if (!items.length) return "No matching products found in the catalog."; const lines = items.map((i) => { const parts = [ `• ${i.Model ?? "—"}`, i.Manufacturer ? `by ${i.Manufacturer}` : null, i.Description ? `— ${i.Description}` : null, i.Type ? `[${i.Type}]` : null, i.Status ? `Status: ${i.Status}` : null, i.StockQTY != null ? `Stock: ${i.StockQTY} ${i.UOM ?? ""}`.trim() : null, ].filter(Boolean); return parts.join(" "); }); return `Product catalog results (${items.length}):\n${lines.join("\n")}`; } // ─── Stock lookup ───────────────────────────────────────────────────────────── export async function lookupStock(params: { model: string; warehouse_cid?: string; limit?: number; }, userCtx?: UserCtx): Promise { const records = await fetchStock({ ...params, limit: params.limit ?? 30 }, userCtx); if (!records.length) return `No stock records found for model "${params.model}".`; // Aggregate quantity by model const byModel: Record; eta: string | null }> = {}; for (const r of records) { const key = r.Model ?? "UNKNOWN"; if (!byModel[key]) byModel[key] = { qty: 0, wh: new Set(), eta: null }; byModel[key].qty += r.Quantity ?? 0; if (r.WarehouseCID) byModel[key].wh.add(r.WarehouseCID); if (!byModel[key].eta && (r.A_ETA ?? r.B_ETA)) byModel[key].eta = r.A_ETA ?? r.B_ETA; } const lines = Object.entries(byModel).map(([model, { qty, wh, eta }]) => { const parts = [`• ${model}`, `Qty: ${qty}`]; if (wh.size) parts.push(`Warehouse: ${[...wh].join(", ")}`); if (eta) parts.push(`ETA: ${eta}`); return parts.join(" "); }); return `Stock availability for "${params.model}" (${records.length} pallet records):\n${lines.join("\n")}`; } // ─── Contact / customer lookup ──────────────────────────────────────────────── export async function lookupContact(params: { contact_id?: string; company?: string; name?: string; }, userCtx?: UserCtx): Promise { const contacts = await fetchContacts({ ...params, limit: 5 }, userCtx); if (!contacts.length) return "No matching customer records found."; const lines = contacts.map((c) => { const parts = [ `• ${c.Company ?? "—"}`, c.ContactName1 ? `(${c.ContactName1})` : null, c.Phone1 ? `Ph: ${c.Phone1}` : null, c.Email1 ? `Email: ${c.Email1}` : null, c.City ? `${c.City}, ${c.State ?? ""} ${c.Country ?? ""}`.trim() : null, c.Terms ? `Terms: ${c.Terms}` : null, c.AR_CreditHold ? "⚠ Credit Hold" : null, ].filter(Boolean); return parts.join(" "); }); return `Customer records (${contacts.length}):\n${lines.join("\n")}`; } // ─── Product images ─────────────────────────────────────────────────────────── /** * Fetch product images for a model and return markdown-formatted image links * the LLM can embed directly in its reply. */ export async function lookupProductImages(model: string, userCtx?: UserCtx): Promise { const result = await fetchProductImages({ model, limit: 6 }, userCtx); if (!result.images.length) return `No product images found for model "${model}".`; const lines = result.images.map( (img) => `![${img.model}](${img.full_url})` ); return `Product images for **${model}**:\n${lines.join("\n")}`; } // ─── Formatters ─────────────────────────────────────────────────────────────── function formatOrderDetail(o: OrderDetail): string { const header = [ `Order: ${o.SOID ?? "—"}`, `Status: ${o.StatusDesc ?? o.Status ?? "—"}`, o.CustomerName ? `Customer: ${o.CustomerName}` : null, o.POID ? `PO#: ${o.POID}` : null, o.ScheduleDate ? `Ship Date: ${o.ScheduleDate}` : null, o.ShipVia ? `Ship Via: ${o.ShipVia}` : null, o.ReceiverName ? `Receiver: ${o.ReceiverName}, ${o.ReceiverCity ?? ""} ${o.ReceiverState ?? ""}` : null, o.Notes ? `Notes: ${o.Notes}` : null, ] .filter(Boolean) .join("\n"); const items = o.Items?.length ? "\nLine items:\n" + o.Items.map((i) => { const parts = [ ` [${i.LineNo}] ${i.Model ?? "—"}`, i.Description ? `— ${i.Description}` : null, `Qty: ${i.Quantity ?? 0}`, i.UnitPrice != null ? `@ $${i.UnitPrice}` : null, i.StatusDesc ? `(${i.StatusDesc})` : null, i.ETD ? `ETD: ${i.ETD}` : null, i.ItemNote ? `Note: ${i.ItemNote}` : null, ].filter(Boolean); return parts.join(" "); }).join("\n") : ""; return header + items; } function formatOrderList(rows: OrderListItem[]): string { // Group by SOID to deduplicate (one row per line item) const seen = new Set(); const lines: string[] = []; for (const r of rows) { const soId = r.SOID ?? "—"; if (!seen.has(soId)) { seen.add(soId); const parts = [ `• ${soId}`, r.CustomerName ? `— ${r.CustomerName}` : null, r.Status ? `[${r.Status}]` : null, r.ScheduleDate ? `Ship: ${r.ScheduleDate}` : null, r.InvoiceNo ? `Inv: ${r.InvoiceNo}` : null, r.TrackingNumber ? `Track: ${r.TrackingNumber}` : null, ].filter(Boolean); lines.push(parts.join(" ")); } // Show line items beneath the order if (r.Model) { lines.push( ` – ${r.Model} Qty: ${r.Quantity ?? 0} Price: $${r.UnitPrice ?? 0}` ); } } return `Orders (${seen.size} found):\n${lines.join("\n")}`; } function formatTrackingList(rows: OrderListItem[]): string { const seen = new Set(); const lines: string[] = []; for (const r of rows) { const soId = r.SOID ?? "—"; if (seen.has(soId)) continue; seen.add(soId); const parts = [ `• SO: ${soId}`, r.Status ? `[${r.Status}]` : null, r.POID ? `PO: ${r.POID}` : null, r.ShipVia ? `Ship Via: ${r.ShipVia}` : null, r.Carrier ? `Carrier: ${r.Carrier}` : null, r.TrackingNumber ? `Tracking #: ${r.TrackingNumber}` : "Tracking #: Not yet available", r.ETD ? `ETD: ${r.ETD}` : null, r.InvoiceDate ? `Invoice Date: ${r.InvoiceDate}` : null, ].filter(Boolean); lines.push(parts.join(" ")); } return `Shipment tracking for your recent orders (${seen.size}):\n${lines.join("\n")}`; }