소스 검색

feat: add real-time shipment tracking intent via ERP order data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tony T 6 일 전
부모
커밋
f1254e83a2
2개의 변경된 파일52개의 추가작업 그리고 4개의 파일을 삭제
  1. 38 0
      server/erpTools.ts
  2. 14 4
      server/routers.ts

+ 38 - 0
server/erpTools.ts

@@ -43,6 +43,19 @@ export async function lookupOrdersByCustomer(
   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<string> {
+  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.
  */
@@ -205,3 +218,28 @@ function formatOrderList(rows: OrderListItem[]): string {
 
   return `Orders (${seen.size} found):\n${lines.join("\n")}`;
 }
+
+function formatTrackingList(rows: OrderListItem[]): string {
+  const seen = new Set<string>();
+  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")}`;
+}

+ 14 - 4
server/routers.ts

@@ -40,6 +40,7 @@ import {
   lookupOrder,
   lookupOrdersByCustomer,
   lookupOrdersByPO,
+  lookupTrackingByCustomer,
   lookupCatalog,
   lookupStock,
   lookupContact,
@@ -371,7 +372,16 @@ export const appRouter = router({
                 erpContext = "NOTE: This user has not been linked to an ERP dealer account. Their erpContactCid is not set. Let them know they should contact support to link their dealer account before order history can be retrieved.";
               }
 
-            // 3. PO number lookup  —  "PO-12345", "purchase order 5678"
+            // 3. Shipment tracking — "track my shipment", "tracking number", "where is my package"
+            } else if (/\b(track|tracking|shipment|shipped|where.*order|delivery status|carrier|ship.*status)\b/.test(msgLower)) {
+              const cid = ctx.user?.erpContactCid ?? (conversation as any).customerId as string | undefined;
+              if (cid) {
+                erpContext = await lookupTrackingByCustomer(cid, 5, userCtx);
+              } else {
+                erpContext = "NOTE: This user has not been linked to an ERP dealer account. Their erpContactCid is not set. Ask for their order number to look up tracking.";
+              }
+
+            // 4. PO number lookup  —  "PO-12345", "purchase order 5678"
             } else {
               const poMatch = msg.match(/\bPO[-\s]?\d{3,}\b/i);
               if (poMatch) {
@@ -379,7 +389,7 @@ export const appRouter = router({
               }
             }
 
-            // 4. Stock / inventory
+            // 5. Stock / inventory
             if (!erpContext && /\b(in stock|available|inventory|stock|availability)\b/.test(msgLower)) {
               // Try to extract a model number: capital letters + digits, e.g. "B1234-1"
               const modelMatch = msg.match(/\b([A-Z]{1,4}[-\s]?\d{3,}[-\w]*)\b/);
@@ -388,7 +398,7 @@ export const appRouter = router({
               }
             }
 
-            // 5. Product / catalog search
+            // 6. Product / catalog search
             if (!erpContext && /\b(product|catalog|collection|furniture|model|item|sofa|bed|table|chair|dresser|cabinet)\b/.test(msgLower)) {
               // Pull keywords: skip common stop words
               const stopWords = new Set(["the","a","an","is","are","do","you","have","i","can","tell","me","about","show","what","which"]);
@@ -403,7 +413,7 @@ export const appRouter = router({
               }
             }
 
-            // 6. Customer / dealer lookup (admin/agent only — dealers see their own record via CID)
+            // 7. Customer / dealer lookup (admin/agent only — dealers see their own record via CID)
             if (!erpContext && ctx.user?.role !== "user" && /\b(customer|dealer|account|contact|company)\b/.test(msgLower)) {
               const nameMatch = msg.match(/(?:customer|dealer|account|contact|company)[:\s]+([A-Za-z &'.-]{3,40})/i);
               if (nameMatch) {