erpTools.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  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. } from "./erpClient";
  17. // ─── Order lookup ─────────────────────────────────────────────────────────────
  18. /**
  19. * Fetch a single sales order by SO-ID and return a chatbot-friendly summary.
  20. */
  21. export async function lookupOrder(soId: string): Promise<string> {
  22. const order = await fetchOrderDetail(soId.trim().toUpperCase());
  23. if (!order) return `Sales order "${soId}" was not found in the system.`;
  24. return formatOrderDetail(order);
  25. }
  26. /**
  27. * Fetch the N most recent orders for a customer (by CID or name).
  28. */
  29. export async function lookupOrdersByCustomer(
  30. customerCid: string,
  31. limit = 5
  32. ): Promise<string> {
  33. const rows = await fetchOrdersList({ customer_cid: customerCid, limit });
  34. if (!rows.length) return `No orders found for customer "${customerCid}".`;
  35. return formatOrderList(rows);
  36. }
  37. /**
  38. * Search orders by PO number or partial SO-ID.
  39. */
  40. export async function lookupOrdersByPO(poId: string): Promise<string> {
  41. const rows = await fetchOrdersList({ po_id: poId, limit: 10 });
  42. if (!rows.length) return `No orders found with PO number "${poId}".`;
  43. return formatOrderList(rows);
  44. }
  45. // ─── Catalog / product lookup ─────────────────────────────────────────────────
  46. export async function lookupCatalog(params: {
  47. model?: string;
  48. description?: string;
  49. category?: string;
  50. manufacturer?: string;
  51. limit?: number;
  52. }): Promise<string> {
  53. const items = await fetchCatalog({ ...params, limit: params.limit ?? 10 });
  54. if (!items.length) return "No matching products found in the catalog.";
  55. const lines = items.map((i) => {
  56. const parts = [
  57. `• ${i.Model ?? "—"}`,
  58. i.Manufacturer ? `by ${i.Manufacturer}` : null,
  59. i.Description ? `— ${i.Description}` : null,
  60. i.Type ? `[${i.Type}]` : null,
  61. i.Status ? `Status: ${i.Status}` : null,
  62. i.StockQTY != null ? `Stock: ${i.StockQTY} ${i.UOM ?? ""}`.trim() : null,
  63. ].filter(Boolean);
  64. return parts.join(" ");
  65. });
  66. return `Product catalog results (${items.length}):\n${lines.join("\n")}`;
  67. }
  68. // ─── Stock lookup ─────────────────────────────────────────────────────────────
  69. export async function lookupStock(params: {
  70. model: string;
  71. warehouse_cid?: string;
  72. limit?: number;
  73. }): Promise<string> {
  74. const records = await fetchStock({ ...params, limit: params.limit ?? 30 });
  75. if (!records.length)
  76. return `No stock records found for model "${params.model}".`;
  77. // Aggregate quantity by model
  78. const byModel: Record<string, { qty: number; wh: Set<string>; eta: string | null }> = {};
  79. for (const r of records) {
  80. const key = r.Model ?? "UNKNOWN";
  81. if (!byModel[key]) byModel[key] = { qty: 0, wh: new Set(), eta: null };
  82. byModel[key].qty += r.Quantity ?? 0;
  83. if (r.WarehouseCID) byModel[key].wh.add(r.WarehouseCID);
  84. if (!byModel[key].eta && (r.A_ETA ?? r.B_ETA))
  85. byModel[key].eta = r.A_ETA ?? r.B_ETA;
  86. }
  87. const lines = Object.entries(byModel).map(([model, { qty, wh, eta }]) => {
  88. const parts = [`• ${model}`, `Qty: ${qty}`];
  89. if (wh.size) parts.push(`Warehouse: ${[...wh].join(", ")}`);
  90. if (eta) parts.push(`ETA: ${eta}`);
  91. return parts.join(" ");
  92. });
  93. return `Stock availability for "${params.model}" (${records.length} pallet records):\n${lines.join("\n")}`;
  94. }
  95. // ─── Contact / customer lookup ────────────────────────────────────────────────
  96. export async function lookupContact(params: {
  97. contact_id?: string;
  98. company?: string;
  99. name?: string;
  100. }): Promise<string> {
  101. const contacts = await fetchContacts({ ...params, limit: 5 });
  102. if (!contacts.length) return "No matching customer records found.";
  103. const lines = contacts.map((c) => {
  104. const parts = [
  105. `• ${c.Company ?? "—"}`,
  106. c.ContactName1 ? `(${c.ContactName1})` : null,
  107. c.Phone1 ? `Ph: ${c.Phone1}` : null,
  108. c.Email1 ? `Email: ${c.Email1}` : null,
  109. c.City ? `${c.City}, ${c.State ?? ""} ${c.Country ?? ""}`.trim() : null,
  110. c.Terms ? `Terms: ${c.Terms}` : null,
  111. c.AR_CreditHold ? "⚠ Credit Hold" : null,
  112. ].filter(Boolean);
  113. return parts.join(" ");
  114. });
  115. return `Customer records (${contacts.length}):\n${lines.join("\n")}`;
  116. }
  117. // ─── Formatters ───────────────────────────────────────────────────────────────
  118. function formatOrderDetail(o: OrderDetail): string {
  119. const header = [
  120. `Order: ${o.SOID ?? "—"}`,
  121. `Status: ${o.StatusDesc ?? o.Status ?? "—"}`,
  122. o.CustomerName ? `Customer: ${o.CustomerName}` : null,
  123. o.POID ? `PO#: ${o.POID}` : null,
  124. o.ScheduleDate ? `Ship Date: ${o.ScheduleDate}` : null,
  125. o.ShipVia ? `Ship Via: ${o.ShipVia}` : null,
  126. o.ReceiverName
  127. ? `Receiver: ${o.ReceiverName}, ${o.ReceiverCity ?? ""} ${o.ReceiverState ?? ""}`
  128. : null,
  129. o.Notes ? `Notes: ${o.Notes}` : null,
  130. ]
  131. .filter(Boolean)
  132. .join("\n");
  133. const items =
  134. o.Items?.length
  135. ? "\nLine items:\n" +
  136. o.Items.map((i) => {
  137. const parts = [
  138. ` [${i.LineNo}] ${i.Model ?? "—"}`,
  139. i.Description ? `— ${i.Description}` : null,
  140. `Qty: ${i.Quantity ?? 0}`,
  141. i.UnitPrice != null ? `@ $${i.UnitPrice}` : null,
  142. i.StatusDesc ? `(${i.StatusDesc})` : null,
  143. i.ETD ? `ETD: ${i.ETD}` : null,
  144. i.ItemNote ? `Note: ${i.ItemNote}` : null,
  145. ].filter(Boolean);
  146. return parts.join(" ");
  147. }).join("\n")
  148. : "";
  149. return header + items;
  150. }
  151. function formatOrderList(rows: OrderListItem[]): string {
  152. // Group by SOID to deduplicate (one row per line item)
  153. const seen = new Set<string>();
  154. const lines: string[] = [];
  155. for (const r of rows) {
  156. const soId = r.SOID ?? "—";
  157. if (!seen.has(soId)) {
  158. seen.add(soId);
  159. const parts = [
  160. `• ${soId}`,
  161. r.CustomerName ? `— ${r.CustomerName}` : null,
  162. r.Status ? `[${r.Status}]` : null,
  163. r.ScheduleDate ? `Ship: ${r.ScheduleDate}` : null,
  164. r.InvoiceNo ? `Inv: ${r.InvoiceNo}` : null,
  165. r.TrackingNumber ? `Track: ${r.TrackingNumber}` : null,
  166. ].filter(Boolean);
  167. lines.push(parts.join(" "));
  168. }
  169. // Show line items beneath the order
  170. if (r.Model) {
  171. lines.push(
  172. ` – ${r.Model} Qty: ${r.Quantity ?? 0} Price: $${r.UnitPrice ?? 0}`
  173. );
  174. }
  175. }
  176. return `Orders (${seen.size} found):\n${lines.join("\n")}`;
  177. }