diff --git a/Modules/Sources/Fakes/Yosemite.generated.swift b/Modules/Sources/Fakes/Yosemite.generated.swift index a4a74615933..9f4ef148c6b 100644 --- a/Modules/Sources/Fakes/Yosemite.generated.swift +++ b/Modules/Sources/Fakes/Yosemite.generated.swift @@ -58,7 +58,8 @@ extension Yosemite.POSSite { public static func fake() -> Yosemite.POSSite { .init( siteID: .fake(), - lastIncrementalSyncDate: .fake() + lastIncrementalSyncDate: .fake(), + lastFullSyncDate: .fake() ) } } diff --git a/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift b/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift index c951cde5b67..46e179696ea 100644 --- a/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift +++ b/Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift @@ -20,6 +20,7 @@ struct V001InitialSchema { try db.create(table: "site") { siteTable in siteTable.primaryKey("id", .integer).notNull() siteTable.column("lastCatalogIncrementalSyncDate", .datetime) + siteTable.column("lastCatalogFullSyncDate", .datetime) } } diff --git a/Modules/Sources/Storage/GRDB/Model/PersistedSite.swift b/Modules/Sources/Storage/GRDB/Model/PersistedSite.swift index ee54b52e88c..c7fbb3d2062 100644 --- a/Modules/Sources/Storage/GRDB/Model/PersistedSite.swift +++ b/Modules/Sources/Storage/GRDB/Model/PersistedSite.swift @@ -6,12 +6,15 @@ public struct PersistedSite: Codable { // periphery:ignore - TODO: remove ignore when populating database public let id: Int64 // periphery:ignore - TODO: remove ignore when populating database - public let lastCatalogIncrementalSyncDate: Date? + public var lastCatalogIncrementalSyncDate: Date? + // periphery:ignore - TODO: remove ignore when populating database + public var lastCatalogFullSyncDate: Date? // periphery:ignore - TODO: remove ignore when populating database - public init(id: Int64, lastCatalogIncrementalSyncDate: Date? = nil) { + public init(id: Int64, lastCatalogIncrementalSyncDate: Date? = nil, lastCatalogFullSyncDate: Date? = nil) { self.id = id self.lastCatalogIncrementalSyncDate = lastCatalogIncrementalSyncDate + self.lastCatalogFullSyncDate = lastCatalogFullSyncDate } } @@ -21,9 +24,11 @@ extension PersistedSite: FetchableRecord, PersistableRecord { public enum Columns { // periphery:ignore - TODO: remove ignore when populating database - static let id = Column(CodingKeys.id) + public static let id = Column(CodingKeys.id) + // periphery:ignore - TODO: remove ignore when populating database + public static let lastCatalogIncrementalSyncDate = Column(CodingKeys.lastCatalogIncrementalSyncDate) // periphery:ignore - TODO: remove ignore when populating database - static let lastCatalogIncrementalSyncDate = Column(CodingKeys.lastCatalogIncrementalSyncDate) + public static let lastCatalogFullSyncDate = Column(CodingKeys.lastCatalogFullSyncDate) } } @@ -32,5 +37,6 @@ private extension PersistedSite { enum CodingKeys: String, CodingKey { case id case lastCatalogIncrementalSyncDate + case lastCatalogFullSyncDate } } diff --git a/Modules/Sources/Storage/Model/Copiable/Models+Copiable.generated.swift b/Modules/Sources/Storage/Model/Copiable/Models+Copiable.generated.swift index 34327f624f0..5bf17686464 100644 --- a/Modules/Sources/Storage/Model/Copiable/Models+Copiable.generated.swift +++ b/Modules/Sources/Storage/Model/Copiable/Models+Copiable.generated.swift @@ -115,8 +115,7 @@ extension Storage.GeneralStoreSettings { lastSelectedOrderStatus: NullableCopiableProp = .copy, favoriteProductIDs: CopiableProp<[Int64]> = .copy, searchTermsByKey: CopiableProp<[String: [String]]> = .copy, - isPOSTabVisible: NullableCopiableProp = .copy, - posLastFullSyncDate: NullableCopiableProp = .copy + isPOSTabVisible: NullableCopiableProp = .copy ) -> Storage.GeneralStoreSettings { let storeID = storeID ?? self.storeID let isTelemetryAvailable = isTelemetryAvailable ?? self.isTelemetryAvailable @@ -138,7 +137,6 @@ 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, @@ -160,8 +158,7 @@ extension Storage.GeneralStoreSettings { lastSelectedOrderStatus: lastSelectedOrderStatus, favoriteProductIDs: favoriteProductIDs, searchTermsByKey: searchTermsByKey, - isPOSTabVisible: isPOSTabVisible, - posLastFullSyncDate: posLastFullSyncDate + isPOSTabVisible: isPOSTabVisible ) } } diff --git a/Modules/Sources/Storage/Model/GeneralStoreSettings.swift b/Modules/Sources/Storage/Model/GeneralStoreSettings.swift index d0623517227..deefc73eb65 100644 --- a/Modules/Sources/Storage/Model/GeneralStoreSettings.swift +++ b/Modules/Sources/Storage/Model/GeneralStoreSettings.swift @@ -86,9 +86,6 @@ 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, @@ -109,8 +106,7 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable { lastSelectedOrderStatus: String? = nil, favoriteProductIDs: [Int64] = [], searchTermsByKey: [String: [String]] = [:], - isPOSTabVisible: Bool? = nil, - posLastFullSyncDate: Date? = nil) { + isPOSTabVisible: Bool? = nil) { self.storeID = storeID self.isTelemetryAvailable = isTelemetryAvailable self.telemetryLastReportedTime = telemetryLastReportedTime @@ -131,7 +127,6 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable { self.favoriteProductIDs = favoriteProductIDs self.searchTermsByKey = searchTermsByKey self.isPOSTabVisible = isPOSTabVisible - self.posLastFullSyncDate = posLastFullSyncDate } public func erasingSelectedTaxRateID() -> GeneralStoreSettings { @@ -153,8 +148,7 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable { lastSelectedOrderStatus: lastSelectedOrderStatus, favoriteProductIDs: favoriteProductIDs, searchTermsByKey: searchTermsByKey, - isPOSTabVisible: isPOSTabVisible, - posLastFullSyncDate: posLastFullSyncDate) + isPOSTabVisible: isPOSTabVisible) } } @@ -189,7 +183,6 @@ 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/Model/Copiable/Models+Copiable.generated.swift b/Modules/Sources/Yosemite/Model/Copiable/Models+Copiable.generated.swift index a83ba0a2084..f7639a5621f 100644 --- a/Modules/Sources/Yosemite/Model/Copiable/Models+Copiable.generated.swift +++ b/Modules/Sources/Yosemite/Model/Copiable/Models+Copiable.generated.swift @@ -96,14 +96,17 @@ extension Yosemite.POSSimpleProduct { extension Yosemite.POSSite { public func copy( siteID: CopiableProp = .copy, - lastIncrementalSyncDate: NullableCopiableProp = .copy + lastIncrementalSyncDate: NullableCopiableProp = .copy, + lastFullSyncDate: NullableCopiableProp = .copy ) -> Yosemite.POSSite { let siteID = siteID ?? self.siteID let lastIncrementalSyncDate = lastIncrementalSyncDate ?? self.lastIncrementalSyncDate + let lastFullSyncDate = lastFullSyncDate ?? self.lastFullSyncDate return Yosemite.POSSite( siteID: siteID, - lastIncrementalSyncDate: lastIncrementalSyncDate + lastIncrementalSyncDate: lastIncrementalSyncDate, + lastFullSyncDate: lastFullSyncDate ) } } diff --git a/Modules/Sources/Yosemite/Model/Storage/PersistedSite+Conversions.swift b/Modules/Sources/Yosemite/Model/Storage/PersistedSite+Conversions.swift index bb3034d94ed..80378e90466 100644 --- a/Modules/Sources/Yosemite/Model/Storage/PersistedSite+Conversions.swift +++ b/Modules/Sources/Yosemite/Model/Storage/PersistedSite+Conversions.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import Foundation import Storage @@ -5,14 +6,16 @@ extension PersistedSite { init(from posSite: POSSite) { self.init( id: posSite.siteID, - lastCatalogIncrementalSyncDate: posSite.lastIncrementalSyncDate + lastCatalogIncrementalSyncDate: posSite.lastIncrementalSyncDate, + lastCatalogFullSyncDate: posSite.lastFullSyncDate ) } func toPOSSite() -> POSSite { POSSite( siteID: id, - lastIncrementalSyncDate: lastCatalogIncrementalSyncDate + lastIncrementalSyncDate: lastCatalogIncrementalSyncDate, + lastFullSyncDate: lastCatalogFullSyncDate ) } } diff --git a/Modules/Sources/Yosemite/PointOfSale/POSSite.swift b/Modules/Sources/Yosemite/PointOfSale/POSSite.swift index 3eba4437d8c..c882cb1cfde 100644 --- a/Modules/Sources/Yosemite/PointOfSale/POSSite.swift +++ b/Modules/Sources/Yosemite/PointOfSale/POSSite.swift @@ -5,9 +5,11 @@ import Foundation public struct POSSite: Equatable, GeneratedCopiable, GeneratedFakeable { public let siteID: Int64 public let lastIncrementalSyncDate: Date? + public let lastFullSyncDate: Date? - public init(siteID: Int64, lastIncrementalSyncDate: Date? = nil) { + public init(siteID: Int64, lastIncrementalSyncDate: Date? = nil, lastFullSyncDate: Date? = nil) { self.siteID = siteID self.lastIncrementalSyncDate = lastIncrementalSyncDate + self.lastFullSyncDate = lastFullSyncDate } } diff --git a/Modules/Sources/Yosemite/Stores/Helpers/SiteSpecificAppSettingsStoreMethods.swift b/Modules/Sources/Yosemite/Stores/Helpers/SiteSpecificAppSettingsStoreMethods.swift index 976bf32141d..7fbe31065d4 100644 --- a/Modules/Sources/Yosemite/Stores/Helpers/SiteSpecificAppSettingsStoreMethods.swift +++ b/Modules/Sources/Yosemite/Stores/Helpers/SiteSpecificAppSettingsStoreMethods.swift @@ -11,10 +11,6 @@ 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 @@ -102,20 +98,6 @@ 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/POSCatalogFullSyncService.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift index e5752ebc211..462523bc997 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogFullSyncService.swift @@ -2,7 +2,6 @@ import Foundation import protocol Networking.POSCatalogSyncRemoteProtocol import class Networking.AlamofireNetwork import class Networking.POSCatalogSyncRemote -import CocoaLumberjackSwift import Storage // TODO - remove the periphery ignore comment when the catalog is integrated with POS. @@ -20,6 +19,7 @@ public protocol POSCatalogFullSyncServiceProtocol { public struct POSCatalog { public let products: [POSProduct] public let variations: [POSProductVariation] + public let syncDate: Date } // TODO - remove the periphery ignore comment when the service is integrated with POS. @@ -74,6 +74,7 @@ public final class POSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol private extension POSCatalogFullSyncService { func loadCatalog(for siteID: Int64, syncRemote: POSCatalogSyncRemoteProtocol) async throws -> POSCatalog { + let syncStartDate = Date.now // Loads products and variations in batches in parallel. async let productsTask = batchedLoader.loadAll( makeRequest: { pageNumber in @@ -87,7 +88,7 @@ private extension POSCatalogFullSyncService { ) let (products, variations) = try await (productsTask, variationsTask) - return POSCatalog(products: products, variations: variations) + return POSCatalog(products: products, variations: variations, syncDate: syncStartDate) } } diff --git a/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift index 6a21856dde9..e9d8573fd47 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift @@ -2,7 +2,6 @@ import Foundation import protocol Networking.POSCatalogSyncRemoteProtocol import class Networking.AlamofireNetwork import class Networking.POSCatalogSyncRemote -import CocoaLumberjackSwift import protocol Storage.GRDBManagerProtocol // TODO - remove the periphery ignore comment when the service is integrated with POS. @@ -12,7 +11,7 @@ public protocol POSCatalogIncrementalSyncServiceProtocol { /// - Parameters: /// - siteID: The site ID to sync catalog for. /// - lastFullSyncDate: The date of the last full sync to use if no incremental sync date exists. - func startIncrementalSync(for siteID: Int64, lastFullSyncDate: Date) async throws + func startIncrementalSync(for siteID: Int64, lastFullSyncDate: Date, lastIncrementalSyncDate: Date?) async throws } // TODO - remove the periphery ignore comment when the service is integrated with POS. @@ -43,21 +42,18 @@ public final class POSCatalogIncrementalSyncService: POSCatalogIncrementalSyncSe // MARK: - Protocol Conformance - public func startIncrementalSync(for siteID: Int64, lastFullSyncDate: Date) async throws { - let modifiedAfter = try await latestSyncDate(siteID: siteID, lastFullSyncDate: lastFullSyncDate) + public func startIncrementalSync(for siteID: Int64, lastFullSyncDate: Date, lastIncrementalSyncDate: Date?) async throws { + let modifiedAfter = latestSyncDate(fullSyncDate: lastFullSyncDate, incrementalSyncDate: lastIncrementalSyncDate) DDLogInfo("🔄 Starting incremental catalog sync for site ID: \(siteID), modifiedAfter: \(modifiedAfter)") do { - let syncStartDate = Date() let catalog = try await loadCatalog(for: siteID, modifiedAfter: modifiedAfter, syncRemote: syncRemote) DDLogInfo("✅ Loaded \(catalog.products.count) products and \(catalog.variations.count) variations for siteID \(siteID)") try await persistenceService.persistIncrementalCatalogData(catalog, siteID: siteID) DDLogInfo("✅ Persisted \(catalog.products.count) products and \(catalog.variations.count) variations to database for siteID \(siteID)") - try await persistenceService.updateSite(.init(siteID: siteID, lastIncrementalSyncDate: syncStartDate)) - DDLogInfo("✅ Updated last incremental sync date to \(syncStartDate) for siteID \(siteID)") } catch { DDLogError("❌ Failed to sync and persist catalog incrementally: \(error)") throw error @@ -69,6 +65,7 @@ public final class POSCatalogIncrementalSyncService: POSCatalogIncrementalSyncSe private extension POSCatalogIncrementalSyncService { func loadCatalog(for siteID: Int64, modifiedAfter: Date, syncRemote: POSCatalogSyncRemoteProtocol) async throws -> POSCatalog { + let syncStartDate = Date.now async let productsTask = batchedLoader.loadAll( makeRequest: { pageNumber in try await syncRemote.loadProducts(modifiedAfter: modifiedAfter, siteID: siteID, pageNumber: pageNumber) @@ -81,14 +78,14 @@ private extension POSCatalogIncrementalSyncService { ) let (products, variations) = try await (productsTask, variationsTask) - return POSCatalog(products: products, variations: variations) + return POSCatalog(products: products, variations: variations, syncDate: syncStartDate) } } // MARK: - Sync date private extension POSCatalogIncrementalSyncService { - func latestSyncDate(siteID: Int64, lastFullSyncDate: Date) async throws -> Date { - try await persistenceService.loadSite(siteID: siteID)?.lastIncrementalSyncDate ?? lastFullSyncDate + func latestSyncDate(fullSyncDate: Date, incrementalSyncDate: Date?) -> Date { + max(fullSyncDate, incrementalSyncDate ?? .distantPast) } } diff --git a/Modules/Sources/Yosemite/Tools/POS/POSCatalogPersistenceService.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogPersistenceService.swift index 28ba311e192..ad1ee334a58 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSCatalogPersistenceService.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogPersistenceService.swift @@ -3,10 +3,6 @@ import Foundation import Storage import GRDB -enum POSCatalogPersistenceError: Error, Equatable { - case siteNotFound(siteID: Int64) -} - protocol POSCatalogPersistenceServiceProtocol { /// Clears existing data and persists new catalog data /// - Parameters: @@ -19,15 +15,6 @@ protocol POSCatalogPersistenceServiceProtocol { /// - catalog: The catalog difference to persist /// - siteID: The site ID to associate the catalog with func persistIncrementalCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws - - /// Loads the POS site for the given site ID - /// - Parameter siteID: The site ID to load the POSSite for - /// - Returns: The loaded POSSite or nil if not found in storage - func loadSite(siteID: Int64) async throws -> POSSite? - - /// Updates the PersistedSite based on POSSite data - /// - Parameter site: The POSSite containing the updated data - func updateSite(_ site: POSSite) async throws } final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { @@ -45,7 +32,7 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { // currently, we can't save for more than one site as entity IDs are not namespaced. try PersistedSite.deleteAll(db) - let site = PersistedSite(id: siteID) + let site = PersistedSite(id: siteID, lastCatalogFullSyncDate: catalog.syncDate) try site.insert(db) for product in catalog.productsToPersist { @@ -132,6 +119,9 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { for var attribute in catalog.variationAttributesToPersist { try attribute.insert(db, onConflict: .replace) } + + var site = try PersistedSite.fetchOne(db, key: siteID) + try site?.updateChanges(db) { $0.lastCatalogIncrementalSyncDate = catalog.syncDate } } DDLogInfo("✅ Incremental catalog persistence complete") @@ -149,23 +139,6 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { "\(variationImageCount) variation images, \(variationAttributeCount) variation attributes") } } - - func loadSite(siteID: Int64) async throws -> POSSite? { - try await grdbManager.databaseConnection.read { db in - try PersistedSite.filter(key: siteID).fetchOne(db)?.toPOSSite() - } - } - - func updateSite(_ site: POSSite) async throws { - try await grdbManager.databaseConnection.write { db in - guard try PersistedSite.filter(key: site.siteID).fetchOne(db) != nil else { - throw POSCatalogPersistenceError.siteNotFound(siteID: site.siteID) - } - - let persistedSite = PersistedSite(from: site) - try persistedSite.update(db) - } - } } private extension POSCatalog { diff --git a/Modules/Sources/Yosemite/Tools/POS/POSCatalogSyncCoordinator.swift b/Modules/Sources/Yosemite/Tools/POS/POSCatalogSyncCoordinator.swift index be349fa078c..1a4a04b0ca6 100644 --- a/Modules/Sources/Yosemite/Tools/POS/POSCatalogSyncCoordinator.swift +++ b/Modules/Sources/Yosemite/Tools/POS/POSCatalogSyncCoordinator.swift @@ -1,3 +1,4 @@ +// periphery:ignore:all import Foundation import Storage import GRDB @@ -22,17 +23,25 @@ public enum POSCatalogSyncError: Error, Equatable { public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol { private let fullSyncService: POSCatalogFullSyncServiceProtocol - private let settingsStore: SiteSpecificAppSettingsStoreMethodsProtocol + private let persistenceService: POSCatalogPersistenceServiceProtocol private let grdbManager: GRDBManagerProtocol /// Tracks ongoing syncs by site ID to prevent duplicates private var ongoingSyncs: Set = [] public init(fullSyncService: POSCatalogFullSyncServiceProtocol, - settingsStore: SiteSpecificAppSettingsStoreMethodsProtocol? = nil, grdbManager: GRDBManagerProtocol) { self.fullSyncService = fullSyncService - self.settingsStore = settingsStore ?? SiteSpecificAppSettingsStoreMethods(fileStorage: PListFileStorage()) + self.persistenceService = POSCatalogPersistenceService(grdbManager: grdbManager) + self.grdbManager = grdbManager + } + + //periphery:ignore - used for tests to inject persistence service + init(fullSyncService: POSCatalogFullSyncServiceProtocol, + persistenceService: POSCatalogPersistenceServiceProtocol, + grdbManager: GRDBManagerProtocol) { + self.fullSyncService = fullSyncService + self.persistenceService = persistenceService self.grdbManager = grdbManager } @@ -54,8 +63,6 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol { let catalog = try await fullSyncService.startFullSync(for: siteID) - settingsStore.setPOSLastFullSyncDate(Date(), for: siteID) - DDLogInfo("✅ POSCatalogSyncCoordinator completed full sync for site \(siteID)") } @@ -65,7 +72,7 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol { return true } - guard let lastSyncDate = lastFullSyncDate(for: siteID) else { + guard let lastSyncDate = await lastFullSyncDate(for: siteID) else { DDLogInfo("📋 POSCatalogSyncCoordinator: No previous sync found for site \(siteID), sync needed") return true } @@ -84,8 +91,15 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol { // MARK: - Private - private func lastFullSyncDate(for siteID: Int64) -> Date? { - return settingsStore.getPOSLastFullSyncDate(for: siteID) + private func lastFullSyncDate(for siteID: Int64) async -> Date? { + do { + return try await grdbManager.databaseConnection.read { db in + return try PersistedSite.filter(key: siteID).fetchOne(db)?.lastCatalogFullSyncDate + } + } catch { + DDLogError("⛔️ POSCatalogSyncCoordinator: Error loading site \(siteID) for full sync date: \(error)") + return nil + } } private func siteExistsInDatabase(siteID: Int64) -> Bool { diff --git a/Modules/Tests/YosemiteTests/Mocks/MockPOSCatalogPersistenceService.swift b/Modules/Tests/YosemiteTests/Mocks/MockPOSCatalogPersistenceService.swift new file mode 100644 index 00000000000..1dcdb01760e --- /dev/null +++ b/Modules/Tests/YosemiteTests/Mocks/MockPOSCatalogPersistenceService.swift @@ -0,0 +1,24 @@ +@testable import Yosemite +import Foundation + +final class MockPOSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { + // MARK: - persistIncrementalCatalogData tracking + private(set) var persistIncrementalCatalogDataCallCount = 0 + private(set) var persistIncrementalCatalogDataLastPersistedCatalog: POSCatalog? + var persistIncrementalCatalogDataError: Error? + + // MARK: - Protocol Implementation + + func replaceAllCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws { + // Not used in current tests + } + + func persistIncrementalCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws { + persistIncrementalCatalogDataCallCount += 1 + persistIncrementalCatalogDataLastPersistedCatalog = catalog + + if let error = persistIncrementalCatalogDataError { + throw error + } + } +} diff --git a/Modules/Tests/YosemiteTests/Mocks/MockSiteSpecificAppSettingsStoreMethods.swift b/Modules/Tests/YosemiteTests/Mocks/MockSiteSpecificAppSettingsStoreMethods.swift index b1f794039de..e311398ce28 100644 --- a/Modules/Tests/YosemiteTests/Mocks/MockSiteSpecificAppSettingsStoreMethods.swift +++ b/Modules/Tests/YosemiteTests/Mocks/MockSiteSpecificAppSettingsStoreMethods.swift @@ -28,12 +28,6 @@ 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 @@ -92,15 +86,4 @@ final class MockSiteSpecificAppSettingsStoreMethods: SiteSpecificAppSettingsStor 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 9544b73ddee..164649dcab1 100644 --- a/Modules/Tests/YosemiteTests/Stores/Helpers/SiteSpecificAppSettingsStoreMethodsTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/Helpers/SiteSpecificAppSettingsStoreMethodsTests.swift @@ -230,141 +230,6 @@ struct SiteSpecificAppSettingsStoreMethodsTests { #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/POSCatalogFullSyncServiceTests.swift b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift index 5f46359bc52..1c57b8b43aa 100644 --- a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift +++ b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift @@ -169,23 +169,3 @@ struct POSCatalogFullSyncServiceTests { #expect(mockSyncRemote.loadProductVariationsCallCount == 5) } } - -// MARK: - Mock POSCatalogPersistenceService - -private final class MockPOSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { - private(set) var replaceAllCatalogDataCallCount = 0 - private(set) var lastPersistedCatalog: POSCatalog? - private(set) var lastPersistedSiteID: Int64? - - func replaceAllCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws { - replaceAllCatalogDataCallCount += 1 - lastPersistedSiteID = siteID - lastPersistedCatalog = catalog - } - - func persistIncrementalCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws {} - - func loadSite(siteID: Int64) async throws -> POSSite? { nil } - - func updateSite(_ site: POSSite) async throws {} -} diff --git a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogIncrementalSyncServiceTests.swift b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogIncrementalSyncServiceTests.swift index d2e436365e2..c7cd3cb56d0 100644 --- a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogIncrementalSyncServiceTests.swift +++ b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogIncrementalSyncServiceTests.swift @@ -28,7 +28,7 @@ struct POSCatalogIncrementalSyncServiceTests { mockSyncRemote.setIncrementalVariationResult(pageNumber: 1, result: .success(PagedItems(items: expectedVariations, hasMorePages: false, totalItems: 0))) // When - try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil) // Then #expect(mockSyncRemote.loadIncrementalProductsCallCount == 2) @@ -46,10 +46,10 @@ struct POSCatalogIncrementalSyncServiceTests { // First sync to establish incremental date. mockSyncRemote.setIncrementalProductResult(pageNumber: 1, result: .success(PagedItems(items: [], hasMorePages: false, totalItems: 0))) mockSyncRemote.setIncrementalVariationResult(pageNumber: 1, result: .success(PagedItems(items: [], hasMorePages: false, totalItems: 0))) - try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastIncrementalDate) + try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastIncrementalDate, lastIncrementalSyncDate: lastIncrementalDate) // When - try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: lastIncrementalDate) // Then #expect(mockSyncRemote.lastIncrementalProductsModifiedAfter != lastFullSyncDate) @@ -73,7 +73,7 @@ struct POSCatalogIncrementalSyncServiceTests { mockSyncRemote.setIncrementalVariationResult(pageNumber: 1, result: .success(PagedItems(items: [], hasMorePages: false, totalItems: 0))) // When - try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil) // Then #expect(mockSyncRemote.loadIncrementalProductsCallCount == 4) @@ -94,7 +94,7 @@ struct POSCatalogIncrementalSyncServiceTests { ]) // When - try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil) // Then #expect(mockSyncRemote.loadIncrementalProductVariationsCallCount == 2) @@ -114,13 +114,13 @@ struct POSCatalogIncrementalSyncServiceTests { // When/Then await #expect(throws: expectedError) { - try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil) } #expect(mockPersistenceService.persistIncrementalCatalogDataCallCount == 0) // When attempting a second sync mockSyncRemote.setIncrementalProductResult(pageNumber: 1, result: .success(PagedItems(items: [], hasMorePages: false, totalItems: 0))) - try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil) // Then it uses lastFullSyncDate since no incremental date was stored due to previous failure #expect(mockSyncRemote.lastIncrementalProductsModifiedAfter == lastFullSyncDate) @@ -138,13 +138,13 @@ struct POSCatalogIncrementalSyncServiceTests { // When/Then await #expect(throws: Error.self) { - try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil) } #expect(mockPersistenceService.persistIncrementalCatalogDataCallCount == 1) // When attempting a second sync mockPersistenceService.persistIncrementalCatalogDataError = nil // Clear the error - try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: sampleSiteID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil) // Then it uses lastFullSyncDate since no incremental date was stored due to previous persistence failure #expect(mockSyncRemote.lastIncrementalProductsModifiedAfter == lastFullSyncDate) @@ -163,44 +163,14 @@ struct POSCatalogIncrementalSyncServiceTests { mockSyncRemote.setIncrementalVariationResult(pageNumber: 1, result: .success(PagedItems(items: [], hasMorePages: false, totalItems: 0))) // When - Sync site 1 - try await sut.startIncrementalSync(for: site1ID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: site1ID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil) let site1ModifiedAfter = try #require(mockSyncRemote.lastIncrementalProductsModifiedAfter) // When - Sync site 2 - try await sut.startIncrementalSync(for: site2ID, lastFullSyncDate: lastFullSyncDate) + try await sut.startIncrementalSync(for: site2ID, lastFullSyncDate: lastFullSyncDate, lastIncrementalSyncDate: nil) let site2ModifiedAfter = try #require(mockSyncRemote.lastIncrementalProductsModifiedAfter) #expect(site1ModifiedAfter == lastFullSyncDate) #expect(site2ModifiedAfter == lastFullSyncDate) } } - -// MARK: - Mock Classes - -private final class MockPOSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol { - private(set) var persistIncrementalCatalogDataCallCount = 0 - private(set) var persistIncrementalCatalogDataLastPersistedCatalog: POSCatalog? - private(set) var persistIncrementalCatalogDataLastPersistedSiteID: Int64? - var persistIncrementalCatalogDataError: Error? - - private var storedSites: [Int64: POSSite] = [:] - - func replaceAllCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws {} - - func persistIncrementalCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws { - persistIncrementalCatalogDataCallCount += 1 - persistIncrementalCatalogDataLastPersistedSiteID = siteID - persistIncrementalCatalogDataLastPersistedCatalog = catalog - if let error = persistIncrementalCatalogDataError { - throw error - } - } - - func loadSite(siteID: Int64) async throws -> POSSite? { - storedSites[siteID] - } - - func updateSite(_ site: POSSite) async throws { - storedSites[site.siteID] = site - } -} diff --git a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogPersistenceServiceTests.swift b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogPersistenceServiceTests.swift index 1974f156b06..49b0a3788ec 100644 --- a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogPersistenceServiceTests.swift +++ b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogPersistenceServiceTests.swift @@ -29,7 +29,8 @@ struct POSCatalogPersistenceServiceTests { variations: [ POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 2, productVariationID: 1), POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 2, productVariationID: 2) - ] + ], + syncDate: .now ) // When @@ -58,7 +59,7 @@ struct POSCatalogPersistenceServiceTests { images: [ProductImage.fake().copy(imageID: 100), ProductImage.fake().copy(imageID: 101)], attributes: [ProductAttribute.fake(), ProductAttribute.fake()] ) - let catalog = POSCatalog(products: [productWithRelations], variations: []) + let catalog = POSCatalog(products: [productWithRelations], variations: [], syncDate: .now) // When try await sut.replaceAllCatalogData(catalog, siteID: sampleSiteID) @@ -82,7 +83,8 @@ struct POSCatalogPersistenceServiceTests { attributes: [ProductVariationAttribute.fake(), ProductVariationAttribute.fake()], image: ProductImage.fake().copy(imageID: 200) ) let catalog = POSCatalog(products: [POSProduct.fake().copy(siteID: sampleSiteID, productID: 15)], - variations: [variationWithRelations]) + variations: [variationWithRelations], + syncDate: .now) // When try await sut.replaceAllCatalogData(catalog, siteID: sampleSiteID) @@ -110,7 +112,7 @@ struct POSCatalogPersistenceServiceTests { productID: 2, images: [ProductImage.fake().copy(imageID: sharedImageID)] ) - let catalog = POSCatalog(products: [product1, product2], variations: []) + let catalog = POSCatalog(products: [product1, product2], variations: [], syncDate: .now) // When try await sut.replaceAllCatalogData(catalog, siteID: sampleSiteID) @@ -131,14 +133,16 @@ struct POSCatalogPersistenceServiceTests { // Given - existing data let existingCatalog = POSCatalog( products: [POSProduct.fake().copy(siteID: sampleSiteID, productID: 80)], - variations: [POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 80, productVariationID: 100)] + variations: [POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 80, productVariationID: 100)], + syncDate: .now ) try await sut.replaceAllCatalogData(existingCatalog, siteID: sampleSiteID) // When - replace with new data let newCatalog = POSCatalog( products: [POSProduct.fake().copy(siteID: sampleSiteID, productID: 180)], - variations: [POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 180, productVariationID: 200)] + variations: [POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 180, productVariationID: 200)], + syncDate: .now ) try await sut.replaceAllCatalogData(newCatalog, siteID: sampleSiteID) @@ -166,11 +170,11 @@ struct POSCatalogPersistenceServiceTests { images: [ProductImage.fake()], attributes: [ProductAttribute.fake()] ) - let existingCatalog = POSCatalog(products: [existingProduct], variations: []) + let existingCatalog = POSCatalog(products: [existingProduct], variations: [], syncDate: .now) try await sut.replaceAllCatalogData(existingCatalog, siteID: sampleSiteID) // When - replace with empty catalog - let emptyCatalog = POSCatalog(products: [], variations: []) + let emptyCatalog = POSCatalog(products: [], variations: [], syncDate: .now) try await sut.replaceAllCatalogData(emptyCatalog, siteID: sampleSiteID) // Then - all related data should be gone @@ -198,11 +202,11 @@ struct POSCatalogPersistenceServiceTests { attributes: [ProductVariationAttribute.fake()], image: ProductImage.fake().copy(imageID: 500) ) - let existingCatalog = POSCatalog(products: [parentProduct], variations: [existingVariation]) + let existingCatalog = POSCatalog(products: [parentProduct], variations: [existingVariation], syncDate: .now) try await sut.replaceAllCatalogData(existingCatalog, siteID: sampleSiteID) // When - replace with catalog containing only parent product (no variations) - let catalogWithoutVariations = POSCatalog(products: [parentProduct], variations: []) + let catalogWithoutVariations = POSCatalog(products: [parentProduct], variations: [], syncDate: .now) try await sut.replaceAllCatalogData(catalogWithoutVariations, siteID: sampleSiteID) // Then - variation and its related data should be gone @@ -223,13 +227,13 @@ struct POSCatalogPersistenceServiceTests { @Test func persistIncrementalCatalogData_inserts_new_products_when_database_is_empty() async throws { // Given - try await sut.replaceAllCatalogData(.init(products: [], variations: []), siteID: sampleSiteID) + try await sut.replaceAllCatalogData(.init(products: [], variations: [], syncDate: .now), siteID: sampleSiteID) let newProducts = [ POSProduct.fake().copy(siteID: sampleSiteID, productID: 6), POSProduct.fake().copy(siteID: sampleSiteID, productID: 2) ] - let catalog = POSCatalog(products: newProducts, variations: []) + let catalog = POSCatalog(products: newProducts, variations: [], syncDate: .now) // When try await sut.persistIncrementalCatalogData(catalog, siteID: sampleSiteID) @@ -254,7 +258,7 @@ struct POSCatalogPersistenceServiceTests { // When let updatedProduct = POSProduct.fake().copy(siteID: sampleSiteID, productID: 1, name: "New Name") - let updateCatalog = POSCatalog(products: [updatedProduct], variations: []) + let updateCatalog = POSCatalog(products: [updatedProduct], variations: [], syncDate: .now) try await sut.persistIncrementalCatalogData(updateCatalog, siteID: sampleSiteID) // Then @@ -279,7 +283,7 @@ struct POSCatalogPersistenceServiceTests { let updatedAttribute1 = attribute1.copy(options: ["Cardinal", "Blue"]) let newAttribute = ProductAttribute.fake().copy(name: "Material") let updatedProduct = POSProduct.fake().copy(siteID: sampleSiteID, productID: 1, attributes: [newAttribute, updatedAttribute1]) - let updateCatalog = POSCatalog(products: [updatedProduct], variations: []) + let updateCatalog = POSCatalog(products: [updatedProduct], variations: [], syncDate: .now) try await sut.persistIncrementalCatalogData(updateCatalog, siteID: sampleSiteID) // Then @@ -308,7 +312,7 @@ struct POSCatalogPersistenceServiceTests { let updatedImage1 = image1.copy(src: "https://example.com/image1-1.jpg") let newImage = ProductImage.fake().copy(imageID: 3, src: "https://example.com/image3.jpg") let updatedProduct = POSProduct.fake().copy(siteID: sampleSiteID, productID: 1, images: [newImage, updatedImage1]) - let updateCatalog = POSCatalog(products: [updatedProduct], variations: []) + let updateCatalog = POSCatalog(products: [updatedProduct], variations: [], syncDate: .now) try await sut.persistIncrementalCatalogData(updateCatalog, siteID: sampleSiteID) // Then @@ -332,7 +336,7 @@ struct POSCatalogPersistenceServiceTests { // When let updatedExistingProduct = POSProduct.fake().copy(siteID: sampleSiteID, productID: 1, name: "Updated Existing") let newProduct = POSProduct.fake().copy(siteID: sampleSiteID, productID: 2, name: "New Product") - let mixedCatalog = POSCatalog(products: [updatedExistingProduct, newProduct], variations: []) + let mixedCatalog = POSCatalog(products: [updatedExistingProduct, newProduct], variations: [], syncDate: .now) try await sut.persistIncrementalCatalogData(mixedCatalog, siteID: sampleSiteID) // Then @@ -350,14 +354,14 @@ struct POSCatalogPersistenceServiceTests { @Test func persistIncrementalCatalogData_inserts_new_variations_when_database_is_empty() async throws { // Given - try await sut.replaceAllCatalogData(.init(products: [], variations: []), siteID: sampleSiteID) + try await sut.replaceAllCatalogData(.init(products: [], variations: [], syncDate: .now), siteID: sampleSiteID) let parentProduct = POSProduct.fake().copy(siteID: sampleSiteID, productID: 10) let newVariations = [ POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 10, productVariationID: 6), POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 10, productVariationID: 2) ] - let catalog = POSCatalog(products: [parentProduct], variations: newVariations) + let catalog = POSCatalog(products: [parentProduct], variations: newVariations, syncDate: .now) // When try await sut.persistIncrementalCatalogData(catalog, siteID: sampleSiteID) @@ -384,7 +388,7 @@ struct POSCatalogPersistenceServiceTests { // When let updatedVariation = POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 10, productVariationID: 1, price: "15.00") - let updateCatalog = POSCatalog(products: [parentProduct], variations: [updatedVariation]) + let updateCatalog = POSCatalog(products: [parentProduct], variations: [updatedVariation], syncDate: .now) try await sut.persistIncrementalCatalogData(updateCatalog, siteID: sampleSiteID) // Then @@ -411,7 +415,7 @@ struct POSCatalogPersistenceServiceTests { let updatedAttribute1 = attribute1.copy(option: "Cardinal") let newAttribute = ProductVariationAttribute.fake().copy(name: "Material", option: "Cotton") let updatedVariation = variation.copy(attributes: [newAttribute, updatedAttribute1]) - let updateCatalog = POSCatalog(products: [parentProduct], variations: [updatedVariation]) + let updateCatalog = POSCatalog(products: [parentProduct], variations: [updatedVariation], syncDate: .now) try await sut.persistIncrementalCatalogData(updateCatalog, siteID: sampleSiteID) // Then @@ -440,7 +444,7 @@ struct POSCatalogPersistenceServiceTests { // When let updatedImage = image.copy(src: "https://example.com/variation1-updated.jpg") let updatedVariation = POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 10, productVariationID: 1, image: updatedImage) - let updateCatalog = POSCatalog(products: [parentProduct], variations: [updatedVariation]) + let updateCatalog = POSCatalog(products: [parentProduct], variations: [updatedVariation], syncDate: .now) try await sut.persistIncrementalCatalogData(updateCatalog, siteID: sampleSiteID) // Then @@ -464,7 +468,7 @@ struct POSCatalogPersistenceServiceTests { // When let updatedExistingVariation = existingVariation.copy(price: "12.00") let newVariation = POSProductVariation.fake().copy(siteID: sampleSiteID, productID: 10, productVariationID: 2, price: "8.00") - let mixedCatalog = POSCatalog(products: [parentProduct], variations: [updatedExistingVariation, newVariation]) + let mixedCatalog = POSCatalog(products: [parentProduct], variations: [updatedExistingVariation, newVariation], syncDate: .now) try await sut.persistIncrementalCatalogData(mixedCatalog, siteID: sampleSiteID) // Then @@ -480,143 +484,71 @@ struct POSCatalogPersistenceServiceTests { } } - // MARK: - Site Management Tests - - @Test func loadSite_returns_nil_when_site_does_not_exist() async throws { - // When - let result = try await sut.loadSite(siteID: 999) - - // Then - #expect(result == nil) - } - - @Test func loadSite_returns_site_when_site_exists() async throws { - // Given - let siteID: Int64 = 123 - let lastSyncDate = Date(timeIntervalSince1970: 1000) - let site = POSSite(siteID: siteID, lastIncrementalSyncDate: lastSyncDate) - try await insertSite(site) - - // When - let result = try await sut.loadSite(siteID: siteID) - - // Then - let loadedSite = try #require(result) - #expect(loadedSite.siteID == siteID) - #expect(loadedSite.lastIncrementalSyncDate == lastSyncDate) - } + // MARK: - Sync Date Tracking Tests - @Test func loadSite_returns_site_with_nil_sync_date_when_no_sync_date_stored() async throws { + @Test func replaceAllCatalogData_stores_full_sync_date() async throws { // Given - let siteID: Int64 = 456 - let site = POSSite(siteID: siteID, lastIncrementalSyncDate: nil) - try await insertSite(site) + let syncDate = Date() + let catalog = POSCatalog(products: [], variations: [], syncDate: syncDate) // When - let result = try await sut.loadSite(siteID: siteID) + try await sut.replaceAllCatalogData(catalog, siteID: sampleSiteID) // Then - let loadedSite = try #require(result) - #expect(loadedSite.siteID == siteID) - #expect(loadedSite.lastIncrementalSyncDate == nil) - } - - @Test func updateSite_throws_error_when_site_does_not_exist() async throws { - // Given - let siteID: Int64 = 789 - let lastSyncDate = Date(timeIntervalSince1970: 2000) - let site = POSSite(siteID: siteID, lastIncrementalSyncDate: lastSyncDate) - - // When/Then - await #expect(throws: POSCatalogPersistenceError.siteNotFound(siteID: siteID)) { - try await sut.updateSite(site) - } - - // And verify no site was created try await db.read { db in - let siteCount = try PersistedSite.fetchCount(db) - #expect(siteCount == 0) + let site = try PersistedSite.fetchOne(db, key: sampleSiteID) + let storedDate = site?.lastCatalogFullSyncDate + #expect(storedDate != nil) + #expect(abs(storedDate!.timeIntervalSince(syncDate)) < 1.0) // Within 1 second tolerance + #expect(site?.id == sampleSiteID) } } - @Test func updateSite_updates_existing_site() async throws { - // Given - create initial site - let siteID: Int64 = 101112 - let initialSyncDate = Date(timeIntervalSince1970: 1500) - let initialSite = POSSite(siteID: siteID, lastIncrementalSyncDate: initialSyncDate) - try await insertSite(initialSite) + @Test func persistIncrementalCatalogData_stores_incremental_sync_date() async throws { + // Given - site with existing full sync date + let fullSyncDate = Date().addingTimeInterval(-3600) // 1 hour ago + try await sut.replaceAllCatalogData(POSCatalog(products: [], variations: [], syncDate: fullSyncDate), siteID: sampleSiteID) - // When - update with new sync date - let updatedSyncDate = Date(timeIntervalSince1970: 3000) - let updatedSite = POSSite(siteID: siteID, lastIncrementalSyncDate: updatedSyncDate) - try await sut.updateSite(updatedSite) + // When - perform incremental sync + let incrementalSyncDate = Date() + let catalog = POSCatalog(products: [POSProduct.fake().copy(siteID: sampleSiteID, productID: 1)], variations: [], syncDate: incrementalSyncDate) + try await sut.persistIncrementalCatalogData(catalog, siteID: sampleSiteID) - // Then + // Then - both dates should be stored try await db.read { db in - let siteCount = try PersistedSite.fetchCount(db) - #expect(siteCount == 1) - - let persistedSite = try PersistedSite.fetchOne(db) - #expect(persistedSite?.id == siteID) - #expect(persistedSite?.lastCatalogIncrementalSyncDate == updatedSyncDate) + let site = try PersistedSite.fetchOne(db, key: sampleSiteID) + let storedFullSyncDate = site?.lastCatalogFullSyncDate + let storedIncrementalSyncDate = site?.lastCatalogIncrementalSyncDate + #expect(storedFullSyncDate != nil) + #expect(storedIncrementalSyncDate != nil) + #expect(abs(storedFullSyncDate!.timeIntervalSince(fullSyncDate)) < 1.0) // Within 1 second tolerance + #expect(abs(storedIncrementalSyncDate!.timeIntervalSince(incrementalSyncDate)) < 1.0) // Within 1 second tolerance + #expect(site?.id == sampleSiteID) } } - @Test func updateSite_can_set_sync_date_to_nil() async throws { - // Given - let siteID: Int64 = 131415 - let initialSyncDate = Date(timeIntervalSince1970: 4000) - let initialSite = POSSite(siteID: siteID, lastIncrementalSyncDate: initialSyncDate) - try await insertSite(initialSite) + @Test func replaceAllCatalogData_updates_existing_site_sync_date() async throws { + // Given - existing site with old sync date + let oldSyncDate = Date().addingTimeInterval(-7200) // 2 hours ago + try await sut.replaceAllCatalogData(POSCatalog(products: [], variations: [], syncDate: oldSyncDate), siteID: sampleSiteID) - // When - let updatedSite = POSSite(siteID: siteID, lastIncrementalSyncDate: nil) - try await sut.updateSite(updatedSite) + // When - new full sync with updated date + let newSyncDate = Date() + let catalog = POSCatalog(products: [POSProduct.fake().copy(siteID: sampleSiteID, productID: 1)], variations: [], syncDate: newSyncDate) + try await sut.replaceAllCatalogData(catalog, siteID: sampleSiteID) - // Then + // Then - sync date should be updated try await db.read { db in - let persistedSite = try PersistedSite.fetchOne(db) - #expect(persistedSite?.id == siteID) - #expect(persistedSite?.lastCatalogIncrementalSyncDate == nil) + let site = try PersistedSite.fetchOne(db, key: sampleSiteID) + let storedDate = site?.lastCatalogFullSyncDate + #expect(storedDate != nil) + #expect(abs(storedDate!.timeIntervalSince(newSyncDate)) < 1.0) // Within 1 second tolerance + #expect(site?.id == sampleSiteID) } } - - @Test func loadSite_and_updateSite_work_together_for_multiple_sites() async throws { - // Given - let site1ID: Int64 = 100 - let site2ID: Int64 = 200 - let site1Date = Date(timeIntervalSince1970: 1000) - let site2Date = Date(timeIntervalSince1970: 2000) - - let site1 = POSSite(siteID: site1ID, lastIncrementalSyncDate: site1Date) - let site2 = POSSite(siteID: site2ID, lastIncrementalSyncDate: site2Date) - - // When - try await insertSite(site1) - try await insertSite(site2) - - // Then - let loadedSite1 = try await sut.loadSite(siteID: site1ID) - let loadedSite2 = try await sut.loadSite(siteID: site2ID) - - #expect(loadedSite1?.siteID == site1ID) - #expect(loadedSite1?.lastIncrementalSyncDate == site1Date) - #expect(loadedSite2?.siteID == site2ID) - #expect(loadedSite2?.lastIncrementalSyncDate == site2Date) - - // When loading non-existent site returns nil - let nonExistentSite = try await sut.loadSite(siteID: 999) - #expect(nonExistentSite == nil) - } } private extension POSCatalogPersistenceServiceTests { - func insertSite(_ site: POSSite) async throws { - try await db.write { db in - try PersistedSite(from: site).insert(db, onConflict: .replace) - } - } - func insertProduct(_ product: POSProduct) async throws { try await db.write { db in try PersistedSite(id: sampleSiteID).insert(db, onConflict: .ignore) diff --git a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogSyncCoordinatorTests.swift b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogSyncCoordinatorTests.swift index 5a8ff2ea4b9..6e18daf25cb 100644 --- a/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogSyncCoordinatorTests.swift +++ b/Modules/Tests/YosemiteTests/Tools/POS/POSCatalogSyncCoordinatorTests.swift @@ -5,18 +5,18 @@ import Testing struct POSCatalogSyncCoordinatorTests { private let mockSyncService: MockPOSCatalogFullSyncService - private let mockSettingsStore: MockSiteSpecificAppSettingsStoreMethods + private let mockPersistenceService: MockPOSCatalogPersistenceService private let grdbManager: GRDBManager private let sut: POSCatalogSyncCoordinator private let sampleSiteID: Int64 = 134 init() throws { self.mockSyncService = MockPOSCatalogFullSyncService() - self.mockSettingsStore = MockSiteSpecificAppSettingsStoreMethods() + self.mockPersistenceService = MockPOSCatalogPersistenceService() self.grdbManager = try GRDBManager() self.sut = POSCatalogSyncCoordinator( fullSyncService: mockSyncService, - settingsStore: mockSettingsStore, + persistenceService: mockPersistenceService, grdbManager: grdbManager ) } @@ -27,7 +27,8 @@ struct POSCatalogSyncCoordinatorTests { // Given let expectedCatalog = POSCatalog( products: [POSProduct.fake()], - variations: [POSProductVariation.fake()] + variations: [POSProductVariation.fake()], + syncDate: .now ) mockSyncService.startFullSyncResult = .success(expectedCatalog) @@ -39,26 +40,6 @@ struct POSCatalogSyncCoordinatorTests { #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"]) @@ -68,17 +49,13 @@ struct POSCatalogSyncCoordinatorTests { 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_site_is_not_in_database_with_no_sync_history() async { - // Given - site doesn't exist in database AND has no sync history - mockSettingsStore.storedDates = [:] - // Note: NOT creating site in database + // Given - site does not exist in database + // Note: not creating site in database so it won't exist // When let shouldSync = await sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 60 * 60) @@ -87,25 +64,21 @@ struct POSCatalogSyncCoordinatorTests { #expect(shouldSync == true) } - @Test func shouldPerformFullSync_returns_true_when_site_is_in_database_with_no_previous_sync() async throws { - // Given - no previous sync date stored, but site exists in database - // This is much less likely to happen, but could help at a migration point - mockSettingsStore.storedDates = [:] - try createSiteInDatabase(siteID: sampleSiteID) + @Test func shouldPerformFullSync_returns_true_when_site_has_no_previous_sync() async throws { + // Given - site exists in database but has no previous sync date + try createSiteInDatabase(siteID: sampleSiteID, lastFullSyncDate: nil) // When let shouldSync = await sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 3600) // Then #expect(shouldSync == true) - #expect(mockSettingsStore.getPOSLastFullSyncDateCallCount == 1) } @Test func shouldPerformFullSync_returns_true_when_sync_is_stale() async throws { - // Given - previous sync was 2 hours ago, and site exists in database + // Given - previous sync was 2 hours ago let twoHoursAgo = Date().addingTimeInterval(-2 * 60 * 60) - mockSettingsStore.storedDates[sampleSiteID] = twoHoursAgo - try createSiteInDatabase(siteID: sampleSiteID) + try createSiteInDatabase(siteID: sampleSiteID, lastFullSyncDate: twoHoursAgo) // When - max age is 1 hour let shouldSync = await sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 60 * 60) @@ -115,10 +88,9 @@ struct POSCatalogSyncCoordinatorTests { } @Test func shouldPerformFullSync_returns_false_when_sync_is_fresh() async throws { - // Given - previous sync was 30 minutes ago, and site exists in database + // Given - previous sync was 30 minutes ago let thirtyMinutesAgo = Date().addingTimeInterval(-30 * 60) - mockSettingsStore.storedDates[sampleSiteID] = thirtyMinutesAgo - try createSiteInDatabase(siteID: sampleSiteID) + try createSiteInDatabase(siteID: sampleSiteID, lastFullSyncDate: thirtyMinutesAgo) // When - max age is 1 hour let shouldSync = await sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 60 * 60) @@ -133,12 +105,9 @@ struct POSCatalogSyncCoordinatorTests { let siteB: Int64 = 456 let oneHourAgo = Date().addingTimeInterval(-60 * 60) - mockSettingsStore.storedDates[siteA] = oneHourAgo // Has previous sync - // siteB has no previous sync - - // Create both sites in database to test timing logic - try createSiteInDatabase(siteID: siteA) - try createSiteInDatabase(siteID: siteB) + // Create sites with different sync states + try createSiteInDatabase(siteID: siteA, lastFullSyncDate: oneHourAgo) + try createSiteInDatabase(siteID: siteB, lastFullSyncDate: nil) // When let shouldSyncA = await sut.shouldPerformFullSync(for: siteA, maxAge: 2 * 60 * 60) // 2 hours @@ -150,10 +119,9 @@ struct POSCatalogSyncCoordinatorTests { } @Test func shouldPerformFullSync_with_zero_maxAge_always_returns_true() async throws { - // Given - previous sync was just now, and site exists in database + // Given - previous sync was just now let justNow = Date() - mockSettingsStore.storedDates[sampleSiteID] = justNow - try createSiteInDatabase(siteID: sampleSiteID) + try createSiteInDatabase(siteID: sampleSiteID, lastFullSyncDate: justNow) // When - max age is 0 (always sync) let shouldSync = await sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 0) @@ -162,39 +130,11 @@ struct POSCatalogSyncCoordinatorTests { #expect(shouldSync == true) } - // MARK: - Database Check Tests - - @Test func shouldPerformFullSync_returns_true_when_site_not_in_database() async { - // Given - site does not exist in database, but has recent sync date - let recentSyncDate = Date().addingTimeInterval(-30 * 60) // 30 minutes ago - mockSettingsStore.storedDates[sampleSiteID] = recentSyncDate - // Note: not creating site in database so it won't exist - - // When - max age is 1 hour (normally wouldn't sync) - let shouldSync = await sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 60 * 60) - - // Then - should sync because site doesn't exist in database - #expect(shouldSync == true) - } - - @Test func shouldPerformFullSync_respects_time_when_site_exists_in_database() async throws { - // Given - site exists in database with recent sync date - let recentSyncDate = Date().addingTimeInterval(-30 * 60) // 30 minutes ago - mockSettingsStore.storedDates[sampleSiteID] = recentSyncDate - try createSiteInDatabase(siteID: sampleSiteID) - - // When - max age is 1 hour - let shouldSync = await sut.shouldPerformFullSync(for: sampleSiteID, maxAge: 60 * 60) - - // Then - should not sync because site exists and time hasn't passed - #expect(shouldSync == false) - } - // MARK: - Sync Tracking Tests @Test func performFullSync_throws_error_when_sync_already_in_progress() async throws { // Given - block the sync service so first sync will wait - let expectedCatalog = POSCatalog(products: [], variations: []) + let expectedCatalog = POSCatalog(products: [], variations: [], syncDate: .now) mockSyncService.startFullSyncResult = .success(expectedCatalog) mockSyncService.blockNextSync() @@ -224,7 +164,7 @@ struct POSCatalogSyncCoordinatorTests { // Given let siteA: Int64 = 123 let siteB: Int64 = 456 - let expectedCatalog = POSCatalog(products: [], variations: []) + let expectedCatalog = POSCatalog(products: [], variations: [], syncDate: .now) mockSyncService.startFullSyncResult = .success(expectedCatalog) // When - start syncs for different sites concurrently @@ -251,16 +191,16 @@ struct POSCatalogSyncCoordinatorTests { } // Then - subsequent sync should be allowed - mockSyncService.startFullSyncResult = .success(POSCatalog(products: [], variations: [])) + mockSyncService.startFullSyncResult = .success(POSCatalog(products: [], variations: [], syncDate: .now)) try await sut.performFullSync(for: sampleSiteID) } // MARK: - Helper Methods - private func createSiteInDatabase(siteID: Int64) throws { + private func createSiteInDatabase(siteID: Int64, lastFullSyncDate: Date? = nil) throws { try grdbManager.databaseConnection.write { db in - let site = PersistedSite(id: siteID) + let site = PersistedSite(id: siteID, lastCatalogFullSyncDate: lastFullSyncDate) try site.insert(db) } } @@ -269,7 +209,7 @@ struct POSCatalogSyncCoordinatorTests { // MARK: - Mock Services final class MockPOSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol { - var startFullSyncResult: Result = .success(POSCatalog(products: [], variations: [])) + var startFullSyncResult: Result = .success(POSCatalog(products: [], variations: [], syncDate: .now)) var syncDelay: UInt64 = 0 // nanoseconds to delay before returning // Controlled sync mechanism