Skip to content

Commit 5182fcd

Browse files
authored
[POS][Local Catalog] Add top level catalog sync coordinator (#16098)
2 parents ed08dcd + ffe3aeb commit 5182fcd

File tree

9 files changed

+453
-21
lines changed

9 files changed

+453
-21
lines changed

Modules/Sources/Storage/Model/Copiable/Models+Copiable.generated.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ extension Storage.GeneralStoreSettings {
115115
lastSelectedOrderStatus: NullableCopiableProp<String> = .copy,
116116
favoriteProductIDs: CopiableProp<[Int64]> = .copy,
117117
searchTermsByKey: CopiableProp<[String: [String]]> = .copy,
118-
isPOSTabVisible: NullableCopiableProp<Bool> = .copy
118+
isPOSTabVisible: NullableCopiableProp<Bool> = .copy,
119+
posLastFullSyncDate: NullableCopiableProp<Date> = .copy
119120
) -> Storage.GeneralStoreSettings {
120121
let storeID = storeID ?? self.storeID
121122
let isTelemetryAvailable = isTelemetryAvailable ?? self.isTelemetryAvailable
@@ -137,6 +138,7 @@ extension Storage.GeneralStoreSettings {
137138
let favoriteProductIDs = favoriteProductIDs ?? self.favoriteProductIDs
138139
let searchTermsByKey = searchTermsByKey ?? self.searchTermsByKey
139140
let isPOSTabVisible = isPOSTabVisible ?? self.isPOSTabVisible
141+
let posLastFullSyncDate = posLastFullSyncDate ?? self.posLastFullSyncDate
140142

141143
return Storage.GeneralStoreSettings(
142144
storeID: storeID,
@@ -158,7 +160,8 @@ extension Storage.GeneralStoreSettings {
158160
lastSelectedOrderStatus: lastSelectedOrderStatus,
159161
favoriteProductIDs: favoriteProductIDs,
160162
searchTermsByKey: searchTermsByKey,
161-
isPOSTabVisible: isPOSTabVisible
163+
isPOSTabVisible: isPOSTabVisible,
164+
posLastFullSyncDate: posLastFullSyncDate
162165
)
163166
}
164167
}

Modules/Sources/Storage/Model/GeneralStoreSettings.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
8686
///
8787
public var isPOSTabVisible: Bool?
8888

89+
/// Last time a POS catalog full sync was completed for this store.
90+
///
91+
public var posLastFullSyncDate: Date?
92+
8993
public init(storeID: String? = nil,
9094
isTelemetryAvailable: Bool = false,
9195
telemetryLastReportedTime: Date? = nil,
@@ -105,7 +109,8 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
105109
lastSelectedOrderStatus: String? = nil,
106110
favoriteProductIDs: [Int64] = [],
107111
searchTermsByKey: [String: [String]] = [:],
108-
isPOSTabVisible: Bool? = nil) {
112+
isPOSTabVisible: Bool? = nil,
113+
posLastFullSyncDate: Date? = nil) {
109114
self.storeID = storeID
110115
self.isTelemetryAvailable = isTelemetryAvailable
111116
self.telemetryLastReportedTime = telemetryLastReportedTime
@@ -126,6 +131,7 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
126131
self.favoriteProductIDs = favoriteProductIDs
127132
self.searchTermsByKey = searchTermsByKey
128133
self.isPOSTabVisible = isPOSTabVisible
134+
self.posLastFullSyncDate = posLastFullSyncDate
129135
}
130136

131137
public func erasingSelectedTaxRateID() -> GeneralStoreSettings {
@@ -147,7 +153,8 @@ public struct GeneralStoreSettings: Codable, Equatable, GeneratedCopiable {
147153
lastSelectedOrderStatus: lastSelectedOrderStatus,
148154
favoriteProductIDs: favoriteProductIDs,
149155
searchTermsByKey: searchTermsByKey,
150-
isPOSTabVisible: isPOSTabVisible)
156+
isPOSTabVisible: isPOSTabVisible,
157+
posLastFullSyncDate: posLastFullSyncDate)
151158
}
152159
}
153160

@@ -182,6 +189,7 @@ extension GeneralStoreSettings {
182189
self.searchTermsByKey = try container.decodeIfPresent([String: [String]].self, forKey: .searchTermsByKey) ?? [:]
183190

184191
self.isPOSTabVisible = try container.decodeIfPresent(Bool.self, forKey: .isPOSTabVisible)
192+
self.posLastFullSyncDate = try container.decodeIfPresent(Date.self, forKey: .posLastFullSyncDate)
185193

186194
// Decode new properties with `decodeIfPresent` and provide a default value if necessary.
187195
}

Modules/Sources/Yosemite/Stores/Helpers/SiteSpecificAppSettingsStoreMethods.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ public protocol SiteSpecificAppSettingsStoreMethodsProtocol {
1111
// Search history methods
1212
func getSearchTerms(for itemType: POSItemType, siteID: Int64) -> [String]
1313
func setSearchTerms(_ terms: [String], for itemType: POSItemType, siteID: Int64)
14+
15+
// POS catalog sync timestamp methods
16+
func getPOSLastFullSyncDate(for siteID: Int64) -> Date?
17+
func setPOSLastFullSyncDate(_ date: Date?, for siteID: Int64)
1418
}
1519

1620
/// Methods for managing site-specific app settings
@@ -98,6 +102,20 @@ extension SiteSpecificAppSettingsStoreMethods {
98102
}
99103
}
100104

105+
// MARK: - POS Catalog Sync Timestamps
106+
extension SiteSpecificAppSettingsStoreMethods {
107+
func getPOSLastFullSyncDate(for siteID: Int64) -> Date? {
108+
let storeSettings = getStoreSettings(for: siteID)
109+
return storeSettings.posLastFullSyncDate
110+
}
111+
112+
func setPOSLastFullSyncDate(_ date: Date?, for siteID: Int64) {
113+
let storeSettings = getStoreSettings(for: siteID)
114+
let updatedSettings = storeSettings.copy(posLastFullSyncDate: date)
115+
setStoreSettings(settings: updatedSettings, for: siteID)
116+
}
117+
}
118+
101119
// MARK: - Constants
102120
private enum Constants {
103121
static let generalStoreSettingsFileName = "general-store-settings.plist"
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Foundation
2+
import Storage
3+
4+
public protocol POSCatalogSyncCoordinatorProtocol {
5+
/// Performs a full catalog sync for the specified site
6+
/// - Parameter siteID: The site ID to sync catalog for
7+
func performFullSync(for siteID: Int64) async throws
8+
9+
/// Determines if a full sync should be performed based on the age of the last sync
10+
/// - Parameters:
11+
/// - siteID: The site ID to check
12+
/// - maxAge: Maximum age before a sync is considered stale
13+
/// - Returns: True if a sync should be performed
14+
func shouldPerformFullSync(for siteID: Int64, maxAge: TimeInterval) -> Bool
15+
}
16+
17+
public final class POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
18+
private let fullSyncService: POSCatalogFullSyncServiceProtocol
19+
private let settingsStore: SiteSpecificAppSettingsStoreMethodsProtocol
20+
21+
public init(fullSyncService: POSCatalogFullSyncServiceProtocol,
22+
settingsStore: SiteSpecificAppSettingsStoreMethodsProtocol? = nil) {
23+
self.fullSyncService = fullSyncService
24+
self.settingsStore = settingsStore ?? SiteSpecificAppSettingsStoreMethods(fileStorage: PListFileStorage())
25+
}
26+
27+
public func performFullSync(for siteID: Int64) async throws {
28+
DDLogInfo("🔄 POSCatalogSyncCoordinator starting full sync for site \(siteID)")
29+
30+
let catalog = try await fullSyncService.startFullSync(for: siteID)
31+
32+
// Record the sync timestamp
33+
settingsStore.setPOSLastFullSyncDate(Date(), for: siteID)
34+
35+
DDLogInfo("✅ POSCatalogSyncCoordinator completed full sync for site \(siteID)")
36+
}
37+
38+
public func shouldPerformFullSync(for siteID: Int64, maxAge: TimeInterval) -> Bool {
39+
guard let lastSyncDate = lastFullSyncDate(for: siteID) else {
40+
DDLogInfo("📋 POSCatalogSyncCoordinator: No previous sync found for site \(siteID), sync needed")
41+
return true
42+
}
43+
44+
let age = Date().timeIntervalSince(lastSyncDate)
45+
let shouldSync = age > maxAge
46+
47+
if shouldSync {
48+
DDLogInfo("📋 POSCatalogSyncCoordinator: Last sync for site \(siteID) was \(Int(age))s ago (max: \(Int(maxAge))s), sync needed")
49+
} else {
50+
DDLogInfo("📋 POSCatalogSyncCoordinator: Last sync for site \(siteID) was \(Int(age))s ago (max: \(Int(maxAge))s), sync not needed")
51+
}
52+
53+
return shouldSync
54+
}
55+
56+
// MARK: - Private
57+
58+
private func lastFullSyncDate(for siteID: Int64) -> Date? {
59+
return settingsStore.getPOSLastFullSyncDate(for: siteID)
60+
}
61+
}

Modules/Tests/YosemiteTests/Mocks/MockSiteSpecificAppSettingsStoreMethods.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@testable import Yosemite
2+
import Foundation
23
import Storage
34

45
final class MockSiteSpecificAppSettingsStoreMethods: SiteSpecificAppSettingsStoreMethodsProtocol {
@@ -27,6 +28,13 @@ final class MockSiteSpecificAppSettingsStoreMethods: SiteSpecificAppSettingsStor
2728
var spySetSearchTermsSiteID: Int64?
2829
var mockSearchTerms: [POSItemType: [String]] = [:]
2930

31+
// POS sync timestamp properties
32+
var storedDates: [Int64: Date] = [:]
33+
private(set) var getPOSLastFullSyncDateCallCount = 0
34+
private(set) var setPOSLastFullSyncDateCallCount = 0
35+
private(set) var lastSetSiteID: Int64?
36+
private(set) var lastSetDate: Date?
37+
3038
func getStoreSettings(for siteID: Int64) -> GeneralStoreSettings {
3139
getStoreSettingsCalled = true
3240
return storeSettings
@@ -83,4 +91,16 @@ final class MockSiteSpecificAppSettingsStoreMethods: SiteSpecificAppSettingsStor
8391
spySetSearchTermsSiteID = siteID
8492
mockSearchTerms[itemType] = terms
8593
}
94+
95+
func getPOSLastFullSyncDate(for siteID: Int64) -> Date? {
96+
getPOSLastFullSyncDateCallCount += 1
97+
return storedDates[siteID]
98+
}
99+
100+
func setPOSLastFullSyncDate(_ date: Date?, for siteID: Int64) {
101+
setPOSLastFullSyncDateCallCount += 1
102+
lastSetSiteID = siteID
103+
lastSetDate = date
104+
storedDates[siteID] = date
105+
}
86106
}

Modules/Tests/YosemiteTests/Stores/Helpers/SiteSpecificAppSettingsStoreMethodsTests.swift

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,142 @@ struct SiteSpecificAppSettingsStoreMethodsTests {
229229
#expect(retrievedVariationTerms == variationTerms)
230230
#expect(retrievedCouponTerms == couponTerms)
231231
}
232+
233+
// MARK: - POS Last Full Sync Date Tests
234+
235+
@Test func getPOSLastFullSyncDate_returns_nil_when_no_date_exists() {
236+
// When
237+
let syncDate = sut.getPOSLastFullSyncDate(for: siteID)
238+
239+
// Then
240+
#expect(syncDate == nil)
241+
}
242+
243+
@Test func getPOSLastFullSyncDate_returns_saved_date() throws {
244+
// Given
245+
let expectedDate = Date()
246+
let storeSettings = GeneralStoreSettings(posLastFullSyncDate: expectedDate)
247+
let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: storeSettings])
248+
try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
249+
250+
// When
251+
let syncDate = sut.getPOSLastFullSyncDate(for: siteID)
252+
253+
// Then
254+
#expect(syncDate == expectedDate)
255+
}
256+
257+
@Test func setPOSLastFullSyncDate_saves_date_successfully() throws {
258+
// Given
259+
let dateToSave = Date()
260+
let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: GeneralStoreSettings()])
261+
try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
262+
263+
// When
264+
sut.setPOSLastFullSyncDate(dateToSave, for: siteID)
265+
266+
// Then
267+
let savedData: GeneralStoreSettingsBySite = try fileStorage.data(for: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
268+
#expect(savedData.storeSettingsBySite[siteID]?.posLastFullSyncDate == dateToSave)
269+
}
270+
271+
@Test func setPOSLastFullSyncDate_can_set_nil_date() throws {
272+
// Given
273+
let existingDate = Date()
274+
let storeSettings = GeneralStoreSettings(posLastFullSyncDate: existingDate)
275+
let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: storeSettings])
276+
try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
277+
278+
// When
279+
sut.setPOSLastFullSyncDate(nil, for: siteID)
280+
281+
// Then
282+
let savedData: GeneralStoreSettingsBySite = try fileStorage.data(for: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
283+
#expect(savedData.storeSettingsBySite[siteID]?.posLastFullSyncDate == nil)
284+
}
285+
286+
@Test func setPOSLastFullSyncDate_preserves_other_settings() throws {
287+
// Given
288+
let existingStoreID = "existing-store"
289+
let existingTerms = ["existing", "terms"]
290+
let storeSettings = GeneralStoreSettings(
291+
storeID: existingStoreID,
292+
searchTermsByKey: ["product_search_terms": existingTerms]
293+
)
294+
let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: storeSettings])
295+
try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
296+
297+
let dateToSave = Date()
298+
299+
// When
300+
sut.setPOSLastFullSyncDate(dateToSave, for: siteID)
301+
302+
// Then
303+
let savedData: GeneralStoreSettingsBySite = try fileStorage.data(for: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
304+
let savedSettings = savedData.storeSettingsBySite[siteID]
305+
#expect(savedSettings?.posLastFullSyncDate == dateToSave)
306+
#expect(savedSettings?.storeID == existingStoreID)
307+
#expect(savedSettings?.searchTermsByKey["product_search_terms"] == existingTerms)
308+
}
309+
310+
@Test func setPOSLastFullSyncDate_preserves_dates_for_other_sites() throws {
311+
// Given
312+
let otherSiteID: Int64 = 456
313+
let otherSiteDate = Date().addingTimeInterval(-3600)
314+
let otherSiteSettings = GeneralStoreSettings(posLastFullSyncDate: otherSiteDate)
315+
let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [otherSiteID: otherSiteSettings])
316+
try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
317+
318+
let newDate = Date()
319+
320+
// When
321+
sut.setPOSLastFullSyncDate(newDate, for: siteID)
322+
323+
// Then
324+
let savedData: GeneralStoreSettingsBySite = try fileStorage.data(for: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
325+
#expect(savedData.storeSettingsBySite[siteID]?.posLastFullSyncDate == newDate)
326+
#expect(savedData.storeSettingsBySite[otherSiteID]?.posLastFullSyncDate == otherSiteDate)
327+
}
328+
329+
@Test func getPOSLastFullSyncDate_handles_different_sites_independently() throws {
330+
// Given
331+
let siteA: Int64 = 123
332+
let siteB: Int64 = 456
333+
let dateA = Date()
334+
let dateB = Date().addingTimeInterval(-3600)
335+
336+
let storeSettingsA = GeneralStoreSettings(posLastFullSyncDate: dateA)
337+
let storeSettingsB = GeneralStoreSettings(posLastFullSyncDate: dateB)
338+
let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [
339+
siteA: storeSettingsA,
340+
siteB: storeSettingsB
341+
])
342+
try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
343+
344+
// When
345+
let retrievedDateA = sut.getPOSLastFullSyncDate(for: siteA)
346+
let retrievedDateB = sut.getPOSLastFullSyncDate(for: siteB)
347+
348+
// Then
349+
#expect(retrievedDateA == dateA)
350+
#expect(retrievedDateB == dateB)
351+
}
352+
353+
@Test func resetStoreSettings_clears_pos_sync_date() throws {
354+
// Given
355+
let syncDate = Date()
356+
let storeSettings = GeneralStoreSettings(posLastFullSyncDate: syncDate)
357+
let existingData = GeneralStoreSettingsBySite(storeSettingsBySite: [siteID: storeSettings])
358+
try fileStorage.write(existingData, to: SiteSpecificAppSettingsStoreMethods.defaultGeneralStoreSettingsFileURL)
359+
360+
// When
361+
sut.resetStoreSettings()
362+
363+
// Then
364+
#expect(fileStorage.deleteIsHit == true)
365+
let retrievedDate = sut.getPOSLastFullSyncDate(for: siteID)
366+
#expect(retrievedDate == nil)
367+
}
232368
}
233369

234370
// MARK: - Mock FileStorage

0 commit comments

Comments
 (0)