erpTools.ts 9.3 KB

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