Skip to content

Commit 061ac26

Browse files
authored
[Local Catalog] POS Settings: add POSCatalogSettingsService for Yosemite layer (#16166)
2 parents f1e3742 + d1f456b commit 061ac26

File tree

2 files changed

+290
-0
lines changed

2 files changed

+290
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// periphery:ignore:all
2+
import Foundation
3+
import GRDB
4+
import protocol Storage.GRDBManagerProtocol
5+
6+
public protocol POSCatalogSettingsServiceProtocol {
7+
/// Gets catalog information for the specified site.
8+
/// - Parameter siteID: The site ID to get catalog information for.
9+
/// - Returns: Catalog information including statistics and sync dates.
10+
func loadCatalogInfo(for siteID: Int64) async throws -> POSCatalogInfo
11+
}
12+
13+
public struct POSCatalogInfo {
14+
public let productCount: Int
15+
public let variationCount: Int
16+
public let lastFullSyncDate: Date?
17+
public let lastIncrementalSyncDate: Date?
18+
19+
public init(productCount: Int, variationCount: Int, lastFullSyncDate: Date?, lastIncrementalSyncDate: Date?) {
20+
self.productCount = productCount
21+
self.variationCount = variationCount
22+
self.lastFullSyncDate = lastFullSyncDate
23+
self.lastIncrementalSyncDate = lastIncrementalSyncDate
24+
}
25+
}
26+
27+
public class POSCatalogSettingsService: POSCatalogSettingsServiceProtocol {
28+
private let grdbManager: GRDBManagerProtocol
29+
30+
public init(grdbManager: GRDBManagerProtocol) {
31+
self.grdbManager = grdbManager
32+
}
33+
34+
public func loadCatalogInfo(for siteID: Int64) async throws -> POSCatalogInfo {
35+
try await grdbManager.databaseConnection.read { db in
36+
let productCount = try PersistedProduct.filter { $0.siteID == siteID }.fetchCount(db)
37+
let variationCount = try PersistedProductVariation.filter { $0.siteID == siteID }.fetchCount(db)
38+
let site = try PersistedSite.filter(key: siteID).fetchOne(db)
39+
return POSCatalogInfo(
40+
productCount: productCount,
41+
variationCount: variationCount,
42+
lastFullSyncDate: site?.lastCatalogFullSyncDate,
43+
lastIncrementalSyncDate: site?.lastCatalogIncrementalSyncDate
44+
)
45+
}
46+
}
47+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import Foundation
2+
import Testing
3+
import GRDB
4+
@testable import Yosemite
5+
@testable import Storage
6+
7+
struct POSCatalogSettingsServiceTests {
8+
private let grdbManager: GRDBManager
9+
private let sut: POSCatalogSettingsService
10+
private let sampleSiteID: Int64 = 134
11+
12+
init() throws {
13+
self.grdbManager = try GRDBManager()
14+
self.sut = POSCatalogSettingsService(grdbManager: grdbManager)
15+
}
16+
17+
// MARK: - `loadCatalogInfo` Tests
18+
19+
@Test(arguments: [6, 8], [7, 0])
20+
func loadCatalogInfo_returns_correct_counts_for_products_and_variations(productCount: Int, variationCount: Int) async throws {
21+
// Given
22+
try insertSite(siteID: sampleSiteID)
23+
try insertTestProducts(siteID: sampleSiteID, productCount: productCount, variationCount: variationCount)
24+
25+
// When
26+
let catalogInfo = try await sut.loadCatalogInfo(for: sampleSiteID)
27+
28+
// Then
29+
#expect(catalogInfo.productCount == productCount)
30+
#expect(catalogInfo.variationCount == variationCount)
31+
}
32+
33+
@Test func loadCatalogInfo_returns_zero_counts_when_no_data_exists() async throws {
34+
// Given - no data in database for the site
35+
36+
// When
37+
let catalogInfo = try await sut.loadCatalogInfo(for: sampleSiteID)
38+
39+
// Then
40+
#expect(catalogInfo.productCount == 0)
41+
#expect(catalogInfo.variationCount == 0)
42+
}
43+
44+
@Test func loadCatalogInfo_only_counts_items_for_specified_site() async throws {
45+
// Given
46+
let siteA: Int64 = 100
47+
let siteB: Int64 = 200
48+
try insertSite(siteID: siteA)
49+
try insertTestProducts(siteID: siteA, productCount: 3, variationCount: 4)
50+
try insertSite(siteID: siteB)
51+
try insertTestProducts(siteID: siteB, productCount: 2, variationCount: 1)
52+
53+
// When
54+
let catalogInfoA = try await sut.loadCatalogInfo(for: siteA)
55+
let catalogInfoB = try await sut.loadCatalogInfo(for: siteB)
56+
57+
// Then
58+
#expect(catalogInfoA.productCount == 3)
59+
#expect(catalogInfoA.variationCount == 4)
60+
#expect(catalogInfoB.productCount == 2)
61+
#expect(catalogInfoB.variationCount == 1)
62+
}
63+
64+
@Test func loadCatalogInfo_returns_sync_dates_when_site_has_sync_history() async throws {
65+
// Given
66+
let fullSyncDate = Date(timeIntervalSinceNow: -3600) // 1 hour ago
67+
let incrementalSyncDate = Date(timeIntervalSinceNow: -1800) // 30 minutes ago
68+
try insertSite(siteID: sampleSiteID,
69+
lastFullSyncDate: fullSyncDate,
70+
lastIncrementalSyncDate: incrementalSyncDate)
71+
72+
// When
73+
let catalogInfo = try await sut.loadCatalogInfo(for: sampleSiteID)
74+
75+
// Then
76+
#expect(catalogInfo.lastFullSyncDate?.timeIntervalSince(fullSyncDate) ?? 0 < 1.0)
77+
#expect(catalogInfo.lastIncrementalSyncDate?.timeIntervalSince(incrementalSyncDate) ?? 0 < 1.0)
78+
}
79+
80+
@Test func loadCatalogInfo_returns_nil_dates_when_site_has_no_sync_history() async throws {
81+
// Given
82+
try insertSite(siteID: sampleSiteID,
83+
lastFullSyncDate: nil,
84+
lastIncrementalSyncDate: nil)
85+
86+
// When
87+
let catalogInfo = try await sut.loadCatalogInfo(for: sampleSiteID)
88+
89+
// Then
90+
#expect(catalogInfo.lastFullSyncDate == nil)
91+
#expect(catalogInfo.lastIncrementalSyncDate == nil)
92+
}
93+
94+
@Test func loadCatalogInfo_returns_nil_dates_when_site_does_not_exist() async throws {
95+
// Given - site does not exist in database
96+
97+
// When
98+
let catalogInfo = try await sut.loadCatalogInfo(for: sampleSiteID)
99+
100+
// Then
101+
#expect(catalogInfo.lastFullSyncDate == nil)
102+
#expect(catalogInfo.lastIncrementalSyncDate == nil)
103+
}
104+
105+
@Test func loadCatalogInfo_returns_partial_sync_dates() async throws {
106+
// Given - only full sync date is set
107+
let fullSyncDate = Date(timeIntervalSinceNow: -7200) // 2 hours ago
108+
109+
try insertSite(siteID: sampleSiteID,
110+
lastFullSyncDate: fullSyncDate,
111+
lastIncrementalSyncDate: nil)
112+
113+
// When
114+
let catalogInfo = try await sut.loadCatalogInfo(for: sampleSiteID)
115+
116+
// Then
117+
#expect(catalogInfo.lastFullSyncDate?.timeIntervalSince(fullSyncDate) ?? 0 < 1.0)
118+
#expect(catalogInfo.lastIncrementalSyncDate == nil)
119+
}
120+
121+
@Test func loadCatalogInfo_handles_different_sites_independently() async throws {
122+
// Given
123+
let siteA: Int64 = 100
124+
let siteB: Int64 = 200
125+
let dateA = Date(timeIntervalSinceNow: -3600)
126+
let dateB = Date(timeIntervalSinceNow: -7200)
127+
128+
try insertSite(siteID: siteA,
129+
lastFullSyncDate: dateA,
130+
lastIncrementalSyncDate: nil)
131+
try insertSite(siteID: siteB,
132+
lastFullSyncDate: nil,
133+
lastIncrementalSyncDate: dateB)
134+
135+
// When
136+
let catalogInfoA = try await sut.loadCatalogInfo(for: siteA)
137+
let catalogInfoB = try await sut.loadCatalogInfo(for: siteB)
138+
139+
// Then
140+
#expect(catalogInfoA.lastFullSyncDate?.timeIntervalSince(dateA) ?? 0 < 1.0)
141+
#expect(catalogInfoA.lastIncrementalSyncDate == nil)
142+
#expect(catalogInfoB.lastFullSyncDate == nil)
143+
#expect(catalogInfoB.lastIncrementalSyncDate?.timeIntervalSince(dateB) ?? 0 < 1.0)
144+
}
145+
146+
@Test func loadCatalogInfo_propagates_database_errors() async throws {
147+
// Given - close the database to simulate an error
148+
try grdbManager.databaseConnection.close()
149+
150+
// When/Then
151+
await #expect(throws: DatabaseError.self) {
152+
_ = try await sut.loadCatalogInfo(for: sampleSiteID)
153+
}
154+
}
155+
156+
// MARK: - Concurrent Operations Tests
157+
158+
@Test func concurrent_loadCatalogInfo_calls_work_correctly() async throws {
159+
// Given
160+
let siteA: Int64 = 100
161+
let siteB: Int64 = 200
162+
let dateA = Date(timeIntervalSinceNow: -3600)
163+
let dateB = Date(timeIntervalSinceNow: -7200)
164+
165+
try insertSite(siteID: siteA, lastFullSyncDate: dateA, lastIncrementalSyncDate: nil)
166+
try insertTestProducts(siteID: siteA, productCount: 10, variationCount: 15)
167+
try insertSite(siteID: siteB, lastFullSyncDate: nil, lastIncrementalSyncDate: dateB)
168+
try insertTestProducts(siteID: siteB, productCount: 6, variationCount: 8)
169+
170+
// When
171+
async let catalogInfoA = sut.loadCatalogInfo(for: siteA)
172+
async let catalogInfoB = sut.loadCatalogInfo(for: siteB)
173+
174+
let (resultA, resultB) = try await (catalogInfoA, catalogInfoB)
175+
176+
// Then
177+
#expect(resultA.productCount == 10)
178+
#expect(resultA.variationCount == 15)
179+
#expect(resultA.lastFullSyncDate?.timeIntervalSince(dateA) ?? 0 < 1.0)
180+
#expect(resultA.lastIncrementalSyncDate == nil)
181+
#expect(resultB.productCount == 6)
182+
#expect(resultB.variationCount == 8)
183+
#expect(resultB.lastFullSyncDate == nil)
184+
#expect(resultB.lastIncrementalSyncDate?.timeIntervalSince(dateB) ?? 0 < 1.0)
185+
}
186+
187+
@Test func loadCatalogInfo_returns_complete_catalog_information() async throws {
188+
// Given
189+
let syncDate = Date(timeIntervalSinceNow: -1800)
190+
try insertSite(siteID: sampleSiteID, lastFullSyncDate: syncDate, lastIncrementalSyncDate: syncDate)
191+
try insertTestProducts(siteID: sampleSiteID, productCount: 20, variationCount: 30)
192+
193+
// When
194+
let catalogInfo = try await sut.loadCatalogInfo(for: sampleSiteID)
195+
196+
// Then
197+
#expect(catalogInfo.productCount == 20)
198+
#expect(catalogInfo.variationCount == 30)
199+
#expect(catalogInfo.lastFullSyncDate?.timeIntervalSince(syncDate) ?? 0 < 1.0)
200+
#expect(catalogInfo.lastIncrementalSyncDate?.timeIntervalSince(syncDate) ?? 0 < 1.0)
201+
}
202+
}
203+
204+
// MARK: - Helper Methods
205+
206+
private extension POSCatalogSettingsServiceTests {
207+
func insertTestProducts(siteID: Int64, productCount: Int, variationCount: Int) throws {
208+
try grdbManager.databaseConnection.write { db in
209+
if productCount > 0 {
210+
for i in 1...productCount {
211+
let product = PersistedProduct(from: POSProduct.fake().copy(
212+
siteID: siteID,
213+
productID: Int64(i),
214+
name: "Product \(i)"
215+
))
216+
try product.insert(db)
217+
}
218+
}
219+
220+
if variationCount > 0, productCount > 0 {
221+
for i in 1...variationCount {
222+
let variation = PersistedProductVariation(from: POSProductVariation.fake().copy(
223+
siteID: siteID,
224+
productID: Int64(1),
225+
productVariationID: Int64(i)
226+
))
227+
try variation.insert(db)
228+
}
229+
}
230+
}
231+
}
232+
233+
func insertSite(siteID: Int64, lastFullSyncDate: Date? = nil, lastIncrementalSyncDate: Date? = nil) throws {
234+
try grdbManager.databaseConnection.write { db in
235+
let site = PersistedSite(
236+
id: siteID,
237+
lastCatalogIncrementalSyncDate: lastIncrementalSyncDate,
238+
lastCatalogFullSyncDate: lastFullSyncDate
239+
)
240+
try site.insert(db)
241+
}
242+
}
243+
}

0 commit comments

Comments
 (0)