From 8b9a2f682f65418484b068e820420c3e0ee2d27d Mon Sep 17 00:00:00 2001 From: nikunj Date: Mon, 11 Mar 2019 17:10:51 +0530 Subject: [PATCH 1/6] Multiple fixes for configurable products. Allow adding item to cart with multiple parents. Remove obsolete code related to stock rework. Update code to handle product tree, make it fast, efficient and less memory consuming. --- .../AcquiaCommerce/SKUType/Configurable.php | 369 +++++++++--------- 1 file changed, 183 insertions(+), 186 deletions(-) diff --git a/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php b/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php index b92cd9b..0021baf 100644 --- a/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php +++ b/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php @@ -31,7 +31,7 @@ public function addToCartForm(array $form, FormStateInterface $form_state, SKU $ return $form; } - $form_state->set('tree', $this->deriveProductTree($sku)); + $form_state->set('tree_sku', $sku->getSku()); $form['ajax'] = [ '#type' => 'container', @@ -44,15 +44,11 @@ public function addToCartForm(array $form, FormStateInterface $form_state, SKU $ '#tree' => TRUE, ]; - $configurables = unserialize($sku->field_configurable_attributes->getString()); + $configurables = self::getSortedConfigurableAttributes($sku); /** @var \Drupal\acm_sku\CartFormHelper $helper */ $helper = \Drupal::service('acm_sku.cart_form_helper'); - $configurable_weights = $helper->getConfigurableAttributeWeights( - $sku->get('attribute_set')->getString() - ); - foreach ($configurables as $configurable) { $attribute_code = $configurable['code']; @@ -64,22 +60,18 @@ public function addToCartForm(array $form, FormStateInterface $form_state, SKU $ // Sort the options. if (!empty($options)) { + $sorted_options = $options; // Sort config options before pushing them to the select list based on // the config. if ($helper->isAttributeSortable($attribute_code)) { $sorted_options = self::sortConfigOptions($options, $attribute_code); } - else { - // Use this in case the attribute is not sortable as per the config. - $sorted_options = $options; - } $form['ajax']['configurables'][$attribute_code] = [ '#type' => 'select', '#title' => $configurable['label'], '#options' => $sorted_options, - '#weight' => $configurable_weights[$attribute_code], '#required' => TRUE, '#ajax' => [ 'callback' => [get_class($this), 'configurableAjaxCallback'], @@ -203,22 +195,15 @@ public static function configurableAjaxCallback(array $form, FormStateInterface $dynamic_parts = &$form['ajax']; $configurables = $form_state->getValue('configurables'); + $sku = SKU::loadFromSku($form_state->get('tree_sku')); + $tree = self::deriveProductTree($sku); - /** @var \Drupal\acm_sku\CartFormHelper $helper */ - $helper = \Drupal::service('acm_sku.cart_form_helper'); - - $tree = $form_state->get('tree'); - $tree_pointer = &$tree['options']; - - foreach ($configurables as $key => $value) { - if (empty($value)) { - continue; - } + $configurable_codes = array_keys($tree['configurables']); + $combination = self::getSelectedCombination($configurables, $configurable_codes); - // Move the tree pointer if the selection is valid. - if (isset($tree_pointer["$key:$value"])) { - $tree_pointer = &$tree_pointer["$key:$value"]; - } + $tree_pointer = NULL; + if (!empty($tree['combinations']['by_attribute'][$combination])) { + $tree_pointer = SKU::loadFromSku($tree['combinations']['by_attribute'][$combination]); } if ($tree_pointer instanceof SKU) { @@ -232,10 +217,15 @@ public static function configurableAjaxCallback(array $form, FormStateInterface $dynamic_parts['add_to_cart'] = [ 'entity_render' => ['#markup' => render($view)], ]; + + $form_state->set('variant_sku', $tree_pointer->getSku()); } else { $available_config = $tree_pointer['#available_config']; + /** @var \Drupal\acq_sku\CartFormHelper $helper */ + $helper = \Drupal::service('acq_sku.cart_form_helper'); + foreach ($available_config as $key => $config) { $options = [ '' => $dynamic_parts['configurables']['color']['#options'][''], @@ -245,6 +235,9 @@ public static function configurableAjaxCallback(array $form, FormStateInterface $options[$value['value_id']] = $value['label']; } + // Use this in case the attribute is not sortable as per the config. + $sorted_options = $options; + // Sort config options before pushing them to the select list based on // the config. if ($helper->isAttributeSortable($key)) { @@ -254,10 +247,6 @@ public static function configurableAjaxCallback(array $form, FormStateInterface ]; $sorted_options += self::sortConfigOptions($options, $key); } - else { - // Use this in case the attribute is not sortable as per the config. - $sorted_options = $options; - } $dynamic_parts['configurables'][$key]['#options'] = $sorted_options; } @@ -272,22 +261,19 @@ public static function configurableAjaxCallback(array $form, FormStateInterface public function addToCartSubmit(array &$form, FormStateInterface $form_state) { $quantity = $form_state->getValue('quantity'); $configurables = $form_state->getValue('configurables'); - $tree = $form_state->get('tree'); - $tree_pointer = &$tree['options']; - foreach ($configurables as $key => $value) { - if (empty($value)) { - continue; - } + $sku = SKU::loadFromSku($form_state->get('tree_sku')); + $tree = self::deriveProductTree($sku); - // Move the tree pointer if the selection is valid. - if (isset($tree_pointer["$key:$value"])) { - $tree_pointer = &$tree_pointer["$key:$value"]; - } + $configurable_codes = array_keys($tree['configurables']); + $combination = self::getSelectedCombination($configurables, $configurable_codes); + + $tree_pointer = NULL; + if (!empty($tree['combinations']['by_attribute'][$combination])) { + $tree_pointer = SKU::loadFromSku($tree['combinations']['by_attribute'][$combination]); } if ($tree_pointer instanceof SKU) { - /* @var \Drupal\acm_cart\CartStorageInterface */ $cartStorage = \Drupal::service('acm_cart.cart_storage'); @@ -335,10 +321,17 @@ public function addToCartSubmit(array &$form, FormStateInterface $form_state) { [ '@quantity' => $quantity, '@name' => $label, - ] - )); - try { - $cartStorage->addRawItemToCart([ + ]) + ); + + // Check if item already in cart. + // @TODO: This needs to be fixed further to handle multiple parent + // products for a child SKU. To be done as part of CORE-7003. + if ($cart->hasItem($tree_pointer->getSku())) { + $cart->addItemToCart($tree_pointer->getSku(), $quantity); + } + else { + $cart->addRawItemToCart([ 'name' => $label, 'sku' => $tree['parent']->getSKU(), 'qty' => $quantity, @@ -346,9 +339,12 @@ public function addToCartSubmit(array &$form, FormStateInterface $form_state) { 'configurable_item_options' => $options, ], ]); + } + + $form_state->setTemporaryValue('child_sku', $tree_pointer->getSKU()); + try { // Add child SKU to form state to allow other modules to use it. - $form_state->setTemporaryValue('child_sku', $tree_pointer->getSKU()); $cartStorage->updateCart(); } catch (\Exception $e) { @@ -382,8 +378,8 @@ public function addToCartSubmit(array &$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function processImport(SKUInterface $configuredSkuEntity, array $product) { - $configuredSkuEntity->field_configurable_attributes->value = - serialize($product['extension']['configurable_product_options']); + $configuredSkuEntity->get('field_configurable_attributes') + ->setValue(serialize($product['extension']['configurable_product_options'])); $this->extractConfigurableOptions( $configuredSkuEntity->get('attribute_set')->getString(), @@ -412,7 +408,7 @@ public function processImport(SKUInterface $configuredSkuEntity, array $product) // (otherwise, much later, the product won't work on the front end). $simpleSkuValues[] = ['value' => $product['sku']]; - $price = (float) $simpleSkuEntity->price->first()->value; + $price = (float) $simpleSkuEntity->get('price')->getString(); if ($price < $min_price || $min_price === NULL) { $min_price = $price; @@ -445,7 +441,7 @@ public function processImport(SKUInterface $configuredSkuEntity, array $product) } } - $configuredSkuEntity->price->value = $price; + $configuredSkuEntity->get('price')->setValue($price); $configuredSkuEntity->get('field_configured_skus')->setValue($simpleSkuValues); if ($skippedAtLeastOneSimple) { @@ -456,7 +452,6 @@ public function processImport(SKUInterface $configuredSkuEntity, array $product) // Indicate this configurable was fully processed. return TRUE; } - } /** @@ -471,7 +466,7 @@ public function processImport(SKUInterface $configuredSkuEntity, array $product) * @return array * Configurables tree. */ - public function deriveProductTree(SKU $sku) { + public static function deriveProductTree(SKU $sku) { static $cache = []; if (isset($cache[$sku->language()->getId()], $cache[$sku->language()->getId()][$sku->id()])) { @@ -482,14 +477,14 @@ public function deriveProductTree(SKU $sku) { 'parent' => $sku, 'products' => self::getChildren($sku), 'combinations' => [], + 'configurables' => [], ]; - $configurables = unserialize( - $sku->get('field_configurable_attributes')->getString() - ); + $combinations =& $tree['combinations']; - $tree['configurables'] = []; - foreach ($configurables as $configurable) { + $configurables = self::getSortedConfigurableAttributes($sku); + + foreach ($configurables ?? [] as $configurable) { $tree['configurables'][$configurable['code']] = $configurable; } @@ -505,101 +500,84 @@ public function deriveProductTree(SKU $sku) { continue; } - $tree['combinations']['by_sku'][$sku_code][$code] = $value; + $combinations['by_sku'][$sku_code][$code] = $value; + $combinations['attribute_sku'][$code][$value][] = $sku_code; } } /** @var \Drupal\acm_sku\CartFormHelper $helper */ $helper = \Drupal::service('acm_sku.cart_form_helper'); - $configurable_weights = $helper->getConfigurableAttributeWeights( - $sku->get('attribute_set')->getString() - ); - - // Sort configurables based on the config. - uasort($tree['configurables'], function ($a, $b) use ($configurable_weights) { - return $configurable_weights[$a['code']] - $configurable_weights[$b['code']]; - }); - - $tree['options'] = Configurable::recursiveConfigurableTree( - $tree, - $tree['configurables'] - ); - - $cache[$sku->language()->getId()][$sku->id()] = $tree; - - return $tree; - } - - /** - * Creates subtrees based on available config. - * - * @param array $tree - * Tree of products. - * @param array $available_config - * Available configs. - * @param array $current_config - * Config of current product. - * - * @return array - * Subtree. - */ - public static function recursiveConfigurableTree(array &$tree, array $available_config, array $current_config = []) { - $subtree = ['#available_config' => $available_config]; - - foreach ($available_config as $id => $config) { - $subtree_available_config = $available_config; - unset($subtree_available_config[$id]); - - foreach ($config['values'] as $option) { - $value = $option['value_id']; - $subtree_current_config = array_merge($current_config, [$id => $value]); - - if (count($subtree_available_config) > 0) { - $subtree["$id:$value"] = Configurable::recursiveConfigurableTree( - $tree, - $subtree_available_config, - $subtree_current_config - ); + // Sort the values in attribute_sku so we can use it later. + foreach ($combinations['attribute_sku'] ?? [] as $code => $values) { + if ($helper->isAttributeSortable($code)) { + $combinations['attribute_sku'][$code] = Configurable::sortConfigOptions($values, $code); } else { - $subtree["$id:$value"] = Configurable::findProductInTreeWithConfig( - $tree, - $subtree_current_config - ); + // Sort from field_configurable_attributes. + $configurable_attribute = []; + foreach ($configurables as $configurable) { + if ($configurable['code'] === $code) { + $configurable_attribute = $configurable['values']; + break; + } + } + + if ($configurable_attribute) { + $configurable_attribute_weights = array_flip(array_column($configurable_attribute, 'value_id')); + uksort($combinations['attribute_sku'][$code], function ($a, $b) use ($configurable_attribute_weights) { + return $configurable_attribute_weights[$a] - $configurable_attribute_weights[$b]; + }); } } } - return $subtree; + // Prepare combinations array grouped by attributes to check later which + // combination is possible using isset(). + $combinations['by_attribute'] = []; + + foreach ($combinations['by_sku'] ?? [] as $sku_string => $combination) { + $combination_string = self::getSelectedCombination($combination); + $combinations['by_attribute'][$combination_string] = $sku_string; + } + + $cache[$sku->language()->getId()][$sku->id()] = $tree; + + return $cache[$sku->language()->getId()][$sku->id()]; } /** - * Finds product in tree base on config. + * Get combination for selected configurable values. * - * @param array $tree - * The whole configurable tree. - * @param array $config - * Config for the product. + * @param array $configurables + * Configurable values to build the combination string from. + * @param array $configurable_codes + * Codes to use for sorting the values array. * - * @return \Drupal\acm_sku\Entity\SKU - * Reference to SKU in existing tree. + * @return string + * Combination string. */ - public static function findProductInTreeWithConfig(array $tree, array $config) { - if (isset($tree['products'])) { - $attributes = []; - foreach ($config as $key => $value) { - $attributes[$key] = $value; + public static function getSelectedCombination(array $configurables, array $configurable_codes = []) { + if ($configurable_codes) { + $selected = []; + foreach ($configurable_codes as $code) { + if (isset($configurables[$code])) { + $selected[$code] = $configurables[$code]; } - - foreach ($tree['combinations']['by_sku'] ?? [] as $sku => $sku_attributes) { - if (count(array_intersect_assoc($sku_attributes, $attributes)) === count($sku_attributes)) { - return $tree['products'][$sku]; + } + $configurables = $selected; } + + $combination = ''; + + foreach ($configurables as $key => $value) { + if (empty($value)) { + continue; } + $combination .= $key . '|' . $value . '||'; } - return NULL; + return $combination; } /** @@ -682,61 +660,6 @@ public function cartName(SKU $sku, array $cart, $asString = FALSE) { return $cartName; } - /** - * Extract configurable options. - * - * Extract new configurable options during import and store them. - * - * @param string $attribute_set - * Attribute set. - * @param array $configurable_options - * Array with configurable options. - */ - protected function extractConfigurableOptions($attribute_set, array $configurable_options) { - /** @var \Drupal\acm_sku\CartFormHelper $helper */ - $helper = \Drupal::service('acm_sku.cart_form_helper'); - - // Load existing options. - $existing_options = $helper->getConfigurableAttributeWeights($attribute_set); - - // Transform incoming options. - foreach ($configurable_options as $configurable) { - $existing_options[$configurable['code']] = $configurable['position']; - } - - // Save options. - $helper->setConfigurableAttributeWeights($attribute_set, $existing_options); - } - - /** - * {@inheritdoc} - */ - public function getProcessedStock(SKU $sku, $reset = FALSE) { - $stock = &drupal_static('stock_static_cache', []); - - if (!$reset && isset($stock[$sku->getSku()])) { - return $stock[$sku->getSku()]; - } - - $quantities = []; - - foreach ($sku->get('field_configured_skus') as $child_sku) { - try { - $child_sku = $child_sku->getString(); - $child_stock = (int) $this->getStock($child_sku, $reset); - $quantities[$child_sku] = $child_stock; - } - catch (\Exception $e) { - // Child SKU might be deleted or translation not available. - // Log messages are already set in previous functions. - } - } - - $stock[$sku->getSku()] = empty($quantities) ? 0 : max($quantities); - - return $stock[$sku->getSku()]; - } - /** * Helper function to sort config options based on taxonomy term weight. * @@ -748,7 +671,7 @@ public function getProcessedStock(SKU $sku, $reset = FALSE) { * @return array * Array of options sorted based on term weight. */ - public static function sortConfigOptions(array &$options, $attribute_code) { + public static function sortConfigOptions(array $options, $attribute_code) { $sorted_options = []; $query = \Drupal::database()->select('taxonomy_term_field_data', 'ttfd'); @@ -763,11 +686,11 @@ public static function sortConfigOptions(array &$options, $attribute_code) { $query->orderBy('weight', 'ASC'); $tids = $query->execute()->fetchAllAssoc('tid'); - foreach ($tids as $tid => $values) { + foreach ($tids as $values) { $sorted_options[$values->field_sku_option_id_value] = $options[$values->field_sku_option_id_value]; } - return $sorted_options; + return $sorted_options ?: $options; } /** @@ -796,4 +719,78 @@ public static function getChildren(SKU $sku) { return $children; } + /** + * Extract configurable options. + * + * Extract new configurable options during import and store them. + * + * @param string $attribute_set + * Attribute set. + * @param array $configurable_options + * Array with configurable options. + */ + protected function extractConfigurableOptions($attribute_set, array $configurable_options) { + /** @var \Drupal\acm_sku\CartFormHelper $helper */ + $helper = \Drupal::service('acm_sku.cart_form_helper'); + + // Load existing options. + $existing_options = $helper->getConfigurableAttributeWeights($attribute_set); + + // Transform incoming options. + foreach ($configurable_options as $configurable) { + $existing_options[$configurable['code']] = $configurable['position']; + } + + // Save options. + $helper->setConfigurableAttributeWeights($attribute_set, $existing_options); + } + + /** + * Get sorted configurable options. + * + * @param \Drupal\acq_commerce\SKUInterface $sku + * SKU Entity. + * + * @return array + * Sorted configurable options. + */ + public static function getSortedConfigurableAttributes(SKUInterface $sku): array { + $static = &drupal_static(__FUNCTION__, []); + + $langcode = $sku->language()->getId(); + $sku_code = $sku->getSku(); + + // Do not process the same thing again and again. + if (isset($static[$langcode][$sku_code])) { + return $static[$langcode][$sku_code]; + } + + $configurables = unserialize($sku->get('field_configurable_attributes')->getString()); + + if (empty($configurables) || !is_array($configurables)) { + return []; + } + + /** @var \Drupal\acq_sku\CartFormHelper $helper */ + $helper = \Drupal::service('acm_sku.cart_form_helper'); + + $configurable_weights = $helper->getConfigurableAttributeWeights( + $sku->get('attribute_set')->getString() + ); + + // Sort configurables based on the config. + uasort($configurables, function ($a, $b) use ($configurable_weights) { + // We may keep getting new configurable options not defined in config. + // Use default values for them and keep their sequence as is. + // Still move the ones defined in our config as per weight in config. + $a = $configurable_weights[$a['code']] ?? -50; + $b = $configurable_weights[$b['code']] ?? 50; + return $a - $b; + }); + + $static[$langcode][$sku_code] = $configurables; + + return $configurables; + } + } From 99a5d1d0fa3463538aee65362ac70ba03175f63f Mon Sep 17 00:00:00 2001 From: nikunj Date: Mon, 11 Mar 2019 17:40:18 +0530 Subject: [PATCH 2/6] PHPCS fixes. --- .../AcquiaCommerce/SKUType/Configurable.php | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php b/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php index 0021baf..de99d7a 100644 --- a/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php +++ b/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php @@ -511,9 +511,10 @@ public static function deriveProductTree(SKU $sku) { // Sort the values in attribute_sku so we can use it later. foreach ($combinations['attribute_sku'] ?? [] as $code => $values) { if ($helper->isAttributeSortable($code)) { - $combinations['attribute_sku'][$code] = Configurable::sortConfigOptions($values, $code); - } - else { + $combinations['attribute_sku'][$code] + = Configurable::sortConfigOptions($values, $code); + } + else { // Sort from field_configurable_attributes. $configurable_attribute = []; foreach ($configurables as $configurable) { @@ -525,9 +526,12 @@ public static function deriveProductTree(SKU $sku) { if ($configurable_attribute) { $configurable_attribute_weights = array_flip(array_column($configurable_attribute, 'value_id')); - uksort($combinations['attribute_sku'][$code], function ($a, $b) use ($configurable_attribute_weights) { - return $configurable_attribute_weights[$a] - $configurable_attribute_weights[$b]; - }); + + uksort($combinations['attribute_sku'][$code], + function ($a, $b) use ($configurable_attribute_weights) { + return $configurable_attribute_weights[$a] + - $configurable_attribute_weights[$b]; + }); } } } @@ -563,10 +567,10 @@ public static function getSelectedCombination(array $configurables, array $confi foreach ($configurable_codes as $code) { if (isset($configurables[$code])) { $selected[$code] = $configurables[$code]; - } + } } $configurables = $selected; - } + } $combination = ''; @@ -745,7 +749,7 @@ protected function extractConfigurableOptions($attribute_set, array $configurabl $helper->setConfigurableAttributeWeights($attribute_set, $existing_options); } - /** + /** * Get sorted configurable options. * * @param \Drupal\acq_commerce\SKUInterface $sku From 774ba9f6f06f938a92b03fc68de7828b4a5fd2ed Mon Sep 17 00:00:00 2001 From: nikunj Date: Tue, 12 Mar 2019 14:17:14 +0530 Subject: [PATCH 3/6] Add hasItem function in Cart. --- modules/acm_cart/src/Cart.php | 14 ++++++++++++++ modules/acm_cart/src/CartInterface.php | 11 +++++++++++ 2 files changed, 25 insertions(+) diff --git a/modules/acm_cart/src/Cart.php b/modules/acm_cart/src/Cart.php index 4de1b73..a5e5bd3 100644 --- a/modules/acm_cart/src/Cart.php +++ b/modules/acm_cart/src/Cart.php @@ -652,4 +652,18 @@ public function get($property_name) { return NULL; } + /** + * {@inheritdoc} + */ + public function hasItem(string $sku) { + $items = $this->items(); + + if (empty($items)) { + return FALSE; + } + + $skus = array_column($items, 'sku'); + return in_array($sku, $skus); + } + } diff --git a/modules/acm_cart/src/CartInterface.php b/modules/acm_cart/src/CartInterface.php index 1b8c162..a55b378 100644 --- a/modules/acm_cart/src/CartInterface.php +++ b/modules/acm_cart/src/CartInterface.php @@ -327,4 +327,15 @@ public function setGuestCartEmail($email); */ public function get($property_name); + /** + * Check if Cart has an sku. + * + * @param string $sku + * SKU to check. + * + * @return bool + * TRUE if sku is already in cart. + */ + public function hasItem(string $sku); + } From 21197b9da03c0fd130f9bc9ece7ba29141bc80eb Mon Sep 17 00:00:00 2001 From: nikunj Date: Tue, 12 Mar 2019 14:22:14 +0530 Subject: [PATCH 4/6] Add hasItem function in CartStorage too. --- modules/acm_cart/src/CartStorage.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/acm_cart/src/CartStorage.php b/modules/acm_cart/src/CartStorage.php index 0195c2d..d23639c 100644 --- a/modules/acm_cart/src/CartStorage.php +++ b/modules/acm_cart/src/CartStorage.php @@ -871,4 +871,15 @@ public function clearPayment() { unset($this->cart->payment); } + /** + * {@inheritdoc} + */ + public function hasItem(string $sku) { + if ($this->isEmpty()) { + return FALSE; + } + + return $this->cart->hasItem($sku); + } + } From 91276ba4a889c86b6706687809b35961a0ca9fbd Mon Sep 17 00:00:00 2001 From: nikunj Date: Thu, 30 May 2019 22:07:03 +0530 Subject: [PATCH 5/6] More changes to fix some bugs and improve performance. Also make it more extendable. --- modules/acm_sku/acm_sku.api.php | 36 ++++++++++ .../AcquiaCommerce/SKUType/Configurable.php | 67 ++++++++++++++----- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/modules/acm_sku/acm_sku.api.php b/modules/acm_sku/acm_sku.api.php index d5bf7e1..96e4459 100644 --- a/modules/acm_sku/acm_sku.api.php +++ b/modules/acm_sku/acm_sku.api.php @@ -5,6 +5,7 @@ * Hooks specific to the acm_sku module. */ +use Drupal\acm_sku\Entity\SKU; use Drupal\node\NodeInterface; use Drupal\taxonomy\TermInterface; @@ -69,6 +70,41 @@ function hook_acm_sku_commerce_category_alter(TermInterface $term, array $catego } +/** + * Alter the children of configurable products. + * + * @param array $children + * Variants for the SKU. + * @param \Drupal\acm_sku\Entity\SKU $sku + * Parent sku which is being added to cart. + */ +function hook_acm_sku_configurable_variants_alter(array &$children, SKU $sku) { + +} + +/** + * Alter the configurations for configurable product. + * + * @param array $configurations + * Configurations available for configurable product. + * @param \Drupal\acm_sku\Entity\SKU $sku + * Parent sku which is being added to cart. + */ +function hook_acm_sku_configurable_product_configurations_alter(array &$configurations, SKU $sku) { + +} +/** + * Alter the options added to cart item. + * + * @param array $options + * Options to be added to cart item. + * @param \Drupal\acm_sku\Entity\SKU $sku + * Parent sku which is being added to cart. + */ +function hook_acm_sku_configurable_cart_options_alter(array &$options, SKU $sku) { + +} + /** * @} End of "addtogroup hooks". */ diff --git a/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php b/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php index de99d7a..f4c733e 100644 --- a/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php +++ b/modules/acm_sku/src/Plugin/AcquiaCommerce/SKUType/Configurable.php @@ -223,12 +223,12 @@ public static function configurableAjaxCallback(array $form, FormStateInterface else { $available_config = $tree_pointer['#available_config']; - /** @var \Drupal\acq_sku\CartFormHelper $helper */ - $helper = \Drupal::service('acq_sku.cart_form_helper'); + /** @var \Drupal\acm_sku\CartFormHelper $helper */ + $helper = \Drupal::service('acm_sku.cart_form_helper'); foreach ($available_config as $key => $config) { $options = [ - '' => $dynamic_parts['configurables']['color']['#options'][''], + '' => $dynamic_parts['configurables'][$key]['#options'][''], ]; foreach ($config['values'] as $value) { @@ -324,6 +324,9 @@ public function addToCartSubmit(array &$form, FormStateInterface $form_state) { ]) ); + // Allow other modules to update the options info sent to ACM. + \Drupal::moduleHandler()->alter('acm_sku_configurable_cart_options', $options, $sku); + // Check if item already in cart. // @TODO: This needs to be fixed further to handle multiple parent // products for a child SKU. To be done as part of CORE-7003. @@ -341,6 +344,7 @@ public function addToCartSubmit(array &$form, FormStateInterface $form_state) { ]); } + // Add child SKU to form state to allow other modules to use it. $form_state->setTemporaryValue('child_sku', $tree_pointer->getSKU()); try { @@ -497,6 +501,8 @@ public static function deriveProductTree(SKU $sku) { $value = $attributes[$code] ?? ''; if (empty($value)) { + // Ignore variants with empty value in configurable options. + unset($tree['products'][$sku_code]); continue; } @@ -538,8 +544,6 @@ function ($a, $b) use ($configurable_attribute_weights) { // Prepare combinations array grouped by attributes to check later which // combination is possible using isset(). - $combinations['by_attribute'] = []; - foreach ($combinations['by_sku'] ?? [] as $sku_string => $combination) { $combination_string = self::getSelectedCombination($combination); $combinations['by_attribute'][$combination_string] = $sku_string; @@ -707,19 +711,30 @@ public static function sortConfigOptions(array $options, $attribute_code) { * Full loaded child SKUs. */ public static function getChildren(SKU $sku) { - $children = []; + if ($sku->bundle() != 'configurable') { + return []; + } - foreach ($sku->get('field_configured_skus')->getValue() as $child) { - if (empty($child['value'])) { - continue; - } + $static = &drupal_static(__METHOD__, []); + $langcode = $sku->language()->getId(); + $sku_string = $sku->getSku(); + if (isset($static[$langcode][$sku_string])) { + return $static[$langcode][$sku_string]; + } - $child_sku = SKU::loadFromSku($child['value']); + $children = []; + + foreach (self::getChildSkus($sku) as $child) { + $child_sku = SKU::loadFromSku($child); if ($child_sku instanceof SKU) { - $children[$child_sku->getSKU()] = $child_sku; + $children[$child_sku->getSku()] = $child_sku; } } + // Allow other modules to add/remove variants. + \Drupal::moduleHandler()->alter('acm_sku_configurable_variants', $children, $sku); + + $static[$langcode][$sku_string] = $children; return $children; } @@ -775,6 +790,13 @@ public static function getSortedConfigurableAttributes(SKUInterface $sku): array return []; } + $configurations = []; + foreach ($configurables as $configuration) { + $configurations[$configuration['code']] = $configuration; + } + + \Drupal::moduleHandler()->alter('acm_sku_configurable_product_configurations', $configurations, $sku); + /** @var \Drupal\acq_sku\CartFormHelper $helper */ $helper = \Drupal::service('acm_sku.cart_form_helper'); @@ -782,8 +804,8 @@ public static function getSortedConfigurableAttributes(SKUInterface $sku): array $sku->get('attribute_set')->getString() ); - // Sort configurables based on the config. - uasort($configurables, function ($a, $b) use ($configurable_weights) { + // Sort configurations based on the config. + uasort($configurations, function ($a, $b) use ($configurable_weights) { // We may keep getting new configurable options not defined in config. // Use default values for them and keep their sequence as is. // Still move the ones defined in our config as per weight in config. @@ -792,9 +814,22 @@ public static function getSortedConfigurableAttributes(SKUInterface $sku): array return $a - $b; }); - $static[$langcode][$sku_code] = $configurables; + $static[$langcode][$sku_code] = $configurations; - return $configurables; + return $configurations; + } + + /** + * Wrapper function to get child skus as string array for configurable. + * + * @param \Drupal\acm_sku\Entity\SKU $sku + * Configurable SKU. + * + * @return array + * Child skus as string array. + */ + public static function getChildSkus(SKU $sku) { + return array_filter(array_map('trim', explode(',', $sku->get('field_configured_skus')->getString()))); } } From 151963bf7b3963eaa53f73bbdf3581e0783705c7 Mon Sep 17 00:00:00 2001 From: nikunj Date: Mon, 3 Jun 2019 12:12:40 +0530 Subject: [PATCH 6/6] Add tests for configurable products and make configurable products work. --- modules/acm/config/install/acm.connector.yml | 2 + modules/acm/config/schema/acm.schema.yml | 6 + modules/acm_sku/acm_sku.install | 34 ++ .../acm_sku.configurable_form_settings.yml | 8 +- .../acm_sku/config/schema/acm_sku.schema.yml | 18 +- modules/acm_sku/src/CartFormHelper.php | 35 +- modules/acm_sku/src/ProductManager.php | 13 - .../tests/data/configurable_products.data | 455 ++++++++++++++++++ .../src/Functional/ProductManagerTest.php | 53 ++ 9 files changed, 604 insertions(+), 20 deletions(-) create mode 100644 modules/acm_sku/tests/data/configurable_products.data diff --git a/modules/acm/config/install/acm.connector.yml b/modules/acm/config/install/acm.connector.yml index 6b1fe89..667cd36 100644 --- a/modules/acm/config/install/acm.connector.yml +++ b/modules/acm/config/install/acm.connector.yml @@ -8,3 +8,5 @@ product_publish: false api_version: v2 filter_root_category: false text_format: rich_text +delete_disabled_skus: true +product_title_use_sku: false diff --git a/modules/acm/config/schema/acm.schema.yml b/modules/acm/config/schema/acm.schema.yml index db601d2..a187018 100644 --- a/modules/acm/config/schema/acm.schema.yml +++ b/modules/acm/config/schema/acm.schema.yml @@ -99,6 +99,12 @@ acm.connector: category_field_name: type: string label: 'category_field_name' + delete_disabled_skus: + type: boolean + label: 'delete_disabled_skus' + product_title_use_sku: + type: boolean + label: 'product_title_use_sku' acm.session: type: config_object diff --git a/modules/acm_sku/acm_sku.install b/modules/acm_sku/acm_sku.install index a09f57e..800aa88 100644 --- a/modules/acm_sku/acm_sku.install +++ b/modules/acm_sku/acm_sku.install @@ -322,3 +322,37 @@ function acm_sku_update_8109() { $field->setTranslatable(FALSE); $field->save(); } + +/** + * Implements hook_update_N(). + * + * Re-structure attribute weights to make it schema compatible. + */ +function acm_sku_update_8110() { + $config = \Drupal::configFactory()->getEditable('acm_sku.configurable_form_settings'); + + $attribute_weights = []; + foreach ($config->get('attribute_weights') ?? [] as $attribute_set => $weights) { + $fields = []; + + foreach ($weights as $field => $weight) { + if (is_array($weight)) { + // Ignore corrupt data created because of buggy code. + continue; + } + + $fields[] = [ + 'field' => $field, + 'weight' => $weight, + ]; + } + + $attribute_weights[] = [ + 'attribute_set' => $attribute_set, + 'fields' => $fields, + ]; + } + + $config->set('attribute_weights', $attribute_weights); + $config->save(TRUE); +} diff --git a/modules/acm_sku/config/install/acm_sku.configurable_form_settings.yml b/modules/acm_sku/config/install/acm_sku.configurable_form_settings.yml index 23e0595..9e68d05 100644 --- a/modules/acm_sku/config/install/acm_sku.configurable_form_settings.yml +++ b/modules/acm_sku/config/install/acm_sku.configurable_form_settings.yml @@ -1,4 +1,8 @@ attribute_weights: - default: - size: 1 + - + attribute_set: default + fields: + - + field: 'size' + weight: 1 show_quantity: TRUE diff --git a/modules/acm_sku/config/schema/acm_sku.schema.yml b/modules/acm_sku/config/schema/acm_sku.schema.yml index a82b2b6..2664897 100644 --- a/modules/acm_sku/config/schema/acm_sku.schema.yml +++ b/modules/acm_sku/config/schema/acm_sku.schema.yml @@ -29,9 +29,21 @@ acm_sku.configurable_form_settings: sequence: type: mapping mapping: - size: - label: 'Size' - type: integer + attribute_set: + label: 'attribute_set' + type: string + fields: + label: 'fields' + type: sequence + sequence: + type: mapping + mapping: + field: + label: 'field' + type: 'string' + weight: + label: 'weight' + type: integer show_quantity: type: boolean label: 'show_quantity' diff --git a/modules/acm_sku/src/CartFormHelper.php b/modules/acm_sku/src/CartFormHelper.php index b2c6a57..238d464 100644 --- a/modules/acm_sku/src/CartFormHelper.php +++ b/modules/acm_sku/src/CartFormHelper.php @@ -50,7 +50,15 @@ public function __construct(ConfigFactoryInterface $config_factory) { public function getConfigurableAttributeWeights($attribute_set = 'default') { $attribute_set = strtolower($attribute_set); $weights = $this->config->get('attribute_weights'); - $set_weights = $weights[$attribute_set] ?? $weights['default']; + + $processed_weights = []; + foreach ($weights as $weight) { + foreach ($weight['fields'] ?? [] as $field) { + $processed_weights[$weight['attribute_set']][$field['field']] = $field['weight']; + } + } + + $set_weights = $processed_weights[$attribute_set] ?? $processed_weights['default']; asort($set_weights); return $set_weights; } @@ -66,9 +74,32 @@ public function getConfigurableAttributeWeights($attribute_set = 'default') { public function setConfigurableAttributeWeights($attribute_set = 'default', array $weights = []) { $attribute_set = strtolower($attribute_set); + $fields = []; + foreach ($weights as $field => $weight) { + $fields[] = [ + 'field' => $field, + 'weight' => $weight, + ]; + } + // Update weights for particular attribute set in config. $existing_weights = $this->config->get('attribute_weights'); - $existing_weights[$attribute_set] = $weights; + + $exists = FALSE; + foreach ($existing_weights as &$weight) { + if ($weight['attribute_set'] == $attribute_set) { + $weight['fields'] = $fields; + $exists = TRUE; + break; + } + } + + if (!$exists) { + $existing_weights[] = [ + 'attribute_set' => $attribute_set, + 'fields' => $fields, + ]; + } $config = $this->configFactory->getEditable(self::CONFIG_KEY); $config->set('attribute_weights', $existing_weights); diff --git a/modules/acm_sku/src/ProductManager.php b/modules/acm_sku/src/ProductManager.php index 37ae01c..d2546e5 100644 --- a/modules/acm_sku/src/ProductManager.php +++ b/modules/acm_sku/src/ProductManager.php @@ -422,19 +422,6 @@ public function synchronizeProducts(array $products = [], $storeId = '') { continue; } - $query = $this->entityManager->getStorage('acm_sku')->getQuery() - ->condition('sku', $product['sku']); - $sku_ids = $query->execute(); - - if (count($sku_ids) > 1) { - $this->failedSkus[] = $product['sku'] . '(Duplicate product SKU found.)'; - $this->failed++; - // Release the lock on this sku. - $lock->release($lock_key); - $lock_key = NULL; - continue; - } - $sku = $this->processSku($product, $langcode); if (is_null($sku)) { diff --git a/modules/acm_sku/tests/data/configurable_products.data b/modules/acm_sku/tests/data/configurable_products.data new file mode 100644 index 0000000..aabde22 --- /dev/null +++ b/modules/acm_sku/tests/data/configurable_products.data @@ -0,0 +1,455 @@ + 24, + 'store_id' => 1, + 'attribute_set_id' => 0, + 'attribute_set_label' => 'Default', + 'sku' => 'cf1', + 'name' => 'Configurable product', + 'type' => 'configurable', + 'price' => '1212.5', + 'special_price' => '', + 'final_price' => '1212.5', + 'regular_price' => '1250', + 'status' => 1, + 'visibility' => 1, + 'attributes' => + [ + 'description' => '

Long Description

', + 'short_description' => 'short desc', + ], + 'linked' => + [ + ], + 'categories' => + [ + 0 => '5', + ], + 'extension' => + [ + 'configurable_product_links' => + [ + 0 => + [ + 'id' => '27', + 'sku' => 'cf1-variant1', + ], + 1 => + [ + 'id' => '30', + 'sku' => 'cf1-variant2', + ], + 2 => + [ + 'id' => '33', + 'sku' => 'cf1-variant3', + ], + 3 => + [ + 'id' => '123', + 'sku' => 'cf1-variant4', + ], + ], + 'configurable_product_options' => + [ + 0 => + [ + 'attribute_id' => '282', + 'code' => 'color', + 'label' => 'Color', + 'position' => '1', + 'values' => + [ + 0 => + [ + 'label' => 'Neutral', + 'value_id' => '108', + ], + 1 => + [ + 'label' => 'Brown', + 'value_id' => '333', + ], + 2 => + [ + 'label' => 'Ivory', + 'value_id' => '336', + ], + 3 => + [ + 'label' => 'Alfresco Brown', + 'value_id' => '123', + ], + ], + ], + 1 => + [ + 'attribute_id' => '660', + 'code' => 'size', + 'label' => 'Size', + 'position' => '0', + 'values' => + [ + 0 => + [ + 'label' => '5 x 8 ft', + 'value_id' => '99', + ], + 1 => + [ + 'label' => '8 x 10 ft', + 'value_id' => '102', + ], + 2 => + [ + 'label' => '9 x 12 ft', + 'value_id' => '105', + ], + 3 => + [ + 'label' => '21.5 x 23 x 36.5"', + 'value_id' => '201', + ], + ], + ], + ], + 'labels' => + [ + ], + 'media' => + [ + 0 => + [ + ], + 1 => + [ + ], + ], + 'stock_item' => + [ + 'backorders' => 0, + 'enable_qty_increments' => FALSE, + 'is_decimal_divided' => FALSE, + 'is_in_stock' => FALSE, + 'is_qty_decimal' => FALSE, + 'item_id' => 10830, + 'low_stock_date' => NULL, + 'manage_stock' => TRUE, + 'max_sale_qty' => 10000, + 'min_qty' => 0, + 'min_sale_qty' => 1, + 'notify_stock_qty' => 1, + 'product_id' => 24, + 'qty' => 0, + 'qty_increments' => 0, + 'show_default_notification_message' => FALSE, + 'stock_id' => 1, + 'stock_status_changed_auto' => 1, + 'use_config_backorders' => TRUE, + 'use_config_enable_qty_inc' => TRUE, + 'use_config_manage_stock' => TRUE, + 'use_config_max_sale_qty' => TRUE, + 'use_config_min_qty' => TRUE, + 'use_config_min_sale_qty' => 1, + 'use_config_notify_stock_qty' => TRUE, + 'use_config_qty_increments' => TRUE, + ], + ], + ], + [ + 'product_id' => 27, + 'store_id' => 1, + 'attribute_set_id' => 0, + 'attribute_set_label' => 'Default', + 'sku' => 'cf1-variant1', + 'name' => 'Configurable product, 5x8, Neutral', + 'type' => 'simple', + 'price' => '2390.0000', + 'special_price' => '', + 'final_price' => '2390', + 'regular_price' => '2390', + 'status' => 1, + 'visibility' => 2, + 'attributes' => + [ + 'color' => '108', + 'description' => '

Long Description

', + 'short_description' => 'short desc', + 'size' => '99', + 'sku_name' => 'Configurable product, 5x8, Neutral', + ], + 'linked' => + [ + ], + 'categories' => + [ + ], + 'extension' => + [ + 'labels' => + [ + ], + 'media' => + [ + 0 => + [ + ], + 1 => + [ + ], + ], + 'stock_item' => + [ + 'backorders' => 0, + 'enable_qty_increments' => FALSE, + 'is_decimal_divided' => FALSE, + 'is_in_stock' => TRUE, + 'is_qty_decimal' => FALSE, + 'item_id' => 10833, + 'low_stock_date' => NULL, + 'manage_stock' => TRUE, + 'max_sale_qty' => 10000, + 'min_qty' => 0, + 'min_sale_qty' => 1, + 'notify_stock_qty' => 1, + 'product_id' => 27, + 'qty' => 5000, + 'qty_increments' => 0, + 'show_default_notification_message' => FALSE, + 'stock_id' => 1, + 'stock_status_changed_auto' => 0, + 'use_config_backorders' => TRUE, + 'use_config_enable_qty_inc' => TRUE, + 'use_config_manage_stock' => TRUE, + 'use_config_max_sale_qty' => TRUE, + 'use_config_min_qty' => TRUE, + 'use_config_min_sale_qty' => 1, + 'use_config_notify_stock_qty' => TRUE, + 'use_config_qty_increments' => TRUE, + ], + ], + ], + [ + 'product_id' => 30, + 'store_id' => 1, + 'attribute_set_id' => 0, + 'attribute_set_label' => 'Default', + 'sku' => 'cf1-variant2', + 'name' => 'Configurable product, 8x10, Neutral', + 'type' => 'simple', + 'price' => '4190.0000', + 'special_price' => '4000.0000', + 'final_price' => '4000', + 'regular_price' => '4190', + 'status' => 1, + 'visibility' => 2, + 'attributes' => + [ + 'color' => '333', + 'description' => '

Long description

', + 'short_description' => 'short desc', + 'size' => '102', + 'sku_name' => 'Configurable product, 8x10, Neutral', + ], + 'linked' => + [ + ], + 'categories' => + [ + ], + 'extension' => + [ + 'labels' => + [ + ], + 'media' => + [ + 0 => + [ + ], + 1 => + [ + ], + ], + 'stock_item' => + [ + 'backorders' => 0, + 'enable_qty_increments' => FALSE, + 'is_decimal_divided' => FALSE, + 'is_in_stock' => TRUE, + 'is_qty_decimal' => FALSE, + 'item_id' => 10836, + 'low_stock_date' => NULL, + 'manage_stock' => TRUE, + 'max_sale_qty' => 10000, + 'min_qty' => 0, + 'min_sale_qty' => 1, + 'notify_stock_qty' => 1, + 'product_id' => 30, + 'qty' => 5000, + 'qty_increments' => 0, + 'show_default_notification_message' => FALSE, + 'stock_id' => 1, + 'stock_status_changed_auto' => 0, + 'use_config_backorders' => TRUE, + 'use_config_enable_qty_inc' => TRUE, + 'use_config_manage_stock' => TRUE, + 'use_config_max_sale_qty' => TRUE, + 'use_config_min_qty' => TRUE, + 'use_config_min_sale_qty' => 1, + 'use_config_notify_stock_qty' => TRUE, + 'use_config_qty_increments' => TRUE, + ], + ], + ], + [ + 'product_id' => 33, + 'store_id' => 1, + 'attribute_set_id' => 0, + 'attribute_set_label' => 'Default', + 'sku' => 'cf1-variant3', + 'name' => 'Configurable product, 9x12, Neutral', + 'type' => 'simple', + 'price' => '5080.0000', + 'special_price' => '', + 'final_price' => '4927.6', + 'regular_price' => '5080', + 'status' => 1, + 'visibility' => 2, + 'attributes' => + [ + 'color' => '336', + 'description' => '

Long Description

', + 'short_description' => 'short desc', + 'size' => '105', + 'sku_name' => 'Configurable product, 9x12, Neutral', + ], + 'linked' => + [ + ], + 'categories' => + [ + ], + 'extension' => + [ + 'labels' => + [ + ], + 'media' => + [ + 0 => + [ + ], + 1 => + [ + ], + ], + 'stock_item' => + [ + 'backorders' => 0, + 'enable_qty_increments' => FALSE, + 'is_decimal_divided' => FALSE, + 'is_in_stock' => TRUE, + 'is_qty_decimal' => FALSE, + 'item_id' => 10839, + 'low_stock_date' => NULL, + 'manage_stock' => TRUE, + 'max_sale_qty' => 10000, + 'min_qty' => 0, + 'min_sale_qty' => 1, + 'notify_stock_qty' => 1, + 'product_id' => 33, + 'qty' => 5000, + 'qty_increments' => 0, + 'show_default_notification_message' => FALSE, + 'stock_id' => 1, + 'stock_status_changed_auto' => 0, + 'use_config_backorders' => TRUE, + 'use_config_enable_qty_inc' => TRUE, + 'use_config_manage_stock' => TRUE, + 'use_config_max_sale_qty' => TRUE, + 'use_config_min_qty' => TRUE, + 'use_config_min_sale_qty' => 1, + 'use_config_notify_stock_qty' => TRUE, + 'use_config_qty_increments' => TRUE, + ], + ], + ], + [ + 'product_id' => 123, + 'store_id' => 1, + 'attribute_set_id' => 0, + 'attribute_set_label' => 'Default', + 'sku' => 'cf1-variant4', + 'name' => 'Configurable Product, Alfresco Brown', + 'type' => 'simple', + 'price' => '1250.0000', + 'special_price' => '', + 'final_price' => '1212.5', + 'regular_price' => '1250', + 'status' => 1, + 'visibility' => 2, + 'attributes' => + [ + 'color' => '123', + 'size' => '201', + 'sku_name' => 'Configurable Product, Alfresco Brown', + 'description' => '

Long Description

', + 'short_description' => 'short desc', + ], + 'linked' => + [ + ], + 'categories' => + [ + ], + 'extension' => + [ + 'labels' => + [ + ], + 'media' => + [ + 0 => + [ + ], + 1 => + [ + ], + ], + 'stock_item' => + [ + 'backorders' => 0, + 'enable_qty_increments' => FALSE, + 'is_decimal_divided' => FALSE, + 'is_in_stock' => TRUE, + 'is_qty_decimal' => FALSE, + 'item_id' => 10881, + 'low_stock_date' => NULL, + 'manage_stock' => TRUE, + 'max_sale_qty' => 10000, + 'min_qty' => 0, + 'min_sale_qty' => 1, + 'notify_stock_qty' => 1, + 'product_id' => 123, + 'qty' => 5990, + 'qty_increments' => 0, + 'show_default_notification_message' => FALSE, + 'stock_id' => 1, + 'stock_status_changed_auto' => 1, + 'use_config_backorders' => TRUE, + 'use_config_enable_qty_inc' => TRUE, + 'use_config_manage_stock' => TRUE, + 'use_config_max_sale_qty' => TRUE, + 'use_config_min_qty' => TRUE, + 'use_config_min_sale_qty' => 1, + 'use_config_notify_stock_qty' => TRUE, + 'use_config_qty_increments' => TRUE, + ], + ], + ], +]; diff --git a/modules/acm_sku/tests/src/Functional/ProductManagerTest.php b/modules/acm_sku/tests/src/Functional/ProductManagerTest.php index 2fff869..5acc07d 100644 --- a/modules/acm_sku/tests/src/Functional/ProductManagerTest.php +++ b/modules/acm_sku/tests/src/Functional/ProductManagerTest.php @@ -43,14 +43,20 @@ class ProductManagerTest extends BrowserTestBase { */ public function setUp() { parent::setUp(); + Term::create([ 'vid' => 'acm_product_category', 'name' => 'Cat1', 'middleware_id' => 1, + ], [ + 'vid' => 'acm_product_category', + 'name' => 'Cat2', + 'middleware_id' => 5, ])->save(); // Load categories data. module_load_include('data', 'acm_sku', 'tests/data/products'); + module_load_include('data', 'acm_sku', 'tests/data/configurable_products'); } /** @@ -178,4 +184,51 @@ public function testSynchronizeSimpleProductsWithMoreData() { $sku = NULL; } + /** + * Tests the synchronizeProducts method with configurable product data passed. + * + * @covers ::synchronizeProducts + */ + public function testSynchronizeConfigurableProducts() { + global $_acm_commerce_configurable_products; + + $result = $this->container + ->get('acm_sku.product_manager') + ->synchronizeProducts($_acm_commerce_configurable_products); + + $this->assertSame($result['success'], TRUE); + + $title = 'Configurable product'; + $nodes = $this->container->get('entity_type.manager') + ->getStorage('node') + ->loadByProperties(['title' => $title]); + // Fetch the first element of the array $nodes. + /** @var \Drupal\node\NodeInterface $node */ + $node = reset($nodes); + $this->assertNotNull($node); + $this->assertSame($node->label(), $title); + $this->assertSame($node->get('field_skus')->getString(), 'cf1'); + $nodes = NULL; + $node = NULL; + + $validate_skus = [ + 'cf1', + 'cf1-variant1', + 'cf1-variant2', + 'cf1-variant3', + 'cf1-variant4', + ]; + + $skuStorage = $this->container->get('entity_type.manager')->getStorage('acm_sku'); + foreach ($validate_skus as $validate_sku) { + $skus = $skuStorage->loadByProperties(['sku' => $validate_sku]); + // Fetch the first element of the array $skus. + $sku = reset($skus); + $this->assertNotNull($sku); + $this->assertSame($sku->get('sku')->getString(), $validate_sku); + $skus = NULL; + $sku = NULL; + } + } + }