瀏覽代碼

feat: implement WooCommerce data synchronization (products, orders, customers) #13

- Add database migration for WooCommerce cache tables (products, orders, customers)
- Create woocommerce-sync Edge Function with POST/GET endpoints
- Implement rate limiting (5 req/sec) and retry logic with exponential backoff
- Add pagination support for large datasets
- Update WooCommerce TypeScript interfaces with missing fields
- Follow ShopRenter sync patterns for consistency

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

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 5 月之前
父節點
當前提交
66b96d8a86

+ 7 - 0
.mcp-gogs.json

@@ -7,6 +7,13 @@
         "/home/claude/gogs-mcp/dist/index.js"
         "/home/claude/gogs-mcp/dist/index.js"
       ],
       ],
       "env": {}
       "env": {}
+    },
+    "supabase": {
+      "type": "http",
+      "url": "https://mcp.supabase.com/mcp?project_ref=ztklqodcdjeqpsvhlpud",
+      "headers": {
+        "Authorization": "Bearer sbp_893d89bb424a8095d8c504b49615189fee5c503a"
+      }
     }
     }
   }
   }
 }
 }

+ 6 - 2
CLAUDE.md

@@ -107,8 +107,12 @@ supabase functions serve
 - User management and session handling via Supabase client
 - User management and session handling via Supabase client
 
 
 **E-commerce Integrations**:
 **E-commerce Integrations**:
-- Shopify OAuth flow
-- WooCommerce OAuth flow
+- Shopify OAuth flow (not yet implemented)
+- **WooCommerce integration** (fully implemented)
+  - OAuth flow: `oauth-woocommerce` (OAuth 1.0a authentication)
+  - API client: `_shared/woocommerce-client.ts`
+  - Read-only access to products, orders, and customers
+  - Secure OAuth 1.0a with HMAC-SHA256 signatures
 - **ShopRenter integration** (fully implemented)
 - **ShopRenter integration** (fully implemented)
   - OAuth flow: `oauth-shoprenter-init`, `oauth-shoprenter-callback`
   - OAuth flow: `oauth-shoprenter-init`, `oauth-shoprenter-callback`
   - Uninstall webhook: `webhook-shoprenter-uninstall`
   - Uninstall webhook: `webhook-shoprenter-uninstall`

+ 24 - 0
supabase/functions/_shared/woocommerce-client.ts

@@ -13,6 +13,7 @@ export interface WooCommerceProduct {
   stock_status: string
   stock_status: string
   description: string
   description: string
   short_description: string
   short_description: string
+  type?: string // 'simple', 'variable', 'grouped', 'external'
   categories: Array<{ id: number; name: string }>
   categories: Array<{ id: number; name: string }>
   images: Array<{ src: string }>
   images: Array<{ src: string }>
 }
 }
@@ -31,6 +32,16 @@ export interface WooCommerceOrder {
     email: string
     email: string
     phone: string
     phone: string
   }
   }
+  shipping?: {
+    first_name: string
+    last_name: string
+    address_1: string
+    address_2: string
+    city: string
+    postcode: string
+    country: string
+    state: string
+  }
   line_items: Array<{
   line_items: Array<{
     id: number
     id: number
     name: string
     name: string
@@ -61,6 +72,19 @@ export interface WooCommerceCustomer {
     email: string
     email: string
     phone: string
     phone: string
   }
   }
+  shipping?: {
+    first_name: string
+    last_name: string
+    company: string
+    address_1: string
+    address_2: string
+    city: string
+    postcode: string
+    country: string
+    state: string
+  }
+  orders_count?: number
+  total_spent?: string
   date_created: string
   date_created: string
   date_modified: string
   date_modified: string
 }
 }

+ 502 - 0
supabase/functions/woocommerce-sync/index.ts

@@ -0,0 +1,502 @@
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
+import {
+  fetchProducts,
+  fetchOrders,
+  fetchCustomers,
+  WooCommerceProduct,
+  WooCommerceOrder,
+  WooCommerceCustomer
+} from '../_shared/woocommerce-client.ts'
+
+const corsHeaders = {
+  'Access-Control-Allow-Origin': '*',
+  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+}
+
+// Rate limiter class to prevent API throttling
+class RateLimiter {
+  private queue: Array<() => Promise<any>> = []
+  private running = 0
+  private maxConcurrent = 5
+  private delayMs = 200 // 5 requests per second
+
+  async add<T>(fn: () => Promise<T>): Promise<T> {
+    while (this.running >= this.maxConcurrent) {
+      await new Promise(resolve => setTimeout(resolve, this.delayMs))
+    }
+
+    this.running++
+    try {
+      const result = await fn()
+      await new Promise(resolve => setTimeout(resolve, this.delayMs))
+      return result
+    } finally {
+      this.running--
+    }
+  }
+}
+
+// Retry logic for API calls
+async function fetchWithRetry<T>(
+  fn: () => Promise<T>,
+  maxRetries = 3,
+  retryDelay = 1000
+): Promise<T> {
+  for (let i = 0; i < maxRetries; i++) {
+    try {
+      return await fn()
+    } catch (error: any) {
+      const isLastAttempt = i === maxRetries - 1
+
+      // Check if error is rate limiting
+      if (error.message?.includes('429') || error.message?.includes('Rate limit')) {
+        if (!isLastAttempt) {
+          const delay = retryDelay * Math.pow(2, i) // Exponential backoff
+          console.log(`[WooCommerce] Rate limited, retrying in ${delay}ms...`)
+          await new Promise(resolve => setTimeout(resolve, delay))
+          continue
+        }
+      }
+
+      // For other errors or last attempt, throw
+      if (isLastAttempt) {
+        throw error
+      }
+
+      // Retry other errors with shorter delay
+      await new Promise(resolve => setTimeout(resolve, retryDelay))
+    }
+  }
+
+  throw new Error('Max retries exceeded')
+}
+
+// Sync products from WooCommerce
+async function syncProducts(
+  storeId: string,
+  supabaseAdmin: any,
+  rateLimiter: RateLimiter
+): Promise<{ synced: number; errors: number }> {
+  console.log('[WooCommerce] Syncing products...')
+  let synced = 0
+  let errors = 0
+  let page = 1
+  const perPage = 25
+
+  try {
+    while (true) {
+      const products = await rateLimiter.add(() =>
+        fetchWithRetry(() => fetchProducts(storeId, page, perPage))
+      )
+
+      if (!products || products.length === 0) {
+        break
+      }
+
+      // Map and upsert products
+      const productsToCache = products.map((product: WooCommerceProduct) => ({
+        store_id: storeId,
+        wc_product_id: product.id.toString(),
+        name: product.name,
+        sku: product.sku || null,
+        price: parseFloat(product.price) || 0,
+        currency: 'USD', // Default, should be from store settings
+        description: product.description || null,
+        short_description: product.short_description || null,
+        stock_quantity: product.stock_quantity,
+        stock_status: product.stock_status,
+        type: product.type || 'simple',
+        categories: product.categories || [],
+        images: product.images || [],
+        raw_data: product,
+        last_synced_at: new Date().toISOString()
+      }))
+
+      const { error: upsertError } = await supabaseAdmin
+        .from('woocommerce_products_cache')
+        .upsert(productsToCache, {
+          onConflict: 'store_id,wc_product_id'
+        })
+
+      if (upsertError) {
+        console.error('[WooCommerce] Error caching products:', upsertError)
+        errors += productsToCache.length
+      } else {
+        synced += productsToCache.length
+        console.log(`[WooCommerce] Cached ${productsToCache.length} products (page ${page})`)
+      }
+
+      // Check if there are more pages
+      if (products.length < perPage) {
+        break
+      }
+
+      page++
+    }
+
+    console.log(`[WooCommerce] Products sync complete: ${synced} synced, ${errors} errors`)
+  } catch (error) {
+    console.error('[WooCommerce] Product sync error:', error)
+    errors++
+  }
+
+  return { synced, errors }
+}
+
+// Sync orders from WooCommerce
+async function syncOrders(
+  storeId: string,
+  supabaseAdmin: any,
+  rateLimiter: RateLimiter
+): Promise<{ synced: number; errors: number }> {
+  console.log('[WooCommerce] Syncing orders...')
+  let synced = 0
+  let errors = 0
+  let page = 1
+  const perPage = 25
+
+  try {
+    while (true) {
+      const orders = await rateLimiter.add(() =>
+        fetchWithRetry(() => fetchOrders(storeId, page, perPage))
+      )
+
+      if (!orders || orders.length === 0) {
+        break
+      }
+
+      // Map and upsert orders
+      const ordersToCache = orders.map((order: WooCommerceOrder) => ({
+        store_id: storeId,
+        wc_order_id: order.id.toString(),
+        order_number: order.number || order.id.toString(),
+        status: order.status,
+        total: parseFloat(order.total) || 0,
+        currency: order.currency || 'USD',
+        customer_name: `${order.billing?.first_name || ''} ${order.billing?.last_name || ''}`.trim(),
+        customer_email: order.billing?.email || null,
+        line_items: order.line_items || [],
+        billing_address: order.billing || null,
+        shipping_address: order.shipping || null,
+        created_at: order.date_created || new Date().toISOString(),
+        raw_data: order,
+        last_synced_at: new Date().toISOString()
+      }))
+
+      const { error: upsertError } = await supabaseAdmin
+        .from('woocommerce_orders_cache')
+        .upsert(ordersToCache, {
+          onConflict: 'store_id,wc_order_id'
+        })
+
+      if (upsertError) {
+        console.error('[WooCommerce] Error caching orders:', upsertError)
+        errors += ordersToCache.length
+      } else {
+        synced += ordersToCache.length
+        console.log(`[WooCommerce] Cached ${ordersToCache.length} orders (page ${page})`)
+      }
+
+      // Check if there are more pages
+      if (orders.length < perPage) {
+        break
+      }
+
+      page++
+    }
+
+    console.log(`[WooCommerce] Orders sync complete: ${synced} synced, ${errors} errors`)
+  } catch (error) {
+    console.error('[WooCommerce] Order sync error:', error)
+    errors++
+  }
+
+  return { synced, errors }
+}
+
+// Sync customers from WooCommerce
+async function syncCustomers(
+  storeId: string,
+  supabaseAdmin: any,
+  rateLimiter: RateLimiter
+): Promise<{ synced: number; errors: number }> {
+  console.log('[WooCommerce] Syncing customers...')
+  let synced = 0
+  let errors = 0
+  let page = 1
+  const perPage = 25
+
+  try {
+    while (true) {
+      const customers = await rateLimiter.add(() =>
+        fetchWithRetry(() => fetchCustomers(storeId, page, perPage))
+      )
+
+      if (!customers || customers.length === 0) {
+        break
+      }
+
+      // Map and upsert customers
+      const customersToCache = customers.map((customer: WooCommerceCustomer) => ({
+        store_id: storeId,
+        wc_customer_id: customer.id.toString(),
+        email: customer.email || null,
+        first_name: customer.first_name || null,
+        last_name: customer.last_name || null,
+        username: customer.username || null,
+        billing_address: customer.billing || null,
+        shipping_address: customer.shipping || null,
+        orders_count: customer.orders_count || 0,
+        total_spent: parseFloat(customer.total_spent || '0'),
+        raw_data: customer,
+        last_synced_at: new Date().toISOString()
+      }))
+
+      const { error: upsertError } = await supabaseAdmin
+        .from('woocommerce_customers_cache')
+        .upsert(customersToCache, {
+          onConflict: 'store_id,wc_customer_id'
+        })
+
+      if (upsertError) {
+        console.error('[WooCommerce] Error caching customers:', upsertError)
+        errors += customersToCache.length
+      } else {
+        synced += customersToCache.length
+        console.log(`[WooCommerce] Cached ${customersToCache.length} customers (page ${page})`)
+      }
+
+      // Check if there are more pages
+      if (customers.length < perPage) {
+        break
+      }
+
+      page++
+    }
+
+    console.log(`[WooCommerce] Customers sync complete: ${synced} synced, ${errors} errors`)
+  } catch (error) {
+    console.error('[WooCommerce] Customer sync error:', error)
+    errors++
+  }
+
+  return { synced, errors }
+}
+
+serve(async (req) => {
+  if (req.method === 'OPTIONS') {
+    return new Response('ok', { headers: corsHeaders })
+  }
+
+  try {
+    const supabaseUrl = Deno.env.get('SUPABASE_URL')!
+    const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY')!
+    const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+
+    const supabase = createClient(supabaseUrl, supabaseKey)
+    const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey)
+
+    // GET request - Get sync status
+    if (req.method === 'GET') {
+      // Get user from authorization header
+      const authHeader = req.headers.get('authorization')
+      if (!authHeader) {
+        return new Response(
+          JSON.stringify({ error: 'No authorization header' }),
+          { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      const token = authHeader.replace('Bearer ', '')
+      const { data: { user }, error: userError } = await supabase.auth.getUser(token)
+
+      if (userError || !user) {
+        return new Response(
+          JSON.stringify({ error: 'Invalid token' }),
+          { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Get storeId from query params
+      const url = new URL(req.url)
+      const storeId = url.searchParams.get('store_id')
+
+      if (!storeId) {
+        return new Response(
+          JSON.stringify({ error: 'store_id parameter is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store belongs to user
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id')
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+        .eq('platform_name', 'woocommerce')
+        .single()
+
+      if (storeError || !store) {
+        return new Response(
+          JSON.stringify({ error: 'Store not found or access denied' }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Get sync status using helper function
+      const { data: statusData, error: statusError } = await supabaseAdmin
+        .rpc('get_woocommerce_sync_status', { p_store_id: storeId })
+
+      if (statusError) {
+        console.error('[WooCommerce] Error fetching sync status:', statusError)
+        return new Response(
+          JSON.stringify({ error: 'Failed to fetch sync status' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      const status = statusData && statusData.length > 0 ? statusData[0] : {
+        last_sync_at: null,
+        sync_status: 'idle',
+        products_count: 0,
+        orders_count: 0,
+        customers_count: 0
+      }
+
+      return new Response(
+        JSON.stringify(status),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // POST request - Trigger sync
+    if (req.method === 'POST') {
+      // Get user from authorization header
+      const authHeader = req.headers.get('authorization')
+      if (!authHeader) {
+        return new Response(
+          JSON.stringify({ error: 'No authorization header' }),
+          { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      const token = authHeader.replace('Bearer ', '')
+      const { data: { user }, error: userError } = await supabase.auth.getUser(token)
+
+      if (userError || !user) {
+        return new Response(
+          JSON.stringify({ error: 'Invalid token' }),
+          { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Parse request body
+      const body = await req.json()
+      const { store_id, sync_type } = body
+
+      if (!store_id) {
+        return new Response(
+          JSON.stringify({ error: 'store_id is required' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      if (!sync_type || !['products', 'orders', 'customers', 'all'].includes(sync_type)) {
+        return new Response(
+          JSON.stringify({ error: 'sync_type must be one of: products, orders, customers, all' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store belongs to user
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, store_name, store_url')
+        .eq('id', store_id)
+        .eq('user_id', user.id)
+        .eq('platform_name', 'woocommerce')
+        .single()
+
+      if (storeError || !store) {
+        return new Response(
+          JSON.stringify({ error: 'Store not found or access denied' }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      console.log(`[WooCommerce] Starting ${sync_type} sync for store ${store_id}`)
+
+      const syncStats = {
+        products: { synced: 0, errors: 0 },
+        orders: { synced: 0, errors: 0 },
+        customers: { synced: 0, errors: 0 }
+      }
+
+      const rateLimiter = new RateLimiter()
+
+      // Sync based on type
+      if (sync_type === 'products' || sync_type === 'all') {
+        syncStats.products = await syncProducts(store_id, supabaseAdmin, rateLimiter)
+      }
+
+      if (sync_type === 'orders' || sync_type === 'all') {
+        syncStats.orders = await syncOrders(store_id, supabaseAdmin, rateLimiter)
+      }
+
+      if (sync_type === 'customers' || sync_type === 'all') {
+        syncStats.customers = await syncCustomers(store_id, supabaseAdmin, rateLimiter)
+      }
+
+      // Update store alt_data with sync info
+      const { data: currentStore } = await supabaseAdmin
+        .from('stores')
+        .select('alt_data')
+        .eq('id', store_id)
+        .single()
+
+      const altData = currentStore?.alt_data || {}
+
+      await supabaseAdmin
+        .from('stores')
+        .update({
+          alt_data: {
+            ...altData,
+            lastSync: new Date().toISOString(),
+            syncStatus: 'completed',
+            productsCount: syncStats.products.synced,
+            ordersCount: syncStats.orders.synced,
+            customersCount: syncStats.customers.synced,
+            lastSyncStats: syncStats
+          }
+        })
+        .eq('id', store_id)
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: `${sync_type} sync completed`,
+          stats: syncStats,
+          timestamp: new Date().toISOString()
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    return new Response(
+      JSON.stringify({ error: 'Method not allowed' }),
+      { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+
+  } catch (error) {
+    console.error('[WooCommerce] Sync endpoint error:', error)
+    return new Response(
+      JSON.stringify({
+        error: 'Sync failed',
+        details: error instanceof Error ? error.message : 'Unknown error'
+      }),
+      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+  }
+})

+ 230 - 0
supabase/migrations/20250130_woocommerce_sync.sql

@@ -0,0 +1,230 @@
+-- Migration: WooCommerce Data Synchronization
+-- Description: Creates tables for caching WooCommerce products, orders, and customers
+-- Date: 2025-01-30
+
+-- ============================================================================
+-- STEP 1: Create WooCommerce Products Cache Table
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS woocommerce_products_cache (
+  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+  store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
+  wc_product_id TEXT NOT NULL,
+  name TEXT,
+  sku TEXT,
+  price DECIMAL(10, 2),
+  currency TEXT,
+  description TEXT,
+  short_description TEXT,
+  stock_quantity INTEGER,
+  stock_status TEXT,
+  type TEXT, -- 'simple', 'variable', 'grouped', 'external'
+  categories JSONB,
+  images JSONB,
+  raw_data JSONB,
+  last_synced_at TIMESTAMPTZ DEFAULT NOW(),
+  created_at TIMESTAMPTZ DEFAULT NOW(),
+  UNIQUE(store_id, wc_product_id)
+);
+
+-- Index for querying products by store
+CREATE INDEX IF NOT EXISTS idx_wc_products_store_id
+  ON woocommerce_products_cache(store_id);
+
+-- Index for querying products by SKU
+CREATE INDEX IF NOT EXISTS idx_wc_products_sku
+  ON woocommerce_products_cache(store_id, sku);
+
+-- Index for sorting by last sync
+CREATE INDEX IF NOT EXISTS idx_wc_products_last_synced
+  ON woocommerce_products_cache(store_id, last_synced_at DESC);
+
+-- ============================================================================
+-- STEP 2: Create WooCommerce Orders Cache Table
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS woocommerce_orders_cache (
+  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+  store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
+  wc_order_id TEXT NOT NULL,
+  order_number TEXT,
+  status TEXT,
+  total DECIMAL(10, 2),
+  currency TEXT,
+  customer_name TEXT,
+  customer_email TEXT,
+  line_items JSONB,
+  billing_address JSONB,
+  shipping_address JSONB,
+  created_at TIMESTAMPTZ,
+  raw_data JSONB,
+  last_synced_at TIMESTAMPTZ DEFAULT NOW(),
+  UNIQUE(store_id, wc_order_id)
+);
+
+-- Index for querying orders by store
+CREATE INDEX IF NOT EXISTS idx_wc_orders_store_id
+  ON woocommerce_orders_cache(store_id);
+
+-- Index for querying orders by status
+CREATE INDEX IF NOT EXISTS idx_wc_orders_status
+  ON woocommerce_orders_cache(store_id, status);
+
+-- Index for querying orders by date
+CREATE INDEX IF NOT EXISTS idx_wc_orders_created_at
+  ON woocommerce_orders_cache(store_id, created_at DESC);
+
+-- Index for querying orders by customer email
+CREATE INDEX IF NOT EXISTS idx_wc_orders_customer_email
+  ON woocommerce_orders_cache(store_id, customer_email);
+
+-- ============================================================================
+-- STEP 3: Create WooCommerce Customers Cache Table
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS woocommerce_customers_cache (
+  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+  store_id UUID NOT NULL REFERENCES stores(id) ON DELETE CASCADE,
+  wc_customer_id TEXT NOT NULL,
+  email TEXT,
+  first_name TEXT,
+  last_name TEXT,
+  username TEXT,
+  billing_address JSONB,
+  shipping_address JSONB,
+  orders_count INTEGER,
+  total_spent DECIMAL(10, 2),
+  raw_data JSONB,
+  last_synced_at TIMESTAMPTZ DEFAULT NOW(),
+  created_at TIMESTAMPTZ DEFAULT NOW(),
+  UNIQUE(store_id, wc_customer_id)
+);
+
+-- Index for querying customers by store
+CREATE INDEX IF NOT EXISTS idx_wc_customers_store_id
+  ON woocommerce_customers_cache(store_id);
+
+-- Index for querying customers by email
+CREATE INDEX IF NOT EXISTS idx_wc_customers_email
+  ON woocommerce_customers_cache(store_id, email);
+
+-- Index for sorting by last sync
+CREATE INDEX IF NOT EXISTS idx_wc_customers_last_synced
+  ON woocommerce_customers_cache(store_id, last_synced_at DESC);
+
+-- ============================================================================
+-- STEP 4: Enable Row Level Security
+-- ============================================================================
+
+ALTER TABLE woocommerce_products_cache ENABLE ROW LEVEL SECURITY;
+ALTER TABLE woocommerce_orders_cache ENABLE ROW LEVEL SECURITY;
+ALTER TABLE woocommerce_customers_cache ENABLE ROW LEVEL SECURITY;
+
+-- ============================================================================
+-- STEP 5: Create RLS Policies for Products
+-- ============================================================================
+
+-- Policy: Users can view products from their own stores
+CREATE POLICY "Users can view their WooCommerce products"
+  ON woocommerce_products_cache FOR SELECT
+  TO authenticated
+  USING (
+    EXISTS (
+      SELECT 1 FROM stores s
+      WHERE s.id = woocommerce_products_cache.store_id
+      AND s.user_id = auth.uid()
+    )
+  );
+
+-- Policy: Service role can manage all products
+CREATE POLICY "Service role can manage WooCommerce products"
+  ON woocommerce_products_cache FOR ALL
+  TO service_role
+  USING (true);
+
+-- ============================================================================
+-- STEP 6: Create RLS Policies for Orders
+-- ============================================================================
+
+-- Policy: Users can view orders from their own stores
+CREATE POLICY "Users can view their WooCommerce orders"
+  ON woocommerce_orders_cache FOR SELECT
+  TO authenticated
+  USING (
+    EXISTS (
+      SELECT 1 FROM stores s
+      WHERE s.id = woocommerce_orders_cache.store_id
+      AND s.user_id = auth.uid()
+    )
+  );
+
+-- Policy: Service role can manage all orders
+CREATE POLICY "Service role can manage WooCommerce orders"
+  ON woocommerce_orders_cache FOR ALL
+  TO service_role
+  USING (true);
+
+-- ============================================================================
+-- STEP 7: Create RLS Policies for Customers
+-- ============================================================================
+
+-- Policy: Users can view customers from their own stores
+CREATE POLICY "Users can view their WooCommerce customers"
+  ON woocommerce_customers_cache FOR SELECT
+  TO authenticated
+  USING (
+    EXISTS (
+      SELECT 1 FROM stores s
+      WHERE s.id = woocommerce_customers_cache.store_id
+      AND s.user_id = auth.uid()
+    )
+  );
+
+-- Policy: Service role can manage all customers
+CREATE POLICY "Service role can manage WooCommerce customers"
+  ON woocommerce_customers_cache FOR ALL
+  TO service_role
+  USING (true);
+
+-- ============================================================================
+-- STEP 8: Create Helper Function to Get Sync Status
+-- ============================================================================
+
+CREATE OR REPLACE FUNCTION get_woocommerce_sync_status(p_store_id UUID)
+RETURNS TABLE (
+  last_sync_at TIMESTAMPTZ,
+  sync_status TEXT,
+  products_count BIGINT,
+  orders_count BIGINT,
+  customers_count BIGINT
+) AS $$
+BEGIN
+  RETURN QUERY
+  SELECT
+    GREATEST(
+      (SELECT MAX(last_synced_at) FROM woocommerce_products_cache WHERE store_id = p_store_id),
+      (SELECT MAX(last_synced_at) FROM woocommerce_orders_cache WHERE store_id = p_store_id),
+      (SELECT MAX(last_synced_at) FROM woocommerce_customers_cache WHERE store_id = p_store_id)
+    ) as last_sync_at,
+    'idle'::TEXT as sync_status,
+    (SELECT COUNT(*) FROM woocommerce_products_cache WHERE store_id = p_store_id) as products_count,
+    (SELECT COUNT(*) FROM woocommerce_orders_cache WHERE store_id = p_store_id) as orders_count,
+    (SELECT COUNT(*) FROM woocommerce_customers_cache WHERE store_id = p_store_id) as customers_count;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER;
+
+-- ============================================================================
+-- Migration Complete
+-- ============================================================================
+
+DO $$
+BEGIN
+  RAISE NOTICE 'WooCommerce sync migration completed successfully';
+  RAISE NOTICE 'Created tables:';
+  RAISE NOTICE '  - woocommerce_products_cache';
+  RAISE NOTICE '  - woocommerce_orders_cache';
+  RAISE NOTICE '  - woocommerce_customers_cache';
+  RAISE NOTICE 'Next steps:';
+  RAISE NOTICE '1. Deploy woocommerce-sync Edge Function';
+  RAISE NOTICE '2. Test sync with connected WooCommerce stores';
+END $$;