|
|
@@ -0,0 +1,270 @@
|
|
|
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
|
|
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
|
|
+
|
|
|
+const corsHeaders = {
|
|
|
+ 'Access-Control-Allow-Origin': '*',
|
|
|
+ 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Scheduled Background Sync for WooCommerce Stores
|
|
|
+ *
|
|
|
+ * This Edge Function is designed to be called by pg_cron or external schedulers.
|
|
|
+ * It requires an internal secret key for authentication instead of user tokens.
|
|
|
+ *
|
|
|
+ * Security: Uses INTERNAL_SYNC_SECRET environment variable for authentication
|
|
|
+ */
|
|
|
+
|
|
|
+serve(async (req) => {
|
|
|
+ if (req.method === 'OPTIONS') {
|
|
|
+ return new Response('ok', { headers: corsHeaders })
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Verify internal secret key for scheduled sync
|
|
|
+ const internalSecret = req.headers.get('x-internal-secret')
|
|
|
+ const expectedSecret = Deno.env.get('INTERNAL_SYNC_SECRET')
|
|
|
+
|
|
|
+ if (!expectedSecret) {
|
|
|
+ console.error('[WooCommerce Scheduled Sync] INTERNAL_SYNC_SECRET not configured')
|
|
|
+ return new Response(
|
|
|
+ JSON.stringify({ error: 'Service not configured' }),
|
|
|
+ { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!internalSecret || internalSecret !== expectedSecret) {
|
|
|
+ console.warn('[WooCommerce Scheduled Sync] Invalid or missing internal secret')
|
|
|
+ return new Response(
|
|
|
+ JSON.stringify({ error: 'Unauthorized' }),
|
|
|
+ { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ const supabaseUrl = Deno.env.get('SUPABASE_URL')!
|
|
|
+ const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
|
|
+ const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
|
|
|
+
|
|
|
+ // Get sync frequency from request (set by pg_cron job)
|
|
|
+ const body = await req.json().catch(() => ({}))
|
|
|
+ const frequency = body.frequency || 'all' // Default to all frequencies
|
|
|
+
|
|
|
+ // Build query to get WooCommerce stores that need syncing
|
|
|
+ let query = supabaseAdmin
|
|
|
+ .from('stores')
|
|
|
+ .select(`
|
|
|
+ id,
|
|
|
+ user_id,
|
|
|
+ store_name,
|
|
|
+ store_url,
|
|
|
+ alt_data,
|
|
|
+ store_sync_config (
|
|
|
+ enabled,
|
|
|
+ sync_frequency,
|
|
|
+ sync_products,
|
|
|
+ sync_orders,
|
|
|
+ sync_customers,
|
|
|
+ last_sync_at,
|
|
|
+ next_sync_at
|
|
|
+ )
|
|
|
+ `)
|
|
|
+ .eq('platform_name', 'woocommerce')
|
|
|
+
|
|
|
+ // If specific frequency requested, filter by it
|
|
|
+ if (frequency !== 'all') {
|
|
|
+ query = query.eq('store_sync_config.sync_frequency', frequency)
|
|
|
+ }
|
|
|
+
|
|
|
+ const { data: stores, error: storesError } = await query
|
|
|
+
|
|
|
+ if (storesError) {
|
|
|
+ console.error('[WooCommerce Scheduled Sync] Error fetching stores:', storesError)
|
|
|
+ return new Response(
|
|
|
+ JSON.stringify({ error: 'Failed to fetch stores', details: storesError.message }),
|
|
|
+ { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // Filter stores that have sync enabled and are due for sync
|
|
|
+ const now = new Date()
|
|
|
+ const storesToSync = stores?.filter(store => {
|
|
|
+ const config = store.store_sync_config?.[0]
|
|
|
+ if (!config || !config.enabled) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if sync is due
|
|
|
+ if (config.next_sync_at) {
|
|
|
+ const nextSync = new Date(config.next_sync_at)
|
|
|
+ return nextSync <= now
|
|
|
+ }
|
|
|
+
|
|
|
+ // If no next_sync_at, sync is due
|
|
|
+ return true
|
|
|
+ }) || []
|
|
|
+
|
|
|
+ if (storesToSync.length === 0) {
|
|
|
+ console.log('[WooCommerce Scheduled Sync] No WooCommerce stores due for sync')
|
|
|
+ return new Response(
|
|
|
+ JSON.stringify({
|
|
|
+ success: true,
|
|
|
+ message: 'No stores due for sync',
|
|
|
+ stores_processed: 0,
|
|
|
+ frequency: frequency
|
|
|
+ }),
|
|
|
+ { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`[WooCommerce Scheduled Sync] Found ${storesToSync.length} stores due for sync`)
|
|
|
+
|
|
|
+ const syncResults = []
|
|
|
+
|
|
|
+ // Sync each store by calling the woocommerce-sync function
|
|
|
+ for (const store of storesToSync) {
|
|
|
+ const storeId = store.id
|
|
|
+ const config = store.store_sync_config?.[0]
|
|
|
+ console.log(`[WooCommerce Scheduled Sync] Starting sync for store ${storeId} (${store.store_name})`)
|
|
|
+
|
|
|
+ const syncStats = {
|
|
|
+ store_id: storeId,
|
|
|
+ store_name: store.store_name,
|
|
|
+ products: { synced: 0, errors: 0 },
|
|
|
+ orders: { synced: 0, errors: 0 },
|
|
|
+ customers: { synced: 0, errors: 0 },
|
|
|
+ started_at: new Date().toISOString(),
|
|
|
+ completed_at: null as string | null,
|
|
|
+ status: 'success' as 'success' | 'partial' | 'failed',
|
|
|
+ error_message: null as string | null
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Determine what to sync based on store config
|
|
|
+ const syncTypes = []
|
|
|
+ if (config?.sync_products !== false) syncTypes.push('products')
|
|
|
+ if (config?.sync_orders !== false) syncTypes.push('orders')
|
|
|
+ if (config?.sync_customers !== false) syncTypes.push('customers')
|
|
|
+
|
|
|
+ const syncType = syncTypes.length > 0 ? 'all' : 'products'
|
|
|
+
|
|
|
+ // Call woocommerce-sync Edge Function
|
|
|
+ const syncResponse = await fetch(`${supabaseUrl}/functions/v1/woocommerce-sync`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${supabaseServiceKey}`,
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ store_id: storeId,
|
|
|
+ sync_type: syncType,
|
|
|
+ internal_call: true // Flag to bypass user auth
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
+ if (!syncResponse.ok) {
|
|
|
+ throw new Error(`Sync failed with status ${syncResponse.status}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ const syncResult = await syncResponse.json()
|
|
|
+
|
|
|
+ if (syncResult.success && syncResult.stats) {
|
|
|
+ syncStats.products = syncResult.stats.products || { synced: 0, errors: 0 }
|
|
|
+ syncStats.orders = syncResult.stats.orders || { synced: 0, errors: 0 }
|
|
|
+ syncStats.customers = syncResult.stats.customers || { synced: 0, errors: 0 }
|
|
|
+
|
|
|
+ // Determine status based on errors
|
|
|
+ const hasErrors = (
|
|
|
+ syncStats.products.errors > 0 ||
|
|
|
+ syncStats.orders.errors > 0 ||
|
|
|
+ syncStats.customers.errors > 0
|
|
|
+ )
|
|
|
+ syncStats.status = hasErrors ? 'partial' : 'success'
|
|
|
+ } else {
|
|
|
+ syncStats.status = 'failed'
|
|
|
+ syncStats.error_message = syncResult.error || 'Unknown error'
|
|
|
+ }
|
|
|
+
|
|
|
+ // Update store_sync_config timestamps
|
|
|
+ if (config) {
|
|
|
+ await supabaseAdmin
|
|
|
+ .from('store_sync_config')
|
|
|
+ .update({
|
|
|
+ last_sync_at: new Date().toISOString(),
|
|
|
+ // next_sync_at will be auto-calculated by trigger
|
|
|
+ })
|
|
|
+ .eq('store_id', storeId)
|
|
|
+ } else {
|
|
|
+ // Create sync config if it doesn't exist
|
|
|
+ await supabaseAdmin
|
|
|
+ .from('store_sync_config')
|
|
|
+ .insert({
|
|
|
+ store_id: storeId,
|
|
|
+ enabled: true,
|
|
|
+ sync_frequency: 'hourly',
|
|
|
+ last_sync_at: new Date().toISOString()
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ syncStats.completed_at = new Date().toISOString()
|
|
|
+ console.log(`[WooCommerce Scheduled Sync] Store ${storeId} sync completed with status: ${syncStats.status}`)
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error(`[WooCommerce Scheduled Sync] Fatal error syncing store ${storeId}:`, error)
|
|
|
+ syncStats.status = 'failed'
|
|
|
+ syncStats.error_message = error.message
|
|
|
+ syncStats.completed_at = new Date().toISOString()
|
|
|
+ }
|
|
|
+
|
|
|
+ syncResults.push(syncStats)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Log sync summary to database
|
|
|
+ try {
|
|
|
+ await supabaseAdmin
|
|
|
+ .from('sync_logs')
|
|
|
+ .insert({
|
|
|
+ sync_type: 'scheduled',
|
|
|
+ platform: 'woocommerce',
|
|
|
+ stores_processed: storesToSync.length,
|
|
|
+ results: syncResults,
|
|
|
+ started_at: syncResults[0]?.started_at || new Date().toISOString(),
|
|
|
+ completed_at: new Date().toISOString()
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[WooCommerce Scheduled Sync] Failed to log sync results:', error)
|
|
|
+ // Don't fail the request if logging fails
|
|
|
+ }
|
|
|
+
|
|
|
+ const successCount = syncResults.filter(r => r.status === 'success').length
|
|
|
+ const partialCount = syncResults.filter(r => r.status === 'partial').length
|
|
|
+ const failedCount = syncResults.filter(r => r.status === 'failed').length
|
|
|
+
|
|
|
+ console.log(`[WooCommerce Scheduled Sync] Batch complete: ${successCount} success, ${partialCount} partial, ${failedCount} failed`)
|
|
|
+
|
|
|
+ return new Response(
|
|
|
+ JSON.stringify({
|
|
|
+ success: true,
|
|
|
+ message: 'Scheduled sync completed',
|
|
|
+ summary: {
|
|
|
+ stores_processed: storesToSync.length,
|
|
|
+ success: successCount,
|
|
|
+ partial: partialCount,
|
|
|
+ failed: failedCount
|
|
|
+ },
|
|
|
+ results: syncResults,
|
|
|
+ timestamp: new Date().toISOString()
|
|
|
+ }),
|
|
|
+ { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
+ )
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[WooCommerce Scheduled Sync] Fatal error:', error)
|
|
|
+ return new Response(
|
|
|
+ JSON.stringify({
|
|
|
+ error: 'Scheduled sync failed',
|
|
|
+ details: error.message
|
|
|
+ }),
|
|
|
+ { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
|
+ )
|
|
|
+ }
|
|
|
+})
|