Skip to content

Commit 12e2fcf

Browse files
authored
[Local Catalog] Implement POS catalog generation/status endpoints for full sync (#16041)
2 parents 46a5125 + 95865d6 commit 12e2fcf

File tree

6 files changed

+207
-0
lines changed

6 files changed

+207
-0
lines changed

Modules/Sources/Networking/Remote/POSCatalogSyncRemote.swift

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,43 @@ public class POSCatalogSyncRemote: Remote {
5555

5656
return createPagedItems(items: variations, responseHeaders: responseHeaders, currentPageNumber: pageNumber)
5757
}
58+
59+
/// Generates a POS catalog. The catalog is generated asynchronously and a download URL is returned in the
60+
/// status response endpoint associated with a job ID.
61+
///
62+
/// - Parameters:
63+
/// - siteID: Site ID to generate catalog for.
64+
/// - fields: Optional array of fields to include in catalog.
65+
/// - forceGenerate: Whether to force generation of a new catalog.
66+
/// - Returns: Catalog job response with job ID.
67+
///
68+
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
69+
public func generateCatalog(for siteID: Int64, forceGenerate: Bool = false) async throws -> POSCatalogGenerationResponse {
70+
let path = "catalog"
71+
let parameters: [String: Any] = [
72+
ParameterKey.fullSyncFields: POSProduct.requestFields
73+
]
74+
75+
let request = JetpackRequest(wooApiVersion: .mark3, method: .post, siteID: siteID, path: path, parameters: parameters, availableAsRESTRequest: true)
76+
let mapper = SingleItemMapper<POSCatalogGenerationResponse>(siteID: siteID)
77+
return try await enqueue(request, mapper: mapper)
78+
}
79+
80+
/// Checks the status of a catalog generation job. A download URL is returned when the job is complete.
81+
///
82+
/// - Parameters:
83+
/// - siteID: Site ID for the catalog job.
84+
/// - jobID: Job ID to check status for.
85+
/// - Returns: Catalog status response.
86+
///
87+
// periphery:ignore - TODO - remove this periphery ignore comment when this endpoint is integrated with catalog sync
88+
public func checkCatalogStatus(for siteID: Int64, jobID: String) async throws -> POSCatalogStatusResponse {
89+
let path = "catalog/status/\(jobID)"
90+
91+
let request = JetpackRequest(wooApiVersion: .mark3, method: .get, siteID: siteID, path: path, availableAsRESTRequest: true)
92+
let mapper = SingleItemMapper<POSCatalogStatusResponse>(siteID: siteID)
93+
return try await enqueue(request, mapper: mapper)
94+
}
5895
}
5996

6097
// MARK: - Constants
@@ -69,5 +106,43 @@ private extension POSCatalogSyncRemote {
69106
static let page = "page"
70107
static let perPage = "per_page"
71108
static let fields = "_fields"
109+
static let fullSyncFields = "fields"
110+
}
111+
}
112+
113+
// MARK: - Response Models
114+
115+
/// Response from catalog generation request.
116+
// periphery:ignore - TODO - remove this periphery ignore comment when the corresponding endpoint is integrated with catalog sync
117+
public struct POSCatalogGenerationResponse: Decodable {
118+
/// Unique identifier for tracking the catalog generation job.
119+
public let jobID: String
120+
121+
private enum CodingKeys: String, CodingKey {
122+
case jobID = "job_id"
72123
}
73124
}
125+
126+
/// Response from catalog status check.
127+
// periphery:ignore - TODO - remove this periphery ignore comment when the corresponding endpoint is integrated with catalog sync
128+
public struct POSCatalogStatusResponse: Decodable {
129+
/// Current status of the catalog generation job.
130+
public let status: POSCatalogStatus
131+
/// Download URL for the completed catalog (available when status is complete).
132+
public let downloadURL: String?
133+
/// Progress percentage of the catalog generation (0.0 to 100.0).
134+
public let progress: Double
135+
136+
private enum CodingKeys: String, CodingKey {
137+
case status
138+
case downloadURL = "download_url"
139+
case progress
140+
}
141+
}
142+
143+
/// Catalog generation status.
144+
public enum POSCatalogStatus: String, Decodable {
145+
case pending
146+
case processing
147+
case complete
148+
}

Modules/Tests/NetworkingTests/Remote/POSCatalogSyncRemoteTests.swift

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,4 +280,97 @@ struct POSCatalogSyncRemoteTests {
280280
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: any Hashable])
281281
#expect(queryParametersDictionary["page"] as? String == String(largePageNumber))
282282
}
283+
284+
// MARK: - Catalog Generation Tests
285+
286+
@Test func generateCatalog_sets_correct_parameters() async throws {
287+
// Given
288+
let remote = POSCatalogSyncRemote(network: network)
289+
290+
// When
291+
_ = try? await remote.generateCatalog(for: sampleSiteID)
292+
293+
// Then
294+
let queryParametersDictionary = try #require(network.queryParametersDictionary as? [String: any Hashable])
295+
#expect(queryParametersDictionary["fields"] as? [String] == POSProduct.requestFields)
296+
}
297+
298+
@Test func generateCatalog_returns_parsed_response() async throws {
299+
// Given
300+
let remote = POSCatalogSyncRemote(network: network)
301+
let expectedJobID = "export_1756177061_7885"
302+
303+
// When
304+
network.simulateResponse(requestUrlSuffix: "catalog", filename: "pos-catalog-generation")
305+
let response = try await remote.generateCatalog(for: sampleSiteID)
306+
307+
// Then
308+
#expect(response.jobID == expectedJobID)
309+
}
310+
311+
@Test func generateCatalog_relays_networking_error() async throws {
312+
// Given
313+
let remote = POSCatalogSyncRemote(network: network)
314+
315+
// When/Then
316+
await #expect(throws: NetworkError.notFound()) {
317+
try await remote.generateCatalog(for: sampleSiteID)
318+
}
319+
}
320+
321+
@Test func checkCatalogStatus_returns_parsed_response_when_status_is_complete() async throws {
322+
// Given
323+
let remote = POSCatalogSyncRemote(network: network)
324+
let jobID = "job_12345"
325+
326+
// When
327+
network.simulateResponse(requestUrlSuffix: "catalog/status/\(jobID)", filename: "pos-catalog-status-complete")
328+
let response = try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)
329+
330+
// Then
331+
#expect(response.status == .complete)
332+
#expect(response.progress == 100.0)
333+
#expect(response.downloadURL != nil)
334+
}
335+
336+
@Test func checkCatalogStatus_returns_parsed_response_when_status_is_pending() async throws {
337+
// Given
338+
let remote = POSCatalogSyncRemote(network: network)
339+
let jobID = "job_12345"
340+
341+
// When
342+
network.simulateResponse(requestUrlSuffix: "catalog/status/\(jobID)", filename: "pos-catalog-status-pending")
343+
let response = try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)
344+
345+
// Then
346+
#expect(response.status == .pending)
347+
#expect(response.progress == 0.0)
348+
#expect(response.downloadURL == nil)
349+
}
350+
351+
@Test func checkCatalogStatus_returns_parsed_response_when_status_is_processing() async throws {
352+
// Given
353+
let remote = POSCatalogSyncRemote(network: network)
354+
let jobID = "job_12345"
355+
356+
// When
357+
network.simulateResponse(requestUrlSuffix: "catalog/status/\(jobID)", filename: "pos-catalog-status-processing")
358+
let response = try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)
359+
360+
// Then
361+
#expect(response.status == .processing)
362+
#expect(response.progress == 5.0)
363+
#expect(response.downloadURL == nil)
364+
}
365+
366+
@Test func checkCatalogStatus_relays_networking_error() async throws {
367+
// Given
368+
let remote = POSCatalogSyncRemote(network: network)
369+
let jobID = "job_12345"
370+
371+
// When/Then
372+
await #expect(throws: NetworkError.notFound()) {
373+
try await remote.checkCatalogStatus(for: sampleSiteID, jobID: jobID)
374+
}
375+
}
283376
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"job_id": "export_1756177061_7885",
3+
"status": "pending",
4+
"format": "json",
5+
"filename": "wc-product-export-2025-08-26-02-57-41.json",
6+
"created_at": "2025-08-26 02:57:41",
7+
"status_url": "https://example.com/wp-json/wc/v3/catalog/status/export_1756177061_7885"
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"job_id": "export_1756177061_7885",
3+
"status": "complete",
4+
"format": "json",
5+
"filename": "wc-product-export-2025-08-26-02-57-41.json",
6+
"created_at": "2025-08-26 02:57:41",
7+
"progress": 100,
8+
"current_batch": 2,
9+
"action_scheduler_status": "complete",
10+
"download_url": "https://example.com/wp-json/wc/v3/catalog/download?filename=wc-product-export-2025-08-26-02-57-41.json&format=json"
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"job_id": "export_1756177181_6269",
3+
"status": "pending",
4+
"format": "json",
5+
"filename": "wc-product-export-2025-08-26-02-59-41.json",
6+
"created_at": "2025-08-26 02:59:41",
7+
"progress": 0,
8+
"current_batch": 1,
9+
"action_scheduler_status": "pending"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"job_id": "export_1756177181_6269",
3+
"status": "processing",
4+
"format": "json",
5+
"filename": "wc-product-export-2025-08-26-02-59-41.json",
6+
"created_at": "2025-08-26 02:59:41",
7+
"progress": 5,
8+
"current_batch": 2,
9+
"action_scheduler_status": "pending"
10+
}

0 commit comments

Comments
 (0)