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
16 changes: 16 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@
02DD6492248A3EC00082523E /* product-external.json in Resources */ = {isa = PBXBuildFile; fileRef = 02DD6491248A3EC00082523E /* product-external.json */; };
02E7FFCB256218F600C53030 /* ShippingLabelRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E7FFCA256218F600C53030 /* ShippingLabelRemoteTests.swift */; };
02E7FFCF25621C7900C53030 /* shipping-label-print.json in Resources */ = {isa = PBXBuildFile; fileRef = 02E7FFCE25621C7900C53030 /* shipping-label-print.json */; };
02EF1664292DADDE00D90AD6 /* PaymentRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EF1663292DADDE00D90AD6 /* PaymentRemote.swift */; };
02EF166E292F0C5800D90AD6 /* PaymentRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EF166D292F0C5800D90AD6 /* PaymentRemoteTests.swift */; };
02EF1670292F0CF400D90AD6 /* create-cart-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 02EF166F292F0CF400D90AD6 /* create-cart-success.json */; };
02EF1672292F0D1900D90AD6 /* load-plan-success.json in Resources */ = {isa = PBXBuildFile; fileRef = 02EF1671292F0D1900D90AD6 /* load-plan-success.json */; };
02F096C22406691100C0C1D5 /* media-library.json in Resources */ = {isa = PBXBuildFile; fileRef = 02F096C12406691100C0C1D5 /* media-library.json */; };
0313651928AE559D00EEE571 /* PaymentGatewayMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651828AE559D00EEE571 /* PaymentGatewayMapper.swift */; };
0313651B28AE60E000EEE571 /* payment-gateway-cod.json in Resources */ = {isa = PBXBuildFile; fileRef = 0313651A28AE60E000EEE571 /* payment-gateway-cod.json */; };
Expand Down Expand Up @@ -841,6 +845,10 @@
02DD6491248A3EC00082523E /* product-external.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "product-external.json"; sourceTree = "<group>"; };
02E7FFCA256218F600C53030 /* ShippingLabelRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelRemoteTests.swift; sourceTree = "<group>"; };
02E7FFCE25621C7900C53030 /* shipping-label-print.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "shipping-label-print.json"; sourceTree = "<group>"; };
02EF1663292DADDE00D90AD6 /* PaymentRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRemote.swift; sourceTree = "<group>"; };
02EF166D292F0C5800D90AD6 /* PaymentRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentRemoteTests.swift; sourceTree = "<group>"; };
02EF166F292F0CF400D90AD6 /* create-cart-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "create-cart-success.json"; sourceTree = "<group>"; };
02EF1671292F0D1900D90AD6 /* load-plan-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "load-plan-success.json"; sourceTree = "<group>"; };
02F096C12406691100C0C1D5 /* media-library.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-library.json"; sourceTree = "<group>"; };
0313651828AE559D00EEE571 /* PaymentGatewayMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGatewayMapper.swift; sourceTree = "<group>"; };
0313651A28AE60E000EEE571 /* payment-gateway-cod.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "payment-gateway-cod.json"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1738,6 +1746,7 @@
68F48B0E28E3BB850045C15B /* WCAnalyticsCustomerRemoteTests.swift */,
0239306A291A96F800B2632F /* DomainRemoteTests.swift */,
02616F8B292132800095BC00 /* SiteRemoteTests.swift */,
02EF166D292F0C5800D90AD6 /* PaymentRemoteTests.swift */,
);
path = Remote;
sourceTree = "<group>";
Expand Down Expand Up @@ -1879,6 +1888,7 @@
68F48B0A28E3B1CD0045C15B /* WCAnalyticsCustomerRemote.swift */,
023930622918FF5400B2632F /* DomainRemote.swift */,
021940E1291E3CFD0090354E /* SiteRemote.swift */,
02EF1663292DADDE00D90AD6 /* PaymentRemote.swift */,
);
path = Remote;
sourceTree = "<group>";
Expand Down Expand Up @@ -2006,6 +2016,7 @@
028CB713290223CB00331C09 /* create-account-error-password.json */,
028CB71A290224D700331C09 /* create-account-error-username.json */,
028CB712290223CB00331C09 /* create-account-success.json */,
02EF166F292F0CF400D90AD6 /* create-cart-success.json */,
0239306C291A973F00B2632F /* domain-suggestions.json */,
DE50295F28C609A300551736 /* jetpack-connected-user.json */,
DE34051A28BDF12C00CF0D97 /* jetpack-connection-url.json */,
Expand Down Expand Up @@ -2072,6 +2083,7 @@
E16C59B828F927CA007D55BB /* iap-order-create.json */,
26B2F74624C55A6E0065CCC8 /* leaderboards-year.json */,
268B68FC24C87E37007EBF1D /* leaderboards-year-alt.json */,
02EF1671292F0D1900D90AD6 /* load-plan-success.json */,
B505F6D420BEE4E600BB1B69 /* me.json */,
93D8BBFE226BC1DA00AD2EB3 /* me-settings.json */,
02F096C12406691100C0C1D5 /* media-library.json */,
Expand Down Expand Up @@ -2709,6 +2721,7 @@
743E84FC22174CE100FAC9D7 /* restnoroute_error.json in Resources */,
CE20179320E3EFA7005B4C18 /* broken-orders.json in Resources */,
D8FBFF2722D529F2006E3336 /* order-stats-v4-month.json in Resources */,
02EF1672292F0D1900D90AD6 /* load-plan-success.json in Resources */,
2685C0DE263B5A4200D9EE97 /* add-on-groups.json in Resources */,
DEC51A9B274E3206009F3DF4 /* plugin-inactive.json in Resources */,
CCF48B382628AEAE0034EA83 /* shipping-label-account-settings-no-payment-methods.json in Resources */,
Expand Down Expand Up @@ -2800,6 +2813,7 @@
B554FA8D2180B59700C54DFF /* notifications-load-hashes.json in Resources */,
022902D622E2436400059692 /* no_stats_permission_error.json in Resources */,
FE28F6E826842D57004465C7 /* user-complete.json in Resources */,
02EF1670292F0CF400D90AD6 /* create-cart-success.json in Resources */,
261CF2CB255C50010090D8D3 /* payment-gateway-list-half.json in Resources */,
02C254D72563999300A04423 /* order-shipping-labels.json in Resources */,
45B204BC24890B1200FE6526 /* category.json in Resources */,
Expand Down Expand Up @@ -3090,6 +3104,7 @@
B5A2417D217F9ECC00595DEF /* MetaContainer.swift in Sources */,
025CA2C4238EBC4300B05C81 /* ProductShippingClassRemote.swift in Sources */,
D88D5A4B230BCF0A007B6E01 /* ProductReviewListMapper.swift in Sources */,
02EF1664292DADDE00D90AD6 /* PaymentRemote.swift in Sources */,
029C9E5C291507A40013E5EE /* UnauthenticatedRequest.swift in Sources */,
93D8BBFB226BBC5100AD2EB3 /* AccountSettings.swift in Sources */,
B53EF53821813806003E146F /* DotcomError.swift in Sources */,
Expand Down Expand Up @@ -3401,6 +3416,7 @@
CC851D1425E52AB500249E9C /* Decimal+ExtensionsTests.swift in Sources */,
B554FA8B2180B1D500C54DFF /* NotificationsRemoteTests.swift in Sources */,
B518662A20A09C6F00037A38 /* OrdersRemoteTests.swift in Sources */,
02EF166E292F0C5800D90AD6 /* PaymentRemoteTests.swift in Sources */,
B5969E1520A47F99005E9DF1 /* RemoteTests.swift in Sources */,
74C8F06E20EEC1E800B6EDC9 /* OrderNotesMapperTests.swift in Sources */,
45ED4F10239E8A54004F1BE3 /* TaxClassListMapperTest.swift in Sources */,
Expand Down
102 changes: 102 additions & 0 deletions Networking/Networking/Remote/PaymentRemote.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Foundation

/// Protocol for `PaymentRemote` mainly used for mocking.
public protocol PaymentRemoteProtocol {
/// Loads the WPCOM plan remotely that matches the product ID.
/// - Parameter productID: The ID of the WPCOM plan product.
/// - Returns: The WPCOM plan that matches the given product ID.
func loadPlan(thatMatchesID productID: Int64) async throws -> WPComPlan

/// Creates a cart with the given product ID for the site ID.
/// - Parameters:
/// - siteID: The ID of the site that the product is being added to.
/// - productID: The ID of the product to be added to the site.
/// - Returns: The remote response from creating a cart.
func createCart(siteID: Int64, productID: Int64) async throws
}

/// WPCOM Payment Endpoints
///
public class PaymentRemote: Remote, PaymentRemoteProtocol {
public func loadPlan(thatMatchesID productID: Int64) async throws -> WPComPlan {
let path = Path.products
let request = DotcomRequest(wordpressApiVersion: .mark1_5, method: .get, path: path)
let plans: [WPComPlan] = try await enqueue(request)
guard let plan = plans.first(where: { $0.productID == productID }) else {
throw LoadPlanError.noMatchingPlan
}
return plan
}

public func createCart(siteID: Int64, productID: Int64) async throws {
let path = "\(Path.cartCreation)/\(siteID)"

let parameters: [String: Any] = [
"products": [
[
"product_id": productID,
"volume": 1
]
],
// Necessary to create a persistent cart for later checkout, the default value is `true`.
"temporary": false
]
let request = DotcomRequest(wordpressApiVersion: .mark1_1, method: .post, path: path, parameters: parameters)
let response: CreateCartResponse = try await enqueue(request)
guard response.products.contains(where: { $0.productID == productID }) else {
throw CreateCartError.productNotInCart
}
}
}

/// Contains necessary data for rendering a WPCOM plan in the app.
public struct WPComPlan: Decodable, Equatable {
public let productID: Int64
public let name: String
public let formattedPrice: String

private enum CodingKeys: String, CodingKey {
case productID = "product_id"
case name = "product_name"
case formattedPrice = "formatted_price"
}
}

/// Possible error cases from loading a WPCOM plan.
public enum LoadPlanError: Error {
case noMatchingPlan
}

/// Possible error cases from creating cart for a site with a WPCOM plan.
public enum CreateCartError: Error {
case productNotInCart
}

/// Contains necessary data for handling the remote response from creating a cart.
private struct CreateCartResponse: Decodable {
let products: [Product]

private enum CodingKeys: String, CodingKey {
case products
}
}

private extension CreateCartResponse {
/// Describes a product in a cart.
struct Product: Decodable {
let productID: Int64

private enum CodingKeys: String, CodingKey {
case productID = "product_id"
}
}
}

// MARK: - Constants
//
private extension PaymentRemote {
enum Path {
static let products = "plans"
static let cartCreation = "me/shopping-cart"
}
}
2 changes: 1 addition & 1 deletion Networking/Networking/Requests/DotcomRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ struct DotcomRequest: Request {

func responseDataValidator() -> ResponseDataValidator {
switch wordpressApiVersion {
case .mark1_1, .mark1_2:
case .mark1_1, .mark1_2, .mark1_5:
return DotcomValidator()
case .wpcomMark2, .wpMark2:
return WordPressApiValidator()
Expand Down
4 changes: 4 additions & 0 deletions Networking/Networking/Settings/WordPressAPIVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ enum WordPressAPIVersion: String {
///
case mark1_2 = "rest/v1.2/"

/// WordPress.com Endpoint Mark 1.5
///
case mark1_5 = "rest/v1.5/"

/// WPcom REST API Endpoint Mark 2
///
case wpcomMark2 = "wpcom/v2/"
Expand Down
112 changes: 112 additions & 0 deletions Networking/NetworkingTests/Remote/PaymentRemoteTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import XCTest
import TestKit
@testable import Networking

final class PaymentRemoteTests: XCTestCase {
/// Mock network wrapper.
private var network: MockNetwork!

override func setUp() {
super.setUp()
network = MockNetwork()
}

override func tearDown() {
network = nil
super.tearDown()
}

// MARK: - `loadPlan`

func test_loadPlan_returns_plan_on_success() async throws {
// Given
let remote = PaymentRemote(network: network)
network.simulateResponse(requestUrlSuffix: "plans", filename: "load-plan-success")

// When
let plan = try await remote.loadPlan(thatMatchesID: Constants.planProductID)

// Then
XCTAssertEqual(plan, .init(productID: Constants.planProductID,
name: "WordPress.com eCommerce",
formattedPrice: "NT$2,230"))
}

func test_loadPlan_throws_noMatchingPlan_error_when_response_does_not_include_plan_with_given_id() async throws {
// Given
let remote = PaymentRemote(network: network)
network.simulateResponse(requestUrlSuffix: "plans", filename: "load-plan-success")

// When
await assertThrowsError {
_ = try await remote.loadPlan(thatMatchesID: 9)
} errorAssert: { error in
// Then
(error as? LoadPlanError) == .noMatchingPlan
}
}

func test_loadPlan_throws_notFound_error_when_no_response() async throws {
// Given
let remote = PaymentRemote(network: network)

// When
await assertThrowsError {
_ = try await remote.loadPlan(thatMatchesID: 9)
} errorAssert: { error in
// Then
(error as? NetworkError) == .notFound
}
}

// MARK: - `createCart`

func test_createCart_returns_on_success() async throws {
// Given
let siteID: Int64 = 606
let remote = PaymentRemote(network: network)
network.simulateResponse(requestUrlSuffix: "me/shopping-cart/\(siteID)", filename: "create-cart-success")

// When
do {
try await remote.createCart(siteID: siteID, productID: Constants.planProductID)
} catch {
// Then
XCTFail("Unexpected error: \(error)")
}
}

func test_createCart_throws_productNotInCart_error_when_response_does_not_include_plan_with_given_id() async throws {
// Given
let siteID: Int64 = 606
let remote = PaymentRemote(network: network)
network.simulateResponse(requestUrlSuffix: "me/shopping-cart/\(siteID)", filename: "create-cart-success")

// When
await assertThrowsError {
_ = try await remote.createCart(siteID: siteID, productID: 685)
} errorAssert: { error in
// Then
(error as? CreateCartError) == .productNotInCart
}
}

func test_createCart_throws_notFound_error_when_no_response() async throws {
// Given
let remote = PaymentRemote(network: network)

// When
await assertThrowsError {
_ = try await remote.createCart(siteID: 606, productID: 685)
} errorAssert: { error in
// Then
(error as? NetworkError) == .notFound
}
}
}

private extension PaymentRemoteTests {
enum Constants {
static let planProductID: Int64 = 1021
}
}
Loading