Skip to content

Commit 1d7fc6f

Browse files
authored
Merge pull request #7962 from woocommerce/issue/7918-template-ednpoint
Product Onboarding: Adds "Create Template" Endpoint
2 parents 02fd571 + c79882e commit 1d7fc6f

File tree

11 files changed

+141
-0
lines changed

11 files changed

+141
-0
lines changed

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@
164164
2670C3FE270F4E6A002FE931 /* sites-malformed.json in Resources */ = {isa = PBXBuildFile; fileRef = 2670C3FD270F4E6A002FE931 /* sites-malformed.json */; };
165165
267313312559CC930026F7EF /* PaymentGateway.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267313302559CC930026F7EF /* PaymentGateway.swift */; };
166166
26731337255ACA850026F7EF /* PaymentGatewayListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26731336255ACA850026F7EF /* PaymentGatewayListMapper.swift */; };
167+
2676F4CE290AE6BB00C7A15B /* EntityIDMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2676F4CD290AE6BB00C7A15B /* EntityIDMapper.swift */; };
168+
2676F4D0290B0EC800C7A15B /* product-id-only.json in Resources */ = {isa = PBXBuildFile; fileRef = 2676F4CF290B0EC700C7A15B /* product-id-only.json */; };
167169
2683D70E24456DB8002A1589 /* categories-empty.json in Resources */ = {isa = PBXBuildFile; fileRef = 2683D70D24456DB7002A1589 /* categories-empty.json */; };
168170
2683D71024456EE4002A1589 /* categories-extra.json in Resources */ = {isa = PBXBuildFile; fileRef = 2683D70F24456EE4002A1589 /* categories-extra.json */; };
169171
2685C0D2263B069500D9EE97 /* AddOnGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2685C0D1263B069500D9EE97 /* AddOnGroup.swift */; };
@@ -887,6 +889,8 @@
887889
2670C3FD270F4E6A002FE931 /* sites-malformed.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "sites-malformed.json"; sourceTree = "<group>"; };
888890
267313302559CC930026F7EF /* PaymentGateway.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGateway.swift; sourceTree = "<group>"; };
889891
26731336255ACA850026F7EF /* PaymentGatewayListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGatewayListMapper.swift; sourceTree = "<group>"; };
892+
2676F4CD290AE6BB00C7A15B /* EntityIDMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityIDMapper.swift; sourceTree = "<group>"; };
893+
2676F4CF290B0EC700C7A15B /* product-id-only.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-id-only.json"; sourceTree = "<group>"; };
890894
2683D70D24456DB7002A1589 /* categories-empty.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "categories-empty.json"; sourceTree = "<group>"; };
891895
2683D70F24456EE4002A1589 /* categories-extra.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "categories-extra.json"; sourceTree = "<group>"; };
892896
2685C0D1263B069500D9EE97 /* AddOnGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOnGroup.swift; sourceTree = "<group>"; };
@@ -2083,6 +2087,7 @@
20832087
09885C7F27C3FFD200910A62 /* product-variations-bulk-update.json */,
20842088
451274A525276C82009911FF /* product-variation.json */,
20852089
CE0A0F1E223998A00075ED8D /* products-load-all.json */,
2090+
2676F4CF290B0EC700C7A15B /* product-id-only.json */,
20862091
CCF434652906C2A400B4475A /* products-ids-only.json */,
20872092
CCF434692906C9C300B4475A /* products-ids-only-empty.json */,
20882093
0282DD90233A120A006A5FDB /* products-search-photo.json */,
@@ -2215,6 +2220,7 @@
22152220
DE2095BE279583A100171F1C /* CouponReportListMapper.swift */,
22162221
45150A9D26836A57006922EA /* CountryListMapper.swift */,
22172222
B524193E21AC5FE400D6FC0A /* DotcomDeviceMapper.swift */,
2223+
2676F4CD290AE6BB00C7A15B /* EntityIDMapper.swift */,
22182224
24F98C572502EA8800F49B68 /* FeatureFlagMapper.swift */,
22192225
DE50295C28C6068B00551736 /* JetpackUserMapper.swift */,
22202226
AEF9458A27297FF6001DCCFB /* IgnoringResponseMapper.swift */,
@@ -2821,6 +2827,7 @@
28212827
68CB801628D8A39700E169F8 /* customer.json in Resources */,
28222828
03EB99982907F4AA00F06A39 /* just-in-time-message-list-multiple.json in Resources */,
28232829
451A97DE260B59870059D135 /* shipping-label-packages-success.json in Resources */,
2830+
2676F4D0290B0EC800C7A15B /* product-id-only.json in Resources */,
28242831
31D27C8F2602B553002EDB1D /* plugins.json in Resources */,
28252832
261CF1B4255AD6B30090D8D3 /* payment-gateway-list.json in Resources */,
28262833
268B68FB24C87384007EBF1D /* leaderboards-products.json in Resources */,
@@ -3240,6 +3247,7 @@
32403247
029BA53B255DFABD006171FD /* ShippingLabelPrintDataMapper.swift in Sources */,
32413248
0359EA0D27AAC5F80048DE2D /* WCPayChargeStatus.swift in Sources */,
32423249
CE430674234BA6AD0073CBFF /* RefundMapper.swift in Sources */,
3250+
2676F4CE290AE6BB00C7A15B /* EntityIDMapper.swift in Sources */,
32433251
CE0A0F1B223989670075ED8D /* ProductsRemote.swift in Sources */,
32443252
0359EA1527AAC7460048DE2D /* WCPayCardBrand.swift in Sources */,
32453253
2665032E261F4FBF0079A159 /* ProductAddOnOption.swift in Sources */,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
3+
/// Mapper: Single Entity ID
4+
///
5+
struct EntityIDMapper: Mapper {
6+
7+
/// (Attempts) to convert an instance of Data into an into an ID
8+
///
9+
func map(response: Data) throws -> Int64 {
10+
let decoder = JSONDecoder()
11+
12+
return try decoder.decode(EntityIDEnvelope.self, from: response).id
13+
}
14+
}
15+
16+
/// Disposable Entity:
17+
/// Allows us to parse a product ID with JSONDecoder.
18+
///
19+
private struct EntityIDEnvelope: Decodable {
20+
private let data: [String: Int64]
21+
22+
// Extracts the entity ID from the underlying data
23+
var id: Int64 {
24+
data["id"] ?? .zero
25+
}
26+
27+
private enum CodingKeys: String, CodingKey {
28+
case data = "data"
29+
}
30+
}

Networking/Networking/Model/Product/ProductStatus.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public enum ProductStatus: Codable, Hashable, GeneratedFakeable {
88
case draft
99
case pending
1010
case privateStatus // `private` is a reserved keyword
11+
case autoDraft
1112
case custom(String) // in case there are extensions modifying product statuses
1213
}
1314

@@ -28,6 +29,8 @@ extension ProductStatus: RawRepresentable {
2829
self = .pending
2930
case Keys.privateStatus:
3031
self = .privateStatus
32+
case Keys.autoDraft:
33+
self = .autoDraft
3134
default:
3235
self = .custom(rawValue)
3336
}
@@ -41,6 +44,7 @@ extension ProductStatus: RawRepresentable {
4144
case .draft: return Keys.draft
4245
case .pending: return Keys.pending
4346
case .privateStatus: return Keys.privateStatus
47+
case .autoDraft: return Keys.autoDraft
4448
case .custom(let payload): return payload
4549
}
4650
}
@@ -57,6 +61,8 @@ extension ProductStatus: RawRepresentable {
5761
return NSLocalizedString("Pending review", comment: "Display label for the product's pending status")
5862
case .privateStatus:
5963
return NSLocalizedString("Privately published", comment: "Display label for the product's private status")
64+
case .autoDraft:
65+
return "Auto Draft" // We don't need to localize this now.
6066
case .custom(let payload):
6167
return payload // unable to localize at runtime.
6268
}
@@ -71,4 +77,5 @@ private enum Keys {
7177
static let draft = "draft"
7278
static let pending = "pending"
7379
static let privateStatus = "private"
80+
static let autoDraft = "auto-draft"
7481
}

Networking/Networking/Remote/ProductsRemote.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public protocol ProductsRemoteProtocol {
4242
func updateProduct(product: Product, completion: @escaping (Result<Product, Error>) -> Void)
4343
func updateProductImages(siteID: Int64, productID: Int64, images: [ProductImage], completion: @escaping (Result<Product, Error>) -> Void)
4444
func loadProductIDs(for siteID: Int64, pageNumber: Int, pageSize: Int, completion: @escaping (Result<[Int64], Error>) -> Void)
45+
func createTemplateProduct(for siteID: Int64, template: ProductsRemote.TemplateType, completion: @escaping (Result<Int64, Error>) -> Void)
4546
}
4647

4748
extension ProductsRemoteProtocol {
@@ -348,6 +349,19 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
348349

349350
enqueue(request, mapper: mapper, completion: completion)
350351
}
352+
353+
/// Creates a product using the provided template.
354+
/// Finishes with a completion block with the product ID.
355+
/// The created product has an `auto-draft` status.
356+
///
357+
public func createTemplateProduct(for siteID: Int64, template: ProductsRemote.TemplateType, completion: @escaping (Result<Int64, Error>) -> Void) {
358+
let parameters = [ParameterKey.templateName: template.rawValue]
359+
let path = Path.templateProducts
360+
let request = JetpackRequest(wooApiVersion: .wcAdmin, method: .post, siteID: siteID, path: path, parameters: parameters)
361+
let mapper = EntityIDMapper()
362+
363+
enqueue(request, mapper: mapper, completion: completion)
364+
}
351365
}
352366

353367

@@ -364,6 +378,16 @@ public extension ProductsRemote {
364378
case descending
365379
}
366380

381+
/// Supported types for creating a template product.
382+
///
383+
enum TemplateType: String {
384+
case physical
385+
case digital
386+
case variable
387+
case external
388+
case grouped
389+
}
390+
367391
enum Default {
368392
public static let pageSize: Int = 25
369393
public static let pageNumber: Int = Remote.Default.firstPageNumber
@@ -372,6 +396,7 @@ public extension ProductsRemote {
372396

373397
private enum Path {
374398
static let products = "products"
399+
static let templateProducts = "onboarding/tasks/create_product_from_template"
375400
}
376401

377402
private enum ParameterKey {
@@ -392,6 +417,7 @@ public extension ProductsRemote {
392417
static let fields: String = "_fields"
393418
static let images: String = "images"
394419
static let id: String = "id"
420+
static let templateName: String = "template_name"
395421
}
396422

397423
private enum ParameterValues {

Networking/Networking/Settings/WooAPIVersion.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ public enum WooAPIVersion: String {
4343
///
4444
case wcTelemetry = "wc-telemetry"
4545

46+
/// WooCommerce Admin.
47+
///
48+
case wcAdmin = "wc-admin"
49+
4650
/// Returns the path for the current API Version
4751
///
4852
var path: String {

Networking/NetworkingTests/Remote/ProductsRemoteTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,23 @@ final class ProductsRemoteTests: XCTestCase {
620620
// Then
621621
XCTAssertTrue(result.isFailure)
622622
}
623+
624+
func test_create_template_product_returns_product_id() throws {
625+
// Given
626+
let remote = ProductsRemote(network: network)
627+
network.simulateResponse(requestUrlSuffix: "onboarding/tasks/create_product_from_template", filename: "product-id-only")
628+
629+
// When
630+
let result = waitFor { promise in
631+
remote.createTemplateProduct(for: self.sampleSiteID, template: .physical) { result in
632+
promise(result)
633+
}
634+
}
635+
636+
// Then
637+
let productID = try XCTUnwrap(result.get())
638+
XCTAssertEqual(productID, 3946)
639+
}
623640
}
624641

625642
// MARK: - Private Helpers
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"data": {
3+
"id": 3946
4+
}
5+
}

Yosemite/Yosemite/Actions/ProductAction.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,8 @@ public enum ProductAction: Action {
8686
/// Checks if the store has at least one product
8787
///
8888
case checkForProducts(siteID: Int64, onCompletion: (Result<Bool, Error>) -> Void)
89+
90+
/// Creates a product using the provided template type.
91+
///
92+
case createTemplateProduct(siteID: Int64, template: ProductsRemote.TemplateType, onCompletion: (Result<Product, Error>) -> Void)
8993
}

Yosemite/Yosemite/Stores/ProductStore.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ public class ProductStore: Store {
102102
replaceProductLocally(product: product, onCompletion: onCompletion)
103103
case let .checkForProducts(siteID: siteID, onCompletion: onCompletion):
104104
checkForProducts(siteID: siteID, onCompletion: onCompletion)
105+
case let .createTemplateProduct(siteID, template, onCompletion):
106+
createTemplateProduct(siteID: siteID, template: template, onCompletion: onCompletion)
105107
}
106108
}
107109
}
@@ -409,6 +411,21 @@ private extension ProductStore {
409411
}
410412
}
411413
}
414+
415+
/// Creates a product using the provided template type.
416+
/// The created product is not stored locally.
417+
///
418+
func createTemplateProduct(siteID: Int64, template: ProductsRemote.TemplateType, onCompletion: @escaping (Result<Product, Error>) -> Void) {
419+
remote.createTemplateProduct(for: siteID, template: template) { [remote] result in
420+
switch result {
421+
case .success(let productID):
422+
remote.loadProduct(for: siteID, productID: productID, completion: onCompletion)
423+
424+
case .failure(let error):
425+
onCompletion(.failure(error))
426+
}
427+
}
428+
}
412429
}
413430

414431

Yosemite/YosemiteTests/Mocks/Networking/Remote/MockProductsRemote.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,8 @@ extension MockProductsRemote: ProductsRemoteProtocol {
199199
func loadProductIDs(for siteID: Int64, pageNumber: Int, pageSize: Int, completion: @escaping (Result<[Int64], Error>) -> Void) {
200200
// no-op
201201
}
202+
203+
func createTemplateProduct(for siteID: Int64, template: ProductsRemote.TemplateType, completion: @escaping (Result<Int64, Error>) -> Void) {
204+
// no-op
205+
}
202206
}

0 commit comments

Comments
 (0)