Skip to content
Merged
14 changes: 14 additions & 0 deletions Fakes/Fakes/Networking.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ extension CreateProductVariation {
)
}
}
extension Customer {
/// Returns a "ready to use" type filled with fake values.
///
public static func fake() -> Customer {
.init(
customerID: .fake(),
email: .fake(),
firstName: .fake(),
lastName: .fake(),
billing: .fake(),
shipping: .fake()
)
}
}
extension DotcomError {
/// Returns a "ready to use" type filled with fake values.
///
Expand Down
24 changes: 24 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,13 @@
57BE08D82409B63800F6DCED /* reviews-missing-avatar-urls.json in Resources */ = {isa = PBXBuildFile; fileRef = 57BE08D72409B63700F6DCED /* reviews-missing-avatar-urls.json */; };
57E8FED3246616AC0057CD68 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57E8FED2246616AC0057CD68 /* Result+Extensions.swift */; };
6647C0161DAC6AB6570C53A7 /* Pods_Networking.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F3F25DC15EC1D7C631169CB5 /* Pods_Networking.framework */; };
68BD37B328D9B8BD00C2A517 /* CustomerRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD37B228D9B8BD00C2A517 /* CustomerRemoteTests.swift */; };
68C87B342862D40E00A99054 /* setting-all-except-countries.json in Resources */ = {isa = PBXBuildFile; fileRef = 68C87B332862D40E00A99054 /* setting-all-except-countries.json */; };
68CB800C28D87BC800E169F8 /* Customer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB800B28D87BC800E169F8 /* Customer.swift */; };
68CB800E28D8901B00E169F8 /* CustomerMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB800D28D8901B00E169F8 /* CustomerMapper.swift */; };
68CB801028D89A0400E169F8 /* CustomerRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB800F28D89A0400E169F8 /* CustomerRemote.swift */; };
68CB801428D8A05200E169F8 /* CustomerMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68CB801328D8A05200E169F8 /* CustomerMapperTests.swift */; };
68CB801628D8A39700E169F8 /* customer.json in Resources */ = {isa = PBXBuildFile; fileRef = 68CB801528D8A39700E169F8 /* customer.json */; };
68FBC5B828928C8C00A05461 /* WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68FBC5B728928C8C00A05461 /* WooFoundation.framework */; };
74002D6A2118B26100A63C19 /* SiteVisitStatsMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74002D692118B26000A63C19 /* SiteVisitStatsMapperTests.swift */; };
74002D6C2118B88200A63C19 /* SiteVisitStatsRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74002D6B2118B88200A63C19 /* SiteVisitStatsRemoteTests.swift */; };
Expand Down Expand Up @@ -1010,7 +1016,13 @@
5726F7332460A8F00031CAAC /* CopiableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiableTests.swift; sourceTree = "<group>"; };
57BE08D72409B63700F6DCED /* reviews-missing-avatar-urls.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "reviews-missing-avatar-urls.json"; sourceTree = "<group>"; };
57E8FED2246616AC0057CD68 /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = "<group>"; };
68BD37B228D9B8BD00C2A517 /* CustomerRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerRemoteTests.swift; sourceTree = "<group>"; };
68C87B332862D40E00A99054 /* setting-all-except-countries.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "setting-all-except-countries.json"; sourceTree = "<group>"; };
68CB800B28D87BC800E169F8 /* Customer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Customer.swift; sourceTree = "<group>"; };
68CB800D28D8901B00E169F8 /* CustomerMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerMapper.swift; sourceTree = "<group>"; };
68CB800F28D89A0400E169F8 /* CustomerRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerRemote.swift; sourceTree = "<group>"; };
68CB801328D8A05200E169F8 /* CustomerMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerMapperTests.swift; sourceTree = "<group>"; };
68CB801528D8A39700E169F8 /* customer.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = customer.json; sourceTree = "<group>"; };
68FBC5B728928C8C00A05461 /* WooFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WooFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
69314EDE650855CAF927057E /* Pods_NetworkingTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetworkingTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74002D692118B26000A63C19 /* SiteVisitStatsMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteVisitStatsMapperTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1616,6 +1628,7 @@
FE28F6EB268436C9004465C7 /* UserRemoteTests.swift */,
077F39D926A58ED700ABEADC /* SystemStatusRemoteTests.swift */,
DE34051E28BDFB0B00CF0D97 /* JetpackConnectionRemoteTests.swift */,
68BD37B228D9B8BD00C2A517 /* CustomerRemoteTests.swift */,
);
path = Remote;
sourceTree = "<group>";
Expand Down Expand Up @@ -1748,6 +1761,7 @@
FE28F6E5268429B6004465C7 /* UserRemote.swift */,
077F39D526A58E4500ABEADC /* SystemStatusRemote.swift */,
AEF94584272974F2001DCCFB /* TelemetryRemote.swift */,
68CB800F28D89A0400E169F8 /* CustomerRemote.swift */,
);
path = Remote;
sourceTree = "<group>";
Expand Down Expand Up @@ -1855,6 +1869,7 @@
FE28F6E126840DED004465C7 /* User.swift */,
DE50295828C5BD0200551736 /* JetpackUser.swift */,
DE50295A28C5F99700551736 /* DotcomUser.swift */,
68CB800B28D87BC800E169F8 /* Customer.swift */,
);
path = Model;
sourceTree = "<group>";
Expand Down Expand Up @@ -2089,6 +2104,7 @@
4513382727A96DE700AE5E78 /* inbox-note.json */,
0205021B27C86B9700FB1C6B /* inbox-note-without-isRead.json */,
68C87B332862D40E00A99054 /* setting-all-except-countries.json */,
68CB801528D8A39700E169F8 /* customer.json */,
);
path = Responses;
sourceTree = "<group>";
Expand Down Expand Up @@ -2181,6 +2197,7 @@
02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */,
0359EA1C27AADE000048DE2D /* WCPayChargeMapper.swift */,
DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */,
68CB800D28D8901B00E169F8 /* CustomerMapper.swift */,
);
path = Mapper;
sourceTree = "<group>";
Expand Down Expand Up @@ -2303,6 +2320,7 @@
DE34051C28BDF1C900CF0D97 /* JetpackConnectionURLMapperTests.swift */,
DE50296428C60A8000551736 /* JetpackUserMapperTests.swift */,
0359EA1E27AAE4680048DE2D /* WCPayChargeMapperTests.swift */,
68CB801328D8A05200E169F8 /* CustomerMapperTests.swift */,
);
path = Mapper;
sourceTree = "<group>";
Expand Down Expand Up @@ -2684,6 +2702,7 @@
7497376A2141F2BE0008C490 /* top-performers-week-alt.json in Resources */,
D865CE61278CA1AE002C8520 /* stripe-payment-intent-processing.json in Resources */,
743E84F222172D0A00FAC9D7 /* shipment_tracking_plugin_not_active.json in Resources */,
68CB801628D8A39700E169F8 /* customer.json in Resources */,
451A97DE260B59870059D135 /* shipping-label-packages-success.json in Resources */,
31D27C8F2602B553002EDB1D /* plugins.json in Resources */,
261CF1B4255AD6B30090D8D3 /* payment-gateway-list.json in Resources */,
Expand Down Expand Up @@ -2982,6 +3001,7 @@
B518662220A097C200037A38 /* Network.swift in Sources */,
B572F69A21AC475C003EEFF0 /* DevicesRemote.swift in Sources */,
3192F220260D33BB0067FEF9 /* WCPayAccount.swift in Sources */,
68CB800E28D8901B00E169F8 /* CustomerMapper.swift in Sources */,
45CCFCE227A2C9BF0012E8CB /* InboxNote.swift in Sources */,
311D412C2783BF7400052F64 /* StripeAccount.swift in Sources */,
B518662420A099BF00037A38 /* AlamofireNetwork.swift in Sources */,
Expand Down Expand Up @@ -3030,6 +3050,7 @@
020D07BE23D8570800FD9580 /* MediaListMapper.swift in Sources */,
0359EA1327AAC6D00048DE2D /* WCPayCardPaymentDetails.swift in Sources */,
CCB2CA9E262091CB00285CA0 /* SuccessDataResultMapper.swift in Sources */,
68CB801028D89A0400E169F8 /* CustomerRemote.swift in Sources */,
DE50295B28C5F99700551736 /* DotcomUser.swift in Sources */,
74C8F06820EEB7BD00B6EDC9 /* OrderNotesMapper.swift in Sources */,
24F98C582502EA8800F49B68 /* FeatureFlagMapper.swift in Sources */,
Expand All @@ -3039,6 +3060,7 @@
74046E1D217A6989007DD7BF /* SiteSetting.swift in Sources */,
B5BB1D1020A237FB00112D92 /* Address.swift in Sources */,
CE43066A23465F340073CBFF /* Refund.swift in Sources */,
68CB800C28D87BC800E169F8 /* Customer.swift in Sources */,
DE50295D28C6068B00551736 /* JetpackUserMapper.swift in Sources */,
B524194121AC60A700D6FC0A /* DotcomDevice.swift in Sources */,
D8EDFE2225EE88C9003D2213 /* ReaderConnectionToken.swift in Sources */,
Expand Down Expand Up @@ -3124,6 +3146,7 @@
CEC4BF8F234E382F008D9195 /* RefundMapperTests.swift in Sources */,
24F98C5E2502EDCF00F49B68 /* BundleWooTests.swift in Sources */,
74AB0ACA21948CE4008220CD /* CommentResultMapperTests.swift in Sources */,
68CB801428D8A05200E169F8 /* CustomerMapperTests.swift in Sources */,
02698CF824C183A5005337C4 /* ProductVariationListMapperTests.swift in Sources */,
B524194921AC659500D6FC0A /* DevicesRemoteTests.swift in Sources */,
2685C0DA263B551300D9EE97 /* AddOnGroupMapperTests.swift in Sources */,
Expand Down Expand Up @@ -3198,6 +3221,7 @@
45CCFCE827A2E5020012E8CB /* InboxNoteListMapperTests.swift in Sources */,
74002D6C2118B88200A63C19 /* SiteVisitStatsRemoteTests.swift in Sources */,
0212683524C046CB00F8A892 /* MockNetwork+Path.swift in Sources */,
68BD37B328D9B8BD00C2A517 /* CustomerRemoteTests.swift in Sources */,
B554FA932180C17200C54DFF /* NoteHashListMapperTests.swift in Sources */,
CC07866526790B1100BA9AC1 /* ShippingLabelPurchaseMapperTests.swift in Sources */,
74002D6A2118B26100A63C19 /* SiteVisitStatsMapperTests.swift in Sources */,
Expand Down
26 changes: 26 additions & 0 deletions Networking/Networking/Mapper/CustomerMapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

/// Mapper: Customer
///
struct CustomerMapper: Mapper {
/// We're injecting this field by copying it in after parsing responses, because `siteID` is not returned in any of the Customer endpoints.
///
let siteID: Int64

/// (Attempts) to convert a dictionary into a `Customer` entity
///
func map(response: Data) throws -> Customer {
let decoder = JSONDecoder()
decoder.userInfo = [.siteID: siteID]
let customer = try decoder.decode(CustomerEnvelope.self, from: response).customer
return customer
}
}

private struct CustomerEnvelope: Decodable {
let customer: Customer

private enum CodingKeys: String, CodingKey {
case customer = "data"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,33 @@ extension CouponReport {
}
}

extension Customer {
func copy(
customerID: CopiableProp<Int64> = .copy,
email: CopiableProp<String> = .copy,
firstName: NullableCopiableProp<String> = .copy,
lastName: NullableCopiableProp<String> = .copy,
billing: NullableCopiableProp<Address> = .copy,
shipping: NullableCopiableProp<Address> = .copy
) -> Customer {
let customerID = customerID ?? self.customerID
let email = email ?? self.email
let firstName = firstName ?? self.firstName
let lastName = lastName ?? self.lastName
let billing = billing ?? self.billing
let shipping = shipping ?? self.shipping

return Customer(
customerID: customerID,
email: email,
firstName: firstName,
lastName: lastName,
billing: billing,
shipping: shipping
)
}
}

extension DotcomUser {
public func copy(
id: CopiableProp<Int64> = .copy,
Expand Down
76 changes: 76 additions & 0 deletions Networking/Networking/Model/Customer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Foundation
import Codegen

/// Represents a Customer entity:
/// https://woocommerce.github.io/woocommerce-rest-api-docs/#customer-properties
///
public struct Customer: Codable, GeneratedCopiable, GeneratedFakeable {

/// Unique identifier for the customer
public let customerID: Int64

/// The email address for the customer
public let email: String

/// Customer first name
public let firstName: String?

/// Customer last name
public let lastName: String?

/// List of billing address data
public let billing: Address?

/// List of shipping address data
public let shipping: Address?

/// Customer struct initializer
///
public init(customerID: Int64,
email: String,
firstName: String?,
lastName: String?,
billing: Address?,
shipping: Address?) {
self.customerID = customerID
self.email = email
self.firstName = firstName
self.lastName = lastName
self.billing = billing
self.shipping = shipping
}

/// Public initializer for the Customer
///
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let customerID = try container.decode(Int64.self, forKey: .customerID)
let email = try container.decode(String.self, forKey: .email)
let firstName = try container.decodeIfPresent(String.self, forKey: .firstName)
let lastName = try container.decodeIfPresent(String.self, forKey: .lastName)
let billing = try? container.decode(Address.self, forKey: .billing)
let shipping = try? container.decode(Address.self, forKey: .shipping)

self.init(customerID: customerID,
email: email,
firstName: firstName,
lastName: lastName,
billing: billing,
shipping: shipping
)
}
}

/// Defines all of the Customer CodingKeys
///
extension Customer {
enum CodingKeys: String, CodingKey {
case customerID = "id"
case email
case firstName = "first_name"
case lastName = "last_name"
case billing
case shipping
}
}
23 changes: 23 additions & 0 deletions Networking/Networking/Remote/CustomerRemote.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

public class CustomerRemote: Remote {
/// Retrieves a `Customer`
///
/// - Parameters:
/// - customerID: ID of the customer that will be retrieved
/// - siteID: Site for which we'll fetch the customer.
/// - completion: Closure to be executed upon completion.
///
func retrieveCustomer(for siteID: Int64, with customerID: Int64, completion: @escaping (Result<Customer, Error>) -> Void) {
let path = "/customers/\(customerID)"
let request = JetpackRequest(wooApiVersion: .mark3,
method: .get,
siteID: siteID,
path: path,
parameters: nil
)

let mapper = CustomerMapper(siteID: siteID)
enqueue(request, mapper: mapper, completion: completion)
}
}
76 changes: 76 additions & 0 deletions Networking/NetworkingTests/Mapper/CustomerMapperTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import XCTest
@testable import Networking

/// CustomerMapper Unit Tests
///
class CustomerMapperTests: XCTestCase {

/// Dummy Site ID.
///
private let dummySiteID: Int64 = 123

/// Local file that holds Customer data representing the API endpoint
///
private let filename: String = "customer"

/// Verifies that the Customer object can be mapped fron the Encoded data
///
func test_Customer_is_mapped_from_encoded_data() {
// Given
let mapper = CustomerMapper(siteID: dummySiteID)
guard let data = Loader.contentsOf(filename) else {
XCTFail("customer.json not found")
return
}

// When
let customer = try? mapper.map(response: data)

// Then
XCTAssertNotNil(mapper)
XCTAssertNotNil(customer)
}

/// Verifies that all of the Customer response values are parsed correctly
///
func test_Customer_response_values_are_correctly_parsed() throws {
// Given
guard let customer = try mapCustomer(from: filename) else {
XCTFail()
return
}

// Then
XCTAssertNotNil(customer)
XCTAssertEqual(customer.customerID, 25)
XCTAssertEqual(customer.email, "john.doe@example.com")
XCTAssertEqual(customer.firstName, "John")
XCTAssertEqual(customer.lastName, "Doe")

let dummyAddresses = [customer.shipping, customer.billing].compactMap({ $0 })
XCTAssertEqual(dummyAddresses.count, 2)

for address in dummyAddresses {
XCTAssertEqual(address.firstName, "John")
XCTAssertEqual(address.lastName, "Doe")
XCTAssertEqual(address.company, "")
XCTAssertEqual(address.address1, "969 Market")
XCTAssertEqual(address.address2, "")
XCTAssertEqual(address.city, "San Francisco")
XCTAssertEqual(address.state, "CA")
XCTAssertEqual(address.postcode, "94103")
XCTAssertEqual(address.country, "US")
}
}
}

private extension CustomerMapperTests {
/// Returns the CustomerMapper output upon receiving `filename` (Data Encoded)
///
func mapCustomer(from filename: String) throws -> Customer? {
guard let response = Loader.contentsOf(filename) else {
return nil
}
return try! CustomerMapper(siteID: dummySiteID).map(response: response)
}
}
Loading