Skip to content

Commit 77ff590

Browse files
authored
Merge pull request #8537 from woocommerce/issue/8488-product-variation-generator
Variations: Generates all variations locally
2 parents 618e1e9 + 946d30f commit 77ff590

File tree

4 files changed

+215
-5
lines changed

4 files changed

+215
-5
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import Foundation
2+
import Yosemite
3+
4+
/// Generates all possible variations from a product attributes
5+
///
6+
struct ProductVariationGenerator {
7+
8+
/// Group a colection of attribute options.
9+
/// EG: [Size: Large, Color: Black, Fabric: Cotton]
10+
///
11+
private struct Combination: Hashable {
12+
let options: [Option]
13+
}
14+
15+
/// Represents an attribute option.
16+
/// EG: Size: Large
17+
///
18+
private struct Option: Hashable {
19+
let attributeID: Int64
20+
let attributeName: String
21+
let value: String
22+
}
23+
24+
/// Generates all possible variations from a product attributes.
25+
/// Additionally it excludes variations that already exists in the `variations` parameter.
26+
///
27+
static func generateVariations(for product: Product, excluding variations: [ProductVariation]) -> [CreateProductVariation] {
28+
let allCombinations = getCombinations(from: product)
29+
let uniqueCombinations = filterExistingCombinations(allCombinations, existing: variations)
30+
return buildVariations(from: uniqueCombinations, for: product)
31+
}
32+
33+
/// Generates all posible combination for a product attributes.
34+
///
35+
private static func getCombinations(from product: Product) -> [Combination] {
36+
// Iterates through attributes while eceiving the previous combinations list.
37+
product.attributes.reduce([Combination(options: [])]) { combinations, attribute in
38+
combinations.flatMap { combination in
39+
// When receiving a previous combination list, we add each attribute to each previous combination util we finish with them.
40+
attribute.options.map { option in
41+
Combination(options: combination.options + [Option(attributeID: attribute.attributeID, attributeName: attribute.name, value: option)])
42+
}
43+
}
44+
}
45+
}
46+
47+
/// Removes the provided variations from the given combinations array.
48+
///
49+
private static func filterExistingCombinations(_ combinations: [Combination], existing variations: [ProductVariation]) -> [Combination] {
50+
// Convert variations into combinations
51+
let existingCombinations = variations.map { existingVariation in
52+
let options = existingVariation.attributes.map { attibute in
53+
Option(attributeID: attibute.id, attributeName: attibute.name, value: attibute.option)
54+
}
55+
return Combination(options: options)
56+
}
57+
58+
// Filter existing combinations.
59+
let existingSet = Set(existingCombinations)
60+
return combinations.filter { combination in
61+
!existingSet.contains(combination)
62+
}
63+
}
64+
65+
/// Convert the provided combinations into `[CreateProductVariation]` types that are consumed by our Yosemite stores.
66+
///
67+
private static func buildVariations(from combinations: [Combination], for product: Product) -> [CreateProductVariation] {
68+
combinations.map { combination in
69+
let attributes = combination.options.map { option in
70+
ProductVariationAttribute(id: option.attributeID, name: option.attributeName, option: option.value)
71+
}
72+
// Setting a regular price is not required when creating a variation.
73+
return CreateProductVariation(regularPrice: "", attributes: attributes)
74+
}
75+
}
76+
}

WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,20 @@ final class ProductVariationsViewModel {
2929
///
3030
func generateAllVariations(for product: Product) {
3131
let action = ProductVariationAction.synchronizeAllProductVariations(siteID: product.siteID, productID: product.productID) { result in
32-
// Temp
33-
let fetched = ServiceLocator.storageManager.viewStorage.loadProductVariations(siteID: product.siteID, productID: product.productID)
34-
print("Synchronized \(fetched?.count ?? 0) variations")
32+
// TODO: Fetch this via a results controller
33+
let existingVariations = ServiceLocator.storageManager.viewStorage.loadProductVariations(siteID: product.siteID, productID: product.productID)?
34+
.map {
35+
$0.toReadOnly()
36+
} ?? []
37+
38+
// TEMP
39+
let variationsToGenerate = ProductVariationGenerator.generateVariations(for: product, excluding: existingVariations)
40+
print("Variations to Generate: \(variationsToGenerate.count)")
41+
3542
}
3643
stores.dispatch(action)
3744

3845
// TODO:
39-
// - Generate all variations locally
40-
// - Substract already created variations
4146
// - Alert if there are more than 100 variations to create
4247
// - Create variations remotely
4348
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@
596596
262C921F26EEF8B100011F92 /* Binding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262C921E26EEF8B100011F92 /* Binding.swift */; };
597597
262C922126F1370000011F92 /* StorePickerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262C922026F1370000011F92 /* StorePickerError.swift */; };
598598
26309F17277D0AEA0012797F /* SafeAreaInsetsKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26309F16277D0AEA0012797F /* SafeAreaInsetsKey.swift */; };
599+
263C4CC02963784900CA7E05 /* ProductVariationGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 263C4CBF2963784900CA7E05 /* ProductVariationGenerator.swift */; };
599600
263E37E12641AD8300260D3B /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 263E37E02641AD8300260D3B /* Codegen */; };
600601
263E37E22641AD8300260D3B /* Codegen in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 263E37E02641AD8300260D3B /* Codegen */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
601602
263E38462641FF3400260D3B /* Codegen in Frameworks */ = {isa = PBXBuildFile; productRef = 263E38452641FF3400260D3B /* Codegen */; };
@@ -632,6 +633,7 @@
632633
2676F4CC2908284800C7A15B /* ProductCreationTypeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2676F4CB2908284800C7A15B /* ProductCreationTypeCommand.swift */; };
633634
26771A14256FFA8700EE030E /* IssueRefundCoordinatingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26771A13256FFA8700EE030E /* IssueRefundCoordinatingController.swift */; };
634635
2678897C270E6E8B00BD249E /* SimplePaymentsAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2678897B270E6E8B00BD249E /* SimplePaymentsAmount.swift */; };
636+
267D6882296485850072ED0C /* ProductVariationGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 267D6881296485850072ED0C /* ProductVariationGeneratorTests.swift */; };
635637
2687165524D21BC80042F6AE /* SurveySubmittedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2687165324D21BC80042F6AE /* SurveySubmittedViewController.swift */; };
636638
2687165624D21BC80042F6AE /* SurveySubmittedViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2687165424D21BC80042F6AE /* SurveySubmittedViewController.xib */; };
637639
2687165A24D350C20042F6AE /* SurveyCoordinatingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2687165924D350C20042F6AE /* SurveyCoordinatingController.swift */; };
@@ -2651,6 +2653,7 @@
26512653
262C921E26EEF8B100011F92 /* Binding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Binding.swift; sourceTree = "<group>"; };
26522654
262C922026F1370000011F92 /* StorePickerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePickerError.swift; sourceTree = "<group>"; };
26532655
26309F16277D0AEA0012797F /* SafeAreaInsetsKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeAreaInsetsKey.swift; sourceTree = "<group>"; };
2656+
263C4CBF2963784900CA7E05 /* ProductVariationGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationGenerator.swift; sourceTree = "<group>"; };
26542657
263EB408242C58EA00F3A15F /* ProductFormActionsFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductFormActionsFactoryTests.swift; sourceTree = "<group>"; };
26552658
2647F7B429280A7F00D59FDF /* AnalyticsHubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsHubView.swift; sourceTree = "<group>"; };
26562659
2647F7B9292BE2F900D59FDF /* AnalyticsReportCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsReportCard.swift; sourceTree = "<group>"; };
@@ -2683,6 +2686,7 @@
26832686
26771A13256FFA8700EE030E /* IssueRefundCoordinatingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueRefundCoordinatingController.swift; sourceTree = "<group>"; };
26842687
2678897B270E6E8B00BD249E /* SimplePaymentsAmount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplePaymentsAmount.swift; sourceTree = "<group>"; };
26852688
267CFE1824435A5500AF3A13 /* ProductCategoryViewModelBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCategoryViewModelBuilderTests.swift; sourceTree = "<group>"; };
2689+
267D6881296485850072ED0C /* ProductVariationGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationGeneratorTests.swift; sourceTree = "<group>"; };
26862690
2687165324D21BC80042F6AE /* SurveySubmittedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveySubmittedViewController.swift; sourceTree = "<group>"; };
26872691
2687165424D21BC80042F6AE /* SurveySubmittedViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SurveySubmittedViewController.xib; sourceTree = "<group>"; };
26882692
2687165924D350C20042F6AE /* SurveyCoordinatingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyCoordinatingController.swift; sourceTree = "<group>"; };
@@ -4772,6 +4776,7 @@
47724776
0202B68C23876BC100F3EBE0 /* ProductsTabProductViewModel+ProductVariation.swift */,
47734777
26F65C9725DEDAF0008FAE29 /* GenerateVariationUseCase.swift */,
47744778
269A2F46295CC683000828A8 /* GenerateVariationsSelectorCommand.swift */,
4779+
263C4CBF2963784900CA7E05 /* ProductVariationGenerator.swift */,
47754780
4515262B2577D48D0076B03C /* Add Attributes */,
47764781
AEDDDA0825CA9C0A0077F9B2 /* Edit Attributes */,
47774782
);
@@ -7817,6 +7822,7 @@
78177822
09C6A26027C01151001FAD73 /* Bulk Update */,
78187823
CCD2E68825DD52C100BD975D /* ProductVariationsViewModelTests.swift */,
78197824
26F65C9D25DEDE67008FAE29 /* GenerateVariationUseCaseTests.swift */,
7825+
267D6881296485850072ED0C /* ProductVariationGeneratorTests.swift */,
78207826
);
78217827
path = Variations;
78227828
sourceTree = "<group>";
@@ -10140,6 +10146,7 @@
1014010146
028FA466257E021100F88A48 /* RefundShippingLabelViewModel.swift in Sources */,
1014110147
CCC284112768C18500F6CC8B /* ProductInOrder.swift in Sources */,
1014210148
DE2FE5882925DD950018040A /* JetpackInstallHeaderView.swift in Sources */,
10149+
263C4CC02963784900CA7E05 /* ProductVariationGenerator.swift in Sources */,
1014310150
B59D49CD219B587E006BF0AD /* UILabel+OrderStatus.swift in Sources */,
1014410151
265BCA0C2430E741004E53EE /* ProductCategoryTableViewCell.swift in Sources */,
1014510152
02ACD25A2852E11700EC928E /* CloseAccountCoordinator.swift in Sources */,
@@ -11284,6 +11291,7 @@
1128411291
CE4DA5C821DD759400074607 /* CurrencyFormatterTests.swift in Sources */,
1128511292
DE61979528A25842005E4362 /* StorePickerViewModelTests.swift in Sources */,
1128611293
B57C745120F56EE900EEFC87 /* UITableViewCellHelpersTests.swift in Sources */,
11294+
267D6882296485850072ED0C /* ProductVariationGeneratorTests.swift in Sources */,
1128711295
0225C42824768A4C00C5B4F0 /* FilterProductListViewModelTests.swift in Sources */,
1128811296
D85136C9231E12B600DD0539 /* ReviewViewModelTests.swift in Sources */,
1128911297
57C5FF7C25091DE50074EC26 /* OrderListSyncActionUseCaseTests.swift in Sources */,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import XCTest
2+
@testable import WooCommerce
3+
@testable import Yosemite
4+
5+
final class ProductVariationGeneratorTests: XCTestCase {
6+
7+
func test_all_variations_are_generated_correctly() {
8+
// Given
9+
let product = Product.fake().copy(attributes: [
10+
ProductAttribute.fake().copy(attributeID: 1, name: "Size", options: ["S", "M"]),
11+
ProductAttribute.fake().copy(attributeID: 2, name: "Color", options: ["Red", "Green"]),
12+
ProductAttribute.fake().copy(attributeID: 3, name: "Fabric", options: ["Cotton", "Nylon"]),
13+
])
14+
15+
// When
16+
let variations = ProductVariationGenerator.generateVariations(for: product, excluding: [])
17+
18+
// Then
19+
XCTAssertEqual(variations, [
20+
CreateProductVariation(regularPrice: "", attributes: [
21+
.init(id: 1, name: "Size", option: "S"),
22+
.init(id: 2, name: "Color", option: "Red"),
23+
.init(id: 3, name: "Fabric", option: "Cotton")
24+
]),
25+
CreateProductVariation(regularPrice: "", attributes: [
26+
.init(id: 1, name: "Size", option: "S"),
27+
.init(id: 2, name: "Color", option: "Red"),
28+
.init(id: 3, name: "Fabric", option: "Nylon")
29+
]),
30+
CreateProductVariation(regularPrice: "", attributes: [
31+
.init(id: 1, name: "Size", option: "S"),
32+
.init(id: 2, name: "Color", option: "Green"),
33+
.init(id: 3, name: "Fabric", option: "Cotton")
34+
]),
35+
CreateProductVariation(regularPrice: "", attributes: [
36+
.init(id: 1, name: "Size", option: "S"),
37+
.init(id: 2, name: "Color", option: "Green"),
38+
.init(id: 3, name: "Fabric", option: "Nylon")
39+
]),
40+
CreateProductVariation(regularPrice: "", attributes: [
41+
.init(id: 1, name: "Size", option: "M"),
42+
.init(id: 2, name: "Color", option: "Red"),
43+
.init(id: 3, name: "Fabric", option: "Cotton")
44+
]),
45+
CreateProductVariation(regularPrice: "", attributes: [
46+
.init(id: 1, name: "Size", option: "M"),
47+
.init(id: 2, name: "Color", option: "Red"),
48+
.init(id: 3, name: "Fabric", option: "Nylon")
49+
]),
50+
CreateProductVariation(regularPrice: "", attributes: [
51+
.init(id: 1, name: "Size", option: "M"),
52+
.init(id: 2, name: "Color", option: "Green"),
53+
.init(id: 3, name: "Fabric", option: "Cotton")
54+
]),
55+
CreateProductVariation(regularPrice: "", attributes: [
56+
.init(id: 1, name: "Size", option: "M"),
57+
.init(id: 2, name: "Color", option: "Green"),
58+
.init(id: 3, name: "Fabric", option: "Nylon")
59+
]),
60+
])
61+
}
62+
63+
func test_existing_variations_are_excluded_correctly() {
64+
// Given
65+
let product = Product.fake().copy(attributes: [
66+
ProductAttribute.fake().copy(attributeID: 1, name: "Size", options: ["S", "M"]),
67+
ProductAttribute.fake().copy(attributeID: 2, name: "Color", options: ["Red", "Green"]),
68+
ProductAttribute.fake().copy(attributeID: 3, name: "Fabric", options: ["Cotton", "Nylon"]),
69+
])
70+
71+
let existingVariations = [
72+
ProductVariation.fake().copy(attributes: [
73+
.init(id: 1, name: "Size", option: "M"),
74+
.init(id: 2, name: "Color", option: "Green"),
75+
.init(id: 3, name: "Fabric", option: "Cotton"),
76+
]),
77+
ProductVariation.fake().copy(attributes: [
78+
.init(id: 1, name: "Size", option: "S"),
79+
.init(id: 2, name: "Color", option: "Red"),
80+
.init(id: 3, name: "Fabric", option: "Nylon"),
81+
])
82+
]
83+
84+
// When
85+
let variations = ProductVariationGenerator.generateVariations(for: product, excluding: existingVariations)
86+
87+
// Then
88+
XCTAssertEqual(variations, [
89+
CreateProductVariation(regularPrice: "", attributes: [
90+
.init(id: 1, name: "Size", option: "S"),
91+
.init(id: 2, name: "Color", option: "Red"),
92+
.init(id: 3, name: "Fabric", option: "Cotton")
93+
]),
94+
CreateProductVariation(regularPrice: "", attributes: [
95+
.init(id: 1, name: "Size", option: "S"),
96+
.init(id: 2, name: "Color", option: "Green"),
97+
.init(id: 3, name: "Fabric", option: "Cotton")
98+
]),
99+
CreateProductVariation(regularPrice: "", attributes: [
100+
.init(id: 1, name: "Size", option: "S"),
101+
.init(id: 2, name: "Color", option: "Green"),
102+
.init(id: 3, name: "Fabric", option: "Nylon")
103+
]),
104+
CreateProductVariation(regularPrice: "", attributes: [
105+
.init(id: 1, name: "Size", option: "M"),
106+
.init(id: 2, name: "Color", option: "Red"),
107+
.init(id: 3, name: "Fabric", option: "Cotton")
108+
]),
109+
CreateProductVariation(regularPrice: "", attributes: [
110+
.init(id: 1, name: "Size", option: "M"),
111+
.init(id: 2, name: "Color", option: "Red"),
112+
.init(id: 3, name: "Fabric", option: "Nylon")
113+
]),
114+
CreateProductVariation(regularPrice: "", attributes: [
115+
.init(id: 1, name: "Size", option: "M"),
116+
.init(id: 2, name: "Color", option: "Green"),
117+
.init(id: 3, name: "Fabric", option: "Nylon")
118+
]),
119+
])
120+
}
121+
}

0 commit comments

Comments
 (0)