Skip to content

Commit 63c19b5

Browse files
committed
POSCatalogSyncRemote: add downloadCatalog to download & parse catalog.
1 parent 95865d6 commit 63c19b5

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

Modules/Sources/Networking/Remote/POSCatalogSyncRemote.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@ public class POSCatalogSyncRemote: Remote {
9292
let mapper = SingleItemMapper<POSCatalogStatusResponse>(siteID: siteID)
9393
return try await enqueue(request, mapper: mapper)
9494
}
95+
96+
/// Downloads the generated catalog at the specified download URL.
97+
/// - Parameters:
98+
/// - siteID: Site ID to download catalog for.
99+
/// - downloadURL: Download URL of the catalog file.
100+
/// - Returns: List of products and variations in the POS catalog.
101+
public func downloadCatalog(for siteID: Int64, downloadURL: String) async throws -> POSCatalog {
102+
// TODO: WOOMOB-1173 - move download task to the background using `URLSessionConfiguration.background`
103+
guard let url = URL(string: downloadURL) else {
104+
throw NetworkError.invalidURL
105+
}
106+
let request = URLRequest(url: url)
107+
let mapper = ListMapper<POSProduct>(siteID: siteID)
108+
let items = try await enqueue(request, mapper: mapper)
109+
let variationProductTypeKey = "variation"
110+
let products = items.filter { $0.productTypeKey != variationProductTypeKey }
111+
let variations = items.filter { $0.productTypeKey == variationProductTypeKey }
112+
.map { $0.toVariation }
113+
return POSCatalog(products: products, variations: variations)
114+
}
95115
}
96116

97117
// MARK: - Constants
@@ -146,3 +166,38 @@ public enum POSCatalogStatus: String, Decodable {
146166
case processing
147167
case complete
148168
}
169+
170+
/// POS catalog from download.
171+
// periphery:ignore - TODO - remove this periphery ignore comment when the corresponding endpoint is integrated with catalog sync
172+
public struct POSCatalog {
173+
public let products: [POSProduct]
174+
public let variations: [POSProductVariation]
175+
}
176+
177+
private extension POSProduct {
178+
var toVariation: POSProductVariation {
179+
let variationAttributes = attributes.compactMap { attribute in
180+
try? attribute.toProductVariationAttribute()
181+
}
182+
183+
let firstImage = images.first
184+
185+
return .init(
186+
siteID: siteID,
187+
productID: parentID,
188+
productVariationID: productID,
189+
attributes: variationAttributes,
190+
image: firstImage,
191+
sku: sku,
192+
globalUniqueID: globalUniqueID,
193+
price: price,
194+
regularPrice: regularPrice,
195+
salePrice: salePrice,
196+
onSale: onSale,
197+
downloadable: downloadable,
198+
manageStock: manageStock,
199+
stockQuantity: stockQuantity,
200+
stockStatusKey: stockStatusKey
201+
)
202+
}
203+
}

Modules/Tests/NetworkingTests/Remote/POSCatalogSyncRemoteTests.swift

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,4 +373,108 @@ struct POSCatalogSyncRemoteTests {
373373
try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)
374374
}
375375
}
376+
377+
// MARK: - Download Catalog Tests
378+
379+
@Test func downloadCatalog_returns_parsed_catalog_with_products_and_variations() async throws {
380+
// Given
381+
let remote = POSCatalogSyncRemote(network: network)
382+
let downloadURL = "https://example.com/catalog.json"
383+
384+
// When
385+
network.simulateResponse(requestUrlSuffix: "", filename: "pos-catalog-download-mixed")
386+
let catalog = try await remote.downloadCatalog(for: sampleSiteID, downloadURL: downloadURL)
387+
388+
// Then
389+
#expect(catalog.products.count == 2)
390+
#expect(catalog.variations.count == 2)
391+
392+
let simpleProduct = try #require(catalog.products.first { $0.productType == .simple })
393+
#expect(simpleProduct.siteID == sampleSiteID)
394+
#expect(simpleProduct.productID == 48)
395+
#expect(simpleProduct.sku == "synergistic-copper-clock-61732018")
396+
#expect(simpleProduct.globalUniqueID == "61732018")
397+
#expect(simpleProduct.name == "Synergistic Copper Clock")
398+
#expect(simpleProduct.price == "220")
399+
#expect(simpleProduct.regularPrice == "230.04")
400+
#expect(simpleProduct.onSale == true)
401+
#expect(simpleProduct.images.count == 1)
402+
#expect(simpleProduct.images.first?.src == "https://example.com/wp-content/uploads/2025/08/img-ad.png")
403+
404+
let variableProduct = try #require(catalog.products.first { $0.productType == .variable })
405+
#expect(variableProduct.siteID == sampleSiteID)
406+
#expect(variableProduct.productID == 31)
407+
#expect(variableProduct.sku == "incredible-silk-chair-13060312")
408+
#expect(variableProduct.globalUniqueID == "")
409+
#expect(variableProduct.name == "Incredible Silk Chair")
410+
#expect(variableProduct.price == "134.58")
411+
#expect(variableProduct.regularPrice == "")
412+
#expect(variableProduct.onSale == false)
413+
#expect(variableProduct.images.count == 1)
414+
#expect(variableProduct.images.first?.src == "https://example.com/wp-content/uploads/2025/08/img-harum.png")
415+
#expect(variableProduct.attributes == [
416+
.init(siteID: sampleSiteID, attributeID: 1, name: "Size", position: 0, visible: true, variation: true, options: ["Earum"]),
417+
.init(siteID: sampleSiteID, attributeID: 0, name: "Ab", position: 1, visible: true, variation: true, options: ["deserunt", "ea", "ut"]),
418+
.init(siteID: sampleSiteID,
419+
attributeID: 2,
420+
name: "Numeric Size",
421+
position: 2,
422+
visible: true,
423+
variation: true,
424+
options: ["19", "8", "9", "At", "Reiciendis"])
425+
])
426+
427+
let variation = try #require(catalog.variations.first)
428+
#expect(variation.siteID == sampleSiteID)
429+
#expect(variation.productVariationID == 32)
430+
#expect(variation.productID == 31)
431+
#expect(variation.sku == "")
432+
#expect(variation.globalUniqueID == "")
433+
#expect(variation.price == "330.34")
434+
#expect(variation.regularPrice == "330.34")
435+
#expect(variation.onSale == false)
436+
#expect(variation.attributes.count == 3)
437+
#expect(variation.image?.src == "https://example.com/wp-content/uploads/2025/08/img-quae.png")
438+
#expect(variation.attributes == [
439+
.init(id: 1, name: "Size", option: "Earum"),
440+
.init(id: 0, name: "ab", option: "deserunt"),
441+
.init(id: 2, name: "Numeric Size", option: "19")
442+
])
443+
}
444+
445+
@Test func downloadCatalog_handles_empty_catalog() async throws {
446+
// Given
447+
let remote = POSCatalogSyncRemote(network: network)
448+
let downloadURL = "https://example.com/catalog.json"
449+
450+
// When
451+
network.simulateResponse(requestUrlSuffix: "", filename: "empty-data-array")
452+
let catalog = try await remote.downloadCatalog(for: sampleSiteID, downloadURL: downloadURL)
453+
454+
// Then
455+
#expect(catalog.products.count == 0)
456+
#expect(catalog.variations.count == 0)
457+
}
458+
459+
@Test func downloadCatalog_throws_error_for_empty_url() async throws {
460+
// Given
461+
let remote = POSCatalogSyncRemote(network: network)
462+
let emptyURL = ""
463+
464+
// When/Then
465+
await #expect(throws: NetworkError.invalidURL) {
466+
try await remote.downloadCatalog(for: sampleSiteID, downloadURL: emptyURL)
467+
}
468+
}
469+
470+
@Test func downloadCatalog_relays_networking_error() async throws {
471+
// Given
472+
let remote = POSCatalogSyncRemote(network: network)
473+
let downloadURL = "https://example.com/catalog.json"
474+
475+
// When/Then
476+
await #expect(throws: NetworkError.notFound()) {
477+
try await remote.downloadCatalog(for: sampleSiteID, downloadURL: downloadURL)
478+
}
479+
}
376480
}

0 commit comments

Comments
 (0)