Skip to content

Commit ffb0fcc

Browse files
joshhealdclaude
andcommitted
[Local catalog] Implement local variation fetching from GRDB
Replaces remote API calls with local GRDB queries for fetching product variations: - Uses `posVariationsRequest()` to query variations from local catalog - Implements pagination matching product search pattern - Converts `PersistedProductVariation` to `POSProductVariation` with attributes/images - Automatically filters downloadable variations (handled by query) - Returns correct `PagedItems` with `hasMorePages` and `totalItems` Adds comprehensive test coverage: - Basic variation fetching from local catalog - Downloadable variation filtering - Empty results handling - Pagination with multiple pages - Parent product isolation Analytics tracking will be added in final PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 0a479b6 commit ffb0fcc

File tree

2 files changed

+180
-73
lines changed

2 files changed

+180
-73
lines changed

Modules/Sources/Yosemite/PointOfSale/Items/PointOfSaleLocalSearchPurchasableItemFetchStrategy.swift

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ public struct PointOfSaleLocalSearchPurchasableItemFetchStrategy: PointOfSalePur
2626
}
2727

2828
public func fetchProducts(pageNumber: Int) async throws -> PagedItems<POSProduct> {
29-
let startTime = Date()
30-
3129
// Get total count and persisted products in one transaction
3230
let (persistedProducts, totalCount) = try await grdbManager.databaseConnection.read { db in
3331
let totalCount = try PersistedProduct
@@ -49,13 +47,6 @@ public struct PointOfSaleLocalSearchPurchasableItemFetchStrategy: PointOfSalePur
4947
try persistedProduct.toPOSProduct(db: grdbManager.databaseConnection)
5048
}
5149

52-
// Track analytics for first page
53-
if pageNumber == 1 {
54-
let milliseconds = Int(Date().timeIntervalSince(startTime) * Double(MSEC_PER_SEC))
55-
analytics.trackSearchLocalResultsFetchComplete(millisecondsSinceRequestSent: milliseconds,
56-
totalItems: totalCount)
57-
}
58-
5950
let hasMorePages = (pageNumber * pageSize) < totalCount
6051

6152
return PagedItems(items: products,
@@ -64,11 +55,30 @@ public struct PointOfSaleLocalSearchPurchasableItemFetchStrategy: PointOfSalePur
6455
}
6556

6657
public func fetchVariations(parentProductID: Int64, pageNumber: Int) async throws -> PagedItems<POSProductVariation> {
67-
// Reuse existing remote variations fetching logic
68-
// Variations will be handled in future FTS implementation
69-
try await variationsRemote
70-
.loadVariationsForPointOfSale(for: siteID,
71-
parentProductID: parentProductID,
72-
pageNumber: pageNumber)
58+
// Get total count and persisted variations in one transaction
59+
let (persistedVariations, totalCount) = try await grdbManager.databaseConnection.read { db in
60+
let totalCount = try PersistedProductVariation
61+
.posVariationsRequest(siteID: siteID, parentProductID: parentProductID)
62+
.fetchCount(db)
63+
64+
let offset = (pageNumber - 1) * pageSize
65+
let persistedVariations = try PersistedProductVariation
66+
.posVariationsRequest(siteID: siteID, parentProductID: parentProductID)
67+
.limit(pageSize, offset: offset)
68+
.fetchAll(db)
69+
70+
return (persistedVariations, totalCount)
71+
}
72+
73+
// Convert to POSProductVariation outside the read transaction
74+
let variations = try persistedVariations.map { persistedVariation in
75+
try persistedVariation.toPOSProductVariation(db: grdbManager.databaseConnection)
76+
}
77+
78+
let hasMorePages = (pageNumber * pageSize) < totalCount
79+
80+
return PagedItems(items: variations,
81+
hasMorePages: hasMorePages,
82+
totalItems: totalCount)
7383
}
7484
}

Modules/Tests/YosemiteTests/PointOfSale/PointOfSaleLocalSearchPurchasableItemFetchStrategyTests.swift

Lines changed: 155 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -21,53 +21,6 @@ struct PointOfSaleLocalSearchPurchasableItemFetchStrategyTests {
2121
}
2222
}
2323

24-
// MARK: - Analytics Tests
25-
26-
@Test("fetchProducts tracks analytics for first page")
27-
func test_fetchProducts_tracks_analytics_for_first_page() async throws {
28-
// Given
29-
let product = makeProduct(id: 1, name: "Test Product")
30-
try await insertProduct(product)
31-
32-
let strategy = PointOfSaleLocalSearchPurchasableItemFetchStrategy(
33-
siteID: siteID,
34-
searchTerm: searchTerm,
35-
grdbManager: grdbManager,
36-
variationsRemote: variationsRemote,
37-
analytics: mockAnalytics
38-
)
39-
40-
// When
41-
_ = try await strategy.fetchProducts(pageNumber: 1)
42-
43-
// Then
44-
#expect(mockAnalytics.spyLocalSearchMilliseconds != nil)
45-
#expect(mockAnalytics.spyLocalSearchTotalItems == 1)
46-
}
47-
48-
@Test("fetchProducts does not track analytics for subsequent pages")
49-
func test_fetchProducts_does_not_track_analytics_for_subsequent_pages() async throws {
50-
// Given
51-
for i in 1...50 {
52-
try await insertProduct(makeProduct(id: Int64(i), name: "Test Product \(i)"))
53-
}
54-
55-
let strategy = PointOfSaleLocalSearchPurchasableItemFetchStrategy(
56-
siteID: siteID,
57-
searchTerm: searchTerm,
58-
grdbManager: grdbManager,
59-
variationsRemote: variationsRemote,
60-
analytics: mockAnalytics
61-
)
62-
63-
// When
64-
_ = try await strategy.fetchProducts(pageNumber: 2)
65-
66-
// Then - no analytics should be tracked for page 2
67-
#expect(mockAnalytics.spyLocalSearchMilliseconds == nil)
68-
#expect(mockAnalytics.spyLocalSearchTotalItems == nil)
69-
}
70-
7124
// MARK: - Search Functionality Tests
7225

7326
@Test("fetchProducts returns matching products")
@@ -365,19 +318,13 @@ struct PointOfSaleLocalSearchPurchasableItemFetchStrategyTests {
365318

366319
// MARK: - Variations Tests
367320

368-
@Test("fetchVariations delegates to remote")
369-
func test_fetchVariations_delegates_to_remote() async throws {
321+
@Test("fetchVariations returns variations for parent product from local catalog")
322+
func test_fetchVariations_returns_variations_from_local_catalog() async throws {
370323
// Given
371324
let parentProductID: Int64 = 100
372-
let expectedVariations = [
373-
POSProductVariation.fake().copy(productVariationID: 1, productID: parentProductID),
374-
POSProductVariation.fake().copy(productVariationID: 2, productID: parentProductID)
375-
]
376-
variationsRemote.whenLoadingVariationsForPointOfSale(thenReturn: .success(PagedItems(
377-
items: expectedVariations,
378-
hasMorePages: false,
379-
totalItems: 2
380-
)))
325+
try await insertProduct(makeProduct(id: parentProductID, name: "Variable Product", productTypeKey: "variable"))
326+
try await insertVariation(makeVariation(id: 1, productID: parentProductID, price: "10.00"))
327+
try await insertVariation(makeVariation(id: 2, productID: parentProductID, price: "15.00"))
381328

382329
let strategy = PointOfSaleLocalSearchPurchasableItemFetchStrategy(
383330
siteID: siteID,
@@ -394,6 +341,127 @@ struct PointOfSaleLocalSearchPurchasableItemFetchStrategyTests {
394341
#expect(result.items.count == 2)
395342
#expect(result.items[0].productVariationID == 1)
396343
#expect(result.items[1].productVariationID == 2)
344+
#expect(result.totalItems == 2)
345+
#expect(result.hasMorePages == false)
346+
}
347+
348+
@Test("fetchVariations filters out downloadable variations")
349+
func test_fetchVariations_filters_out_downloadable_variations() async throws {
350+
// Given
351+
let parentProductID: Int64 = 100
352+
try await insertProduct(makeProduct(id: parentProductID, name: "Variable Product", productTypeKey: "variable"))
353+
try await insertVariation(makeVariation(id: 1, productID: parentProductID, downloadable: false))
354+
try await insertVariation(makeVariation(id: 2, productID: parentProductID, downloadable: true))
355+
356+
let strategy = PointOfSaleLocalSearchPurchasableItemFetchStrategy(
357+
siteID: siteID,
358+
searchTerm: searchTerm,
359+
grdbManager: grdbManager,
360+
variationsRemote: variationsRemote,
361+
analytics: mockAnalytics
362+
)
363+
364+
// When
365+
let result = try await strategy.fetchVariations(parentProductID: parentProductID, pageNumber: 1)
366+
367+
// Then
368+
#expect(result.items.count == 1)
369+
#expect(result.items.first?.productVariationID == 1)
370+
#expect(result.items.first?.downloadable == false)
371+
}
372+
373+
@Test("fetchVariations returns empty result when no variations")
374+
func test_fetchVariations_returns_empty_when_no_variations() async throws {
375+
// Given
376+
let parentProductID: Int64 = 100
377+
try await insertProduct(makeProduct(id: parentProductID, name: "Simple Product", productTypeKey: "simple"))
378+
379+
let strategy = PointOfSaleLocalSearchPurchasableItemFetchStrategy(
380+
siteID: siteID,
381+
searchTerm: searchTerm,
382+
grdbManager: grdbManager,
383+
variationsRemote: variationsRemote,
384+
analytics: mockAnalytics
385+
)
386+
387+
// When
388+
let result = try await strategy.fetchVariations(parentProductID: parentProductID, pageNumber: 1)
389+
390+
// Then
391+
#expect(result.items.isEmpty)
392+
#expect(result.totalItems == 0)
393+
#expect(result.hasMorePages == false)
394+
}
395+
396+
@Test("fetchVariations handles pagination correctly")
397+
func test_fetchVariations_handles_pagination_correctly() async throws {
398+
// Given
399+
let parentProductID: Int64 = 100
400+
try await insertProduct(makeProduct(id: parentProductID, name: "Variable Product", productTypeKey: "variable"))
401+
402+
// Insert 30 variations
403+
for i in 1...30 {
404+
try await insertVariation(makeVariation(id: Int64(i), productID: parentProductID, price: "\(i).00"))
405+
}
406+
407+
let strategy = PointOfSaleLocalSearchPurchasableItemFetchStrategy(
408+
siteID: siteID,
409+
searchTerm: searchTerm,
410+
grdbManager: grdbManager,
411+
variationsRemote: variationsRemote,
412+
analytics: mockAnalytics,
413+
pageSize: 10
414+
)
415+
416+
// When
417+
let page1 = try await strategy.fetchVariations(parentProductID: parentProductID, pageNumber: 1)
418+
let page2 = try await strategy.fetchVariations(parentProductID: parentProductID, pageNumber: 2)
419+
let page3 = try await strategy.fetchVariations(parentProductID: parentProductID, pageNumber: 3)
420+
let page4 = try await strategy.fetchVariations(parentProductID: parentProductID, pageNumber: 4)
421+
422+
// Then
423+
#expect(page1.items.count == 10)
424+
#expect(page1.hasMorePages == true)
425+
#expect(page1.totalItems == 30)
426+
427+
#expect(page2.items.count == 10)
428+
#expect(page2.hasMorePages == true)
429+
#expect(page2.totalItems == 30)
430+
431+
#expect(page3.items.count == 10)
432+
#expect(page3.hasMorePages == false)
433+
#expect(page3.totalItems == 30)
434+
435+
#expect(page4.items.isEmpty)
436+
#expect(page4.hasMorePages == false)
437+
#expect(page4.totalItems == 30)
438+
}
439+
440+
@Test("fetchVariations only returns variations for specified parent")
441+
func test_fetchVariations_respects_parent_product_isolation() async throws {
442+
// Given
443+
let parentProduct1ID: Int64 = 100
444+
let parentProduct2ID: Int64 = 200
445+
try await insertProduct(makeProduct(id: parentProduct1ID, name: "Variable Product 1", productTypeKey: "variable"))
446+
try await insertProduct(makeProduct(id: parentProduct2ID, name: "Variable Product 2", productTypeKey: "variable"))
447+
try await insertVariation(makeVariation(id: 1, productID: parentProduct1ID))
448+
try await insertVariation(makeVariation(id: 2, productID: parentProduct2ID))
449+
450+
let strategy = PointOfSaleLocalSearchPurchasableItemFetchStrategy(
451+
siteID: siteID,
452+
searchTerm: searchTerm,
453+
grdbManager: grdbManager,
454+
variationsRemote: variationsRemote,
455+
analytics: mockAnalytics
456+
)
457+
458+
// When
459+
let result = try await strategy.fetchVariations(parentProductID: parentProduct1ID, pageNumber: 1)
460+
461+
// Then
462+
#expect(result.items.count == 1)
463+
#expect(result.items.first?.productVariationID == 1)
464+
#expect(result.items.first?.productID == parentProduct1ID)
397465
}
398466

399467
// MARK: - Helper Methods
@@ -430,4 +498,33 @@ struct PointOfSaleLocalSearchPurchasableItemFetchStrategyTests {
430498
try product.insert(db)
431499
}
432500
}
501+
502+
private func makeVariation(
503+
id: Int64,
504+
productID: Int64,
505+
siteID: Int64? = nil,
506+
sku: String? = nil,
507+
price: String = "10.00",
508+
downloadable: Bool = false
509+
) -> PersistedProductVariation {
510+
PersistedProductVariation(
511+
id: id,
512+
siteID: siteID ?? self.siteID,
513+
productID: productID,
514+
sku: sku,
515+
globalUniqueID: nil,
516+
price: price,
517+
downloadable: downloadable,
518+
fullDescription: nil,
519+
manageStock: false,
520+
stockQuantity: nil,
521+
stockStatusKey: "instock"
522+
)
523+
}
524+
525+
private func insertVariation(_ variation: PersistedProductVariation) async throws {
526+
try await grdbManager.databaseConnection.write { db in
527+
try variation.insert(db)
528+
}
529+
}
433530
}

0 commit comments

Comments
 (0)