Browse Source

feat: implement comprehensive exclusion logic in all e-commerce sync functions

- Add proper exclusion status computation using database function is_product_excluded_by_category()
- Check individual product exclusions from existing cache records
- Filter excluded products from Qdrant vector database sync
- Delete excluded products from Qdrant using deletePointsByFilter with correct collection naming
- Implement across all three platforms: ShopRenter, WooCommerce, and Shopify
- Ensure excluded products are removed from AI context while maintaining cache consistency
- Add comprehensive logging for excluded vs synced product counts

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

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 4 months ago
parent
commit
69387048bc

+ 86 - 23
supabase/functions/shopify-sync/index.ts

@@ -555,29 +555,60 @@ async function syncProducts(
 
     const currency = store?.alt_data?.currency || 'USD'
 
-    // Map and upsert products to SQL cache
-    const productsToCache = products.map((product: ShopifyProduct) => {
-      const primaryVariant = product.variants?.[0]
+    // Map and upsert products to SQL cache with exclusion status
+    const productsToCache = await Promise.all(
+      products.map(async (product: ShopifyProduct) => {
+        const primaryVariant = product.variants?.[0]
+
+        // Build categories array from product_type and vendor
+        const categories: any[] = []
+        if (product.product_type) {
+          categories.push({
+            id: product.product_type.toLowerCase().replace(/\s+/g, '-'),
+            name: product.product_type
+          })
+        }
 
-      // Build categories array from product_type and vendor
-      const categories: any[] = []
-      if (product.product_type) {
-        categories.push({
-          id: product.product_type.toLowerCase().replace(/\s+/g, '-'),
-          name: product.product_type
-        })
-      }
+        // Check if product is excluded by category using database function
+        const { data: categoryExcluded, error: categoryExclusionError } = await supabaseAdmin
+          .rpc('is_product_excluded_by_category', {
+            p_store_id: storeId,
+            p_categories: categories,
+            p_platform: 'shopify'
+          })
 
-      return {
-        store_id: storeId,
-        product_id: product.id.toString(),
-        name: product.title, // Map title to name for consistency
-        sku: primaryVariant?.sku || null,
-        categories: categories,
-        excluded: false, // Will be set if in exclusion list
-        last_synced_at: new Date().toISOString()
-      }
-    })
+        if (categoryExclusionError) {
+          console.error(`[Shopify] Error checking category exclusion for product ${product.id}:`, categoryExclusionError)
+        }
+
+        // Check if product is individually excluded from cache
+        const { data: existingProduct, error: existingError } = await supabaseAdmin
+          .from('shopify_products_cache')
+          .select('excluded')
+          .eq('store_id', storeId)
+          .eq('product_id', product.id.toString())
+          .single()
+
+        if (existingError && existingError.code !== 'PGRST116') {
+          console.error(`[Shopify] Error checking existing product exclusion for ${product.id}:`, existingError)
+        }
+
+        // Product is excluded if:
+        // 1. Excluded by category, OR
+        // 2. Individually excluded (from existing cache record)
+        const isExcluded = categoryExcluded || (existingProduct?.excluded === true)
+
+        return {
+          store_id: storeId,
+          product_id: product.id.toString(),
+          name: product.title, // Map title to name for consistency
+          sku: primaryVariant?.sku || null,
+          categories: categories,
+          excluded: isExcluded,
+          last_synced_at: new Date().toISOString()
+        }
+      })
+    )
 
     // Batch upsert in chunks of 100 to avoid payload size limits
     const chunkSize = 100
@@ -601,10 +632,42 @@ async function syncProducts(
 
     console.log(`[Shopify] Products sync complete: ${synced} synced, ${errors} errors`)
 
-    // Sync to Qdrant if enabled
+    // Handle Qdrant sync with exclusion logic
     let qdrantResult
     if (qdrantEnabled) {
-      qdrantResult = await syncProductsToQdrant(storeId, storeName, products, supabaseAdmin)
+      // Get excluded product IDs for Qdrant cleanup
+      const excludedProductIds = productsToCache
+        .filter(p => p.excluded)
+        .map(p => p.product_id)
+
+      // Filter products for Qdrant sync (only non-excluded)
+      const productsForQdrant = products.filter(product => {
+        const productCacheItem = productsToCache.find(p => p.product_id === product.id.toString())
+        return productCacheItem && !productCacheItem.excluded
+      })
+
+      // Delete excluded products from Qdrant
+      if (excludedProductIds.length > 0) {
+        console.log(`[Shopify] Deleting ${excludedProductIds.length} excluded products from Qdrant...`)
+        try {
+          const { deletePointsByFilter, getCollectionName } = await import('../_shared/qdrant-client.ts')
+          const collectionName = getCollectionName(storeName, 'products')
+          await deletePointsByFilter(
+            collectionName,
+            {
+              key: 'product_id',
+              match: { any: excludedProductIds }
+            }
+          )
+          console.log(`[Shopify] Deleted excluded products from Qdrant successfully`)
+        } catch (qdrantDeleteError) {
+          console.error('[Shopify] Error deleting excluded products from Qdrant:', qdrantDeleteError)
+        }
+      }
+
+      // Sync remaining (non-excluded) products to Qdrant
+      qdrantResult = await syncProductsToQdrant(storeId, storeName, productsForQdrant, supabaseAdmin)
+      console.log(`[Shopify] Qdrant sync complete: ${qdrantResult.synced} synced, ${qdrantResult.errors} errors, ${excludedProductIds.length} excluded`)
     }
 
     return { synced, errors, qdrant: qdrantResult }

+ 65 - 5
supabase/functions/shoprenter-sync/index.ts

@@ -1029,7 +1029,7 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
         }
         console.log(`[ShopRenter] Fetched ${categoryCache.size} categories successfully`)
 
-        // Now process all products and cache them with proper category data
+        // Now process all products and cache them with proper category data and exclusion status
         const productsToCache = await Promise.all(
           allProductsForCache.map(async (product: any) => {
             const categories = await extractProductCategories(product, categoryCache)
@@ -1037,6 +1037,35 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
             const productDesc = product.productDescriptions?.[0] || {}
             const productName = productDesc.name || product.name || null
 
+            // Check if product is excluded by category using database function
+            const { data: categoryExcluded, error: categoryExclusionError } = await supabaseAdmin
+              .rpc('is_product_excluded_by_category', {
+                p_store_id: storeId,
+                p_categories: categories,
+                p_platform: 'shoprenter'
+              })
+
+            if (categoryExclusionError) {
+              console.error(`[ShopRenter] Error checking category exclusion for product ${product.id}:`, categoryExclusionError)
+            }
+
+            // Check if product is individually excluded from cache
+            const { data: existingProduct, error: existingError } = await supabaseAdmin
+              .from('shoprenter_products_cache')
+              .select('excluded')
+              .eq('store_id', storeId)
+              .eq('product_id', product.id)
+              .single()
+
+            if (existingError && existingError.code !== 'PGRST116') {
+              console.error(`[ShopRenter] Error checking existing product exclusion for ${product.id}:`, existingError)
+            }
+
+            // Product is excluded if:
+            // 1. Excluded by category, OR
+            // 2. Individually excluded (from existing cache record)
+            const isExcluded = categoryExcluded || (existingProduct?.excluded === true)
+
             return {
               store_id: storeId,
               product_id: product.id,
@@ -1044,7 +1073,7 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
               name: productName,
               sku: product.sku,
               categories: categories,  // Now stores [{ id: "206", name: "Category Name" }]
-              excluded: false, // Will be set if in exclusion list
+              excluded: isExcluded,
               last_synced_at: new Date().toISOString()
             }
           })
@@ -1068,10 +1097,41 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
 
         console.log(`[ShopRenter] Products synced: ${syncStats.products.synced}`)
 
-        // Sync to Qdrant if enabled
+        // Handle Qdrant sync with exclusion logic
         if (qdrantEnabled && allProducts.length > 0) {
-          const qdrantResult = await syncProductsToQdrant(storeId, store.store_name, allProducts, supabaseAdmin)
-          console.log(`[ShopRenter] Qdrant sync complete: ${qdrantResult.synced} synced, ${qdrantResult.errors} errors`)
+          // Get excluded product IDs for Qdrant cleanup
+          const excludedProductIds = productsToCache
+            .filter(p => p.excluded)
+            .map(p => p.product_id)
+
+          // Filter products for Qdrant sync (only non-excluded)
+          const productsForQdrant = allProducts.filter(product => {
+            const productCacheItem = productsToCache.find(p => p.product_id === product.id)
+            return productCacheItem && !productCacheItem.excluded
+          })
+
+          // Delete excluded products from Qdrant
+          if (excludedProductIds.length > 0) {
+            console.log(`[ShopRenter] Deleting ${excludedProductIds.length} excluded products from Qdrant...`)
+            try {
+              const { deletePointsByFilter, getCollectionName } = await import('../_shared/qdrant-client.ts')
+              const collectionName = getCollectionName(store.store_name, 'products')
+              await deletePointsByFilter(
+                collectionName,
+                {
+                  key: 'product_id',
+                  match: { any: excludedProductIds }
+                }
+              )
+              console.log(`[ShopRenter] Deleted excluded products from Qdrant successfully`)
+            } catch (qdrantDeleteError) {
+              console.error('[ShopRenter] Error deleting excluded products from Qdrant:', qdrantDeleteError)
+            }
+          }
+
+          // Sync remaining (non-excluded) products to Qdrant
+          const qdrantResult = await syncProductsToQdrant(storeId, store.store_name, productsForQdrant, supabaseAdmin)
+          console.log(`[ShopRenter] Qdrant sync complete: ${qdrantResult.synced} synced, ${qdrantResult.errors} errors, ${excludedProductIds.length} excluded`)
         }
       }
     } catch (error) {

+ 81 - 12
supabase/functions/woocommerce-sync/index.ts

@@ -571,6 +571,7 @@ async function syncProducts(
   let page = 1
   const perPage = 25
   const allProducts: WooCommerceProduct[] = []
+  const allProductsToCache: any[] = []
 
   if (!canSyncProducts) {
     console.log('[WooCommerce] Product sync disabled by store permissions')
@@ -590,16 +591,52 @@ async function syncProducts(
       // Collect all products for Qdrant sync
       allProducts.push(...products)
 
-      // Map and upsert products to SQL cache (minimal data for sync tracking only)
-      const productsToCache = products.map((product: WooCommerceProduct) => ({
-        store_id: storeId,
-        product_id: product.id.toString(),
-        name: product.name,
-        sku: product.sku || null,
-        categories: product.categories || [],
-        excluded: false, // Will be set if in exclusion list
-        last_synced_at: new Date().toISOString()
-      }))
+      // Map and upsert products to SQL cache with exclusion status
+      const productsToCache = await Promise.all(
+        products.map(async (product: WooCommerceProduct) => {
+          // Check if product is excluded by category using database function
+          const { data: categoryExcluded, error: categoryExclusionError } = await supabaseAdmin
+            .rpc('is_product_excluded_by_category', {
+              p_store_id: storeId,
+              p_categories: product.categories || [],
+              p_platform: 'woocommerce'
+            })
+
+          if (categoryExclusionError) {
+            console.error(`[WooCommerce] Error checking category exclusion for product ${product.id}:`, categoryExclusionError)
+          }
+
+          // Check if product is individually excluded from cache
+          const { data: existingProduct, error: existingError } = await supabaseAdmin
+            .from('woocommerce_products_cache')
+            .select('excluded')
+            .eq('store_id', storeId)
+            .eq('product_id', product.id.toString())
+            .single()
+
+          if (existingError && existingError.code !== 'PGRST116') {
+            console.error(`[WooCommerce] Error checking existing product exclusion for ${product.id}:`, existingError)
+          }
+
+          // Product is excluded if:
+          // 1. Excluded by category, OR
+          // 2. Individually excluded (from existing cache record)
+          const isExcluded = categoryExcluded || (existingProduct?.excluded === true)
+
+          return {
+            store_id: storeId,
+            product_id: product.id.toString(),
+            name: product.name,
+            sku: product.sku || null,
+            categories: product.categories || [],
+            excluded: isExcluded,
+            last_synced_at: new Date().toISOString()
+          }
+        })
+      )
+
+      // Collect products for exclusion tracking
+      allProductsToCache.push(...productsToCache)
 
       const { error: upsertError } = await supabaseAdmin
         .from('woocommerce_products_cache')
@@ -625,10 +662,42 @@ async function syncProducts(
 
     console.log(`[WooCommerce] Products sync complete: ${synced} synced, ${errors} errors`)
 
-    // Sync to Qdrant if enabled
+    // Handle Qdrant sync with exclusion logic
     let qdrantResult
     if (qdrantEnabled && allProducts.length > 0) {
-      qdrantResult = await syncProductsToQdrant(storeId, storeName, allProducts, supabaseAdmin)
+      // Get excluded product IDs for Qdrant cleanup
+      const excludedProductIds = allProductsToCache
+        .filter(p => p.excluded)
+        .map(p => p.product_id)
+
+      // Filter products for Qdrant sync (only non-excluded)
+      const productsForQdrant = allProducts.filter(product => {
+        const productCacheItem = allProductsToCache.find(p => p.product_id === product.id.toString())
+        return productCacheItem && !productCacheItem.excluded
+      })
+
+      // Delete excluded products from Qdrant
+      if (excludedProductIds.length > 0) {
+        console.log(`[WooCommerce] Deleting ${excludedProductIds.length} excluded products from Qdrant...`)
+        try {
+          const { deletePointsByFilter, getCollectionName } = await import('../_shared/qdrant-client.ts')
+          const collectionName = getCollectionName(storeName, 'products')
+          await deletePointsByFilter(
+            collectionName,
+            {
+              key: 'product_id',
+              match: { any: excludedProductIds }
+            }
+          )
+          console.log(`[WooCommerce] Deleted excluded products from Qdrant successfully`)
+        } catch (qdrantDeleteError) {
+          console.error('[WooCommerce] Error deleting excluded products from Qdrant:', qdrantDeleteError)
+        }
+      }
+
+      // Sync remaining (non-excluded) products to Qdrant
+      qdrantResult = await syncProductsToQdrant(storeId, storeName, productsForQdrant, supabaseAdmin)
+      console.log(`[WooCommerce] Qdrant sync complete: ${qdrantResult.synced} synced, ${qdrantResult.errors} errors, ${excludedProductIds.length} excluded`)
     }
 
     return { synced, errors, qdrant: qdrantResult }