Skip to content

Commit f5bca54

Browse files
authored
[Woo POS][Local Catalog] Trigger sync periodically in the system background (#16244)
2 parents 8eefce5 + 571f3cf commit f5bca54

File tree

17 files changed

+537
-79
lines changed

17 files changed

+537
-79
lines changed

Modules/Sources/PointOfSale/Presentation/Barcode Scanning/GameControllerBarcodeParser.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import GameController
3+
import WooFoundation
34

45
/// Parses GameController keyboard input into barcode scans.
56
/// This class handles the core logic for interpreting GameController GCKeyCode input as barcode data,

Modules/Sources/PointOfSale/Presentation/Barcode Scanning/UIKitBarcodeObserver.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22
import GameController
33
import UIKit
4+
import WooFoundation
45

56
/// An observer that processes UIKit UIPress events for barcode scanner input.
67
/// This class serves as a fallback for VoiceOver scenarios where GameController framework

Modules/Sources/PointOfSale/Utils/PreviewHelpers.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,11 @@ final class POSPreviewCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol
618618
// Simulates an incremental sync operation with a 0.5 second delay.
619619
try await Task.sleep(nanoseconds: 500_000_000)
620620
}
621+
622+
func performSmartSync(for siteID: Int64, fullSyncMaxAge: TimeInterval) async throws {
623+
// Simulates a smart sync operation with a 1 second delay.
624+
try await Task.sleep(nanoseconds: 1_000_000_000)
625+
}
621626
}
622627

623628
#endif
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import Foundation
22

3-
protocol TimeProvider {
3+
public protocol TimeProvider {
44
func now() -> Date
55
func scheduleTimer(timeInterval: TimeInterval, target: Any, selector: Selector) -> Timer
66
}
77

8-
struct DefaultTimeProvider: TimeProvider {
9-
func now() -> Date {
8+
public struct DefaultTimeProvider: TimeProvider {
9+
public init() {}
10+
11+
public func now() -> Date {
1012
Date()
1113
}
1214

13-
func scheduleTimer(timeInterval: TimeInterval, target: Any, selector: Selector) -> Timer {
15+
public func scheduleTimer(timeInterval: TimeInterval, target: Any, selector: Selector) -> Timer {
1416
return Timer.scheduledTimer(timeInterval: timeInterval, target: target, selector: selector, userInfo: nil, repeats: false)
1517
}
1618
}

Modules/Sources/Yosemite/Tools/POS/POSCatalogIncrementalSyncService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ public final class POSCatalogIncrementalSyncService: POSCatalogIncrementalSyncSe
6060

6161
do {
6262
let catalog = try await loadCatalog(for: siteID, modifiedAfter: modifiedAfter, syncRemote: syncRemote)
63-
DDLogInfo("✅ Loaded \(catalog.products.count) products and \(catalog.variations.count) variations for siteID \(siteID)")
63+
DDLogInfo("✅ Loaded \(catalog.products.count) updated products and \(catalog.variations.count) updated variations for siteID \(siteID)")
6464

6565
try await persistenceService.persistIncrementalCatalogData(catalog, siteID: siteID)
66-
DDLogInfo("✅ Persisted \(catalog.products.count) products and \(catalog.variations.count) variations to database for siteID \(siteID)")
66+
DDLogInfo("✅ Persisted \(catalog.products.count) updated products and \(catalog.variations.count) updated variations to database for siteID \(siteID)")
6767

6868
} catch {
6969
DDLogError("❌ Failed to sync and persist catalog incrementally: \(error)")

Modules/Sources/Yosemite/Tools/POS/POSCatalogPersistenceService.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol {
6868
DDLogInfo("✅ Catalog persistence complete")
6969

7070
try await grdbManager.databaseConnection.read { db in
71-
let productCount = try PersistedProduct.fetchCount(db)
72-
let productImageCount = try PersistedProductImage.fetchCount(db)
73-
let productAttributeCount = try PersistedProductAttribute.fetchCount(db)
74-
let variationCount = try PersistedProductVariation.fetchCount(db)
75-
let variationImageCount = try PersistedProductVariationImage.fetchCount(db)
76-
let variationAttributeCount = try PersistedProductVariationAttribute.fetchCount(db)
71+
let productCount = try PersistedProduct.filter { $0.siteID == siteID }.fetchCount(db)
72+
let productImageCount = try PersistedProductImage.filter { $0.siteID == siteID }.fetchCount(db)
73+
let productAttributeCount = try PersistedProductAttribute.filter { $0.siteID == siteID }.fetchCount(db)
74+
let variationCount = try PersistedProductVariation.filter { $0.siteID == siteID }.fetchCount(db)
75+
let variationImageCount = try PersistedProductVariationImage.filter { $0.siteID == siteID }.fetchCount(db)
76+
let variationAttributeCount = try PersistedProductVariationAttribute.filter { $0.siteID == siteID }.fetchCount(db)
7777

7878
DDLogInfo("Persisted \(productCount) products, \(productImageCount) product images, " +
7979
"\(productAttributeCount) product attributes, \(variationCount) variations, " +
@@ -82,7 +82,7 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol {
8282
}
8383

8484
func persistIncrementalCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws {
85-
DDLogInfo("💾 Persisting incremental catalog with \(catalog.products.count) products and \(catalog.variations.count) variations")
85+
DDLogInfo("💾 Persisting incremental catalog with \(catalog.products.count) updated products and \(catalog.variations.count) updated variations")
8686

8787
try await grdbManager.databaseConnection.write { db in
8888
for product in catalog.productsToPersist {
@@ -140,12 +140,12 @@ final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol {
140140
DDLogInfo("✅ Incremental catalog persistence complete")
141141

142142
try await grdbManager.databaseConnection.read { db in
143-
let productCount = try PersistedProduct.fetchCount(db)
144-
let productImageCount = try PersistedProductImage.fetchCount(db)
145-
let productAttributeCount = try PersistedProductAttribute.fetchCount(db)
146-
let variationCount = try PersistedProductVariation.fetchCount(db)
147-
let variationImageCount = try PersistedProductVariationImage.fetchCount(db)
148-
let variationAttributeCount = try PersistedProductVariationAttribute.fetchCount(db)
143+
let productCount = try PersistedProduct.filter { $0.siteID == siteID }.fetchCount(db)
144+
let productImageCount = try PersistedProductImage.filter { $0.siteID == siteID }.fetchCount(db)
145+
let productAttributeCount = try PersistedProductAttribute.filter { $0.siteID == siteID }.fetchCount(db)
146+
let variationCount = try PersistedProductVariation.filter { $0.siteID == siteID }.fetchCount(db)
147+
let variationImageCount = try PersistedProductVariationImage.filter { $0.siteID == siteID }.fetchCount(db)
148+
let variationAttributeCount = try PersistedProductVariationAttribute.filter { $0.siteID == siteID }.fetchCount(db)
149149

150150
DDLogInfo("Total after incremental update: \(productCount) products, \(productImageCount) product images, " +
151151
"\(productAttributeCount) product attributes, \(variationCount) variations, " +

Modules/Sources/Yosemite/Tools/POS/POSCatalogSyncCoordinator.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ public protocol POSCatalogSyncCoordinatorProtocol {
1818
/// - Throws: POSCatalogSyncError.syncAlreadyInProgress if a sync is already running for this site
1919
//periphery:ignore - remove ignore comment when incremental sync is integrated with POS
2020
func performIncrementalSyncIfApplicable(for siteID: Int64, maxAge: TimeInterval) async throws
21+
22+
/// Performs a smart sync that decides between full and incremental sync based on the last full sync date
23+
/// - Parameters:
24+
/// - siteID: The site ID to sync catalog for
25+
/// - fullSyncMaxAge: Maximum age before a full sync is triggered. If the last full sync is older than this,
26+
/// performs full sync; otherwise, performs incremental sync
27+
/// - Throws: POSCatalogSyncError.syncAlreadyInProgress if a sync is already running for this site
28+
func performSmartSync(for siteID: Int64, fullSyncMaxAge: TimeInterval) async throws
2129
}
2230

2331
public extension POSCatalogSyncCoordinatorProtocol {
@@ -28,6 +36,12 @@ public extension POSCatalogSyncCoordinatorProtocol {
2836
func performIncrementalSync(for siteID: Int64) async throws {
2937
try await performIncrementalSyncIfApplicable(for: siteID, maxAge: .zero)
3038
}
39+
40+
/// Performs a smart sync with a default 24-hour threshold for full sync
41+
func performSmartSync(for siteID: Int64) async throws {
42+
let twentyFourHours: TimeInterval = 24 * 60 * 60
43+
try await performSmartSync(for: siteID, fullSyncMaxAge: twentyFourHours)
44+
}
3145
}
3246

3347
public enum POSCatalogSyncError: Error, Equatable {
@@ -88,6 +102,19 @@ public actor POSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
88102
DDLogInfo("✅ POSCatalogSyncCoordinator completed full sync for site \(siteID)")
89103
}
90104

105+
public func performSmartSync(for siteID: Int64, fullSyncMaxAge: TimeInterval) async throws {
106+
let lastFullSync = await lastFullSyncDate(for: siteID) ?? Date(timeIntervalSince1970: 0)
107+
let lastFullSyncUTC = ISO8601DateFormatter().string(from: lastFullSync)
108+
109+
if Date().timeIntervalSince(lastFullSync) >= fullSyncMaxAge {
110+
DDLogInfo("🔄 POSCatalogSyncCoordinator: Performing full sync for site \(siteID) (last full sync: \(lastFullSyncUTC) UTC)")
111+
try await performFullSync(for: siteID)
112+
} else {
113+
DDLogInfo("🔄 POSCatalogSyncCoordinator: Performing incremental sync for site \(siteID) (last full sync: \(lastFullSyncUTC) UTC)")
114+
try await performIncrementalSync(for: siteID)
115+
}
116+
}
117+
91118
/// Determines if a full sync should be performed based on the age of the last sync
92119
/// - Parameters:
93120
/// - siteID: The site ID to check

Modules/Tests/PointOfSaleTests/Mocks/MockPOSCatalogSyncCoordinator.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ final class MockPOSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
55
var performFullSyncInvocationCount = 0
66
var performFullSyncSiteID: Int64?
77
var performFullSyncResult: Result<Void, Error> = .success(())
8+
var lastSyncDate: Date?
9+
10+
var performSmartSyncInvocationCount = 0
11+
var performSmartSyncSiteID: Int64?
12+
var performSmartSyncFullSyncMaxAge: TimeInterval?
13+
var performSmartSyncResult: Result<Void, Error> = .success(())
814

915
var onPerformFullSyncCalled: (() -> Void)?
1016

@@ -23,4 +29,17 @@ final class MockPOSCatalogSyncCoordinator: POSCatalogSyncCoordinatorProtocol {
2329
}
2430

2531
func performIncrementalSyncIfApplicable(for siteID: Int64, maxAge: TimeInterval) async throws {}
32+
33+
func performSmartSync(for siteID: Int64, fullSyncMaxAge: TimeInterval) async throws {
34+
performSmartSyncInvocationCount += 1
35+
performSmartSyncSiteID = siteID
36+
performSmartSyncFullSyncMaxAge = fullSyncMaxAge
37+
38+
switch performSmartSyncResult {
39+
case .success:
40+
return
41+
case .failure(let error):
42+
throw error
43+
}
44+
}
2645
}

Modules/Tests/PointOfSaleTests/Presentation/Barcode Scanning/GameControllerBarcodeParserTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Testing
22
import GameController
33
@testable import PointOfSale
4+
import WooFoundation
45

56
struct GameControllerBarcodeParserTests {
67

Modules/Tests/PointOfSaleTests/Presentation/Barcode Scanning/MockTimeProvider.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
@testable import PointOfSale
3+
import WooFoundation
34

45
final class MockTimer: Timer {
56
var isCancelled = false

0 commit comments

Comments
 (0)