@@ -149,7 +149,8 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
149149
150150 guard try await shouldPerformFullSync ( for: siteID, maxAge: maxAge) else {
151151 let reason = await getSyncSkipReason ( for: siteID, maxAge: maxAge)
152- trackAnalytics ( WooAnalyticsEvent . LocalCatalog. syncSkipped ( reason: reason) )
152+ trackAnalytics ( WooAnalyticsEvent . LocalCatalog. syncSkipped ( reason: reason,
153+ syncType: POSCatalogSyncType . full. rawValue) )
153154 throw POSCatalogSyncError . shouldNotSync
154155 }
155156
@@ -315,7 +316,8 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
315316
316317 guard try await shouldPerformIncrementalSync ( for: siteID, maxAge: maxAge) else {
317318 let reason = await getIncrementalSyncSkipReason ( for: siteID, maxAge: maxAge)
318- trackAnalytics ( WooAnalyticsEvent . LocalCatalog. syncSkipped ( reason: reason) )
319+ trackAnalytics ( WooAnalyticsEvent . LocalCatalog. syncSkipped ( reason: reason,
320+ syncType: POSCatalogSyncType . incremental. rawValue) )
319321 return
320322 }
321323
@@ -596,6 +598,16 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
596598 return " eligibility_check_failed "
597599 }
598600
601+ // Check sync eligibility
602+ do {
603+ let eligibility = try await syncEligibility ( for: siteID)
604+ if case . ineligible( let reason) = eligibility {
605+ return reason. skipReason
606+ }
607+ } catch {
608+ return " sync_eligibility_check_failed "
609+ }
610+
599611 // Check if sync is needed based on age
600612 guard let lastSyncDate = await lastFullSyncDate ( for: siteID) else {
601613 return " no_previous_sync " // This shouldn't happen if shouldPerformFullSync returned false
@@ -610,7 +622,7 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
610622 }
611623
612624 private func getIncrementalSyncSkipReason( for siteID: Int64 , maxAge: TimeInterval ) async -> String {
613- // Check eligibility first
625+ // Check overall local catalog eligibility first
614626 do {
615627 let eligibility = try await catalogEligibilityChecker. catalogEligibility ( for: siteID)
616628 if case . ineligible( let reason) = eligibility {
@@ -620,6 +632,16 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
620632 return " eligibility_check_failed "
621633 }
622634
635+ // Check sync eligibility
636+ do {
637+ let eligibility = try await syncEligibility ( for: siteID)
638+ if case . ineligible( let reason) = eligibility {
639+ return reason. skipReason
640+ }
641+ } catch {
642+ return " sync_eligibility_check_failed "
643+ }
644+
623645 // Check if full sync exists
624646 guard await lastFullSyncDate ( for: siteID) != nil else {
625647 return " no_full_sync "
@@ -703,19 +725,43 @@ private extension POSCatalogSyncCoordinator {
703725
704726 /// Checks if sync is eligible for the given site based on catalog eligibility and temporal criteria
705727 func checkSyncEligibility( for siteID: Int64 ) async throws -> Bool {
706- guard try await catalogEligibilityChecker. catalogEligibility ( for: siteID) == . eligible else {
707- DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) - Catalog ineligible " )
728+ switch try await syncEligibility ( for: siteID) {
729+ case . eligible ( reason: let reason) :
730+ switch reason {
731+ case . neverSynced:
732+ DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) eligible (no first sync date recorded) " )
733+ case . openedRecently( daysSinceOpened: let daysSinceOpened) :
734+ DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) eligible (last opened \( daysSinceOpened) days ago) " )
735+ case . recentFirstSync( daysSinceFirstSync: let daysSinceFirstSync) :
736+ DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) eligible (within grace period: \( daysSinceFirstSync) days since first sync) " )
737+ }
738+ return true
739+ case . ineligible ( reason: let reason) :
740+ switch reason {
741+ case . notEligibleForLocalCatalog:
742+ DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) - Catalog ineligible " )
743+ case . notOpenedRecently( daysSinceOpened: let daysSinceOpened) :
744+ DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) ineligible (POS last opened \( daysSinceOpened) days ago) " )
745+ case . neverOpenedAndPastFirstSyncGracePeriod( daysSinceFirstSync: let daysSinceFirstSync) :
746+ DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) ineligible (past 30-day grace period, " +
747+ " no recent POS open, \( daysSinceFirstSync) days since first sync) " )
748+ }
708749 return false
709750 }
751+ }
752+
753+ func syncEligibility( for siteID: Int64) async throws -> SyncEligibility {
754+ guard try await catalogEligibilityChecker. catalogEligibility ( for: siteID) == . eligible else {
755+ return . ineligible( reason: . notEligibleForLocalCatalog)
756+ }
710757
711758 // Then check temporal eligibility (30-day criteria)
712759 let firstSyncDate = siteSettings. getFirstPOSCatalogSyncDate ( siteID: siteID)
713760 let lastOpenedDate = siteSettings. getPOSLastOpenedDate ( siteID: siteID)
714761
715762 // Case 1: No first sync date yet - eligible (will be set on first sync)
716763 guard let firstSync = firstSyncDate else {
717- DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) eligible (no first sync date recorded) " )
718- return true
764+ return . eligible( reason: . neverSynced)
719765 }
720766
721767 // Case 2: Has synced before. Check if within 30-day window from first sync
@@ -724,23 +770,44 @@ private extension POSCatalogSyncCoordinator {
724770 if daysSinceFirstSync > Constants . maxDaysSinceLastOpened {
725771 // More than 30 days since first sync - must have opened POS recently to remain eligible
726772 guard let lastOpened = lastOpenedDate else {
727- DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) ineligible (past 30-day grace period, no recent POS open) " )
728- return false
773+ return . ineligible( reason: . neverOpenedAndPastFirstSyncGracePeriod( daysSinceFirstSync: daysSinceFirstSync) )
729774 }
730775
731776 let daysSinceLastOpened = Calendar . current. dateComponents ( [ . day] , from: lastOpened, to: Date ( ) ) . day ?? 0
732777
733778 if daysSinceLastOpened <= Constants . maxDaysSinceLastOpened {
734- DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) eligible (last opened \( daysSinceLastOpened) days ago) " )
735- return true
779+ return . eligible( reason: . openedRecently( daysSinceOpened: daysSinceLastOpened) )
736780 } else {
737- DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) ineligible (POS last opened \( daysSinceLastOpened) days ago) " )
738- return false
781+ return . ineligible( reason: . notOpenedRecently( daysSinceOpened: daysSinceLastOpened) )
739782 }
740783 } else {
741784 // Within 30 days of first sync - always eligible (new user grace period)
742- DDLogInfo ( " 📋 POSCatalogSyncCoordinator: Site \( siteID) eligible (within grace period: \( daysSinceFirstSync) days since first sync) " )
743- return true
785+ return . eligible( reason: . recentFirstSync( daysSinceFirstSync: daysSinceFirstSync) )
786+ }
787+ }
788+
789+ enum SyncEligibility {
790+ case eligible ( reason: EligibleReason)
791+ case ineligible ( reason: IneligibleReason)
792+
793+ enum EligibleReason {
794+ case neverSynced
795+ case openedRecently( daysSinceOpened: Int )
796+ case recentFirstSync( daysSinceFirstSync: Int )
797+ }
798+
799+ enum IneligibleReason {
800+ case notEligibleForLocalCatalog
801+ case notOpenedRecently( daysSinceOpened: Int )
802+ case neverOpenedAndPastFirstSyncGracePeriod( daysSinceFirstSync: Int )
803+
804+ var skipReason : String {
805+ switch self {
806+ case . notEligibleForLocalCatalog: " not_eligible "
807+ case . notOpenedRecently: " pos_not_opened_30_days "
808+ case . neverOpenedAndPastFirstSyncGracePeriod: " pos_not_opened_30_days "
809+ }
810+ }
744811 }
745812 }
746813
0 commit comments