Преглед изворни кода

fix: consolidate sync timing to store_sync_config.last_sync_at - remove duplication #75

- Update Web UI to read last_sync_at from store_sync_config instead of stores.sync_completed_at
- Update API to always include store_sync_config with last_sync_at and next_sync_at fields
- Remove all writes to stores.sync_completed_at from sync functions
- Keep stores.sync_status for runtime status (idle, syncing, completed, error)
- Use store_sync_config.last_sync_at as single source of truth for sync timing
- Add migration to drop stores.sync_completed_at column after data migration
- This resolves the sync timestamp duplication issue identified in #75

Affected files:
- Frontend: IntegrationsContent.tsx - read from store_sync_config
- Backend API: api/index.ts - always include sync config
- Sync functions: woocommerce-scheduled-sync, shoprenter-scheduled-sync, shoprenter-sync, trigger-sync
- Database: migration to remove sync_completed_at column

Co-Authored-By: Claude <noreply@anthropic.com>
Claude пре 5 месеци
родитељ
комит
5f5b0e8cd7

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

@@ -707,9 +707,9 @@ export function IntegrationsContent() {
                         <span className="text-xs text-slate-400">{t('integrations.syncStatus')}</span>
                         <span className="text-xs text-slate-400">{t('integrations.syncStatus')}</span>
                         {getSyncStatusBadge(shop)}
                         {getSyncStatusBadge(shop)}
                       </div>
                       </div>
-                      {shop.sync_completed_at && shop.sync_status === 'completed' && (
+                      {shop.store_sync_config?.[0]?.last_sync_at && shop.sync_status === 'completed' && (
                         <div className="text-xs text-slate-500">
                         <div className="text-xs text-slate-500">
-                          {t('integrations.lastSync')}: {new Date(shop.sync_completed_at).toLocaleString()}
+                          {t('integrations.lastSync')}: {new Date(shop.store_sync_config[0].last_sync_at).toLocaleString()}
                         </div>
                         </div>
                       )}
                       )}
                       {shop.sync_error && (
                       {shop.sync_error && (

+ 5 - 15
supabase/functions/api/index.ts

@@ -56,11 +56,10 @@ serve(async (req) => {
 
 
     // GET /api/stores - List all stores for the user
     // GET /api/stores - List all stores for the user
     if (path === 'stores' && req.method === 'GET') {
     if (path === 'stores' && req.method === 'GET') {
-      const includeSyncConfig = url.searchParams.get('include_sync_config') === 'true'
-
+      // Always include sync config since Web UI needs last_sync_at
       let query = supabase
       let query = supabase
         .from('stores')
         .from('stores')
-        .select(includeSyncConfig ? `
+        .select(`
           *,
           *,
           phone_numbers!stores_phone_number_id_fkey (
           phone_numbers!stores_phone_number_id_fkey (
             id,
             id,
@@ -74,18 +73,9 @@ serve(async (req) => {
           store_sync_config (
           store_sync_config (
             enabled,
             enabled,
             sync_frequency,
             sync_frequency,
-            sync_products
-          )
-        ` : `
-          *,
-          phone_numbers!stores_phone_number_id_fkey (
-            id,
-            phone_number,
-            country_code,
-            country_name,
-            location,
-            phone_type,
-            price
+            sync_products,
+            last_sync_at,
+            next_sync_at
           )
           )
         `)
         `)
         .eq('user_id', user.id)
         .eq('user_id', user.id)

+ 2 - 3
supabase/functions/shoprenter-scheduled-sync/index.ts

@@ -245,18 +245,17 @@ serve(wrapHandler('shoprenter-scheduled-sync', async (req) => {
           last_sync_type: 'scheduled'
           last_sync_type: 'scheduled'
         }
         }
 
 
-        // Update stores table (for Web UI display)
+        // Update stores table (runtime status only, preserve alt_data)
         await supabaseAdmin
         await supabaseAdmin
           .from('stores')
           .from('stores')
           .update({
           .update({
             alt_data: updatedAltData,
             alt_data: updatedAltData,
             sync_status: syncStats.status === 'success' ? 'completed' : 'error',
             sync_status: syncStats.status === 'success' ? 'completed' : 'error',
-            sync_completed_at: syncCompletedAt,
             sync_error: syncStats.error_message
             sync_error: syncStats.error_message
           })
           })
           .eq('id', storeId)
           .eq('id', storeId)
 
 
-        // Update store_sync_config timestamps (for scheduling logic)
+        // Update store_sync_config (single source of truth for sync timing)
         if (config) {
         if (config) {
           await supabaseAdmin
           await supabaseAdmin
             .from('store_sync_config')
             .from('store_sync_config')

+ 1 - 2
supabase/functions/shoprenter-sync/index.ts

@@ -541,12 +541,11 @@ serve(wrapHandler('shoprenter-sync', async (req) => {
           last_sync_type: 'manual'
           last_sync_type: 'manual'
         },
         },
         sync_status: 'completed',
         sync_status: 'completed',
-        sync_completed_at: syncCompletedAt,
         sync_error: null
         sync_error: null
       })
       })
       .eq('id', storeId)
       .eq('id', storeId)
 
 
-    // Update store_sync_config timestamps (for scheduling logic)
+    // Update store_sync_config (single source of truth for sync timing)
     const { error: configUpdateError } = await supabaseAdmin
     const { error: configUpdateError } = await supabaseAdmin
       .from('store_sync_config')
       .from('store_sync_config')
       .update({
       .update({

+ 4 - 8
supabase/functions/trigger-sync/index.ts

@@ -95,7 +95,6 @@ serve(wrapHandler('trigger-sync', async (req) => {
           .from('stores')
           .from('stores')
           .update({
           .update({
             sync_status: 'error',
             sync_status: 'error',
-            sync_completed_at: new Date().toISOString(),
             sync_error: `Unsupported platform: ${store.platform_name}`
             sync_error: `Unsupported platform: ${store.platform_name}`
           })
           })
           .eq('id', store_id)
           .eq('id', store_id)
@@ -145,12 +144,11 @@ serve(wrapHandler('trigger-sync', async (req) => {
           const altData = currentStore?.alt_data || {}
           const altData = currentStore?.alt_data || {}
           const syncCompletedAt = new Date().toISOString()
           const syncCompletedAt = new Date().toISOString()
 
 
-          // Update stores table (for Web UI display)
+          // Update stores table (runtime status only, preserve alt_data)
           await supabaseAdmin
           await supabaseAdmin
             .from('stores')
             .from('stores')
             .update({
             .update({
               sync_status: 'completed',
               sync_status: 'completed',
-              sync_completed_at: syncCompletedAt,
               alt_data: {
               alt_data: {
                 ...altData,
                 ...altData,
                 last_sync_at: syncCompletedAt,
                 last_sync_at: syncCompletedAt,
@@ -161,7 +159,7 @@ serve(wrapHandler('trigger-sync', async (req) => {
             })
             })
             .eq('id', store_id)
             .eq('id', store_id)
 
 
-          // Update store_sync_config timestamps (for scheduling logic)
+          // Update store_sync_config (single source of truth for sync timing)
           const { error: configUpdateError } = await supabaseAdmin
           const { error: configUpdateError } = await supabaseAdmin
             .from('store_sync_config')
             .from('store_sync_config')
             .update({
             .update({
@@ -187,12 +185,11 @@ serve(wrapHandler('trigger-sync', async (req) => {
             .from('stores')
             .from('stores')
             .update({
             .update({
               sync_status: 'error',
               sync_status: 'error',
-              sync_completed_at: errorTime,
               sync_error: result.error || 'Unknown error during sync'
               sync_error: result.error || 'Unknown error during sync'
             })
             })
             .eq('id', store_id)
             .eq('id', store_id)
 
 
-          // Still update store_sync_config timestamp even on error (for retry logic)
+          // Update store_sync_config timestamp even on error (for retry logic)
           await supabaseAdmin
           await supabaseAdmin
             .from('store_sync_config')
             .from('store_sync_config')
             .update({
             .update({
@@ -208,12 +205,11 @@ serve(wrapHandler('trigger-sync', async (req) => {
           .from('stores')
           .from('stores')
           .update({
           .update({
             sync_status: 'error',
             sync_status: 'error',
-            sync_completed_at: errorTime,
             sync_error: error.message || 'Unknown error during sync'
             sync_error: error.message || 'Unknown error during sync'
           })
           })
           .eq('id', store_id)
           .eq('id', store_id)
 
 
-        // Still update store_sync_config timestamp even on error (for retry logic)
+        // Update store_sync_config timestamp even on error (for retry logic)
         await supabaseAdmin
         await supabaseAdmin
           .from('store_sync_config')
           .from('store_sync_config')
           .update({
           .update({

+ 3 - 4
supabase/functions/woocommerce-scheduled-sync/index.ts

@@ -228,20 +228,19 @@ serve(wrapHandler('woocommerce-scheduled-sync', async (req) => {
           syncStats.error_message = syncResult.error || 'Unknown error'
           syncStats.error_message = syncResult.error || 'Unknown error'
         }
         }
 
 
-        // Update BOTH stores table and store_sync_config to maintain consistency
+        // Update stores table status and store_sync_config timestamps
         const syncCompletedAt = new Date().toISOString()
         const syncCompletedAt = new Date().toISOString()
 
 
-        // Update stores table (for Web UI display)
+        // Update stores table (runtime status only, no timestamps)
         await supabaseAdmin
         await supabaseAdmin
           .from('stores')
           .from('stores')
           .update({
           .update({
             sync_status: syncStats.status === 'success' ? 'completed' : 'error',
             sync_status: syncStats.status === 'success' ? 'completed' : 'error',
-            sync_completed_at: syncCompletedAt,
             sync_error: syncStats.error_message
             sync_error: syncStats.error_message
           })
           })
           .eq('id', storeId)
           .eq('id', storeId)
 
 
-        // Update store_sync_config timestamps (for scheduling logic)
+        // Update store_sync_config (single source of truth for sync timing)
         if (config) {
         if (config) {
           await supabaseAdmin
           await supabaseAdmin
             .from('store_sync_config')
             .from('store_sync_config')

+ 20 - 0
supabase/migrations/20251112_remove_sync_completed_at.sql

@@ -0,0 +1,20 @@
+-- Migration: Remove sync_completed_at column from stores table
+-- This consolidates sync timing to use store_sync_config.last_sync_at as single source of truth
+
+-- First, migrate any existing sync_completed_at data to store_sync_config.last_sync_at
+-- This ensures no data is lost during the migration
+UPDATE store_sync_config
+SET last_sync_at = stores.sync_completed_at
+FROM stores
+WHERE store_sync_config.store_id = stores.id
+  AND stores.sync_completed_at IS NOT NULL
+  AND (store_sync_config.last_sync_at IS NULL 
+       OR stores.sync_completed_at > store_sync_config.last_sync_at);
+
+-- Drop the sync_completed_at column from stores table
+ALTER TABLE stores DROP COLUMN IF EXISTS sync_completed_at;
+
+-- Add comment explaining the change
+COMMENT ON TABLE store_sync_config IS 'Store sync configuration and timing. The last_sync_at field is the single source of truth for when a sync last completed.';
+COMMENT ON COLUMN store_sync_config.last_sync_at IS 'Single source of truth for last sync completion time. Used by both Web UI display and scheduling logic.';
+COMMENT ON COLUMN stores.sync_status IS 'Runtime sync status (idle, syncing, completed, error). Does not include timing - see store_sync_config.last_sync_at for that.';