erpTools.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. /**
  2. * ERP business-query helpers for the chatbot.
  3. * Each function fetches ERP data and returns a concise, LLM-friendly string
  4. * (or structured object) that can be injected into the system prompt context.
  5. *
  6. * Sensitive fields (cost prices, internal IDs, etc.) are intentionally omitted.
  7. */
  8. import {
  9. fetchCatalog,
  10. fetchContacts,
  11. fetchOrderDetail,
  12. fetchOrdersList,
  13. fetchStock,
  14. type OrderDetail,
  15. type OrderListItem,
  16. type UserCtx,
  17. } from "./erpClient";
  18. export type { UserCtx };
  19. // ─── Order lookup ─────────────────────────────────────────────────────────────
  20. /**
  21. * Fetch a single sales order by SO-ID and return a chatbot-friendly summary.
  22. */
  23. export async function lookupOrder(soId: string, userCtx?: UserCtx): Promise<string> {
  24. const order = await fetchOrderDetail(soId.trim().toUpperCase(), userCtx);
  25. if (!order) return `Sales order "${soId}" was not found in the system.`;
  26. return formatOrderDetail(order);
  27. }
  28. /**
  29. * Fetch the N most recent orders for a customer (by CID or name).
  30. */
  31. export async function lookupOrdersByCustomer(
  32. customerCid: string,
  33. limit = 5,
  34. userCtx?: UserCtx
  35. ): Promise<string> {
  36. const rows = await fetchOrdersList({ customer_cid: customerCid, limit }, userCtx);
  37. if (!rows.length) return `No orders found for customer "${customerCid}".`;
  38. return formatOrderList(rows);
  39. }
  40. /**
  41. * Fetch recent orders for a customer and return tracking-focused summary.
  42. */
  43. export async function lookupTrackingByCustomer(
  44. customerCid: string,
  45. limit = 5,
  46. userCtx?: UserCtx
  47. ): Promise<string> {
  48. const rows = await fetchOrdersList({ customer_cid: customerCid, limit }, userCtx);
  49. if (!rows.length) return `No recent orders found for customer "${customerCid}".`;
  50. return formatTrackingList(rows);
  51. }
  52. /**
  53. * Search orders by PO number or partial SO-ID.
  54. */
  55. export async function lookupOrdersByPO(poId: string, userCtx?: UserCtx): Promise<string> {
  56. const rows = await fetchOrdersList({ po_id: poId, limit: 10 }, userCtx);
  57. if (!rows.length) return `No orders found with PO number "${poId}".`;
  58. return formatOrderList(rows);
  59. }
  60. // ─── Catalog / product lookup ─────────────────────────────────────────────────
  61. export async function lookupCatalog(params: {
  62. model?: string;
  63. description?: string;
  64. category?: string;
  65. manufacturer?: string;
  66. limit?: number;
  67. }, userCtx?: UserCtx): Promise<string> {
  68. const items = await fetchCatalog({ ...params, limit: params.limit ?? 10 }, userCtx);
  69. if (!items.length) return "No matching products found in the catalog.";
  70. const lines = items.map((i) => {
  71. const parts = [
  72. `• ${i.Model ?? "—"}`,
  73. i.Manufacturer ? `by ${i.Manufacturer}` : null,
  74. i.Description ? `— ${i.Description}` : null,
  75. i.Type ? `[${i.Type}]` : null,
  76. i.Status ? `Status: ${i.Status}` : null,
  77. i.StockQTY != null ? `Stock: ${i.StockQTY} ${i.UOM ?? ""}`.trim() : null,
  78. ].filter(Boolean);
  79. return parts.join(" ");
  80. });
  81. return `Product catalog results (${items.length}):\n${lines.join("\n")}`;
  82. }
  83. // ─── Stock lookup ─────────────────────────────────────────────────────────────
  84. export async function lookupStock(params: {
  85. model: string;
  86. warehouse_cid?: string;
  87. limit?: number;
  88. }, userCtx?: UserCtx): Promise<string> {
  89. const records = await fetchStock({ ...params, limit: params.limit ?? 30 }, userCtx);
  90. if (!records.length)
  91. return `No stock records found for model "${params.model}".`;
  92. // Aggregate quantity by model
  93. const byModel: Record<string, { qty: number; wh: Set<string>; eta: string | null }> = {};
  94. for (const r of records) {
  95. const key = r.Model ?? "UNKNOWN";
  96. if (!byModel[key]) byModel[key] = { qty: 0, wh: new Set(), eta: null };
  97. byModel[key].qty += r.Quantity ?? 0;
  98. if (r.WarehouseCID) byModel[key].wh.add(r.WarehouseCID);
  99. if (!byModel[key].eta && (r.A_ETA ?? r.B_ETA))
  100. byModel[key].eta = r.A_ETA ?? r.B_ETA;
  101. }
  102. const lines = Object.entries(byModel).map(([model, { qty, wh, eta }]) => {
  103. const parts = [`• ${model}`, `Qty: ${qty}`];
  104. if (wh.size) parts.push(`Warehouse: ${[...wh].join(", ")}`);
  105. if (eta) parts.push(`ETA: ${eta}`);
  106. return parts.join(" ");
  107. });
  108. return `Stock availability for "${params.model}" (${records.length} pallet records):\n${lines.join("\n")}`;
  109. }
  110. // ─── Contact / customer lookup ────────────────────────────────────────────────
  111. export async function lookupContact(params: {
  112. contact_id?: string;
  113. company?: string;
  114. name?: string;
  115. }, userCtx?: UserCtx): Promise<string> {
  116. const contacts = await fetchContacts({ ...params, limit: 5 }, userCtx);
  117. if (!contacts.length) return "No matching customer records found.";
  118. const lines = contacts.map((c) => {
  119. const parts = [
  120. `• ${c.Company ?? "—"}`,
  121. c.ContactName1 ? `(${c.ContactName1})` : null,
  122. c.Phone1 ? `Ph: ${c.Phone1}` : null,
  123. c.Email1 ? `Email: ${c.Email1}` : null,
  124. c.City ? `${c.City}, ${c.State ?? ""} ${c.Country ?? ""}`.trim() : null,
  125. c.Terms ? `Terms: ${c.Terms}` : null,
  126. c.AR_CreditHold ? "⚠ Credit Hold" : null,
  127. ].filter(Boolean);
  128. return parts.join(" ");
  129. });
  130. return `Customer records (${contacts.length}):\n${lines.join("\n")}`;
  131. }
  132. // ─── Formatters ───────────────────────────────────────────────────────────────
  133. function formatOrderDetail(o: OrderDetail): string {
  134. const header = [
  135. `Order: ${o.SOID ?? "—"}`,
  136. `Status: ${o.StatusDesc ?? o.Status ?? "—"}`,
  137. o.CustomerName ? `Customer: ${o.CustomerName}` : null,
  138. o.POID ? `PO#: ${o.POID}` : null,
  139. o.ScheduleDate ? `Ship Date: ${o.ScheduleDate}` : null,
  140. o.ShipVia ? `Ship Via: ${o.ShipVia}` : null,
  141. o.ReceiverName
  142. ? `Receiver: ${o.ReceiverName}, ${o.ReceiverCity ?? ""} ${o.ReceiverState ?? ""}`
  143. : null,
  144. o.Notes ? `Notes: ${o.Notes}` : null,
  145. ]
  146. .filter(Boolean)
  147. .join("\n");
  148. const items =
  149. o.Items?.length
  150. ? "\nLine items:\n" +
  151. o.Items.map((i) => {
  152. const parts = [
  153. ` [${i.LineNo}] ${i.Model ?? "—"}`,
  154. i.Description ? `— ${i.Description}` : null,
  155. `Qty: ${i.Quantity ?? 0}`,
  156. i.UnitPrice != null ? `@ $${i.UnitPrice}` : null,
  157. i.StatusDesc ? `(${i.StatusDesc})` : null,
  158. i.ETD ? `ETD: ${i.ETD}` : null,
  159. i.ItemNote ? `Note: ${i.ItemNote}` : null,
  160. ].filter(Boolean);
  161. return parts.join(" ");
  162. }).join("\n")
  163. : "";
  164. return header + items;
  165. }
  166. function formatOrderList(rows: OrderListItem[]): string {
  167. // Group by SOID to deduplicate (one row per line item)
  168. const seen = new Set<string>();
  169. const lines: string[] = [];
  170. for (const r of rows) {
  171. const soId = r.SOID ?? "—";
  172. if (!seen.has(soId)) {
  173. seen.add(soId);
  174. const parts = [
  175. `• ${soId}`,
  176. r.CustomerName ? `— ${r.CustomerName}` : null,
  177. r.Status ? `[${r.Status}]` : null,
  178. r.ScheduleDate ? `Ship: ${r.ScheduleDate}` : null,
  179. r.InvoiceNo ? `Inv: ${r.InvoiceNo}` : null,
  180. r.TrackingNumber ? `Track: ${r.TrackingNumber}` : null,
  181. ].filter(Boolean);
  182. lines.push(parts.join(" "));
  183. }
  184. // Show line items beneath the order
  185. if (r.Model) {
  186. lines.push(
  187. ` – ${r.Model} Qty: ${r.Quantity ?? 0} Price: $${r.UnitPrice ?? 0}`
  188. );
  189. }
  190. }
  191. return `Orders (${seen.size} found):\n${lines.join("\n")}`;
  192. }
  193. function formatTrackingList(rows: OrderListItem[]): string {
  194. const seen = new Set<string>();
  195. const lines: string[] = [];
  196. for (const r of rows) {
  197. const soId = r.SOID ?? "—";
  198. if (seen.has(soId)) continue;
  199. seen.add(soId);
  200. const parts = [
  201. `• SO: ${soId}`,
  202. r.Status ? `[${r.Status}]` : null,
  203. r.POID ? `PO: ${r.POID}` : null,
  204. r.ShipVia ? `Ship Via: ${r.ShipVia}` : null,
  205. r.Carrier ? `Carrier: ${r.Carrier}` : null,
  206. r.TrackingNumber ? `Tracking #: ${r.TrackingNumber}` : "Tracking #: Not yet available",
  207. r.ETD ? `ETD: ${r.ETD}` : null,
  208. r.InvoiceDate ? `Invoice Date: ${r.InvoiceDate}` : null,
  209. ].filter(Boolean);
  210. lines.push(parts.join(" "));
  211. }
  212. return `Shipment tracking for your recent orders (${seen.size}):\n${lines.join("\n")}`;
  213. }