diff --git a/Modules/Sources/Yosemite/Internal/NSOrderedSet+Array.swift b/Modules/Sources/Yosemite/Internal/NSOrderedSet+Array.swift index 468b75526d8..3a8ed9c6956 100644 --- a/Modules/Sources/Yosemite/Internal/NSOrderedSet+Array.swift +++ b/Modules/Sources/Yosemite/Internal/NSOrderedSet+Array.swift @@ -1,7 +1,7 @@ import Foundation extension NSOrderedSet { - func toArray() -> [T] { + public func toArray() -> [T] { guard let array = array as? [T] else { return [] } diff --git a/Modules/Sources/Yosemite/Model/Storage/Product+ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Model/Storage/Product+ReadOnlyConvertible.swift index 65ddd38c5a9..229ea1fbb59 100644 --- a/Modules/Sources/Yosemite/Model/Storage/Product+ReadOnlyConvertible.swift +++ b/Modules/Sources/Yosemite/Model/Storage/Product+ReadOnlyConvertible.swift @@ -2,7 +2,7 @@ import Foundation import Storage extension Storage.Product { - var imagesArray: [Storage.ProductImage] { + public var imagesArray: [Storage.ProductImage] { return images?.toArray() ?? [] } var tagsArray: [Storage.ProductTag] { diff --git a/Modules/Sources/Yosemite/Tools/ResultsController.swift b/Modules/Sources/Yosemite/Tools/ResultsController.swift index ee9ea7b1992..7a1a25a761b 100644 --- a/Modules/Sources/Yosemite/Tools/ResultsController.swift +++ b/Modules/Sources/Yosemite/Tools/ResultsController.swift @@ -9,9 +9,9 @@ import CoreData public typealias ResultsControllerMutableType = NSManagedObject & ReadOnlyConvertible -// MARK: - ResultsController +// MARK: - GenericResultsController (Core Implementation) // -public class ResultsController { +public class GenericResultsController { /// The `StorageType` used to fetch objects. /// @@ -79,7 +79,7 @@ public class ResultsController { /// Closure to be executed whenever an Object is updated. /// - public var onDidChangeObject: ((_ object: T.ReadOnlyType, _ indexPath: IndexPath?, _ type: ChangeType, _ newIndexPath: IndexPath?) -> Void)? + public var onDidChangeObject: ((_ object: Output, _ indexPath: IndexPath?, _ type: ChangeType, _ newIndexPath: IndexPath?) -> Void)? /// Closure to be executed whenever an entire Section is updated. /// @@ -94,19 +94,25 @@ public class ResultsController { /// private let fetchLimit: Int? + /// Transformer closure to convert T to Output type. + /// + private let transformer: (T) -> Output + /// Designated Initializer. /// public init(viewStorage: StorageType, sectionNameKeyPath: String? = nil, matching predicate: NSPredicate? = nil, fetchLimit: Int? = nil, - sortedBy descriptors: [NSSortDescriptor]) { + sortedBy descriptors: [NSSortDescriptor], + transformer: @escaping (T) -> Output) { self.viewStorage = viewStorage self.sectionNameKeyPath = sectionNameKeyPath self.predicate = predicate self.fetchLimit = fetchLimit self.sortDescriptors = descriptors + self.transformer = transformer setupResultsController() setupEventsForwarding() @@ -119,13 +125,15 @@ public class ResultsController { sectionNameKeyPath: String? = nil, matching predicate: NSPredicate? = nil, fetchLimit: Int? = nil, - sortedBy descriptors: [NSSortDescriptor]) { + sortedBy descriptors: [NSSortDescriptor], + transformer: @escaping (T) -> Output) { self.init(viewStorage: storageManager.viewStorage, sectionNameKeyPath: sectionNameKeyPath, matching: predicate, fetchLimit: fetchLimit, - sortedBy: descriptors) + sortedBy: descriptors, + transformer: transformer) } @@ -139,14 +147,14 @@ public class ResultsController { /// /// Prefer to use `safeObject(at:)` instead. /// - public func object(at indexPath: IndexPath) -> T.ReadOnlyType { - return controller.object(at: indexPath).toReadOnly() + public func object(at indexPath: IndexPath) -> Output { + return transformer(controller.object(at: indexPath)) } /// Returns the fetched object at the given `indexPath`. Returns `nil` if the `indexPath` /// does not exist. /// - public func safeObject(at indexPath: IndexPath) -> T.ReadOnlyType? { + public func safeObject(at indexPath: IndexPath) -> Output? { guard !isEmpty else { return nil } @@ -160,7 +168,7 @@ public class ResultsController { return nil } - return controller.object(at: indexPath).toReadOnly() + return transformer(controller.object(at: indexPath)) } /// Returns the Plain ObjectIndex corresponding to a given IndexPath. You can use this index to map the @@ -194,23 +202,24 @@ public class ResultsController { } /// Returns an array of all of the (ReadOnly) Fetched Objects. + /// Note: Avoid calling this in computed variables as the conversion of storage items can be costly. /// - public var fetchedObjects: [T.ReadOnlyType] { - let readOnlyObjects = controller.fetchedObjects?.compactMap { mutableObject in - mutableObject.toReadOnly() + public var fetchedObjects: [Output] { + let transformedObjects = controller.fetchedObjects?.compactMap { mutableObject in + transformer(mutableObject) } - return readOnlyObjects ?? [] + return transformedObjects ?? [] } /// Returns an array of SectionInfo Entitites. /// public var sections: [SectionInfo] { - let readOnlySections = controller.sections?.compactMap { mutableSection in - SectionInfo(mutableSection: mutableSection) + let transformedSections = controller.sections?.compactMap { mutableSection in + SectionInfo(mutableSection: mutableSection, transformer: transformer) } - return readOnlySections ?? [] + return transformedSections ?? [] } /// Returns an optional index path of the first matching object. @@ -260,8 +269,8 @@ public class ResultsController { return } - let readOnlyObject = object.toReadOnly() - self.onDidChangeObject?(readOnlyObject, indexPath, type, newIndexPath) + let transformedObject = transformer(object) + self.onDidChangeObject?(transformedObject, indexPath, type, newIndexPath) } internalDelegate.onDidChangeSection = { [weak self] (mutableSection, sectionIndex, type) in @@ -269,8 +278,8 @@ public class ResultsController { return } - let readOnlySection = SectionInfo(mutableSection: mutableSection) - self.onDidChangeSection?(readOnlySection, sectionIndex, type) + let transformedSection = SectionInfo(mutableSection: mutableSection, transformer: transformer) + self.onDidChangeSection?(transformedSection, sectionIndex, type) } } @@ -294,7 +303,7 @@ public class ResultsController { // MARK: - Nested Types // -public extension ResultsController { +public extension GenericResultsController { // MARK: - ResultsController.ChangeType // @@ -322,9 +331,13 @@ public extension ResultsController { mutableSectionInfo.numberOfObjects } - /// Returns the array of (ReadOnly) objects in the section. + /// Transformer closure to convert objects in the section. + /// + private let transformer: (T) -> Output + + /// Returns the array of transformed objects in the section. /// - private(set) public lazy var objects: [T.ReadOnlyType] = { + private(set) public lazy var objects: [Output] = { guard let objects = mutableSectionInfo.objects else { return [] } @@ -333,13 +346,47 @@ public extension ResultsController { return [] } - return castedObjects.map { $0.toReadOnly() } + return castedObjects.map { transformer($0) } }() /// Designated Initializer /// - init(mutableSection: NSFetchedResultsSectionInfo) { + init(mutableSection: NSFetchedResultsSectionInfo, transformer: @escaping (T) -> Output) { mutableSectionInfo = mutableSection + self.transformer = transformer } } } + +// MARK: - ResultsController (Backward Compatible Specialization) +// +public class ResultsController: GenericResultsController { + /// Designated Initializer. + /// + public init(viewStorage: StorageType, + sectionNameKeyPath: String? = nil, + matching predicate: NSPredicate? = nil, + fetchLimit: Int? = nil, + sortedBy descriptors: [NSSortDescriptor]) { + super.init(viewStorage: viewStorage, + sectionNameKeyPath: sectionNameKeyPath, + matching: predicate, + fetchLimit: fetchLimit, + sortedBy: descriptors, + transformer: { $0.toReadOnly() }) + } + + /// Convenience Initializer. + /// + public convenience init(storageManager: StorageManagerType, + sectionNameKeyPath: String? = nil, + matching predicate: NSPredicate? = nil, + fetchLimit: Int? = nil, + sortedBy descriptors: [NSSortDescriptor]) { + self.init(viewStorage: storageManager.viewStorage, + sectionNameKeyPath: sectionNameKeyPath, + matching: predicate, + fetchLimit: fetchLimit, + sortedBy: descriptors) + } +} diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index e823639d18c..f7dc6cc697a 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -6,6 +6,7 @@ - [*] Increased decimal sensitivity in order creation to mitigate tax rounding issues [https://github.com/woocommerce/woocommerce-ios/pull/15957] - [*] Fix initialization of authenticator to avoid crashes during login [https://github.com/woocommerce/woocommerce-ios/pull/15953] - [*] Shipping Labels: Made HS tariff number field required in customs form for EU destinations [https://github.com/woocommerce/woocommerce-ios/pull/15946] +- [*] Order Details: Attempt to improve performance by using a simplified version of product objects. [https://github.com/woocommerce/woocommerce-ios/pull/15959] - [*] Order Details > Edit Shipping/Billing Address: Added map-based address lookup support for iOS 17+. [https://github.com/woocommerce/woocommerce-ios/pull/15964] - [*] Order Creation: Prevent subscription products to be added to an order [https://github.com/woocommerce/woocommerce-ios/pull/15960] - [internal] Replace COTS_DEVICE reader model name with TAP_TO_PAY_DEVICE. [https://github.com/woocommerce/woocommerce-ios/pull/15961] diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift b/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift index 75ec2ebd734..02cc24ce27e 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift @@ -601,7 +601,7 @@ extension WooAnalyticsEvent { WooAnalyticsEvent(statName: .orderAddNew, properties: [:]) } - static func orderProductsLoaded(order: Order, products: [Product], addOnGroups: [AddOnGroup]) -> WooAnalyticsEvent { + static func orderProductsLoaded(order: Order, products: [OrderDetailsProduct], addOnGroups: [AddOnGroup]) -> WooAnalyticsEvent { let productTypes = productTypes(order: order, products: products) let hasAddOns = hasAddOns(order: order, products: products, addOnGroups: addOnGroups) return WooAnalyticsEvent(statName: .orderProductsLoaded, properties: [Keys.orderID: order.orderID, @@ -609,7 +609,7 @@ extension WooAnalyticsEvent { Keys.hasAddOns: hasAddOns]) } - private static func hasAddOns(order: Order, products: [Product], addOnGroups: [AddOnGroup]) -> Bool { + private static func hasAddOns(order: Order, products: [OrderDetailsProduct], addOnGroups: [AddOnGroup]) -> Bool { for item in order.items { guard let product = products.first(where: { $0.productID == item.productID }) else { continue @@ -628,6 +628,13 @@ extension WooAnalyticsEvent { return false } + private static func productTypes(order: Order, products: [OrderDetailsProduct]) -> String { + let productIDs = order.items.map { $0.productID } + return productIDs.compactMap { productID in + products.first(where: { $0.productID == productID })?.productType.rawValue + }.uniqued().sorted().joined(separator: ",") + } + private static func productTypes(order: Order, products: [Product]) -> String { let productIDs = order.items.map { $0.productID } return productIDs.compactMap { productID in diff --git a/WooCommerce/Classes/ViewModels/Order Details/AddOns/AddOnCrossreferenceUseCase.swift b/WooCommerce/Classes/ViewModels/Order Details/AddOns/AddOnCrossreferenceUseCase.swift index c45736a2d9c..a6cfeb9f791 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/AddOns/AddOnCrossreferenceUseCase.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/AddOns/AddOnCrossreferenceUseCase.swift @@ -15,13 +15,13 @@ struct AddOnCrossreferenceUseCase { /// Product entity with known addOns that matches the order item. /// - private let product: Product + private let product: OrderDetailsProduct /// Global add-ons for the site. /// private let addOnGroups: [AddOnGroup] - init(orderItemAttributes: [OrderItemAttribute], product: Product, addOnGroups: [AddOnGroup]) { + init(orderItemAttributes: [OrderItemAttribute], product: OrderDetailsProduct, addOnGroups: [AddOnGroup]) { self.orderItemAttributes = orderItemAttributes self.product = product self.addOnGroups = addOnGroups diff --git a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsDataSource.swift b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsDataSource.swift index 43c113f9ce0..bb77957b682 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsDataSource.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsDataSource.swift @@ -149,7 +149,7 @@ final class OrderDetailsDataSource: NSObject { /// Products from an Order /// - var products: [Product] = [] + var products: [OrderDetailsProduct] = [] /// Product variations from an order /// @@ -891,13 +891,14 @@ private extension OrderDetailsDataSource { } let imageURL: URL? = { - guard let imageURLString = aggregateItem.variationID != 0 ? - lookUpProductVariation(productID: aggregateItem.productID, variationID: aggregateItem.variationID)?.image?.src: - lookUpProduct(by: aggregateItem.productID)?.images.first?.src, - let encodedImageURLString = imageURLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { - return nil + if aggregateItem.variationID != 0 { + guard let imageURLString = lookUpProductVariation(productID: aggregateItem.productID, variationID: aggregateItem.variationID)?.image?.src, + let encodedImageURLString = imageURLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { + return nil + } + return URL(string: encodedImageURLString) } - return URL(string: encodedImageURLString) + return lookUpProduct(by: aggregateItem.productID)?.imageURL }() let addOns: [OrderItemProductAddOn] = { @@ -1234,7 +1235,7 @@ extension OrderDetailsDataSource { return currentSiteStatuses.filter({$0.status == order.status}).first } - func lookUpProduct(by productID: Int64) -> Product? { + func lookUpProduct(by productID: Int64) -> OrderDetailsProduct? { return products.filter({ $0.productID == productID }).first } diff --git a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsProduct.swift b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsProduct.swift new file mode 100644 index 00000000000..9378f915cb9 --- /dev/null +++ b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsProduct.swift @@ -0,0 +1,71 @@ +import Foundation +import Yosemite + +/// Represents a Product Entity with basic details to display in the product section of order details screen. +/// +struct OrderDetailsProduct: Equatable { + let siteID: Int64 + let productID: Int64 + let name: String + + let productTypeKey: String + let sku: String? + + let price: String + let virtual: Bool + + let stockQuantity: Decimal? // Core API reports Int or null; some extensions allow decimal values as well + + let imageURL: URL? + + let addOns: [Yosemite.ProductAddOn] //TODO: migrate AddOns to MetaData + + var productType: ProductType { + return ProductType(rawValue: productTypeKey) + } + + init(siteID: Int64, + productID: Int64, + name: String, + productTypeKey: String, + sku: String?, + price: String, + virtual: Bool, + stockQuantity: Decimal?, + imageURL: URL?, + addOns: [Yosemite.ProductAddOn]) { + self.siteID = siteID + self.productID = productID + self.name = name + self.productTypeKey = productTypeKey + self.sku = sku + self.price = price + self.virtual = virtual + self.stockQuantity = stockQuantity + self.imageURL = imageURL + self.addOns = addOns + } + + init(storageProduct: StorageProduct) { + self.siteID = storageProduct.siteID + self.productID = storageProduct.productID + self.name = storageProduct.name + self.productTypeKey = storageProduct.productTypeKey + self.sku = storageProduct.sku + self.price = storageProduct.price + self.virtual = storageProduct.virtual + + self.stockQuantity = { + var quantity: Decimal? + if let stockQuantity = storageProduct.stockQuantity { + quantity = Decimal(string: stockQuantity) + } + return quantity + }() + + self.imageURL = storageProduct.imagesArray.first?.toReadOnly().imageURL + + let addOnsArray: [StorageProductAddOn] = storageProduct.addOns?.toArray() ?? [] + self.addOns = addOnsArray.map { $0.toReadOnly() } + } +} diff --git a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsResultsControllers.swift b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsResultsControllers.swift index 4fe5dec9baf..ec091109a4e 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsResultsControllers.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsResultsControllers.swift @@ -23,12 +23,7 @@ final class OrderDetailsResultsControllers { /// Product ResultsController. /// - private lazy var productResultsController: ResultsController = { - let predicate = NSPredicate(format: "siteID == %lld", siteID) - let descriptor = NSSortDescriptor(key: "name", ascending: true) - - return ResultsController(storageManager: storageManager, matching: predicate, sortedBy: [descriptor]) - }() + private lazy var productResultsController: GenericResultsController = createProductResultsController() /// ProductVariation ResultsController. /// @@ -100,9 +95,7 @@ final class OrderDetailsResultsControllers { /// Products from an Order /// - var products: [Product] { - return productResultsController.fetchedObjects - } + private(set) var products: [OrderDetailsProduct] = [] /// ProductVariations from an Order /// @@ -181,11 +174,13 @@ final class OrderDetailsResultsControllers { func update(order: Order) { self.order = order - // Product variation results controller depends on order items to load variations, + // Product and variation results controller depends on order items to load variations, // so we need to recreate it whenever receiving an updated order. self.productVariationResultsController = getProductVariationResultsController() + self.productResultsController = createProductResultsController() if let onReload = onReload { configureProductVariationResultsController(onReload: onReload) + configureProductResultsController(onReload: onReload) } } } @@ -250,7 +245,9 @@ private extension OrderDetailsResultsControllers { } private func configureProductResultsController(onReload: @escaping () -> Void) { - productResultsController.onDidChangeContent = { + productResultsController.onDidChangeContent = { [weak self] in + guard let self else { return } + products = productResultsController.fetchedObjects onReload() } @@ -264,6 +261,7 @@ private extension OrderDetailsResultsControllers { do { try productResultsController.performFetch() + products = productResultsController.fetchedObjects } catch { DDLogError("⛔️ Unable to fetch Products for Site \(siteID): \(error)") } @@ -375,4 +373,17 @@ private extension OrderDetailsResultsControllers { try? sitePluginsResultsController.performFetch() try? shippingMethodsResultsController.performFetch() } + + func createProductResultsController() -> GenericResultsController { + let productIDs = order.items.map { $0.productID } + let predicate = NSPredicate(format: "siteID == %lld AND productID IN %@", siteID, productIDs) + let descriptor = NSSortDescriptor(key: "name", ascending: true) + + return GenericResultsController( + storageManager: storageManager, + matching: predicate, + sortedBy: [descriptor], + transformer: { OrderDetailsProduct(storageProduct: $0) } + ) + } } diff --git a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift index 19eba8e33e0..1fc72b3fb94 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/OrderDetailsViewModel.swift @@ -63,14 +63,14 @@ final class OrderDetailsViewModel { /// Products from an Order /// - var products: [Product] { + var products: [OrderDetailsProduct] { return dataSource.products } /// If the products for all order items have been loaded, checks if all products are virtual to skip shipping related syncs. private var orderContainsOnlyVirtualProducts: Bool { let productIDs = order.items.map { $0.productID } - let orderProducts = productIDs.compactMap { productID -> Product? in + let orderProducts = productIDs.compactMap { productID -> OrderDetailsProduct? in products.first(where: { $0.productID == productID }) } // Early returns `false` when the products haven't been fully loaded for all order items. @@ -218,7 +218,7 @@ final class OrderDetailsViewModel { return dataSource.lookUpOrderStatus(for: order) } - func lookUpProduct(by productID: Int64) -> Product? { + func lookUpProduct(by productID: Int64) -> OrderDetailsProduct? { return dataSource.lookUpProduct(by: productID) } diff --git a/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsDataSource.swift b/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsDataSource.swift index 61d8f681dc5..c5b93bfcdf5 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsDataSource.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsDataSource.swift @@ -31,7 +31,7 @@ final class RefundDetailsDataSource: NSObject { /// Products from a Refund /// - var products: [Product] { + var products: [OrderDetailsProduct] { return resultsControllers.products } @@ -206,7 +206,7 @@ private extension RefundDetailsDataSource { // MARK: - Lookup products // private extension RefundDetailsDataSource { - func lookUpProduct(by productID: Int64) -> Product? { + func lookUpProduct(by productID: Int64) -> OrderDetailsProduct? { return products.filter({ $0.productID == productID }).first } } diff --git a/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsResultController.swift b/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsResultController.swift index 5dbf5bd0b18..06a8b87aaca 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsResultController.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsResultController.swift @@ -7,18 +7,23 @@ import Yosemite final class RefundDetailsResultController { /// Product ResultsController. /// - private lazy var productResultsController: ResultsController = { + private lazy var productResultsController: GenericResultsController = { let storageManager = ServiceLocator.storageManager let predicate = NSPredicate(format: "siteID == %lld", siteID) let descriptor = NSSortDescriptor(key: "name", ascending: true) - return ResultsController(storageManager: storageManager, matching: predicate, sortedBy: [descriptor]) + return GenericResultsController( + storageManager: storageManager, + matching: predicate, + sortedBy: [descriptor], + transformer: { OrderDetailsProduct(storageProduct: $0) } + ) }() /// Products from an Order /// - var products: [Product] { - return productResultsController.fetchedObjects + var products: [OrderDetailsProduct] { + productResultsController.fetchedObjects } private let siteID: Int64 diff --git a/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsViewModel.swift index 279f27d9947..227d9875a44 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/Refund Details/RefundDetailsViewModel.swift @@ -27,7 +27,7 @@ final class RefundDetailsViewModel { /// Products from a Refund /// - var products: [Product] { + var products: [OrderDetailsProduct] { return dataSource.products } diff --git a/WooCommerce/Classes/ViewModels/Order Details/Refunded Products/RefundedProductsDataSource.swift b/WooCommerce/Classes/ViewModels/Order Details/Refunded Products/RefundedProductsDataSource.swift index 714e07ad37b..309db8a1a11 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/Refunded Products/RefundedProductsDataSource.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/Refunded Products/RefundedProductsDataSource.swift @@ -46,7 +46,7 @@ final class RefundedProductsDataSource: NSObject { /// Products from a Refund /// - var products: [Product] { + var products: [OrderDetailsProduct] { return resultsControllers.products } } @@ -130,7 +130,7 @@ private extension RefundedProductsDataSource { // MARK: - Lookup products // private extension RefundedProductsDataSource { - func lookUpProduct(by productID: Int64) -> Product? { + func lookUpProduct(by productID: Int64) -> OrderDetailsProduct? { return products.filter({ $0.productID == productID }).first } } diff --git a/WooCommerce/Classes/ViewModels/Order Details/Shipping Labels/AggregatedShippingLabelOrderItems.swift b/WooCommerce/Classes/ViewModels/Order Details/Shipping Labels/AggregatedShippingLabelOrderItems.swift index afc6c13d6c6..417c2c5972b 100644 --- a/WooCommerce/Classes/ViewModels/Order Details/Shipping Labels/AggregatedShippingLabelOrderItems.swift +++ b/WooCommerce/Classes/ViewModels/Order Details/Shipping Labels/AggregatedShippingLabelOrderItems.swift @@ -23,7 +23,7 @@ struct AggregatedShippingLabelOrderItems { /// - currencyFormatter: Used to convert a product/variation's price string to number. init(shippingLabels: [ShippingLabel], orderItems: [OrderItem], - products: [Product], + products: [OrderDetailsProduct], productVariations: [ProductVariation], currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) { self.currencyFormatter = currencyFormatter @@ -47,7 +47,7 @@ struct AggregatedShippingLabelOrderItems { private extension AggregatedShippingLabelOrderItems { mutating func aggregateProductsToOrderItems(shippingLabels: [ShippingLabel], orderItems: [OrderItem], - products: [Product], + products: [OrderDetailsProduct], productVariations: [ProductVariation]) { orderItemsByShippingLabelID = shippingLabels.reduce(into: [Int64: [AggregateOrderItem]]()) { result, shippingLabel in result[shippingLabel.shippingLabelID] = aggregateProductsToOrderItems(shippingLabel: shippingLabel, @@ -59,7 +59,7 @@ private extension AggregatedShippingLabelOrderItems { func aggregateProductsToOrderItems(shippingLabel: ShippingLabel, orderItems: [OrderItem], - products: [Product], + products: [OrderDetailsProduct], productVariations: [ProductVariation]) -> [AggregateOrderItem] { // ShippingLabel's `productNames` is always available, but `productIDs` is only available in WooCommerce Shipping & Tax v1.24.1+. // Here we map a ShippingLabel's `productNames` to `ProductInformation` with an optional product ID at the corresponding index. @@ -90,7 +90,7 @@ private extension AggregatedShippingLabelOrderItems { func orderItemModel(productInfo: ProductInformation, orderItems: [OrderItem], - products: [Product], + products: [OrderDetailsProduct], productVariations: [ProductVariation]) -> OrderItemModel { guard let productID = productInfo.id else { return .productName(name: productInfo.name) @@ -98,7 +98,7 @@ private extension AggregatedShippingLabelOrderItems { if let product = lookUpProduct(by: productID, products: products) { let orderItem = orderItems.first(where: { $0.productID == productID }) - return .product(product: product, orderItem: orderItem, name: productInfo.name) + return .orderDetailsProduct(product: product, orderItem: orderItem, name: productInfo.name) } else if let productVariation = lookUpProductVariation(by: productID, productVariations: productVariations) { let orderItem = orderItems.first(where: { $0.variationID == productID }) return .productVariation(productVariation: productVariation, orderItem: orderItem, name: productInfo.name) @@ -121,27 +121,21 @@ private extension AggregatedShippingLabelOrderItems { attributes: [], addOns: [], parent: nil) - case .product(let product, let orderItem, let name): + case let .orderDetailsProduct(item, orderItem, name): let itemID = orderItem?.itemID.description ?? "0" let productName = orderItem?.name ?? name let price = orderItem?.price ?? - currencyFormatter.convertToDecimal(product.price) ?? 0 + currencyFormatter.convertToDecimal(item.price) ?? 0 let totalPrice = price.multiplying(by: .init(decimal: Decimal(quantity))) - let imageURL: URL? - if let encodedImageURLString = product.images.first?.src.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - imageURL = URL(string: encodedImageURLString) - } else { - imageURL = nil - } return .init(itemID: itemID, - productID: product.productID, + productID: item.productID, variationID: 0, name: productName, price: price, quantity: Decimal(quantity), - sku: orderItem?.sku ?? product.sku, + sku: orderItem?.sku ?? item.sku, total: totalPrice, - imageURL: imageURL, + imageURL: item.imageURL, attributes: orderItem?.attributes ?? [], addOns: orderItem?.addOns ?? [], parent: orderItem?.parent) @@ -172,7 +166,7 @@ private extension AggregatedShippingLabelOrderItems { } } - func lookUpProduct(by productID: Int64, products: [Product]) -> Product? { + func lookUpProduct(by productID: Int64, products: [OrderDetailsProduct]) -> OrderDetailsProduct? { products.first(where: { $0.productID == productID }) } @@ -191,7 +185,7 @@ private extension AggregatedShippingLabelOrderItems { /// The underlying model for an order item. enum OrderItemModel { case productName(name: String) - case product(product: Product, orderItem: OrderItem?, name: String) case productVariation(productVariation: ProductVariation, orderItem: OrderItem?, name: String) + case orderDetailsProduct(product: OrderDetailsProduct, orderItem: OrderItem?, name: String) } } diff --git a/WooCommerce/Classes/ViewModels/ProductDetailsCellViewModel.swift b/WooCommerce/Classes/ViewModels/ProductDetailsCellViewModel.swift index 60c4ba086db..2ce31339865 100644 --- a/WooCommerce/Classes/ViewModels/ProductDetailsCellViewModel.swift +++ b/WooCommerce/Classes/ViewModels/ProductDetailsCellViewModel.swift @@ -97,7 +97,7 @@ struct ProductDetailsCellViewModel { init(item: OrderItem, currency: String, formatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings), - product: Product? = nil, + product: OrderDetailsProduct? = nil, hasAddOns: Bool, isChildWithParent: Bool) { self.init(currency: currency, @@ -119,7 +119,7 @@ struct ProductDetailsCellViewModel { init(aggregateItem: AggregateOrderItem, currency: String, formatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings), - product: Product? = nil, + product: OrderDetailsProduct? = nil, hasAddOns: Bool, isChildWithParent: Bool) { self.init(currency: currency, @@ -141,7 +141,7 @@ struct ProductDetailsCellViewModel { init(refundedItem: OrderItemRefund, currency: String, formatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings), - product: Product? = nil) { + product: OrderDetailsProduct? = nil) { self.init(currency: currency, currencyFormatter: formatter, imageURL: product?.imageURL, diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewModel.swift index 2d535ba21fd..13f4aaeb35c 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewModel.swift @@ -17,7 +17,7 @@ final class ReviewOrderViewModel { /// Products in the order /// - private let products: [Product] + private let products: [OrderDetailsProduct] /// StorageManager to load details of order from storage /// @@ -101,7 +101,7 @@ final class ReviewOrderViewModel { }() init(order: Order, - products: [Product], + products: [OrderDetailsProduct], showAddOns: Bool, stores: StoresManager = ServiceLocator.stores, storageManager: StorageManagerType = ServiceLocator.storageManager) { @@ -180,7 +180,7 @@ extension ReviewOrderViewModel { /// Filter product for an order item /// - func filterProduct(for item: AggregateOrderItem) -> Product? { + func filterProduct(for item: AggregateOrderItem) -> OrderDetailsProduct? { products.first(where: { $0.productID == item.productID }) } diff --git a/WooCommerce/Classes/ViewRelated/Products/Media/Product+Media.swift b/WooCommerce/Classes/ViewRelated/Products/Media/Product+Media.swift index 76d2c750329..83936366437 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Media/Product+Media.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Media/Product+Media.swift @@ -24,7 +24,7 @@ extension ProductVariation { } } -private extension ProductImage { +extension ProductImage { var imageURL: URL? { guard let encodedProductImageURLString = src.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return nil diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 6be9658cbc2..d4d7db1f1b4 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -2759,6 +2759,8 @@ DE9F2D292A1B1AB2004E5957 /* FirstProductCreatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE9F2D282A1B1AB2004E5957 /* FirstProductCreatedView.swift */; }; DEA0D0682BA82EA2007786F2 /* StatsGranularityV4+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA0D0672BA82EA2007786F2 /* StatsGranularityV4+UI.swift */; }; DEA357132ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA357122ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift */; }; + DEA64C532E40B04700791018 /* OrderDetailsProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA64C522E40B04000791018 /* OrderDetailsProduct.swift */; }; + DEA64C552E41A2D000791018 /* Product+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA64C542E41A2CA00791018 /* Product+Helpers.swift */; }; DEA6BCAF2BC6A9B10017D671 /* StoreStatsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA6BCAE2BC6A9B10017D671 /* StoreStatsChart.swift */; }; DEA6BCB12BC6AA040017D671 /* StoreStatsChartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA6BCB02BC6AA040017D671 /* StoreStatsChartViewModel.swift */; }; DEA88F502AA9D0100037273B /* AddEditProductCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA88F4F2AA9D0100037273B /* AddEditProductCategoryViewModel.swift */; }; @@ -5956,6 +5958,8 @@ DE9F2D282A1B1AB2004E5957 /* FirstProductCreatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstProductCreatedView.swift; sourceTree = ""; }; DEA0D0672BA82EA2007786F2 /* StatsGranularityV4+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatsGranularityV4+UI.swift"; sourceTree = ""; }; DEA357122ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignListViewModelTests.swift; sourceTree = ""; }; + DEA64C522E40B04000791018 /* OrderDetailsProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailsProduct.swift; sourceTree = ""; }; + DEA64C542E41A2CA00791018 /* Product+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Product+Helpers.swift"; sourceTree = ""; }; DEA6BCAE2BC6A9B10017D671 /* StoreStatsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsChart.swift; sourceTree = ""; }; DEA6BCB02BC6AA040017D671 /* StoreStatsChartViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsChartViewModel.swift; sourceTree = ""; }; DEA88F4F2AA9D0100037273B /* AddEditProductCategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditProductCategoryViewModel.swift; sourceTree = ""; }; @@ -9897,6 +9901,7 @@ 6856D6E9B1C3C89938DCAD5C /* Testing */ = { isa = PBXGroup; children = ( + DEA64C542E41A2CA00791018 /* Product+Helpers.swift */, 6856D66A1963092C34D20674 /* Calendar+Extensions.swift */, 571FDDAD24C768DC00D486A5 /* MockZendeskManager.swift */, 2084B7A32C776A6900EFBD2E /* XCTestCase+PropertyCount.swift */, @@ -12898,6 +12903,7 @@ D817586322BDD81600289CFE /* OrderDetailsDataSource.swift */, DE8C63AD2E1E2D1400DA48AC /* OrderDetailsShipmentDetailsView.swift */, D8C11A4D22DD235F00D4A88D /* OrderDetailsResultsControllers.swift */, + DEA64C522E40B04000791018 /* OrderDetailsProduct.swift */, D8C11A5D22E2440400D4A88D /* OrderPaymentDetailsViewModel.swift */, 31316F9B25CB20FD00D9F129 /* OrderStatusListViewModel.swift */, D8652E572630BFF500350F37 /* OrderDetailsPaymentAlerts.swift */, @@ -15340,6 +15346,7 @@ 03A9F3B22A03E70700385673 /* AdaptiveAsyncImage.swift in Sources */, DE2004602BF7092900660A72 /* ProductStockDashboardCard.swift in Sources */, 205B7EB92C19FAF700D14A36 /* PointOfSaleCardPresentPaymentScanningForReadersAlertViewModel.swift in Sources */, + DEA64C532E40B04700791018 /* OrderDetailsProduct.swift in Sources */, 31C21FA426D9949000916E2E /* SeveralReadersFoundViewController.swift in Sources */, 205B7ED12C19FD8500D14A36 /* PointOfSaleCardPresentPaymentErrorMessageViewModel.swift in Sources */, CECEFA6D2CA2CEB50071C7DB /* WooShippingPackageAndRatePlaceholder.swift in Sources */, @@ -17368,6 +17375,7 @@ 01A3093C2DAE768600B672F6 /* MockPointOfSaleCouponService.swift in Sources */, 03CF78D127C3DBC000523706 /* WCPayCardBrand+IconsTests.swift in Sources */, CEEF742C2B9A052300B03948 /* OrdersReportCardViewModelTests.swift in Sources */, + DEA64C552E41A2D000791018 /* Product+Helpers.swift in Sources */, DEFE1B242DF2C6C8005B3D39 /* UPSTermsViewModelTests.swift in Sources */, AEFF77AA29786DAA00667F7A /* PriceInputViewModelTests.swift in Sources */, EEC2D27F292CF60E0072132E /* JetpackSetupHostingControllerTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Testing/Product+Helpers.swift b/WooCommerce/WooCommerceTests/Testing/Product+Helpers.swift new file mode 100644 index 00000000000..fa6af36778a --- /dev/null +++ b/WooCommerce/WooCommerceTests/Testing/Product+Helpers.swift @@ -0,0 +1,17 @@ +@testable import WooCommerce +import Yosemite + +extension Product { + func toOrderDetailsProduct() -> OrderDetailsProduct { + OrderDetailsProduct(siteID: siteID, + productID: productID, + name: name, + productTypeKey: productTypeKey, + sku: sku, + price: price, + virtual: virtual, + stockQuantity: stockQuantity, + imageURL: imageURL, + addOns: addOns) + } +} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Details/AddOns/AddOnCrossreferenceTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Details/AddOns/AddOnCrossreferenceTests.swift index a409c5f7331..be3fd86fadf 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Details/AddOns/AddOnCrossreferenceTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Details/AddOns/AddOnCrossreferenceTests.swift @@ -23,7 +23,7 @@ class AddOnCrossreferenceTests: XCTestCase { ]) // When - let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product, addOnGroups: []) + let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product.toOrderDetailsProduct(), addOnGroups: []) let addOns = useCase.addOns() // Then @@ -43,7 +43,7 @@ class AddOnCrossreferenceTests: XCTestCase { ]) // When - let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product, addOnGroups: []) + let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product.toOrderDetailsProduct(), addOnGroups: []) let addOns = useCase.addOns() // Then @@ -62,7 +62,7 @@ class AddOnCrossreferenceTests: XCTestCase { ]) // When - let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product, addOnGroups: []) + let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product.toOrderDetailsProduct(), addOnGroups: []) let addOns = useCase.addOns() // Then @@ -82,7 +82,7 @@ class AddOnCrossreferenceTests: XCTestCase { let product = Product.fake() // When - let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product, addOnGroups: []) + let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product.toOrderDetailsProduct(), addOnGroups: []) let addOns = useCase.addOns() // Then @@ -98,7 +98,7 @@ class AddOnCrossreferenceTests: XCTestCase { ]) // When - let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: [], product: product, addOnGroups: []) + let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: [], product: product.toOrderDetailsProduct(), addOnGroups: []) let addOns = useCase.addOns() // Then @@ -119,7 +119,7 @@ class AddOnCrossreferenceTests: XCTestCase { ] // When - let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product, addOnGroups: addOnGroups) + let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product.toOrderDetailsProduct(), addOnGroups: addOnGroups) let addOns = useCase.addOns() // Then @@ -148,7 +148,7 @@ class AddOnCrossreferenceTests: XCTestCase { ] // When - let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product, addOnGroups: addOnGroups) + let useCase = AddOnCrossreferenceUseCase(orderItemAttributes: orderItemAttributes, product: product.toOrderDetailsProduct(), addOnGroups: addOnGroups) let addOns = useCase.addOns() // Then diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Details/Shipping Labels/AggregatedShippingLabelOrderItemsTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Details/Shipping Labels/AggregatedShippingLabelOrderItemsTests.swift index 49f691735dd..ca60197f0ed 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Details/Shipping Labels/AggregatedShippingLabelOrderItemsTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Details/Shipping Labels/AggregatedShippingLabelOrderItemsTests.swift @@ -37,8 +37,12 @@ final class AggregatedShippingLabelOrderItemsTests: XCTestCase { let shippingLabel = MockShippingLabel.emptyLabel().copy(productIDs: [2020, 3013, 3013, 3013], productNames: ["Woo", "PW", "PW", "PW"]) let imageURL1 = URL(string: "woocommerce.com/woocommerce.jpeg")! - let product1 = Product.fake().copy(productID: 2020, name: "Whoa", price: "25.9", images: [createProductImage(src: imageURL1.absoluteString)]) - let product2 = Product.fake().copy(productID: 3013, name: "Password", price: "25.9") + let product1 = Product.fake() + .copy(productID: 2020, name: "Whoa", price: "25.9", images: [createProductImage(src: imageURL1.absoluteString)]) + .toOrderDetailsProduct() + let product2 = Product.fake() + .copy(productID: 3013, name: "Password", price: "25.9") + .toOrderDetailsProduct() let orderItem1 = MockOrderItem.sampleItem(itemID: 1, name: "Woooo", productID: 2020, price: 59.2, sku: "woo") let aggregatedOrderItems = AggregatedShippingLabelOrderItems(shippingLabels: [shippingLabel], orderItems: [orderItem1], diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Review Order/ReviewOrderViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Review Order/ReviewOrderViewModelTests.swift index 542fcd8518c..a2b45403812 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Orders/Review Order/ReviewOrderViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Orders/Review Order/ReviewOrderViewModelTests.swift @@ -36,7 +36,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID, quantity: 1) let order = Order.fake().copy(status: .processing, items: [item]) - let product = Product.fake().copy(productID: productID) + let product = Product.fake().copy(productID: productID).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: false) @@ -52,7 +52,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID, quantity: 1) let order = Order.fake().copy(status: .processing, items: [item]) - let product = Product.fake().copy(productID: productID) + let product = Product.fake().copy(productID: productID).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: false) @@ -84,7 +84,7 @@ final class ReviewOrderViewModelTests: XCTestCase { let item = OrderItem.fake().copy(productID: productID, quantity: 1, attributes: [itemAttribute]) let order = Order.fake().copy(siteID: siteID, status: .processing, items: [item]) let addOn = ProductAddOn.fake().copy(name: addOnName) - let product = Product.fake().copy(productID: productID, addOns: [addOn]) + let product = Product.fake().copy(productID: productID, addOns: [addOn]).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: true, storageManager: storageManager) @@ -101,8 +101,8 @@ final class ReviewOrderViewModelTests: XCTestCase { let itemID1: Int64 = 134 let itemID2: Int64 = 432 - let product1 = Product.fake().copy(productID: productID) - let product2 = Product.fake().copy(productID: productID2) + let product1 = Product.fake().copy(productID: productID).toOrderDetailsProduct() + let product2 = Product.fake().copy(productID: productID2).toOrderDetailsProduct() let item1 = OrderItem.fake().copy(itemID: itemID1, productID: product1.productID, quantity: 1) let item2 = OrderItem.fake().copy(itemID: itemID2, productID: product2.productID, quantity: -1) @@ -133,7 +133,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(status: .processing, customerNote: nil, items: [item]) - let product = Product.fake().copy(productID: productID) + let product = Product.fake().copy(productID: productID).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: false) @@ -156,7 +156,7 @@ final class ReviewOrderViewModelTests: XCTestCase { let note = "Test" let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(status: .processing, customerNote: note, items: [item]) - let product = Product.fake().copy(productID: productID) + let product = Product.fake().copy(productID: productID).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: false) @@ -178,7 +178,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(status: .processing, items: [item], shippingLines: []) - let product = Product.fake().copy(productID: productID) + let product = Product.fake().copy(productID: productID).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: false) @@ -200,7 +200,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(status: .processing, items: [item], shippingLines: [ShippingLine.fake()]) - let product = Product.fake().copy(productID: productID) + let product = Product.fake().copy(productID: productID).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: false) @@ -222,7 +222,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(status: .processing, items: [item]) - let product = Product.fake().copy(productID: productID, virtual: true) + let product = Product.fake().copy(productID: productID, virtual: true).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: false) @@ -244,7 +244,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(status: .processing, items: [item], shippingAddress: Address.fake()) - let product = Product.fake().copy(productID: productID, virtual: false) + let product = Product.fake().copy(productID: productID, virtual: false).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: false) @@ -268,7 +268,7 @@ final class ReviewOrderViewModelTests: XCTestCase { insertShippingLabel(shippingLabel) let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(siteID: siteID, orderID: orderID, status: .processing, items: [item], shippingAddress: Address.fake()) - let product = Product.fake().copy(productID: productID, virtual: false) + let product = Product.fake().copy(productID: productID, virtual: false).toOrderDetailsProduct() // When let viewModel = ReviewOrderViewModel(order: order, products: [product], showAddOns: false, storageManager: storageManager) @@ -285,7 +285,7 @@ final class ReviewOrderViewModelTests: XCTestCase { insertShippingLabel(shippingLabel) let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(siteID: siteID, orderID: orderID, status: .processing, items: [item], shippingAddress: Address.fake()) - let product = Product.fake().copy(productID: productID, virtual: false) + let product = Product.fake().copy(productID: productID, virtual: false).toOrderDetailsProduct() let stores = MockShipmentActionStoresManager(syncSuccessfully: true) // When @@ -309,7 +309,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(orderID: orderID, status: .processing, items: [item], shippingAddress: Address.fake()) - let product = Product.fake().copy(productID: productID, virtual: false) + let product = Product.fake().copy(productID: productID, virtual: false).toOrderDetailsProduct() let stores = MockShipmentActionStoresManager(syncSuccessfully: true) // When @@ -333,7 +333,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(orderID: orderID, status: .processing, items: [item], shippingAddress: Address.fake()) - let product = Product.fake().copy(productID: productID, virtual: false) + let product = Product.fake().copy(productID: productID, virtual: false).toOrderDetailsProduct() let stores = MockShipmentActionStoresManager(syncSuccessfully: false) // When @@ -349,7 +349,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(orderID: orderID, status: .processing, items: [item], shippingAddress: Address.fake()) - let product = Product.fake().copy(productID: productID, virtual: false) + let product = Product.fake().copy(productID: productID, virtual: false).toOrderDetailsProduct() let stores = MockShipmentActionStoresManager(syncSuccessfully: true) // When @@ -373,7 +373,7 @@ final class ReviewOrderViewModelTests: XCTestCase { // Given let item = OrderItem.fake().copy(productID: productID) let order = Order.fake().copy(siteID: siteID, orderID: orderID, status: .processing, items: [item], shippingAddress: Address.fake()) - let product = Product.fake().copy(productID: productID, virtual: false) + let product = Product.fake().copy(productID: productID, virtual: false).toOrderDetailsProduct() let stores = MockShipmentActionStoresManager(syncSuccessfully: true) let shipmentTracking = ShipmentTracking.fake().copy(siteID: siteID, orderID: orderID, dateShipped: Date()) insertShipmentTracking(shipmentTracking)