Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Shipping labels] Add support for retrieving products and product variations related to the order items #13964

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import UIKit
import Yosemite
import protocol Storage.StorageManagerType

/// Provides data about items to ship for an order with the Woo Shipping extension.
///
protocol WooShippingItemsDataSource {
var orderItems: [OrderItem] { get }
var products: [Product] { get }
var productVariations: [ProductVariation] { get }
}

final class DefaultWooShippingItemsDataSource: WooShippingItemsDataSource {
private let order: Order
private let storageManager: StorageManagerType
private let stores: StoresManager

/// Items in the order.
///
var orderItems: [OrderItem] {
order.items
}

/// Stored products that match the items in the order.
///
var products: [Product] {
try? productResultsController.performFetch()
return productResultsController.fetchedObjects
}

/// Stored product variations that match the items in the order.
///
var productVariations: [ProductVariation] {
try? productVariationResultsController.performFetch()
return productVariationResultsController.fetchedObjects
}

/// Product ResultsController.
///
private lazy var productResultsController: ResultsController<StorageProduct> = {
let productIDs = order.items.map(\.productID)
let predicate = NSPredicate(format: "siteID == %lld AND productID in %@", order.siteID, productIDs)
let descriptor = NSSortDescriptor(key: "name", ascending: true)

return ResultsController<StorageProduct>(storageManager: storageManager, matching: predicate, sortedBy: [descriptor])
}()

/// ProductVariation ResultsController.
///
private lazy var productVariationResultsController: ResultsController<StorageProductVariation> = {
let variationIDs = order.items.map(\.variationID).filter { $0 != 0 }
let predicate = NSPredicate(format: "siteID == %lld AND productVariationID in %@", order.siteID, variationIDs)

return ResultsController<StorageProductVariation>(storageManager: storageManager, matching: predicate, sortedBy: [])
}()


init(order: Order,
storageManager: StorageManagerType = ServiceLocator.storageManager,
stores: StoresManager = ServiceLocator.stores) {
self.order = order
self.storageManager = storageManager
self.stores = stores

configureProductResultsController()
configureProductVariationResultsController()
syncProducts()
syncProductVariations()
}

private func configureProductResultsController() {
do {
try productResultsController.performFetch()
} catch {
DDLogError("⛔️ Error fetching products for Woo Shipping label creation: \(error)")
}
}

private func configureProductVariationResultsController() {
do {
try productVariationResultsController.performFetch()
} catch {
DDLogError("⛔️ Error fetching product variations for Woo Shipping label creation: \(error)")
}
}

private func syncProducts() {
let action = ProductAction.requestMissingProducts(for: order) { error in
if let error {
DDLogError("⛔️ Error synchronizing products for Woo Shipping label creation: \(error)")
return
}
}

stores.dispatch(action)
}

private func syncProductVariations() {
let action = ProductVariationAction.requestMissingVariations(for: order) { error in
if let error {
DDLogError("⛔️ Error synchronizing product variations for Woo Shipping label creation: \(error)")
return
}
}
stores.dispatch(action)
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import Foundation
import Yosemite
import WooFoundation
import protocol Storage.StorageManagerType

/// Provides view data for `WooShippingItems`.
///
final class WooShippingItemsViewModel: ObservableObject {
private let orderItems: [OrderItem]
private let currencyFormatter: CurrencyFormatter

/// Data source for items to be shipped.
private var dataSource: WooShippingItemsDataSource

/// Label with the total number of items to ship.
@Published var itemsCountLabel: String = ""

Expand All @@ -17,9 +20,9 @@ final class WooShippingItemsViewModel: ObservableObject {
/// View models for rows of items to ship.
@Published var itemRows: [WooShippingItemRowViewModel] = []

init(orderItems: [OrderItem],
init(dataSource: WooShippingItemsDataSource,
currencySettings: CurrencySettings = ServiceLocator.currencySettings) {
self.orderItems = orderItems
self.dataSource = dataSource
self.currencyFormatter = CurrencyFormatter(currencySettings: currencySettings)

configureSectionHeader()
Expand All @@ -44,7 +47,7 @@ private extension WooShippingItemsViewModel {
/// Generates a label with the total number of items to ship.
///
func generateItemsCountLabel() -> String {
let itemsCount = orderItems.map(\.quantity).reduce(0, +)
let itemsCount = dataSource.orderItems.map(\.quantity).reduce(0, +)
return Localization.itemsCount(itemsCount)
}

Expand All @@ -53,7 +56,7 @@ private extension WooShippingItemsViewModel {
///
func generateItemsDetailLabel() -> String {
let formattedWeight = "1 kg" // TODO-13550: Get the total weight (each product/variation * item quantity) and weight unit
let itemsTotal = orderItems.map { $0.price.decimalValue * $0.quantity }.reduce(0, +)
let itemsTotal = dataSource.orderItems.map { $0.price.decimalValue * $0.quantity }.reduce(0, +)
let formattedPrice = currencyFormatter.formatAmount(itemsTotal) ?? itemsTotal.description

return "\(formattedWeight) • \(formattedPrice)"
Expand All @@ -62,7 +65,7 @@ private extension WooShippingItemsViewModel {
/// Generates an item row view model for each order item.
///
func generateItemRows() -> [WooShippingItemRowViewModel] {
orderItems.map { item in
dataSource.orderItems.map { item in
WooShippingItemRowViewModel(imageUrl: nil, // TODO-13550: Get the product/variation imageURL
quantityLabel: item.quantity.description,
name: item.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
@Published private(set) var items: WooShippingItemsViewModel

init(order: Order) {
self.items = WooShippingItemsViewModel(orderItems: order.items)
self.items = WooShippingItemsViewModel(dataSource: DefaultWooShippingItemsDataSource(order: order))
}
}
8 changes: 8 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2278,6 +2278,8 @@
CEC3CC6D2C93127300B93FBE /* WooShippingItemsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3CC6C2C93127300B93FBE /* WooShippingItemsViewModel.swift */; };
CEC3CC6F2C93146700B93FBE /* WooShippingCreateLabelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3CC6E2C93146700B93FBE /* WooShippingCreateLabelsViewModel.swift */; };
CEC3CC742C9343DF00B93FBE /* WooShippingItemsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3CC732C9343DF00B93FBE /* WooShippingItemsViewModelTests.swift */; };
CEC3CC762C934F5500B93FBE /* WooShippingItemsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3CC752C934F5500B93FBE /* WooShippingItemsDataSource.swift */; };
CEC3CC7C2C94A06500B93FBE /* WooShippingItemsDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3CC7B2C94A06500B93FBE /* WooShippingItemsDataSourceTests.swift */; };
CEC8188C2A3B7C8B00459843 /* AppStartupWaitingTimeTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8188B2A3B7C8B00459843 /* AppStartupWaitingTimeTracker.swift */; };
CEC8188E2A3C75DD00459843 /* AppStartupWaitingTimeTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8188D2A3C75DD00459843 /* AppStartupWaitingTimeTrackerTests.swift */; };
CECC758623D21AC200486676 /* AggregateOrderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECC758523D21AC200486676 /* AggregateOrderItem.swift */; };
Expand Down Expand Up @@ -5322,6 +5324,8 @@
CEC3CC6C2C93127300B93FBE /* WooShippingItemsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingItemsViewModel.swift; sourceTree = "<group>"; };
CEC3CC6E2C93146700B93FBE /* WooShippingCreateLabelsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingCreateLabelsViewModel.swift; sourceTree = "<group>"; };
CEC3CC732C9343DF00B93FBE /* WooShippingItemsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingItemsViewModelTests.swift; sourceTree = "<group>"; };
CEC3CC752C934F5500B93FBE /* WooShippingItemsDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingItemsDataSource.swift; sourceTree = "<group>"; };
CEC3CC7B2C94A06500B93FBE /* WooShippingItemsDataSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingItemsDataSourceTests.swift; sourceTree = "<group>"; };
CEC8188B2A3B7C8B00459843 /* AppStartupWaitingTimeTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStartupWaitingTimeTracker.swift; sourceTree = "<group>"; };
CEC8188D2A3C75DD00459843 /* AppStartupWaitingTimeTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStartupWaitingTimeTrackerTests.swift; sourceTree = "<group>"; };
CECA64B020D9990E005A44C4 /* WooCommerce-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WooCommerce-Bridging-Header.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -11751,6 +11755,7 @@
CEC3CC6A2C92FDB700B93FBE /* WooShippingItemRowViewModel.swift */,
CE6E110C2C91E5FF00563DD4 /* WooShippingItems.swift */,
CEC3CC6C2C93127300B93FBE /* WooShippingItemsViewModel.swift */,
CEC3CC752C934F5500B93FBE /* WooShippingItemsDataSource.swift */,
);
path = "WooShipping Items Section";
sourceTree = "<group>";
Expand Down Expand Up @@ -11861,6 +11866,7 @@
isa = PBXGroup;
children = (
CEC3CC732C9343DF00B93FBE /* WooShippingItemsViewModelTests.swift */,
CEC3CC7B2C94A06500B93FBE /* WooShippingItemsDataSourceTests.swift */,
);
path = "WooShipping Create Shipping Labels";
sourceTree = "<group>";
Expand Down Expand Up @@ -16294,6 +16300,7 @@
DE8BEB932ABAF07700F5E56C /* AddProductFeaturesViewModel.swift in Sources */,
7E7C5F872719A93C00315B61 /* ProductCategoryListViewController.swift in Sources */,
2631D4FE29F2141D00F13F20 /* StorePlanBannerPresenter.swift in Sources */,
CEC3CC762C934F5500B93FBE /* WooShippingItemsDataSource.swift in Sources */,
DEC51B06276B3F3C009F3DF4 /* Int64+Helpers.swift in Sources */,
02BE9CC029C05CFD00292333 /* SitePreviewView.swift in Sources */,
D8B4D5EE26C2C26C00F34E94 /* InPersonPaymentsStripeAcountReviewView.swift in Sources */,
Expand Down Expand Up @@ -16903,6 +16910,7 @@
453770D12431FF4700AC718D /* ProductSettingsViewModelTests.swift in Sources */,
2619FA2C25C897930006DAFF /* AddAttributeOptionsViewModelTests.swift in Sources */,
DEB387A02C35109E0025256E /* GoogleAdsCampaignCoordinatorTests.swift in Sources */,
CEC3CC7C2C94A06500B93FBE /* WooShippingItemsDataSourceTests.swift in Sources */,
020BE77523B4A7EC007FE54C /* AztecSourceCodeFormatBarCommandTests.swift in Sources */,
DE4D23B429B58C5A003A4B5D /* MockWordPressComAccountService.swift in Sources */,
57A5D8D92534FEBB00AA54D6 /* TotalRefundedCalculationUseCaseTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import XCTest
@testable import WooCommerce
import Yosemite

final class WooShippingItemsDataSourceTests: XCTestCase {

private var storageManager: MockStorageManager!
private var stores: MockStoresManager!

override func setUp() {
super.setUp()
storageManager = MockStorageManager()
stores = MockStoresManager(sessionManager: .testingInstance)
}

func test_it_inits_with_expected_order_items() {
// Given
let order = Order.fake().copy(items: [OrderItem.fake(), OrderItem.fake()])

// When
let dataSource = DefaultWooShippingItemsDataSource(order: order)

// Then
assertEqual(2, dataSource.orderItems.count)
}

func test_it_inits_with_expected_stored_products_and_variations() {
// Given
let product = Product.fake().copy(productID: 11)
let variation = ProductVariation.fake().copy(productVariationID: 12)
storageManager.insertSampleProduct(readOnlyProduct: product)
storageManager.insertSampleProductVariation(readOnlyProductVariation: variation)
let order = Order.fake().copy(items: [OrderItem.fake().copy(productID: product.productID),
OrderItem.fake().copy(variationID: variation.productVariationID)])

// When
let dataSource = DefaultWooShippingItemsDataSource(order: order, storageManager: storageManager)

// Then
assertEqual(1, dataSource.products.count)
assertEqual(1, dataSource.productVariations.count)
}

func test_it_inits_with_expected_products_and_variations_from_remote() {
// Given
let product = Product.fake().copy(productID: 13)
let variation = ProductVariation.fake().copy(productVariationID: 14)
let order = Order.fake().copy(items: [OrderItem.fake().copy(productID: product.productID),
OrderItem.fake().copy(variationID: variation.productVariationID)])
stores.whenReceivingAction(ofType: ProductAction.self) { action in
switch action {
case .requestMissingProducts:
self.storageManager.insertSampleProduct(readOnlyProduct: product)
default:
XCTFail("Received unexpected action: \(action)")
}
}
stores.whenReceivingAction(ofType: ProductVariationAction.self) { action in
switch action {
case .requestMissingVariations:
self.storageManager.insertSampleProductVariation(readOnlyProductVariation: variation)
default:
XCTFail("Received unexpected action: \(action)")
}
}


// When
let dataSource = DefaultWooShippingItemsDataSource(order: order, storageManager: storageManager, stores: stores)

// Then
assertEqual(1, dataSource.products.count)
assertEqual(1, dataSource.productVariations.count)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ final class WooShippingItemsViewModelTests: XCTestCase {
price: 10,
attributes: [OrderItemAttribute.fake().copy(value: "Red")]),
OrderItem.fake().copy(quantity: 1, price: 2.5)]
let dataSource = MockDataSource(orderItems: orderItems)

// When
let viewModel = WooShippingItemsViewModel(orderItems: orderItems, currencySettings: currencySettings)
let viewModel = WooShippingItemsViewModel(dataSource: dataSource, currencySettings: currencySettings)

// Then
// Section header labels have expected values
Expand All @@ -39,9 +40,10 @@ final class WooShippingItemsViewModelTests: XCTestCase {
func test_total_items_count_handles_items_with_quantity_greater_than_one() {
// Given
let orderItems = [OrderItem.fake().copy(quantity: 2), OrderItem.fake().copy(quantity: 1)]
let dataSource = MockDataSource(orderItems: orderItems)

// When
let viewModel = WooShippingItemsViewModel(orderItems: orderItems, currencySettings: currencySettings)
let viewModel = WooShippingItemsViewModel(dataSource: dataSource, currencySettings: currencySettings)

// Then
assertEqual("3 items", viewModel.itemsCountLabel)
Expand All @@ -50,9 +52,10 @@ final class WooShippingItemsViewModelTests: XCTestCase {
func test_total_items_details_handles_total_price_for_items_with_quantity_greater_than_one() {
// Given
let orderItems = [OrderItem.fake().copy(quantity: 2, price: 10), OrderItem.fake().copy(quantity: 1, price: 2.5)]
let dataSource = MockDataSource(orderItems: orderItems)

// When
let viewModel = WooShippingItemsViewModel(orderItems: orderItems, currencySettings: currencySettings)
let viewModel = WooShippingItemsViewModel(dataSource: dataSource, currencySettings: currencySettings)

// Then
assertEqual("1 kg • $22.50", viewModel.itemsDetailLabel)
Expand All @@ -62,13 +65,30 @@ final class WooShippingItemsViewModelTests: XCTestCase {
// Given
let orderItems = [OrderItem.fake().copy(attributes: [OrderItemAttribute.fake().copy(value: "Red"),
OrderItemAttribute.fake().copy(value: "Small")])]
let dataSource = MockDataSource(orderItems: orderItems)

// When
let viewModel = WooShippingItemsViewModel(orderItems: orderItems, currencySettings: currencySettings)
let viewModel = WooShippingItemsViewModel(dataSource: dataSource, currencySettings: currencySettings)

// Then
let firstItem = try XCTUnwrap(viewModel.itemRows.first)
assertEqual("Red, Small", firstItem.detailsLabel)
}

}

private final class MockDataSource: WooShippingItemsDataSource {
var orderItems: [OrderItem]

var products: [Product]

var productVariations: [ProductVariation]

init(orderItems: [OrderItem] = [],
products: [Product] = [],
productVariations: [ProductVariation] = []) {
self.orderItems = orderItems
self.products = products
self.productVariations = productVariations
}
}