Browse Source

feat: remove deprecated sync_* boolean columns from store_sync_config #95

Completed cleanup of deprecated boolean columns (sync_products, sync_orders, sync_customers)
in favor of the new GDPR-compliant enum columns (products_access_policy, customers_access_policy, orders_access_policy).

Changes:
- Updated all Edge Functions to use only new policy columns
- Removed old boolean column references from API queries
- Fixed shoprenter-sync and shoprenter-scheduled-sync to use new policies
- Updated frontend TypeScript interface in IntegrationsContent.tsx
- Created and applied migration to drop deprecated columns from database
- Updated documentation (CLAUDE.md, QDRANT_INTEGRATION.md, DEPLOYMENT_GUIDE.md)

All sync operations now fully respect the 3-state GDPR access policy (sync, api_only, not_allowed).
Backend and database are now fully migrated to the new system.

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

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 5 months ago
parent
commit
52daa6d185

+ 3 - 3
CLAUDE.md

@@ -528,9 +528,9 @@ cd /home/claude/shopcall/supabase
 - sync_frequency: text ('15min', '30min', 'hourly', '6hours', 'daily')
 - sync_frequency: text ('15min', '30min', 'hourly', '6hours', 'daily')
 - last_sync_at: timestamptz
 - last_sync_at: timestamptz
 - next_sync_at: timestamptz (auto-calculated)
 - next_sync_at: timestamptz (auto-calculated)
-- sync_products: boolean (default: true)
-- sync_orders: boolean (default: true)
-- sync_customers: boolean (default: true)
+- products_access_policy: data_access_policy enum ('sync', 'api_only', 'not_allowed') - GDPR-compliant access control
+- customers_access_policy: data_access_policy enum ('sync', 'api_only', 'not_allowed') - GDPR-compliant access control
+- orders_access_policy: data_access_policy enum ('sync', 'api_only', 'not_allowed') - GDPR-compliant access control
 - created_at: timestamptz
 - created_at: timestamptz
 - updated_at: timestamptz
 - updated_at: timestamptz
 ```
 ```

+ 3 - 3
DEPLOYMENT_GUIDE.md

@@ -342,9 +342,9 @@ SELECT
   ssc.sync_frequency,
   ssc.sync_frequency,
   ssc.last_sync_at,
   ssc.last_sync_at,
   ssc.next_sync_at,
   ssc.next_sync_at,
-  ssc.sync_products,
-  ssc.sync_orders,
-  ssc.sync_customers
+  ssc.products_access_policy,
+  ssc.customers_access_policy,
+  ssc.orders_access_policy
 FROM store_sync_config ssc
 FROM store_sync_config ssc
 JOIN stores s ON s.id = ssc.store_id
 JOIN stores s ON s.id = ssc.store_id
 WHERE s.platform_name = 'shoprenter';
 WHERE s.platform_name = 'shoprenter';

+ 13 - 5
docs/QDRANT_INTEGRATION.md

@@ -37,12 +37,20 @@ The `stores.data_access_permissions` JSONB field controls what data can be synce
 }
 }
 ```
 ```
 
 
-### Privacy Compliance
+### Privacy Compliance (GDPR-Compliant 3-State Access Policy)
 
 
-- Products are always synced (core catalog data)
-- Orders and customers respect store owner preferences
-- SQL cache and Qdrant sync both check permissions
-- Helper functions: `can_sync_products()`, `can_sync_orders()`, `can_sync_customers()`
+Store owners can control data access with three levels per data type (products, orders, customers):
+
+- **`sync`** - Full synchronization to SQL cache and Qdrant (fastest, best AI performance)
+- **`api_only`** - Direct API access only, no caching (privacy-focused, slower)
+- **`not_allowed`** - No access allowed (maximum privacy)
+
+MCP tools automatically respect these policies:
+- `sync`: Use Qdrant or SQL cache for fast access
+- `api_only`: Fetch directly from platform API, bypass all caching
+- `not_allowed`: Return access denied error
+
+Sync functions check policies before syncing - only data with `sync` policy is cached.
 
 
 ## Collection Schemas
 ## Collection Schemas
 
 

+ 3 - 3
shopcall.ai-main/src/components/IntegrationsContent.tsx

@@ -44,9 +44,9 @@ interface ConnectedStore {
   store_sync_config?: {
   store_sync_config?: {
     enabled: boolean;
     enabled: boolean;
     sync_frequency: string;
     sync_frequency: string;
-    sync_products: boolean;
-    sync_orders: boolean;
-    sync_customers: boolean;
+    products_access_policy?: 'sync' | 'api_only' | 'not_allowed';
+    customers_access_policy?: 'sync' | 'api_only' | 'not_allowed';
+    orders_access_policy?: 'sync' | 'api_only' | 'not_allowed';
   }[];
   }[];
 }
 }
 
 

+ 6 - 12
supabase/functions/_shared/mcp-qdrant-helpers.ts

@@ -58,30 +58,24 @@ export async function getStoreQdrantConfig(storeId: string): Promise<{
     return null;
     return null;
   }
   }
 
 
-  // Get sync config with new access policy columns
+  // Get sync config with access policy columns
   const { data: syncConfig, error: syncError } = await supabase
   const { data: syncConfig, error: syncError } = await supabase
     .from('store_sync_config')
     .from('store_sync_config')
-    .select('sync_products, sync_orders, sync_customers, products_access_policy, customers_access_policy, orders_access_policy')
+    .select('products_access_policy, customers_access_policy, orders_access_policy')
     .eq('store_id', storeId)
     .eq('store_id', storeId)
     .single();
     .single();
 
 
   // Default to all enabled if no config exists
   // Default to all enabled if no config exists
   const config = syncConfig || {
   const config = syncConfig || {
-    sync_products: true,
-    sync_orders: true,
-    sync_customers: true,
     products_access_policy: 'sync' as DataAccessPolicy,
     products_access_policy: 'sync' as DataAccessPolicy,
     customers_access_policy: 'sync' as DataAccessPolicy,
     customers_access_policy: 'sync' as DataAccessPolicy,
     orders_access_policy: 'sync' as DataAccessPolicy
     orders_access_policy: 'sync' as DataAccessPolicy
   };
   };
 
 
-  // Use new policy columns, fallback to old boolean columns for backward compatibility
-  const productsPolicy = (config.products_access_policy ||
-    (config.sync_products ? 'sync' : 'not_allowed')) as DataAccessPolicy;
-  const customersPolicy = (config.customers_access_policy ||
-    (config.sync_customers ? 'sync' : 'not_allowed')) as DataAccessPolicy;
-  const ordersPolicy = (config.orders_access_policy ||
-    (config.sync_orders ? 'sync' : 'not_allowed')) as DataAccessPolicy;
+  // Use policy columns (default to 'sync' if not set)
+  const productsPolicy = (config.products_access_policy || 'sync') as DataAccessPolicy;
+  const customersPolicy = (config.customers_access_policy || 'sync') as DataAccessPolicy;
+  const ordersPolicy = (config.orders_access_policy || 'sync') as DataAccessPolicy;
 
 
   return {
   return {
     enabled: store.qdrant_sync_enabled !== false, // Default to true
     enabled: store.qdrant_sync_enabled !== false, // Default to true

+ 0 - 3
supabase/functions/api/index.ts

@@ -73,9 +73,6 @@ serve(async (req) => {
           store_sync_config (
           store_sync_config (
             enabled,
             enabled,
             sync_frequency,
             sync_frequency,
-            sync_products,
-            sync_orders,
-            sync_customers,
             products_access_policy,
             products_access_policy,
             customers_access_policy,
             customers_access_policy,
             orders_access_policy,
             orders_access_policy,

+ 5 - 8
supabase/functions/shoprenter-scheduled-sync/index.ts

@@ -62,9 +62,6 @@ serve(wrapHandler('shoprenter-scheduled-sync', async (req) => {
         store_sync_config (
         store_sync_config (
           enabled,
           enabled,
           sync_frequency,
           sync_frequency,
-          sync_products,
-          sync_orders,
-          sync_customers,
           last_sync_at,
           last_sync_at,
           next_sync_at,
           next_sync_at,
           products_access_policy,
           products_access_policy,
@@ -151,7 +148,7 @@ serve(wrapHandler('shoprenter-scheduled-sync', async (req) => {
         })
         })
 
 
         // Sync Products (only if policy allows sync)
         // Sync Products (only if policy allows sync)
-        if (config?.sync_products !== false && 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}`)
             let page = 0  // ShopRenter API uses zero-based pagination
             let page = 0  // ShopRenter API uses zero-based pagination
@@ -209,16 +206,16 @@ serve(wrapHandler('shoprenter-scheduled-sync', async (req) => {
             syncStats.products.errors++
             syncStats.products.errors++
             syncStats.status = 'partial'
             syncStats.status = 'partial'
           }
           }
-        } else if (config?.sync_products !== false && productsPolicy !== 'sync') {
+        } else {
           console.log(`[ShopRenter Scheduled Sync] Products sync skipped for store ${storeId}: access policy is '${productsPolicy}' (requires 'sync')`)
           console.log(`[ShopRenter Scheduled Sync] Products sync skipped for store ${storeId}: access policy is '${productsPolicy}' (requires 'sync')`)
         }
         }
 
 
         // Sync Orders and Customers to Qdrant (if enabled and policy allows)
         // Sync Orders and Customers to Qdrant (if enabled and policy allows)
         // Note: Orders/customers are NOT cached to database for GDPR compliance
         // Note: Orders/customers are NOT cached to database for GDPR compliance
-        // They are only synced to Qdrant for AI access when flags are enabled
+        // They are only synced to Qdrant for AI access when policy allows
         const qdrantEnabled = store.qdrant_sync_enabled !== false
         const qdrantEnabled = store.qdrant_sync_enabled !== false
-        const shouldSyncOrders = config?.sync_orders === true && ordersPolicy === 'sync'
-        const shouldSyncCustomers = config?.sync_customers === true && customersPolicy === 'sync'
+        const shouldSyncOrders = ordersPolicy === 'sync'
+        const shouldSyncCustomers = customersPolicy === 'sync'
 
 
         // Call shoprenter-sync to handle full sync (products + orders + customers)
         // Call shoprenter-sync to handle full sync (products + orders + customers)
         // This ensures orders and customers are synced to Qdrant (only if policies allow)
         // This ensures orders and customers are synced to Qdrant (only if policies allow)

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

@@ -741,8 +741,8 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
         qdrant_sync_enabled,
         qdrant_sync_enabled,
         data_access_permissions,
         data_access_permissions,
         store_sync_config (
         store_sync_config (
-          sync_orders,
-          sync_customers
+          orders_access_policy,
+          customers_access_policy
         )
         )
       `)
       `)
       .eq('id', storeId)
       .eq('id', storeId)
@@ -787,11 +787,6 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
       qdrant: qdrantEnabled
       qdrant: qdrantEnabled
     })
     })
 
 
-    console.log('[ShopRenter] Sync config:', {
-      syncOrders: shouldSyncOrders,
-      syncCustomers: shouldSyncCustomers
-    })
-
     const syncStats = {
     const syncStats = {
       products: { synced: 0, errors: 0 },
       products: { synced: 0, errors: 0 },
       orders: { synced: 0, errors: 0 },
       orders: { synced: 0, errors: 0 },
@@ -884,7 +879,7 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
     // Note: Not cached to database for GDPR compliance, only synced to Qdrant for AI access
     // Note: Not cached to database for GDPR compliance, only synced to Qdrant for AI access
     const allOrders: any[] = []
     const allOrders: any[] = []
     let orderSyncError: Error | null = null
     let orderSyncError: Error | null = null
-    if (qdrantEnabled && shouldSyncOrders && canSyncOrders) {
+    if (qdrantEnabled && canSyncOrders) {
       try {
       try {
         console.log('[ShopRenter] Syncing orders to Qdrant...')
         console.log('[ShopRenter] Syncing orders to Qdrant...')
         let page = 0
         let page = 0
@@ -925,7 +920,6 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
     } else {
     } else {
       console.log('[ShopRenter] Order sync skipped:', {
       console.log('[ShopRenter] Order sync skipped:', {
         qdrantEnabled,
         qdrantEnabled,
-        shouldSyncOrders,
         canSyncOrders
         canSyncOrders
       })
       })
     }
     }
@@ -934,7 +928,7 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
     // Note: Not cached to database for GDPR compliance, only synced to Qdrant for AI access
     // Note: Not cached to database for GDPR compliance, only synced to Qdrant for AI access
     const allCustomers: any[] = []
     const allCustomers: any[] = []
     let customerSyncError: Error | null = null
     let customerSyncError: Error | null = null
-    if (qdrantEnabled && shouldSyncCustomers && canSyncCustomers) {
+    if (qdrantEnabled && canSyncCustomers) {
       try {
       try {
         console.log('[ShopRenter] Syncing customers to Qdrant...')
         console.log('[ShopRenter] Syncing customers to Qdrant...')
         let page = 0
         let page = 0
@@ -975,7 +969,6 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
     } else {
     } else {
       console.log('[ShopRenter] Customer sync skipped:', {
       console.log('[ShopRenter] Customer sync skipped:', {
         qdrantEnabled,
         qdrantEnabled,
-        shouldSyncCustomers,
         canSyncCustomers
         canSyncCustomers
       })
       })
     }
     }

+ 32 - 0
supabase/migrations/20251117_drop_deprecated_sync_columns.sql

@@ -0,0 +1,32 @@
+-- Migration: Drop deprecated boolean sync columns from store_sync_config
+-- These have been replaced by the new access policy enum columns
+-- Created: 2025-11-17 (Issue #95 - GDPR compliance)
+
+-- Drop the deprecated boolean columns
+ALTER TABLE store_sync_config
+  DROP COLUMN IF EXISTS sync_products CASCADE,
+  DROP COLUMN IF EXISTS sync_orders CASCADE,
+  DROP COLUMN IF EXISTS sync_customers CASCADE;
+
+-- Add comments to confirm the new system
+COMMENT ON COLUMN store_sync_config.products_access_policy IS 'GDPR-compliant access policy for products: sync (full sync to cache), api_only (direct API access only), not_allowed (no access)';
+COMMENT ON COLUMN store_sync_config.customers_access_policy IS 'GDPR-compliant access policy for customers: sync (full sync to cache), api_only (direct API access only), not_allowed (no access)';
+COMMENT ON COLUMN store_sync_config.orders_access_policy IS 'GDPR-compliant access policy for orders: sync (full sync to cache), api_only (direct API access only), not_allowed (no access)';
+
+-- Ensure all existing stores have default policies if not set
+UPDATE store_sync_config
+SET
+  products_access_policy = COALESCE(products_access_policy, 'sync'::data_access_policy),
+  customers_access_policy = COALESCE(customers_access_policy, 'sync'::data_access_policy),
+  orders_access_policy = COALESCE(orders_access_policy, 'sync'::data_access_policy)
+WHERE
+  products_access_policy IS NULL
+  OR customers_access_policy IS NULL
+  OR orders_access_policy IS NULL;
+
+-- Log completion
+DO $$
+BEGIN
+  RAISE NOTICE 'Migration completed: Dropped deprecated sync_products, sync_orders, sync_customers columns from store_sync_config';
+  RAISE NOTICE 'All stores now use the new GDPR-compliant data_access_policy enum columns';
+END $$;