Browse Source

fix: ShopRenter sync - add UI checkbox fix and Qdrant sync for orders/customers #85

- Fix: Added sync_orders and sync_customers fields to API query
  - UI checkbox now shows actual state from store_sync_config

- Feature: Implement Qdrant sync for orders when sync_orders enabled
  - New syncOrdersToQdrant() function
  - Fetches orders from ShopRenter API
  - Creates embeddings and syncs to Qdrant
  - No database caching (GDPR compliance)

- Feature: Implement Qdrant sync for customers when sync_customers enabled
  - New syncCustomersToQdrant() function
  - Fetches customers from ShopRenter API
  - Creates embeddings and syncs to Qdrant
  - No database caching (GDPR compliance)

- Update: ShopRenter sync now checks store_sync_config flags
  - Respects sync_orders and sync_customers settings
  - Only syncs when both permission and config enabled

- Deployed: api and shoprenter-sync Edge Functions
Claude 5 months ago
parent
commit
d67aac4109
2 changed files with 464 additions and 14 deletions
  1. 2 0
      supabase/functions/api/index.ts
  2. 462 14
      supabase/functions/shoprenter-sync/index.ts

+ 2 - 0
supabase/functions/api/index.ts

@@ -74,6 +74,8 @@ serve(async (req) => {
             enabled,
             enabled,
             sync_frequency,
             sync_frequency,
             sync_products,
             sync_products,
+            sync_orders,
+            sync_customers,
             last_sync_at,
             last_sync_at,
             next_sync_at
             next_sync_at
           )
           )

+ 462 - 14
supabase/functions/shoprenter-sync/index.ts

@@ -1,7 +1,7 @@
 import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { wrapHandler, logError } from '../_shared/error-handler.ts'
 import { wrapHandler, logError } from '../_shared/error-handler.ts'
-import { fetchProducts } from '../_shared/shoprenter-client.ts'
+import { fetchProducts, fetchOrders, fetchCustomers } from '../_shared/shoprenter-client.ts'
 import { detectCountryCode } from '../_shared/phone-formatter.ts'
 import { detectCountryCode } from '../_shared/phone-formatter.ts'
 import {
 import {
   collectionExists,
   collectionExists,
@@ -265,6 +265,326 @@ async function syncProductsToQdrant(
   return { synced, errors }
   return { synced, errors }
 }
 }
 
 
+// Sync orders to Qdrant
+async function syncOrdersToQdrant(
+  storeId: string,
+  storeName: string,
+  orders: any[],
+  supabaseAdmin: any
+): Promise<{ synced: number; errors: number }> {
+  const startTime = new Date()
+  const collectionName = getCollectionName(storeName, 'orders')
+
+  // Filter out orders without valid IDs
+  const validOrders = orders.filter(o => o && o.innerId)
+
+  if (validOrders.length === 0) {
+    console.log('[Qdrant] No valid orders to sync (all orders missing innerId)')
+    return { synced: 0, errors: 0 }
+  }
+
+  console.log(`[Qdrant] Syncing ${validOrders.length} orders to ${collectionName}`)
+
+  let synced = 0
+  let errors = 0
+
+  try {
+    if (!(await collectionExists(collectionName))) {
+      await createCollection(collectionName, [
+        { field: 'store_id', type: 'keyword' },
+        { field: 'order_id', type: 'keyword' },
+        { field: 'platform', type: 'keyword' },
+        { field: 'status', type: 'keyword' },
+        { field: 'total_price', type: 'float' },
+        { field: 'customer_email', type: 'keyword' },
+        { field: 'customer_phone', type: 'keyword' },
+        { field: 'billing_city', type: 'keyword' },
+        { field: 'billing_country', type: 'keyword' },
+        { field: 'shipping_city', type: 'keyword' },
+        { field: 'shipping_country', type: 'keyword' },
+      ])
+    }
+
+    // Get existing order IDs from Qdrant to detect deletions
+    const existingPoints = await scrollPoints(collectionName, {
+      must: [{ key: 'store_id', match: { value: storeId } }]
+    }, 1000)
+
+    const existingOrderIds = new Set(
+      existingPoints.points.map((p: any) => p.payload?.order_id).filter(Boolean)
+    )
+
+    const currentOrderIds = new Set(validOrders.map(o => o.innerId.toString()))
+    const deletedOrderIds = Array.from(existingOrderIds).filter(
+      id => !currentOrderIds.has(id)
+    )
+
+    // Delete orders that no longer exist
+    if (deletedOrderIds.length > 0) {
+      console.log(`[Qdrant] Deleting ${deletedOrderIds.length} orders that no longer exist`)
+      await deletePointsByFilter(collectionName, {
+        must: [
+          { key: 'store_id', match: { value: storeId } },
+          { key: 'order_id', match: { any: deletedOrderIds } }
+        ]
+      })
+      await logQdrantSync(
+        supabaseAdmin,
+        storeId,
+        'orders',
+        collectionName,
+        'delete',
+        deletedOrderIds.length,
+        deletedOrderIds.length,
+        0,
+        startTime
+      )
+    }
+
+    // Generate embeddings for all orders
+    const orderTexts = validOrders.map(order => createOrderText(order))
+    const embeddings = await generateEmbeddingBatch(orderTexts)
+
+    // Create Qdrant points
+    const points: QdrantPoint[] = validOrders.map((order, index) => {
+      return {
+        id: generatePointId('shoprenter', storeId, order.innerId),
+        vector: embeddings[index],
+        payload: {
+          // Basic identification
+          store_id: storeId,
+          order_id: order.innerId.toString(),
+          platform: 'shoprenter',
+          order_number: order.orderNumber || order.innerId.toString(),
+
+          // Customer information
+          customer_name: `${order.firstname || ''} ${order.lastname || ''}`.trim() || null,
+          customer_email: order.email || null,
+          customer_phone: order.phone || null,
+
+          // Billing address
+          billing_address: {
+            address1: order.billingAddress || null,
+            city: order.billingCity || null,
+            zip: order.billingZipCode || null,
+            country: order.billingCountryName || null,
+          },
+
+          // Shipping address
+          shipping_address: {
+            address1: order.shippingAddress || null,
+            city: order.shippingCity || null,
+            zip: order.shippingZipCode || null,
+            country: order.shippingCountryName || null,
+          },
+
+          // Pricing
+          total_price: parseFloat(order.total || '0') || 0,
+          currency: order.currency || 'HUF',
+
+          // Status
+          status: order.orderStatus?.name || 'unknown',
+
+          // Order items
+          line_items: order.orderProducts || [],
+
+          // Metadata
+          synced_at: new Date().toISOString(),
+        }
+      }
+    })
+
+    await upsertPoints(collectionName, points)
+    synced = points.length
+
+    await logQdrantSync(
+      supabaseAdmin,
+      storeId,
+      'orders',
+      collectionName,
+      'upsert',
+      orders.length,
+      synced,
+      errors,
+      startTime
+    )
+
+    console.log(`[Qdrant] Orders sync complete: ${synced} synced, ${deletedOrderIds.length} deleted`)
+  } catch (error: any) {
+    console.error('[Qdrant] Order sync error:', error)
+    errors = orders.length
+    await logQdrantSync(
+      supabaseAdmin,
+      storeId,
+      'orders',
+      collectionName,
+      'upsert',
+      orders.length,
+      synced,
+      errors,
+      startTime,
+      error.message
+    )
+  }
+
+  return { synced, errors }
+}
+
+// Sync customers to Qdrant
+async function syncCustomersToQdrant(
+  storeId: string,
+  storeName: string,
+  customers: any[],
+  supabaseAdmin: any
+): Promise<{ synced: number; errors: number }> {
+  const startTime = new Date()
+  const collectionName = getCollectionName(storeName, 'customers')
+
+  // Filter out customers without valid IDs
+  const validCustomers = customers.filter(c => c && c.innerId)
+
+  if (validCustomers.length === 0) {
+    console.log('[Qdrant] No valid customers to sync (all customers missing innerId)')
+    return { synced: 0, errors: 0 }
+  }
+
+  console.log(`[Qdrant] Syncing ${validCustomers.length} customers to ${collectionName}`)
+
+  let synced = 0
+  let errors = 0
+
+  try {
+    if (!(await collectionExists(collectionName))) {
+      await createCollection(collectionName, [
+        { field: 'store_id', type: 'keyword' },
+        { field: 'customer_id', type: 'keyword' },
+        { field: 'platform', type: 'keyword' },
+        { field: 'email', type: 'keyword' },
+        { field: 'phone', type: 'keyword' },
+        { field: 'city', type: 'keyword' },
+        { field: 'country', type: 'keyword' },
+      ])
+    }
+
+    // Get existing customer IDs from Qdrant to detect deletions
+    const existingPoints = await scrollPoints(collectionName, {
+      must: [{ key: 'store_id', match: { value: storeId } }]
+    }, 1000)
+
+    const existingCustomerIds = new Set(
+      existingPoints.points.map((p: any) => p.payload?.customer_id).filter(Boolean)
+    )
+
+    const currentCustomerIds = new Set(validCustomers.map(c => c.innerId.toString()))
+    const deletedCustomerIds = Array.from(existingCustomerIds).filter(
+      id => !currentCustomerIds.has(id)
+    )
+
+    // Delete customers that no longer exist
+    if (deletedCustomerIds.length > 0) {
+      console.log(`[Qdrant] Deleting ${deletedCustomerIds.length} customers that no longer exist`)
+      await deletePointsByFilter(collectionName, {
+        must: [
+          { key: 'store_id', match: { value: storeId } },
+          { key: 'customer_id', match: { any: deletedCustomerIds } }
+        ]
+      })
+      await logQdrantSync(
+        supabaseAdmin,
+        storeId,
+        'customers',
+        collectionName,
+        'delete',
+        deletedCustomerIds.length,
+        deletedCustomerIds.length,
+        0,
+        startTime
+      )
+    }
+
+    // Generate embeddings for all customers
+    const customerTexts = validCustomers.map(customer => createCustomerText(customer))
+    const embeddings = await generateEmbeddingBatch(customerTexts)
+
+    // Create Qdrant points
+    const points: QdrantPoint[] = validCustomers.map((customer, index) => {
+      return {
+        id: generatePointId('shoprenter', storeId, customer.innerId),
+        vector: embeddings[index],
+        payload: {
+          // Basic identification
+          store_id: storeId,
+          customer_id: customer.innerId.toString(),
+          platform: 'shoprenter',
+
+          // Customer information
+          first_name: customer.firstname || null,
+          last_name: customer.lastname || null,
+          email: customer.email || null,
+          phone: customer.phone || null,
+
+          // Address
+          default_address: {
+            address1: customer.shippingAddress || null,
+            city: customer.shippingCity || null,
+            zip: customer.shippingZipCode || null,
+            country: customer.shippingCountryName || null,
+          },
+
+          // Billing address
+          billing_address: {
+            address1: customer.billingAddress || null,
+            city: customer.billingCity || null,
+            zip: customer.billingZipCode || null,
+            country: customer.billingCountryName || null,
+          },
+
+          // Customer stats (if available)
+          orders_count: customer.ordersCount || 0,
+          total_spent: parseFloat(customer.totalSpent || '0') || 0,
+
+          // Metadata
+          synced_at: new Date().toISOString(),
+        }
+      }
+    })
+
+    await upsertPoints(collectionName, points)
+    synced = points.length
+
+    await logQdrantSync(
+      supabaseAdmin,
+      storeId,
+      'customers',
+      collectionName,
+      'upsert',
+      customers.length,
+      synced,
+      errors,
+      startTime
+    )
+
+    console.log(`[Qdrant] Customers sync complete: ${synced} synced, ${deletedCustomerIds.length} deleted`)
+  } catch (error: any) {
+    console.error('[Qdrant] Customer sync error:', error)
+    errors = customers.length
+    await logQdrantSync(
+      supabaseAdmin,
+      storeId,
+      'customers',
+      collectionName,
+      'upsert',
+      customers.length,
+      synced,
+      errors,
+      startTime,
+      error.message
+    )
+  }
+
+  return { synced, errors }
+}
+
 serve(wrapHandler('shoprenter-sync', async (req) => {
 serve(wrapHandler('shoprenter-sync', async (req) => {
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
     return new Response('ok', { headers: corsHeaders })
     return new Response('ok', { headers: corsHeaders })
@@ -337,11 +657,22 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
       userId = user.id
       userId = user.id
     }
     }
 
 
-    // Now fetch store with proper authorization
+    // Now fetch store with proper authorization (include sync config)
     const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
     const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
     const { data: store, error: storeError } = await supabaseAdmin
     const { data: store, error: storeError } = await supabaseAdmin
       .from('stores')
       .from('stores')
-      .select('id, store_name, platform_name, store_url, qdrant_sync_enabled, data_access_permissions')
+      .select(`
+        id,
+        store_name,
+        platform_name,
+        store_url,
+        qdrant_sync_enabled,
+        data_access_permissions,
+        store_sync_config (
+          sync_orders,
+          sync_customers
+        )
+      `)
       .eq('id', storeId)
       .eq('id', storeId)
       .eq('user_id', userId)
       .eq('user_id', userId)
       .eq('platform_name', 'shoprenter')
       .eq('platform_name', 'shoprenter')
@@ -367,6 +698,11 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
     const canSyncCustomers = permissions.allow_customer_access !== false
     const canSyncCustomers = permissions.allow_customer_access !== false
     const qdrantEnabled = store.qdrant_sync_enabled !== false
     const qdrantEnabled = store.qdrant_sync_enabled !== false
 
 
+    // Check store_sync_config for orders/customers sync flags
+    const syncConfig = (store as any).store_sync_config?.[0] || {}
+    const shouldSyncOrders = syncConfig.sync_orders === true
+    const shouldSyncCustomers = syncConfig.sync_customers === true
+
     console.log('[ShopRenter] Sync permissions:', {
     console.log('[ShopRenter] Sync permissions:', {
       products: canSyncProducts,
       products: canSyncProducts,
       orders: canSyncOrders,
       orders: canSyncOrders,
@@ -374,9 +710,15 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
       qdrant: qdrantEnabled
       qdrant: qdrantEnabled
     })
     })
 
 
+    console.log('[ShopRenter] Sync config:', {
+      syncOrders: shouldSyncOrders,
+      syncCustomers: shouldSyncCustomers
+    })
+
     const syncStats = {
     const syncStats = {
-      products: { synced: 0, errors: 0 }
-      // Note: orders and customers removed for GDPR compliance (migration 20251031_160300)
+      products: { synced: 0, errors: 0 },
+      orders: { synced: 0, errors: 0 },
+      customers: { synced: 0, errors: 0 }
     }
     }
 
 
     // supabaseAdmin already created above, reuse it
     // supabaseAdmin already created above, reuse it
@@ -461,22 +803,128 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
       syncStats.products.errors++
       syncStats.products.errors++
     }
     }
 
 
-    // Note: Customer and Order caching removed for GDPR compliance (migration 20251031_160300)
-    // Customer/order data is now accessed in real-time via webshop-data-api endpoint
-    console.log('[ShopRenter] Skipping customer/order caching (GDPR compliance - use real-time API access)')
+    // Sync Orders to Qdrant (if enabled)
+    // Note: Not cached to database for GDPR compliance, only synced to Qdrant for AI access
+    const allOrders: any[] = []
+    let orderSyncError: Error | null = null
+    if (qdrantEnabled && shouldSyncOrders && canSyncOrders) {
+      try {
+        console.log('[ShopRenter] Syncing orders to Qdrant...')
+        let page = 0
+        let hasMore = true
+        const limit = 50
+
+        while (hasMore) {
+          const ordersData = await fetchOrders(storeId, page, limit)
+
+          if (ordersData.items && ordersData.items.length > 0) {
+            allOrders.push(...ordersData.items)
+
+            // Check if there are more pages
+            if (ordersData.pagination && ordersData.pagination.total) {
+              const totalPages = Math.ceil(ordersData.pagination.total / limit)
+              hasMore = page < totalPages - 1
+            } else {
+              hasMore = ordersData.items.length === limit
+            }
+
+            page++
+          } else {
+            hasMore = false
+          }
+        }
+
+        if (allOrders.length > 0) {
+          const qdrantResult = await syncOrdersToQdrant(storeId, store.store_name, allOrders, supabaseAdmin)
+          syncStats.orders.synced = qdrantResult.synced
+          syncStats.orders.errors = qdrantResult.errors
+          console.log(`[ShopRenter] Orders synced to Qdrant: ${qdrantResult.synced} synced, ${qdrantResult.errors} errors`)
+        }
+      } catch (error) {
+        console.error('[ShopRenter] Order sync error:', error)
+        orderSyncError = error as Error
+        syncStats.orders.errors++
+      }
+    } else {
+      console.log('[ShopRenter] Order sync skipped:', {
+        qdrantEnabled,
+        shouldSyncOrders,
+        canSyncOrders
+      })
+    }
+
+    // Sync Customers to Qdrant (if enabled)
+    // Note: Not cached to database for GDPR compliance, only synced to Qdrant for AI access
+    const allCustomers: any[] = []
+    let customerSyncError: Error | null = null
+    if (qdrantEnabled && shouldSyncCustomers && canSyncCustomers) {
+      try {
+        console.log('[ShopRenter] Syncing customers to Qdrant...')
+        let page = 0
+        let hasMore = true
+        const limit = 50
+
+        while (hasMore) {
+          const customersData = await fetchCustomers(storeId, page, limit)
+
+          if (customersData.items && customersData.items.length > 0) {
+            allCustomers.push(...customersData.items)
+
+            // Check if there are more pages
+            if (customersData.pagination && customersData.pagination.total) {
+              const totalPages = Math.ceil(customersData.pagination.total / limit)
+              hasMore = page < totalPages - 1
+            } else {
+              hasMore = customersData.items.length === limit
+            }
+
+            page++
+          } else {
+            hasMore = false
+          }
+        }
+
+        if (allCustomers.length > 0) {
+          const qdrantResult = await syncCustomersToQdrant(storeId, store.store_name, allCustomers, supabaseAdmin)
+          syncStats.customers.synced = qdrantResult.synced
+          syncStats.customers.errors = qdrantResult.errors
+          console.log(`[ShopRenter] Customers synced to Qdrant: ${qdrantResult.synced} synced, ${qdrantResult.errors} errors`)
+        }
+      } catch (error) {
+        console.error('[ShopRenter] Customer sync error:', error)
+        customerSyncError = error as Error
+        syncStats.customers.errors++
+      }
+    } else {
+      console.log('[ShopRenter] Customer sync skipped:', {
+        qdrantEnabled,
+        shouldSyncCustomers,
+        canSyncCustomers
+      })
+    }
 
 
     // Update BOTH stores table and store_sync_config to maintain consistency
     // Update BOTH stores table and store_sync_config to maintain consistency
     const syncCompletedAt = new Date().toISOString()
     const syncCompletedAt = new Date().toISOString()
 
 
-    // Check if any critical errors occurred (only products now, customer/order removed for GDPR)
-    const hasErrors = productSyncError !== null
-    const totalErrors = syncStats.products.errors
-    const totalSynced = syncStats.products.synced
+    // Check if any critical errors occurred
+    const hasErrors = productSyncError !== null || orderSyncError !== null || customerSyncError !== null
+    const totalErrors = syncStats.products.errors + syncStats.orders.errors + syncStats.customers.errors
+    const totalSynced = syncStats.products.synced + syncStats.orders.synced + syncStats.customers.synced
 
 
     // Build error message if any errors occurred
     // Build error message if any errors occurred
     let errorMessage: string | null = null
     let errorMessage: string | null = null
-    if (hasErrors && productSyncError) {
-      errorMessage = `Products: ${productSyncError.message}`
+    const errorParts: string[] = []
+    if (productSyncError) {
+      errorParts.push(`Products: ${productSyncError.message}`)
+    }
+    if (orderSyncError) {
+      errorParts.push(`Orders: ${orderSyncError.message}`)
+    }
+    if (customerSyncError) {
+      errorParts.push(`Customers: ${customerSyncError.message}`)
+    }
+    if (errorParts.length > 0) {
+      errorMessage = errorParts.join('; ')
     }
     }
 
 
     // Update stores table (for Web UI display)
     // Update stores table (for Web UI display)