Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
75 changes: 75 additions & 0 deletions Modules/Sources/Networking/Remote/POSCatalogSyncRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,43 @@ public class POSCatalogSyncRemote: Remote {

return createPagedItems(items: variations, responseHeaders: responseHeaders, currentPageNumber: pageNumber)
}

/// Generates a POS catalog. The catalog is generated asynchronously and a download URL is returned in the
/// status response endpoint associated with a job ID.
///
/// - Parameters:
/// - siteID: Site ID to generate catalog for.
/// - fields: Optional array of fields to include in catalog.
/// - forceGenerate: Whether to force generation of a new catalog.
/// - Returns: Catalog job response with job ID.
///
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
public func generateCatalog(for siteID: Int64, forceGenerate: Bool = false) async throws -> POSCatalogGenerationResponse {
let path = "catalog"
let parameters: [String: Any] = [
ParameterKey.fullSyncFields: POSProduct.requestFields
]

let request = JetpackRequest(wooApiVersion: .mark3, method: .post, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
let mapper = SingleItemMapper<POSCatalogGenerationResponse>(siteID: siteID)
return try await enqueue(request, mapper: mapper)
}

/// Checks the status of a catalog generation job. A download URL is returned when the job is complete.
///
/// - Parameters:
/// - siteID: Site ID for the catalog job.
/// - jobID: Job ID to check status for.
/// - Returns: Catalog status response.
///
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
public func checkCatalogStatus(for siteID: Int64, jobID: String) async throws -> POSCatalogStatusResponse {
let path = "catalog/status/\(jobID)"

let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, availableAsRESTRequest: true)
let mapper = SingleItemMapper<POSCatalogStatusResponse>(siteID: siteID)
return try await enqueue(request, mapper: mapper)
}
}

// MARK: - Constants
Expand All @@ -69,5 +106,43 @@ private extension POSCatalogSyncRemote {
static let page = "page"
static let perPage = "per_page"
static let fields = "_fields"
static let fullSyncFields = "fields"
}
}

// MARK: - Response Models

/// Response from catalog generation request.
// periphery:ignore - TODO - remove this periphery ignore comment when the corresponding endpoint is integrated with catalog sync
public struct POSCatalogGenerationResponse: Decodable {
/// Unique identifier for tracking the catalog generation job.
public let jobID: String

private enum CodingKeys: String, CodingKey {
case jobID = "job_id"
}
}

/// Response from catalog status check.
// periphery:ignore - TODO - remove this periphery ignore comment when the corresponding endpoint is integrated with catalog sync
public struct POSCatalogStatusResponse: Decodable {
/// Current status of the catalog generation job.
public let status: POSCatalogStatus
/// Download URL for the completed catalog (available when status is complete).
public let downloadURL: String?
/// Progress percentage of the catalog generation (0.0 to 100.0).
public let progress: Double

private enum CodingKeys: String, CodingKey {
case status
case downloadURL = "download_url"
case progress
}
}

/// Catalog generation status.
public enum POSCatalogStatus: String, Decodable {
case pending
case processing
case complete
}
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,97 @@ struct POSCatalogSyncRemoteTests {
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: any Hashable])
#expect(queryParametersDictionary["page"] as? String == String(largePageNumber))
}

// MARK: - Catalog Generation Tests

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

// When
_ = try? await remote.generateCatalog(for: sampleSiteID)

// Then
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: any Hashable])
#expect(queryParametersDictionary["fields"] as? [String] == POSProduct.requestFields)
}

@Test func generateCatalog_returns_parsed_response() async throws {
// Given
let remote = POSCatalogSyncRemote(network: network)
let expectedJobID = "export_1756177061_7885"

// When
network.simulateResponse(requestUrlSuffix: "catalog", filename: "pos-catalog-generation")
let response = try await remote.generateCatalog(for: sampleSiteID)

// Then
#expect(response.jobID == expectedJobID)
}

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

// When/Then
await #expect(throws: NetworkError.notFound()) {
try await remote.generateCatalog(for: sampleSiteID)
}
}

@Test func checkCatalogStatus_returns_parsed_response_when_status_is_complete() async throws {
// Given
let remote = POSCatalogSyncRemote(network: network)
let jobID = "job_12345"

// When
network.simulateResponse(requestUrlSuffix: "catalog/status/\(jobID)", filename: "pos-catalog-status-complete")
let response = try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)

// Then
#expect(response.status == .complete)
#expect(response.progress == 100.0)
#expect(response.downloadURL != nil)
}

@Test func checkCatalogStatus_returns_parsed_response_when_status_is_pending() async throws {
// Given
let remote = POSCatalogSyncRemote(network: network)
let jobID = "job_12345"

// When
network.simulateResponse(requestUrlSuffix: "catalog/status/\(jobID)", filename: "pos-catalog-status-pending")
let response = try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)

// Then
#expect(response.status == .pending)
#expect(response.progress == 0.0)
#expect(response.downloadURL == nil)
}

@Test func checkCatalogStatus_returns_parsed_response_when_status_is_processing() async throws {
// Given
let remote = POSCatalogSyncRemote(network: network)
let jobID = "job_12345"

// When
network.simulateResponse(requestUrlSuffix: "catalog/status/\(jobID)", filename: "pos-catalog-status-processing")
let response = try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)

// Then
#expect(response.status == .processing)
#expect(response.progress == 5.0)
#expect(response.downloadURL == nil)
}

@Test func checkCatalogStatus_relays_networking_error() async throws {
// Given
let remote = POSCatalogSyncRemote(network: network)
let jobID = "job_12345"

// When/Then
await #expect(throws: NetworkError.notFound()) {
try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"job_id": "export_1756177061_7885",
"status": "pending",
"format": "json",
"filename": "wc-product-export-2025-08-26-02-57-41.json",
"created_at": "2025-08-26 02:57:41",
"status_url": "https://example.com/wp-json/wc/v3/catalog/status/export_1756177061_7885"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"job_id": "export_1756177061_7885",
"status": "complete",
"format": "json",
"filename": "wc-product-export-2025-08-26-02-57-41.json",
"created_at": "2025-08-26 02:57:41",
"progress": 100,
"current_batch": 2,
"action_scheduler_status": "complete",
"download_url": "https://example.com/wp-json/wc/v3/catalog/download?filename=wc-product-export-2025-08-26-02-57-41.json&format=json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"job_id": "export_1756177181_6269",
"status": "pending",
"format": "json",
"filename": "wc-product-export-2025-08-26-02-59-41.json",
"created_at": "2025-08-26 02:59:41",
"progress": 0,
"current_batch": 1,
"action_scheduler_status": "pending"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"job_id": "export_1756177181_6269",
"status": "processing",
"format": "json",
"filename": "wc-product-export-2025-08-26-02-59-41.json",
"created_at": "2025-08-26 02:59:41",
"progress": 5,
"current_batch": 2,
"action_scheduler_status": "pending"
}