From fca2d2457ab0539c0e7db5995d730187d9d91eae Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 13 Oct 2022 12:30:29 +0900 Subject: [PATCH 01/21] Add Customer+ReadOnlyConvertible --- .../Tools/StorageType+Extensions.swift | 9 ++ Yosemite/Yosemite.xcodeproj/project.pbxproj | 4 + Yosemite/Yosemite/Model/Model.swift | 1 + .../Customer+ReadOnlyConvertible.swift | 91 +++++++++++++++++++ Yosemite/Yosemite/Stores/CustomerStore.swift | 12 ++- 5 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 Yosemite/Yosemite/Model/Storage/Customer+ReadOnlyConvertible.swift diff --git a/Storage/Storage/Tools/StorageType+Extensions.swift b/Storage/Storage/Tools/StorageType+Extensions.swift index 4d294d0c959..5833e52ec19 100644 --- a/Storage/Storage/Tools/StorageType+Extensions.swift +++ b/Storage/Storage/Tools/StorageType+Extensions.swift @@ -599,6 +599,15 @@ public extension StorageType { return firstObject(ofType: WCPayCharge.self, matching: predicate) } + // MARK: - Customers + + /// Returns a single Customer given a `CustomerID` + /// + func loadCustomer(siteID: Int64, customerID: Int64) -> Customer? { + let predicate = \Customer.customerID == customerID + return firstObject(ofType: Customer.self, matching: predicate) + } + // MARK: - System plugins /// Returns all stored system plugins for a provided `siteID`. diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index 734d59e6871..6ac59f950c8 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -213,6 +213,7 @@ 578CE7972475FD8200492EBF /* MockProductReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578CE7962475FD8200492EBF /* MockProductReview.swift */; }; 57DFCC7925003C4000251E0C /* FetchResultSnapshotsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DFCC7825003C4000251E0C /* FetchResultSnapshotsProvider.swift */; }; 681D952B28E0F62B00C4039E /* CustomerAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681D952A28E0F62B00C4039E /* CustomerAction.swift */; }; + 6889089F28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */; }; 689D11D52891B9A400F6A83F /* WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 689D11D42891B9A400F6A83F /* WooFoundation.framework */; }; 68BD37B528DB2E9800C2A517 /* CustomerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD37B428DB2E9800C2A517 /* CustomerStore.swift */; }; 68BD37B928DB323D00C2A517 /* CustomerStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD37B828DB323D00C2A517 /* CustomerStoreTests.swift */; }; @@ -613,6 +614,7 @@ 57DFCC7825003C4000251E0C /* FetchResultSnapshotsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchResultSnapshotsProvider.swift; sourceTree = ""; }; 585B973F61632665297738A3 /* Pods-Yosemite.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Yosemite.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-Yosemite/Pods-Yosemite.release-alpha.xcconfig"; sourceTree = ""; }; 681D952A28E0F62B00C4039E /* CustomerAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerAction.swift; sourceTree = ""; }; + 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Customer+ReadOnlyConvertible.swift"; sourceTree = ""; }; 689D11D42891B9A400F6A83F /* WooFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WooFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 68BD37B428DB2E9800C2A517 /* CustomerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerStore.swift; sourceTree = ""; }; 68BD37B828DB323D00C2A517 /* CustomerStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerStoreTests.swift; sourceTree = ""; }; @@ -1262,6 +1264,7 @@ 031C1EAB27B1873200298699 /* WCPayCardPresentReceiptDetails+ReadOnlyConvertible.swift */, 031C1EAF27B1879C00298699 /* WCPayCardPaymentDetails+ReadOnlyConvertible.swift */, 031C1EAD27B1877000298699 /* WCPayCardPresentPaymentDetails+ReadOnlyConvertible.swift */, + 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */, ); path = Storage; sourceTree = ""; @@ -2075,6 +2078,7 @@ 7499936420EFBC1B00CF01CD /* OrderNoteAction.swift in Sources */, 7455D4692141B59E00FA8C1F /* TopEarnerStatsItem+ReadOnlyConvertible.swift in Sources */, D8C11A5222DF2DA200D4A88D /* StatsActionV4.swift in Sources */, + 6889089F28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift in Sources */, 68BD37B528DB2E9800C2A517 /* CustomerStore.swift in Sources */, CE179D55235F4E1700C24EB3 /* RefundAction.swift in Sources */, CE5F9A7A22B2D455001755E8 /* Array+Helpers.swift in Sources */, diff --git a/Yosemite/Yosemite/Model/Model.swift b/Yosemite/Yosemite/Model/Model.swift index ddf2da90f79..4371d89cc67 100644 --- a/Yosemite/Yosemite/Model/Model.swift +++ b/Yosemite/Yosemite/Model/Model.swift @@ -165,6 +165,7 @@ public typealias StorageAccount = Storage.Account public typealias StorageAccountSettings = Storage.AccountSettings public typealias StorageAttribute = Storage.GenericAttribute public typealias StorageCoupon = Storage.Coupon +public typealias StorageCustomer = Storage.Customer public typealias StorageCouponSearchResult = Storage.CouponSearchResult public typealias StorageAddOnGroup = Storage.AddOnGroup public typealias StorageAnnouncement = Storage.Announcement diff --git a/Yosemite/Yosemite/Model/Storage/Customer+ReadOnlyConvertible.swift b/Yosemite/Yosemite/Model/Storage/Customer+ReadOnlyConvertible.swift new file mode 100644 index 00000000000..85af83737ca --- /dev/null +++ b/Yosemite/Yosemite/Model/Storage/Customer+ReadOnlyConvertible.swift @@ -0,0 +1,91 @@ +import Foundation +import Storage + +// MARK: - Storage.Customer: ReadOnlyConvertible +// +extension Storage.Customer: ReadOnlyConvertible { + /// Updates the `Storage.Customer` from the ReadOnly representation (`Networking.Customer`) + /// + public func update(with customer: Yosemite.Customer) { + customerID = customer.customerID + email = customer.email + firstName = customer.firstName + lastName = customer.lastName + + billingFirstName = customer.billing?.firstName + billingLastName = customer.billing?.lastName + billingCompany = customer.billing?.company + billingAddress1 = customer.billing?.address1 + billingAddress2 = customer.billing?.address2 + billingCity = customer.billing?.city + billingState = customer.billing?.state + billingPostcode = customer.billing?.postcode + billingCountry = customer.billing?.country + billingPhone = customer.billing?.phone + billingEmail = customer.billing?.email + + shippingFirstName = customer.shipping?.firstName + shippingLastName = customer.shipping?.lastName + shippingCompany = customer.shipping?.company + shippingAddress1 = customer.shipping?.address1 + shippingAddress2 = customer.shipping?.address2 + shippingCity = customer.shipping?.city + shippingState = customer.shipping?.state + shippingPostcode = customer.shipping?.postcode + shippingCountry = customer.shipping?.country + shippingPhone = customer.shipping?.phone + shippingEmail = customer.shipping?.email + } + + /// Returns a ReadOnly (`Networking.Customer`) version of the `Storage.Customer` + /// + public func toReadOnly() -> Yosemite.Customer { + return Customer( + customerID: customerID, + email: email ?? "", + firstName: firstName ?? "", + lastName: lastName ?? "", + billing: createReadOnlyBillingAddress(), + shipping: createReadOnlyShippingAddress() + ) + } + + /// Helpers + private func createReadOnlyBillingAddress() -> Yosemite.Address? { + guard let billingCountry = billingCountry else { + return nil + } + + return Address(firstName: billingFirstName ?? "", + lastName: billingLastName ?? "", + company: billingCompany ?? "", + address1: billingAddress1 ?? "", + address2: billingAddress2 ?? "", + city: billingCity ?? "", + state: billingState ?? "", + postcode: billingPostcode ?? "", + country: billingCountry, + phone: billingPhone, + email: billingEmail + ) + } + + private func createReadOnlyShippingAddress() -> Yosemite.Address? { + guard let shippingCountry = shippingCountry else { + return nil + } + + return Address(firstName: shippingFirstName ?? "", + lastName: shippingLastName ?? "", + company: shippingCompany ?? "", + address1: shippingAddress1 ?? "", + address2: shippingAddress2 ?? "", + city: shippingCity ?? "", + state: shippingState ?? "", + postcode: shippingPostcode ?? "", + country: shippingCountry, + phone: shippingPhone, + email: shippingEmail + ) + } +} diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index db62641f4bb..19803ee316f 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -7,6 +7,12 @@ public final class CustomerStore: Store { private let customerRemote: CustomerRemote private let searchRemote: WCAnalyticsCustomerRemote + // 1 - We need access to Storage, and to write operations: + // Returns a shared derived storage instance dedicated for write operations + private lazy var sharedDerivedStorage: StorageType = { + return storageManager.writerDerivedStorage + }() + init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network, @@ -127,7 +133,10 @@ public final class CustomerStore: Store { onCompletion(.success(results)) } } +} +// MARK: Storage operations +private extension CustomerStore { /// Inserts or updates CustomerSearchResults in Storage /// private func upsertSearchCustomerResults(siteID: Int64, readOnlySearchResults: [Networking.WCAnalyticsCustomer], onCompletion: @escaping () -> Void) { @@ -136,7 +145,8 @@ public final class CustomerStore: Store { // https://github.com/woocommerce/woocommerce-ios/issues/7741 } } - /// Inserts or updates Customers in Storage + + /// Inserts or updates Customer entities in Storage /// private func upsertCustomer(siteID: Int64, readOnlyCustomer: Networking.Customer, onCompletion: @escaping () -> Void) { // Logic for inserting or updating in Storage will go here. Not implemented yet. From 6cd872826041b752c4fafcc1a858364600752626 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 13 Oct 2022 13:08:36 +0900 Subject: [PATCH 02/21] Add initial upsertCustomer logic & StorageTypeExension test --- .../Tools/StorageTypeExtensionsTests.swift | 13 +++++++++ Yosemite/Yosemite/Stores/CustomerStore.swift | 29 ++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift b/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift index 68ba90cb0a4..8053f0ab06f 100644 --- a/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift +++ b/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift @@ -147,6 +147,19 @@ final class StorageTypeExtensionsTests: XCTestCase { XCTAssertEqual(coupon, storedCoupon) } + func test_loadCustomer_by_customerID() throws { + // Given + let customerID: Int64 = 123 + let customer = storage.insertNewObject(ofType: Customer.self) + customer.customerID = customerID + + // When + let storedCustomer = try XCTUnwrap(storage.loadCustomer(siteID: sampleSiteID, customerID: customerID)) + + // Then + XCTAssertEqual(customer, storedCustomer) + } + func test_loadOrderFeeLine_by_siteID_feeID() throws { // Given let feeID: Int64 = 123 diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index 19803ee316f..49bc315b4a6 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -98,7 +98,7 @@ public final class CustomerStore: Store { guard let self else { return } switch result { case .success(let customer): - self.upsertCustomer(siteID: siteID, readOnlyCustomer: customer, onCompletion: {}) + self.upsertCustomer(siteID: siteID, readOnlyCustomer: customer, in: self.sharedDerivedStorage, onCompletion: {}) onCompletion(.success(customer)) case .failure(let error): onCompletion(.failure(error)) @@ -148,8 +148,29 @@ private extension CustomerStore { /// Inserts or updates Customer entities in Storage /// - private func upsertCustomer(siteID: Int64, readOnlyCustomer: Networking.Customer, onCompletion: @escaping () -> Void) { - // Logic for inserting or updating in Storage will go here. Not implemented yet. - // https://github.com/woocommerce/woocommerce-ios/issues/7741 + private func upsertCustomer(siteID: Int64, readOnlyCustomer: Networking.Customer, in storage: StorageType, onCompletion: @escaping () -> Void) { + + // 2. Differentiate between immutable Customer that comes from the Networking layer, and what we upsert in Storage + let networkingCustomer: Networking.Customer = { + return Customer( + customerID: readOnlyCustomer.customerID, + email: readOnlyCustomer.email, + firstName: readOnlyCustomer.firstName, + lastName: readOnlyCustomer.lastName, + billing: readOnlyCustomer.billing, + shipping: readOnlyCustomer.shipping + ) + }() + // If the specific customerID for that siteID already exists, return it + // If doesn't, insert a new one in Storage + let storageCustomer: Storage.Customer = { + if let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: networkingCustomer.customerID) { + return storedCustomer + } else { + return storage.insertNewObject(ofType: Storage.Customer.self) + } + }() + // 3. Update the entity + storageCustomer.update(with: readOnlyCustomer) } } From 1d89a6c1fa64215563f55dc2be1fde5c4d1cdf44 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 13 Oct 2022 13:44:39 +0900 Subject: [PATCH 03/21] Add upsertSearchCustomerResults & ReadOnlyConvertible --- .../Storage/Tools/StorageType+Extensions.swift | 6 ++++++ .../Tools/StorageTypeExtensionsTests.swift | 13 +++++++++++++ Yosemite/Yosemite.xcodeproj/project.pbxproj | 4 ++++ Yosemite/Yosemite/Model/Model.swift | 2 ++ ...stomerSearchResult+ReadOnlyConvertible.swift | 16 ++++++++++++++++ Yosemite/Yosemite/Stores/CustomerStore.swift | 17 ++++++++++++----- 6 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift diff --git a/Storage/Storage/Tools/StorageType+Extensions.swift b/Storage/Storage/Tools/StorageType+Extensions.swift index 5833e52ec19..6f9440f4f27 100644 --- a/Storage/Storage/Tools/StorageType+Extensions.swift +++ b/Storage/Storage/Tools/StorageType+Extensions.swift @@ -607,6 +607,12 @@ public extension StorageType { let predicate = \Customer.customerID == customerID return firstObject(ofType: Customer.self, matching: predicate) } + /// Returns a CustomerSearchResult given a `CustomerID` + /// + func loadCustomerSearchResult(siteID: Int64, customerID: Int64) -> CustomerSearchResult? { + let predicate = \CustomerSearchResult.customerID == customerID + return firstObject(ofType: CustomerSearchResult.self, matching: predicate) + } // MARK: - System plugins diff --git a/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift b/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift index 8053f0ab06f..ee53fdedbf4 100644 --- a/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift +++ b/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift @@ -160,6 +160,19 @@ final class StorageTypeExtensionsTests: XCTestCase { XCTAssertEqual(customer, storedCustomer) } + func test_loadCustomerSearchResult_by_customerID() throws { + // Given + let customerID: Int64 = 123 + let customerSearchResult = storage.insertNewObject(ofType: CustomerSearchResult.self) + customerSearchResult.customerID = customerID + + // When + let storedCustomerSearchResult = try XCTUnwrap(storage.loadCustomerSearchResult(siteID: sampleSiteID, customerID: customerID)) + + // Then + XCTAssertEqual(customerSearchResult, storedCustomerSearchResult) + } + func test_loadOrderFeeLine_by_siteID_feeID() throws { // Given let feeID: Int64 = 123 diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index 6ac59f950c8..d7c1fc84dc3 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -214,6 +214,7 @@ 57DFCC7925003C4000251E0C /* FetchResultSnapshotsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DFCC7825003C4000251E0C /* FetchResultSnapshotsProvider.swift */; }; 681D952B28E0F62B00C4039E /* CustomerAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681D952A28E0F62B00C4039E /* CustomerAction.swift */; }; 6889089F28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */; }; + 688908A128F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 688908A028F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift */; }; 689D11D52891B9A400F6A83F /* WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 689D11D42891B9A400F6A83F /* WooFoundation.framework */; }; 68BD37B528DB2E9800C2A517 /* CustomerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD37B428DB2E9800C2A517 /* CustomerStore.swift */; }; 68BD37B928DB323D00C2A517 /* CustomerStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD37B828DB323D00C2A517 /* CustomerStoreTests.swift */; }; @@ -615,6 +616,7 @@ 585B973F61632665297738A3 /* Pods-Yosemite.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Yosemite.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-Yosemite/Pods-Yosemite.release-alpha.xcconfig"; sourceTree = ""; }; 681D952A28E0F62B00C4039E /* CustomerAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerAction.swift; sourceTree = ""; }; 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Customer+ReadOnlyConvertible.swift"; sourceTree = ""; }; + 688908A028F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomerSearchResult+ReadOnlyConvertible.swift"; sourceTree = ""; }; 689D11D42891B9A400F6A83F /* WooFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WooFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 68BD37B428DB2E9800C2A517 /* CustomerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerStore.swift; sourceTree = ""; }; 68BD37B828DB323D00C2A517 /* CustomerStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerStoreTests.swift; sourceTree = ""; }; @@ -1265,6 +1267,7 @@ 031C1EAF27B1879C00298699 /* WCPayCardPaymentDetails+ReadOnlyConvertible.swift */, 031C1EAD27B1877000298699 /* WCPayCardPresentPaymentDetails+ReadOnlyConvertible.swift */, 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */, + 688908A028F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift */, ); path = Storage; sourceTree = ""; @@ -1925,6 +1928,7 @@ E1BD4D0027ABF84D006416D9 /* CardPresentPaymentsConfiguration.swift in Sources */, CE3B7AD52225EBF10050FE4B /* OrderStatusAction.swift in Sources */, 7493750C224987D9007D85D1 /* ProductAttribute+ReadOnlyConvertible.swift in Sources */, + 688908A128F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift in Sources */, 744A3219216D55F80051439B /* SiteVisitStatsItem+ReadOnlyConvertible.swift in Sources */, 45151A8F27B156E40080845F /* InboxNotesAction.swift in Sources */, 45E462122684C7A400011BF2 /* DataAction.swift in Sources */, diff --git a/Yosemite/Yosemite/Model/Model.swift b/Yosemite/Yosemite/Model/Model.swift index 4371d89cc67..4903e365909 100644 --- a/Yosemite/Yosemite/Model/Model.swift +++ b/Yosemite/Yosemite/Model/Model.swift @@ -158,6 +158,7 @@ public typealias WCPayCardPaymentDetails = Networking.WCPayCardPaymentDetails public typealias WCPayCardPresentReceiptDetails = Networking.WCPayCardPresentReceiptDetails public typealias WCPayPaymentMethodDetails = Networking.WCPayPaymentMethodDetails public typealias WCPayChargeStatus = Networking.WCPayChargeStatus +public typealias WCAnalyticsCustomer = Networking.WCAnalyticsCustomer // MARK: - Exported Storage Symbols @@ -166,6 +167,7 @@ public typealias StorageAccountSettings = Storage.AccountSettings public typealias StorageAttribute = Storage.GenericAttribute public typealias StorageCoupon = Storage.Coupon public typealias StorageCustomer = Storage.Customer +public typealias StorageCustomerSearchResult = Storage.CustomerSearchResult public typealias StorageCouponSearchResult = Storage.CouponSearchResult public typealias StorageAddOnGroup = Storage.AddOnGroup public typealias StorageAnnouncement = Storage.Announcement diff --git a/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift b/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift new file mode 100644 index 00000000000..3539d03a80c --- /dev/null +++ b/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift @@ -0,0 +1,16 @@ +import Foundation +import Storage + +// MARK: - Storage.CustomerSearchResult: ReadOnlyConvertible +extension Storage.CustomerSearchResult: ReadOnlyConvertible { + /// Updates the `Storage.CustomerSearchResult` from the ReadOnly representation (`Networking.WCAnalyticsCustomer`) + /// + public func update(with searchResult: Yosemite.WCAnalyticsCustomer) { + customerID = searchResult.userID + } + + /// Returns a ReadOnly (`Networking.WCAnalyticsCustomer`) version of the `Storage.CustomerSearchResult` + public func toReadOnly() -> Yosemite.WCAnalyticsCustomer { + return WCAnalyticsCustomer(userID: customerID, name: "") + } +} diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index 49bc315b4a6..645ab44f4f3 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -129,7 +129,7 @@ public final class CustomerStore: Store { } group.notify(queue: .main) { - self.upsertSearchCustomerResults(siteID: siteID, readOnlySearchResults: searchResults, onCompletion: {}) + self.upsertSearchCustomerResults(siteID: siteID, readOnlySearchResults: searchResults, in: self.sharedDerivedStorage, onCompletion: {}) onCompletion(.success(results)) } } @@ -139,10 +139,17 @@ public final class CustomerStore: Store { private extension CustomerStore { /// Inserts or updates CustomerSearchResults in Storage /// - private func upsertSearchCustomerResults(siteID: Int64, readOnlySearchResults: [Networking.WCAnalyticsCustomer], onCompletion: @escaping () -> Void) { - for _ in readOnlySearchResults { - // Logic for inserting or updating in Storage will go here. Not implemented yet. - // https://github.com/woocommerce/woocommerce-ios/issues/7741 + private func upsertSearchCustomerResults(siteID: Int64, readOnlySearchResults: [Networking.WCAnalyticsCustomer], in storage: StorageType, onCompletion: @escaping () -> Void) { + for searchResult in readOnlySearchResults { + let storageSearchResult: Storage.CustomerSearchResult = { + if let storedSearchResult = storage.loadCustomerSearchResult(siteID: siteID, customerID: searchResult.userID) { + return storedSearchResult + } else { + return storage.insertNewObject(ofType: Storage.CustomerSearchResult.self) + } + }() + // Update + storageSearchResult.update(with: searchResult) } } From 60f2f65e8251d296d82fadb87adcda4e9a10ef30 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Thu, 13 Oct 2022 13:46:51 +0900 Subject: [PATCH 04/21] Fix whitespace & line length --- .../Storage/CustomerSearchResult+ReadOnlyConvertible.swift | 2 +- Yosemite/Yosemite/Stores/CustomerStore.swift | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift b/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift index 3539d03a80c..16504804b38 100644 --- a/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift +++ b/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift @@ -8,7 +8,7 @@ extension Storage.CustomerSearchResult: ReadOnlyConvertible { public func update(with searchResult: Yosemite.WCAnalyticsCustomer) { customerID = searchResult.userID } - + /// Returns a ReadOnly (`Networking.WCAnalyticsCustomer`) version of the `Storage.CustomerSearchResult` public func toReadOnly() -> Yosemite.WCAnalyticsCustomer { return WCAnalyticsCustomer(userID: customerID, name: "") diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index 645ab44f4f3..7613043b85f 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -139,7 +139,10 @@ public final class CustomerStore: Store { private extension CustomerStore { /// Inserts or updates CustomerSearchResults in Storage /// - private func upsertSearchCustomerResults(siteID: Int64, readOnlySearchResults: [Networking.WCAnalyticsCustomer], in storage: StorageType, onCompletion: @escaping () -> Void) { + private func upsertSearchCustomerResults(siteID: Int64, + readOnlySearchResults: [Networking.WCAnalyticsCustomer], + in storage: StorageType, + onCompletion: @escaping () -> Void) { for searchResult in readOnlySearchResults { let storageSearchResult: Storage.CustomerSearchResult = { if let storedSearchResult = storage.loadCustomerSearchResult(siteID: siteID, customerID: searchResult.userID) { From 1d400756fb7622592f29e9af596a8481e1a79a60 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Oct 2022 11:17:56 +0900 Subject: [PATCH 05/21] Add siteID to loadCustomer & loadCustomerResult --- .../Tools/StorageType+Extensions.swift | 6 ++-- ...omerSearchResult+ReadOnlyConvertible.swift | 8 ++--- Yosemite/Yosemite/Stores/CustomerStore.swift | 32 +++++++++---------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Storage/Storage/Tools/StorageType+Extensions.swift b/Storage/Storage/Tools/StorageType+Extensions.swift index 6f9440f4f27..0723051c82d 100644 --- a/Storage/Storage/Tools/StorageType+Extensions.swift +++ b/Storage/Storage/Tools/StorageType+Extensions.swift @@ -604,13 +604,13 @@ public extension StorageType { /// Returns a single Customer given a `CustomerID` /// func loadCustomer(siteID: Int64, customerID: Int64) -> Customer? { - let predicate = \Customer.customerID == customerID + let predicate = \Customer.siteID == siteID && \Customer.customerID == customerID return firstObject(ofType: Customer.self, matching: predicate) } /// Returns a CustomerSearchResult given a `CustomerID` /// - func loadCustomerSearchResult(siteID: Int64, customerID: Int64) -> CustomerSearchResult? { - let predicate = \CustomerSearchResult.customerID == customerID + func loadCustomerSearchResult(siteID: Int64, keyword: String) -> CustomerSearchResult? { + let predicate = \CustomerSearchResult.siteID == siteID && \CustomerSearchResult.keyword == keyword return firstObject(ofType: CustomerSearchResult.self, matching: predicate) } diff --git a/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift b/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift index 16504804b38..200e296722d 100644 --- a/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift +++ b/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift @@ -5,12 +5,12 @@ import Storage extension Storage.CustomerSearchResult: ReadOnlyConvertible { /// Updates the `Storage.CustomerSearchResult` from the ReadOnly representation (`Networking.WCAnalyticsCustomer`) /// - public func update(with searchResult: Yosemite.WCAnalyticsCustomer) { - customerID = searchResult.userID + public func update(with searchResult: [Yosemite.WCAnalyticsCustomer]) { + } /// Returns a ReadOnly (`Networking.WCAnalyticsCustomer`) version of the `Storage.CustomerSearchResult` - public func toReadOnly() -> Yosemite.WCAnalyticsCustomer { - return WCAnalyticsCustomer(userID: customerID, name: "") + public func toReadOnly() -> [Yosemite.WCAnalyticsCustomer] { + return [WCAnalyticsCustomer(userID: 2, name: ""), WCAnalyticsCustomer(userID: 2, name: "")] } } diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index 7613043b85f..04281c08cee 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -6,12 +6,7 @@ public final class CustomerStore: Store { private let customerRemote: CustomerRemote private let searchRemote: WCAnalyticsCustomerRemote - - // 1 - We need access to Storage, and to write operations: - // Returns a shared derived storage instance dedicated for write operations - private lazy var sharedDerivedStorage: StorageType = { - return storageManager.writerDerivedStorage - }() + private let sharedDerivedStorage: StorageType init(dispatcher: Dispatcher, storageManager: StorageManagerType, @@ -20,6 +15,7 @@ public final class CustomerStore: Store { searchRemote: WCAnalyticsCustomerRemote) { self.customerRemote = customerRemote self.searchRemote = searchRemote + self.sharedDerivedStorage = storageManager.writerDerivedStorage super.init(dispatcher: dispatcher, storageManager: storageManager, network: network) } @@ -143,20 +139,20 @@ private extension CustomerStore { readOnlySearchResults: [Networking.WCAnalyticsCustomer], in storage: StorageType, onCompletion: @escaping () -> Void) { - for searchResult in readOnlySearchResults { - let storageSearchResult: Storage.CustomerSearchResult = { - if let storedSearchResult = storage.loadCustomerSearchResult(siteID: siteID, customerID: searchResult.userID) { - return storedSearchResult - } else { - return storage.insertNewObject(ofType: Storage.CustomerSearchResult.self) - } - }() +// for searchResult in readOnlySearchResults { +// let storageSearchResult: Storage.CustomerSearchResult = { +// if let storedSearchResult = storage.loadCustomerSearchResult(siteID: siteID, customerID: searchResult.userID) { +// return storedSearchResult +// } else { +// return storage.insertNewObject(ofType: Storage.CustomerSearchResult.self) +// } +// }() // Update - storageSearchResult.update(with: searchResult) - } +// storageSearchResult.update(with: searchResult) +// } } - /// Inserts or updates Customer entities in Storage + /// Inserts or updates Customer entities into Storage /// private func upsertCustomer(siteID: Int64, readOnlyCustomer: Networking.Customer, in storage: StorageType, onCompletion: @escaping () -> Void) { @@ -175,8 +171,10 @@ private extension CustomerStore { // If doesn't, insert a new one in Storage let storageCustomer: Storage.Customer = { if let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: networkingCustomer.customerID) { + print("Customer exists on Storage. ID: \(networkingCustomer.customerID)") return storedCustomer } else { + print("Customer does not exist on Storage. ID: \(networkingCustomer.customerID). Inserting new") return storage.insertNewObject(ofType: Storage.Customer.self) } }() From b25fd0feb89a90675ca6aeb8ddf084cdf9863171 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Oct 2022 11:29:18 +0900 Subject: [PATCH 06/21] Modify loadCustomer tests for siteID and keyword params --- .../Tools/StorageTypeExtensionsTests.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift b/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift index ee53fdedbf4..fb08429aab6 100644 --- a/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift +++ b/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift @@ -147,29 +147,33 @@ final class StorageTypeExtensionsTests: XCTestCase { XCTAssertEqual(coupon, storedCoupon) } - func test_loadCustomer_by_customerID() throws { + func test_loadCustomer_by_siteID_and_customerID() throws { // Given let customerID: Int64 = 123 let customer = storage.insertNewObject(ofType: Customer.self) + customer.siteID = sampleSiteID customer.customerID = customerID // When let storedCustomer = try XCTUnwrap(storage.loadCustomer(siteID: sampleSiteID, customerID: customerID)) // Then + XCTAssertEqual(storedCustomer.siteID, sampleSiteID) XCTAssertEqual(customer, storedCustomer) } - func test_loadCustomerSearchResult_by_customerID() throws { + func test_loadCustomerSearchResult_by_siteID_and_keyword() throws { // Given - let customerID: Int64 = 123 + let keyword: String = "some keyword" let customerSearchResult = storage.insertNewObject(ofType: CustomerSearchResult.self) - customerSearchResult.customerID = customerID + customerSearchResult.siteID = sampleSiteID + customerSearchResult.keyword = keyword // When - let storedCustomerSearchResult = try XCTUnwrap(storage.loadCustomerSearchResult(siteID: sampleSiteID, customerID: customerID)) + let storedCustomerSearchResult = try XCTUnwrap(storage.loadCustomerSearchResult(siteID: sampleSiteID, keyword: keyword )) // Then + XCTAssertEqual(customerSearchResult.siteID, sampleSiteID) XCTAssertEqual(customerSearchResult, storedCustomerSearchResult) } From 6c3d363765f8e112f7c28cdf89486eb7ff188efd Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Oct 2022 13:41:44 +0900 Subject: [PATCH 07/21] Update Customer Copiable with siteID --- Fakes/Fakes/Networking.generated.swift | 1 + .../Copiable/Models+Copiable.generated.swift | 5 ++++- Networking/Networking/Model/Customer.swift | 17 +++++++++++++++-- .../Storage/Customer+ReadOnlyConvertible.swift | 2 ++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index 92703032655..c80eaab681c 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -186,6 +186,7 @@ extension Customer { /// public static func fake() -> Customer { .init( + siteID: .fake(), customerID: .fake(), email: .fake(), firstName: .fake(), diff --git a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift index 117dc48d6bc..94d6219f50f 100644 --- a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -170,7 +170,8 @@ extension CouponReport { } extension Customer { - func copy( + public func copy( + siteID: CopiableProp = .copy, customerID: CopiableProp = .copy, email: CopiableProp = .copy, firstName: NullableCopiableProp = .copy, @@ -178,6 +179,7 @@ extension Customer { billing: NullableCopiableProp
= .copy, shipping: NullableCopiableProp
= .copy ) -> Customer { + let siteID = siteID ?? self.siteID let customerID = customerID ?? self.customerID let email = email ?? self.email let firstName = firstName ?? self.firstName @@ -186,6 +188,7 @@ extension Customer { let shipping = shipping ?? self.shipping return Customer( + siteID: siteID, customerID: customerID, email: email, firstName: firstName, diff --git a/Networking/Networking/Model/Customer.swift b/Networking/Networking/Model/Customer.swift index 70d57332fe8..0e8db6e7cb8 100644 --- a/Networking/Networking/Model/Customer.swift +++ b/Networking/Networking/Model/Customer.swift @@ -5,6 +5,8 @@ import Codegen /// https://woocommerce.github.io/woocommerce-rest-api-docs/#customer-properties /// public struct Customer: Codable, GeneratedCopiable, GeneratedFakeable { + /// The siteID for the customer + public let siteID: Int64 /// Unique identifier for the customer public let customerID: Int64 @@ -26,12 +28,14 @@ public struct Customer: Codable, GeneratedCopiable, GeneratedFakeable { /// Customer struct initializer /// - public init(customerID: Int64, + public init(siteID: Int64, + customerID: Int64, email: String, firstName: String?, lastName: String?, billing: Address?, shipping: Address?) { + self.siteID = siteID self.customerID = customerID self.email = email self.firstName = firstName @@ -43,6 +47,10 @@ public struct Customer: Codable, GeneratedCopiable, GeneratedFakeable { /// Public initializer for the Customer /// public init(from decoder: Decoder) throws { + guard let siteID = decoder.userInfo[.siteID] as? Int64 else { + throw CustomerDecodingError.missingSiteID + } + let container = try decoder.container(keyedBy: CodingKeys.self) let customerID = try container.decode(Int64.self, forKey: .customerID) @@ -52,7 +60,8 @@ public struct Customer: Codable, GeneratedCopiable, GeneratedFakeable { let billing = try? container.decode(Address.self, forKey: .billing) let shipping = try? container.decode(Address.self, forKey: .shipping) - self.init(customerID: customerID, + self.init(siteID: siteID, + customerID: customerID, email: email, firstName: firstName, lastName: lastName, @@ -73,4 +82,8 @@ extension Customer { case billing case shipping } + + enum CustomerDecodingError: Error { + case missingSiteID + } } diff --git a/Yosemite/Yosemite/Model/Storage/Customer+ReadOnlyConvertible.swift b/Yosemite/Yosemite/Model/Storage/Customer+ReadOnlyConvertible.swift index 85af83737ca..f0a5dd7fbe4 100644 --- a/Yosemite/Yosemite/Model/Storage/Customer+ReadOnlyConvertible.swift +++ b/Yosemite/Yosemite/Model/Storage/Customer+ReadOnlyConvertible.swift @@ -8,6 +8,7 @@ extension Storage.Customer: ReadOnlyConvertible { /// public func update(with customer: Yosemite.Customer) { customerID = customer.customerID + siteID = customer.siteID email = customer.email firstName = customer.firstName lastName = customer.lastName @@ -41,6 +42,7 @@ extension Storage.Customer: ReadOnlyConvertible { /// public func toReadOnly() -> Yosemite.Customer { return Customer( + siteID: siteID, customerID: customerID, email: email ?? "", firstName: firstName ?? "", From 08dbbbed6f9ac19529feb01f07d097119f0a0661 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Oct 2022 13:45:40 +0900 Subject: [PATCH 08/21] Use storage.perform when upsertCustomer --- Yosemite/Yosemite/Stores/CustomerStore.swift | 48 +++++++++----------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index 04281c08cee..1cb0a77b7eb 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -94,8 +94,9 @@ public final class CustomerStore: Store { guard let self else { return } switch result { case .success(let customer): - self.upsertCustomer(siteID: siteID, readOnlyCustomer: customer, in: self.sharedDerivedStorage, onCompletion: {}) - onCompletion(.success(customer)) + self.upsertCustomer(siteID: siteID, readOnlyCustomer: customer, in: self.sharedDerivedStorage, onCompletion: { + onCompletion(.success(customer)) + }) case .failure(let error): onCompletion(.failure(error)) } @@ -156,29 +157,24 @@ private extension CustomerStore { /// private func upsertCustomer(siteID: Int64, readOnlyCustomer: Networking.Customer, in storage: StorageType, onCompletion: @escaping () -> Void) { - // 2. Differentiate between immutable Customer that comes from the Networking layer, and what we upsert in Storage - let networkingCustomer: Networking.Customer = { - return Customer( - customerID: readOnlyCustomer.customerID, - email: readOnlyCustomer.email, - firstName: readOnlyCustomer.firstName, - lastName: readOnlyCustomer.lastName, - billing: readOnlyCustomer.billing, - shipping: readOnlyCustomer.shipping - ) - }() - // If the specific customerID for that siteID already exists, return it - // If doesn't, insert a new one in Storage - let storageCustomer: Storage.Customer = { - if let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: networkingCustomer.customerID) { - print("Customer exists on Storage. ID: \(networkingCustomer.customerID)") - return storedCustomer - } else { - print("Customer does not exist on Storage. ID: \(networkingCustomer.customerID). Inserting new") - return storage.insertNewObject(ofType: Storage.Customer.self) - } - }() - // 3. Update the entity - storageCustomer.update(with: readOnlyCustomer) + storage.perform { + // If the specific customerID for that siteID already exists, return it + // If doesn't, insert a new one in Storage + let storageCustomer: Storage.Customer = { + if let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: readOnlyCustomer.customerID) { + print("Customer exists on Storage. ID: \(readOnlyCustomer.customerID)") + return storedCustomer + } else { + print("Customer does not exist on Storage. ID: \(readOnlyCustomer.customerID). Inserting new") + return storage.insertNewObject(ofType: Storage.Customer.self) + } + }() + // 3. Update the entity + storageCustomer.update(with: readOnlyCustomer) + } + + storageManager.saveDerivedType(derivedStorage: storage) { + DispatchQueue.main.async(execute: onCompletion) + } } } From 8bb6b9d783d660c90e72f616dbc1f593b7205a6a Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Oct 2022 13:46:03 +0900 Subject: [PATCH 09/21] Unit test for retrieveCustomer upserts data --- .../Stores/CustomerStoreTests.swift | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift b/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift index ffecf0c24bc..0968af3ad1e 100644 --- a/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift @@ -1,11 +1,13 @@ import XCTest @testable import Networking @testable import Yosemite +@testable import Storage -class CustomerStoreTests: XCTestCase { +final class CustomerStoreTests: XCTestCase { private var dispatcher: Dispatcher! private var storageManager: MockStorageManager! + private var viewStorage: StorageType! private var network: MockNetwork! private var customerRemote: CustomerRemote! private var searchRemote: WCAnalyticsCustomerRemote! @@ -18,6 +20,7 @@ class CustomerStoreTests: XCTestCase { super.setUp() dispatcher = Dispatcher() storageManager = MockStorageManager() + viewStorage = storageManager.viewStorage network = MockNetwork() customerRemote = CustomerRemote(network: network) searchRemote = WCAnalyticsCustomerRemote(network: network) @@ -35,7 +38,7 @@ class CustomerStoreTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "", filename: "customer") // When - let result: Result = waitFor { promise in + let result: Result = waitFor { promise in let action = CustomerAction.retrieveCustomer(siteID: self.dummySiteID, customerID: self.dummyCustomerID) { result in promise(result) } @@ -76,7 +79,7 @@ class CustomerStoreTests: XCTestCase { network.simulateError(requestUrlSuffix: "", error: expectedError) // When - let result: Result = waitFor { promise in + let result: Result = waitFor { promise in let action = CustomerAction.retrieveCustomer(siteID: self.dummySiteID, customerID: self.dummyCustomerID) { result in promise(result) } @@ -96,7 +99,7 @@ class CustomerStoreTests: XCTestCase { network.simulateResponse(requestUrlSuffix: "customers/3", filename: "customer") // When - let response: Result<[Customer], Error> = waitFor { promise in + let response: Result<[Networking.Customer], Error> = waitFor { promise in let action = CustomerAction.searchCustomers(siteID: self.dummySiteID, keyword: self.dummyKeyword) { result in promise(result) } @@ -114,7 +117,7 @@ class CustomerStoreTests: XCTestCase { network.simulateError(requestUrlSuffix: "", error: expectedError) // When - let result: Result<[Customer], Error> = waitFor { promise in + let result: Result<[Networking.Customer], Error> = waitFor { promise in let action = CustomerAction.searchCustomers( siteID: self.dummySiteID, keyword: self.dummyKeyword) { result in @@ -127,4 +130,28 @@ class CustomerStoreTests: XCTestCase { XCTAssertTrue(result.isFailure) XCTAssertEqual(result.failure as? NetworkError, expectedError) } + + func test_retrieveCustomer_upserts_the_returned_Customer() { + // Given + network.simulateResponse(requestUrlSuffix: "customers/25", filename: "customer") + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.Customer.self), 0) + + // When + let result: Result = waitFor { promise in + let action = CustomerAction.retrieveCustomer(siteID: self.dummySiteID, customerID: self.dummyCustomerID) { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + XCTAssertTrue(result.isSuccess) + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.Customer.self), 1) + + let storedCustomer = viewStorage.loadCustomer(siteID: dummySiteID, customerID: dummyCustomerID) + XCTAssertNotNil(storedCustomer) + XCTAssertEqual(storedCustomer?.siteID, dummySiteID) + XCTAssertEqual(storedCustomer?.customerID, dummyCustomerID) + XCTAssertEqual(storedCustomer?.firstName, "John") + } } From 00432135459a96033b2ee337ff36a1c886636c17 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Oct 2022 14:07:24 +0900 Subject: [PATCH 10/21] Make WCAnalytics Copiable & Fakeable --- Fakes/Fakes/Networking.generated.swift | 10 ++++++++++ .../Copiable/Models+Copiable.generated.swift | 15 +++++++++++++++ .../Networking/Model/WCAnalyticsCustomer.swift | 3 ++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index c80eaab681c..9035b97c433 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -1695,6 +1695,16 @@ extension User { ) } } +extension WCAnalyticsCustomer { + /// Returns a "ready to use" type filled with fake values. + /// + public static func fake() -> WCAnalyticsCustomer { + .init( + userID: .fake(), + name: .fake() + ) + } +} extension WCPayAccountStatusEnum { /// Returns a "ready to use" type filled with fake values. /// diff --git a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift index 94d6219f50f..83e23848ca9 100644 --- a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -1951,6 +1951,21 @@ extension TopEarnerStatsItem { } } +extension WCAnalyticsCustomer { + public func copy( + userID: CopiableProp = .copy, + name: NullableCopiableProp = .copy + ) -> WCAnalyticsCustomer { + let userID = userID ?? self.userID + let name = name ?? self.name + + return WCAnalyticsCustomer( + userID: userID, + name: name + ) + } +} + extension WCPayCardPaymentDetails { public func copy( brand: CopiableProp = .copy, diff --git a/Networking/Networking/Model/WCAnalyticsCustomer.swift b/Networking/Networking/Model/WCAnalyticsCustomer.swift index 641167f02d9..9ea39f05789 100644 --- a/Networking/Networking/Model/WCAnalyticsCustomer.swift +++ b/Networking/Networking/Model/WCAnalyticsCustomer.swift @@ -1,6 +1,7 @@ import Foundation +import Codegen -public struct WCAnalyticsCustomer: Codable { +public struct WCAnalyticsCustomer: Codable, GeneratedCopiable, GeneratedFakeable { /// Unique identifier for the user public let userID: Int64 From e57ddbc4766c19d6aee9cf10f7b774cb1bc1a081 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Tue, 18 Oct 2022 16:55:19 +0900 Subject: [PATCH 11/21] Add upsertCustomerSearchResult to Storage --- Yosemite/Yosemite.xcodeproj/project.pbxproj | 4 -- Yosemite/Yosemite/Model/Model.swift | 1 - ...omerSearchResult+ReadOnlyConvertible.swift | 16 ------ Yosemite/Yosemite/Stores/CustomerStore.swift | 52 ++++++++++++------- .../Stores/CustomerStoreTests.swift | 30 +++++++++++ 5 files changed, 64 insertions(+), 39 deletions(-) delete mode 100644 Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index d7c1fc84dc3..6ac59f950c8 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -214,7 +214,6 @@ 57DFCC7925003C4000251E0C /* FetchResultSnapshotsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57DFCC7825003C4000251E0C /* FetchResultSnapshotsProvider.swift */; }; 681D952B28E0F62B00C4039E /* CustomerAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 681D952A28E0F62B00C4039E /* CustomerAction.swift */; }; 6889089F28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */; }; - 688908A128F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 688908A028F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift */; }; 689D11D52891B9A400F6A83F /* WooFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 689D11D42891B9A400F6A83F /* WooFoundation.framework */; }; 68BD37B528DB2E9800C2A517 /* CustomerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD37B428DB2E9800C2A517 /* CustomerStore.swift */; }; 68BD37B928DB323D00C2A517 /* CustomerStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BD37B828DB323D00C2A517 /* CustomerStoreTests.swift */; }; @@ -616,7 +615,6 @@ 585B973F61632665297738A3 /* Pods-Yosemite.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Yosemite.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-Yosemite/Pods-Yosemite.release-alpha.xcconfig"; sourceTree = ""; }; 681D952A28E0F62B00C4039E /* CustomerAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerAction.swift; sourceTree = ""; }; 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Customer+ReadOnlyConvertible.swift"; sourceTree = ""; }; - 688908A028F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomerSearchResult+ReadOnlyConvertible.swift"; sourceTree = ""; }; 689D11D42891B9A400F6A83F /* WooFoundation.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WooFoundation.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 68BD37B428DB2E9800C2A517 /* CustomerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerStore.swift; sourceTree = ""; }; 68BD37B828DB323D00C2A517 /* CustomerStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerStoreTests.swift; sourceTree = ""; }; @@ -1267,7 +1265,6 @@ 031C1EAF27B1879C00298699 /* WCPayCardPaymentDetails+ReadOnlyConvertible.swift */, 031C1EAD27B1877000298699 /* WCPayCardPresentPaymentDetails+ReadOnlyConvertible.swift */, 6889089E28F7B8540081A07E /* Customer+ReadOnlyConvertible.swift */, - 688908A028F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift */, ); path = Storage; sourceTree = ""; @@ -1928,7 +1925,6 @@ E1BD4D0027ABF84D006416D9 /* CardPresentPaymentsConfiguration.swift in Sources */, CE3B7AD52225EBF10050FE4B /* OrderStatusAction.swift in Sources */, 7493750C224987D9007D85D1 /* ProductAttribute+ReadOnlyConvertible.swift in Sources */, - 688908A128F7CAB70081A07E /* CustomerSearchResult+ReadOnlyConvertible.swift in Sources */, 744A3219216D55F80051439B /* SiteVisitStatsItem+ReadOnlyConvertible.swift in Sources */, 45151A8F27B156E40080845F /* InboxNotesAction.swift in Sources */, 45E462122684C7A400011BF2 /* DataAction.swift in Sources */, diff --git a/Yosemite/Yosemite/Model/Model.swift b/Yosemite/Yosemite/Model/Model.swift index 4903e365909..905a6f920fe 100644 --- a/Yosemite/Yosemite/Model/Model.swift +++ b/Yosemite/Yosemite/Model/Model.swift @@ -158,7 +158,6 @@ public typealias WCPayCardPaymentDetails = Networking.WCPayCardPaymentDetails public typealias WCPayCardPresentReceiptDetails = Networking.WCPayCardPresentReceiptDetails public typealias WCPayPaymentMethodDetails = Networking.WCPayPaymentMethodDetails public typealias WCPayChargeStatus = Networking.WCPayChargeStatus -public typealias WCAnalyticsCustomer = Networking.WCAnalyticsCustomer // MARK: - Exported Storage Symbols diff --git a/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift b/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift deleted file mode 100644 index 200e296722d..00000000000 --- a/Yosemite/Yosemite/Model/Storage/CustomerSearchResult+ReadOnlyConvertible.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import Storage - -// MARK: - Storage.CustomerSearchResult: ReadOnlyConvertible -extension Storage.CustomerSearchResult: ReadOnlyConvertible { - /// Updates the `Storage.CustomerSearchResult` from the ReadOnly representation (`Networking.WCAnalyticsCustomer`) - /// - public func update(with searchResult: [Yosemite.WCAnalyticsCustomer]) { - - } - - /// Returns a ReadOnly (`Networking.WCAnalyticsCustomer`) version of the `Storage.CustomerSearchResult` - public func toReadOnly() -> [Yosemite.WCAnalyticsCustomer] { - return [WCAnalyticsCustomer(userID: 2, name: ""), WCAnalyticsCustomer(userID: 2, name: "")] - } -} diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index 1cb0a77b7eb..78c9ece10d6 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -71,7 +71,7 @@ public final class CustomerStore: Store { guard let self else { return } switch result { case .success(let customers): - self.mapSearchResultsToCustomerObjects(for: siteID, with: customers, onCompletion: onCompletion) + self.mapSearchResultsToCustomerObjects(for: siteID, with: keyword, with: customers, onCompletion: onCompletion) case .failure(let error): onCompletion(.failure(error)) } @@ -111,6 +111,7 @@ public final class CustomerStore: Store { /// - onCompletion: Invoked when the operation finishes. Will map the result to a `[Customer]` entity. /// private func mapSearchResultsToCustomerObjects(for siteID: Int64, + with keyword: String, with searchResults: [WCAnalyticsCustomer], onCompletion: @escaping (Result<[Customer], Error>) -> Void) { var results = [Customer]() @@ -126,8 +127,16 @@ public final class CustomerStore: Store { } group.notify(queue: .main) { - self.upsertSearchCustomerResults(siteID: siteID, readOnlySearchResults: searchResults, in: self.sharedDerivedStorage, onCompletion: {}) - onCompletion(.success(results)) + self.upsertSearchCustomerResult( + siteID: siteID, + keyword: keyword, + readOnlySearchResults: searchResults, + in: self.sharedDerivedStorage, + onCompletion: { + onCompletion(.success(results)) + } + ) + //onCompletion(.success(results)) } } } @@ -136,21 +145,28 @@ public final class CustomerStore: Store { private extension CustomerStore { /// Inserts or updates CustomerSearchResults in Storage /// - private func upsertSearchCustomerResults(siteID: Int64, - readOnlySearchResults: [Networking.WCAnalyticsCustomer], - in storage: StorageType, - onCompletion: @escaping () -> Void) { -// for searchResult in readOnlySearchResults { -// let storageSearchResult: Storage.CustomerSearchResult = { -// if let storedSearchResult = storage.loadCustomerSearchResult(siteID: siteID, customerID: searchResult.userID) { -// return storedSearchResult -// } else { -// return storage.insertNewObject(ofType: Storage.CustomerSearchResult.self) -// } -// }() - // Update -// storageSearchResult.update(with: searchResult) -// } + private func upsertSearchCustomerResult(siteID: Int64, + keyword: String, + readOnlySearchResults: [Networking.WCAnalyticsCustomer], + in storage: StorageType, + onCompletion: @escaping () -> Void) { + + storage.perform { + // Potential stale data, perform search every time -> override existing? So we don't insert for each search. + let searchResult = storage.loadCustomerSearchResult(siteID: siteID, keyword: keyword) ?? + storage.insertNewObject(ofType: Storage.CustomerSearchResult.self) + searchResult.keyword = keyword + for customer in readOnlySearchResults { + guard let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: customer.userID) else { + continue + } + storedCustomer.addToSearchResults(searchResult) + } + } + + storageManager.saveDerivedType(derivedStorage: storage) { + DispatchQueue.main.async(execute: onCompletion) + } } /// Inserts or updates Customer entities into Storage diff --git a/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift b/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift index 0968af3ad1e..85157ef1131 100644 --- a/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift @@ -131,6 +131,36 @@ final class CustomerStoreTests: XCTestCase { XCTAssertEqual(result.failure as? NetworkError, expectedError) } + func test_searchCustomers_upserts_the_returned_CustomerSearchResult() { + // Given + network.simulateResponse(requestUrlSuffix: "customers", filename: "wc-analytics-customers") + network.simulateResponse(requestUrlSuffix: "customers/1", filename: "customer") + network.simulateResponse(requestUrlSuffix: "customers/2", filename: "customer") + network.simulateResponse(requestUrlSuffix: "customers/3", filename: "customer") + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.CustomerSearchResult.self), 0) + + // When + let response: Result<[Networking.Customer], Error> = waitFor { promise in + let action = CustomerAction.searchCustomers(siteID: self.dummySiteID, keyword: self.dummyKeyword) { result in + promise(result) + } + self.dispatcher.dispatch(action) + } + + // Then + XCTAssertTrue(response.isSuccess) + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.CustomerSearchResult.self), 1) + + let storedCustomerSearchResults = viewStorage.loadCustomerSearchResult(siteID: dummySiteID, keyword: dummyKeyword) + /* Failing tests: + XCTAssertNotNil(storedCustomerSearchResults) + XCTAssertEqual(storedCustomerSearchResults?.siteID, dummySiteID) + XCTAssertEqual(storedCustomerSearchResults?.customers?.first?.siteID, dummySiteID) + XCTAssertEqual(storedCustomerSearchResults?.customers?.first?.customerID, dummySiteID) + XCTAssertTrue(((storedCustomerSearchResults?.customers?.first?.firstName?.contains(dummyKeyword)) != nil)) + */ + } + func test_retrieveCustomer_upserts_the_returned_Customer() { // Given network.simulateResponse(requestUrlSuffix: "customers/25", filename: "customer") From 263dc79735fdbdba75402979e043f27d95835f3d Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 19 Oct 2022 10:54:45 +0900 Subject: [PATCH 12/21] Update WCAnalyticsCustomer with siteID param --- Fakes/Fakes/Networking.generated.swift | 1 + .../Copiable/Models+Copiable.generated.swift | 3 +++ .../Networking/Model/WCAnalyticsCustomer.swift | 15 +++++++++++++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Fakes/Fakes/Networking.generated.swift b/Fakes/Fakes/Networking.generated.swift index 9035b97c433..3270fa4e817 100644 --- a/Fakes/Fakes/Networking.generated.swift +++ b/Fakes/Fakes/Networking.generated.swift @@ -1700,6 +1700,7 @@ extension WCAnalyticsCustomer { /// public static func fake() -> WCAnalyticsCustomer { .init( + siteID: .fake(), userID: .fake(), name: .fake() ) diff --git a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift index 83e23848ca9..8d0c68c087c 100644 --- a/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Networking/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -1953,13 +1953,16 @@ extension TopEarnerStatsItem { extension WCAnalyticsCustomer { public func copy( + siteID: CopiableProp = .copy, userID: CopiableProp = .copy, name: NullableCopiableProp = .copy ) -> WCAnalyticsCustomer { + let siteID = siteID ?? self.siteID let userID = userID ?? self.userID let name = name ?? self.name return WCAnalyticsCustomer( + siteID: siteID, userID: userID, name: name ) diff --git a/Networking/Networking/Model/WCAnalyticsCustomer.swift b/Networking/Networking/Model/WCAnalyticsCustomer.swift index 9ea39f05789..8a697cd1dd1 100644 --- a/Networking/Networking/Model/WCAnalyticsCustomer.swift +++ b/Networking/Networking/Model/WCAnalyticsCustomer.swift @@ -2,6 +2,8 @@ import Foundation import Codegen public struct WCAnalyticsCustomer: Codable, GeneratedCopiable, GeneratedFakeable { + /// The siteID for the WCAnalyticsCustomer + public let siteID: Int64 /// Unique identifier for the user public let userID: Int64 @@ -11,7 +13,8 @@ public struct WCAnalyticsCustomer: Codable, GeneratedCopiable, GeneratedFakeable /// WCAnalyticsCustomer struct Initializer /// - public init(userID: Int64, name: String?) { + public init(siteID: Int64, userID: Int64, name: String?) { + self.siteID = siteID self.userID = userID self.name = name } @@ -19,12 +22,16 @@ public struct WCAnalyticsCustomer: Codable, GeneratedCopiable, GeneratedFakeable /// Public initializer for WCAnalyticsCustomer /// public init(from decoder: Decoder) throws { + guard let siteID = decoder.userInfo[.siteID] as? Int64 else { + throw WCAnalyticsCustomerDecodingError.missingSiteID + } + let container = try decoder.container(keyedBy: CodingKeys.self) let userID = try container.decode(Int64.self, forKey: .userID) let name = try container.decode(String.self, forKey: .name) - self.init(userID: userID, name: name) + self.init(siteID: siteID, userID: userID, name: name) } } @@ -33,4 +40,8 @@ extension WCAnalyticsCustomer { case userID = "user_id" case name = "name" } + + enum WCAnalyticsCustomerDecodingError: Error { + case missingSiteID + } } From caf7b81d37df3e34b4fd7d61c884d097b11be653 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 19 Oct 2022 12:54:06 +0900 Subject: [PATCH 13/21] Add customer-2 json response --- .../Networking.xcodeproj/project.pbxproj | 4 ++ .../NetworkingTests/Responses/customer-2.json | 55 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 Networking/NetworkingTests/Responses/customer-2.json diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj index bd39dd288dd..224d4e6d309 100644 --- a/Networking/Networking.xcodeproj/project.pbxproj +++ b/Networking/Networking.xcodeproj/project.pbxproj @@ -324,6 +324,7 @@ 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 */; }; + 688908AE28FF920C0081A07E /* customer-2.json in Resources */ = {isa = PBXBuildFile; fileRef = 688908AD28FF920C0081A07E /* customer-2.json */; }; 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 */; }; @@ -1024,6 +1025,7 @@ 5726F7332460A8F00031CAAC /* CopiableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiableTests.swift; sourceTree = ""; }; 57BE08D72409B63700F6DCED /* reviews-missing-avatar-urls.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "reviews-missing-avatar-urls.json"; sourceTree = ""; }; 57E8FED2246616AC0057CD68 /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = ""; }; + 688908AD28FF920C0081A07E /* customer-2.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "customer-2.json"; sourceTree = ""; }; 68BD37B228D9B8BD00C2A517 /* CustomerRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerRemoteTests.swift; sourceTree = ""; }; 68C87B332862D40E00A99054 /* setting-all-except-countries.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "setting-all-except-countries.json"; sourceTree = ""; }; 68CB800B28D87BC800E169F8 /* Customer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Customer.swift; sourceTree = ""; }; @@ -2124,6 +2126,7 @@ 68C87B332862D40E00A99054 /* setting-all-except-countries.json */, 68CB801528D8A39700E169F8 /* customer.json */, 68F48B1228E3E5750045C15B /* wc-analytics-customers.json */, + 688908AD28FF920C0081A07E /* customer-2.json */, ); path = Responses; sourceTree = ""; @@ -2553,6 +2556,7 @@ D823D91422377EE600C90817 /* shipment_tracking_providers.json in Resources */, 4599FC5C24A6276F0056157A /* product-tags-all.json in Resources */, 03DCB77E262738E300C8953D /* coupon.json in Resources */, + 688908AE28FF920C0081A07E /* customer-2.json in Resources */, 034480C327A42F9100DFACD2 /* order-with-charge.json in Resources */, 74A7B4BE217A841400E85A8B /* broken-settings-general.json in Resources */, 026CF624237D839B009563D4 /* product-variations-load-all.json in Resources */, diff --git a/Networking/NetworkingTests/Responses/customer-2.json b/Networking/NetworkingTests/Responses/customer-2.json new file mode 100644 index 00000000000..377e18e7911 --- /dev/null +++ b/Networking/NetworkingTests/Responses/customer-2.json @@ -0,0 +1,55 @@ +{ + "data": { + "id": 26, + "date_created": "2017-03-21T16:09:28", + "date_created_gmt": "2017-03-21T19:09:28", + "date_modified": "2017-03-21T16:09:30", + "date_modified_gmt": "2017-03-21T19:09:30", + "email": "john.doe@example.com", + "first_name": "Johnny", + "last_name": "Doe", + "role": "customer", + "username": "johnny.doe", + "billing": { + "first_name": "John", + "last_name": "Doe", + "company": "", + "address_1": "969 Market", + "address_2": "", + "city": "San Francisco", + "state": "CA", + "postcode": "94103", + "country": "US", + "email": "john.doe@example.com", + "phone": "(555) 555-5555" + }, + "shipping": { + "first_name": "John", + "last_name": "Doe", + "company": "", + "address_1": "969 Market", + "address_2": "", + "city": "San Francisco", + "state": "CA", + "postcode": "94103", + "country": "US" + }, + "is_paying_customer": false, + "avatar_url": "https://secure.gravatar.com/avatar/8eb1b522f60d11fa897de1dc6351b7e8?s=96", + "meta_data": [], + "_links": { + "self": [ + { + "href": "https://example.com/wp-json/wc/v3/customers/25" + } + ], + "collection": [ + { + "href": "https://example.com/wp-json/wc/v3/customers" + } + ] + } + } +} + + From 7d672875c9a95dba4414cc8a40d6024eead28ea9 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 19 Oct 2022 12:58:49 +0900 Subject: [PATCH 14/21] Add upsert customerSearchResults to Storage --- Yosemite/Yosemite/Stores/CustomerStore.swift | 35 ++++++++----------- .../Stores/CustomerStoreTests.swift | 16 ++++----- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index 78c9ece10d6..34fc796f288 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -114,13 +114,13 @@ public final class CustomerStore: Store { with keyword: String, with searchResults: [WCAnalyticsCustomer], onCompletion: @escaping (Result<[Customer], Error>) -> Void) { - var results = [Customer]() + var customers = [Customer]() let group = DispatchGroup() for result in searchResults { group.enter() self.retrieveCustomer(for: siteID, with: result.userID, onCompletion: { result in if let customer = try? result.get() { - results.append(customer) + customers.append(customer) } group.leave() }) @@ -130,13 +130,12 @@ public final class CustomerStore: Store { self.upsertSearchCustomerResult( siteID: siteID, keyword: keyword, - readOnlySearchResults: searchResults, + readOnlyCustomers: customers, in: self.sharedDerivedStorage, onCompletion: { - onCompletion(.success(results)) + onCompletion(.success(customers)) } ) - //onCompletion(.success(results)) } } } @@ -147,23 +146,22 @@ private extension CustomerStore { /// private func upsertSearchCustomerResult(siteID: Int64, keyword: String, - readOnlySearchResults: [Networking.WCAnalyticsCustomer], + readOnlyCustomers: [Networking.Customer], in storage: StorageType, onCompletion: @escaping () -> Void) { - storage.perform { - // Potential stale data, perform search every time -> override existing? So we don't insert for each search. - let searchResult = storage.loadCustomerSearchResult(siteID: siteID, keyword: keyword) ?? + let storedSeachResult = storage.loadCustomerSearchResult(siteID: siteID, keyword: keyword) ?? storage.insertNewObject(ofType: Storage.CustomerSearchResult.self) - searchResult.keyword = keyword - for customer in readOnlySearchResults { - guard let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: customer.userID) else { - continue + + storedSeachResult.siteID = siteID + storedSeachResult.keyword = keyword + + for result in readOnlyCustomers { + if let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: result.customerID) { + storedSeachResult.addToCustomers(storedCustomer) } - storedCustomer.addToSearchResults(searchResult) } } - storageManager.saveDerivedType(derivedStorage: storage) { DispatchQueue.main.async(execute: onCompletion) } @@ -174,18 +172,15 @@ private extension CustomerStore { private func upsertCustomer(siteID: Int64, readOnlyCustomer: Networking.Customer, in storage: StorageType, onCompletion: @escaping () -> Void) { storage.perform { - // If the specific customerID for that siteID already exists, return it - // If doesn't, insert a new one in Storage let storageCustomer: Storage.Customer = { + // If the specific customerID for that siteID already exists, return it + // If doesn't, insert a new one in Storage if let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: readOnlyCustomer.customerID) { - print("Customer exists on Storage. ID: \(readOnlyCustomer.customerID)") return storedCustomer } else { - print("Customer does not exist on Storage. ID: \(readOnlyCustomer.customerID). Inserting new") return storage.insertNewObject(ofType: Storage.Customer.self) } }() - // 3. Update the entity storageCustomer.update(with: readOnlyCustomer) } diff --git a/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift b/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift index 85157ef1131..458020ef3a6 100644 --- a/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift +++ b/Yosemite/YosemiteTests/Stores/CustomerStoreTests.swift @@ -135,8 +135,8 @@ final class CustomerStoreTests: XCTestCase { // Given network.simulateResponse(requestUrlSuffix: "customers", filename: "wc-analytics-customers") network.simulateResponse(requestUrlSuffix: "customers/1", filename: "customer") - network.simulateResponse(requestUrlSuffix: "customers/2", filename: "customer") - network.simulateResponse(requestUrlSuffix: "customers/3", filename: "customer") + network.simulateResponse(requestUrlSuffix: "customers/2", filename: "customer-2") + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.CustomerSearchResult.self), 0) // When @@ -149,16 +149,16 @@ final class CustomerStoreTests: XCTestCase { // Then XCTAssertTrue(response.isSuccess) + XCTAssertEqual(viewStorage.countObjects(ofType: Storage.Customer.self), 2) XCTAssertEqual(viewStorage.countObjects(ofType: Storage.CustomerSearchResult.self), 1) let storedCustomerSearchResults = viewStorage.loadCustomerSearchResult(siteID: dummySiteID, keyword: dummyKeyword) - /* Failing tests: - XCTAssertNotNil(storedCustomerSearchResults) + + XCTAssertNotNil(storedCustomerSearchResults) XCTAssertEqual(storedCustomerSearchResults?.siteID, dummySiteID) - XCTAssertEqual(storedCustomerSearchResults?.customers?.first?.siteID, dummySiteID) - XCTAssertEqual(storedCustomerSearchResults?.customers?.first?.customerID, dummySiteID) - XCTAssertTrue(((storedCustomerSearchResults?.customers?.first?.firstName?.contains(dummyKeyword)) != nil)) - */ + XCTAssertEqual(storedCustomerSearchResults?.keyword, dummyKeyword) + XCTAssertEqual(storedCustomerSearchResults?.customers?.count, 2) + XCTAssertTrue(storedCustomerSearchResults?.customers?.allSatisfy { $0.firstName?.contains(dummyKeyword) == true } ?? false ) } func test_retrieveCustomer_upserts_the_returned_Customer() { From ec113232b7d02b3ee9d74a56f71c7c2c7ecda6f5 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Wed, 19 Oct 2022 13:25:37 +0900 Subject: [PATCH 15/21] Add temporary method to debug responses --- .../CreateOrderAddressFormViewModel.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CustomerSection/CreateOrderAddressFormViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CustomerSection/CreateOrderAddressFormViewModel.swift index b4c19f20d6c..8fbc9b475ce 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CustomerSection/CreateOrderAddressFormViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Creation/CustomerSection/CreateOrderAddressFormViewModel.swift @@ -107,8 +107,19 @@ final class CreateOrderAddressFormViewModel: AddressFormViewModel, AddressFormVi siteID: siteID, keyword: "hello") { result in switch result { - case .success(_): - print("Success!") + case .success(let customers): + let storage = ServiceLocator.storageManager + guard let result = storage.viewStorage.loadCustomerSearchResult(siteID: self.siteID, keyword: "hello") else { + return + } + print("Site ID: \(result.siteID), keyword: \(result.keyword), Customers: \(result.customers?.count as Any)") + for eachCustomer in customers { + let output = """ + Customer: \(eachCustomer.customerID), + Name: \(String(describing: eachCustomer.firstName)) \(String(describing: eachCustomer.lastName)) + """ + print(output) + } case .failure(let error): print(error) } From 5dca4ec6c440e9caa3e5a83a38dc275905424947 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 21 Oct 2022 10:15:23 +0900 Subject: [PATCH 16/21] Add comments. Fix typo. --- Storage/Storage/Tools/StorageType+Extensions.swift | 4 ++-- Yosemite/Yosemite/Stores/CustomerStore.swift | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Storage/Storage/Tools/StorageType+Extensions.swift b/Storage/Storage/Tools/StorageType+Extensions.swift index 0723051c82d..e8925f18845 100644 --- a/Storage/Storage/Tools/StorageType+Extensions.swift +++ b/Storage/Storage/Tools/StorageType+Extensions.swift @@ -601,13 +601,13 @@ public extension StorageType { // MARK: - Customers - /// Returns a single Customer given a `CustomerID` + /// Returns a single Customer given a `siteID` and `customerID` /// func loadCustomer(siteID: Int64, customerID: Int64) -> Customer? { let predicate = \Customer.siteID == siteID && \Customer.customerID == customerID return firstObject(ofType: Customer.self, matching: predicate) } - /// Returns a CustomerSearchResult given a `CustomerID` + /// Returns a CustomerSearchResult given a `siteID` and a `keyword` /// func loadCustomerSearchResult(siteID: Int64, keyword: String) -> CustomerSearchResult? { let predicate = \CustomerSearchResult.siteID == siteID && \CustomerSearchResult.keyword == keyword diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index 34fc796f288..d22a5241654 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -107,6 +107,7 @@ public final class CustomerStore: Store { /// /// - Parameters: /// - siteID: The site for which customers should be fetched. + /// - keyword: The keyword used for the Customer search query. /// - searchResults: A WCAnalyticsCustomer collection that represents the matches we've got from the API based in our keyword search. /// - onCompletion: Invoked when the operation finishes. Will map the result to a `[Customer]` entity. /// @@ -150,15 +151,15 @@ private extension CustomerStore { in storage: StorageType, onCompletion: @escaping () -> Void) { storage.perform { - let storedSeachResult = storage.loadCustomerSearchResult(siteID: siteID, keyword: keyword) ?? + let storedSearchResult = storage.loadCustomerSearchResult(siteID: siteID, keyword: keyword) ?? storage.insertNewObject(ofType: Storage.CustomerSearchResult.self) - storedSeachResult.siteID = siteID - storedSeachResult.keyword = keyword + storedSearchResult.siteID = siteID + storedSearchResult.keyword = keyword for result in readOnlyCustomers { if let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: result.customerID) { - storedSeachResult.addToCustomers(storedCustomer) + storedSearchResult.addToCustomers(storedCustomer) } } } From 457ae8a61ea23e5c04995ed02d0841e49cc885bc Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 21 Oct 2022 10:17:02 +0900 Subject: [PATCH 17/21] Rename DecodingError --- Networking/Networking/Model/WCAnalyticsCustomer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Networking/Networking/Model/WCAnalyticsCustomer.swift b/Networking/Networking/Model/WCAnalyticsCustomer.swift index 8a697cd1dd1..d8c5c2c15c3 100644 --- a/Networking/Networking/Model/WCAnalyticsCustomer.swift +++ b/Networking/Networking/Model/WCAnalyticsCustomer.swift @@ -23,7 +23,7 @@ public struct WCAnalyticsCustomer: Codable, GeneratedCopiable, GeneratedFakeable /// public init(from decoder: Decoder) throws { guard let siteID = decoder.userInfo[.siteID] as? Int64 else { - throw WCAnalyticsCustomerDecodingError.missingSiteID + throw DecodingError.missingSiteID } let container = try decoder.container(keyedBy: CodingKeys.self) @@ -41,7 +41,7 @@ extension WCAnalyticsCustomer { case name = "name" } - enum WCAnalyticsCustomerDecodingError: Error { + enum DecodingError: Error { case missingSiteID } } From a21284707041a3bda8b5713dfb08e72d73c4ebf5 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 21 Oct 2022 10:20:21 +0900 Subject: [PATCH 18/21] Remove unnecessary XCTAsserts --- Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift b/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift index fb08429aab6..801310e8e2d 100644 --- a/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift +++ b/Storage/StorageTests/Tools/StorageTypeExtensionsTests.swift @@ -158,7 +158,6 @@ final class StorageTypeExtensionsTests: XCTestCase { let storedCustomer = try XCTUnwrap(storage.loadCustomer(siteID: sampleSiteID, customerID: customerID)) // Then - XCTAssertEqual(storedCustomer.siteID, sampleSiteID) XCTAssertEqual(customer, storedCustomer) } @@ -173,7 +172,6 @@ final class StorageTypeExtensionsTests: XCTestCase { let storedCustomerSearchResult = try XCTUnwrap(storage.loadCustomerSearchResult(siteID: sampleSiteID, keyword: keyword )) // Then - XCTAssertEqual(customerSearchResult.siteID, sampleSiteID) XCTAssertEqual(customerSearchResult, storedCustomerSearchResult) } From 470010e1ff62dd8257c38cdc07f7242ec9933b83 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 21 Oct 2022 10:39:18 +0900 Subject: [PATCH 19/21] Make CustomerStore StorageType lazy --- Yosemite/Yosemite/Stores/CustomerStore.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index d22a5241654..b4d8e2a0292 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -6,7 +6,9 @@ public final class CustomerStore: Store { private let customerRemote: CustomerRemote private let searchRemote: WCAnalyticsCustomerRemote - private let sharedDerivedStorage: StorageType + private lazy var sharedDerivedStorage: StorageType = { + return storageManager.writerDerivedStorage + }() init(dispatcher: Dispatcher, storageManager: StorageManagerType, @@ -15,7 +17,6 @@ public final class CustomerStore: Store { searchRemote: WCAnalyticsCustomerRemote) { self.customerRemote = customerRemote self.searchRemote = searchRemote - self.sharedDerivedStorage = storageManager.writerDerivedStorage super.init(dispatcher: dispatcher, storageManager: storageManager, network: network) } From 27d82f22eb54ea05e05de00b0b8bcf1a1682ff84 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 21 Oct 2022 10:46:41 +0900 Subject: [PATCH 20/21] Remove passing storage directly to upsertSearchCustomerResult --- Yosemite/Yosemite/Stores/CustomerStore.swift | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index b4d8e2a0292..268c1611540 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -133,7 +133,6 @@ public final class CustomerStore: Store { siteID: siteID, keyword: keyword, readOnlyCustomers: customers, - in: self.sharedDerivedStorage, onCompletion: { onCompletion(.success(customers)) } @@ -149,22 +148,21 @@ private extension CustomerStore { private func upsertSearchCustomerResult(siteID: Int64, keyword: String, readOnlyCustomers: [Networking.Customer], - in storage: StorageType, onCompletion: @escaping () -> Void) { - storage.perform { - let storedSearchResult = storage.loadCustomerSearchResult(siteID: siteID, keyword: keyword) ?? - storage.insertNewObject(ofType: Storage.CustomerSearchResult.self) + sharedDerivedStorage.perform { + let storedSearchResult = self.sharedDerivedStorage.loadCustomerSearchResult(siteID: siteID, keyword: keyword) ?? + self.sharedDerivedStorage.insertNewObject(ofType: Storage.CustomerSearchResult.self) storedSearchResult.siteID = siteID storedSearchResult.keyword = keyword for result in readOnlyCustomers { - if let storedCustomer = storage.loadCustomer(siteID: siteID, customerID: result.customerID) { + if let storedCustomer = self.sharedDerivedStorage.loadCustomer(siteID: siteID, customerID: result.customerID) { storedSearchResult.addToCustomers(storedCustomer) } } } - storageManager.saveDerivedType(derivedStorage: storage) { + storageManager.saveDerivedType(derivedStorage: self.sharedDerivedStorage) { DispatchQueue.main.async(execute: onCompletion) } } From 0a791296f2ae8a406239bc30cfad413c9f71ad90 Mon Sep 17 00:00:00 2001 From: iamgabrielma Date: Fri, 21 Oct 2022 23:16:45 +0900 Subject: [PATCH 21/21] capture weak self --- Yosemite/Yosemite/Stores/CustomerStore.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Yosemite/Yosemite/Stores/CustomerStore.swift b/Yosemite/Yosemite/Stores/CustomerStore.swift index 268c1611540..1cd6bdd6be9 100644 --- a/Yosemite/Yosemite/Stores/CustomerStore.swift +++ b/Yosemite/Yosemite/Stores/CustomerStore.swift @@ -149,7 +149,8 @@ private extension CustomerStore { keyword: String, readOnlyCustomers: [Networking.Customer], onCompletion: @escaping () -> Void) { - sharedDerivedStorage.perform { + sharedDerivedStorage.perform { [weak self] in + guard let self = self else { return } let storedSearchResult = self.sharedDerivedStorage.loadCustomerSearchResult(siteID: siteID, keyword: keyword) ?? self.sharedDerivedStorage.insertNewObject(ofType: Storage.CustomerSearchResult.self)