Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions Modules/Sources/Networking/Remote/POSCatalogSyncRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,26 @@ public class POSCatalogSyncRemote: Remote {
let mapper = SingleItemMapper<POSCatalogStatusResponse>(siteID: siteID)
return try await enqueue(request, mapper: mapper)
}

/// Downloads the generated catalog at the specified download URL.
/// - Parameters:
/// - siteID: Site ID to download catalog for.
/// - downloadURL: Download URL of the catalog file.
/// - Returns: List of products and variations in the POS catalog.
public func downloadCatalog(for siteID: Int64, downloadURL: String) async throws -> POSCatalog {
// TODO: WOOMOB-1173 - move download task to the background using `URLSessionConfiguration.background`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to change the function signature when we do this. I don't think it will be an async returning function any more. The app will get a callback when it's done, and have to handle the data there instead of here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, the background download could be tricky as we might need to handle related events from app delegate from this part of the documentation:

If your app is in the background, the system may suspend your app while the download is performed in another process. In this case, when the download finishes, the system resumes the app and calls the UIApplicationDelegate method application(_:handleEventsForBackgroundURLSession:completionHandler:). This method receives the session identifier you created in Creating a background URL session as its second parameter.

Going to start checking out the background download implementation today.

guard let url = URL(string: downloadURL) else {
throw NetworkError.invalidURL
}
let request = URLRequest(url: url)
let mapper = ListMapper<POSProduct>(siteID: siteID)
let items = try await enqueue(request, mapper: mapper)
let variationProductTypeKey = "variation"
let products = items.filter { $0.productTypeKey != variationProductTypeKey }
let variations = items.filter { $0.productTypeKey == variationProductTypeKey }
.map { $0.toVariation }
return POSCatalog(products: products, variations: variations)
}
}

// MARK: - Constants
Expand Down Expand Up @@ -146,3 +166,38 @@ public enum POSCatalogStatus: String, Decodable {
case processing
case complete
}

/// POS catalog from download.
// periphery:ignore - TODO - remove this periphery ignore comment when the corresponding endpoint is integrated with catalog sync
public struct POSCatalog {
public let products: [POSProduct]
public let variations: [POSProductVariation]
}

private extension POSProduct {
var toVariation: POSProductVariation {
let variationAttributes = attributes.compactMap { attribute in
try? attribute.toProductVariationAttribute()
}

let firstImage = images.first

return .init(
siteID: siteID,
productID: parentID,
productVariationID: productID,
attributes: variationAttributes,
image: firstImage,
sku: sku,
globalUniqueID: globalUniqueID,
price: price,
regularPrice: regularPrice,
salePrice: salePrice,
onSale: onSale,
downloadable: downloadable,
manageStock: manageStock,
stockQuantity: stockQuantity,
stockStatusKey: stockStatusKey
)
}
}
104 changes: 104 additions & 0 deletions Modules/Tests/NetworkingTests/Remote/POSCatalogSyncRemoteTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -373,4 +373,108 @@ struct POSCatalogSyncRemoteTests {
try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)
}
}

// MARK: - Download Catalog Tests

@Test func downloadCatalog_returns_parsed_catalog_with_products_and_variations() async throws {
// Given
let remote = POSCatalogSyncRemote(network: network)
let downloadURL = "https://example.com/catalog.json"

// When
network.simulateResponse(requestUrlSuffix: "", filename: "pos-catalog-download-mixed")
let catalog = try await remote.downloadCatalog(for: sampleSiteID, downloadURL: downloadURL)

// Then
#expect(catalog.products.count == 2)
#expect(catalog.variations.count == 2)

let simpleProduct = try #require(catalog.products.first { $0.productType == .simple })
#expect(simpleProduct.siteID == sampleSiteID)
#expect(simpleProduct.productID == 48)
#expect(simpleProduct.sku == "synergistic-copper-clock-61732018")
#expect(simpleProduct.globalUniqueID == "61732018")
#expect(simpleProduct.name == "Synergistic Copper Clock")
#expect(simpleProduct.price == "220")
#expect(simpleProduct.regularPrice == "230.04")
#expect(simpleProduct.onSale == true)
#expect(simpleProduct.images.count == 1)
#expect(simpleProduct.images.first?.src == "https://example.com/wp-content/uploads/2025/08/img-ad.png")

let variableProduct = try #require(catalog.products.first { $0.productType == .variable })
#expect(variableProduct.siteID == sampleSiteID)
#expect(variableProduct.productID == 31)
#expect(variableProduct.sku == "incredible-silk-chair-13060312")
#expect(variableProduct.globalUniqueID == "")
#expect(variableProduct.name == "Incredible Silk Chair")
#expect(variableProduct.price == "134.58")
#expect(variableProduct.regularPrice == "")
#expect(variableProduct.onSale == false)
#expect(variableProduct.images.count == 1)
#expect(variableProduct.images.first?.src == "https://example.com/wp-content/uploads/2025/08/img-harum.png")
#expect(variableProduct.attributes == [
.init(siteID: sampleSiteID, attributeID: 1, name: "Size", position: 0, visible: true, variation: true, options: ["Earum"]),
.init(siteID: sampleSiteID, attributeID: 0, name: "Ab", position: 1, visible: true, variation: true, options: ["deserunt", "ea", "ut"]),
.init(siteID: sampleSiteID,
attributeID: 2,
name: "Numeric Size",
position: 2,
visible: true,
variation: true,
options: ["19", "8", "9", "At", "Reiciendis"])
])

let variation = try #require(catalog.variations.first)
#expect(variation.siteID == sampleSiteID)
#expect(variation.productVariationID == 32)
#expect(variation.productID == 31)
#expect(variation.sku == "")
#expect(variation.globalUniqueID == "")
#expect(variation.price == "330.34")
#expect(variation.regularPrice == "330.34")
#expect(variation.onSale == false)
#expect(variation.attributes.count == 3)
#expect(variation.image?.src == "https://example.com/wp-content/uploads/2025/08/img-quae.png")
#expect(variation.attributes == [
.init(id: 1, name: "Size", option: "Earum"),
.init(id: 0, name: "ab", option: "deserunt"),
.init(id: 2, name: "Numeric Size", option: "19")
])
}

@Test func downloadCatalog_handles_empty_catalog() async throws {
// Given
let remote = POSCatalogSyncRemote(network: network)
let downloadURL = "https://example.com/catalog.json"

// When
network.simulateResponse(requestUrlSuffix: "", filename: "empty-data-array")
let catalog = try await remote.downloadCatalog(for: sampleSiteID, downloadURL: downloadURL)

// Then
#expect(catalog.products.count == 0)
#expect(catalog.variations.count == 0)
}

@Test func downloadCatalog_throws_error_for_empty_url() async throws {
// Given
let remote = POSCatalogSyncRemote(network: network)
let emptyURL = ""

// When/Then
await #expect(throws: NetworkError.invalidURL) {
try await remote.downloadCatalog(for: sampleSiteID, downloadURL: emptyURL)
}
}

@Test func downloadCatalog_relays_networking_error() async throws {
// Given
let remote = POSCatalogSyncRemote(network: network)
let downloadURL = "https://example.com/catalog.json"

// When/Then
await #expect(throws: NetworkError.notFound()) {
try await remote.downloadCatalog(for: sampleSiteID, downloadURL: downloadURL)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
[
{
"id": 48,
"type": "simple",
"sku": "synergistic-copper-clock-61732018",
"global_unique_id": "61732018",
"name": "Synergistic Copper Clock",
"short_description": "Aut nulla accusantium mollitia aut dolor. Nesciunt dolor eligendi enim voluptas.",
"description": "Assumenda id quidem iste incidunt velit. Illo quae voluptatem voluptatum tempore in fuga.",
"status": "publish",
"on_sale": true,
"stock_status": "instock",
"backorders_allowed": false,
"manage_stock": false,
"stock_quantity": null,
"price": "220",
"sale_price": "220",
"regular_price": "230.04",
"images": [
{
"id": 77,
"date_created": "2025-08-06T02:37:12",
"date_created_gmt": "2025-08-06T02:37:12",
"date_modified": "2025-08-06T02:37:12",
"date_modified_gmt": "2025-08-06T02:37:12",
"src": "https://example.com/wp-content/uploads/2025/08/img-ad.png",
"name": "img-ad.png",
"alt": "",
"srcset": "https://example.com/wp-content/uploads/2025/08/img-ad.png 700w, https://example.com/wp-content/uploads/2025/08/img-ad-300x300.png 300w, https://example.com/wp-content/uploads/2025/08/img-ad-150x150.png 150w, https://example.com/wp-content/uploads/2025/08/img-ad-600x600.png 600w, https://example.com/wp-content/uploads/2025/08/img-ad-100x100.png 100w",
"sizes": "(max-width: 700px) 100vw, 700px",
"thumbnail": "https://example.com/wp-content/uploads/2025/08/img-ad-300x300.png"
}
],
"parent_id": 0,
"attributes": [],
"downloadable": false
},
{
"id": 31,
"type": "variable",
"sku": "incredible-silk-chair-13060312",
"global_unique_id": "",
"name": "Incredible Silk Chair",
"short_description": "",
"description": "",
"status": "publish",
"on_sale": false,
"stock_status": "onbackorder",
"backorders_allowed": true,
"manage_stock": true,
"stock_quantity": -83,
"price": "134.58",
"sale_price": "",
"regular_price": "",
"images": [
{
"id": 61,
"date_created": "2025-08-06T02:37:03",
"date_created_gmt": "2025-08-06T02:37:03",
"date_modified": "2025-08-06T02:37:03",
"date_modified_gmt": "2025-08-06T02:37:03",
"src": "https://example.com/wp-content/uploads/2025/08/img-harum.png",
"name": "img-harum.png",
"alt": "",
"srcset": "https://example.com/wp-content/uploads/2025/08/img-harum.png 700w, https://example.com/wp-content/uploads/2025/08/img-harum-300x300.png 300w, https://example.com/wp-content/uploads/2025/08/img-harum-150x150.png 150w, https://example.com/wp-content/uploads/2025/08/img-harum-600x600.png 600w, https://example.com/wp-content/uploads/2025/08/img-harum-100x100.png 100w",
"sizes": "(max-width: 700px) 100vw, 700px",
"thumbnail": "https://example.com/wp-content/uploads/2025/08/img-harum-300x300.png"
}
],
"parent_id": 0,
"attributes": [
{
"id": 1,
"name": "Size",
"position": 0,
"visible": true,
"variation": true,
"options": [
"Earum"
]
},
{
"id": 0,
"name": "Ab",
"position": 1,
"visible": true,
"variation": true,
"options": [
"deserunt",
"ea",
"ut"
]
},
{
"id": 2,
"name": "Numeric Size",
"position": 2,
"visible": true,
"variation": true,
"options": [
"19",
"8",
"9",
"At",
"Reiciendis"
]
}
],
"downloadable": false
},
{
"id": 32,
"type": "variation",
"sku": "",
"global_unique_id": "",
"name": "Incredible Silk Chair",
"short_description": "",
"description": "",
"status": "publish",
"on_sale": false,
"stock_status": "instock",
"backorders_allowed": false,
"manage_stock": true,
"stock_quantity": 69,
"price": "330.34",
"sale_price": "",
"regular_price": "330.34",
"images": [
{
"id": 62,
"date_created": "2025-08-06T02:37:04",
"date_created_gmt": "2025-08-06T02:37:04",
"date_modified": "2025-08-06T02:37:04",
"date_modified_gmt": "2025-08-06T02:37:04",
"src": "https://example.com/wp-content/uploads/2025/08/img-quae.png",
"name": "img-quae.png",
"alt": "",
"srcset": "https://example.com/wp-content/uploads/2025/08/img-quae.png 700w, https://example.com/wp-content/uploads/2025/08/img-quae-300x300.png 300w, https://example.com/wp-content/uploads/2025/08/img-quae-150x150.png 150w, https://example.com/wp-content/uploads/2025/08/img-quae-600x600.png 600w, https://example.com/wp-content/uploads/2025/08/img-quae-100x100.png 100w",
"sizes": "(max-width: 700px) 100vw, 700px",
"thumbnail": "https://example.com/wp-content/uploads/2025/08/img-quae-300x300.png"
}
],
"parent_id": 31,
"attributes": [
{
"id": 1,
"name": "Size",
"option": "Earum"
},
{
"id": 0,
"name": "ab",
"option": "deserunt"
},
{
"id": 2,
"name": "Numeric Size",
"option": "19"
}
],
"downloadable": false
},
{
"id": 33,
"type": "variation",
"sku": "",
"global_unique_id": "",
"name": "Incredible Silk Chair",
"short_description": "",
"description": "",
"status": "publish",
"on_sale": false,
"stock_status": "instock",
"backorders_allowed": false,
"manage_stock": true,
"stock_quantity": 64,
"price": "580.05",
"sale_price": "",
"regular_price": "580.05",
"images": [
{
"id": 63,
"date_created": "2025-08-06T02:37:05",
"date_created_gmt": "2025-08-06T02:37:05",
"date_modified": "2025-08-06T02:37:05",
"date_modified_gmt": "2025-08-06T02:37:05",
"src": "https://example.com/wp-content/uploads/2025/08/img-delectus-1.png",
"name": "img-delectus-1.png",
"alt": "",
"srcset": "https://example.com/wp-content/uploads/2025/08/img-delectus-1.png 700w, https://example.com/wp-content/uploads/2025/08/img-delectus-1-300x300.png 300w, https://example.com/wp-content/uploads/2025/08/img-delectus-1-150x150.png 150w, https://example.com/wp-content/uploads/2025/08/img-delectus-1-600x600.png 600w, https://example.com/wp-content/uploads/2025/08/img-delectus-1-100x100.png 100w",
"sizes": "(max-width: 700px) 100vw, 700px",
"thumbnail": "https://example.com/wp-content/uploads/2025/08/img-delectus-1-300x300.png"
}
],
"parent_id": 31,
"attributes": [
{
"id": 1,
"name": "Size",
"option": "Earum"
},
{
"id": 0,
"name": "ab",
"option": "deserunt"
},
{
"id": 2,
"name": "Numeric Size",
"option": "8"
}
],
"downloadable": false
},
]
Loading