Skip to content

Commit 7688160

Browse files
authored
[Woo POS][Local Catalog] Save parsed full catalog into database (#16087)
2 parents f374495 + 76b396b commit 7688160

File tree

5 files changed

+374
-11
lines changed

5 files changed

+374
-11
lines changed

Modules/Sources/Storage/GRDB/Migrations/V001InitialSchema.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ struct V001InitialSchema {
112112
private static func createProductVariationImageTable(_ db: Database) throws {
113113
try db.create(table: "productVariationImage") { productVariationImageTable in
114114
productVariationImageTable.primaryKey("id", .integer).notNull()
115-
productVariationImageTable.belongsTo("productVariation").notNull()
115+
productVariationImageTable.belongsTo("productVariation", onDelete: .cascade).notNull()
116116

117117
productVariationImageTable.column("dateCreated", .datetime).notNull()
118118
productVariationImageTable.column("dateModified", .datetime)

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import protocol Networking.POSCatalogSyncRemoteProtocol
33
import class Networking.AlamofireNetwork
44
import class Networking.POSCatalogSyncRemote
55
import CocoaLumberjackSwift
6+
import Storage
67

78
// TODO - remove the periphery ignore comment when the catalog is integrated with POS.
89
// periphery:ignore
@@ -26,20 +27,23 @@ public struct POSCatalog {
2627
public final class POSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol {
2728
private let syncRemote: POSCatalogSyncRemoteProtocol
2829
private let batchSize: Int
30+
private let persistenceService: POSCatalogPersistenceServiceProtocol
2931

30-
public convenience init?(credentials: Credentials?, batchSize: Int = 2) {
32+
public convenience init?(credentials: Credentials?, batchSize: Int = 2, grdbManager: GRDBManagerProtocol) {
3133
guard let credentials else {
3234
DDLogError("⛔️ Could not create POSCatalogFullSyncService due missing credentials")
3335
return nil
3436
}
3537
let network = AlamofireNetwork(credentials: credentials, ensuresSessionManagerIsInitialized: true)
3638
let syncRemote = POSCatalogSyncRemote(network: network)
37-
self.init(syncRemote: syncRemote, batchSize: batchSize)
39+
let persistenceService = POSCatalogPersistenceService(grdbManager: grdbManager)
40+
self.init(syncRemote: syncRemote, batchSize: batchSize, persistenceService: persistenceService)
3841
}
3942

40-
init(syncRemote: POSCatalogSyncRemoteProtocol, batchSize: Int) {
43+
init(syncRemote: POSCatalogSyncRemoteProtocol, batchSize: Int, persistenceService: POSCatalogPersistenceServiceProtocol) {
4144
self.syncRemote = syncRemote
4245
self.batchSize = batchSize
46+
self.persistenceService = persistenceService
4347
}
4448

4549
// MARK: - Protocol Conformance
@@ -48,10 +52,17 @@ public final class POSCatalogFullSyncService: POSCatalogFullSyncServiceProtocol
4852
DDLogInfo("🔄 Starting full catalog sync for site ID: \(siteID)")
4953

5054
do {
55+
// Sync from network
5156
let catalog = try await loadCatalog(for: siteID, syncRemote: syncRemote)
5257
DDLogInfo("✅ Loaded \(catalog.products.count) products and \(catalog.variations.count) variations for siteID \(siteID)")
58+
59+
// Persist to database
60+
try await persistenceService.replaceAllCatalogData(catalog, siteID: siteID)
61+
DDLogInfo("✅ Persisted \(catalog.products.count) products and \(catalog.variations.count) variations to database for siteID \(siteID)")
62+
5363
return catalog
5464
} catch {
65+
DDLogError("❌ Failed to sync and persist catalog: \(error)")
5566
throw error
5667
}
5768
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// periphery:ignore:all
2+
import Foundation
3+
import Storage
4+
5+
protocol POSCatalogPersistenceServiceProtocol {
6+
/// Clears existing data and persists new catalog data
7+
/// - Parameters:
8+
/// - catalog: The catalog to persist
9+
/// - siteID: The site ID to associate the catalog with
10+
func replaceAllCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws
11+
}
12+
13+
final class POSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol {
14+
private let grdbManager: GRDBManagerProtocol
15+
16+
init(grdbManager: GRDBManagerProtocol) {
17+
self.grdbManager = grdbManager
18+
}
19+
20+
func replaceAllCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws {
21+
DDLogInfo("💾 Persisting catalog with \(catalog.products.count) products and \(catalog.variations.count) variations")
22+
23+
try await grdbManager.databaseConnection.write { db in
24+
DDLogInfo("🗑️ Clearing catalog data for site \(siteID)")
25+
// currently, we can't save for more than one site as entity IDs are not namespaced.
26+
try PersistedSite.deleteAll(db)
27+
28+
let site = PersistedSite(id: siteID)
29+
try site.insert(db)
30+
31+
for product in catalog.productsToPersist {
32+
try product.insert(db, onConflict: .ignore)
33+
}
34+
35+
for image in catalog.productImagesToPersist {
36+
try image.insert(db, onConflict: .ignore)
37+
}
38+
39+
for var attribute in catalog.productAttributesToPersist {
40+
try attribute.insert(db)
41+
}
42+
43+
for variation in catalog.variationsToPersist {
44+
try variation.insert(db, onConflict: .ignore)
45+
}
46+
47+
for image in catalog.variationImagesToPersist {
48+
try image.insert(db, onConflict: .ignore)
49+
}
50+
51+
for var attribute in catalog.variationAttributesToPersist {
52+
try attribute.insert(db)
53+
}
54+
}
55+
56+
DDLogInfo("✅ Catalog persistence complete")
57+
58+
try await grdbManager.databaseConnection.read { db in
59+
let productCount = try PersistedProduct.fetchCount(db)
60+
let productImageCount = try PersistedProductImage.fetchCount(db)
61+
let productAttributeCount = try PersistedProductAttribute.fetchCount(db)
62+
let variationCount = try PersistedProductVariation.fetchCount(db)
63+
let variationImageCount = try PersistedProductVariationImage.fetchCount(db)
64+
let variationAttributeCount = try PersistedProductVariationAttribute.fetchCount(db)
65+
66+
DDLogInfo("Persisted \(productCount) products, \(productImageCount) product images, " +
67+
"\(productAttributeCount) product attributes, \(variationCount) variations, " +
68+
"\(variationImageCount) variation images, \(variationAttributeCount) variation attributes")
69+
}
70+
}
71+
}
72+
73+
private extension POSCatalog {
74+
var productsToPersist: [PersistedProduct] {
75+
products.map { PersistedProduct(from: $0) }
76+
}
77+
78+
var productImagesToPersist: [PersistedProductImage] {
79+
products.flatMap { product in
80+
product.images.map { PersistedProductImage(from: $0, productID: product.productID) }
81+
}
82+
}
83+
84+
var productAttributesToPersist: [PersistedProductAttribute] {
85+
products.flatMap { product in
86+
product.attributes.map { PersistedProductAttribute(from: $0, productID: product.productID) }
87+
}
88+
}
89+
90+
var variationsToPersist: [PersistedProductVariation] {
91+
variations.map { PersistedProductVariation(from: $0) }
92+
}
93+
94+
var variationImagesToPersist: [PersistedProductVariationImage] {
95+
variations.compactMap { variation in
96+
variation.image.map { PersistedProductVariationImage(from: $0, productVariationID: variation.productVariationID) }
97+
}
98+
}
99+
100+
var variationAttributesToPersist: [PersistedProductVariationAttribute] {
101+
variations.flatMap { variation in
102+
variation.attributes.map { PersistedProductVariationAttribute(from: $0, productVariationID: variation.productVariationID) }
103+
}
104+
}
105+
}

Modules/Tests/YosemiteTests/Tools/POS/POSCatalogFullSyncServiceTests.swift

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import Foundation
22
import Testing
33
@testable import Networking
44
@testable import Yosemite
5+
@testable import Storage
56

67
struct POSCatalogFullSyncServiceTests {
78
private let sut: POSCatalogFullSyncService
89
private let mockSyncRemote: MockPOSCatalogSyncRemote
10+
private let mockPersistenceService: MockPOSCatalogPersistenceService
911
private let sampleSiteID: Int64 = 134
1012

1113
init() {
1214
self.mockSyncRemote = MockPOSCatalogSyncRemote()
13-
self.sut = POSCatalogFullSyncService(syncRemote: mockSyncRemote, batchSize: 2)
15+
self.mockPersistenceService = MockPOSCatalogPersistenceService()
16+
self.sut = POSCatalogFullSyncService(syncRemote: mockSyncRemote, batchSize: 2, persistenceService: mockPersistenceService)
1417
}
1518

1619
// MARK: - Full Sync Tests
@@ -128,20 +131,24 @@ struct POSCatalogFullSyncServiceTests {
128131

129132
// MARK: - Initialization Tests
130133

131-
@Test func init_with_valid_credentials_creates_service() {
134+
@Test func init_with_valid_credentials_creates_service() throws {
132135
// Given
133136
let credentials = Credentials.wpcom(username: "test", authToken: "token", siteAddress: "site.com")
137+
let grdbManager = try GRDBManager()
134138

135139
// When
136-
let service = POSCatalogFullSyncService(credentials: credentials)
140+
let service = POSCatalogFullSyncService(credentials: credentials, grdbManager: grdbManager)
137141

138142
// Then
139143
#expect(service != nil)
140144
}
141145

142-
@Test func init_with_nil_credentials_returns_nil() {
143-
// Given/When
144-
let service = POSCatalogFullSyncService(credentials: nil)
146+
@Test func init_with_nil_credentials_returns_nil() throws {
147+
// Given
148+
let grdbManager = try GRDBManager()
149+
150+
// When
151+
let service = POSCatalogFullSyncService(credentials: nil, grdbManager: grdbManager)
145152

146153
// Then
147154
#expect(service == nil)
@@ -152,7 +159,9 @@ struct POSCatalogFullSyncServiceTests {
152159
let customBatchSize = 5
153160

154161
// When
155-
let service = POSCatalogFullSyncService(syncRemote: mockSyncRemote, batchSize: customBatchSize)
162+
let service = POSCatalogFullSyncService(syncRemote: mockSyncRemote,
163+
batchSize: customBatchSize,
164+
persistenceService: mockPersistenceService)
156165
_ = try await service.startFullSync(for: sampleSiteID)
157166

158167
// Then
@@ -231,3 +240,17 @@ final class MockPOSCatalogSyncRemote: POSCatalogSyncRemoteProtocol {
231240
return fallbackVariationResult
232241
}
233242
}
243+
244+
// MARK: - Mock POSCatalogPersistenceService
245+
246+
final class MockPOSCatalogPersistenceService: POSCatalogPersistenceServiceProtocol {
247+
private(set) var replaceAllCatalogDataCallCount = 0
248+
private(set) var lastPersistedCatalog: POSCatalog?
249+
private(set) var lastPersistedSiteID: Int64?
250+
251+
func replaceAllCatalogData(_ catalog: POSCatalog, siteID: Int64) async throws {
252+
replaceAllCatalogDataCallCount += 1
253+
lastPersistedSiteID = siteID
254+
lastPersistedCatalog = catalog
255+
}
256+
}

0 commit comments

Comments
 (0)