diff --git a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationGenerator.swift b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationGenerator.swift index 4245c06c6c5..b3668bb0e7d 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationGenerator.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationGenerator.swift @@ -5,17 +5,21 @@ import Yosemite /// struct ProductVariationGenerator { - /// Group a colection of attribute options. + /// Group a collection of attribute options. /// EG: [Size: Large, Color: Black, Fabric: Cotton] /// - private struct Combination: Hashable { + private struct Combination: Hashable, Equatable { let options: [Option] + + static func == (lhs: Combination, rhs: Combination) -> Bool { + Set(lhs.options) == Set(rhs.options) + } } /// Represents an attribute option. /// EG: Size: Large /// - private struct Option: Hashable { + private struct Option: Hashable, Equatable { let attributeID: Int64 let attributeName: String let value: String @@ -33,7 +37,7 @@ struct ProductVariationGenerator { /// Generates all posible combination for a product attributes. /// private static func getCombinations(from product: Product) -> [Combination] { - // Iterates through attributes while eceiving the previous combinations list. + // Iterates through attributes while receiving the previous combinations list. product.attributes.reduce([Combination(options: [])]) { combinations, attribute in combinations.flatMap { combination in // When receiving a previous combination list, we add each attribute to each previous combination util we finish with them. @@ -49,17 +53,17 @@ struct ProductVariationGenerator { private static func filterExistingCombinations(_ combinations: [Combination], existing variations: [ProductVariation]) -> [Combination] { // Convert variations into combinations let existingCombinations = variations.map { existingVariation in - let options = existingVariation.attributes.map { attibute in - Option(attributeID: attibute.id, attributeName: attibute.name, value: attibute.option) + let options = existingVariation.attributes.map { attribute in + Option(attributeID: attribute.id, attributeName: attribute.name, value: attribute.option) } return Combination(options: options) } // Filter existing combinations. - let existingSet = Set(existingCombinations) - return combinations.filter { combination in - !existingSet.contains(combination) + let unique = combinations.filter { combination in + !existingCombinations.contains(combination) } + return unique } /// Convert the provided combinations into `[CreateProductVariation]` types that are consumed by our Yosemite stores. diff --git a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewController.swift b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewController.swift index 065d232e709..103e0665b3d 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewController.swift @@ -619,6 +619,13 @@ private extension ProductVariationsViewController { let bottomSheetPresenter = BottomSheetListSelectorPresenter(viewProperties: viewProperties, command: command) bottomSheetPresenter.show(from: self, sourceView: topStackView) } + + /// Informs the merchant about errors that happen during the variation generation + /// + private func presentGenerationError(_ error: ProductVariationsViewModel.GenerationError) { + let notice = Notice(title: error.errorTitle, message: error.errorDescription) + noticePresenter.enqueue(notice: notice) + } } // MARK: - Placeholders @@ -695,10 +702,17 @@ extension ProductVariationsViewController: SyncingCoordinatorDelegate { /// Generates all possible variations for the product attibutes. /// private func generateAllVariations() { - viewModel.generateAllVariations(for: product) + viewModel.generateAllVariations(for: product) { [weak self] result in + guard let self else { return } + switch result { + case .success: + break + case .failure(let error): + self.presentGenerationError(error) + } + } // TODO: // - Show Loading Indicator - // - Alert if there are more than 100 variations to create // - Hide Loading Indicator } } diff --git a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift index c2d828ec67a..5cd8ed7dc0d 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift @@ -27,7 +27,7 @@ final class ProductVariationsViewModel { /// Generates all missing variations for a product. Up to 100 variations. /// - func generateAllVariations(for product: Product) { + func generateAllVariations(for product: Product, onCompletion: @escaping (Result) -> Void) { let action = ProductVariationAction.synchronizeAllProductVariations(siteID: product.siteID, productID: product.productID) { result in // TODO: Fetch this via a results controller let existingVariations = ServiceLocator.storageManager.viewStorage.loadProductVariations(siteID: product.siteID, productID: product.productID)? @@ -39,6 +39,13 @@ final class ProductVariationsViewModel { let variationsToGenerate = ProductVariationGenerator.generateVariations(for: product, excluding: existingVariations) print("Variations to Generate: \(variationsToGenerate.count)") + // Guard for 100 variation limit + guard variationsToGenerate.count <= 100 else { + return onCompletion(.failure(.tooManyVariations(variationCount: variationsToGenerate.count))) + } + + onCompletion(.success(())) + } stores.dispatch(action) @@ -71,3 +78,29 @@ extension ProductVariationsViewModel { product.attributesForVariations.isEmpty } } + +extension ProductVariationsViewModel { + /// Type to represent known generation errors + /// + enum GenerationError: LocalizedError, Equatable { + case tooManyVariations(variationCount: Int) + + var errorTitle: String { + switch self { + case .tooManyVariations: + return NSLocalizedString("Generation limit exceeded", comment: "Error title for for when there are too many variations to generate.") + } + } + + var errorDescription: String? { + switch self { + case .tooManyVariations(let variationCount): + let format = NSLocalizedString( + "Currently creation is supported for 100 variations maximum. Generating variations for this product would create %d variations.", + comment: "Error description for when there are too many variations to generate." + ) + return String.localizedStringWithFormat(format, variationCount) + } + } + } +} diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Products/Variations/ProductVariationsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Products/Variations/ProductVariationsViewModelTests.swift index 1080df4cfa2..cc5042b091d 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Products/Variations/ProductVariationsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Products/Variations/ProductVariationsViewModelTests.swift @@ -114,4 +114,38 @@ final class ProductVariationsViewModelTests: XCTestCase { XCTAssertTrue(product.existsRemotely) XCTAssertEqual(viewModel.formType, .readonly) } + + func test_trying_to_generate_more_than_100_variations_will_return_error() { + // Given + let product = Product.fake().copy(attributes: [ + ProductAttribute.fake().copy(attributeID: 1, name: "Size", options: ["XS", "S", "M", "L", "XL"]), + ProductAttribute.fake().copy(attributeID: 2, name: "Color", options: ["Red", "Green", "Blue", "White", "Black"]), + ProductAttribute.fake().copy(attributeID: 3, name: "Fabric", options: ["Cotton", "Nylon", "Polyester", "Silk", "Linen"]), + ]) + + let stores = MockStoresManager(sessionManager: SessionManager.makeForTesting()) + stores.whenReceivingAction(ofType: ProductVariationAction.self) { action in + switch action { + case .synchronizeAllProductVariations(_, _, let onCompletion): + onCompletion(.success(())) + default: + break + } + } + + let viewModel = ProductVariationsViewModel(stores: stores, formType: .edit) + + // When + let error = waitFor { promise in + viewModel.generateAllVariations(for: product) { result in + if case let .failure(error) = result { + promise(error) + } + } + } + + // Then + XCTAssertEqual(error, .tooManyVariations(variationCount: 125)) + + } }