Parcourir la source

feat(mcp): resolve ShopRenter order status names from API

- Add functions to extract order_status_id from base64-encoded URLs
- Add orderStatusDescriptions API endpoint support with caching
- Update mcp-shoprenter to resolve human-readable status names
- Add error handling for pending_shoprenter_installs deletion

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh il y a 4 mois
Parent
commit
08c2053434

+ 120 - 0
supabase/functions/_shared/shoprenter-client.ts

@@ -805,3 +805,123 @@ export async function deleteWebhook(storeId: string, webhookId: string): Promise
     'DELETE'
   )
 }
+
+/**
+ * Extract order_status_id from ShopRenter orderStatus href
+ *
+ * The orderStatus.href looks like:
+ * http://shop.api.myshoprenter.hu/orderStatuses/b3JkZXJTdGF0dXMtb3JkZXJfc3RhdHVzX2lkPTE=
+ *
+ * The base64 part decodes to: orderStatus-order_status_id=1
+ */
+export function extractOrderStatusId(orderStatusHref: string): string | null {
+  try {
+    // Extract the base64 ID from the URL
+    const base64Id = orderStatusHref.split('/').pop()
+    if (!base64Id) return null
+
+    // Decode base64 to get: orderStatus-order_status_id=X
+    const decoded = atob(base64Id)
+
+    // Extract the order_status_id value
+    const match = decoded.match(/order_status_id=(\d+)/)
+    if (!match) return null
+
+    return match[1]
+  } catch (error) {
+    console.error('[ShopRenter] Error extracting order_status_id:', error)
+    return null
+  }
+}
+
+/**
+ * Build orderStatusDescription ID for ShopRenter API
+ *
+ * The ID format is: orderStatusDescription-order_status_id=X&language_id=Y
+ * This needs to be base64 encoded
+ */
+export function buildOrderStatusDescriptionId(orderStatusId: string, languageId: string = '1'): string {
+  const idString = `orderStatusDescription-order_status_id=${orderStatusId}&language_id=${languageId}`
+  return btoa(idString)
+}
+
+/**
+ * Fetch order status description from ShopRenter
+ * Returns the human-readable status name
+ */
+export async function fetchOrderStatusDescription(
+  storeId: string,
+  orderStatusId: string,
+  languageId: string = '1'
+): Promise<{ name: string; color?: string } | null> {
+  try {
+    const descriptionId = buildOrderStatusDescriptionId(orderStatusId, languageId)
+    const response = await shopRenterApiRequest(
+      storeId,
+      `/orderStatusDescriptions/${descriptionId}`,
+      'GET'
+    )
+
+    return {
+      name: response.name || 'Unknown',
+      color: response.color
+    }
+  } catch (error) {
+    console.error('[ShopRenter] Error fetching order status description:', error)
+    return null
+  }
+}
+
+/**
+ * Resolve order status name from orderStatus href
+ * Combines extracting the ID and fetching the description
+ */
+export async function resolveOrderStatusName(
+  storeId: string,
+  orderStatusHref: string,
+  languageId: string = '1'
+): Promise<string> {
+  const statusId = extractOrderStatusId(orderStatusHref)
+  if (!statusId) {
+    console.warn('[ShopRenter] Could not extract order_status_id from:', orderStatusHref)
+    return 'Unknown'
+  }
+
+  const description = await fetchOrderStatusDescription(storeId, statusId, languageId)
+  return description?.name || 'Unknown'
+}
+
+/**
+ * Cache for order status descriptions to avoid repeated API calls
+ */
+const orderStatusCache = new Map<string, { name: string; color?: string }>()
+
+/**
+ * Get order status description with caching
+ */
+export async function getOrderStatusWithCache(
+  storeId: string,
+  orderStatusHref: string,
+  languageId: string = '1'
+): Promise<{ name: string; color?: string }> {
+  const statusId = extractOrderStatusId(orderStatusHref)
+  if (!statusId) {
+    return { name: 'Unknown' }
+  }
+
+  const cacheKey = `${storeId}:${statusId}:${languageId}`
+
+  // Check cache first
+  if (orderStatusCache.has(cacheKey)) {
+    return orderStatusCache.get(cacheKey)!
+  }
+
+  // Fetch from API
+  const description = await fetchOrderStatusDescription(storeId, statusId, languageId)
+  const result = description || { name: 'Unknown' }
+
+  // Store in cache
+  orderStatusCache.set(cacheKey, result)
+
+  return result
+}

+ 12 - 2
supabase/functions/auto-register-shoprenter/index.ts

@@ -360,18 +360,28 @@ serve(wrapHandler('auto-register-shoprenter', async (req) => {
     }
 
     // Clean up pending installation
-    await supabase
+    const { error: deleteError } = await supabase
       .from('pending_shoprenter_installs')
       .delete()
       .eq('id', pendingInstall.id)
 
+    if (deleteError) {
+      console.error(`[AutoRegister] Failed to delete pending installation ${pendingInstall.id}:`, deleteError)
+    } else {
+      console.log(`[AutoRegister] Deleted pending installation ${pendingInstall.id}`)
+    }
+
     // Clean up any related oauth_states
-    await supabase
+    const { error: oauthDeleteError } = await supabase
       .from('oauth_states')
       .delete()
       .eq('platform', 'shoprenter')
       .eq('shopname', pendingInstall.shopname)
 
+    if (oauthDeleteError) {
+      console.error(`[AutoRegister] Failed to delete oauth_states:`, oauthDeleteError)
+    }
+
     console.log(`[AutoRegister] Successfully completed auto-registration for ${email}`)
 
     // Register store with scraper service in background

+ 7 - 1
supabase/functions/complete-shoprenter-install/index.ts

@@ -291,11 +291,17 @@ serve(wrapHandler('complete-shoprenter-install', async (req) => {
     }
 
     // Clean up pending installation
-    await supabase
+    const { error: deleteError } = await supabase
       .from('pending_shoprenter_installs')
       .delete()
       .eq('id', pendingInstall.id)
 
+    if (deleteError) {
+      console.error('[ShopRenter] Failed to delete pending installation:', deleteError)
+    } else {
+      console.log(`[ShopRenter] Deleted pending installation ${pendingInstall.id}`)
+    }
+
     // Clean up any related oauth_states
     await supabase
       .from('oauth_states')

+ 82 - 11
supabase/functions/mcp-shoprenter/index.ts

@@ -23,7 +23,8 @@ import {
   ShopRenterOrder,
   ShopRenterCustomer,
   ShopRenterOrderFilters,
-  ShopRenterCustomerFilters
+  ShopRenterCustomerFilters,
+  getOrderStatusWithCache
 } from '../_shared/shoprenter-client.ts';
 import {
   McpTool,
@@ -258,11 +259,23 @@ function formatCustomerForLlm(customer: any): LlmCustomer {
  * - paymentAddress1, paymentCity, etc.
  *
  * We preserve the raw data and apply cleanResponseData to remove URLs/empty values
+ *
+ * @param order - The raw order from ShopRenter API
+ * @param storeId - The store ID (optional, used to resolve status name)
+ * @param resolvedStatusName - Pre-resolved status name (optional)
  */
-function formatOrderForLlm(order: any): any {
+function formatOrderForLlm(order: any, storeId?: string, resolvedStatusName?: string): any {
   // ShopRenter orderExtend API returns the order data in a specific structure
   // We'll preserve most of the original structure but ensure key fields are present
 
+  // Determine the status - use resolved name if provided, otherwise use raw status
+  let status: any = order.status || order.orderStatus;
+
+  // If we have a resolved status name, use it
+  if (resolvedStatusName) {
+    status = { name: resolvedStatusName };
+  }
+
   const formattedOrder: any = {
     id: order.id,
     orderNumber: order.innerId || order.orderNumber || order.id,
@@ -274,7 +287,7 @@ function formatOrderForLlm(order: any): any {
     phone: order.phone || '',
 
     // Order status and totals
-    status: order.status || order.orderStatus,
+    status: status,
     total: order.total || '0',
     currency: order.currency || {},
 
@@ -329,6 +342,44 @@ function formatOrderForLlm(order: any): any {
   return formattedOrder;
 }
 
+/**
+ * Resolve order status name from ShopRenter orderStatus href
+ * Uses caching to avoid repeated API calls for the same status
+ */
+async function resolveOrderStatus(storeId: string, order: any): Promise<string | undefined> {
+  // Check if orderStatus has an href (URL reference)
+  const orderStatusHref = order.orderStatus?.href;
+  if (!orderStatusHref) {
+    return undefined;
+  }
+
+  try {
+    // Get the language ID from the order if available
+    let languageId = '1'; // Default to Hungarian (language_id=1)
+    if (order.language?.href) {
+      // Extract language ID from href like: .../languages/bGFuZ3VhZ2UtbGFuZ3VhZ2VfaWQ9MQ==
+      const langBase64 = order.language.href.split('/').pop();
+      if (langBase64) {
+        try {
+          const decoded = atob(langBase64);
+          const match = decoded.match(/language_id=(\d+)/);
+          if (match) {
+            languageId = match[1];
+          }
+        } catch (e) {
+          // Keep default language ID
+        }
+      }
+    }
+
+    const statusInfo = await getOrderStatusWithCache(storeId, orderStatusHref, languageId);
+    return statusInfo.name;
+  } catch (error) {
+    console.error('[MCP ShopRenter] Error resolving order status:', error);
+    return undefined;
+  }
+}
+
 /**
  * Format a product as plain text for LLM consumption
  */
@@ -704,8 +755,12 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
 
       console.log('[MCP ShopRenter] Order found:', { innerId: orders[0].innerId || orders[0].id });
 
-      // Format for LLM
-      const formattedOrder = formatOrderForLlm(orders[0]);
+      // Resolve order status name
+      const resolvedStatusName = await resolveOrderStatus(shop_id, orders[0]);
+      console.log('[MCP ShopRenter] Resolved status name:', resolvedStatusName);
+
+      // Format for LLM with resolved status name
+      const formattedOrder = formatOrderForLlm(orders[0], shop_id, resolvedStatusName);
 
       // Format as plain text
       return {
@@ -777,8 +832,12 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
 
         console.log('[MCP ShopRenter] Order found:', { innerId: orders[0].innerId || orders[0].id });
 
-        // Format for LLM
-        const formattedOrder = formatOrderForLlm(orders[0]);
+        // Resolve order status name
+        const resolvedStatusName = await resolveOrderStatus(shop_id, orders[0]);
+        console.log('[MCP ShopRenter] Resolved status name:', resolvedStatusName);
+
+        // Format for LLM with resolved status name
+        const formattedOrder = formatOrderForLlm(orders[0], shop_id, resolvedStatusName);
 
         // Format as plain text
         return {
@@ -874,8 +933,14 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
       // Apply limit
       const limitedOrders = orders.slice(0, actualLimit);
 
-      // Format for LLM (now preserves all ShopRenter fields)
-      const formattedOrders = limitedOrders.map(formatOrderForLlm);
+      // Resolve status names for all orders (in parallel for efficiency)
+      const statusPromises = limitedOrders.map((order: any) => resolveOrderStatus(shop_id, order));
+      const resolvedStatuses = await Promise.all(statusPromises);
+
+      // Format for LLM with resolved status names
+      const formattedOrders = limitedOrders.map((order: any, index: number) =>
+        formatOrderForLlm(order, shop_id, resolvedStatuses[index])
+      );
 
       // Format as plain text
       const plainTextOrders = formattedOrders.map(formatOrderAsPlainText).join('\n\n---\n\n');
@@ -972,8 +1037,14 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
         // Apply limit
         const limitedOrders = orders.slice(0, actualLimit);
 
-        // Format for LLM (now preserves all ShopRenter fields)
-        const formattedOrders = limitedOrders.map(formatOrderForLlm);
+        // Resolve status names for all orders (in parallel for efficiency)
+        const statusPromises = limitedOrders.map((order: any) => resolveOrderStatus(shop_id, order));
+        const resolvedStatuses = await Promise.all(statusPromises);
+
+        // Format for LLM with resolved status names
+        const formattedOrders = limitedOrders.map((order: any, index: number) =>
+          formatOrderForLlm(order, shop_id, resolvedStatuses[index])
+        );
 
         // Format as plain text
         const plainTextOrders = formattedOrders.map(formatOrderAsPlainText).join('\n\n---\n\n');