Browse Source

feat: store category ID and name in shoprenter_products_cache #112

- Updated shoprenter-sync to fetch and store category names alongside IDs
- Updated shoprenter-scheduled-sync with same category structure
- Categories now stored as [{id: '206', name: 'Category Name'}, ...] format
- Added parallel category name fetching for better performance
- Reuses fetchCategory() API that was already working for Qdrant sync

This enables category filtering and exclusion features in the WebUI.
Claude 4 months ago
parent
commit
0d1a4ff258

+ 70 - 11
supabase/functions/shoprenter-scheduled-sync/index.ts

@@ -1,9 +1,16 @@
 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, fetchOrders, fetchCustomers } from '../_shared/shoprenter-client.ts'
+import { fetchProducts, fetchOrders, fetchCustomers, fetchCategory } from '../_shared/shoprenter-client.ts'
 import { detectCountryCode } from '../_shared/phone-formatter.ts'
 import { detectCountryCode } from '../_shared/phone-formatter.ts'
 
 
+// Helper function to extract category ID from ShopRenter href
+function extractCategoryId(categoryHref: string): string | null {
+  if (!categoryHref) return null
+  const parts = categoryHref.split('/')
+  return parts[parts.length - 1] || null
+}
+
 const corsHeaders = {
 const corsHeaders = {
   'Access-Control-Allow-Origin': '*',
   'Access-Control-Allow-Origin': '*',
   'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
   'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
@@ -181,16 +188,68 @@ serve(wrapHandler('shoprenter-scheduled-sync', async (req) => {
               const productsData = await fetchProducts(storeId, page, limit)
               const productsData = await fetchProducts(storeId, page, limit)
 
 
               if (productsData.items && productsData.items.length > 0) {
               if (productsData.items && productsData.items.length > 0) {
-                const productsToCache = productsData.items.map((product: any) => ({
-                  store_id: storeId,
-                  product_id: product.id,
-                  inner_id: product.innerId,
-                  name: product.name,
-                  sku: product.sku,
-                  categories: product.productCategory || [],
-                  excluded: false, // Will be set if in exclusion list
-                  last_synced_at: new Date().toISOString()
-                }))
+                // First, collect unique category IDs for this page
+                const pageCategoryIds = new Set<string>()
+                for (const product of productsData.items) {
+                  const categoryRelations = product.productCategoryRelations || []
+                  for (const rel of categoryRelations) {
+                    const categoryId = extractCategoryId(rel.category?.href)
+                    if (categoryId) {
+                      pageCategoryIds.add(categoryId)
+                    }
+                  }
+                }
+
+                // Fetch category names for this page (parallel fetching)
+                const pageCategoryCache = new Map<string, string>()
+                if (pageCategoryIds.size > 0) {
+                  console.log(`[ShopRenter Scheduled Sync] Fetching ${pageCategoryIds.size} category names for store ${storeId}...`)
+                  const categoryPromises = Array.from(pageCategoryIds).map(async (categoryId) => {
+                    try {
+                      const categoryData = await fetchCategory(storeId, categoryId)
+                      const categoryDesc = categoryData.categoryDescriptions?.[0]
+                      if (categoryDesc && categoryDesc.name) {
+                        pageCategoryCache.set(categoryId, categoryDesc.name)
+                      }
+                    } catch (error) {
+                      console.error(`[ShopRenter Scheduled Sync] Failed to fetch category ${categoryId}:`, error)
+                    }
+                  })
+                  await Promise.all(categoryPromises)
+                  console.log(`[ShopRenter Scheduled Sync] Fetched ${pageCategoryCache.size} category names successfully`)
+                }
+
+                const productsToCache = productsData.items.map((product: any) => {
+                  // Extract first language description from productDescriptions array
+                  const productDesc = product.productDescriptions?.[0] || {}
+
+                  // Extract categories with both ID and name
+                  const categories: Array<{id: string, name: string}> = []
+                  const categoryRelations = product.productCategoryRelations || []
+                  for (const rel of categoryRelations) {
+                    const categoryId = extractCategoryId(rel.category?.href)
+                    if (categoryId) {
+                      const categoryName = pageCategoryCache.get(categoryId)
+                      if (categoryName) {
+                        categories.push({ id: categoryId, name: categoryName })
+                      } else {
+                        // Fallback: store ID only if name fetch failed
+                        categories.push({ id: categoryId, name: categoryId })
+                      }
+                    }
+                  }
+
+                  return {
+                    store_id: storeId,
+                    product_id: product.id,
+                    inner_id: product.innerId,
+                    name: productDesc.name || product.name || null,
+                    sku: product.sku || null,
+                    categories: categories,
+                    excluded: false, // Will be set if in exclusion list
+                    last_synced_at: new Date().toISOString()
+                  }
+                })
 
 
                 const { error: upsertError } = await supabaseAdmin
                 const { error: upsertError } = await supabaseAdmin
                   .from('shoprenter_products_cache')
                   .from('shoprenter_products_cache')

+ 41 - 4
supabase/functions/shoprenter-sync/index.ts

@@ -941,17 +941,54 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
             // Collect all products for Qdrant sync
             // Collect all products for Qdrant sync
             allProducts.push(...productsData.items)
             allProducts.push(...productsData.items)
 
 
+            // First, collect unique category IDs for this page
+            const pageCategoryIds = new Set<string>()
+            for (const product of productsData.items) {
+              const categoryRelations = product.productCategoryRelations || []
+              for (const rel of categoryRelations) {
+                const categoryId = extractCategoryId(rel.category?.href)
+                if (categoryId) {
+                  pageCategoryIds.add(categoryId)
+                }
+              }
+            }
+
+            // Fetch category names for this page (parallel fetching)
+            const pageCategoryCache = new Map<string, string>()
+            if (pageCategoryIds.size > 0) {
+              console.log(`[ShopRenter] Fetching ${pageCategoryIds.size} category names...`)
+              const categoryPromises = Array.from(pageCategoryIds).map(async (categoryId) => {
+                try {
+                  const categoryData = await fetchCategory(storeId, categoryId)
+                  const categoryDesc = categoryData.categoryDescriptions?.[0]
+                  if (categoryDesc && categoryDesc.name) {
+                    pageCategoryCache.set(categoryId, categoryDesc.name)
+                  }
+                } catch (error) {
+                  console.error(`[ShopRenter] Failed to fetch category ${categoryId}:`, error)
+                }
+              })
+              await Promise.all(categoryPromises)
+              console.log(`[ShopRenter] Fetched ${pageCategoryCache.size} category names successfully`)
+            }
+
             const productsToCache = productsData.items.map((product: any) => {
             const productsToCache = productsData.items.map((product: any) => {
               // Extract first language description from productDescriptions array
               // Extract first language description from productDescriptions array
               const productDesc = product.productDescriptions?.[0] || {}
               const productDesc = product.productDescriptions?.[0] || {}
 
 
-              // Extract category IDs from productCategoryRelations
-              const categoryIds: string[] = []
+              // Extract categories with both ID and name
+              const categories: Array<{id: string, name: string}> = []
               const categoryRelations = product.productCategoryRelations || []
               const categoryRelations = product.productCategoryRelations || []
               for (const rel of categoryRelations) {
               for (const rel of categoryRelations) {
                 const categoryId = extractCategoryId(rel.category?.href)
                 const categoryId = extractCategoryId(rel.category?.href)
                 if (categoryId) {
                 if (categoryId) {
-                  categoryIds.push(categoryId)
+                  const categoryName = pageCategoryCache.get(categoryId)
+                  if (categoryName) {
+                    categories.push({ id: categoryId, name: categoryName })
+                  } else {
+                    // Fallback: store ID only if name fetch failed
+                    categories.push({ id: categoryId, name: categoryId })
+                  }
                 }
                 }
               }
               }
 
 
@@ -961,7 +998,7 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
                 inner_id: product.innerId,
                 inner_id: product.innerId,
                 name: productDesc.name || product.name || null,
                 name: productDesc.name || product.name || null,
                 sku: product.sku || null,
                 sku: product.sku || null,
-                categories: categoryIds,
+                categories: categories,
                 excluded: false, // Will be set if in exclusion list
                 excluded: false, // Will be set if in exclusion list
                 last_synced_at: new Date().toISOString()
                 last_synced_at: new Date().toISOString()
               }
               }