|
@@ -1,8 +1,79 @@
|
|
|
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'
|
|
|
|
|
+import { cleanHtmlContent } from '../_shared/html-cleaner.ts'
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Extract category ID from ShopRenter category href URL
|
|
|
|
|
+ * Example: "http://shopname.api.myshoprenter.hu/categories/Y2F0ZWdvcnktY2F0ZWdvcnlfaWQ9NTU=" -> "Y2F0ZWdvcnktY2F0ZWdvcnlfaWQ9NTU="
|
|
|
|
|
+ */
|
|
|
|
|
+function extractCategoryId(categoryHref: string): string | null {
|
|
|
|
|
+ if (!categoryHref) return null
|
|
|
|
|
+ const parts = categoryHref.split('/')
|
|
|
|
|
+ return parts[parts.length - 1] || null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Extract numeric category ID from base64-encoded category ID
|
|
|
|
|
+ * Example: "Y2F0ZWdvcnktY2F0ZWdvcnlfaWQ9MjA2" -> "206"
|
|
|
|
|
+ */
|
|
|
|
|
+function extractNumericCategoryId(base64CategoryId: string): string | null {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const decoded = atob(base64CategoryId)
|
|
|
|
|
+ // Format: "category-category_id=206"
|
|
|
|
|
+ const match = decoded.match(/category_id=(\d+)/)
|
|
|
|
|
+ return match ? match[1] : null
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to decode category ID:', base64CategoryId, error)
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Fetch and process category details from ShopRenter API
|
|
|
|
|
+ * Returns category name (HTML cleaned)
|
|
|
|
|
+ */
|
|
|
|
|
+async function fetchCategoryName(storeId: string, categoryId: string): Promise<string | null> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const categoryData = await fetchCategory(storeId, categoryId)
|
|
|
|
|
+
|
|
|
|
|
+ // Extract first language description (Hungarian is typically first)
|
|
|
|
|
+ const categoryDesc = categoryData.categoryDescriptions?.[0]
|
|
|
|
|
+ if (!categoryDesc) return null
|
|
|
|
|
+
|
|
|
|
|
+ const name = categoryDesc.name || ''
|
|
|
|
|
+ return name || null
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`[ShopRenter] Failed to fetch category ${categoryId}:`, error)
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Extract categories from ShopRenter product and return array with ID and name
|
|
|
|
|
+ * Returns: [{ id: "206", name: "Category Name" }, ...]
|
|
|
|
|
+ */
|
|
|
|
|
+function extractProductCategories(product: any, categoryCache: Map<string, string>): Array<{ id: string; name: string }> {
|
|
|
|
|
+ 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 numericId = extractNumericCategoryId(categoryId)
|
|
|
|
|
+ if (numericId && categoryCache.has(categoryId)) {
|
|
|
|
|
+ categories.push({
|
|
|
|
|
+ id: numericId,
|
|
|
|
|
+ name: categoryCache.get(categoryId)!
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return categories
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
const corsHeaders = {
|
|
const corsHeaders = {
|
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Origin': '*',
|
|
@@ -173,37 +244,19 @@ serve(wrapHandler('shoprenter-scheduled-sync', async (req) => {
|
|
|
if (productsPolicy === 'sync') {
|
|
if (productsPolicy === 'sync') {
|
|
|
try {
|
|
try {
|
|
|
console.log(`[ShopRenter Scheduled Sync] Syncing products for store ${storeId}`)
|
|
console.log(`[ShopRenter Scheduled Sync] Syncing products for store ${storeId}`)
|
|
|
|
|
+
|
|
|
|
|
+ // First, fetch all products to collect category IDs
|
|
|
let page = 0 // ShopRenter API uses zero-based pagination
|
|
let page = 0 // ShopRenter API uses zero-based pagination
|
|
|
let hasMore = true
|
|
let hasMore = true
|
|
|
const limit = 50
|
|
const limit = 50
|
|
|
|
|
+ const allProducts: any[] = []
|
|
|
|
|
|
|
|
|
|
+ // Phase 1: Fetch all products
|
|
|
while (hasMore) {
|
|
while (hasMore) {
|
|
|
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()
|
|
|
|
|
- }))
|
|
|
|
|
-
|
|
|
|
|
- const { error: upsertError } = await supabaseAdmin
|
|
|
|
|
- .from('shoprenter_products_cache')
|
|
|
|
|
- .upsert(productsToCache, {
|
|
|
|
|
- onConflict: 'store_id,product_id'
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- if (upsertError) {
|
|
|
|
|
- console.error(`[ShopRenter Scheduled Sync] Error caching products for store ${storeId}:`, upsertError)
|
|
|
|
|
- syncStats.products.errors += productsToCache.length
|
|
|
|
|
- } else {
|
|
|
|
|
- syncStats.products.synced += productsToCache.length
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ allProducts.push(...productsData.items)
|
|
|
|
|
|
|
|
// Check if there are more pages
|
|
// Check if there are more pages
|
|
|
if (productsData.pagination && productsData.pagination.total) {
|
|
if (productsData.pagination && productsData.pagination.total) {
|
|
@@ -219,6 +272,80 @@ serve(wrapHandler('shoprenter-scheduled-sync', async (req) => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ console.log(`[ShopRenter Scheduled Sync] Store ${storeId}: Fetched ${allProducts.length} products, building category cache...`)
|
|
|
|
|
+
|
|
|
|
|
+ // Phase 2: Build category cache from all products
|
|
|
|
|
+ const categoryCache = new Map<string, string>()
|
|
|
|
|
+ const uniqueCategoryIds = new Set<string>()
|
|
|
|
|
+
|
|
|
|
|
+ for (const product of allProducts) {
|
|
|
|
|
+ const categoryRelations = product.productCategoryRelations || []
|
|
|
|
|
+ for (const rel of categoryRelations) {
|
|
|
|
|
+ const categoryId = extractCategoryId(rel.category?.href)
|
|
|
|
|
+ if (categoryId && !uniqueCategoryIds.has(categoryId)) {
|
|
|
|
|
+ uniqueCategoryIds.add(categoryId)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`[ShopRenter Scheduled Sync] Store ${storeId}: Found ${uniqueCategoryIds.size} unique categories, fetching names...`)
|
|
|
|
|
+
|
|
|
|
|
+ // Fetch category names (with concurrency limit to avoid rate limiting)
|
|
|
|
|
+ const categoryIds = Array.from(uniqueCategoryIds)
|
|
|
|
|
+ const batchSize = 5 // Fetch 5 categories at a time
|
|
|
|
|
+ for (let i = 0; i < categoryIds.length; i += batchSize) {
|
|
|
|
|
+ const batch = categoryIds.slice(i, i + batchSize)
|
|
|
|
|
+ await Promise.all(
|
|
|
|
|
+ batch.map(async (categoryId) => {
|
|
|
|
|
+ const categoryName = await fetchCategoryName(storeId, categoryId)
|
|
|
|
|
+ if (categoryName) {
|
|
|
|
|
+ categoryCache.set(categoryId, categoryName)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`[ShopRenter Scheduled Sync] Store ${storeId}: Category cache built with ${categoryCache.size} categories`)
|
|
|
|
|
+
|
|
|
|
|
+ // Phase 3: Cache products with proper names and categories
|
|
|
|
|
+ const productsToCache = allProducts.map((product: any) => {
|
|
|
|
|
+ // Extract product name from productDescriptions (localized name)
|
|
|
|
|
+ const productDesc = product.productDescriptions?.[0] || {}
|
|
|
|
|
+ const productName = productDesc.name || product.name || null
|
|
|
|
|
+
|
|
|
|
|
+ // Extract categories using the category cache
|
|
|
|
|
+ const categories = extractProductCategories(product, categoryCache)
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ store_id: storeId,
|
|
|
|
|
+ product_id: product.id,
|
|
|
|
|
+ inner_id: product.innerId,
|
|
|
|
|
+ name: productName,
|
|
|
|
|
+ sku: product.sku,
|
|
|
|
|
+ categories: categories,
|
|
|
|
|
+ excluded: false, // Will be set if in exclusion list
|
|
|
|
|
+ last_synced_at: new Date().toISOString()
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // Batch upsert products (in chunks to avoid large payload issues)
|
|
|
|
|
+ const upsertBatchSize = 100
|
|
|
|
|
+ for (let i = 0; i < productsToCache.length; i += upsertBatchSize) {
|
|
|
|
|
+ const batch = productsToCache.slice(i, i + upsertBatchSize)
|
|
|
|
|
+ const { error: upsertError } = await supabaseAdmin
|
|
|
|
|
+ .from('shoprenter_products_cache')
|
|
|
|
|
+ .upsert(batch, {
|
|
|
|
|
+ onConflict: 'store_id,product_id'
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ if (upsertError) {
|
|
|
|
|
+ console.error(`[ShopRenter Scheduled Sync] Error caching products batch for store ${storeId}:`, upsertError)
|
|
|
|
|
+ syncStats.products.errors += batch.length
|
|
|
|
|
+ } else {
|
|
|
|
|
+ syncStats.products.synced += batch.length
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
console.log(`[ShopRenter Scheduled Sync] Store ${storeId}: Products synced: ${syncStats.products.synced}, errors: ${syncStats.products.errors}`)
|
|
console.log(`[ShopRenter Scheduled Sync] Store ${storeId}: Products synced: ${syncStats.products.synced}, errors: ${syncStats.products.errors}`)
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error(`[ShopRenter Scheduled Sync] Product sync error for store ${storeId}:`, error)
|
|
console.error(`[ShopRenter Scheduled Sync] Product sync error for store ${storeId}:`, error)
|