Skip to content

Commit b8a3e88

Browse files
authored
Merge pull request #7519 from woocommerce/issue/7477-payment-gateway-put
[Mobile Payments] PUT PaymentGateway network request and Action
2 parents 010953a + 3db6230 commit b8a3e88

File tree

11 files changed

+331
-9
lines changed

11 files changed

+331
-9
lines changed

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@
7777
02E7FFCB256218F600C53030 /* ShippingLabelRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E7FFCA256218F600C53030 /* ShippingLabelRemoteTests.swift */; };
7878
02E7FFCF25621C7900C53030 /* shipping-label-print.json in Resources */ = {isa = PBXBuildFile; fileRef = 02E7FFCE25621C7900C53030 /* shipping-label-print.json */; };
7979
02F096C22406691100C0C1D5 /* media-library.json in Resources */ = {isa = PBXBuildFile; fileRef = 02F096C12406691100C0C1D5 /* media-library.json */; };
80+
0313651928AE559D00EEE571 /* PaymentGatewayMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651828AE559D00EEE571 /* PaymentGatewayMapper.swift */; };
81+
0313651B28AE60E000EEE571 /* payment-gateway-cod.json in Resources */ = {isa = PBXBuildFile; fileRef = 0313651A28AE60E000EEE571 /* payment-gateway-cod.json */; };
82+
0313651D28AE625300EEE571 /* PaymentGatewayMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0313651C28AE625300EEE571 /* PaymentGatewayMapperTests.swift */; };
8083
0329CF9B27A82E19008AFF91 /* WCPayCharge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0329CF9A27A82E19008AFF91 /* WCPayCharge.swift */; };
8184
034480C327A42F9100DFACD2 /* order-with-charge.json in Resources */ = {isa = PBXBuildFile; fileRef = 034480C227A42F9100DFACD2 /* order-with-charge.json */; };
8285
0359EA0D27AAC5F80048DE2D /* WCPayChargeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0359EA0C27AAC5F80048DE2D /* WCPayChargeStatus.swift */; };
@@ -751,6 +754,9 @@
751754
02E7FFCA256218F600C53030 /* ShippingLabelRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelRemoteTests.swift; sourceTree = "<group>"; };
752755
02E7FFCE25621C7900C53030 /* shipping-label-print.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "shipping-label-print.json"; sourceTree = "<group>"; };
753756
02F096C12406691100C0C1D5 /* media-library.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "media-library.json"; sourceTree = "<group>"; };
757+
0313651828AE559D00EEE571 /* PaymentGatewayMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGatewayMapper.swift; sourceTree = "<group>"; };
758+
0313651A28AE60E000EEE571 /* payment-gateway-cod.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "payment-gateway-cod.json"; sourceTree = "<group>"; };
759+
0313651C28AE625300EEE571 /* PaymentGatewayMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentGatewayMapperTests.swift; sourceTree = "<group>"; };
754760
0329CF9A27A82E19008AFF91 /* WCPayCharge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCPayCharge.swift; sourceTree = "<group>"; };
755761
034480C227A42F9100DFACD2 /* order-with-charge.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "order-with-charge.json"; sourceTree = "<group>"; };
756762
0359EA0C27AAC5F80048DE2D /* WCPayChargeStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCPayChargeStatus.swift; sourceTree = "<group>"; };
@@ -1913,6 +1919,7 @@
19131919
0272E3FA254AABB800436277 /* order-with-line-item-attributes-before-API-support.json */,
19141920
A69FE19C2588D70E0059A96B /* order-with-deleted-refunds.json */,
19151921
261CF1B3255AD6B30090D8D3 /* payment-gateway-list.json */,
1922+
0313651A28AE60E000EEE571 /* payment-gateway-cod.json */,
19161923
261CF2CA255C50010090D8D3 /* payment-gateway-list-half.json */,
19171924
DEC51A96274DD962009F3DF4 /* plugin.json */,
19181925
DEC51A9A274E3206009F3DF4 /* plugin-inactive.json */,
@@ -2077,6 +2084,7 @@
20772084
02C254B825637BA000A04423 /* OrderShippingLabelListMapper.swift */,
20782085
D8FBFF1022D3B3FC006E3336 /* OrderStatsV4Mapper.swift */,
20792086
26731336255ACA850026F7EF /* PaymentGatewayListMapper.swift */,
2087+
0313651828AE559D00EEE571 /* PaymentGatewayMapper.swift */,
20802088
74749B96224134FF005C4CF2 /* ProductMapper.swift */,
20812089
45152810257A81730076B03C /* ProductAttributeMapper.swift */,
20822090
4515280C257A7EEC0076B03C /* ProductAttributeListMapper.swift */,
@@ -2217,6 +2225,7 @@
22172225
74C8F06D20EEC1E700B6EDC9 /* OrderNotesMapperTests.swift */,
22182226
D8FBFF1D22D51F39006E3336 /* OrderStatsMapperV4Tests.swift */,
22192227
262E5AD4255ACD6F000B2416 /* PaymentGatewayListMapperTests.swift */,
2228+
0313651C28AE625300EEE571 /* PaymentGatewayMapperTests.swift */,
22202229
D8A2849925FBB2E70019A84B /* ProductAttributeTermListMapperTests.swift */,
22212230
CE0A0F1C22398D520075ED8D /* ProductListMapperTests.swift */,
22222231
74CF0A8B22414D7800DB993F /* ProductMapperTests.swift */,
@@ -2534,6 +2543,7 @@
25342543
D865CE6B278CA266002C8520 /* stripe-payment-intent-error.json in Resources */,
25352544
CCB2CAA82620ABCC00285CA0 /* shipping-label-create-package-error.json in Resources */,
25362545
B57B1E6C21C94C9F0046E764 /* timeout_error.json in Resources */,
2546+
0313651B28AE60E000EEE571 /* payment-gateway-cod.json in Resources */,
25372547
B5C6FCD620A3768900A4F8E4 /* order.json in Resources */,
25382548
3158FE7426129D9F00E566B9 /* wcpay-account-rejected-other.json in Resources */,
25392549
CC0786C7267BB10700BA9AC1 /* shipping-label-status-success.json in Resources */,
@@ -2877,6 +2887,7 @@
28772887
CC6A1FF5270E042200F6AF4A /* OrderMetaData.swift in Sources */,
28782888
DEC51B02276AFB35009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift in Sources */,
28792889
02C254A4256371B200A04423 /* ShippingLabelSettings.swift in Sources */,
2890+
0313651928AE559D00EEE571 /* PaymentGatewayMapper.swift in Sources */,
28802891
B554FA912180BCFC00C54DFF /* NoteHash.swift in Sources */,
28812892
CE0A0F19223987DF0075ED8D /* ProductListMapper.swift in Sources */,
28822893
021C7BF723863D1800A3BCBD /* Encodable+Serialization.swift in Sources */,
@@ -3067,6 +3078,7 @@
30673078
B505F6D320BEE3A500BB1B69 /* AccountMapperTests.swift in Sources */,
30683079
CC9A254A26442D26005DE56E /* ShippingLabelCreationEligibilityMapperTests.swift in Sources */,
30693080
5726F7342460A8F00031CAAC /* CopiableTests.swift in Sources */,
3081+
0313651D28AE625300EEE571 /* PaymentGatewayMapperTests.swift in Sources */,
30703082
26615479242DA54D00A31661 /* ProductCategoyListMapperTests.swift in Sources */,
30713083
B5C6FCC820A32E4800A4F8E4 /* DateFormatterWooTests.swift in Sources */,
30723084
74C8F06A20EEBC8C00B6EDC9 /* OrderMapperTests.swift in Sources */,

Networking/Networking/Mapper/PaymentGatewayListMapper.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Foundation
44
///
55
struct PaymentGatewayListMapper: Mapper {
66

7-
/// Site Identifier associated to the shipment trackings that will be parsed.
7+
/// Site Identifier associated to the payment gateways that will be parsed.
88
/// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints don't
99
/// return the siteID for the payment gateway endpoint
1010
///
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
3+
/// Mapper for a `PaymentGateway` JSON object
4+
///
5+
struct PaymentGatewayMapper: Mapper {
6+
7+
/// Site Identifier associated to the payment gateway that will be parsed.
8+
/// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints don't
9+
/// return the siteID for the payment gateway endpoint
10+
///
11+
let siteID: Int64
12+
13+
/// (Attempts) to convert a dictionary into `PaymentGateway`
14+
///
15+
func map(response: Data) throws -> PaymentGateway {
16+
let decoder = JSONDecoder()
17+
decoder.userInfo = [
18+
.siteID: siteID,
19+
]
20+
return try decoder.decode(PaymentGatewayEnvelope.self, from: response).paymentGateway
21+
}
22+
}
23+
24+
/// PaymentGateway list disposable entity:
25+
/// `Load Payment Gateway` endpoint returns all of the gateway information within a `body` obejcts in the `data` key. This entity
26+
/// allows us to parse all the things with JSONDecoder.
27+
///
28+
private struct PaymentGatewayEnvelope: Decodable {
29+
private enum CodingKeys: String, CodingKey {
30+
case paymentGateway = "data"
31+
}
32+
33+
let paymentGateway: PaymentGateway
34+
}

Networking/Networking/Model/PaymentGateway.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public struct PaymentGateway: Equatable, GeneratedFakeable, GeneratedCopiable {
4848
}
4949

5050
// MARK: Gateway Decodable
51-
extension PaymentGateway: Decodable {
51+
extension PaymentGateway: Codable {
5252

5353
public enum DecodingError: Error {
5454
case missingSiteID
@@ -85,7 +85,7 @@ extension PaymentGateway: Decodable {
8585
}
8686

8787
// MARK: Features Decodable
88-
extension PaymentGateway.Feature: RawRepresentable, Decodable {
88+
extension PaymentGateway.Feature: RawRepresentable, Codable {
8989

9090
/// Enum containing the 'Known' Feature Keys
9191
///

Networking/Networking/Remote/PaymentGatewayRemote.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,35 @@ public class PaymentGatewayRemote: Remote {
1515
let mapper = PaymentGatewayListMapper(siteID: siteID)
1616
enqueue(request, mapper: mapper, completion: completion)
1717
}
18+
19+
// MARK: - Update Payment Gateway
20+
21+
/// Updates a `PaymentGateway`.
22+
///
23+
/// - Parameters:
24+
/// - paymentGateway: The Payment Gateway to be updated remotely.
25+
/// - completion: Closure to be executed upon completion.
26+
///
27+
public func updatePaymentGateway(_ paymentGateway: PaymentGateway,
28+
completion: @escaping (Result<PaymentGateway, Error>) -> Void) {
29+
do {
30+
let parameters = try paymentGateway.toDictionary(keyEncodingStrategy: .convertToSnakeCase)
31+
let siteID = paymentGateway.siteID
32+
let path = Constants.path + "/\(paymentGateway.gatewayID)"
33+
34+
let request = JetpackRequest(wooApiVersion: .mark3,
35+
method: .put,
36+
siteID: siteID,
37+
path: path,
38+
parameters: parameters)
39+
40+
let mapper = PaymentGatewayMapper(siteID: siteID)
41+
42+
enqueue(request, mapper: mapper, completion: completion)
43+
} catch {
44+
completion(.failure(error))
45+
}
46+
}
1847
}
1948

2049
// MARK: Constant
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import XCTest
2+
@testable import Networking
3+
4+
final class PaymentGatewayMapperTests: XCTestCase {
5+
6+
/// Dummy Site ID.
7+
///
8+
fileprivate let dummySiteID: Int64 = 12983476
9+
10+
/// Verifies that the PaymentGateway is parsed.
11+
///
12+
func test_PaymentGateway_map_parses_all_paymentGateways_in_response() throws {
13+
let paymentGateway = try mapRetrievePaymentGatewayResponse()
14+
XCTAssertNotNil(paymentGateway)
15+
}
16+
17+
/// Verifies that the `siteID` is added in the mapper, because it's not provided by the API endpoint
18+
///
19+
func test_PaymentGatewaysList_map_includes_siteID_in_parsed_results() throws {
20+
let paymentGateway = try mapRetrievePaymentGatewayResponse()
21+
XCTAssertEqual(paymentGateway.siteID, dummySiteID)
22+
}
23+
24+
/// Verifies that the fields are all parsed correctly
25+
///
26+
func test_PaymentGatewaysList_map_parses_all_fields_in_result() throws {
27+
let paymentGateway = try mapRetrievePaymentGatewayResponse()
28+
29+
let expectedPaymentGateway = PaymentGateway(siteID: dummySiteID,
30+
gatewayID: "cod",
31+
title: "Cash on delivery",
32+
description: "Pay with cash upon delivery.",
33+
enabled: true,
34+
features: [.products])
35+
36+
XCTAssertEqual(paymentGateway, expectedPaymentGateway)
37+
}
38+
}
39+
40+
41+
// MARK: - Test Helpers
42+
///
43+
private extension PaymentGatewayMapperTests {
44+
45+
/// Returns the PaymentGatewayMapper output upon receiving `filename` (Data Encoded)
46+
///
47+
func mapPaymentGateway(from filename: String) throws -> PaymentGateway {
48+
guard let response = Loader.contentsOf(filename) else {
49+
throw FileNotFoundError()
50+
}
51+
52+
return try PaymentGatewayMapper(siteID: dummySiteID).map(response: response)
53+
}
54+
55+
/// Returns the PaymentGatewayMapper output from `payment-gateway-cod.json`
56+
///
57+
func mapRetrievePaymentGatewayResponse() throws -> PaymentGateway {
58+
return try mapPaymentGateway(from: "payment-gateway-cod")
59+
}
60+
61+
struct FileNotFoundError: Error {}
62+
}

Networking/NetworkingTests/Remote/PaymentsGatewayRemoteTests.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,63 @@ final class PaymentsGatewayRemoteTests: XCTestCase {
3838
let gateways = try result.get()
3939
XCTAssertFalse(gateways.isEmpty)
4040
}
41+
42+
// MARK: - Update Payment Gateway tests
43+
44+
/// Verifies that updatepaymentGateway properly parses the `paymentGateway` sample response.
45+
///
46+
func test_updatePaymentGateway_properly_returns_parsed_paymentGateway() throws {
47+
// Given
48+
let remote = PaymentGatewayRemote(network: network)
49+
let paymentGateway = samplePaymentGateway()
50+
network.simulateResponse(requestUrlSuffix: "payment_gateways/\(paymentGateway.gatewayID)", filename: "payment-gateway-cod")
51+
52+
// When
53+
let result = waitFor { promise in
54+
remote.updatePaymentGateway(paymentGateway) { result in
55+
promise(result)
56+
}
57+
}
58+
59+
// Then
60+
XCTAssert(result.isSuccess)
61+
let returnedPaymentGateway = try XCTUnwrap(result.get())
62+
XCTAssertEqual(returnedPaymentGateway, paymentGateway)
63+
}
64+
65+
/// Verifies that updatepaymentGateway properly relays Networking Layer errors.
66+
///
67+
func test_updatePaymentGateway_properly_relays_networking_errors() throws {
68+
// Given
69+
let remote = PaymentGatewayRemote(network: network)
70+
let paymentGateway = samplePaymentGateway()
71+
72+
let error = NetworkError.unacceptableStatusCode(statusCode: 500)
73+
network.simulateError(requestUrlSuffix: "payment_gateways/\(paymentGateway.gatewayID)", error: error)
74+
75+
// When
76+
let result = waitFor { promise in
77+
remote.updatePaymentGateway(paymentGateway) { (result) in
78+
promise(result)
79+
}
80+
}
81+
82+
// Then
83+
XCTAssertTrue(result.isFailure)
84+
let resultError = try XCTUnwrap(result.failure as? NetworkError)
85+
XCTAssertEqual(resultError, .unacceptableStatusCode(statusCode: 500))
86+
}
87+
}
88+
89+
// MARK: - Helper methods
90+
91+
extension PaymentsGatewayRemoteTests {
92+
func samplePaymentGateway() -> PaymentGateway {
93+
PaymentGateway.fake().copy(siteID: sampleSiteID,
94+
gatewayID: "cod",
95+
title: "Cash on delivery",
96+
description: "Pay with cash upon delivery.",
97+
enabled: true,
98+
features: [.products])
99+
}
41100
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"data": {
3+
"id": "cod",
4+
"title": "Cash on delivery",
5+
"description": "Pay with cash upon delivery.",
6+
"order": "",
7+
"enabled": true,
8+
"method_title": "Cash on delivery",
9+
"method_description": "Have your customers pay with cash (or by other means) upon delivery.",
10+
"method_supports": [
11+
"products"
12+
]
13+
}
14+
}

Yosemite/Yosemite/Actions/PaymentGatewayAction.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,12 @@ public enum PaymentGatewayAction: Action {
66
/// Retrieves and stores all payment gateways for the provided `siteID`
77
///
88
case synchronizePaymentGateways(siteID: Int64, onCompletion: (Result<Void, Error>) -> Void)
9+
10+
/// Updates a Payment Gateway for a site given its ID and returns the updated Payment Gateway if the request succeeds.
11+
///
12+
/// - `paymentGateway`: the Payment Gateway to be updated.
13+
/// - `onCompletion`: invoked when the update finishes.
14+
///
15+
case updatePaymentGateway(_ paymentGateway: PaymentGateway,
16+
onCompletion: (Result<PaymentGateway, Error>) -> Void)
917
}

Yosemite/Yosemite/Stores/PaymentGatewayStore.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@ public final class PaymentGatewayStore: Store {
3636
switch action {
3737
case let .synchronizePaymentGateways(siteID, onCompletion):
3838
synchronizePaymentGateways(siteID: siteID, onCompletion: onCompletion)
39+
case let .updatePaymentGateway(paymentGateway, onCompletion):
40+
updatePaymentGateway(paymentGateway, onCompletion: onCompletion)
3941
}
4042
}
4143
}
4244

43-
// MARK: Storage Methods
45+
// MARK: Payment Gateway store services
4446
private extension PaymentGatewayStore {
45-
4647
/// Loads and stores all payment gateways for the provided `siteID`
4748
func synchronizePaymentGateways(siteID: Int64, onCompletion: @escaping (Result<Void, Error>) -> Void) {
4849
remote.loadAllPaymentGateways(siteID: siteID) { [weak self] result in
@@ -59,6 +60,31 @@ private extension PaymentGatewayStore {
5960
}
6061
}
6162

63+
/// Updates a paymentGateway given its details.
64+
/// After the API request succeeds, the stored paymentGateway should be updated accordingly.
65+
/// - Parameters:
66+
/// - paymentGateway: The paymentGateway to be updated
67+
/// - onCompletion: Closure to call after update is complete. Called on the main thread.
68+
///
69+
func updatePaymentGateway(_ paymentGateway: PaymentGateway,
70+
siteTimezone: TimeZone? = nil,
71+
onCompletion: @escaping (Result<PaymentGateway, Error>) -> Void) {
72+
remote.updatePaymentGateway(paymentGateway) { [weak self] result in
73+
guard let self = self else { return }
74+
switch result {
75+
case .failure(let error):
76+
onCompletion(.failure(error))
77+
case .success(let paymentGateway):
78+
self.upsertPaymentGatewaysInBackground(siteID: paymentGateway.siteID, paymentGateways: [paymentGateway]) {
79+
onCompletion(.success(paymentGateway))
80+
}
81+
}
82+
}
83+
}
84+
}
85+
86+
// MARK: Storage Methods
87+
private extension PaymentGatewayStore {
6288
/// Updates (OR Inserts) the specified ReadOnly Payment Gateways Entities
6389
/// *in a background thread*. `onCompletion` will be called on the main thread!
6490
///

0 commit comments

Comments
 (0)