ソースを参照

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tony T 5 日 前
コミット
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) {