From a284a503a4c54f133f0382602233395ea95031c0 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 22 Jul 2025 11:32:48 +0700 Subject: [PATCH 1/5] Add siteID to WooShippingOriginAddress --- Modules/Sources/Fakes/Networking.generated.swift | 1 + .../WooShippingOriginAddressesMapper.swift | 8 ++++++++ .../Copiable/Models+Copiable.generated.swift | 3 +++ .../Packages/WooShippingOriginAddress.swift | 16 ++++++++++++++-- .../Networking/Remote/WooShippingRemote.swift | 2 +- 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Modules/Sources/Fakes/Networking.generated.swift b/Modules/Sources/Fakes/Networking.generated.swift index 2cfafea1da5..962c699cf7c 100644 --- a/Modules/Sources/Fakes/Networking.generated.swift +++ b/Modules/Sources/Fakes/Networking.generated.swift @@ -2383,6 +2383,7 @@ extension Networking.WooShippingOriginAddress { /// public static func fake() -> Networking.WooShippingOriginAddress { .init( + siteID: .fake(), id: .fake(), company: .fake(), address1: .fake(), diff --git a/Modules/Sources/Networking/Mapper/WooShippingOriginAddressesMapper.swift b/Modules/Sources/Networking/Mapper/WooShippingOriginAddressesMapper.swift index 291d359e368..767d2ec63c0 100644 --- a/Modules/Sources/Networking/Mapper/WooShippingOriginAddressesMapper.swift +++ b/Modules/Sources/Networking/Mapper/WooShippingOriginAddressesMapper.swift @@ -1,10 +1,18 @@ import Foundation struct WooShippingOriginAddressesMapper: Mapper { + + /// Site ID associated to the origin addresses that will be parsed. + /// + /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned from the remote. + /// + let siteID: Int64 + /// (Attempts) to convert a dictionary into WooShippingOriginAddress array. /// func map(response: Data) throws -> [WooShippingOriginAddress] { let decoder = JSONDecoder() + decoder.userInfo = [.siteID: siteID] if hasDataEnvelope(in: response) { return try decoder.decode(WooShippingOriginAddressesMapperEnvelope.self, from: response).data } else { diff --git a/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift b/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift index fb474e39423..58a8682b6c8 100644 --- a/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -3636,6 +3636,7 @@ extension Networking.WooShippingNormalizedAddress { extension Networking.WooShippingOriginAddress { public func copy( + siteID: CopiableProp = .copy, id: CopiableProp = .copy, company: CopiableProp = .copy, address1: CopiableProp = .copy, @@ -3651,6 +3652,7 @@ extension Networking.WooShippingOriginAddress { defaultAddress: CopiableProp = .copy, isVerified: CopiableProp = .copy ) -> Networking.WooShippingOriginAddress { + let siteID = siteID ?? self.siteID let id = id ?? self.id let company = company ?? self.company let address1 = address1 ?? self.address1 @@ -3667,6 +3669,7 @@ extension Networking.WooShippingOriginAddress { let isVerified = isVerified ?? self.isVerified return Networking.WooShippingOriginAddress( + siteID: siteID, id: id, company: company, address1: address1, diff --git a/Modules/Sources/Networking/Model/ShippingLabel/Packages/WooShippingOriginAddress.swift b/Modules/Sources/Networking/Model/ShippingLabel/Packages/WooShippingOriginAddress.swift index 7e1a6887fc7..c76bed1db45 100644 --- a/Modules/Sources/Networking/Model/ShippingLabel/Packages/WooShippingOriginAddress.swift +++ b/Modules/Sources/Networking/Model/ShippingLabel/Packages/WooShippingOriginAddress.swift @@ -2,6 +2,7 @@ import Foundation import Codegen public struct WooShippingOriginAddress: Identifiable, Equatable, GeneratedFakeable, GeneratedCopiable { + public let siteID: Int64 public let id: String public let company: String public let address1: String @@ -17,7 +18,8 @@ public struct WooShippingOriginAddress: Identifiable, Equatable, GeneratedFakeab public let defaultAddress: Bool public let isVerified: Bool - public init(id: String, + public init(siteID: Int64, + id: String, company: String, address1: String, address2: String, @@ -32,6 +34,7 @@ public struct WooShippingOriginAddress: Identifiable, Equatable, GeneratedFakeab defaultAddress: Bool, isVerified: Bool) { + self.siteID = siteID self.id = id self.company = company self.address1 = address1 @@ -52,6 +55,9 @@ public struct WooShippingOriginAddress: Identifiable, Equatable, GeneratedFakeab // MARK: Decodable extension WooShippingOriginAddress: Codable { public init(from decoder: Decoder) throws { + guard let siteID = decoder.userInfo[.siteID] as? Int64 else { + throw DecodingError.missingSiteID + } let container = try decoder.container(keyedBy: CodingKeys.self) let id = try container.decode(String.self, forKey: CodingKeys.id) @@ -70,7 +76,8 @@ extension WooShippingOriginAddress: Codable { let defaultAddress = try container.decodeIfPresent(Bool.self, forKey: CodingKeys.defaultAddress) ?? false let isVerified = try container.decodeIfPresent(Bool.self, forKey: CodingKeys.isVerified) ?? false - self.init(id: id, + self.init(siteID: siteID, + id: id, company: company, address1: address1, address2: address2, @@ -120,4 +127,9 @@ extension WooShippingOriginAddress: Codable { case defaultAddress = "default_address" case isVerified = "is_verified" } + + /// Decoding Errors + enum DecodingError: Error { + case missingSiteID + } } diff --git a/Modules/Sources/Networking/Remote/WooShippingRemote.swift b/Modules/Sources/Networking/Remote/WooShippingRemote.swift index 3a56bb585e9..7ecb08f3b4d 100644 --- a/Modules/Sources/Networking/Remote/WooShippingRemote.swift +++ b/Modules/Sources/Networking/Remote/WooShippingRemote.swift @@ -379,7 +379,7 @@ public final class WooShippingRemote: Remote, WooShippingRemoteProtocol { path: Path.originAddresses, parameters: nil, availableAsRESTRequest: true) - let mapper = WooShippingOriginAddressesMapper() + let mapper = WooShippingOriginAddressesMapper(siteID: siteID) enqueue(request, mapper: mapper, completion: completion) } From 0bceb3af6a0d77af3fc09af4494b3ae12cb63376 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 22 Jul 2025 11:41:26 +0700 Subject: [PATCH 2/5] Add new entity WooShippingOriginAddress --- ...oShippingOriginAddress+CoreDataClass.swift | 7 +++++ ...pingOriginAddress+CoreDataProperties.swift | 26 +++++++++++++++++++ .../Model 124.xcdatamodel/contents | 17 ++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 Modules/Sources/Storage/Model/WooShippingOriginAddress+CoreDataClass.swift create mode 100644 Modules/Sources/Storage/Model/WooShippingOriginAddress+CoreDataProperties.swift diff --git a/Modules/Sources/Storage/Model/WooShippingOriginAddress+CoreDataClass.swift b/Modules/Sources/Storage/Model/WooShippingOriginAddress+CoreDataClass.swift new file mode 100644 index 00000000000..3638c288733 --- /dev/null +++ b/Modules/Sources/Storage/Model/WooShippingOriginAddress+CoreDataClass.swift @@ -0,0 +1,7 @@ +import Foundation +import CoreData + +@objc(WooShippingOriginAddress) +public class WooShippingOriginAddress: NSManagedObject { + +} diff --git a/Modules/Sources/Storage/Model/WooShippingOriginAddress+CoreDataProperties.swift b/Modules/Sources/Storage/Model/WooShippingOriginAddress+CoreDataProperties.swift new file mode 100644 index 00000000000..fb5a6d23003 --- /dev/null +++ b/Modules/Sources/Storage/Model/WooShippingOriginAddress+CoreDataProperties.swift @@ -0,0 +1,26 @@ +import Foundation +import CoreData + +extension WooShippingOriginAddress { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "WooShippingOriginAddress") + } + + @NSManaged public var siteID: Int64 + @NSManaged public var id: String + @NSManaged public var company: String + @NSManaged public var address1: String + @NSManaged public var address2: String + @NSManaged public var city: String + @NSManaged public var state: String + @NSManaged public var postcode: String + @NSManaged public var country: String + @NSManaged public var phone: String + @NSManaged public var firstName: String + @NSManaged public var lastName: String + @NSManaged public var email: String + @NSManaged public var defaultAddress: Bool + @NSManaged public var isVerified: Bool + +} diff --git a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 124.xcdatamodel/contents b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 124.xcdatamodel/contents index 834e8e3683e..db135cc6dc7 100644 --- a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 124.xcdatamodel/contents +++ b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 124.xcdatamodel/contents @@ -1041,6 +1041,23 @@ + + + + + + + + + + + + + + + + + From e522ea127493999596dd28d04865250d064d262d Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 22 Jul 2025 12:33:32 +0700 Subject: [PATCH 3/5] Sync origin addresses --- .../Tools/StorageType+Extensions.swift | 7 ++ Modules/Sources/Yosemite/Model/Model.swift | 1 + ...ingOriginAddress+ReadOnlyConvertible.swift | 46 ++++++++ .../Yosemite/Stores/WooShippingStore.swift | 35 +++++- .../Remote/WooShippingRemoteTests.swift | 3 +- .../Stores/WooShippingStoreTests.swift | 101 ++++++++++++++++++ .../WooShippingEditAddressViewModel.swift | 29 ++--- ...ooShippingOriginAddressListViewModel.swift | 6 +- ...ooShippingCreateLabelsViewModelTests.swift | 35 +++--- ...WooShippingEditAddressViewModelTests.swift | 6 +- 10 files changed, 233 insertions(+), 36 deletions(-) create mode 100644 Modules/Sources/Yosemite/Model/Storage/WooShippingOriginAddress+ReadOnlyConvertible.swift diff --git a/Modules/Sources/Storage/Tools/StorageType+Extensions.swift b/Modules/Sources/Storage/Tools/StorageType+Extensions.swift index 6f4d857b881..8612ca2efaa 100644 --- a/Modules/Sources/Storage/Tools/StorageType+Extensions.swift +++ b/Modules/Sources/Storage/Tools/StorageType+Extensions.swift @@ -607,6 +607,13 @@ public extension StorageType { return allObjects(ofType: WooShippingShipment.self, matching: predicate, sortedBy: nil) } + /// Returns all stored origin addresses for a site. + /// + func loadAllOriginAddresses(siteID: Int64) -> [WooShippingOriginAddress] { + let predicate = \WooShippingOriginAddress.siteID == siteID + return allObjects(ofType: WooShippingOriginAddress.self, matching: predicate, sortedBy: nil) + } + // MARK: - BlazeCampaignListItem /// Returns a single BlazeCampaignListItem given a `siteID` and `campaignID` diff --git a/Modules/Sources/Yosemite/Model/Model.swift b/Modules/Sources/Yosemite/Model/Model.swift index 5e8c5bf455c..9ed769d9d6b 100644 --- a/Modules/Sources/Yosemite/Model/Model.swift +++ b/Modules/Sources/Yosemite/Model/Model.swift @@ -346,6 +346,7 @@ public typealias StorageWooShippingPredefinedPackage = Storage.WooShippingPredef public typealias StorageWooShippingCustomPackage = Storage.WooShippingCustomPackage public typealias StorageWooShippingSavedPredefinedPackage = Storage.WooShippingSavedPredefinedPackage public typealias StorageWooShippingShipment = Storage.WooShippingShipment +public typealias StorageWooShippingOriginAddress = Storage.WooShippingOriginAddress // MARK: - Internal ReadOnly Models diff --git a/Modules/Sources/Yosemite/Model/Storage/WooShippingOriginAddress+ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Model/Storage/WooShippingOriginAddress+ReadOnlyConvertible.swift new file mode 100644 index 00000000000..7b7990057e3 --- /dev/null +++ b/Modules/Sources/Yosemite/Model/Storage/WooShippingOriginAddress+ReadOnlyConvertible.swift @@ -0,0 +1,46 @@ +import Foundation +import Storage + +// Storage.WooShippingOriginAddress: ReadOnlyConvertible Conformance. +// +extension Storage.WooShippingOriginAddress: ReadOnlyConvertible { + /// Updates the Storage.ShippingLabelAddress with the a ReadOnly ShippingLabelAddress. + /// + public func update(with address: Yosemite.WooShippingOriginAddress) { + siteID = address.siteID + id = address.id + company = address.company + firstName = address.firstName + lastName = address.lastName + phone = address.phone + country = address.country + state = address.state + address1 = address.address1 + address2 = address.address2 + city = address.city + postcode = address.postcode + email = address.email + defaultAddress = address.defaultAddress + isVerified = address.isVerified + } + + /// Returns a ReadOnly version of the receiver. + /// + public func toReadOnly() -> Yosemite.WooShippingOriginAddress { + .init(siteID: siteID, + id: id, + company: company, + address1: address1, + address2: address2, + city: city, + state: state, + postcode: postcode, + country: country, + phone: phone, + firstName: firstName, + lastName: lastName, + email: email, + defaultAddress: defaultAddress, + isVerified: isVerified) + } +} diff --git a/Modules/Sources/Yosemite/Stores/WooShippingStore.swift b/Modules/Sources/Yosemite/Stores/WooShippingStore.swift index 4ca485fdbba..aa97ecb6b5a 100644 --- a/Modules/Sources/Yosemite/Stores/WooShippingStore.swift +++ b/Modules/Sources/Yosemite/Stores/WooShippingStore.swift @@ -267,7 +267,17 @@ private extension WooShippingStore { func loadOriginAddresses(siteID: Int64, completion: @escaping (Result<[WooShippingOriginAddress], Error>) -> Void) { - remote.loadOriginAddresses(siteID: siteID, completion: completion) + remote.loadOriginAddresses(siteID: siteID, completion: { [weak self] result in + guard let self else { return } + switch result { + case .success(let addresses): + upsertOriginAddressesInBackground(siteID: siteID, originAddresses: addresses) { + completion(.success(addresses)) + } + case .failure(let error): + completion(.failure(error)) + } + }) } func refundShippingLabel(shippingLabel: ShippingLabel, @@ -656,6 +666,29 @@ private extension WooShippingStore { storageSavedPackage.package = predefinedPackage } + /// Updates the specified origin addresses with the given refund *in a background thread*. + /// `onCompletion` will be called on the main thread! + func upsertOriginAddressesInBackground(siteID: Int64, + originAddresses: [WooShippingOriginAddress], + onCompletion: @escaping () -> Void) { + storageManager.performAndSave({ storage in + let storedOriginAddresses = storage.loadAllOriginAddresses(siteID: siteID) + for address in originAddresses { + let storageAddress = storedOriginAddresses.first(where: { $0.id == address.id }) ?? + storage.insertNewObject(ofType: Storage.WooShippingOriginAddress.self) + storageAddress.update(with: address) + } + + // Now, remove any objects that exist in storage but not in `originAddresses` + let addressIDs = originAddresses.map(\.id) + storedOriginAddresses.filter { + !addressIDs.contains($0.id) + }.forEach { + storage.deleteObject($0) + } + }, completion: onCompletion, on: .main) + } + /// Updates the specified shipping label with the given refund *in a background thread*. /// `onCompletion` will be called on the main thread! func upsertShippingLabelRefundInBackground(shippingLabel: ShippingLabel, diff --git a/Modules/Tests/NetworkingTests/Remote/WooShippingRemoteTests.swift b/Modules/Tests/NetworkingTests/Remote/WooShippingRemoteTests.swift index 5efd1ce41af..73e6a09d919 100644 --- a/Modules/Tests/NetworkingTests/Remote/WooShippingRemoteTests.swift +++ b/Modules/Tests/NetworkingTests/Remote/WooShippingRemoteTests.swift @@ -1084,7 +1084,8 @@ private extension WooShippingRemoteTests { } func sampleOriginAddress() -> WooShippingOriginAddress { - WooShippingOriginAddress(id: "store_details", + WooShippingOriginAddress(siteID: sampleSiteID, + id: "store_details", company: "Superlative Centaur", address1: "60 29TH ST PMB 343", address2: "", diff --git a/Modules/Tests/YosemiteTests/Stores/WooShippingStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/WooShippingStoreTests.swift index 3534d6d8830..042de449617 100644 --- a/Modules/Tests/YosemiteTests/Stores/WooShippingStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/WooShippingStoreTests.swift @@ -823,6 +823,107 @@ final class WooShippingStoreTests: XCTestCase { XCTAssertEqual(error as? NetworkError, expectedError) } + func test_loadOriginAddresses_persists_fetched_addresses_to_local_storage_on_success() { + // Given + let address1 = WooShippingOriginAddress( + siteID: sampleSiteID, + id: "address1", + company: "Company A", + address1: "123 Main St", + address2: "Suite 100", + city: "San Francisco", + state: "CA", + postcode: "94102", + country: "US", + phone: "555-0123", + firstName: "John", + lastName: "Doe", + email: "john@company.com", + defaultAddress: true, + isVerified: false + ) + let address2 = WooShippingOriginAddress( + siteID: sampleSiteID, + id: "address2", + company: "Company B", + address1: "456 Oak Ave", + address2: "", + city: "Los Angeles", + state: "CA", + postcode: "90210", + country: "US", + phone: "555-0456", + firstName: "Jane", + lastName: "Smith", + email: "jane@companyb.com", + defaultAddress: false, + isVerified: true + ) + let expectedAddresses = [address1, address2] + + let remote = MockWooShippingRemote() + remote.whenOriginAddresses(siteID: sampleSiteID, thenReturn: .success(expectedAddresses)) + let store = WooShippingStore(dispatcher: dispatcher, storageManager: storageManager, network: network, remote: remote) + + // Confidence check - no addresses in storage initially + XCTAssertEqual(storageManager.viewStorage.countObjects(ofType: StorageWooShippingOriginAddress.self), 0) + + // When + let onSuccess: Bool = waitFor { promise in + let action = WooShippingAction.loadOriginAddresses(siteID: self.sampleSiteID) { result in + promise(result.isSuccess) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(onSuccess) + + // Verify addresses are persisted to storage + let storedAddresses = storageManager.viewStorage.loadAllOriginAddresses(siteID: sampleSiteID) + XCTAssertEqual(storedAddresses.count, 2) + + // Verify first address details + let storedAddress1 = storedAddresses.first { $0.id == "address1" } + XCTAssertNotNil(storedAddress1) + XCTAssertEqual(storedAddress1?.company, "Company A") + XCTAssertEqual(storedAddress1?.address1, "123 Main St") + XCTAssertEqual(storedAddress1?.address2, "Suite 100") + XCTAssertEqual(storedAddress1?.city, "San Francisco") + XCTAssertEqual(storedAddress1?.state, "CA") + XCTAssertEqual(storedAddress1?.postcode, "94102") + XCTAssertEqual(storedAddress1?.country, "US") + XCTAssertEqual(storedAddress1?.phone, "555-0123") + XCTAssertEqual(storedAddress1?.firstName, "John") + XCTAssertEqual(storedAddress1?.lastName, "Doe") + XCTAssertEqual(storedAddress1?.email, "john@company.com") + XCTAssertEqual(storedAddress1?.defaultAddress, true) + XCTAssertEqual(storedAddress1?.isVerified, false) + + // Verify second address details + let storedAddress2 = storedAddresses.first { $0.id == "address2" } + XCTAssertNotNil(storedAddress2) + XCTAssertEqual(storedAddress2?.company, "Company B") + XCTAssertEqual(storedAddress2?.address1, "456 Oak Ave") + XCTAssertEqual(storedAddress2?.address2, "") + XCTAssertEqual(storedAddress2?.city, "Los Angeles") + XCTAssertEqual(storedAddress2?.state, "CA") + XCTAssertEqual(storedAddress2?.postcode, "90210") + XCTAssertEqual(storedAddress2?.country, "US") + XCTAssertEqual(storedAddress2?.phone, "555-0456") + XCTAssertEqual(storedAddress2?.firstName, "Jane") + XCTAssertEqual(storedAddress2?.lastName, "Smith") + XCTAssertEqual(storedAddress2?.email, "jane@companyb.com") + XCTAssertEqual(storedAddress2?.defaultAddress, false) + XCTAssertEqual(storedAddress2?.isVerified, true) + + // Verify read-only conversion works correctly + let readOnlyAddresses = storedAddresses + .map { $0.toReadOnly() } + .sorted(by: { $0.id < $1.id }) + XCTAssertEqual(readOnlyAddresses, expectedAddresses) + } + // MARK: `validateAddress` func test_validateAddress_returns_WooShippingAddressValidationSuccess_on_success() throws { diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingAddresses/WooShippingEditAddressViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingAddresses/WooShippingEditAddressViewModel.swift index 842b8661a04..aee5affe1c5 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingAddresses/WooShippingEditAddressViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingAddresses/WooShippingEditAddressViewModel.swift @@ -417,20 +417,21 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable { } // Merge the provided (normalized) address with the edited address fields. - let originAddress = WooShippingOriginAddress(id: id, - company: address.company, - address1: address.address1, - address2: address.address2, - city: address.city, - state: address.state, - postcode: address.postcode, - country: address.country, - phone: address.phone, - firstName: name.value, - lastName: "", - email: email.value, - defaultAddress: isDefaultAddress, - isVerified: true) + let originAddress = WooShippingOriginAddress(siteID: siteID, + id: id, + company: address.company, + address1: address.address1, + address2: address.address2, + city: address.city, + state: address.state, + postcode: address.postcode, + country: address.country, + phone: address.phone, + firstName: name.value, + lastName: "", + email: email.value, + defaultAddress: isDefaultAddress, + isVerified: true) Task { @MainActor in do { diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingAddresses/WooShippingOriginAddressListViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingAddresses/WooShippingOriginAddressListViewModel.swift index 0d267a06aff..240b3bc6ceb 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingAddresses/WooShippingOriginAddressListViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingAddresses/WooShippingOriginAddressListViewModel.swift @@ -53,7 +53,8 @@ final class WooShippingOriginAddressListViewModel: ObservableObject { // MARK: SwiftUI Previews extension WooShippingOriginAddressListView { static func sampleAddresses() -> [WooShippingOriginAddress] { - [WooShippingOriginAddress(id: "1", + [WooShippingOriginAddress(siteID: 123, + id: "1", company: "HEADQUARTERS", address1: "417 MONTGOMERY ST", address2: "", @@ -67,7 +68,8 @@ extension WooShippingOriginAddressListView { email: "", defaultAddress: true, isVerified: true), - WooShippingOriginAddress(id: "2", + WooShippingOriginAddress(siteID: 123, + id: "2", company: "WAREHOUSE", address1: "15 ALGONKIN ST", address2: "", diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift index 29cb3d13460..02ff97ef76a 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift @@ -108,20 +108,21 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase { func test_state_is_ready_when_loading_required_data_succeeds() { // Given let stores = MockStoresManager(sessionManager: .testingInstance) - let originAddress = WooShippingOriginAddress(id: "default_address", - company: "HEADQUARTERS", - address1: "15 ALGONKIN ST", - address2: "STE 100", - city: "TICONDEROGA", - state: "NY", - postcode: "12883-1487", - country: "US", - phone: "223-456-7890", - firstName: "JANE", - lastName: "DOE", - email: "TEST@EXAMPLE.COM", - defaultAddress: true, - isVerified: false) + let originAddress = WooShippingOriginAddress(siteID: 123, + id: "default_address", + company: "HEADQUARTERS", + address1: "15 ALGONKIN ST", + address2: "STE 100", + city: "TICONDEROGA", + state: "NY", + postcode: "12883-1487", + country: "US", + phone: "223-456-7890", + firstName: "JANE", + lastName: "DOE", + email: "TEST@EXAMPLE.COM", + defaultAddress: true, + isVerified: false) stores.whenReceivingAction(ofType: WooShippingAction.self) { action in switch action { case .loadOriginAddresses(_, let completion): @@ -170,7 +171,8 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase { func test_origin_unverified_state_is_correct() { // Given let stores = MockStoresManager(sessionManager: .testingInstance) - let originAddress = WooShippingOriginAddress(id: "default_address", + let originAddress = WooShippingOriginAddress(siteID: 123, + id: "default_address", company: "HEADQUARTERS", address1: "15 ALGONKIN ST", address2: "STE 100", @@ -216,7 +218,8 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase { func test_editSelectedOriginAddress_sets_addressToEdit_view_model() throws { // Given let stores = MockStoresManager(sessionManager: .testingInstance) - let originAddress = WooShippingOriginAddress(id: "default_address", + let originAddress = WooShippingOriginAddress(siteID: 123, + id: "default_address", company: "HEADQUARTERS", address1: "15 ALGONKIN ST", address2: "STE 100", diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingEditAddressViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingEditAddressViewModelTests.swift index 8eb60786312..0ed2bb2866f 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingEditAddressViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingEditAddressViewModelTests.swift @@ -67,7 +67,8 @@ final class WooShippingEditAddressViewModelTests: XCTestCase { let state = StateOfACountry(code: "NY", name: "New York") let countries = [Country(code: "US", name: "United States", states: [state]), Country(code: "CA", name: "Canada", states: [])] storageManager.insertSampleCountries(readOnlyCountries: countries) - let address = WooShippingOriginAddress(id: "default_address", + let address = WooShippingOriginAddress(siteID: 123, + id: "default_address", company: "HEADQUARTERS", address1: "15 ALGONKIN ST", address2: "STE 100", @@ -964,7 +965,8 @@ final class WooShippingEditAddressViewModelTests: XCTestCase { } // Then - let expectedAddress = WooShippingOriginAddress(id: originAddress.id, + let expectedAddress = WooShippingOriginAddress(siteID: 123, + id: originAddress.id, company: suggestedAddress.company, address1: suggestedAddress.address1, address2: suggestedAddress.address2, From d82bb25fb2e4b16919234da23687ab63a304e99f Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 22 Jul 2025 12:43:15 +0700 Subject: [PATCH 4/5] Add migration tests --- Modules/Sources/Storage/Model/MIGRATIONS.md | 2 ++ .../CoreData/MigrationTests.swift | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/Modules/Sources/Storage/Model/MIGRATIONS.md b/Modules/Sources/Storage/Model/MIGRATIONS.md index 6cae8d4356a..46d7c2ecc7b 100644 --- a/Modules/Sources/Storage/Model/MIGRATIONS.md +++ b/Modules/Sources/Storage/Model/MIGRATIONS.md @@ -8,6 +8,8 @@ This file documents changes in the WCiOS Storage data model. Please explain any - Added `WooShippingShipmentItem` entity. - Added `shipment` relationship to `ShippingLabel` entity. - Added `shipments` relationship to `Order` entity. +- @itsmeichigo 2025-07-22 + - Added `WooShippingOriginAddress` entity. ## Model 123 (Release 22.8.0.0) - @iamgabrielma 2025-06-30 diff --git a/Modules/Tests/StorageTests/CoreData/MigrationTests.swift b/Modules/Tests/StorageTests/CoreData/MigrationTests.swift index 835152c876f..d21df4518e0 100644 --- a/Modules/Tests/StorageTests/CoreData/MigrationTests.swift +++ b/Modules/Tests/StorageTests/CoreData/MigrationTests.swift @@ -3493,6 +3493,31 @@ final class MigrationTests: XCTestCase { XCTAssertEqual(migratedOrder.value(forKey: "shipments") as? Set, [shipment]) } + + func test_migrating_from_123_to_124_enables_creating_new_WooShippingOriginAddress_entity() throws { + // Given + let sourceContainer = try startPersistentContainer("Model 123") + let sourceContext = sourceContainer.viewContext + + try sourceContext.save() + + // Confidence Check. WooShippingOriginAddress should not exist in Model 123 + XCTAssertNil(NSEntityDescription.entity(forEntityName: "WooShippingOriginAddress", in: sourceContext)) + + // When + let targetContainer = try migrate(sourceContainer, to: "Model 124") + let targetContext = targetContainer.viewContext + + // Then + XCTAssertEqual(try targetContext.count(entityName: "WooShippingOriginAddress"), 0) + + let address = insertWooShippingOriginAddress(to: targetContext) + XCTAssertNoThrow(try targetContext.save()) + + XCTAssertEqual(try targetContext.count(entityName: "WooShippingOriginAddress"), 1) + let insertedAddress = try XCTUnwrap(targetContext.firstObject(ofType: WooShippingOriginAddress.self)) + XCTAssertEqual(insertedAddress, address) + } } // MARK: - Persistent Store Setup and Migrations @@ -4409,4 +4434,12 @@ private extension MigrationTests { "subItems": ["sub_1", "sub_2"] ]) } + + @discardableResult + func insertWooShippingOriginAddress(to context: NSManagedObjectContext) -> NSManagedObject { + context.insert(entityName: "WooShippingOriginAddress", properties: [ + "siteID": 1, + "id": "test-address" + ]) + } } From 367c36a408bea962e258679fb74e235ee5e97283 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 22 Jul 2025 14:44:49 +0700 Subject: [PATCH 5/5] Fix failed tests --- .../Networking/Mapper/WooShippingOriginAddressMapper.swift | 7 +++++++ Modules/Sources/Networking/Remote/WooShippingRemote.swift | 2 +- .../WooShippingEditAddressViewModelTests.swift | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Modules/Sources/Networking/Mapper/WooShippingOriginAddressMapper.swift b/Modules/Sources/Networking/Mapper/WooShippingOriginAddressMapper.swift index adc1a812677..c1a7bc383c3 100644 --- a/Modules/Sources/Networking/Mapper/WooShippingOriginAddressMapper.swift +++ b/Modules/Sources/Networking/Mapper/WooShippingOriginAddressMapper.swift @@ -1,10 +1,17 @@ import Foundation struct WooShippingOriginAddressUpdateMapper: Mapper { + /// Site ID associated to the origin addresses that will be parsed. + /// + /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned from the remote. + /// + let siteID: Int64 + /// (Attempts) to convert a dictionary into a WooShippingOriginAddressUpdate. /// func map(response: Data) throws -> WooShippingOriginAddressUpdate { let decoder = JSONDecoder() + decoder.userInfo = [.siteID: siteID] if hasDataEnvelope(in: response) { return try decoder.decode(WooShippingOriginAddressUpdateMapperEnvelope.self, from: response).data } else { diff --git a/Modules/Sources/Networking/Remote/WooShippingRemote.swift b/Modules/Sources/Networking/Remote/WooShippingRemote.swift index 7ecb08f3b4d..6764fbd0a43 100644 --- a/Modules/Sources/Networking/Remote/WooShippingRemote.swift +++ b/Modules/Sources/Networking/Remote/WooShippingRemote.swift @@ -431,7 +431,7 @@ public final class WooShippingRemote: Remote, WooShippingRemoteProtocol { path: Path.updateOrigin, parameters: parameters, availableAsRESTRequest: true) - let mapper = WooShippingOriginAddressUpdateMapper() + let mapper = WooShippingOriginAddressUpdateMapper(siteID: siteID) enqueue(request, mapper: mapper, completion: completion) } catch { completion(.failure(error)) diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingEditAddressViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingEditAddressViewModelTests.swift index 0ed2bb2866f..ddb4c49aa4e 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingEditAddressViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingEditAddressViewModelTests.swift @@ -944,7 +944,7 @@ final class WooShippingEditAddressViewModelTests: XCTestCase { address2: "", city: "TICONDEROGA", postcode: "12883-1487") - let stores = MockStoresManager(sessionManager: .testingInstance) + let stores = MockStoresManager(sessionManager: .makeForTesting(defaultSite: .fake().copy(siteID: 123))) let viewModel = WooShippingEditAddressViewModel(address: originAddress, stores: stores) // When