diff --git a/Modules/Sources/Storage/Model/Copiable/Models+Copiable.generated.swift b/Modules/Sources/Storage/Model/Copiable/Models+Copiable.generated.swift index 5bf17686464..34327f624f0 100644 --- a/Modules/Sources/Storage/Model/Copiable/Models+Copiable.generated.swift +++ b/Modules/Sources/Storage/Model/Copiable/Models+Copiable.generated.swift @@ -115,7 +115,8 @@ extension Storage.GeneralStoreSettings { lastSelectedOrderStatus: NullableCopiableProp = .copy, favoriteProductIDs: CopiableProp<[Int64]> = .copy, searchTermsByKey: CopiableProp<[String: [String]]> = .copy, - isPOSTabVisible: NullableCopiableProp = .copy + isPOSTabVisible: NullableCopiableProp = .copy, + posLastFullSyncDate: NullableCopiableProp = .copy ) -> Storage.GeneralStoreSettings { let storeID = storeID ?? self.storeID let isTelemetryAvailable = isTelemetryAvailable ?? self.isTelemetryAvailable @@ -137,6 +138,7 @@ extension Storage.GeneralStoreSettings { let favoriteProductIDs = favoriteProductIDs ?? self.favoriteProductIDs let searchTermsByKey = searchTermsByKey ?? self.searchTermsByKey let isPOSTabVisible = isPOSTabVisible ?? self.isPOSTabVisible + let posLastFullSyncDate = posLastFullSyncDate ?? self.posLastFullSyncDate return Storage.GeneralStoreSettings( storeID: storeID, @@ -158,7 +160,8 @@ extension Storage.GeneralStoreSettings { lastSelectedOrderStatus: lastSelectedOrderStatus, favoriteProductIDs: favoriteProductIDs, searchTermsByKey: searchTermsByKey, - isPOSTabVisible: isPOSTabVisible + isPOSTabVisible: isPOSTabVisible, + posLastFullSyncDate: posLastFullSyncDate ) } } diff --git a/Modules/Sources/Storage/Model/GeneralStoreSettings.swift b/Modules/Sources/Storage/Model/GeneralStoreSettings.swift index 021f26dd53c..d0623517227 100644 --- a/Modules/Sources/Storage/Model/GeneralStoreSettings.swift +++ b/Modules/Sources/Storage/Model/GeneralStoreSettings.swift @@ -86,6 +86,10 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable { /// public var isPOSTabVisible: Bool? + /// Last time a POS catalog full sync was completed for this store. + /// + public var posLastFullSyncDate: Date? + public init(storeID: String? = nil, isTelemetryAvailable: Bool = false, telemetryLastReportedTime: Date? = nil, @@ -105,7 +109,8 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable { lastSelectedOrderStatus: String? = nil, favoriteProductIDs: [Int64] = [], searchTermsByKey: [String: [String]] = [:], - isPOSTabVisible: Bool? = nil) { + isPOSTabVisible: Bool? = nil, + posLastFullSyncDate: Date? = nil) { self.storeID = storeID self.isTelemetryAvailable = isTelemetryAvailable self.telemetryLastReportedTime = telemetryLastReportedTime @@ -126,6 +131,7 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable { self.favoriteProductIDs = favoriteProductIDs self.searchTermsByKey = searchTermsByKey self.isPOSTabVisible = isPOSTabVisible + self.posLastFullSyncDate = posLastFullSyncDate } public func erasingSelectedTaxRateID() -> GeneralStoreSettings { @@ -147,7 +153,8 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable { lastSelectedOrderStatus: lastSelectedOrderStatus, favoriteProductIDs: favoriteProductIDs, searchTermsByKey: searchTermsByKey, - isPOSTabVisible: isPOSTabVisible) + isPOSTabVisible: isPOSTabVisible, + posLastFullSyncDate: posLastFullSyncDate) } } @@ -182,6 +189,7 @@ extension GeneralStoreSettings { self.searchTermsByKey = try container.decodeIfPresent([String: [String]].self, forKey: .searchTermsByKey) ?? [:] self.isPOSTabVisible = try container.decodeIfPresent(Bool.self, forKey: .isPOSTabVisible) + self.posLastFullSyncDate = try container.decodeIfPresent(Date.self, forKey: .posLastFullSyncDate) // Decode new properties with `decodeIfPresent` and provide a default value if necessary. } diff --git a/Modules/Sources/Yosemite/Stores/Helpers/SiteSpecificAppSettingsStoreMethods.swift b/Modules/Sources/Yosemite/Stores/Helpers/SiteSpecificAppSettingsStoreMethods.swift index 7fbe31065d4..976bf32141d 100644 --- a/Modules/Sources/Yosemite/Stores/Helpers/SiteSpecificAppSettingsStoreMethods.swift +++ b/Modules/Sources/Yosemite/Stores/Helpers/SiteSpecificAppSettingsStoreMethods.swift @@ -11,6 +11,10 @@ public protocol SiteSpecificAppSettingsStoreMethodsProtocol { // Search history methods func getSearchTerms(for itemType: POSItemType, siteID: Int64) -> [String] func setSearchTerms(_ terms: [String], for itemType: POSItemType, siteID: Int64) + + // POS catalog sync timestamp methods + func getPOSLastFullSyncDate(for siteID: Int64) -> Date? + func setPOSLastFullSyncDate(_ date: Date?, for siteID: Int64) } /// Methods for managing site-specific app settings @@ -98,6 +102,20 @@ extension SiteSpecificAppSettingsStoreMethods { } } +// MARK: - POS Catalog Sync Timestamps +extension SiteSpecificAppSettingsStoreMethods { + func getPOSLastFullSyncDate(for siteID: Int64) -> Date? { + let storeSettings = getStoreSettings(for: siteID) + return storeSettings.posLastFullSyncDate + } + + func setPOSLastFullSyncDate(_ date: Date?, for siteID: Int64) { + let storeSettings = getStoreSettings(for: siteID) + let updatedSettings = storeSettings.copy(posLastFullSyncDate: date) + setStoreSettings(settings: updatedSettings, for: siteID) + } +} + // MARK: - Constants private enum Constants { static let generalStoreSettingsFileName = "general-store-settings.plist" diff --git a/Modules/Sources/Yosemite/Tools/POS/POSCatalogSyncCoordinator.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogSyncCoordinator.swift new file mode 100644 index 00000000000..1a7004a460e --- /dev/null +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogSyncCoordinator.swift @@ -0,0 +1,61 @@ +import Foundation +import Storage + +public protocol POSCatalogSyncCoordinatorProtocol { + /// Performs a full catalog sync for the specified site + /// - Parameter siteID: The site ID to sync catalog for + func performFullSync(for siteID: Int64) async throws + + /// Determines if a full sync should be performed based on the age of the last sync + /// - Parameters: + /// - siteID: The site ID to check + /// - maxAge: Maximum age before a sync is considered stale + /// - Returns: True if a sync should be performed + func shouldPerformFullSync(for siteID: Int64, maxAge: TimeInterval) -> Bool +} + +public final class POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol { + private let fullSyncService: POSCatalogFullSyncServiceProtocol + private let settingsStore: SiteSpecificAppSettingsStoreMethodsProtocol + + public init(fullSyncService: POSCatalogFullSyncServiceProtocol, + settingsStore: SiteSpecificAppSettingsStoreMethodsProtocol? = nil) { + self.fullSyncService = fullSyncService + self.settingsStore = settingsStore ?? SiteSpecificAppSettingsStoreMethods(fileStorage: PListFileStorage()) + } + + public func performFullSync(for siteID: Int64) async throws { + DDLogInfo("🔄 POSCatalogSyncCoordinator starting full sync for site \(siteID)") + + let catalog = try await fullSyncService.startFullSync(for: siteID) + + // Record the sync timestamp + settingsStore.setPOSLastFullSyncDate(Date(), for: siteID) + + DDLogInfo("✅ POSCatalogSyncCoordinator completed full sync for site \(siteID)") + } + + public func shouldPerformFullSync(for siteID: Int64, maxAge: TimeInterval) -> Bool { + guard let lastSyncDate = lastFullSyncDate(for: siteID) else { + DDLogInfo("📋 POSCatalogSyncCoordinator: No previous sync found for site \(siteID), sync needed") + return true + } + + let age = Date().timeIntervalSince(lastSyncDate) + let shouldSync = age > maxAge + + if shouldSync { + DDLogInfo("📋 POSCatalogSyncCoordinator: Last sync for site \(siteID) was \(Int(age))s ago (max: \(Int(maxAge))s), sync needed") + } else { + DDLogInfo("📋 POSCatalogSyncCoordinator: Last sync for site \(siteID) was \(Int(age))s ago (max: \(Int(maxAge))s), sync not needed") + } + + return shouldSync + } + + // MARK: - Private + + private func lastFullSyncDate(for siteID: Int64) -> Date? { + return settingsStore.getPOSLastFullSyncDate(for: siteID) + } +} diff --git a/Modules/Tests/YosemiteTests/Mocks/MockSiteSpecificAppSettingsStoreMethods.swift b/Modules/Tests/YosemiteTests/Mocks/MockSiteSpecificAppSettingsStoreMethods.swift index 5f990bc3a45..b1f794039de 100644 --- a/Modules/Tests/YosemiteTests/Mocks/MockSiteSpecificAppSettingsStoreMethods.swift +++ b/Modules/Tests/YosemiteTests/Mocks/MockSiteSpecificAppSettingsStoreMethods.swift @@ -1,4 +1,5 @@ @testable import Yosemite +import Foundation import Storage final class MockSiteSpecificAppSettingsStoreMethods: SiteSpecificAppSettingsStoreMethodsProtocol { @@ -27,6 +28,13 @@ final class MockSiteSpecificAppSettingsStoreMethods: SiteSpecificAppSettingsStor var spySetSearchTermsSiteID: Int64? var mockSearchTerms: [POSItemType: [String]] = [:] + // POS sync timestamp properties + var storedDates: [Int64: Date] = [:] + private(set) var getPOSLastFullSyncDateCallCount = 0 + private(set) var setPOSLastFullSyncDateCallCount = 0 + private(set) var lastSetSiteID: Int64? + private(set) var lastSetDate: Date? + func getStoreSettings(for siteID: Int64) -> GeneralStoreSettings { getStoreSettingsCalled = true return storeSettings @@ -83,4 +91,16 @@ final class MockSiteSpecificAppSettingsStoreMethods: SiteSpecificAppSettingsStor spySetSearchTermsSiteID = siteID mockSearchTerms[itemType] = terms } + + func getPOSLastFullSyncDate(for siteID: Int64) -> Date? { + getPOSLastFullSyncDateCallCount += 1 + return storedDates[siteID] + } + + func setPOSLastFullSyncDate(_ date: Date?, for siteID: Int64) { + setPOSLastFullSyncDateCallCount += 1 + lastSetSiteID = siteID + lastSetDate = date + storedDates[siteID] = date + } } diff --git a/Modules/Tests/YosemiteTests/Stores/Helpers/SiteSpecificAppSettingsStoreMethodsTests.swift b/Modules/Tests/YosemiteTests/Stores/Helpers/SiteSpecificAppSettingsStoreMethodsTests.swift index 1feb831f275..9544b73ddee 100644 --- a/Modules/Tests/YosemiteTests/Stores/Helpers/SiteSpecificAppSettingsStoreMethodsTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/Helpers/SiteSpecificAppSettingsStoreMethodsTests.swift @@ -229,6 +229,142 @@ struct SiteSpecificAppSettingsStoreMethodsTests { #expect(retrievedVariationTerms == variationTerms) #expect(retrievedCouponTerms == couponTerms) } + + // MARK: - POS Last Full Sync Date Tests + + @Test func getPOSLastFullSyncDate_returns_nil_when_no_date_exists() { + // When + let syncDate = sut.getPOSLastFullSyncDate(for: siteID) + + // Then + #expect(syncDate == nil) + } + + @Test func getPOSLastFullSyncDate_returns_saved_date() throws { + // Given + let expectedDate = Date() + let storeSettings = GeneralStoreSettings(posLastFullSyncDate: expectedDate) + let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: storeSettings]) + try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + + // When + let syncDate = sut.getPOSLastFullSyncDate(for: siteID) + + // Then + #expect(syncDate == expectedDate) + } + + @Test func setPOSLastFullSyncDate_saves_date_successfully() throws { + // Given + let dateToSave = Date() + let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: GeneralStoreSettings()]) + try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + + // When + sut.setPOSLastFullSyncDate(dateToSave, for: siteID) + + // Then + let savedData: GeneralStoreSettingsBySite = try fileStorage.data(for: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + #expect(savedData.storeSettingsBySite[siteID]?.posLastFullSyncDate == dateToSave) + } + + @Test func setPOSLastFullSyncDate_can_set_nil_date() throws { + // Given + let existingDate = Date() + let storeSettings = GeneralStoreSettings(posLastFullSyncDate: existingDate) + let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: storeSettings]) + try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + + // When + sut.setPOSLastFullSyncDate(nil, for: siteID) + + // Then + let savedData: GeneralStoreSettingsBySite = try fileStorage.data(for: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + #expect(savedData.storeSettingsBySite[siteID]?.posLastFullSyncDate == nil) + } + + @Test func setPOSLastFullSyncDate_preserves_other_settings() throws { + // Given + let existingStoreID = "existing-store" + let existingTerms = ["existing", "terms"] + let storeSettings = GeneralStoreSettings( + storeID: existingStoreID, + searchTermsByKey: ["product_search_terms": existingTerms] + ) + let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: storeSettings]) + try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + + let dateToSave = Date() + + // When + sut.setPOSLastFullSyncDate(dateToSave, for: siteID) + + // Then + let savedData: GeneralStoreSettingsBySite = try fileStorage.data(for: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + let savedSettings = savedData.storeSettingsBySite[siteID] + #expect(savedSettings?.posLastFullSyncDate == dateToSave) + #expect(savedSettings?.storeID == existingStoreID) + #expect(savedSettings?.searchTermsByKey["product_search_terms"] == existingTerms) + } + + @Test func setPOSLastFullSyncDate_preserves_dates_for_other_sites() throws { + // Given + let otherSiteID: Int64 = 456 + let otherSiteDate = Date().addingTimeInterval(-3600) + let otherSiteSettings = GeneralStoreSettings(posLastFullSyncDate: otherSiteDate) + let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [otherSiteID: otherSiteSettings]) + try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + + let newDate = Date() + + // When + sut.setPOSLastFullSyncDate(newDate, for: siteID) + + // Then + let savedData: GeneralStoreSettingsBySite = try fileStorage.data(for: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + #expect(savedData.storeSettingsBySite[siteID]?.posLastFullSyncDate == newDate) + #expect(savedData.storeSettingsBySite[otherSiteID]?.posLastFullSyncDate == otherSiteDate) + } + + @Test func getPOSLastFullSyncDate_handles_different_sites_independently() throws { + // Given + let siteA: Int64 = 123 + let siteB: Int64 = 456 + let dateA = Date() + let dateB = Date().addingTimeInterval(-3600) + + let storeSettingsA = GeneralStoreSettings(posLastFullSyncDate: dateA) + let storeSettingsB = GeneralStoreSettings(posLastFullSyncDate: dateB) + let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [ + siteA: storeSettingsA, + siteB: storeSettingsB + ]) + try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + + // When + let retrievedDateA = sut.getPOSLastFullSyncDate(for: siteA) + let retrievedDateB = sut.getPOSLastFullSyncDate(for: siteB) + + // Then + #expect(retrievedDateA == dateA) + #expect(retrievedDateB == dateB) + } + + @Test func resetStoreSettings_clears_pos_sync_date() throws { + // Given + let syncDate = Date() + let storeSettings = GeneralStoreSettings(posLastFullSyncDate: syncDate) + let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: storeSettings]) + try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL) + + // When + sut.resetStoreSettings() + + // Then + #expect(fileStorage.deleteIsHit == true) + let retrievedDate = sut.getPOSLastFullSyncDate(for: siteID) + #expect(retrievedDate == nil) + } } // MARK: - Mock FileStorage diff --git a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogSyncCoordinatorTests.swift b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogSyncCoordinatorTests.swift new file mode 100644 index 00000000000..1953d2ae700 --- /dev/null +++ b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogSyncCoordinatorTests.swift @@ -0,0 +1,161 @@ +import Foundation +import Testing +@testable import Yosemite +import Storage + +struct POSCatalogSyncCoordinatorTests { + private let mockSyncService: MockPOSCatalogFullSyncService + private let mockSettingsStore: MockSiteSpecificAppSettingsStoreMethods + private let sut: POSCatalogSyncCoordinator + private let sampleSiteID: Int64 = 134 + + init() { + self.mockSyncService = MockPOSCatalogFullSyncService() + self.mockSettingsStore = MockSiteSpecificAppSettingsStoreMethods() + self.sut = POSCatalogSyncCoordinator( + fullSyncService: mockSyncService, + settingsStore: mockSettingsStore + ) + } + + // MARK: - Full Sync Tests + + @Test func performFullSync_delegates_to_sync_service() async throws { + // Given + let expectedCatalog = POSCatalog( + products: [POSProduct.fake()], + variations: [POSProductVariation.fake()] + ) + mockSyncService.startFullSyncResult = .success(expectedCatalog) + + // When + try await sut.performFullSync(for: sampleSiteID) + + // Then + #expect(mockSyncService.startFullSyncCallCount == 1) + #expect(mockSyncService.lastSyncSiteID == sampleSiteID) + } + + @Test func performFullSync_stores_sync_timestamp() async throws { + // Given + let beforeSync = Date() + let expectedCatalog = POSCatalog(products: [], variations: []) + mockSyncService.startFullSyncResult = .success(expectedCatalog) + + // When + _ = try await sut.performFullSync(for: sampleSiteID) + let afterSync = Date() + + // Then + #expect(mockSettingsStore.setPOSLastFullSyncDateCallCount == 1) + #expect(mockSettingsStore.lastSetSiteID == sampleSiteID) + + let storedDate = mockSettingsStore.lastSetDate + #expect(storedDate != nil) + #expect(storedDate! >= beforeSync) + #expect(storedDate! <= afterSync) + } + + @Test func performFullSync_propagates_errors() async throws { + // Given + let expectedError = NSError(domain: "sync", code: 500, userInfo: [NSLocalizedDescriptionKey: "Sync failed"]) + mockSyncService.startFullSyncResult = .failure(expectedError) + + // When/Then + await #expect(throws: expectedError) { + _ = try await sut.performFullSync(for: sampleSiteID) + } + + // Should not store timestamp on failure + #expect(mockSettingsStore.setPOSLastFullSyncDateCallCount == 0) + } + + // MARK: - Should Sync Decision Tests + + @Test func shouldPerformFullSync_returns_true_when_no_previous_sync() { + // Given - no previous sync date stored + mockSettingsStore.storedDates = [:] + + // When + let shouldSync = sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 3600) + + // Then + #expect(shouldSync == true) + #expect(mockSettingsStore.getPOSLastFullSyncDateCallCount == 1) + } + + @Test func shouldPerformFullSync_returns_true_when_sync_is_stale() { + // Given - previous sync was 2 hours ago + let twoHoursAgo = Date().addingTimeInterval(-2 * 60 * 60) + mockSettingsStore.storedDates[sampleSiteID] = twoHoursAgo + + // When - max age is 1 hour + let shouldSync = sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 60 * 60) + + // Then + #expect(shouldSync == true) + } + + @Test func shouldPerformFullSync_returns_false_when_sync_is_fresh() { + // Given - previous sync was 30 minutes ago + let thirtyMinutesAgo = Date().addingTimeInterval(-30 * 60) + mockSettingsStore.storedDates[sampleSiteID] = thirtyMinutesAgo + + // When - max age is 1 hour + let shouldSync = sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 60 * 60) + + // Then + #expect(shouldSync == false) + } + + @Test func shouldPerformFullSync_handles_different_sites_independently() { + // Given + let siteA: Int64 = 123 + let siteB: Int64 = 456 + let oneHourAgo = Date().addingTimeInterval(-60 * 60) + + mockSettingsStore.storedDates[siteA] = oneHourAgo // Has previous sync + // siteB has no previous sync + + // When + let shouldSyncA = sut.shouldPerformFullSync(for: siteA, maxAge: 2 * 60 * 60) // 2 hours + let shouldSyncB = sut.shouldPerformFullSync(for: siteB, maxAge: 2 * 60 * 60) // 2 hours + + // Then + #expect(shouldSyncA == false) // Recent sync exists + #expect(shouldSyncB == true) // No previous sync + } + + @Test func shouldPerformFullSync_with_zero_maxAge_always_returns_true() { + // Given - previous sync was just now + let justNow = Date() + mockSettingsStore.storedDates[sampleSiteID] = justNow + + // When - max age is 0 (always sync) + let shouldSync = sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 0) + + // Then + #expect(shouldSync == true) + } +} + +// MARK: - Mock Services + +final class MockPOSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol { + var startFullSyncResult: Result = .success(POSCatalog(products: [], variations: [])) + + private(set) var startFullSyncCallCount = 0 + private(set) var lastSyncSiteID: Int64? + + func startFullSync(for siteID: Int64) async throws -> POSCatalog { + startFullSyncCallCount += 1 + lastSyncSiteID = siteID + + switch startFullSyncResult { + case .success(let catalog): + return catalog + case .failure(let error): + throw error + } + } +} diff --git a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift index cb36b197379..cfb95b28abf 100644 --- a/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift +++ b/WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift @@ -4,7 +4,6 @@ import SwiftUI import Yosemite import class WooFoundation.CurrencySettings import protocol Storage.StorageManagerType -import protocol Storage.GRDBManagerProtocol import class WooFoundationCore.CurrencyFormatter /// View controller that provides the tab bar item for the Point of Sale tab. @@ -28,7 +27,6 @@ final class POSTabCoordinator { private let storesManager: StoresManager private let credentials: Credentials? private let storageManager: StorageManagerType - private let grdbManager: GRDBManagerProtocol? private let currencySettings: CurrencySettings private let pushNotesManager: PushNotesManager private let eligibilityChecker: POSEntryPointEligibilityCheckerProtocol @@ -80,13 +78,6 @@ final class POSTabCoordinator { self.eligibilityChecker = eligibilityChecker tabContainerController.wrappedController = POSTabViewController() - - if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) { - self.grdbManager = ServiceLocator.grdbManager - logDatabaseSchema() - } else { - self.grdbManager = nil - } } func onTabSelected() { @@ -181,11 +172,3 @@ private extension POSTabCoordinator { TracksProvider.setPOSMode(isPointOfSaleActive) } } - -private extension POSTabCoordinator { - func logDatabaseSchema() { - try? grdbManager?.databaseConnection.read { db in - return try db.dumpSchema() - } - } -} diff --git a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift index 80fd6f1be28..2ac4e86158f 100644 --- a/WooCommerce/Classes/ViewRelated/MainTabBarController.swift +++ b/WooCommerce/Classes/ViewRelated/MainTabBarController.swift @@ -130,6 +130,7 @@ final class MainTabBarController: UITabBarController { private var posEligibilityChecker: POSEntryPointEligibilityCheckerProtocol? private var posEligibilityCheckTask: Task? + private var posCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol? private var isPOSTabVisible: Bool = false @@ -685,6 +686,11 @@ private extension MainTabBarController { cachePOSTabVisibility(siteID: siteID, isPOSTabVisible: isPOSTabVisible) updateTabViewControllers(isPOSTabVisible: isPOSTabVisible) viewModel.loadHubMenuTabBadge() + + // Trigger POS catalog sync if tab is visible and feature flag is enabled + if isPOSTabVisible, ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) { + await triggerPOSCatalogSyncIfNeeded(for: siteID) + } } } @@ -760,6 +766,11 @@ private extension MainTabBarController { eligibilityChecker: posEligibilityChecker ) + // Configure POS catalog sync coordinator for local catalog syncing + if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleLocalCatalogi1) { + posCatalogSyncCoordinator = createPOSCatalogSyncCoordinator() + } + // Configure hub menu tab coordinator once per logged in session potentially with multiple sites. if hubMenuTabCoordinator == nil { let hubTabCoordinator = createHubMenuTabCoordinator() @@ -781,6 +792,37 @@ private extension MainTabBarController { OrdersSplitViewWrapperController(siteID: siteID) } + func createPOSCatalogSyncCoordinator() -> POSCatalogSyncCoordinatorProtocol? { + guard let credentials = ServiceLocator.stores.sessionManager.defaultCredentials, + let fullSyncService = POSCatalogFullSyncService(credentials: credentials, grdbManager: ServiceLocator.grdbManager) + else { + return nil + } + + return POSCatalogSyncCoordinator(fullSyncService: fullSyncService) + } + + func triggerPOSCatalogSyncIfNeeded(for siteID: Int64) async { + guard let coordinator = posCatalogSyncCoordinator else { + return + } + + // Check if sync is needed (older than 24 hours) + let maxAge: TimeInterval = 24 * 60 * 60 + guard coordinator.shouldPerformFullSync(for: siteID, maxAge: maxAge) else { + return + } + + // Perform background sync + Task.detached { + do { + _ = try await coordinator.performFullSync(for: siteID) + } catch { + DDLogError("⚠️ POS catalog sync failed: \(error)") + } + } + } + func createHubMenuTabCoordinator() -> HubMenuCoordinator { HubMenuCoordinator(tabContainerController: hubMenuContainerController, storesManager: stores,