From 5be879dfbbefd127231ee3684ca17d567d87b6c4 Mon Sep 17 00:00:00 2001 From: czigor Date: Fri, 24 Nov 2017 15:20:25 +0100 Subject: [PATCH 001/103] Issue #2923656 by czigor, kiwimind, opdavies, utement: Unable to use cart with new configuration --- modules/order/commerce_order.module | 2 +- modules/order/tests/src/Functional/OrderTypeTest.php | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/order/commerce_order.module b/modules/order/commerce_order.module index 0b47e43cdd..a901cf1403 100644 --- a/modules/order/commerce_order.module +++ b/modules/order/commerce_order.module @@ -140,7 +140,7 @@ function commerce_order_add_order_items_field(OrderTypeInterface $order_type) { ->setLabel('Order items') ->setCardinality(BundleFieldDefinition::CARDINALITY_UNLIMITED) ->setRequired(TRUE) - ->setSetting('target_type', 'commerce_order_type') + ->setSetting('target_type', 'commerce_order_item') ->setSetting('handler', 'default') ->setDisplayOptions('form', [ 'type' => 'inline_entity_form_complex', diff --git a/modules/order/tests/src/Functional/OrderTypeTest.php b/modules/order/tests/src/Functional/OrderTypeTest.php index e2e24cb717..05eb78e7e4 100644 --- a/modules/order/tests/src/Functional/OrderTypeTest.php +++ b/modules/order/tests/src/Functional/OrderTypeTest.php @@ -26,6 +26,10 @@ public function testDefaultOrderType() { * Tests creating an order Type programaticaly and through the add form. */ public function testCreateOrderType() { + // Remove the default order type to be able to test creating the + // order_items field anew. + OrderType::load('default')->delete(); + // Create an order type programmaticaly. $type = $this->createEntity('commerce_order_type', [ 'id' => 'kitten', @@ -48,6 +52,10 @@ public function testCreateOrderType() { $type_exists = (bool) OrderType::load($values['id']); $this->assertNotEmpty($type_exists, 'The new order type has been created in the database.'); + + // Testing the target type of the order_items field. + $settings = $this->config('field.storage.commerce_order.order_items')->get('settings'); + $this->assertEquals('commerce_order_item', $settings['target_type'], t('Order item field target type is correct.')); } /** @@ -69,7 +77,7 @@ public function testDraftOrderRefreshSettings() { } /** - * Tests deleting an order Type programmaticaly and through the form. + * Tests deleting an order Type through the form. */ public function testDeleteOrderType() { // Create an order type programmaticaly. From 0472e91c674fdd764cf0e2a8f6da29a9d08ed6ee Mon Sep 17 00:00:00 2001 From: ptmkenny Date: Fri, 24 Nov 2017 16:01:29 +0100 Subject: [PATCH 002/103] Issue #2925745 by ptmkenny: Commerce Payment credit card errors cannot be translated --- modules/payment/src/PluginForm/PaymentMethodAddForm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php index 0a3ece9eba..ffefe78090 100644 --- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php +++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php @@ -121,11 +121,11 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s } catch (DeclineException $e) { \Drupal::logger('commerce_payment')->warning($e->getMessage()); - throw new DeclineException('We encountered an error processing your payment method. Please verify your details and try again.'); + throw new DeclineException(t('We encountered an error processing your payment method. Please verify your details and try again.')); } catch (PaymentGatewayException $e) { \Drupal::logger('commerce_payment')->error($e->getMessage()); - throw new PaymentGatewayException('We encountered an unexpected error processing your payment method. Please try again later.'); + throw new PaymentGatewayException(t('We encountered an unexpected error processing your payment method. Please try again later.')); } } From 080e26fcc8a1ed0742e4e46abc5bdd04bbc0cc42 Mon Sep 17 00:00:00 2001 From: alexpott Date: Tue, 28 Nov 2017 14:17:08 +0100 Subject: [PATCH 003/103] Issue #2926518 by alexpott: AdjustmentItemList::getAdjustments() can return NULL items --- .../Field/FieldType/AdjustmentItemList.php | 4 +++- .../commerce_order_test.module | 22 +++++++++++++++++++ .../tests/src/Functional/OrderAdminTest.php | 4 ++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 modules/order/tests/modules/commerce_order_test/commerce_order_test.module diff --git a/modules/order/src/Plugin/Field/FieldType/AdjustmentItemList.php b/modules/order/src/Plugin/Field/FieldType/AdjustmentItemList.php index 69c03d387e..cb33856da0 100644 --- a/modules/order/src/Plugin/Field/FieldType/AdjustmentItemList.php +++ b/modules/order/src/Plugin/Field/FieldType/AdjustmentItemList.php @@ -17,7 +17,9 @@ public function getAdjustments() { $adjustments = []; /** @var \Drupal\commerce_order\Plugin\Field\FieldType\AdjustmentItem $field_item */ foreach ($this->list as $key => $field_item) { - $adjustments[$key] = $field_item->value; + if (!$field_item->isEmpty()) { + $adjustments[$key] = $field_item->value; + } } return $adjustments; diff --git a/modules/order/tests/modules/commerce_order_test/commerce_order_test.module b/modules/order/tests/modules/commerce_order_test/commerce_order_test.module new file mode 100644 index 0000000000..e1dfe1e194 --- /dev/null +++ b/modules/order/tests/modules/commerce_order_test/commerce_order_test.module @@ -0,0 +1,22 @@ +getFormObject()->getEntity(); + // This will error if getAdjustments() returns invalid items. + $order->recalculateTotalPrice(); + \Drupal::state()->set("commerce_order_test_field_widget_form_alter", $order->getAdjustments()); + } +} diff --git a/modules/order/tests/src/Functional/OrderAdminTest.php b/modules/order/tests/src/Functional/OrderAdminTest.php index 4b9451e729..b5dccc49af 100644 --- a/modules/order/tests/src/Functional/OrderAdminTest.php +++ b/modules/order/tests/src/Functional/OrderAdminTest.php @@ -62,6 +62,10 @@ public function testCreateOrder() { $this->assertSession()->buttonExists('Create order item'); $entity = $this->variation->getSku() . ' (' . $this->variation->id() . ')'; + // Test that commerce_order_test_field_widget_form_alter() has the expected + // outcome. + $this->assertSame([], \Drupal::state()->get("commerce_order_test_field_widget_form_alter")); + $checkbox = $this->getSession()->getPage()->findField('Override the unit price'); if ($checkbox) { $checkbox->check(); From eca0e04420ecdcd8d3cbf762bb4e38f185814e7b Mon Sep 17 00:00:00 2001 From: alexpott Date: Tue, 28 Nov 2017 14:19:05 +0100 Subject: [PATCH 004/103] Issue #2924930 by alexpott: Support #title_display on commerce_price element --- modules/price/src/Element/Price.php | 1 + .../commerce_price_test/src/Form/PriceTestForm.php | 8 ++++++++ modules/price/tests/src/Functional/PriceElementTest.php | 7 +++++++ 3 files changed, 16 insertions(+) diff --git a/modules/price/src/Element/Price.php b/modules/price/src/Element/Price.php index b3b746b8e2..5e7470c09a 100644 --- a/modules/price/src/Element/Price.php +++ b/modules/price/src/Element/Price.php @@ -103,6 +103,7 @@ public static function processElement(array $element, FormStateInterface $form_s $element['number'] = [ '#type' => 'commerce_number', '#title' => $element['#title'], + '#title_display' => $element['#title_display'], '#default_value' => $default_value ? $default_value['number'] : NULL, '#required' => $element['#required'], '#size' => $element['#size'], diff --git a/modules/price/tests/modules/commerce_price_test/src/Form/PriceTestForm.php b/modules/price/tests/modules/commerce_price_test/src/Form/PriceTestForm.php index 16f4048dfa..c08f34405a 100644 --- a/modules/price/tests/modules/commerce_price_test/src/Form/PriceTestForm.php +++ b/modules/price/tests/modules/commerce_price_test/src/Form/PriceTestForm.php @@ -26,6 +26,14 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#required' => TRUE, '#available_currencies' => ['USD', 'EUR'], ]; + $form['amount_hidden_title'] = [ + '#type' => 'commerce_price', + '#title' => $this->t('Hidden title amount'), + '#title_display' => 'invisible', + '#default_value' => ['number' => '99.99', 'currency_code' => 'USD'], + '#required' => TRUE, + '#available_currencies' => ['USD', 'EUR'], + ]; $form['submit'] = [ '#type' => 'submit', '#value' => $this->t('Submit'), diff --git a/modules/price/tests/src/Functional/PriceElementTest.php b/modules/price/tests/src/Functional/PriceElementTest.php index 2d536c9ab2..abc0c0f5ac 100644 --- a/modules/price/tests/src/Functional/PriceElementTest.php +++ b/modules/price/tests/src/Functional/PriceElementTest.php @@ -43,6 +43,13 @@ public function testSingleCurrency() { ]; $this->submitForm($edit, 'Submit'); $this->assertSession()->pageTextContains('The number is "10.99" and the currency code is "USD".'); + + // Ensure that the form titles are displayed as expected. + $elements = $this->xpath('//input[@id="edit-amount-hidden-title-number"]/preceding-sibling::label[@for="edit-amount-hidden-title-number" and contains(@class, "visually-hidden")]'); + $this->assertTrue(isset($elements[0]), 'Label preceding field and label class is visually-hidden.'); + + $elements = $this->xpath('//input[@id="edit-amount-number"]/preceding-sibling::label[@for="edit-amount-number" and not(contains(@class, "visually-hidden"))]'); + $this->assertTrue(isset($elements[0]), 'Label preceding field and label class is not visually visually-hidden.'); } /** From d5b83abc7fd260ec103f1f6f2fc6c6f889b00c09 Mon Sep 17 00:00:00 2001 From: alexpott Date: Tue, 28 Nov 2017 14:26:30 +0100 Subject: [PATCH 005/103] Issue #2926563 by alexpott: Adjustment label is required but not on the form resulting PHP error --- .../Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php | 7 +++++++ modules/order/tests/src/Functional/OrderAdminTest.php | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php b/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php index e4c11ad465..64b6d910d3 100644 --- a/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php +++ b/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php @@ -72,6 +72,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#type' => 'textfield', '#title' => $this->t('Label'), '#default_value' => ($adjustment) ? $adjustment->getLabel() : '', + '#required' => TRUE, ]; $element['definition']['amount'] = [ '#type' => 'commerce_price', @@ -101,6 +102,12 @@ public function massageFormValues(array $values, array $form, FormStateInterface if ($value['type'] == '_none') { continue; } + // The method can be called with invalid or incomplete data, in + // preparation for validation. Passing such data to the Adjustment + // object would result in an exception. + if (empty($value['definition']['label'])) { + continue; + } $values[$key] = new Adjustment([ 'type' => $value['type'], diff --git a/modules/order/tests/src/Functional/OrderAdminTest.php b/modules/order/tests/src/Functional/OrderAdminTest.php index b5dccc49af..350ae8930e 100644 --- a/modules/order/tests/src/Functional/OrderAdminTest.php +++ b/modules/order/tests/src/Functional/OrderAdminTest.php @@ -99,10 +99,13 @@ public function testCreateOrder() { 'billing_profile[0][profile][address][0][address][locality]' => 'Mountain View', 'billing_profile[0][profile][address][0][address][administrative_area]' => 'CA', 'adjustments[0][type]' => 'custom', - 'adjustments[0][definition][label]' => 'Test fee', + 'adjustments[0][definition][label]' => '', 'adjustments[0][definition][amount][number]' => '2.00', ]; $this->submitForm($edit, 'Save'); + $this->assertSession()->pageTextContains('Label field is required.'); + $edit['adjustments[0][definition][label]'] = 'Test fee'; + $this->submitForm($edit, 'Save'); $this->drupalGet('/admin/commerce/orders'); $order_number = $this->getSession()->getPage()->find('css', 'tr td.views-field-order-number'); $this->assertEquals(1, count($order_number), 'Order exists in the table.'); From 4d9b905408a5b3e80aa0da0bc99a9f1d714f78a2 Mon Sep 17 00:00:00 2001 From: alexpott Date: Tue, 28 Nov 2017 15:07:18 +0100 Subject: [PATCH 006/103] Issue #2923388 by alexpott: Add #ajax support to the commerce_price form element --- modules/price/src/Element/Number.php | 4 + modules/price/src/Element/Price.php | 12 ++- .../commerce_price_test.routing.yml | 8 ++ .../src/Form/AjaxPriceTestForm.php | 81 +++++++++++++++++++ .../AjaxPriceElementTest.php | 50 ++++++++++++ 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 modules/price/tests/modules/commerce_price_test/src/Form/AjaxPriceTestForm.php create mode 100644 modules/price/tests/src/FunctionalJavascript/AjaxPriceElementTest.php diff --git a/modules/price/src/Element/Number.php b/modules/price/src/Element/Number.php index 6bc391d687..e1cdc54309 100644 --- a/modules/price/src/Element/Number.php +++ b/modules/price/src/Element/Number.php @@ -95,6 +95,10 @@ public static function valueCallback(&$element, $input, FormStateInterface $form * The built commerce_number form element. */ public static function processElement(array $element, FormStateInterface $form_state, array &$complete_form) { + // Add a sensible default AJAX event. + if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) { + $element['#ajax']['event'] = 'blur'; + } // Provide an example to the end user so that they know which decimal // separator to use. This is the same pattern Drupal core uses. $number_formatter = self::getNumberFormatter($element); diff --git a/modules/price/src/Element/Price.php b/modules/price/src/Element/Price.php index 5e7470c09a..2088971bbb 100644 --- a/modules/price/src/Element/Price.php +++ b/modules/price/src/Element/Price.php @@ -113,8 +113,9 @@ public static function processElement(array $element, FormStateInterface $form_s '#max_fraction_digits' => 6, '#min' => $element['#allow_negative'] ? NULL : 0, ]; - unset($element['#size']); - unset($element['#maxlength']); + if (isset($element['#ajax'])) { + $element['number']['#ajax'] = $element['#ajax']; + } if (count($currency_codes) == 1) { $last_visible_element = 'number'; @@ -135,11 +136,18 @@ public static function processElement(array $element, FormStateInterface $form_s '#title_display' => 'invisible', '#field_suffix' => '', ]; + if (isset($element['#ajax'])) { + $element['currency_code']['#ajax'] = $element['#ajax']; + } } // Add the help text if specified. if (!empty($element['#description'])) { $element[$last_visible_element]['#field_suffix'] .= '
' . $element['#description'] . '
'; } + // Remove the keys that were transfered to child elements. + unset($element['#size']); + unset($element['#maxlength']); + unset($element['#ajax']); return $element; } diff --git a/modules/price/tests/modules/commerce_price_test/commerce_price_test.routing.yml b/modules/price/tests/modules/commerce_price_test/commerce_price_test.routing.yml index 80daa11579..af065f86d2 100644 --- a/modules/price/tests/modules/commerce_price_test/commerce_price_test.routing.yml +++ b/modules/price/tests/modules/commerce_price_test/commerce_price_test.routing.yml @@ -13,3 +13,11 @@ commerce_price_test.price_test_form: _title: 'Price test form' requirements: _access: 'TRUE' + +commerce_price_test.ajax_price_test_form: + path: '/commerce_price_test/ajax_price_test_form' + defaults: + _form: '\Drupal\commerce_price_test\Form\AjaxPriceTestForm' + _title: 'AJAX price test form' + requirements: + _access: 'TRUE' diff --git a/modules/price/tests/modules/commerce_price_test/src/Form/AjaxPriceTestForm.php b/modules/price/tests/modules/commerce_price_test/src/Form/AjaxPriceTestForm.php new file mode 100644 index 0000000000..cd85aa792f --- /dev/null +++ b/modules/price/tests/modules/commerce_price_test/src/Form/AjaxPriceTestForm.php @@ -0,0 +1,81 @@ + 'commerce_price', + '#title' => $this->t('Amount'), + '#default_value' => ['number' => '99.99', 'currency_code' => 'USD'], + '#required' => TRUE, + '#available_currencies' => ['USD', 'EUR'], + '#ajax' => [ + 'callback' => [$this, 'ajaxSubmit'], + 'wrapper' => 'ajax-replace', + ], + ]; + $form['ajax_info'] = [ + '#prefix' => '
', + '#markup' => 'waiting', + '#suffix' => '
', + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Create a Price to ensure the values are valid. + $value = $form_state->getValue('amount'); + $price = new Price($value['number'], $value['currency_code']); + drupal_set_message(t('The number is "@number" and the currency code is "@currency_code".', [ + '@number' => $price->getNumber(), + '@currency_code' => $price->getCurrencyCode(), + ])); + } + + /** + * An AJAX callback for the form. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * Some markup. + */ + public function ajaxSubmit(array $form, FormStateInterface $form_state) { + return [ + '#prefix' => '
', + '#markup' => 'AJAX successful: ' . $form_state->getTriggeringElement()['#name'], + '#suffix' => '
', + ]; + } + +} diff --git a/modules/price/tests/src/FunctionalJavascript/AjaxPriceElementTest.php b/modules/price/tests/src/FunctionalJavascript/AjaxPriceElementTest.php new file mode 100644 index 0000000000..f7396e0a41 --- /dev/null +++ b/modules/price/tests/src/FunctionalJavascript/AjaxPriceElementTest.php @@ -0,0 +1,50 @@ +container->get('commerce_price.currency_importer')->import('EUR'); + $this->drupalGet('/commerce_price_test/ajax_price_test_form'); + $this->assertSession()->fieldExists('amount[number]'); + + // Default value. + $this->assertSession()->fieldValueEquals('amount[number]', '99.99'); + $this->assertSession()->pageTextNotContains('Ajax successful'); + + // Change the amount[currency_code] field to trigger AJAX request. + $this->getSession()->getPage()->findField('amount[currency_code]')->selectOption('EUR'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->pageTextNotContains('AJAX successful: amount[number]'); + $this->assertSession()->pageTextContains('AJAX successful: amount[currency_code]'); + + // Blur the amount[number] field to trigger AJAX request. + $this->getSession()->getPage()->findField('amount[number]')->blur(); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->pageTextContains('AJAX successful: amount[number]'); + $this->assertSession()->pageTextNotContains('AJAX successful: amount[currency_code]'); + } + +} From 979a4dfe4f1ffd1d72c20df7a0770fc140d4ecea Mon Sep 17 00:00:00 2001 From: bojanz Date: Thu, 30 Nov 2017 15:36:05 +0100 Subject: [PATCH 007/103] Issue #2891770 by bojanz, Baik Ho: ProductVariationSkuConstraintValidator crashes if the SKU field is empty --- .../Constraint/ProductVariationSkuConstraintValidator.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/product/src/Plugin/Validation/Constraint/ProductVariationSkuConstraintValidator.php b/modules/product/src/Plugin/Validation/Constraint/ProductVariationSkuConstraintValidator.php index bb9f3de731..36c27eb7e6 100644 --- a/modules/product/src/Plugin/Validation/Constraint/ProductVariationSkuConstraintValidator.php +++ b/modules/product/src/Plugin/Validation/Constraint/ProductVariationSkuConstraintValidator.php @@ -14,7 +14,11 @@ class ProductVariationSkuConstraintValidator extends ConstraintValidator { * {@inheritdoc} */ public function validate($items, Constraint $constraint) { - $sku = $items->first()->value; + if (!$item = $items->first()) { + return; + } + + $sku = $item->value; if (isset($sku) && $sku !== '') { $sku_exists = (bool) \Drupal::entityQuery('commerce_product_variation') ->condition('sku', $sku) From 4ce73909bec2a42c70f3ec7c66a02b842e00050f Mon Sep 17 00:00:00 2001 From: alexpott Date: Fri, 1 Dec 2017 00:58:05 +0100 Subject: [PATCH 008/103] Issue #2926542 by alexpott, sorabh.v6: Adjustments added via AdjustmentDefaultWidget need to be locked --- .../Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php | 7 +++++++ modules/order/tests/src/Functional/OrderAdminTest.php | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php b/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php index 64b6d910d3..a5c775838a 100644 --- a/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php +++ b/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php @@ -57,6 +57,12 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#type' => 'value', '#value' => empty($source_id) ? 'custom' : $source_id, ]; + // If this is being added through the UI, the adjustment should be locked. + // UI added adjustments need to be locked to persist after an order refresh. + $element['locked'] = [ + '#type' => 'value', + '#value' => ($adjustment) ? $adjustment->isLocked() : TRUE, + ]; $states_selector_name = $this->fieldDefinition->getName() . "[$delta][type]"; $element['definition'] = [ @@ -115,6 +121,7 @@ public function massageFormValues(array $values, array $form, FormStateInterface 'amount' => new Price($value['definition']['amount']['number'], $value['definition']['amount']['currency_code']), 'source_id' => $value['source_id'], 'included' => $value['definition']['included'], + 'locked' => $value['locked'], ]); } return $values; diff --git a/modules/order/tests/src/Functional/OrderAdminTest.php b/modules/order/tests/src/Functional/OrderAdminTest.php index 350ae8930e..583270a7ef 100644 --- a/modules/order/tests/src/Functional/OrderAdminTest.php +++ b/modules/order/tests/src/Functional/OrderAdminTest.php @@ -98,7 +98,8 @@ public function testCreateOrder() { 'billing_profile[0][profile][address][0][address][postal_code]' => '94043', 'billing_profile[0][profile][address][0][address][locality]' => 'Mountain View', 'billing_profile[0][profile][address][0][address][administrative_area]' => 'CA', - 'adjustments[0][type]' => 'custom', + // Use an adjustment that is not locked by default. + 'adjustments[0][type]' => 'fee', 'adjustments[0][definition][label]' => '', 'adjustments[0][definition][amount][number]' => '2.00', ]; @@ -113,6 +114,7 @@ public function testCreateOrder() { $order = Order::load(1); $this->assertEquals(1, count($order->getItems())); $this->assertEquals(new Price('5.33', 'USD'), $order->getTotalPrice()); + $this->assertCount(1, $order->getAdjustments()); } /** From 654bf3db4c1a879193ab4437e0865d2b69c74ccf Mon Sep 17 00:00:00 2001 From: bradjones1 Date: Sat, 2 Dec 2017 21:27:52 +0100 Subject: [PATCH 009/103] Issue #2922811 by bradjones1, bojanz: Deprecate BundleFieldDefinition and BundlePluginInterface --- modules/order/commerce_order.module | 2 +- .../src/Plugin/Commerce/PaymentMethodType/CreditCard.php | 2 +- .../src/Plugin/Commerce/PaymentMethodType/PayPal.php | 2 +- .../PaymentMethodType/PaymentMethodTypeInterface.php | 2 +- .../Plugin/Commerce/PaymentType/PaymentTypeInterface.php | 2 +- modules/product/commerce_product.module | 2 +- src/BundleFieldDefinition.php | 4 ++++ src/BundlePluginInterface.php | 4 ++++ src/ConfigurableFieldManager.php | 1 + src/ConfigurableFieldManagerInterface.php | 8 +++++--- src/Plugin/Commerce/EntityTrait/EntityTraitInterface.php | 2 +- .../src/Plugin/Commerce/EntityTrait/First.php | 2 +- .../src/Plugin/Commerce/EntityTrait/Second.php | 2 +- tests/src/Kernel/ConfigurableFieldManagerTest.php | 2 +- 14 files changed, 24 insertions(+), 13 deletions(-) diff --git a/modules/order/commerce_order.module b/modules/order/commerce_order.module index a901cf1403..77468d8eb0 100644 --- a/modules/order/commerce_order.module +++ b/modules/order/commerce_order.module @@ -5,7 +5,7 @@ * Defines the Order entity and associated features. */ -use Drupal\commerce\BundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition; use Drupal\commerce_order\Entity\OrderTypeInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; diff --git a/modules/payment/src/Plugin/Commerce/PaymentMethodType/CreditCard.php b/modules/payment/src/Plugin/Commerce/PaymentMethodType/CreditCard.php index e2ccfa3038..1e92956133 100644 --- a/modules/payment/src/Plugin/Commerce/PaymentMethodType/CreditCard.php +++ b/modules/payment/src/Plugin/Commerce/PaymentMethodType/CreditCard.php @@ -2,7 +2,7 @@ namespace Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType; -use Drupal\commerce\BundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition; use Drupal\commerce_payment\CreditCard as CreditCardHelper; use Drupal\commerce_payment\Entity\PaymentMethodInterface; diff --git a/modules/payment/src/Plugin/Commerce/PaymentMethodType/PayPal.php b/modules/payment/src/Plugin/Commerce/PaymentMethodType/PayPal.php index 72dc8c79d3..1f3cf30ac9 100644 --- a/modules/payment/src/Plugin/Commerce/PaymentMethodType/PayPal.php +++ b/modules/payment/src/Plugin/Commerce/PaymentMethodType/PayPal.php @@ -2,7 +2,7 @@ namespace Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType; -use Drupal\commerce\BundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition; use Drupal\commerce_payment\Entity\PaymentMethodInterface; /** diff --git a/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeInterface.php b/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeInterface.php index 3ffc5adc98..e28eb2dbcd 100644 --- a/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeInterface.php +++ b/modules/payment/src/Plugin/Commerce/PaymentMethodType/PaymentMethodTypeInterface.php @@ -2,7 +2,7 @@ namespace Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType; -use Drupal\commerce\BundlePluginInterface; +use Drupal\entity\BundlePlugin\BundlePluginInterface; use Drupal\commerce_payment\Entity\PaymentMethodInterface; /** diff --git a/modules/payment/src/Plugin/Commerce/PaymentType/PaymentTypeInterface.php b/modules/payment/src/Plugin/Commerce/PaymentType/PaymentTypeInterface.php index 259f40e5a1..790e828aef 100644 --- a/modules/payment/src/Plugin/Commerce/PaymentType/PaymentTypeInterface.php +++ b/modules/payment/src/Plugin/Commerce/PaymentType/PaymentTypeInterface.php @@ -2,7 +2,7 @@ namespace Drupal\commerce_payment\Plugin\Commerce\PaymentType; -use Drupal\commerce\BundlePluginInterface; +use Drupal\entity\BundlePlugin\BundlePluginInterface; /** * Defines the interface for payment types. diff --git a/modules/product/commerce_product.module b/modules/product/commerce_product.module index 313362a0b4..2f6ad469df 100644 --- a/modules/product/commerce_product.module +++ b/modules/product/commerce_product.module @@ -5,7 +5,7 @@ * Defines the Product entity and associated features. */ -use Drupal\commerce\BundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition; use Drupal\commerce_product\Entity\ProductTypeInterface; use Drupal\Core\Cache\Cache; use Drupal\Core\Form\FormStateInterface; diff --git a/src/BundleFieldDefinition.php b/src/BundleFieldDefinition.php index 0f6c15486c..c32d201671 100644 --- a/src/BundleFieldDefinition.php +++ b/src/BundleFieldDefinition.php @@ -2,11 +2,15 @@ namespace Drupal\commerce; +@trigger_error('The ' . __NAMESPACE__ . '\BundleFieldDefinition is deprecated. Instead, use \Drupal\entity\BundleFieldDefinition', E_USER_DEPRECATED); + use Drupal\entity\BundleFieldDefinition as BaseBundleFieldDefinition; /** * Provides a field definition class for bundle fields. * * Note: This code has moved to Entity API, see the parent class. + * + * @deprecated in Commerce 2.0 */ class BundleFieldDefinition extends BaseBundleFieldDefinition {} diff --git a/src/BundlePluginInterface.php b/src/BundlePluginInterface.php index 5270542a2b..5ea26edf33 100644 --- a/src/BundlePluginInterface.php +++ b/src/BundlePluginInterface.php @@ -2,11 +2,15 @@ namespace Drupal\commerce; +@trigger_error('The ' . __NAMESPACE__ . '\BundlePluginInterface is deprecated. Instead, use \Drupal\entity\BundlePlugin\BundlePluginInterface', E_USER_DEPRECATED); + use Drupal\entity\BundlePlugin\BundlePluginInterface as BaseBundlePluginInterface; /** * Interface for plugins which act as entity bundles. * * Note: This code has moved to Entity API, see the parent class. + * + * @deprecated in Commerce 2.0 */ interface BundlePluginInterface extends BaseBundlePluginInterface {} diff --git a/src/ConfigurableFieldManager.php b/src/ConfigurableFieldManager.php index 7f33d69e50..27cad40f83 100644 --- a/src/ConfigurableFieldManager.php +++ b/src/ConfigurableFieldManager.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\entity\BundleFieldDefinition; class ConfigurableFieldManager implements ConfigurableFieldManagerInterface { diff --git a/src/ConfigurableFieldManagerInterface.php b/src/ConfigurableFieldManagerInterface.php index bf38321714..8c9bfdd45f 100644 --- a/src/ConfigurableFieldManagerInterface.php +++ b/src/ConfigurableFieldManagerInterface.php @@ -2,6 +2,8 @@ namespace Drupal\commerce; +use Drupal\entity\BundleFieldDefinition; + /** * Manages configurable fields based on field definitions. * @@ -12,7 +14,7 @@ interface ConfigurableFieldManagerInterface { /** * Creates a configurable field from the given field definition. * - * @param \Drupal\commerce\BundleFieldDefinition $field_definition + * @param \Drupal\entity\BundleFieldDefinition $field_definition * The field definition. * @param bool $lock * Whether the created field should be locked. @@ -28,7 +30,7 @@ public function createField(BundleFieldDefinition $field_definition, $lock = TRU /** * Deletes the configurable field created from the given field definition. * - * @param \Drupal\commerce\BundleFieldDefinition $field_definition + * @param \Drupal\entity\BundleFieldDefinition $field_definition * The field definition. * * @throws \InvalidArgumentException @@ -42,7 +44,7 @@ public function deleteField(BundleFieldDefinition $field_definition); /** * Checks whether the configurable field has data. * - * @param \Drupal\commerce\BundleFieldDefinition $field_definition + * @param \Drupal\entity\BundleFieldDefinition $field_definition * The field definition. * * @return bool diff --git a/src/Plugin/Commerce/EntityTrait/EntityTraitInterface.php b/src/Plugin/Commerce/EntityTrait/EntityTraitInterface.php index 144ecb95c3..cf8d954f48 100644 --- a/src/Plugin/Commerce/EntityTrait/EntityTraitInterface.php +++ b/src/Plugin/Commerce/EntityTrait/EntityTraitInterface.php @@ -38,7 +38,7 @@ public function getEntityTypeIds(); * THe provided field definitions will be created as configurable * fields when the entity trait is installed for an entity type/bundle. * - * @return \Drupal\commerce\BundleFieldDefinition[] + * @return \Drupal\entity\BundleFieldDefinition[] * An array of field definitions, keyed by field name. */ public function buildFieldDefinitions(); diff --git a/tests/modules/commerce_test/src/Plugin/Commerce/EntityTrait/First.php b/tests/modules/commerce_test/src/Plugin/Commerce/EntityTrait/First.php index bc2c9821fb..b1c717c886 100644 --- a/tests/modules/commerce_test/src/Plugin/Commerce/EntityTrait/First.php +++ b/tests/modules/commerce_test/src/Plugin/Commerce/EntityTrait/First.php @@ -2,7 +2,7 @@ namespace Drupal\commerce_test\Plugin\Commerce\EntityTrait; -use Drupal\commerce\BundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition; use Drupal\commerce\Plugin\Commerce\EntityTrait\EntityTraitBase; /** diff --git a/tests/modules/commerce_test/src/Plugin/Commerce/EntityTrait/Second.php b/tests/modules/commerce_test/src/Plugin/Commerce/EntityTrait/Second.php index a9c730ddce..a2d6d74f42 100644 --- a/tests/modules/commerce_test/src/Plugin/Commerce/EntityTrait/Second.php +++ b/tests/modules/commerce_test/src/Plugin/Commerce/EntityTrait/Second.php @@ -2,7 +2,7 @@ namespace Drupal\commerce_test\Plugin\Commerce\EntityTrait; -use Drupal\commerce\BundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition; use Drupal\commerce\Plugin\Commerce\EntityTrait\EntityTraitBase; /** diff --git a/tests/src/Kernel/ConfigurableFieldManagerTest.php b/tests/src/Kernel/ConfigurableFieldManagerTest.php index 3fcf24c5ef..2553d6b1ee 100644 --- a/tests/src/Kernel/ConfigurableFieldManagerTest.php +++ b/tests/src/Kernel/ConfigurableFieldManagerTest.php @@ -2,7 +2,7 @@ namespace Drupal\Tests\commerce\Kernel; -use Drupal\commerce\BundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition; use Drupal\entity_test\Entity\EntityTest; use Drupal\field\Entity\FieldConfig; From 108faa5b98bdd1b1406521617b223d6ab417575c Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Sat, 2 Dec 2017 21:37:58 +0100 Subject: [PATCH 010/103] Issue #2921303 by joachim, bojanz: 'Great Britain' in tax rates list should say 'United Kingdom' --- modules/tax/src/Plugin/Commerce/TaxType/EuropeanUnionVat.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/tax/src/Plugin/Commerce/TaxType/EuropeanUnionVat.php b/modules/tax/src/Plugin/Commerce/TaxType/EuropeanUnionVat.php index 68c15c31d4..d3b2f9de68 100644 --- a/modules/tax/src/Plugin/Commerce/TaxType/EuropeanUnionVat.php +++ b/modules/tax/src/Plugin/Commerce/TaxType/EuropeanUnionVat.php @@ -511,7 +511,7 @@ public function buildZones() { ]); $zones['gb'] = new TaxZone([ 'id' => 'gb', - 'label' => $this->t('Great Britain'), + 'label' => $this->t('United Kingdom'), 'display_label' => $labels['vat'], 'territories' => [ ['country_code' => 'GB'], From f526dbeeaa8c7550c31bba979289c281fa2ad8b5 Mon Sep 17 00:00:00 2001 From: Thomas Cys Date: Sat, 2 Dec 2017 21:53:38 +0100 Subject: [PATCH 011/103] Issue 2889607: Create a views argument default plugin to extract a Product ID from URL (#752) --- .../Plugin/views/argument_default/Product.php | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 modules/product/src/Plugin/views/argument_default/Product.php diff --git a/modules/product/src/Plugin/views/argument_default/Product.php b/modules/product/src/Plugin/views/argument_default/Product.php new file mode 100644 index 0000000000..8f631350b2 --- /dev/null +++ b/modules/product/src/Plugin/views/argument_default/Product.php @@ -0,0 +1,82 @@ +routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_route_match') + ); + } + + /** + * {@inheritdoc} + */ + public function getArgument() { + if (($product = $this->routeMatch->getParameter('commerce_product')) && $product instanceof ProductInterface) { + return $product->id(); + } + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return Cache::PERMANENT; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return ['url']; + } + +} From 0e6d50a171a777ba938a8bcf6d1ae3ed46e328fd Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Sun, 3 Dec 2017 13:34:27 +0100 Subject: [PATCH 012/103] Fix the Travis build failure caused by Symfony 4.0 --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d9052853b7..8bd09fd5a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -107,8 +107,6 @@ before_install: - composer global require "hirak/prestissimo:^0.3" - composer global require "lionsad/drupal_ti:dev-master#396d11d200005eb68491d24170da0a98ae7f51b3" - composer global require "squizlabs/php_codesniffer:2.*" - - composer global require "drupal/coder:8.2.*" - - phpcs --config-set installed_paths $HOME/.composer/vendor/drupal/coder/coder_sniffer - drupal-ti before_install install: @@ -118,6 +116,9 @@ install: - tar -xvf $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -C $PWD/travis-phantomjs - export PATH=$PWD/travis-phantomjs:$PATH - phantomjs --version + # Installed after Drush to prevent Composer conflicts around symfony/yaml. + - composer global require "drupal/coder:8.2.*" + - phpcs --config-set installed_paths $HOME/.composer/vendor/drupal/coder/coder_sniffer before_script: - drupal-ti --include ".travis-before-script.sh" From 2b54f6d86e785ab4679adba48df66b89882571dd Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Sun, 3 Dec 2017 14:06:40 +0100 Subject: [PATCH 013/103] Issue #2926000 by joachim, bojanz: Deprecate CommercePluginEntityFormBase --- modules/checkout/src/Form/CheckoutFlowForm.php | 7 ++++--- modules/payment/src/Form/PaymentGatewayForm.php | 7 ++++--- modules/tax/src/Form/TaxTypeForm.php | 7 ++++--- src/Form/CommercePluginEntityFormBase.php | 3 +++ 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/checkout/src/Form/CheckoutFlowForm.php b/modules/checkout/src/Form/CheckoutFlowForm.php index 654be162b7..55c50d28d9 100644 --- a/modules/checkout/src/Form/CheckoutFlowForm.php +++ b/modules/checkout/src/Form/CheckoutFlowForm.php @@ -2,12 +2,12 @@ namespace Drupal\commerce_checkout\Form; -use Drupal\commerce\Form\CommercePluginEntityFormBase; use Drupal\commerce_checkout\CheckoutFlowManager; +use Drupal\Core\Entity\EntityForm; use Drupal\Core\Form\FormStateInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -class CheckoutFlowForm extends CommercePluginEntityFormBase { +class CheckoutFlowForm extends EntityForm { /** * The checkout flow plugin manager. @@ -66,6 +66,7 @@ public function form(array $form, FormStateInterface $form_state) { '#machine_name' => [ 'exists' => '\Drupal\commerce_checkout\Entity\CheckoutFlow::load', ], + '#disabled' => !$checkout_flow->isNew(), ]; $form['plugin'] = [ '#type' => 'radios', @@ -82,7 +83,7 @@ public function form(array $form, FormStateInterface $form_state) { $form['configuration'] = $checkout_flow->getPlugin()->buildConfigurationForm($form['configuration'], $form_state); } - return $this->protectPluginIdElement($form); + return $form; } /** diff --git a/modules/payment/src/Form/PaymentGatewayForm.php b/modules/payment/src/Form/PaymentGatewayForm.php index 6a93886d0c..f0fe44ce8e 100644 --- a/modules/payment/src/Form/PaymentGatewayForm.php +++ b/modules/payment/src/Form/PaymentGatewayForm.php @@ -2,13 +2,13 @@ namespace Drupal\commerce_payment\Form; -use Drupal\commerce\Form\CommercePluginEntityFormBase; use Drupal\commerce_payment\PaymentGatewayManager; use Drupal\Component\Utility\Html; +use Drupal\Core\Entity\EntityForm; use Drupal\Core\Form\FormStateInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -class PaymentGatewayForm extends CommercePluginEntityFormBase { +class PaymentGatewayForm extends EntityForm { /** * The payment gateway plugin manager. @@ -90,6 +90,7 @@ public function form(array $form, FormStateInterface $form_state) { '#machine_name' => [ 'exists' => '\Drupal\commerce_payment\Entity\PaymentGateway::load', ], + '#disabled' => !$gateway->isNew(), ]; $form['plugin'] = [ '#type' => 'radios', @@ -136,7 +137,7 @@ public function form(array $form, FormStateInterface $form_state) { '#default_value' => (int) $gateway->status(), ]; - return $this->protectPluginIdElement($form); + return $form; } /** diff --git a/modules/tax/src/Form/TaxTypeForm.php b/modules/tax/src/Form/TaxTypeForm.php index 66de801a0e..cbeae6d13f 100644 --- a/modules/tax/src/Form/TaxTypeForm.php +++ b/modules/tax/src/Form/TaxTypeForm.php @@ -2,13 +2,13 @@ namespace Drupal\commerce_tax\Form; -use Drupal\commerce\Form\CommercePluginEntityFormBase; use Drupal\commerce_tax\TaxTypeManager; use Drupal\Component\Utility\Html; +use Drupal\Core\Entity\EntityForm; use Drupal\Core\Form\FormStateInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -class TaxTypeForm extends CommercePluginEntityFormBase { +class TaxTypeForm extends EntityForm { /** * The tax type plugin manager. @@ -79,6 +79,7 @@ public function form(array $form, FormStateInterface $form_state) { '#machine_name' => [ 'exists' => '\Drupal\commerce_tax\Entity\TaxType::load', ], + '#disabled' => !$type->isNew(), ]; $form['plugin'] = [ '#type' => 'radios', @@ -105,7 +106,7 @@ public function form(array $form, FormStateInterface $form_state) { '#default_value' => $type->status(), ]; - return $this->protectPluginIdElement($form); + return $form; } /** diff --git a/src/Form/CommercePluginEntityFormBase.php b/src/Form/CommercePluginEntityFormBase.php index 421bb137a4..ee561d2f00 100644 --- a/src/Form/CommercePluginEntityFormBase.php +++ b/src/Form/CommercePluginEntityFormBase.php @@ -4,6 +4,9 @@ use Drupal\Core\Entity\EntityForm; +/** + * @deprecated in Commerce 2.2. Set #disabled on the ID element directly. + */ abstract class CommercePluginEntityFormBase extends EntityForm { /** From 70ab01e5ec5bdc969e2fda1f8b6c83dcd74052ec Mon Sep 17 00:00:00 2001 From: chrisrockwell Date: Sun, 3 Dec 2017 16:25:03 +0100 Subject: [PATCH 014/103] Issue #2866638 by chrisrockwell, nicola85, subhojit777, bojanz, sumanthkumarc: Prevent double-submit of checkout pages --- modules/checkout/commerce_checkout.libraries.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/checkout/commerce_checkout.libraries.yml b/modules/checkout/commerce_checkout.libraries.yml index 9621b692a7..b64a7f6863 100644 --- a/modules/checkout/commerce_checkout.libraries.yml +++ b/modules/checkout/commerce_checkout.libraries.yml @@ -27,3 +27,5 @@ form: css: theme: css/commerce_checkout.form.css: {} + dependencies: + - core/drupal.form From 59a6f5d0c8b82cb37ead365d1b75a2877375285c Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Wed, 6 Dec 2017 19:24:11 +0100 Subject: [PATCH 015/103] Issue #2928972 by bojanz: Add PaymentGatewayBase::toMinorUnits() --- .../PaymentGateway/PaymentGatewayBase.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php b/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php index 37fca37521..b4883d5b8e 100644 --- a/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php +++ b/modules/payment/src/Plugin/Commerce/PaymentGateway/PaymentGatewayBase.php @@ -9,6 +9,7 @@ use Drupal\commerce_payment\Exception\InvalidRequestException; use Drupal\commerce_payment\PaymentMethodTypeManager; use Drupal\commerce_payment\PaymentTypeManager; +use Drupal\commerce_price\Calculator; use Drupal\commerce_price\Price; use Drupal\Component\Datetime\TimeInterface; use Drupal\Component\Utility\NestedArray; @@ -360,6 +361,30 @@ public function buildPaymentOperations(PaymentInterface $payment) { return $operations; } + /** + * Converts the given amount to its minor units. + * + * For example, 9.99 USD becomes 999. + * + * @param \Drupal\commerce_price\Price $amount + * The amount. + * + * @return int + * The amount in minor units, as an integer. + */ + protected function toMinorUnits(Price $amount) { + $currency_storage = $this->entityTypeManager->getStorage('commerce_currency'); + /** @var \Drupal\commerce_price\Entity\CurrencyInterface $currency */ + $currency = $currency_storage->load($amount->getCurrencyCode()); + $fraction_digits = $currency->getFractionDigits(); + $number = $amount->getNumber(); + if ($fraction_digits > 0) { + $number = Calculator::multiply($number, pow(10, $fraction_digits)); + } + + return round($number, 0); + } + /** * Gets the remote customer ID for the given user. * From c7c387c398c5851f0520e9a5f91cd6989809fa60 Mon Sep 17 00:00:00 2001 From: ClusterFCK Date: Mon, 13 Nov 2017 14:26:57 +0100 Subject: [PATCH 016/103] Issue #2922381 by Rob C: Unable to export the 'online' store entity form display configuration --- modules/store/commerce_store.info.yml | 1 + ..._display.commerce_store.online.default.yml | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 modules/store/config/install/core.entity_form_display.commerce_store.online.default.yml diff --git a/modules/store/commerce_store.info.yml b/modules/store/commerce_store.info.yml index da5f4cbb89..70d8ab0fce 100644 --- a/modules/store/commerce_store.info.yml +++ b/modules/store/commerce_store.info.yml @@ -11,6 +11,7 @@ config_devel: install: - commerce_store.commerce_store_type.online - commerce_store.settings + - core.entity_form_display.commerce_store.online.default - core.entity_view_display.commerce_store.online.default - views.view.commerce_stores - system.action.commerce_delete_store_action diff --git a/modules/store/config/install/core.entity_form_display.commerce_store.online.default.yml b/modules/store/config/install/core.entity_form_display.commerce_store.online.default.yml new file mode 100644 index 0000000000..b4effdc058 --- /dev/null +++ b/modules/store/config/install/core.entity_form_display.commerce_store.online.default.yml @@ -0,0 +1,61 @@ +langcode: en +status: true +dependencies: + config: + - commerce_store.commerce_store_type.online + module: + - address +id: commerce_store.online.default +targetEntityType: commerce_store +bundle: online +mode: default +content: + address: + type: address_default + settings: + default_country: site_default + weight: 3 + region: content + third_party_settings: { } + billing_countries: + type: options_select + weight: 4 + region: content + settings: { } + third_party_settings: { } + default_currency: + type: options_select + weight: 2 + region: content + settings: { } + third_party_settings: { } + mail: + type: email_default + weight: 1 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + name: + type: string_textfield + weight: 0 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + prices_include_tax: + type: boolean_checkbox + settings: + display_label: true + weight: 3 + region: content + third_party_settings: { } + tax_registrations: + type: options_select + weight: 4 + region: content + settings: { } + third_party_settings: { } +hidden: { } From 1688ed64b096396a542c3fcc4813b8a00dbbb4d9 Mon Sep 17 00:00:00 2001 From: ClusterFCK Date: Fri, 8 Dec 2017 14:08:42 -0600 Subject: [PATCH 017/103] Issue #2882378 by Rob C, mglaman, zenimagine: Cannot access edit route for store from canonical route --- modules/store/commerce_store.links.task.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/store/commerce_store.links.task.yml b/modules/store/commerce_store.links.task.yml index 4454b95a81..cef7049bbe 100644 --- a/modules/store/commerce_store.links.task.yml +++ b/modules/store/commerce_store.links.task.yml @@ -10,7 +10,7 @@ entity.commerce_store.collection: entity.commerce_store.edit_form: route_name: entity.commerce_store.edit_form - base_route: entity.commerce_store.edit_form + base_route: entity.commerce_store.canonical title: Edit entity.commerce_store_type.collection: From 631ee7d69768099f202f97b2ecb71b641b3d4d7d Mon Sep 17 00:00:00 2001 From: vasike Date: Fri, 8 Dec 2017 15:53:08 -0600 Subject: [PATCH 018/103] Issue #2848183 by mglaman, agoradesign, vasike: TimestampEventSubscriber has no test coverage --- modules/order/commerce_order.services.yml | 2 +- .../TimestampEventSubscriber.php | 23 +++++- .../tests/src/Kernel/TimestampEventTest.php | 77 +++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 modules/order/tests/src/Kernel/TimestampEventTest.php diff --git a/modules/order/commerce_order.services.yml b/modules/order/commerce_order.services.yml index 7015228286..8d8e5d4914 100644 --- a/modules/order/commerce_order.services.yml +++ b/modules/order/commerce_order.services.yml @@ -34,7 +34,7 @@ services: commerce_order.timestamp_event_subscriber: class: Drupal\commerce_order\EventSubscriber\TimestampEventSubscriber - arguments: ['@entity_type.manager'] + arguments: ['@entity_type.manager', '@datetime.time'] tags: - { name: event_subscriber } diff --git a/modules/order/src/EventSubscriber/TimestampEventSubscriber.php b/modules/order/src/EventSubscriber/TimestampEventSubscriber.php index 4ac52df690..9668e02194 100644 --- a/modules/order/src/EventSubscriber/TimestampEventSubscriber.php +++ b/modules/order/src/EventSubscriber/TimestampEventSubscriber.php @@ -2,11 +2,32 @@ namespace Drupal\commerce_order\EventSubscriber; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\state_machine\Event\WorkflowTransitionEvent; +use Drupal\Component\Datetime\TimeInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class TimestampEventSubscriber implements EventSubscriberInterface { + /** + * The system time. + * + * @var \Drupal\Component\Datetime\TimeInterface + */ + protected $time; + + /** + * Constructs a new TimestampEventSubscriber object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The system time. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, TimeInterface $time) { + $this->time = $time; + } + /** * {@inheritdoc} */ @@ -27,7 +48,7 @@ public function onPlaceTransition(WorkflowTransitionEvent $event) { /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ $order = $event->getEntity(); if (empty($order->getPlacedTime())) { - $order->setPlacedTime(\Drupal::time()->getRequestTime()); + $order->setPlacedTime($this->time->getRequestTime()); } } diff --git a/modules/order/tests/src/Kernel/TimestampEventTest.php b/modules/order/tests/src/Kernel/TimestampEventTest.php new file mode 100644 index 0000000000..ffbf4d229d --- /dev/null +++ b/modules/order/tests/src/Kernel/TimestampEventTest.php @@ -0,0 +1,77 @@ +installEntitySchema('profile'); + $this->installEntitySchema('commerce_order'); + $this->installEntitySchema('commerce_order_item'); + $this->installEntitySchema('commerce_product'); + $this->installEntitySchema('commerce_product_variation'); + $this->installConfig(['commerce_product', 'commerce_order']); + } + + /** + * Tests setting the order number. + */ + public function testOnPlaceTransition() { + $user = $this->createUser(['mail' => $this->randomString() . '@example.com']); + + $order_item = OrderItem::create([ + 'type' => 'default', + 'quantity' => 1, + 'unit_price' => new Price('12.00', 'USD'), + ]); + $order_item->save(); + + /** @var \Drupal\commerce_order\Entity\Order $order */ + $order = Order::create([ + 'type' => 'default', + 'state' => 'draft', + 'mail' => 'text@example.com', + 'uid' => $user->id(), + 'ip_address' => '127.0.0.1', + 'store_id' => $this->store->id(), + 'order_items' => [$order_item], + ]); + $order->save(); + + $transition = $order->getState()->getTransitions(); + $order->getState()->applyTransition($transition['place']); + $order->save(); + $this->assertEquals(\Drupal::time()->getRequestTime(), $order->getPlacedTime(), 'During placement transition, the order placed timestamp is set to the request time.'); + } + +} From b0f013fd2fbcb5c215cceedf511a112484cf799b Mon Sep 17 00:00:00 2001 From: goz Date: Fri, 8 Dec 2017 21:37:05 -0600 Subject: [PATCH 019/103] Issue #2922147 by mglaman, GoZ: Delete a coupon does not delete coupon reference from Promotion --- modules/promotion/src/Entity/Coupon.php | 12 ++++++++++++ .../tests/src/Kernel/Entity/PromotionTest.php | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/modules/promotion/src/Entity/Coupon.php b/modules/promotion/src/Entity/Coupon.php index 9a2ac82781..5b3f0efc49 100644 --- a/modules/promotion/src/Entity/Coupon.php +++ b/modules/promotion/src/Entity/Coupon.php @@ -164,6 +164,18 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti /** @var \Drupal\commerce_promotion\PromotionUsageInterface $usage */ $usage = \Drupal::service('commerce_promotion.usage'); $usage->deleteByCoupon($entities); + // Delete references to those coupons in promotions. + foreach ($entities as $coupon) { + $coupons_id[] = $coupon->id(); + } + $promotions = \Drupal::entityTypeManager()->getStorage('commerce_promotion')->loadByProperties(['coupons' => $coupons_id]); + /** @var \Drupal\commerce_promotion\Entity\PromotionInterface $promotion */ + foreach ($promotions as $promotion) { + foreach ($entities as $entity) { + $promotion->removeCoupon($entity); + } + $promotion->save(); + } } /** diff --git a/modules/promotion/tests/src/Kernel/Entity/PromotionTest.php b/modules/promotion/tests/src/Kernel/Entity/PromotionTest.php index 112f7cae5e..529d754f0f 100644 --- a/modules/promotion/tests/src/Kernel/Entity/PromotionTest.php +++ b/modules/promotion/tests/src/Kernel/Entity/PromotionTest.php @@ -45,6 +45,7 @@ protected function setUp() { $this->installEntitySchema('commerce_order_item'); $this->installEntitySchema('commerce_promotion'); $this->installEntitySchema('commerce_promotion_coupon'); + $this->installSchema('commerce_promotion', ['commerce_promotion_usage']); $this->installConfig([ 'profile', 'commerce_order', @@ -152,6 +153,13 @@ public function testPromotion() { $promotion->addCoupon($coupon1); $this->assertTrue($promotion->hasCoupon($coupon1)); + // Check Coupon::postDelete() remove Coupon reference from promotion. + $promotion->save(); + $promotion = $this->reloadEntity($promotion); + $this->assertEquals($promotion->id(), 1); + $coupon1->delete(); + $this->assertFalse($promotion->hasCoupon($coupon1)); + $promotion->setUsageLimit(10); $this->assertEquals(10, $promotion->getUsageLimit()); From 7af1dd81d0c4da9faebd86c941b4b0472bfa03ee Mon Sep 17 00:00:00 2001 From: mglaman Date: Mon, 11 Dec 2017 13:24:59 -0600 Subject: [PATCH 020/103] Issue #2860840 by mglaman, mgoncalves, Sarahphp1, durum, sorabh.v6, ptmkenny, Regnoy, vasike: Product variation not translated on "add to cart" form --- .../AddToCartMultilingualTest.php | 249 ++++++++++++++++++ modules/product/commerce_product.services.yml | 2 +- .../FieldFormatter/AddToCartFormatter.php | 1 + .../ProductVariationAttributesWidget.php | 15 +- .../ProductVariationTitleWidget.php | 3 +- .../ProductVariationWidgetBase.php | 40 ++- modules/product/src/ProductLazyBuilders.php | 20 +- 7 files changed, 319 insertions(+), 11 deletions(-) create mode 100644 modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php diff --git a/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php new file mode 100644 index 0000000000..98f9ee4899 --- /dev/null +++ b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php @@ -0,0 +1,249 @@ +setupMultilingual(); + + /** @var \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type */ + $variation_type = ProductVariationType::load($this->variation->bundle()); + $color_attributes = $this->createAttributeSet($variation_type, 'color', [ + 'red' => 'Red', + 'blue' => 'Blue', + ]); + + foreach ($color_attributes as $key => $color_attribute) { + $color_attribute->addTranslation('fr', [ + 'name' => 'FR ' . $color_attribute->label(), + ]); + $color_attribute->save(); + } + $size_attributes = $this->createAttributeSet($variation_type, 'size', [ + 'small' => 'Small', + 'medium' => 'Medium', + 'large' => 'Large', + ]); + foreach ($size_attributes as $key => $size_attribute) { + $size_attribute->addTranslation('fr', [ + 'name' => 'FR ' . $size_attribute->label(), + ]); + $size_attribute->save(); + } + + // Reload the variation since we have new fields. + $this->variation = ProductVariation::load($this->variation->id()); + + // Translate the product's title. + $product = $this->variation->getProduct(); + $product->setTitle('My Super Product'); + $product->addTranslation('fr', [ + 'title' => 'Mon super produit', + ]); + $product->save(); + + // Update first variation to have the attribute's value. + $this->variation->get('attribute_color')->setValue($color_attributes['red']); + $this->variation->get('attribute_size')->setValue($size_attributes['small']); + $this->variation->save(); + + // The matrix is intentionally uneven, blue / large is missing. + $attribute_values_matrix = [ + ['red', 'small'], + ['red', 'medium'], + ['red', 'large'], + ['blue', 'small'], + ['blue', 'medium'], + ]; + + // Generate variations off of the attributes values matrix. + foreach ($attribute_values_matrix as $key => $value) { + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ + $variation = $this->createEntity('commerce_product_variation', [ + 'type' => $variation_type->id(), + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + ]); + $variation->get('attribute_color')->setValue($color_attributes[$value[0]]); + $variation->get('attribute_size')->setValue($size_attributes[$value[1]]); + $variation->save(); + $product->addVariation($variation); + } + + $product->save(); + $this->product = Product::load($product->id()); + + // Create a translation for each variation on the product. + foreach ($this->product->getVariations() as $variation) { + $variation->addTranslation('fr')->save(); + } + + $this->variations = $product->getVariations(); + $this->colorAttributes = $color_attributes; + $this->sizeAttributes = $size_attributes; + } + + /** + * Sets up the multilingual items. + */ + protected function setupMultilingual() { + // Add a new language. + ConfigurableLanguage::createFromLangcode('fr')->save(); + + // Enable translation for the product and ensure the change is picked up. + $this->container->get('content_translation.manager')->setEnabled('commerce_product', $this->variation->bundle(), TRUE); + $this->container->get('content_translation.manager')->setEnabled('commerce_product_variation', $this->variation->bundle(), TRUE); + $this->container->get('entity.manager')->clearCachedDefinitions(); + $this->container->get('router.builder')->rebuild(); + $this->container->get('entity.definition_update_manager')->applyUpdates(); + + // Rebuild the container so that the new languages are picked up by services + // that hold a list of languages. + $this->rebuildContainer(); + } + + /** + * Tests that the attribute widget uses translated items. + */ + public function testProductVariationAttributesWidget() { + $this->drupalGet($this->product->toUrl()); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'Red'); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_size]', 'Small'); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_color]', $this->colorAttributes['blue']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['medium']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['large']->id()); + $this->getSession()->getPage()->pressButton('Add to cart'); + + // Change the site language. + $this->config('system.site')->set('default_langcode', 'fr')->save(); + drupal_flush_all_caches(); + + $this->drupalGet($this->product->getTranslation('fr')->toUrl()); + // Use AJAX to change the size to Medium, keeping the color on Red. + $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][attributes][attribute_size]', 'FR Medium'); + $this->waitForAjaxToFinish(); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'FR Red'); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_size]', 'FR Medium'); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_color]', $this->colorAttributes['blue']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['small']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['large']->id()); + + // Use AJAX to change the color to Blue, keeping the size on Medium. + $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][attributes][attribute_color]', 'FR Blue'); + $this->waitForAjaxToFinish(); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'FR Blue'); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_size]', 'FR Medium'); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_color]', $this->colorAttributes['red']->id()); + $this->assertAttributeExists('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['small']->id()); + $this->assertAttributeDoesNotExist('purchased_entity[0][attributes][attribute_size]', $this->sizeAttributes['large']->id()); + $this->getSession()->getPage()->pressButton('Add to cart'); + + $this->cart = Order::load($this->cart->id()); + $order_items = $this->cart->getItems(); + $this->assertOrderItemInOrder($this->variations[0]->getTranslation('fr'), $order_items[0]); + $this->assertOrderItemInOrder($this->variations[5]->getTranslation('fr'), $order_items[1]); + } + + /** + * Tests the title widget has translated variation title. + */ + public function testProductVariationTitleWidget() { + $order_item_form_display = EntityFormDisplay::load('commerce_order_item.default.add_to_cart'); + $order_item_form_display->setComponent('purchased_entity', [ + 'type' => 'commerce_product_variation_title', + ]); + $order_item_form_display->save(); + + $this->drupalGet($this->product->toUrl()); + $this->assertSession()->selectExists('purchased_entity[0][variation]'); + $this->assertAttributeSelected('purchased_entity[0][variation]', 'My Super Product - Red, Small'); + $this->getSession()->getPage()->pressButton('Add to cart'); + + // Change the site language. + $this->config('system.site')->set('default_langcode', 'fr')->save(); + drupal_flush_all_caches(); + + $this->drupalGet($this->product->getTranslation('fr')->toUrl()); + // Use AJAX to change the size to Medium, keeping the color on Red. + $this->assertAttributeSelected('purchased_entity[0][variation]', 'Mon super produit - FR Red, FR Small'); + $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][variation]', 'Mon super produit - FR Red, FR Medium'); + $this->waitForAjaxToFinish(); + $this->assertAttributeSelected('purchased_entity[0][variation]', 'Mon super produit - FR Red, FR Medium'); + $this->assertSession()->pageTextContains('Mon super produit - FR Red, FR Medium'); + // Use AJAX to change the color to Blue, keeping the size on Medium. + $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][variation]', 'Mon super produit - FR Blue, FR Medium'); + $this->waitForAjaxToFinish(); + $this->assertAttributeSelected('purchased_entity[0][variation]', 'Mon super produit - FR Blue, FR Medium'); + $this->assertSession()->pageTextContains('Mon super produit - FR Blue, FR Medium'); + $this->getSession()->getPage()->pressButton('Add to cart'); + + $this->cart = Order::load($this->cart->id()); + $order_items = $this->cart->getItems(); + $this->assertOrderItemInOrder($this->variations[0]->getTranslation('fr'), $order_items[0]); + $this->assertOrderItemInOrder($this->variations[5]->getTranslation('fr'), $order_items[1]); + } + +} diff --git a/modules/product/commerce_product.services.yml b/modules/product/commerce_product.services.yml index cca824dbe1..b86b802e77 100644 --- a/modules/product/commerce_product.services.yml +++ b/modules/product/commerce_product.services.yml @@ -5,7 +5,7 @@ services: commerce_product.lazy_builders: class: Drupal\commerce_product\ProductLazyBuilders - arguments: ['@entity_type.manager', '@form_builder'] + arguments: ['@entity_type.manager', '@form_builder', '@entity.repository'] commerce_product.variation_field_renderer: class: Drupal\commerce_product\ProductVariationFieldRenderer diff --git a/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php b/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php index 8c8fee6f80..deb71c0fb7 100644 --- a/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php +++ b/modules/product/src/Plugin/Field/FieldFormatter/AddToCartFormatter.php @@ -70,6 +70,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { $items->getEntity()->id(), $this->viewMode, $this->getSetting('combine'), + $langcode, ], ], '#create_placeholder' => TRUE, diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php index b997494242..2dc1592c14 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php @@ -6,6 +6,7 @@ use Drupal\commerce_product\ProductAttributeFieldManagerInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; @@ -55,11 +56,13 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem * Any third party settings. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository. * @param \Drupal\commerce_product\ProductAttributeFieldManagerInterface $attribute_field_manager * The attribute field manager. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, ProductAttributeFieldManagerInterface $attribute_field_manager) { - parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $entity_type_manager); + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $entity_type_manager, $entity_repository); $this->attributeFieldManager = $attribute_field_manager; $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); @@ -76,6 +79,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['settings'], $configuration['third_party_settings'], $container->get('entity_type.manager'), + $container->get('entity.repository'), $container->get('commerce_product.attribute_field_manager') ); } @@ -86,7 +90,7 @@ public static function create(ContainerInterface $container, array $configuratio public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ $product = $form_state->get('product'); - $variations = $this->variationStorage->loadEnabled($product); + $variations = $this->loadEnabledVariations($product); if (count($variations) === 0) { // Nothing to purchase, tell the parent form to hide itself. $form_state->set('hide_form', TRUE); @@ -141,6 +145,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen } } } + $element['variation'] = [ '#type' => 'value', '#value' => $selected_variation->id(), @@ -264,10 +269,12 @@ protected function getAttributeInfo(ProductVariationInterface $selected_variatio $field = $field_definitions[$field_name]; /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */ $attribute = $this->attributeStorage->load($attribute_ids[$index]); + // Make sure we have translation for attribute. + $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId()); $attributes[$field_name] = [ 'field_name' => $field_name, - 'title' => $field->getLabel(), + 'title' => $attribute->label(), 'required' => $field->isRequired(), 'element_type' => $attribute->getElementType(), ]; diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php index e6a95d672b..7a431d0181 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php @@ -71,8 +71,7 @@ public function settingsSummary() { public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ $product = $form_state->get('product'); - /** @var \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations */ - $variations = $this->variationStorage->loadEnabled($product); + $variations = $this->loadEnabledVariations($product); if (count($variations) === 0) { // Nothing to purchase, tell the parent form to hide itself. $form_state->set('hide_form', TRUE); diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php index 4c9fd84b00..d2a1cbce86 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php @@ -2,9 +2,11 @@ namespace Drupal\commerce_product\Plugin\Field\FieldWidget; +use Drupal\commerce_product\Entity\ProductInterface; use Drupal\commerce_product\Entity\ProductVariation; use Drupal\commerce_product\Event\ProductVariationAjaxChangeEvent; use Drupal\commerce_product\Event\ProductEvents; +use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\WidgetBase; @@ -29,6 +31,13 @@ abstract class ProductVariationWidgetBase extends WidgetBase implements Containe */ protected $variationStorage; + /** + * The entity repository service. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + /** * Constructs a new ProductVariationWidgetBase object. * @@ -44,10 +53,13 @@ abstract class ProductVariationWidgetBase extends WidgetBase implements Containe * Any third party settings. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); + $this->entityRepository = $entity_repository; $this->variationStorage = $entity_type_manager->getStorage('commerce_product_variation'); } @@ -61,7 +73,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], - $container->get('entity_type.manager') + $container->get('entity_type.manager'), + $container->get('entity.repository') ); } @@ -103,6 +116,11 @@ public static function ajaxRefresh(array $form, FormStateInterface $form_state) $response = $ajax_renderer->renderResponse($form, $request, $route_match); $variation = ProductVariation::load($form_state->get('selected_variation')); + /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ + $product = $form_state->get('product'); + if ($variation->hasTranslation($product->language()->getId())) { + $variation = $variation->getTranslation($product->language()->getId()); + } /** @var \Drupal\commerce_product\ProductVariationFieldRendererInterface $variation_field_renderer */ $variation_field_renderer = \Drupal::service('commerce_product.variation_field_renderer'); $view_mode = $form_state->get('form_display')->getMode(); @@ -115,4 +133,22 @@ public static function ajaxRefresh(array $form, FormStateInterface $form_state) return $response; } + /** + * Gets the enabled variations for the product. + * + * @param \Drupal\commerce_product\Entity\ProductInterface $product + * The product. + * + * @return \Drupal\commerce_product\Entity\ProductVariationInterface[] + * An array of variations. + */ + protected function loadEnabledVariations(ProductInterface $product) { + $langcode = $product->language()->getId(); + $variations = $this->variationStorage->loadEnabled($product); + foreach ($variations as $key => $variation) { + $variations[$key] = $this->entityRepository->getTranslationFromContext($variation, $langcode); + } + return $variations; + } + } diff --git a/modules/product/src/ProductLazyBuilders.php b/modules/product/src/ProductLazyBuilders.php index 1a259124b0..92db20ef3c 100644 --- a/modules/product/src/ProductLazyBuilders.php +++ b/modules/product/src/ProductLazyBuilders.php @@ -2,6 +2,7 @@ namespace Drupal\commerce_product; +use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormState; @@ -25,6 +26,13 @@ class ProductLazyBuilders { */ protected $formBuilder; + /** + * The entity repository. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + /** * Constructs a new CartLazyBuilders object. * @@ -32,10 +40,13 @@ class ProductLazyBuilders { * The entity type manager. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder * The form builder. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EntityRepositoryInterface $entity_repository) { $this->entityTypeManager = $entity_type_manager; $this->formBuilder = $form_builder; + $this->entityRepository = $entity_repository; } /** @@ -47,15 +58,20 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, For * The view mode used to render the product. * @param bool $combine * TRUE to combine order items containing the same product variation. + * @param string $langcode + * The langcode for the language that should be used in form. * * @return array * A renderable array containing the cart form. */ - public function addToCartForm($product_id, $view_mode, $combine) { + public function addToCartForm($product_id, $view_mode, $combine, $langcode) { /** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_storage */ $order_item_storage = $this->entityTypeManager->getStorage('commerce_order_item'); /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ $product = $this->entityTypeManager->getStorage('commerce_product')->load($product_id); + // Load Product for current language. + $product = $this->entityRepository->getTranslationFromContext($product, $langcode); + $default_variation = $product->getDefaultVariation(); if (!$default_variation) { return []; From 419482630c4a593b6457793b005bb22fa4720709 Mon Sep 17 00:00:00 2001 From: joachim Date: Thu, 14 Dec 2017 20:37:43 +0100 Subject: [PATCH 021/103] Issue #2930502 by joachim: add a helper method Payment::isCompleted() --- modules/payment/src/Entity/Payment.php | 7 + .../payment/src/Entity/PaymentInterface.php | 8 + .../src/Kernel/Entity/PaymentMethodTest.php | 8 +- .../tests/src/Kernel/Entity/PaymentTest.php | 148 ++++++++++++++++++ 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 modules/payment/tests/src/Kernel/Entity/PaymentTest.php diff --git a/modules/payment/src/Entity/Payment.php b/modules/payment/src/Entity/Payment.php index f887f6f957..ed56bde172 100644 --- a/modules/payment/src/Entity/Payment.php +++ b/modules/payment/src/Entity/Payment.php @@ -255,6 +255,13 @@ public function setExpiresTime($timestamp) { return $this; } + /** + * {@inheritdoc} + */ + public function isCompleted() { + return !$this->get('completed')->isEmpty(); + } + /** * {@inheritdoc} */ diff --git a/modules/payment/src/Entity/PaymentInterface.php b/modules/payment/src/Entity/PaymentInterface.php index c27acf7261..939f30212f 100644 --- a/modules/payment/src/Entity/PaymentInterface.php +++ b/modules/payment/src/Entity/PaymentInterface.php @@ -207,6 +207,14 @@ public function getExpiresTime(); */ public function setExpiresTime($timestamp); + /** + * Gets whether the payment has been completed. + * + * @return bool + * TRUE if the payment has been completed, FALSE otherwise. + */ + public function isCompleted(); + /** * Gets the payment completed timestamp. * diff --git a/modules/payment/tests/src/Kernel/Entity/PaymentMethodTest.php b/modules/payment/tests/src/Kernel/Entity/PaymentMethodTest.php index fdbdf38974..b3e2544efd 100644 --- a/modules/payment/tests/src/Kernel/Entity/PaymentMethodTest.php +++ b/modules/payment/tests/src/Kernel/Entity/PaymentMethodTest.php @@ -130,13 +130,13 @@ public function testPaymentMethod() { $payment_method->setReusable(FALSE); $this->assertEmpty($payment_method->isReusable()); - $this->assertEmpty($payment_method->isDefault()); + $this->assertFalse($payment_method->isDefault()); $payment_method->setDefault(TRUE); - $this->assertNotEmpty($payment_method->isDefault()); + $this->assertTrue($payment_method->isDefault()); - $this->assertEmpty($payment_method->isExpired()); + $this->assertFalse($payment_method->isExpired()); $payment_method->setExpiresTime(635879700); - $this->assertNotEmpty($payment_method->isExpired()); + $this->assertTrue($payment_method->isExpired()); $this->assertEquals(635879700, $payment_method->getExpiresTime()); $payment_method->setCreatedTime(635879700); diff --git a/modules/payment/tests/src/Kernel/Entity/PaymentTest.php b/modules/payment/tests/src/Kernel/Entity/PaymentTest.php new file mode 100644 index 0000000000..ecca78e91d --- /dev/null +++ b/modules/payment/tests/src/Kernel/Entity/PaymentTest.php @@ -0,0 +1,148 @@ +installEntitySchema('profile'); + $this->installEntitySchema('commerce_order'); + $this->installEntitySchema('commerce_order_item'); + $this->installEntitySchema('commerce_payment'); + $this->installConfig('commerce_order'); + $this->installConfig('commerce_payment'); + + PaymentGateway::create([ + 'id' => 'example', + 'label' => 'Example', + 'plugin' => 'example_onsite', + ])->save(); + + $user = $this->createUser(); + $this->user = $this->reloadEntity($user); + } + + /** + * @covers ::getType + * @covers ::getPaymentGatewayId + * @covers ::getPaymentGatewayMode + * @covers ::getOrder + * @covers ::getOrderId + * @covers ::getRemoteId + * @covers ::setRemoteId + * @covers ::getRemoteState + * @covers ::setRemoteState + * @covers ::getBalance + * @covers ::getAmount + * @covers ::setAmount + * @covers ::getRefundedAmount + * @covers ::setRefundedAmount + * @covers ::getState + * @covers ::setState + * @covers ::isExpired + * @covers ::getExpiresTime + * @covers ::setExpiresTime + * @covers ::isCompleted + * @covers ::getCompletedTime + * @covers ::setCompletedTime + */ + public function testPayment() { + $order = Order::create([ + 'type' => 'default', + 'mail' => $this->user->getEmail(), + 'uid' => $this->user->id(), + 'store_id' => $this->store->id(), + ]); + $order->save(); + $order = $this->reloadEntity($order); + + /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */ + $payment = Payment::create([ + 'type' => 'payment_default', + 'payment_gateway' => 'example', + 'order_id' => $order, + 'amount' => new Price('30', 'USD'), + 'refunded_amount' => new Price('10', 'USD'), + 'state' => 'refunded', + ]); + $payment->save(); + + $this->assertInstanceOf(PaymentDefault::class, $payment->getType()); + $this->assertEquals('example', $payment->getPaymentGatewayId()); + $this->assertEquals('test', $payment->getPaymentGatewayMode()); + + $this->assertEquals($order, $payment->getOrder()); + $this->assertEquals($order->id(), $payment->getOrderId()); + + $payment->setRemoteId('123456'); + $this->assertEquals('123456', $payment->getRemoteId()); + + $payment->setRemoteState('pending'); + $this->assertEquals('pending', $payment->getRemoteState()); + + $this->assertEquals(new Price('30', 'USD'), $payment->getAmount()); + $this->assertEquals(new Price('10', 'USD'), $payment->getRefundedAmount()); + $this->assertEquals(new Price('20', 'USD'), $payment->getBalance()); + + $payment->setAmount(new Price('40', 'USD')); + $this->assertEquals(new Price('40', 'USD'), $payment->getAmount()); + $payment->setRefundedAmount(new Price('15', 'USD')); + $this->assertEquals(new Price('15', 'USD'), $payment->getRefundedAmount()); + + $this->assertEquals('refunded', $payment->getState()->value); + $payment->setState('completed'); + $this->assertEquals('completed', $payment->getState()->value); + + $this->assertFalse($payment->isExpired()); + $payment->setExpiresTime(635879700); + $this->assertTrue($payment->isExpired()); + $this->assertEquals(635879700, $payment->getExpiresTime()); + + $this->assertFalse($payment->isCompleted()); + $payment->setCompletedTime(635879700); + $this->assertEquals(635879700, $payment->getCompletedTime()); + $this->assertTrue($payment->isCompleted()); + } + +} From e6deea15629c36304b16ef4b965f62cb937b882e Mon Sep 17 00:00:00 2001 From: ryross Date: Fri, 15 Dec 2017 02:50:52 +0100 Subject: [PATCH 022/103] Issue #2921896 by ryross, bojanz: Coupon field autofills with address line 1 --- modules/promotion/src/Element/CouponRedemptionForm.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/promotion/src/Element/CouponRedemptionForm.php b/modules/promotion/src/Element/CouponRedemptionForm.php index 7a2dde64d0..8022041e66 100644 --- a/modules/promotion/src/Element/CouponRedemptionForm.php +++ b/modules/promotion/src/Element/CouponRedemptionForm.php @@ -100,6 +100,11 @@ public static function processForm(array $element, FormStateInterface $form_stat '#title' => $element['#title'], '#description' => $element['#description'], '#access' => !$cardinality_reached, + // Chrome autofills this field with the address line 1, and ignores + // autocomplete => 'off', but respects 'new-password'. + '#attributes' => [ + 'autocomplete' => 'new-password', + ], ]; $element['apply'] = [ '#type' => 'submit', From 4691303f8ee38702387d040944eace4306937731 Mon Sep 17 00:00:00 2001 From: flocondetoile Date: Fri, 15 Dec 2017 03:00:52 +0100 Subject: [PATCH 023/103] Issue #2923337 by flocondetoile: Quick edit error when rendering commerce product variation fields injected into the commerce_product template --- modules/product/src/ProductViewBuilder.php | 2 ++ .../tests/src/Functional/ProductVariationFieldInjectionTest.php | 1 + 2 files changed, 3 insertions(+) diff --git a/modules/product/src/ProductViewBuilder.php b/modules/product/src/ProductViewBuilder.php index 92cb7dc5aa..a78283dfd4 100644 --- a/modules/product/src/ProductViewBuilder.php +++ b/modules/product/src/ProductViewBuilder.php @@ -78,6 +78,8 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView $attribute_field_names = $variation->getAttributeFieldNames(); $rendered_fields = $this->variationFieldRenderer->renderFields($variation, $view_mode); foreach ($rendered_fields as $field_name => $rendered_field) { + // Turn off Quick Edit for injected variation fields, to avoid warnings. + $rendered_field['#view_mode'] = '_custom'; // Group attribute fields to allow them to be excluded together. if (in_array($field_name, $attribute_field_names)) { $build['variation_attributes']['variation_' . $field_name] = $rendered_field; diff --git a/modules/product/tests/src/Functional/ProductVariationFieldInjectionTest.php b/modules/product/tests/src/Functional/ProductVariationFieldInjectionTest.php index 087e31f302..e80323d4c3 100644 --- a/modules/product/tests/src/Functional/ProductVariationFieldInjectionTest.php +++ b/modules/product/tests/src/Functional/ProductVariationFieldInjectionTest.php @@ -80,6 +80,7 @@ public function testInjectedVariationDefault() { $this->assertSession()->pageTextContains('Testing product variation field injection!'); $this->assertSession()->pageTextContains('Price'); $this->assertSession()->pageTextContains('$999.00'); + $this->assertSession()->elementNotExists('css', 'div[data-quickedit-field-id="commerce_product_variation/*"]'); // We hide the SKU by default. $this->assertSession()->pageTextNotContains('INJECTION-CYAN'); From cb3d04b7816f4e261dcb14bc157478fdec77f638 Mon Sep 17 00:00:00 2001 From: tortelduif Date: Wed, 20 Dec 2017 00:20:51 +0100 Subject: [PATCH 024/103] Issue #2926563 by alexpott, sorabh.v6, harings_rob, bojanz: Adjustment label is required but not on the form resulting PHP error --- .../Field/FieldWidget/AdjustmentDefaultWidget.php | 4 ++-- .../order/tests/src/Functional/OrderAdminTest.php | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php b/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php index a5c775838a..091cb4ce21 100644 --- a/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php +++ b/modules/order/src/Plugin/Field/FieldWidget/AdjustmentDefaultWidget.php @@ -37,7 +37,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '_none' => $this->t('- Select -'), ]; foreach ($plugin_manager->getDefinitions() as $id => $definition) { - if ($definition['has_ui'] == TRUE) { + if (!empty($definition['has_ui'])) { $types[$id] = $definition['label']; } } @@ -78,7 +78,6 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#type' => 'textfield', '#title' => $this->t('Label'), '#default_value' => ($adjustment) ? $adjustment->getLabel() : '', - '#required' => TRUE, ]; $element['definition']['amount'] = [ '#type' => 'commerce_price', @@ -112,6 +111,7 @@ public function massageFormValues(array $values, array $form, FormStateInterface // preparation for validation. Passing such data to the Adjustment // object would result in an exception. if (empty($value['definition']['label'])) { + $form_state->setErrorByName('adjustments[' . $key . '][definition][label]', $this->t('The adjustment label field is required.')); continue; } diff --git a/modules/order/tests/src/Functional/OrderAdminTest.php b/modules/order/tests/src/Functional/OrderAdminTest.php index 583270a7ef..9fa904dfee 100644 --- a/modules/order/tests/src/Functional/OrderAdminTest.php +++ b/modules/order/tests/src/Functional/OrderAdminTest.php @@ -98,15 +98,24 @@ public function testCreateOrder() { 'billing_profile[0][profile][address][0][address][postal_code]' => '94043', 'billing_profile[0][profile][address][0][address][locality]' => 'Mountain View', 'billing_profile[0][profile][address][0][address][administrative_area]' => 'CA', - // Use an adjustment that is not locked by default. + ]; + // There is no adjustment - the order should save successfully. + $this->submitForm($edit, 'Save'); + $this->assertSession()->pageTextContains('The order has been successfully saved.'); + + // Use an adjustment that is not locked by default. + $this->clickLink('Edit'); + $edit = [ 'adjustments[0][type]' => 'fee', 'adjustments[0][definition][label]' => '', 'adjustments[0][definition][amount][number]' => '2.00', ]; $this->submitForm($edit, 'Save'); - $this->assertSession()->pageTextContains('Label field is required.'); + $this->assertSession()->pageTextContains('The adjustment label field is required.'); $edit['adjustments[0][definition][label]'] = 'Test fee'; $this->submitForm($edit, 'Save'); + $this->assertSession()->pageTextContains('The order has been successfully saved.'); + $this->drupalGet('/admin/commerce/orders'); $order_number = $this->getSession()->getPage()->find('css', 'tr td.views-field-order-number'); $this->assertEquals(1, count($order_number), 'Order exists in the table.'); From e83802a28ae9539740f6a6a4b129606d8efd6151 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Wed, 20 Dec 2017 00:59:21 +0100 Subject: [PATCH 025/103] Issue #2931920 by bojanz: CreditCard::detectType() mismatch between return value and docs --- modules/payment/src/CreditCard.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/payment/src/CreditCard.php b/modules/payment/src/CreditCard.php index 91a269f532..4b41fe067c 100644 --- a/modules/payment/src/CreditCard.php +++ b/modules/payment/src/CreditCard.php @@ -126,7 +126,7 @@ public static function getTypeLabels() { */ public static function detectType($number) { if (!is_numeric($number)) { - return FALSE; + return NULL; } $types = self::getTypes(); foreach ($types as $type) { @@ -137,7 +137,7 @@ public static function detectType($number) { } } - return FALSE; + return NULL; } /** From 725c574c9f0ee11cb020b1a85369ceca6a7f46cf Mon Sep 17 00:00:00 2001 From: rszrama Date: Thu, 21 Dec 2017 00:46:04 +0100 Subject: [PATCH 026/103] Issue #2932197 by rszrama, joachim, bojanz: Expand the comments for PaymentInformation::buildPaymentMethodOptions() --- .../CheckoutPane/PaymentInformation.php | 95 +++++++++++-------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index 2b2e1b0eba..6cce21e053 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -61,6 +61,8 @@ public function buildPaneSummary() { public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) { /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */ $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway'); + // Load the payment gateways. This fires an event for filtering the + // available gateways, and then evaluates conditions on all remaining ones. $payment_gateways = $payment_gateway_storage->loadMultipleForOrder($this->order); // Can't proceed without any payment gateways. if (empty($payment_gateways)) { @@ -157,32 +159,36 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, /** * Builds the payment method options for the given payment gateways. * - * Ordering: - * 1) Stored payment methods. - * 2) The order's payment method (if not listed above). - * 3) "Create new $payment_method_type" options. - * 4) Other gateways (off-site, manual). + * The payment method options will be derived from the given payment gateways + * and added to the return array in the following order: + * 1) The customer's stored payment methods. + * 2) The order's payment method (if not added in the previous step). + * 3) Options to create new payment methods of valid types. + * 4) Options for the remaining gateways (off-site, manual, etc). * * @param \Drupal\commerce_payment\Entity\PaymentGatewayInterface[] $payment_gateways * The payment gateways. * * @return array - * The options. + * The options array keyed by payment method ID (or in the case of the new + * payment method options, a key indicating the type of payment method to + * create) whose values are associative arrays with the following keys: + * - id: the payment method ID (or new payment method key). + * - label: the label to use for selecting this payment method. + * - payment_gateway: the ID of the gateway the payment method is for. + * - payment_method: the ID of an existing stored payment method. + * - payment_method_type: the payment method type ID for new payment methods */ protected function buildPaymentMethodOptions(array $payment_gateways) { - $customer = $this->order->getCustomer(); /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface[] $payment_gateways_with_payment_methods */ $payment_gateways_with_payment_methods = array_filter($payment_gateways, function ($payment_gateway) { /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ return $payment_gateway->getPlugin() instanceof SupportsStoredPaymentMethodsInterface; }); - /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface[] $other_payment_gateways */ - $other_payment_gateways = array_diff_key($payment_gateways, $payment_gateways_with_payment_methods); - /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $order_payment_method */ - $order_payment_method = $this->order->get('payment_method')->entity; $options = []; - // 1) Stored payment methods. + // 1) Add options to reuse stored payment methods for known customers. + $customer = $this->order->getCustomer(); if ($customer) { $billing_countries = $this->order->getStore()->getBillingCountries(); /** @var \Drupal\commerce_payment\PaymentMethodStorageInterface $payment_method_storage */ @@ -190,10 +196,10 @@ protected function buildPaymentMethodOptions(array $payment_gateways) { foreach ($payment_gateways_with_payment_methods as $payment_gateway_id => $payment_gateway) { $payment_methods = $payment_method_storage->loadReusable($customer, $payment_gateway, $billing_countries); + foreach ($payment_methods as $payment_method_id => $payment_method) { - $option_id = $payment_method_id; - $options[$option_id] = [ - 'id' => $option_id, + $options[$payment_method_id] = [ + 'id' => $payment_method_id, 'label' => $payment_method->label(), 'payment_gateway' => $payment_gateway_id, 'payment_method' => $payment_method_id, @@ -201,36 +207,49 @@ protected function buildPaymentMethodOptions(array $payment_gateways) { } } } - // 2) The order's payment method (if not listed above). - if ($order_payment_method && !isset($options[$order_payment_method->id()])) { - $option_id = $order_payment_method->id(); - $options[$option_id] = [ - 'id' => $option_id, - 'label' => $order_payment_method->label(), - 'payment_gateway' => $order_payment_method->getPaymentGatewayId(), - 'payment_method' => $order_payment_method->id(), - ]; + + // 2) Add the order's payment method if it was not included above. + /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $order_payment_method */ + $order_payment_method = $this->order->get('payment_method')->entity; + if ($order_payment_method) { + $order_payment_method_id = $order_payment_method->id(); + + if (!isset($options[$order_payment_method_id])) { + $options[$order_payment_method_id] = [ + 'id' => $order_payment_method_id, + 'label' => $order_payment_method->label(), + 'payment_gateway' => $order_payment_method->getPaymentGatewayId(), + 'payment_method' => $order_payment_method_id, + ]; + } } - // 3) "Create new $payment_method_type" options. + + // 3) Add options to create new stored payment methods of supported types. $payment_method_type_counts = []; + // Count how many new payment method options will be built per gateway. foreach ($payment_gateways_with_payment_methods as $payment_gateway) { $payment_method_types = $payment_gateway->getPlugin()->getPaymentMethodTypes(); + foreach ($payment_method_types as $payment_method_type_id => $payment_method_type) { - $previous_count = 0; - if (isset($payment_method_type_counts[$payment_method_type_id])) { - $previous_count = $payment_method_type_counts[$payment_method_type_id]; - }; - $payment_method_type_counts[$payment_method_type_id] = $previous_count + 1; + if (!isset($payment_method_type_counts[$payment_method_type_id])) { + $payment_method_type_counts[$payment_method_type_id] = 1; + } + else { + $payment_method_type_counts[$payment_method_type_id]++; + } } } + foreach ($payment_gateways_with_payment_methods as $payment_gateway) { $payment_gateway_plugin = $payment_gateway->getPlugin(); $payment_method_types = $payment_gateway_plugin->getPaymentMethodTypes(); + foreach ($payment_method_types as $payment_method_type_id => $payment_method_type) { $option_id = 'new--' . $payment_method_type_id . '--' . $payment_gateway->id(); $option_label = $payment_method_type->getCreateLabel(); + // If there is more than one option for this payment method type, + // append the payment gateway label to avoid duplicate option labels. if ($payment_method_type_counts[$payment_method_type_id] > 1) { - // Append the payment gateway label to avoid duplicate labels. $option_label = $this->t('@payment_method_label (@payment_gateway_label)', [ '@payment_method_label' => $payment_method_type->getCreateLabel(), '@payment_gateway_label' => $payment_gateway_plugin->getDisplayLabel(), @@ -245,13 +264,15 @@ protected function buildPaymentMethodOptions(array $payment_gateways) { ]; } } - // 4) Other gateways (off-site, manual). - foreach ($other_payment_gateways as $payment_gateway) { - $option_id = $payment_gateway->id(); - $options[$option_id] = [ - 'id' => $option_id, + + // 4) Add options for the remaining gateways (off-site, manual, etc). + /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface[] $other_payment_gateways */ + $other_payment_gateways = array_diff_key($payment_gateways, $payment_gateways_with_payment_methods); + foreach ($other_payment_gateways as $payment_gateway_id => $payment_gateway) { + $options[$payment_gateway_id] = [ + 'id' => $payment_gateway_id, 'label' => $payment_gateway->getPlugin()->getDisplayLabel(), - 'payment_gateway' => $payment_gateway->id(), + 'payment_gateway' => $payment_gateway_id, ]; } From a28da927c519206909a97c1ba1f689b46a52d8c0 Mon Sep 17 00:00:00 2001 From: bojanz Date: Thu, 21 Dec 2017 01:38:35 +0100 Subject: [PATCH 027/103] Issue #2932158 by rynnnner, bojanz: Regression in PaymentMethodAddForm::buildPaymentMethodTypeForm() --- modules/payment/src/Form/PaymentMethodAddForm.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/payment/src/Form/PaymentMethodAddForm.php b/modules/payment/src/Form/PaymentMethodAddForm.php index d71df8a9c5..3654943606 100644 --- a/modules/payment/src/Form/PaymentMethodAddForm.php +++ b/modules/payment/src/Form/PaymentMethodAddForm.php @@ -2,7 +2,6 @@ namespace Drupal\commerce_payment\Form; -use Drupal\commerce\EntityHelper; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -102,10 +101,15 @@ public function buildForm(array $form, FormStateInterface $form_state, UserInter */ protected function buildPaymentMethodTypeForm(array $form, FormStateInterface $form_state) { $payment_method_types = $form_state->get('payment_gateway')->getPlugin()->getPaymentMethodTypes(); + $payment_method_type_options = array_map(function ($payment_method_type) { + /** @var \Drupal\commerce_payment\Plugin\Commerce\PaymentMethodType\PaymentMethodTypeInterface $payment_method_type */ + return $payment_method_type->getLabel(); + }, $payment_method_types); + $form['payment_method_type'] = [ '#type' => 'radios', '#title' => $this->t('Payment method type'), - '#options' => EntityHelper::extractLabels($payment_method_types), + '#options' => $payment_method_type_options, '#default_value' => '', '#required' => TRUE, ]; From 05ac550cf673dd9b43c524c16c712635714ba297 Mon Sep 17 00:00:00 2001 From: luksak Date: Thu, 28 Dec 2017 16:51:58 +0100 Subject: [PATCH 028/103] Issue #2920544 by Lukas von Blarer: Swiss tax rates change on 01/01/2018 --- modules/tax/src/Plugin/Commerce/TaxType/SwissVat.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/tax/src/Plugin/Commerce/TaxType/SwissVat.php b/modules/tax/src/Plugin/Commerce/TaxType/SwissVat.php index 9a4a125f97..631a9d5d2a 100644 --- a/modules/tax/src/Plugin/Commerce/TaxType/SwissVat.php +++ b/modules/tax/src/Plugin/Commerce/TaxType/SwissVat.php @@ -49,7 +49,8 @@ public function buildZones() { 'id' => 'standard', 'label' => $this->t('Standard'), 'percentages' => [ - ['number' => '0.08', 'start_date' => '2011-01-01'], + ['number' => '0.08', 'start_date' => '2011-01-01', 'end_date' => '2017-12-31'], + ['number' => '0.077', 'start_date' => '2018-01-01'], ], 'default' => TRUE, ], @@ -57,7 +58,8 @@ public function buildZones() { 'id' => 'hotel', 'label' => $this->t('Hotel'), 'percentages' => [ - ['number' => '0.038', 'start_date' => '2011-01-01'], + ['number' => '0.038', 'start_date' => '2011-01-01', 'end_date' => '2017-12-31'], + ['number' => '0.037', 'start_date' => '2018-01-01'], ], ], [ From 2f051d5b75ce68a0c858e0d63b5919d40f6a1675 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Thu, 28 Dec 2017 17:11:05 +0100 Subject: [PATCH 029/103] Issue #2933375: TaxRate::getPercentage() doesn't assume end dates are inclusive --- modules/tax/src/TaxRate.php | 17 ++++++++++------- modules/tax/tests/src/Kernel/TaxRateTest.php | 18 +++++++++++++++--- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/modules/tax/src/TaxRate.php b/modules/tax/src/TaxRate.php index 97407d3a11..e80ef769a2 100644 --- a/modules/tax/src/TaxRate.php +++ b/modules/tax/src/TaxRate.php @@ -105,14 +105,17 @@ public function getPercentages() { public function getPercentage(DrupalDateTime $date = NULL) { // Default to the current date. $date = $date ?: new DrupalDateTime(); - // Percentage start/end dates don't include the time, so discard the - // time portion of the given date to make the matching precise. - $date->setTime(0, 0); foreach ($this->percentages as $percentage) { - $start_date = $percentage->getStartDate(); - $end_date = $percentage->getEndDate(); - // Match the date against the percentage start/end dates. - if (($start_date <= $date) && (!$end_date || $end_date > $date)) { + // Unlike DateTime, DrupalDateTime objects can't be compared directly. + // Convert them to timestamps, after discarding the time portion. + $time = $date->setTime(0, 0, 0)->format('U'); + $start_time = $percentage->getStartDate()->setTime(0, 0, 0)->format('U'); + $end_time = 0; + if ($end_date = $percentage->getEndDate()) { + $end_time = $end_date->setTime(0, 0, 0)->format('U'); + } + + if (($start_time <= $time) && (!$end_time || $end_time >= $time)) { return $percentage; } } diff --git a/modules/tax/tests/src/Kernel/TaxRateTest.php b/modules/tax/tests/src/Kernel/TaxRateTest.php index 896f5e009c..ba7b837dba 100644 --- a/modules/tax/tests/src/Kernel/TaxRateTest.php +++ b/modules/tax/tests/src/Kernel/TaxRateTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\commerce_tax\Kernel; use Drupal\commerce_tax\TaxRate; +use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase; /** @@ -51,7 +52,8 @@ public function testValid() { 'id' => 'standard', 'label' => 'Standard', 'percentages' => [ - ['number' => '0.23', 'start_date' => '2012-01-01'], + ['number' => '0.23', 'start_date' => '2012-01-01', 'end_date' => '2012-12-31'], + ['number' => '0.24', 'start_date' => '2013-01-01'], ], 'default' => TRUE, ]; @@ -60,11 +62,21 @@ public function testValid() { $this->assertEquals($definition['id'], $rate->getId()); $this->assertEquals($definition['label'], $rate->getLabel()); $this->assertTrue($rate->isDefault()); - $this->assertCount(1, $rate->getPercentages()); + $this->assertCount(2, $rate->getPercentages()); - $percentage = $rate->getPercentage(); + $date = new DrupalDateTime('2012-06-30 12:00:00'); + $percentage = $rate->getPercentage($date); + $this->assertEquals($percentage, $rate->getPercentages()[0]); + $this->assertEquals($definition['percentages'][0]['number'], $percentage->getNumber()); + + $date = new DrupalDateTime('2012-12-31 17:15:00'); + $percentage = $rate->getPercentage($date); $this->assertEquals($percentage, $rate->getPercentages()[0]); $this->assertEquals($definition['percentages'][0]['number'], $percentage->getNumber()); + + $percentage = $rate->getPercentage(); + $this->assertEquals($percentage, $rate->getPercentages()[1]); + $this->assertEquals($definition['percentages'][1]['number'], $percentage->getNumber()); } } From 8c6a11df2ca0a11ba1e9dc620252e21ba1f9c66e Mon Sep 17 00:00:00 2001 From: Ritesh-Manek Date: Wed, 10 Jan 2018 15:53:52 +0100 Subject: [PATCH 030/103] Issue #2935523 by ritzz: entity-meta__title class is a string not an array --- modules/order/src/Form/OrderForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/order/src/Form/OrderForm.php b/modules/order/src/Form/OrderForm.php index 719ed12e72..3655b86db2 100644 --- a/modules/order/src/Form/OrderForm.php +++ b/modules/order/src/Form/OrderForm.php @@ -86,7 +86,7 @@ public function form(array $form, FormStateInterface $form_state) { '#tag' => 'h3', '#value' => $order->getState()->getLabel(), '#attributes' => [ - 'class' => 'entity-meta__title', + 'class' => ['entity-meta__title'], ], // Hide the rendered state if there's a widget for it. '#access' => empty($form['store_id']), From cfc6b8b250a57791c263384c9603d06d93a7ed2c Mon Sep 17 00:00:00 2001 From: joachim Date: Wed, 10 Jan 2018 19:28:37 +0100 Subject: [PATCH 031/103] Issue #2934672 by joachim: product title field should be configurable in entity view --- modules/product/src/Entity/Product.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/product/src/Entity/Product.php b/modules/product/src/Entity/Product.php index f2469355f3..cd7604345e 100644 --- a/modules/product/src/Entity/Product.php +++ b/modules/product/src/Entity/Product.php @@ -345,7 +345,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { 'type' => 'string_textfield', 'weight' => -5, ]) - ->setDisplayConfigurable('form', TRUE); + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); $fields['path'] = BaseFieldDefinition::create('path') ->setLabel(t('URL alias')) @@ -373,12 +374,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Created')) ->setDescription(t('The time when the product was created.')) ->setTranslatable(TRUE) - ->setDisplayConfigurable('view', TRUE) ->setDisplayOptions('form', [ 'type' => 'datetime_timestamp', 'weight' => 10, ]) - ->setDisplayConfigurable('form', TRUE); + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed')) From 0e5a3fbf591420bd63bd66054d20c36c45ccc580 Mon Sep 17 00:00:00 2001 From: mglaman Date: Wed, 10 Jan 2018 19:37:00 +0100 Subject: [PATCH 032/103] Issue #2884083 by mglaman, bradjones1: Product variant pages generated using ProductVariation->toUrl() don't have the right cache contexts --- modules/product/src/Entity/Product.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/product/src/Entity/Product.php b/modules/product/src/Entity/Product.php index cd7604345e..2e984208c2 100644 --- a/modules/product/src/Entity/Product.php +++ b/modules/product/src/Entity/Product.php @@ -3,6 +3,7 @@ namespace Drupal\commerce_product\Entity; use Drupal\commerce\Entity\CommerceContentEntityBase; +use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityChangedTrait; use Drupal\Core\Entity\EntityStorageInterface; @@ -288,6 +289,13 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { } } + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return Cache::mergeContexts(parent::getCacheContexts(), ['url.query_args:v']); + } + /** * {@inheritdoc} */ From a69bb37310b48f5347bbe13b8240f94cd2c371ff Mon Sep 17 00:00:00 2001 From: bojanz Date: Fri, 12 Jan 2018 19:05:57 +0100 Subject: [PATCH 033/103] Issue #2924736 by bojanz: Allow bundle config entities to be locked --- config/schema/commerce.schema.yml | 3 ++ ...erce_order.commerce_order_type.default.yml | 3 +- modules/order/src/Entity/OrderItemType.php | 2 ++ modules/order/src/Entity/OrderType.php | 4 ++- .../src/Functional/OrderItemTypeTest.php | 19 ++++++++--- .../tests/src/Functional/OrderTypeTest.php | 22 ++++++++----- ..._product.commerce_product_type.default.yml | 1 + ...ommerce_product_variation_type.default.yml | 1 + ...order.commerce_order_item_type.default.yml | 1 + modules/product/src/Entity/ProductType.php | 2 ++ .../src/Entity/ProductVariationType.php | 2 ++ .../tests/src/Functional/ProductTypeTest.php | 18 ++++++++--- .../Functional/ProductVariationTypeTest.php | 19 ++++++++--- ...merce_store.commerce_store_type.online.yml | 1 + modules/store/src/Entity/StoreType.php | 2 ++ .../tests/src/Functional/StoreTypeTest.php | 22 ++++++++----- src/CommerceBundleAccessControlHandler.php | 32 +++++++++++++++++++ src/Entity/CommerceBundleEntityBase.php | 30 +++++++++++++++++ src/Entity/CommerceBundleEntityInterface.php | 24 ++++++++++++++ ...erce_store.commerce_store_type.testing.yml | 1 + 20 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 src/CommerceBundleAccessControlHandler.php diff --git a/config/schema/commerce.schema.yml b/config/schema/commerce.schema.yml index 09f506aa93..2cf36cb421 100644 --- a/config/schema/commerce.schema.yml +++ b/config/schema/commerce.schema.yml @@ -13,6 +13,9 @@ commerce_config_entity_bundle: orderby: value sequence: type: string + locked: + type: boolean + label: 'Locked' commerce_condition: type: mapping diff --git a/modules/order/config/install/commerce_order.commerce_order_type.default.yml b/modules/order/config/install/commerce_order.commerce_order_type.default.yml index e97dade97e..a44f1b05da 100644 --- a/modules/order/config/install/commerce_order.commerce_order_type.default.yml +++ b/modules/order/config/install/commerce_order.commerce_order_type.default.yml @@ -3,8 +3,9 @@ status: true label: Default id: default workflow: order_default -traits: { } refresh_mode: customer refresh_frequency: 300 sendReceipt: true receiptBcc: '' +traits: { } +locked: false diff --git a/modules/order/src/Entity/OrderItemType.php b/modules/order/src/Entity/OrderItemType.php index e1dea40405..ea94578068 100644 --- a/modules/order/src/Entity/OrderItemType.php +++ b/modules/order/src/Entity/OrderItemType.php @@ -18,6 +18,7 @@ * plural = "@count order item types", * ), * handlers = { + * "access" = "Drupal\commerce\CommerceBundleAccessControlHandler", * "form" = { * "add" = "Drupal\commerce_order\Form\OrderItemTypeForm", * "edit" = "Drupal\commerce_order\Form\OrderItemTypeForm", @@ -42,6 +43,7 @@ * "purchasableEntityType", * "orderType", * "traits", + * "locked", * }, * links = { * "add-form" = "/admin/commerce/config/order-item-types/add", diff --git a/modules/order/src/Entity/OrderType.php b/modules/order/src/Entity/OrderType.php index 663d356187..98ccb3b50f 100644 --- a/modules/order/src/Entity/OrderType.php +++ b/modules/order/src/Entity/OrderType.php @@ -18,6 +18,7 @@ * plural = "@count order types", * ), * handlers = { + * "access" = "Drupal\commerce\CommerceBundleAccessControlHandler", * "form" = { * "add" = "Drupal\commerce_order\Form\OrderTypeForm", * "edit" = "Drupal\commerce_order\Form\OrderTypeForm", @@ -40,11 +41,12 @@ * "label", * "id", * "workflow", - * "traits", * "refresh_mode", * "refresh_frequency", * "sendReceipt", * "receiptBcc", + * "traits", + * "locked", * }, * links = { * "add-form" = "/admin/commerce/config/order-types/add", diff --git a/modules/order/tests/src/Functional/OrderItemTypeTest.php b/modules/order/tests/src/Functional/OrderItemTypeTest.php index 8b57b90452..84db88273f 100644 --- a/modules/order/tests/src/Functional/OrderItemTypeTest.php +++ b/modules/order/tests/src/Functional/OrderItemTypeTest.php @@ -67,19 +67,28 @@ public function testOrderItemTypeEditing() { * Tests deleting an order item type programmatically and through the form. */ public function testOrderItemTypeDeletion() { - $values = [ + /** @var \Drupal\commerce_order\Entity\OrderItemTypeInterface $type */ + $type = $this->createEntity('commerce_order_item_type', [ 'id' => strtolower($this->randomMachineName(8)), 'label' => $this->randomMachineName(16), 'purchasableEntityType' => 'commerce_product_variation', 'orderType' => 'default', - ]; - $order_item_type = $this->createEntity('commerce_order_item_type', $values); + ]); + + // Confirm that the delete page is not available when the type is locked. + $type->lock(); + $type->save(); + $this->drupalGet($type->toUrl('delete-form')); + $this->assertSession()->statusCodeEquals('403'); - $this->drupalGet($order_item_type->toUrl('delete-form')); + // Unlock the type, confirm that deletion works. + $type->unlock(); + $type->save(); + $this->drupalGet($type->toUrl('delete-form')); $this->assertSession()->statusCodeEquals(200); $this->assertSession()->pageTextContains(t('This action cannot be undone.')); $this->submitForm([], t('Delete')); - $order_item_type_exists = (bool) OrderItemType::load($order_item_type->id()); + $order_item_type_exists = (bool) OrderItemType::load($type->id()); $this->assertEmpty($order_item_type_exists, 'The order item type has been deleted form the database.'); } diff --git a/modules/order/tests/src/Functional/OrderTypeTest.php b/modules/order/tests/src/Functional/OrderTypeTest.php index 05eb78e7e4..c23eb95985 100644 --- a/modules/order/tests/src/Functional/OrderTypeTest.php +++ b/modules/order/tests/src/Functional/OrderTypeTest.php @@ -80,34 +80,40 @@ public function testDraftOrderRefreshSettings() { * Tests deleting an order Type through the form. */ public function testDeleteOrderType() { - // Create an order type programmaticaly. + /** @var \Drupal\commerce_order\Entity\OrderTypeInterface $type */ $type = $this->createEntity('commerce_order_type', [ 'id' => 'foo', 'label' => 'Label for foo', 'workflow' => 'order_default', ]); commerce_order_add_order_items_field($type); - - // Create an order. $order = $this->createEntity('commerce_order', [ 'type' => $type->id(), 'mail' => $this->loggedInUser->getEmail(), 'store_id' => $this->store, ]); - // Try to delete the order type. - $this->drupalGet('admin/commerce/config/order-types/' . $type->id() . '/delete'); + // Confirm that the type can't be deleted while there's an order. + $this->drupalGet($type->toUrl('delete-form')); $this->assertSession()->pageTextContains(t('@type is used by 1 order on your site. You cannot remove this order type until you have removed all of the @type orders.', ['@type' => $type->label()])); $this->assertSession()->pageTextNotContains(t('This action cannot be undone.')); - // Deleting the order type when its not being referenced by an order. + // Confirm that the delete page is not available when the type is locked. + $type->lock(); + $type->save(); + $this->drupalGet($type->toUrl('delete-form')); + $this->assertSession()->statusCodeEquals('403'); + + // Delete the order, unlock the type, confirm that deletion works. $order->delete(); - $this->drupalGet('admin/commerce/config/order-types/' . $type->id() . '/delete'); + $type->unlock(); + $type->save(); + $this->drupalGet($type->toUrl('delete-form')); $this->assertSession()->pageTextContains(t('Are you sure you want to delete the order type @label?', ['@label' => $type->label()])); $this->assertSession()->pageTextContains(t('This action cannot be undone.')); $this->submitForm([], t('Delete')); $type_exists = (bool) OrderType::load($type->id()); - $this->assertEmpty($type_exists, 'The order type has been deleted from the database.'); + $this->assertEmpty($type_exists); } } diff --git a/modules/product/config/install/commerce_product.commerce_product_type.default.yml b/modules/product/config/install/commerce_product.commerce_product_type.default.yml index 94a18c9f09..f079b36960 100644 --- a/modules/product/config/install/commerce_product.commerce_product_type.default.yml +++ b/modules/product/config/install/commerce_product.commerce_product_type.default.yml @@ -7,3 +7,4 @@ description: '' variationType: default injectVariationFields: true traits: { } +locked: false diff --git a/modules/product/config/install/commerce_product.commerce_product_variation_type.default.yml b/modules/product/config/install/commerce_product.commerce_product_variation_type.default.yml index 8b5d72a574..a47d109762 100644 --- a/modules/product/config/install/commerce_product.commerce_product_variation_type.default.yml +++ b/modules/product/config/install/commerce_product.commerce_product_variation_type.default.yml @@ -6,3 +6,4 @@ label: Default orderItemType: default generateTitle: true traits: { } +locked: false diff --git a/modules/product/config/optional/commerce_order.commerce_order_item_type.default.yml b/modules/product/config/optional/commerce_order.commerce_order_item_type.default.yml index d96a4c4b13..0f1efec75c 100644 --- a/modules/product/config/optional/commerce_order.commerce_order_item_type.default.yml +++ b/modules/product/config/optional/commerce_order.commerce_order_item_type.default.yml @@ -9,3 +9,4 @@ id: default purchasableEntityType: commerce_product_variation orderType: default traits: { } +locked: false diff --git a/modules/product/src/Entity/ProductType.php b/modules/product/src/Entity/ProductType.php index 8596ac39c2..8943c042b6 100644 --- a/modules/product/src/Entity/ProductType.php +++ b/modules/product/src/Entity/ProductType.php @@ -18,6 +18,7 @@ * plural = "@count product types", * ), * handlers = { + * "access" = "Drupal\commerce\CommerceBundleAccessControlHandler", * "list_builder" = "Drupal\commerce_product\ProductTypeListBuilder", * "form" = { * "add" = "Drupal\commerce_product\Form\ProductTypeForm", @@ -43,6 +44,7 @@ * "variationType", * "injectVariationFields", * "traits", + * "locked", * }, * links = { * "add-form" = "/admin/commerce/config/product-types/add", diff --git a/modules/product/src/Entity/ProductVariationType.php b/modules/product/src/Entity/ProductVariationType.php index c5a393d4f9..24b9828665 100644 --- a/modules/product/src/Entity/ProductVariationType.php +++ b/modules/product/src/Entity/ProductVariationType.php @@ -18,6 +18,7 @@ * plural = "@count product variation types", * ), * handlers = { + * "access" = "Drupal\commerce\CommerceBundleAccessControlHandler", * "list_builder" = "Drupal\commerce_product\ProductVariationTypeListBuilder", * "form" = { * "add" = "Drupal\commerce_product\Form\ProductVariationTypeForm", @@ -42,6 +43,7 @@ * "orderItemType", * "generateTitle", * "traits", + * "locked", * }, * links = { * "add-form" = "/admin/commerce/config/product-variation-types/add", diff --git a/modules/product/tests/src/Functional/ProductTypeTest.php b/modules/product/tests/src/Functional/ProductTypeTest.php index 380963fa78..663c9e593f 100644 --- a/modules/product/tests/src/Functional/ProductTypeTest.php +++ b/modules/product/tests/src/Functional/ProductTypeTest.php @@ -83,6 +83,7 @@ public function testProductTypeDeletion() { 'id' => 'foo', 'label' => 'foo', ]); + /** @var \Drupal\commerce_product\Entity\ProductTypeInterface $product_type */ $product_type = $this->createEntity('commerce_product_type', [ 'id' => 'foo', 'label' => 'foo', @@ -90,23 +91,32 @@ public function testProductTypeDeletion() { ]); commerce_product_add_stores_field($product_type); commerce_product_add_variations_field($product_type); - $product = $this->createEntity('commerce_product', [ 'type' => $product_type->id(), 'title' => $this->randomMachineName(), ]); + // Confirm that the type can't be deleted while there's a product. // @todo Make sure $product_type->delete() also does nothing if there's // a product of that type. Right now the check is done on the form level. - $this->drupalGet('admin/commerce/config/product-types/' . $product_type->id() . '/delete'); + $this->drupalGet($product_type->toUrl('delete-form')); $this->assertSession()->pageTextContains( t('@type is used by 1 product on your site. You cannot remove this product type until you have removed all of the @type products.', ['@type' => $product_type->label()]), 'The product type will not be deleted until all products of that type are deleted.' ); $this->assertSession()->pageTextNotContains(t('This action cannot be undone.')); + // Confirm that the delete page is not available when the type is locked. + $product_type->lock(); + $product_type->save(); + $this->drupalGet($product_type->toUrl('delete-form')); + $this->assertSession()->statusCodeEquals('403'); + + // Delete the product, unlock the type, confirm that deletion works. $product->delete(); - $this->drupalGet('admin/commerce/config/product-types/' . $product_type->id() . '/delete'); + $product_type->unlock(); + $product_type->save(); + $this->drupalGet($product_type->toUrl('delete-form')); $this->assertSession()->pageTextContains( t('Are you sure you want to delete the product type @type?', ['@type' => $product_type->label()]), 'The product type is available for deletion' @@ -114,7 +124,7 @@ public function testProductTypeDeletion() { $this->assertSession()->pageTextContains(t('This action cannot be undone.')); $this->submitForm([], 'Delete'); $exists = (bool) ProductType::load($product_type->id()); - $this->assertEmpty($exists, 'The new product type has been deleted from the database.'); + $this->assertEmpty($exists); } } diff --git a/modules/product/tests/src/Functional/ProductVariationTypeTest.php b/modules/product/tests/src/Functional/ProductVariationTypeTest.php index fd4e95ae3c..1cc317172f 100644 --- a/modules/product/tests/src/Functional/ProductVariationTypeTest.php +++ b/modules/product/tests/src/Functional/ProductVariationTypeTest.php @@ -79,6 +79,7 @@ public function testProductVariationTypeEditing() { * Tests deleting a product variation type via a form. */ public function testProductVariationTypeDeletion() { + /** @var \Drupal\commerce_product\Entity\ProductTypeInterface $variation_type */ $variation_type = $this->createEntity('commerce_product_variation_type', [ 'id' => 'foo', 'label' => 'foo', @@ -89,17 +90,25 @@ public function testProductVariationTypeDeletion() { 'title' => $this->randomMachineName(), ]); - // @todo Make sure $variation_type->delete() also does nothing if there's - // a variation of that type. Right now the check is done on the form level. - $this->drupalGet('admin/commerce/config/product-variation-types/' . $variation_type->id() . '/delete'); + // Confirm that the type can't be deleted while there's a variation. + $this->drupalGet($variation_type->toUrl('delete-form')); $this->assertSession()->pageTextContains( t('@type is used by 1 product variation on your site. You cannot remove this product variation type until you have removed all of the @type product variations.', ['@type' => $variation_type->label()]), 'The product variation type will not be deleted until all variations of that type are deleted.' ); $this->assertSession()->pageTextNotContains(t('This action cannot be undone.'), 'The product variation type deletion confirmation form is not available'); + // Confirm that the delete page is not available when the type is locked. + $variation_type->lock(); + $variation_type->save(); + $this->drupalGet($variation_type->toUrl('delete-form')); + $this->assertSession()->statusCodeEquals('403'); + + // Delete the variation, unlock the type, confirm that deletion works. $variation->delete(); - $this->drupalGet('admin/commerce/config/product-variation-types/' . $variation_type->id() . '/delete'); + $variation_type->unlock(); + $variation_type->save(); + $this->drupalGet($variation_type->toUrl('delete-form')); $this->assertSession()->pageTextContains( t('Are you sure you want to delete the product variation type @type?', ['@type' => $variation_type->label()]), 'The product variation type is available for deletion' @@ -107,7 +116,7 @@ public function testProductVariationTypeDeletion() { $this->assertSession()->pageTextContains(t('This action cannot be undone.')); $this->getSession()->getPage()->pressButton('Delete'); $exists = (bool) ProductVariationType::load($variation_type->id()); - $this->assertEmpty($exists, 'The new product variation type has been deleted from the database.'); + $this->assertEmpty($exists); } /** diff --git a/modules/store/config/install/commerce_store.commerce_store_type.online.yml b/modules/store/config/install/commerce_store.commerce_store_type.online.yml index 1d5fb95d68..20b62bf4f9 100644 --- a/modules/store/config/install/commerce_store.commerce_store_type.online.yml +++ b/modules/store/config/install/commerce_store.commerce_store_type.online.yml @@ -5,3 +5,4 @@ id: online label: Online description: '' traits: { } +locked: false diff --git a/modules/store/src/Entity/StoreType.php b/modules/store/src/Entity/StoreType.php index 943b113038..31657c9444 100644 --- a/modules/store/src/Entity/StoreType.php +++ b/modules/store/src/Entity/StoreType.php @@ -18,6 +18,7 @@ * plural = "@count store types", * ), * handlers = { + * "access" = "Drupal\commerce\CommerceBundleAccessControlHandler", * "list_builder" = "Drupal\commerce_store\StoreTypeListBuilder", * "form" = { * "add" = "Drupal\commerce_store\Form\StoreTypeForm", @@ -42,6 +43,7 @@ * "uuid", * "description", * "traits", + * "locked", * }, * links = { * "add-form" = "/admin/commerce/config/store-types/add", diff --git a/modules/store/tests/src/Functional/StoreTypeTest.php b/modules/store/tests/src/Functional/StoreTypeTest.php index 8167d69bd3..1901a7321b 100644 --- a/modules/store/tests/src/Functional/StoreTypeTest.php +++ b/modules/store/tests/src/Functional/StoreTypeTest.php @@ -97,30 +97,36 @@ public function testUpdateStoreType() { * Tests deleting a Store Type through the form. */ public function testDeleteStoreType() { - // Create a store type programmaticaly. + /** @var \Drupal\commerce_store\Entity\StoreTypeInterface $type */ $type = $this->createEntity('commerce_store_type', [ 'id' => 'foo', 'label' => 'Label for foo', ]); - - // Create a store. $store = $this->createStore(NULL, NULL, $type->id()); - // Try to delete the store type. - $this->drupalGet('admin/commerce/config/store-types/' . $type->id() . '/delete'); + // Confirm that the type can't be deleted while there's a store. + $this->drupalGet($type->toUrl('delete-form')); $this->assertSession()->pageTextContains(t('@type is used by 1 store on your site. You cannot remove this store type until you have removed all of the @type stores.', ['@type' => $type->label()])); $this->assertSession()->pageTextNotContains('This action cannot be undone.'); $this->assertSession()->pageTextNotContains('The store type deletion confirmation form is not available'); - // Deleting the store type when its not being referenced by a store. + // Confirm that the delete page is not available when the type is locked. + $type->lock(); + $type->save(); + $this->drupalGet($type->toUrl('delete-form')); + $this->assertSession()->statusCodeEquals('403'); + + // Delete the store, unlock the type, confirm that deletion works. $store->delete(); - $this->drupalGet('admin/commerce/config/store-types/' . $type->id() . '/delete'); + $type->unlock(); + $type->save(); + $this->drupalGet($type->toUrl('delete-form')); $this->assertSession()->pageTextContains(t('Are you sure you want to delete the store type @type?', ['@type' => $type->label()])); $this->saveHtmlOutput(); $this->assertSession()->pageTextContains('This action cannot be undone.'); $this->submitForm([], 'Delete'); $type_exists = (bool) StoreType::load($type->id()); - $this->assertEmpty($type_exists, 'The new store type has been deleted from the database.'); + $this->assertEmpty($type_exists); } } diff --git a/src/CommerceBundleAccessControlHandler.php b/src/CommerceBundleAccessControlHandler.php new file mode 100644 index 0000000000..9a3928d236 --- /dev/null +++ b/src/CommerceBundleAccessControlHandler.php @@ -0,0 +1,32 @@ +getEntityType()->getAdminPermission(); + if ($operation === 'delete') { + if ($entity->isLocked()) { + return AccessResult::forbidden()->addCacheableDependency($entity); + } + else { + return AccessResult::allowedIfHasPermission($account, $admin_permission)->addCacheableDependency($entity); + } + } + return AccessResult::allowedIfHasPermission($account, $admin_permission); + } + +} diff --git a/src/Entity/CommerceBundleEntityBase.php b/src/Entity/CommerceBundleEntityBase.php index 8cd369a22e..98793ce6cb 100644 --- a/src/Entity/CommerceBundleEntityBase.php +++ b/src/Entity/CommerceBundleEntityBase.php @@ -30,6 +30,13 @@ class CommerceBundleEntityBase extends ConfigEntityBundleBase implements Commerc */ protected $traits = []; + /** + * Whether the bundle is locked, indicating that it cannot be deleted. + * + * @var bool + */ + protected $locked = FALSE; + /** * {@inheritdoc} */ @@ -52,4 +59,27 @@ public function hasTrait($trait) { return in_array($trait, $this->traits); } + /** + * {@inheritdoc} + */ + public function isLocked() { + return (bool) $this->locked; + } + + /** + * {@inheritdoc} + */ + public function lock() { + $this->locked = TRUE; + return $this; + } + + /** + * {@inheritdoc} + */ + public function unlock() { + $this->locked = FALSE; + return $this; + } + } diff --git a/src/Entity/CommerceBundleEntityInterface.php b/src/Entity/CommerceBundleEntityInterface.php index 1d8b8a72ad..53c52c08a6 100644 --- a/src/Entity/CommerceBundleEntityInterface.php +++ b/src/Entity/CommerceBundleEntityInterface.php @@ -39,4 +39,28 @@ public function setTraits(array $traits); */ public function hasTrait($trait); + /** + * Gets whether the bundle is locked. + * + * Locked bundles cannot be deleted. + * + * @return bool + * TRUE if the bundle is locked, FALSE otherwise. + */ + public function isLocked(); + + /** + * Locks the bundle. + * + * @return $this + */ + public function lock(); + + /** + * Unlocks the bundle. + * + * @return $this + */ + public function unlock(); + } diff --git a/tests/modules/commerce_update_test/config/install/commerce_store.commerce_store_type.testing.yml b/tests/modules/commerce_update_test/config/install/commerce_store.commerce_store_type.testing.yml index 9eddde113e..78edd323ff 100644 --- a/tests/modules/commerce_update_test/config/install/commerce_store.commerce_store_type.testing.yml +++ b/tests/modules/commerce_update_test/config/install/commerce_store.commerce_store_type.testing.yml @@ -5,3 +5,4 @@ id: testing label: Testing description: '' traits: { } +locked: false From eb71ee2f09649239912ccf2bb6bee266246bbd40 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Mon, 15 Jan 2018 23:13:40 +0100 Subject: [PATCH 034/103] Issue #2936869: Expand adjustment type labels, add singular_label and plural_label --- .../commerce_order.commerce_adjustment_types.yml | 16 ++++++++++++---- modules/order/src/AdjustmentTypeManager.php | 8 ++++++-- .../Commerce/AdjustmentType/AdjustmentType.php | 14 ++++++++++++++ .../AdjustmentType/AdjustmentTypeInterface.php | 16 ++++++++++++++++ ...erce_order_test.commerce_adjustment_types.yml | 4 +++- 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/modules/order/commerce_order.commerce_adjustment_types.yml b/modules/order/commerce_order.commerce_adjustment_types.yml index ad929a9e08..e3d016c545 100644 --- a/modules/order/commerce_order.commerce_adjustment_types.yml +++ b/modules/order/commerce_order.commerce_adjustment_types.yml @@ -1,19 +1,27 @@ custom: - label: Custom + label: 'Custom' + singular_label: 'custom adjustment' + plural_label: 'custom adjustments' has_ui: true weight: 10 fee: - label: Fee + label: 'Fee' + singular_label: 'fee' + plural_label: 'fees' has_ui: true weight: 70 promotion: - label: Promotion + label: 'Promotion' + singular_label: 'promotion' + plural_label: 'promotions' has_ui: true weight: 0 tax: - label: Tax + label: 'Tax' + singular_label: 'tax' + plural_label: 'taxes' has_ui: true weight: 50 diff --git a/modules/order/src/AdjustmentTypeManager.php b/modules/order/src/AdjustmentTypeManager.php index f9d522c84b..11be6cf03b 100644 --- a/modules/order/src/AdjustmentTypeManager.php +++ b/modules/order/src/AdjustmentTypeManager.php @@ -26,6 +26,8 @@ class AdjustmentTypeManager extends DefaultPluginManager { protected $defaults = [ 'id' => '', 'label' => '', + 'singular_label' => '', + 'plural_label' => '', 'has_ui' => TRUE, 'weight' => 0, 'class' => AdjustmentType::class, @@ -63,8 +65,10 @@ public function processDefinition(&$definition, $plugin_id) { parent::processDefinition($definition, $plugin_id); $definition['id'] = $plugin_id; - if (empty($definition['label'])) { - throw new PluginException(sprintf('The adjustment type %s must define the label property.', $plugin_id)); + foreach (['label', 'singular_label', 'plural_label'] as $required_property) { + if (empty($definition[$required_property])) { + throw new PluginException(sprintf('The adjustment type %s must define the %s property.', $plugin_id, $required_property)); + } } } diff --git a/modules/order/src/Plugin/Commerce/AdjustmentType/AdjustmentType.php b/modules/order/src/Plugin/Commerce/AdjustmentType/AdjustmentType.php index 630b2b0cf6..ad21326788 100644 --- a/modules/order/src/Plugin/Commerce/AdjustmentType/AdjustmentType.php +++ b/modules/order/src/Plugin/Commerce/AdjustmentType/AdjustmentType.php @@ -23,6 +23,20 @@ public function getLabel() { return $this->pluginDefinition['label']; } + /** + * {@inheritdoc} + */ + public function getSingularLabel() { + return $this->pluginDefinition['singular_label']; + } + + /** + * {@inheritdoc} + */ + public function getPluralLabel() { + return $this->pluginDefinition['plural_label']; + } + /** * {@inheritdoc} */ diff --git a/modules/order/src/Plugin/Commerce/AdjustmentType/AdjustmentTypeInterface.php b/modules/order/src/Plugin/Commerce/AdjustmentType/AdjustmentTypeInterface.php index f2d276f9fd..93b34db890 100644 --- a/modules/order/src/Plugin/Commerce/AdjustmentType/AdjustmentTypeInterface.php +++ b/modules/order/src/Plugin/Commerce/AdjustmentType/AdjustmentTypeInterface.php @@ -23,6 +23,22 @@ public function getId(); */ public function getLabel(); + /** + * Gets the adjustment type singular label. + * + * @return string + * The adjustment type singular label. + */ + public function getSingularLabel(); + + /** + * Gets the adjustment type plural label. + * + * @return string + * The adjustment type plural label. + */ + public function getPluralLabel(); + /** * Gets the adjustment type weight. * diff --git a/modules/order/tests/modules/commerce_order_test/commerce_order_test.commerce_adjustment_types.yml b/modules/order/tests/modules/commerce_order_test/commerce_order_test.commerce_adjustment_types.yml index aa07a1622c..291c8ec855 100644 --- a/modules/order/tests/modules/commerce_order_test/commerce_order_test.commerce_adjustment_types.yml +++ b/modules/order/tests/modules/commerce_order_test/commerce_order_test.commerce_adjustment_types.yml @@ -1,4 +1,6 @@ test_adjustment_type: - label: Test order adjustment type + label: 'Test' + singular_label: 'test' + plural_label: 'tests' has_ui: false weight: -1 From 604f1faae970cc9d495bbc9ca512280272fb5e14 Mon Sep 17 00:00:00 2001 From: edwardahan Date: Mon, 15 Jan 2018 23:35:30 +0100 Subject: [PATCH 035/103] Issue #2936705 by edwardaa: Remove legacy fieldable attribute --- modules/order/src/Entity/Order.php | 1 - modules/order/src/Entity/OrderItem.php | 1 - modules/payment/src/Entity/Payment.php | 1 - modules/payment/src/Entity/PaymentMethod.php | 1 - modules/product/src/Entity/Product.php | 1 - modules/product/src/Entity/ProductAttributeValue.php | 1 - modules/product/src/Entity/ProductVariation.php | 1 - modules/promotion/src/Entity/Promotion.php | 1 - modules/store/src/Entity/Store.php | 1 - 9 files changed, 9 deletions(-) diff --git a/modules/order/src/Entity/Order.php b/modules/order/src/Entity/Order.php index 99eb6bef5d..7a1e303a26 100644 --- a/modules/order/src/Entity/Order.php +++ b/modules/order/src/Entity/Order.php @@ -48,7 +48,6 @@ * base_table = "commerce_order", * admin_permission = "administer commerce_order", * permission_granularity = "bundle", - * fieldable = TRUE, * entity_keys = { * "id" = "order_id", * "label" = "order_number", diff --git a/modules/order/src/Entity/OrderItem.php b/modules/order/src/Entity/OrderItem.php index 3ee87c99fd..6e2333963c 100644 --- a/modules/order/src/Entity/OrderItem.php +++ b/modules/order/src/Entity/OrderItem.php @@ -35,7 +35,6 @@ * }, * base_table = "commerce_order_item", * admin_permission = "administer commerce_order", - * fieldable = TRUE, * entity_keys = { * "id" = "order_item_id", * "uuid" = "uuid", diff --git a/modules/payment/src/Entity/Payment.php b/modules/payment/src/Entity/Payment.php index ed56bde172..5d0bf05538 100644 --- a/modules/payment/src/Entity/Payment.php +++ b/modules/payment/src/Entity/Payment.php @@ -39,7 +39,6 @@ * }, * base_table = "commerce_payment", * admin_permission = "administer commerce_payment", - * fieldable = TRUE, * entity_keys = { * "id" = "payment_id", * "bundle" = "type", diff --git a/modules/payment/src/Entity/PaymentMethod.php b/modules/payment/src/Entity/PaymentMethod.php index 89fe9b146a..6192c675bf 100644 --- a/modules/payment/src/Entity/PaymentMethod.php +++ b/modules/payment/src/Entity/PaymentMethod.php @@ -40,7 +40,6 @@ * }, * base_table = "commerce_payment_method", * admin_permission = "administer commerce_payment_method", - * fieldable = TRUE, * entity_keys = { * "id" = "method_id", * "uuid" = "uuid", diff --git a/modules/product/src/Entity/Product.php b/modules/product/src/Entity/Product.php index 2e984208c2..8ef8c7a984 100644 --- a/modules/product/src/Entity/Product.php +++ b/modules/product/src/Entity/Product.php @@ -47,7 +47,6 @@ * }, * admin_permission = "administer commerce_product", * permission_granularity = "bundle", - * fieldable = TRUE, * translatable = TRUE, * base_table = "commerce_product", * data_table = "commerce_product_field_data", diff --git a/modules/product/src/Entity/ProductAttributeValue.php b/modules/product/src/Entity/ProductAttributeValue.php index ddc9cf7931..a87596c677 100644 --- a/modules/product/src/Entity/ProductAttributeValue.php +++ b/modules/product/src/Entity/ProductAttributeValue.php @@ -29,7 +29,6 @@ * "translation" = "Drupal\content_translation\ContentTranslationHandler" * }, * admin_permission = "administer commerce_product_attribute", - * fieldable = TRUE, * translatable = TRUE, * content_translation_ui_skip = TRUE, * base_table = "commerce_product_attribute_value", diff --git a/modules/product/src/Entity/ProductVariation.php b/modules/product/src/Entity/ProductVariation.php index 249caca735..4e2b10dbe5 100644 --- a/modules/product/src/Entity/ProductVariation.php +++ b/modules/product/src/Entity/ProductVariation.php @@ -40,7 +40,6 @@ * "translation" = "Drupal\content_translation\ContentTranslationHandler" * }, * admin_permission = "administer commerce_product", - * fieldable = TRUE, * translatable = TRUE, * content_translation_ui_skip = TRUE, * base_table = "commerce_product_variation", diff --git a/modules/promotion/src/Entity/Promotion.php b/modules/promotion/src/Entity/Promotion.php index 8837a3fff6..093df11431 100644 --- a/modules/promotion/src/Entity/Promotion.php +++ b/modules/promotion/src/Entity/Promotion.php @@ -48,7 +48,6 @@ * base_table = "commerce_promotion", * data_table = "commerce_promotion_field_data", * admin_permission = "administer commerce_promotion", - * fieldable = TRUE, * translatable = TRUE, * entity_keys = { * "id" = "promotion_id", diff --git a/modules/store/src/Entity/Store.php b/modules/store/src/Entity/Store.php index 1d7c534be5..709a59e012 100644 --- a/modules/store/src/Entity/Store.php +++ b/modules/store/src/Entity/Store.php @@ -48,7 +48,6 @@ * data_table = "commerce_store_field_data", * admin_permission = "administer commerce_store", * permission_granularity = "bundle", - * fieldable = TRUE, * translatable = TRUE, * entity_keys = { * "id" = "store_id", From 5b14399cb369175a62c4b0b89f2cd064cdcf7b01 Mon Sep 17 00:00:00 2001 From: jpstacey Date: Tue, 16 Jan 2018 00:47:26 +0100 Subject: [PATCH 036/103] Issue #2915167 by MegaChriz, jp.stacey, bojanz: Add a commerce_product @ContextDefinition context provider --- modules/product/commerce_product.services.yml | 6 ++ .../ContextProvider/ProductRouteContext.php | 71 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 modules/product/src/ContextProvider/ProductRouteContext.php diff --git a/modules/product/commerce_product.services.yml b/modules/product/commerce_product.services.yml index b86b802e77..619897ee9a 100644 --- a/modules/product/commerce_product.services.yml +++ b/modules/product/commerce_product.services.yml @@ -10,3 +10,9 @@ services: commerce_product.variation_field_renderer: class: Drupal\commerce_product\ProductVariationFieldRenderer arguments: ['@entity_type.manager', '@entity_field.manager'] + + commerce_product.product_route_context: + class: Drupal\commerce_product\ContextProvider\ProductRouteContext + arguments: ['@current_route_match'] + tags: + - { name: 'context_provider' } diff --git a/modules/product/src/ContextProvider/ProductRouteContext.php b/modules/product/src/ContextProvider/ProductRouteContext.php new file mode 100644 index 0000000000..3fe5ebd0b4 --- /dev/null +++ b/modules/product/src/ContextProvider/ProductRouteContext.php @@ -0,0 +1,71 @@ +routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public function getRuntimeContexts(array $unqualified_context_ids) { + $context_definition = new ContextDefinition('entity:commerce_product', NULL, FALSE); + $value = NULL; + if ($product = $this->routeMatch->getParameter('commerce_product')) { + $value = $product; + } + elseif ($this->routeMatch->getRouteName() == 'entity.commerce_product.add_form') { + $product_type = $this->routeMatch->getParameter('commerce_product_type'); + $value = Product::create(['type' => $product_type->id()]); + } + + $cacheability = new CacheableMetadata(); + $cacheability->setCacheContexts(['route']); + $context = new Context($context_definition, $value); + $context->addCacheableDependency($cacheability); + + return ['commerce_product' => $context]; + } + + /** + * {@inheritdoc} + */ + public function getAvailableContexts() { + $context = new Context(new ContextDefinition( + 'entity:commerce_product', $this->t('Product from URL') + )); + return ['commerce_product' => $context]; + } + +} From e60673bca6662fd4149e719f9d830051d35845ef Mon Sep 17 00:00:00 2001 From: mistermoper Date: Wed, 17 Jan 2018 17:34:02 +0100 Subject: [PATCH 037/103] Issue #2932776 by mistermoper, dhwani.addweb: Prefix all dependencies in *.info.yml with the project name --- modules/cart/commerce_cart.info.yml | 4 +--- .../commerce_cart_big_pipe.info.yml | 11 +++++------ .../commerce_cart_test/commerce_cart_test.info.yml | 6 ++---- modules/checkout/commerce_checkout.info.yml | 2 +- modules/log/commerce_log.info.yml | 3 +-- modules/log/tests/module/commerce_log_test.info.yml | 4 ++-- modules/order/commerce_order.info.yml | 10 +++++----- .../commerce_order_test/commerce_order_test.info.yml | 5 ++--- modules/payment/commerce_payment.info.yml | 6 +++--- .../commerce_payment_test.info.yml | 2 +- .../payment_example/commerce_payment_example.info.yml | 1 + modules/price/commerce_price.info.yml | 2 +- .../commerce_price_test/commerce_price_test.info.yml | 4 ++-- modules/product/commerce_product.info.yml | 6 +++--- .../commerce_product_access_test.info.yml | 2 +- modules/promotion/commerce_promotion.info.yml | 6 +++--- .../commerce_promotion_test.info.yml | 4 ++-- modules/store/commerce_store.info.yml | 4 ++-- modules/tax/commerce_tax.info.yml | 1 + tests/modules/commerce_test/commerce_test.info.yml | 2 +- .../commerce_update_test.info.yml | 2 +- 21 files changed, 41 insertions(+), 46 deletions(-) diff --git a/modules/cart/commerce_cart.info.yml b/modules/cart/commerce_cart.info.yml index 4455adb992..5bea2eaa29 100644 --- a/modules/cart/commerce_cart.info.yml +++ b/modules/cart/commerce_cart.info.yml @@ -4,11 +4,9 @@ description: 'Implements the shopping cart system and add to cart features.' package: Commerce core: 8.x dependencies: - - commerce + - commerce:commerce - commerce:commerce_order - - commerce:commerce_price - commerce:commerce_product - - views config_devel: install: - core.entity_form_mode.commerce_order_item.add_to_cart diff --git a/modules/cart/tests/modules/commerce_cart_big_pipe/commerce_cart_big_pipe.info.yml b/modules/cart/tests/modules/commerce_cart_big_pipe/commerce_cart_big_pipe.info.yml index d979695850..625786da67 100644 --- a/modules/cart/tests/modules/commerce_cart_big_pipe/commerce_cart_big_pipe.info.yml +++ b/modules/cart/tests/modules/commerce_cart_big_pipe/commerce_cart_big_pipe.info.yml @@ -4,9 +4,8 @@ description: Provides a slow version of AddToCart form for testing Big Pipe stre package: Testing core: 8.x dependencies: - - commerce_store - - commerce_product - - commerce_cart - - big_pipe - - big_pipe_test - - dynamic_page_cache + - commerce:commerce_cart + - commerce:commerce_product + - drupal:big_pipe + - drupal:big_pipe_test + - drupal:dynamic_page_cache diff --git a/modules/cart/tests/modules/commerce_cart_test/commerce_cart_test.info.yml b/modules/cart/tests/modules/commerce_cart_test/commerce_cart_test.info.yml index 3e871ccf73..81976f77f9 100644 --- a/modules/cart/tests/modules/commerce_cart_test/commerce_cart_test.info.yml +++ b/modules/cart/tests/modules/commerce_cart_test/commerce_cart_test.info.yml @@ -4,7 +4,5 @@ description: Contains various non-specific things needed in tests. package: Testing core: 8.x dependencies: - - commerce_store - - commerce_product - - commerce_cart - - views + - commerce:commerce_cart + - commerce:commerce_product diff --git a/modules/checkout/commerce_checkout.info.yml b/modules/checkout/commerce_checkout.info.yml index 5f75954494..6e50a94154 100644 --- a/modules/checkout/commerce_checkout.info.yml +++ b/modules/checkout/commerce_checkout.info.yml @@ -4,7 +4,7 @@ description: 'Provides configurable checkout flows.' package: Commerce core: 8.x dependencies: - - commerce + - commerce:commerce - commerce:commerce_order - commerce:commerce_cart config_devel: diff --git a/modules/log/commerce_log.info.yml b/modules/log/commerce_log.info.yml index 8cfa7adb6d..0c41213e49 100644 --- a/modules/log/commerce_log.info.yml +++ b/modules/log/commerce_log.info.yml @@ -4,5 +4,4 @@ description: 'Provides activity logs for Commerce entities.' package: Commerce core: 8.x dependencies: - - commerce - - user + - commerce:commerce diff --git a/modules/log/tests/module/commerce_log_test.info.yml b/modules/log/tests/module/commerce_log_test.info.yml index e4c1c535c5..0054d0db35 100644 --- a/modules/log/tests/module/commerce_log_test.info.yml +++ b/modules/log/tests/module/commerce_log_test.info.yml @@ -4,5 +4,5 @@ description: 'Test module for Commerce Log.' package: Commerce core: 8.x dependencies: - - commerce_log - - entity_test + - commerce:commerce_log + - drupal:entity_test diff --git a/modules/order/commerce_order.info.yml b/modules/order/commerce_order.info.yml index d7edcd74a8..62999faa8f 100644 --- a/modules/order/commerce_order.info.yml +++ b/modules/order/commerce_order.info.yml @@ -4,13 +4,13 @@ description: 'Defines the Order entity and associated features.' package: Commerce core: 8.x dependencies: - - commerce + - commerce:commerce - commerce:commerce_price - commerce:commerce_store - - entity_reference_revisions - - options - - profile - - state_machine + - entity_reference_revisions:entity_reference_revisions + - profile:profile + - state_machine:state_machine + - drupal:options config_devel: install: - commerce_order.commerce_order_type.default diff --git a/modules/order/tests/modules/commerce_order_test/commerce_order_test.info.yml b/modules/order/tests/modules/commerce_order_test/commerce_order_test.info.yml index 2ba06d7772..87ce3479d8 100644 --- a/modules/order/tests/modules/commerce_order_test/commerce_order_test.info.yml +++ b/modules/order/tests/modules/commerce_order_test/commerce_order_test.info.yml @@ -4,6 +4,5 @@ description: Contains various non-specific things needed in order tests. package: Testing core: 8.x dependencies: - - commerce_store - - commerce_product - - commerce_order + - commerce:commerce_order + - commerce:commerce_product diff --git a/modules/payment/commerce_payment.info.yml b/modules/payment/commerce_payment.info.yml index 3c6613bce7..6707ff6e86 100644 --- a/modules/payment/commerce_payment.info.yml +++ b/modules/payment/commerce_payment.info.yml @@ -4,10 +4,10 @@ description: 'Provides payment functionality.' package: Commerce core: 8.x dependencies: + - commerce:commerce - commerce:commerce_order - - entity_reference_revisions - - filter - - user + - entity_reference_revisions:entity_reference_revisions + - drupal:filter config_devel: install: - field.storage.user.commerce_remote_id diff --git a/modules/payment/tests/modules/commerce_payment_test/commerce_payment_test.info.yml b/modules/payment/tests/modules/commerce_payment_test/commerce_payment_test.info.yml index e0d14fd9c2..ff5fb161ab 100644 --- a/modules/payment/tests/modules/commerce_payment_test/commerce_payment_test.info.yml +++ b/modules/payment/tests/modules/commerce_payment_test/commerce_payment_test.info.yml @@ -3,4 +3,4 @@ description: Provides testing items for Payment core: 8.x type: module dependencies: - - commerce_payment + - commerce:commerce_payment diff --git a/modules/payment_example/commerce_payment_example.info.yml b/modules/payment_example/commerce_payment_example.info.yml index 26bf54ab07..65bb8c9014 100644 --- a/modules/payment_example/commerce_payment_example.info.yml +++ b/modules/payment_example/commerce_payment_example.info.yml @@ -4,4 +4,5 @@ description: 'Provides payment gateway examples.' package: Commerce core: 8.x dependencies: + - commerce:commerce - commerce:commerce_payment diff --git a/modules/price/commerce_price.info.yml b/modules/price/commerce_price.info.yml index 65715e6a67..835c008ff8 100644 --- a/modules/price/commerce_price.info.yml +++ b/modules/price/commerce_price.info.yml @@ -4,4 +4,4 @@ description: 'Defines the Currency entity.' package: Commerce core: 8.x dependencies: - - commerce + - commerce:commerce diff --git a/modules/price/tests/modules/commerce_price_test/commerce_price_test.info.yml b/modules/price/tests/modules/commerce_price_test/commerce_price_test.info.yml index 85b108aa57..a486e6ebbb 100644 --- a/modules/price/tests/modules/commerce_price_test/commerce_price_test.info.yml +++ b/modules/price/tests/modules/commerce_price_test/commerce_price_test.info.yml @@ -3,5 +3,5 @@ type: module package: Testing core: 8.x dependencies: - - commerce_store - - commerce_price + - commerce:commerce_store + - commerce:commerce_price diff --git a/modules/product/commerce_product.info.yml b/modules/product/commerce_product.info.yml index 2b13ba2a55..7a38bdfa61 100644 --- a/modules/product/commerce_product.info.yml +++ b/modules/product/commerce_product.info.yml @@ -4,11 +4,11 @@ description: 'Defines the Product entity and associated features.' package: Commerce core: 8.x dependencies: - - commerce + - commerce:commerce - commerce:commerce_price - commerce:commerce_store - - path - - text + - drupal:path + - drupal:text config_devel: install: - commerce_product.commerce_product_type.default diff --git a/modules/product/tests/modules/commerce_product_access_test/commerce_product_access_test.info.yml b/modules/product/tests/modules/commerce_product_access_test/commerce_product_access_test.info.yml index aaf297c959..d7f4ce1d98 100644 --- a/modules/product/tests/modules/commerce_product_access_test/commerce_product_access_test.info.yml +++ b/modules/product/tests/modules/commerce_product_access_test/commerce_product_access_test.info.yml @@ -3,4 +3,4 @@ description: Test module for testing product and variation access core: 8.x type: module dependencies: - - commerce_product + - commerce:commerce_product diff --git a/modules/promotion/commerce_promotion.info.yml b/modules/promotion/commerce_promotion.info.yml index 11ed6052c8..17cabc3390 100644 --- a/modules/promotion/commerce_promotion.info.yml +++ b/modules/promotion/commerce_promotion.info.yml @@ -4,7 +4,7 @@ description: 'Provides a UI for managing promotions.' package: Commerce core: 8.x dependencies: - - options - - inline_entity_form - - commerce + - commerce:commerce - commerce:commerce_order + - inline_entity_form:inline_entity_form + - drupal:options diff --git a/modules/promotion/tests/modules/commerce_promotion_test/commerce_promotion_test.info.yml b/modules/promotion/tests/modules/commerce_promotion_test/commerce_promotion_test.info.yml index 23674867ea..72b0c681dc 100644 --- a/modules/promotion/tests/modules/commerce_promotion_test/commerce_promotion_test.info.yml +++ b/modules/promotion/tests/modules/commerce_promotion_test/commerce_promotion_test.info.yml @@ -4,5 +4,5 @@ description: Provides items for testing Commerce Promotion. package: Testing core: 8.x dependencies: - - commerce_cart - - commerce_promotion + - commerce:commerce_cart + - commerce:commerce_promotion diff --git a/modules/store/commerce_store.info.yml b/modules/store/commerce_store.info.yml index 70d8ab0fce..661976034a 100644 --- a/modules/store/commerce_store.info.yml +++ b/modules/store/commerce_store.info.yml @@ -4,9 +4,9 @@ description: 'Defines the Store entity and associated features.' package: Commerce core: 8.x dependencies: - - commerce + - commerce:commerce - commerce:commerce_price - - options + - drupal:options config_devel: install: - commerce_store.commerce_store_type.online diff --git a/modules/tax/commerce_tax.info.yml b/modules/tax/commerce_tax.info.yml index 8a2be9e2ad..9261619c41 100644 --- a/modules/tax/commerce_tax.info.yml +++ b/modules/tax/commerce_tax.info.yml @@ -4,4 +4,5 @@ description: 'Provides tax functionality.' package: Commerce core: 8.x dependencies: + - commerce:commerce - commerce:commerce_order diff --git a/tests/modules/commerce_test/commerce_test.info.yml b/tests/modules/commerce_test/commerce_test.info.yml index 2b9efd96c4..9d5a1b6399 100644 --- a/tests/modules/commerce_test/commerce_test.info.yml +++ b/tests/modules/commerce_test/commerce_test.info.yml @@ -4,4 +4,4 @@ description: Contains various non-specific things needed in tests. package: Testing core: 8.x dependencies: - - commerce_store + - commerce:commerce_store diff --git a/tests/modules/commerce_update_test/commerce_update_test.info.yml b/tests/modules/commerce_update_test/commerce_update_test.info.yml index f75cdad72b..a64584ef3a 100644 --- a/tests/modules/commerce_update_test/commerce_update_test.info.yml +++ b/tests/modules/commerce_update_test/commerce_update_test.info.yml @@ -4,4 +4,4 @@ description: 'Module for testing extension updates to configuration.' package: Testing core: 8.x dependencies: - - commerce_store + - commerce:commerce_store From 5d8ea6a9544f1ffefe8446540b462fee980b81c2 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Thu, 18 Jan 2018 00:44:39 +0100 Subject: [PATCH 038/103] Issue #2937523 by bojanz: Clean up EntityAccessControlHandler usage --- modules/order/src/OrderAccessControlHandler.php | 2 +- modules/product/src/Entity/Product.php | 2 +- modules/product/src/Entity/ProductAttribute.php | 2 +- modules/promotion/src/Entity/Promotion.php | 2 +- modules/store/src/Entity/Store.php | 2 +- src/CommerceBundleAccessControlHandler.php | 4 ++-- src/EntityAccessControlHandler.php | 4 ++++ 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/order/src/OrderAccessControlHandler.php b/modules/order/src/OrderAccessControlHandler.php index 7994308a9e..2063ac0895 100644 --- a/modules/order/src/OrderAccessControlHandler.php +++ b/modules/order/src/OrderAccessControlHandler.php @@ -2,10 +2,10 @@ namespace Drupal\commerce_order; -use Drupal\commerce\EntityAccessControlHandler; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\entity\EntityAccessControlHandler; /** * Controls access based on the Order entity permissions. diff --git a/modules/product/src/Entity/Product.php b/modules/product/src/Entity/Product.php index 8ef8c7a984..6479e46b60 100644 --- a/modules/product/src/Entity/Product.php +++ b/modules/product/src/Entity/Product.php @@ -28,7 +28,7 @@ * handlers = { * "event" = "Drupal\commerce_product\Event\ProductEvent", * "storage" = "Drupal\commerce\CommerceContentEntityStorage", - * "access" = "Drupal\commerce\EntityAccessControlHandler", + * "access" = "Drupal\entity\EntityAccessControlHandler", * "permission_provider" = "Drupal\commerce\EntityPermissionProvider", * "view_builder" = "Drupal\commerce_product\ProductViewBuilder", * "list_builder" = "Drupal\commerce_product\ProductListBuilder", diff --git a/modules/product/src/Entity/ProductAttribute.php b/modules/product/src/Entity/ProductAttribute.php index a0a2cc96c5..677fb3c74c 100644 --- a/modules/product/src/Entity/ProductAttribute.php +++ b/modules/product/src/Entity/ProductAttribute.php @@ -19,7 +19,7 @@ * plural = "@count product attributes", * ), * handlers = { - * "access" = "Drupal\commerce\EntityAccessControlHandler", + * "access" = "Drupal\entity\EntityAccessControlHandler", * "permission_provider" = "Drupal\commerce\EntityPermissionProvider", * "list_builder" = "Drupal\commerce_product\ProductAttributeListBuilder", * "form" = { diff --git a/modules/promotion/src/Entity/Promotion.php b/modules/promotion/src/Entity/Promotion.php index 093df11431..71fb6078f2 100644 --- a/modules/promotion/src/Entity/Promotion.php +++ b/modules/promotion/src/Entity/Promotion.php @@ -28,7 +28,7 @@ * handlers = { * "event" = "Drupal\commerce_promotion\Event\PromotionEvent", * "storage" = "Drupal\commerce_promotion\PromotionStorage", - * "access" = "Drupal\commerce\EntityAccessControlHandler", + * "access" = "Drupal\entity\EntityAccessControlHandler", * "permission_provider" = "Drupal\commerce\EntityPermissionProvider", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "list_builder" = "Drupal\commerce_promotion\PromotionListBuilder", diff --git a/modules/store/src/Entity/Store.php b/modules/store/src/Entity/Store.php index 709a59e012..df43a10327 100644 --- a/modules/store/src/Entity/Store.php +++ b/modules/store/src/Entity/Store.php @@ -27,7 +27,7 @@ * handlers = { * "event" = "Drupal\commerce_store\Event\StoreEvent", * "storage" = "Drupal\commerce_store\StoreStorage", - * "access" = "Drupal\commerce\EntityAccessControlHandler", + * "access" = "Drupal\entity\EntityAccessControlHandler", * "permission_provider" = "Drupal\commerce\EntityPermissionProvider", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "list_builder" = "Drupal\commerce_store\StoreListBuilder", diff --git a/src/CommerceBundleAccessControlHandler.php b/src/CommerceBundleAccessControlHandler.php index 9a3928d236..6d7887d9c8 100644 --- a/src/CommerceBundleAccessControlHandler.php +++ b/src/CommerceBundleAccessControlHandler.php @@ -3,14 +3,14 @@ namespace Drupal\commerce; use Drupal\Core\Access\AccessResult; -use Drupal\Core\Entity\EntityAccessControlHandler; +use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; /** * Defines the access control handler for bundles. */ -class CommerceBundleAccessControlHandler extends EntityAccessControlHandler { +class CommerceBundleAccessControlHandler extends CoreEntityAccessControlHandler { /** * {@inheritdoc} diff --git a/src/EntityAccessControlHandler.php b/src/EntityAccessControlHandler.php index adf839bab0..23d6cb848f 100644 --- a/src/EntityAccessControlHandler.php +++ b/src/EntityAccessControlHandler.php @@ -2,11 +2,15 @@ namespace Drupal\commerce; +@trigger_error('The ' . __NAMESPACE__ . '\EntityAccessControlHandler is deprecated. Instead, use \Drupal\entity\EntityAccessControlHandler', E_USER_DEPRECATED); + use Drupal\entity\EntityAccessControlHandler as BaseEntityAccessControlHandler; /** * Controls access based on the Commerce entity permissions. * * Note: This code has moved to Entity API, see the parent class. + * + * @deprecated in Commerce 2.0 */ class EntityAccessControlHandler extends BaseEntityAccessControlHandler {} From e7a7ea75cbd5fc0b9c56c9dac12a32a732031fac Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Thu, 18 Jan 2018 13:14:15 +0100 Subject: [PATCH 039/103] Issue #2937039 by bojanz: Cannot use BundleFieldDefinition; the name is already in use --- src/BundleFieldDefinition.php | 4 ++-- src/ConfigurableFieldManager.php | 8 ++++---- src/ConfigurableFieldManagerInterface.php | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/BundleFieldDefinition.php b/src/BundleFieldDefinition.php index c32d201671..0690daebdc 100644 --- a/src/BundleFieldDefinition.php +++ b/src/BundleFieldDefinition.php @@ -4,7 +4,7 @@ @trigger_error('The ' . __NAMESPACE__ . '\BundleFieldDefinition is deprecated. Instead, use \Drupal\entity\BundleFieldDefinition', E_USER_DEPRECATED); -use Drupal\entity\BundleFieldDefinition as BaseBundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition as EntityBundleFieldDefinition; /** * Provides a field definition class for bundle fields. @@ -13,4 +13,4 @@ * * @deprecated in Commerce 2.0 */ -class BundleFieldDefinition extends BaseBundleFieldDefinition {} +class BundleFieldDefinition extends EntityBundleFieldDefinition {} diff --git a/src/ConfigurableFieldManager.php b/src/ConfigurableFieldManager.php index 27cad40f83..d0dd27c25f 100644 --- a/src/ConfigurableFieldManager.php +++ b/src/ConfigurableFieldManager.php @@ -5,7 +5,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; -use Drupal\entity\BundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition as EntityBundleFieldDefinition; class ConfigurableFieldManager implements ConfigurableFieldManagerInterface { @@ -29,7 +29,7 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager) { /** * {@inheritdoc} */ - public function createField(BundleFieldDefinition $field_definition, $lock = TRUE) { + public function createField(EntityBundleFieldDefinition $field_definition, $lock = TRUE) { $field_name = $field_definition->getName(); $entity_type_id = $field_definition->getTargetEntityTypeId(); $bundle = $field_definition->getTargetBundle(); @@ -87,7 +87,7 @@ public function createField(BundleFieldDefinition $field_definition, $lock = TRU /** * {@inheritdoc} */ - public function deleteField(BundleFieldDefinition $field_definition) { + public function deleteField(EntityBundleFieldDefinition $field_definition) { $field_name = $field_definition->getName(); $entity_type_id = $field_definition->getTargetEntityTypeId(); $bundle = $field_definition->getTargetBundle(); @@ -106,7 +106,7 @@ public function deleteField(BundleFieldDefinition $field_definition) { /** * {@inheritdoc} */ - public function hasData(BundleFieldDefinition $field_definition) { + public function hasData(EntityBundleFieldDefinition $field_definition) { $field_name = $field_definition->getName(); $entity_type_id = $field_definition->getTargetEntityTypeId(); $bundle = $field_definition->getTargetBundle(); diff --git a/src/ConfigurableFieldManagerInterface.php b/src/ConfigurableFieldManagerInterface.php index 8c9bfdd45f..15a0ff0be6 100644 --- a/src/ConfigurableFieldManagerInterface.php +++ b/src/ConfigurableFieldManagerInterface.php @@ -2,7 +2,7 @@ namespace Drupal\commerce; -use Drupal\entity\BundleFieldDefinition; +use Drupal\entity\BundleFieldDefinition as EntityBundleFieldDefinition; /** * Manages configurable fields based on field definitions. @@ -25,7 +25,7 @@ interface ConfigurableFieldManagerInterface { * @throws \RuntimeException * Thrown when a field with the same name already exists. */ - public function createField(BundleFieldDefinition $field_definition, $lock = TRUE); + public function createField(EntityBundleFieldDefinition $field_definition, $lock = TRUE); /** * Deletes the configurable field created from the given field definition. @@ -39,7 +39,7 @@ public function createField(BundleFieldDefinition $field_definition, $lock = TRU * @throws \RuntimeException * Thrown when no matching field was found. */ - public function deleteField(BundleFieldDefinition $field_definition); + public function deleteField(EntityBundleFieldDefinition $field_definition); /** * Checks whether the configurable field has data. @@ -56,6 +56,6 @@ public function deleteField(BundleFieldDefinition $field_definition); * @throws \RuntimeException * Thrown when no matching field was found. */ - public function hasData(BundleFieldDefinition $field_definition); + public function hasData(EntityBundleFieldDefinition $field_definition); } From 9ffca6fd53f9e1c3e9461c1b6d7bc97d158dfc65 Mon Sep 17 00:00:00 2001 From: alexpott Date: Thu, 18 Jan 2018 14:23:51 +0100 Subject: [PATCH 040/103] Issue #2930209 by alexpott: Random fail in PaymentMethodStorageTest --- modules/payment/src/PaymentMethodStorage.php | 2 +- modules/payment/tests/src/Kernel/PaymentMethodStorageTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/payment/src/PaymentMethodStorage.php b/modules/payment/src/PaymentMethodStorage.php index 8330b3d2d7..a5f7fda686 100644 --- a/modules/payment/src/PaymentMethodStorage.php +++ b/modules/payment/src/PaymentMethodStorage.php @@ -87,7 +87,7 @@ public function loadReusable(UserInterface $account, PaymentGatewayInterface $pa ->condition($query->orConditionGroup() ->condition('expires', $this->time->getRequestTime(), '>') ->condition('expires', 0)) - ->sort('created', 'DESC'); + ->sort('method_id', 'DESC'); $result = $query->execute(); if (empty($result)) { return []; diff --git a/modules/payment/tests/src/Kernel/PaymentMethodStorageTest.php b/modules/payment/tests/src/Kernel/PaymentMethodStorageTest.php index 33a6d21e20..0932e763e8 100644 --- a/modules/payment/tests/src/Kernel/PaymentMethodStorageTest.php +++ b/modules/payment/tests/src/Kernel/PaymentMethodStorageTest.php @@ -120,7 +120,7 @@ public function testLoadReusable() { $payment_method_unlimited->save(); // Confirm that the expired payment method was not loaded. $reusable_payment_methods = $this->storage->loadReusable($this->user, $this->paymentGateway); - $this->assertEquals([$payment_method_active->id(), $payment_method_unlimited->id()], array_keys($reusable_payment_methods)); + $this->assertEquals([$payment_method_unlimited->id(), $payment_method_active->id()], array_keys($reusable_payment_methods)); // Confirm that anonymous users cannot have reusable payment methods. $payment_method_active->setOwnerId(0); From 4cb9e25327e7b40d4f2ae385866aed25f9072137 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Thu, 18 Jan 2018 15:58:03 +0100 Subject: [PATCH 041/103] Issue #2936982 by bojanz: Expand OrderNumberSubscriber docs with a better priority explanation --- .../src/EventSubscriber/OrderNumberSubscriber.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/modules/order/src/EventSubscriber/OrderNumberSubscriber.php b/modules/order/src/EventSubscriber/OrderNumberSubscriber.php index b29f7858f6..edb59fc4b4 100644 --- a/modules/order/src/EventSubscriber/OrderNumberSubscriber.php +++ b/modules/order/src/EventSubscriber/OrderNumberSubscriber.php @@ -8,8 +8,11 @@ /** * Generates the order number for placed orders. * - * Modules wishing to override this logic can register their - * own event subscriber with a higher weight (e.g. -10). + * Modules wishing to provide their own order number logic should register + * an event subscriber with a higher priority (for example, 0). + * + * Modules that need access to the generated order number should register + * an event subscriber with a lower priority (for example, -50). */ class OrderNumberSubscriber implements EventSubscriberInterface { @@ -18,13 +21,15 @@ class OrderNumberSubscriber implements EventSubscriberInterface { */ public static function getSubscribedEvents() { $events = [ - 'commerce_order.place.pre_transition' => ['setOrderNumber', -100], + 'commerce_order.place.pre_transition' => ['setOrderNumber', -30], ]; return $events; } /** - * Sets the order number, if not already set explicitly, to the order ID. + * Sets the order number to the order ID. + * + * Skipped if the order number has already been set. * * @param \Drupal\state_machine\Event\WorkflowTransitionEvent $event * The transition event. From 6a220e8b1f6be2f8bfb23122159bbbb020d94f4f Mon Sep 17 00:00:00 2001 From: alexpott Date: Mon, 29 Jan 2018 13:14:27 +0100 Subject: [PATCH 042/103] Issue #2909308 by alexpott: CouponRedemptionPaneTest fails --- .../tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php b/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php index 3faa3bad07..8cbf9a3234 100644 --- a/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php +++ b/modules/promotion/tests/src/FunctionalJavascript/CouponRedemptionPaneTest.php @@ -265,7 +265,7 @@ public function testCheckoutWithMainSubmit() { $this->getSession()->getPage()->fillField('Coupon code', $coupon->getCode()); $this->submitForm([], 'Continue to review'); - $this->assertSession()->pageTextContains('Visa ending in 1111'); + $this->assertSession()->pageTextContains('Visa ending in 9999'); $this->assertSession()->pageTextContains($coupon->getCode()); $this->assertSession()->pageTextContains('-$99.90'); $this->assertSession()->pageTextContains('$899.10'); From 612abd6b1ad48fb07cfcd064ac0e64e2eeca1cb0 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Tue, 6 Feb 2018 16:38:06 +0100 Subject: [PATCH 043/103] Issue #2922450 by joachim, bojanz: The cart should always show tax for display inclusive prices --- .../tax/src/Plugin/Commerce/TaxType/TaxTypeBase.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeBase.php b/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeBase.php index 6fed8212c1..766a1ec6e7 100644 --- a/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeBase.php +++ b/modules/tax/src/Plugin/Commerce/TaxType/TaxTypeBase.php @@ -222,17 +222,15 @@ protected function getTaxableType(OrderItemInterface $order_item) { */ protected function resolveCustomerProfile(OrderItemInterface $order_item) { $order = $order_item->getOrder(); - $store = $order->getStore(); - $prices_include_tax = $store->get('prices_include_tax')->value; $customer_profile = $order->getBillingProfile(); - // A shipping profile is prefered, when available. + // A shipping profile is preferred, when available. $event = new CustomerProfileEvent($customer_profile, $order_item); $this->eventDispatcher->dispatch(TaxEvents::CUSTOMER_PROFILE, $event); $customer_profile = $event->getCustomerProfile(); - if (!$customer_profile && $prices_include_tax) { - // The customer is still unknown, but prices include tax (VAT scenario), - // better to show the store's default tax than nothing. - $customer_profile = $this->buildStoreProfile($store); + if (!$customer_profile && $this->isDisplayInclusive()) { + // The customer is still unknown, but prices are displayed tax-inclusive + // (VAT scenario), better to show the store's default tax than nothing. + $customer_profile = $this->buildStoreProfile($order->getStore()); } return $customer_profile; From ed774b582f11b8140c108ad9dc583585ce1892c6 Mon Sep 17 00:00:00 2001 From: eclipsegc Date: Tue, 6 Feb 2018 16:53:14 +0100 Subject: [PATCH 044/103] Issue #2939048 by EclipseGc: Required order item fields will fail validation on add to cart form #ajax --- .../Field/FieldWidget/ProductVariationAttributesWidget.php | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php index 2dc1592c14..98361f868b 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php @@ -166,6 +166,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#options' => $attribute['values'], '#required' => $attribute['required'], '#default_value' => $selected_variation->getAttributeValueId($field_name), + '#limit_validation_errors' => [], '#ajax' => [ 'callback' => [get_class($this), 'ajaxRefresh'], 'wrapper' => $form['#wrapper_id'], From 90979994cf3cdf3f587b9916299897f1734cb8f2 Mon Sep 17 00:00:00 2001 From: bucefal91 Date: Wed, 7 Feb 2018 00:15:59 +0100 Subject: [PATCH 045/103] Issue #2871569 by bucefal91, bojanz: OrderTotalSummary and OrderItemTable formatters can crash when used inside Views --- .../Plugin/Field/FieldFormatter/OrderItemTable.php | 6 +++++- .../Field/FieldFormatter/OrderTotalSummary.php | 14 ++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/modules/order/src/Plugin/Field/FieldFormatter/OrderItemTable.php b/modules/order/src/Plugin/Field/FieldFormatter/OrderItemTable.php index f92d2a3c69..0a7f9aa3cf 100644 --- a/modules/order/src/Plugin/Field/FieldFormatter/OrderItemTable.php +++ b/modules/order/src/Plugin/Field/FieldFormatter/OrderItemTable.php @@ -23,14 +23,18 @@ class OrderItemTable extends FormatterBase { * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items, $langcode) { + /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ $order = $items->getEntity(); - return [ + $elements = []; + $elements[0] = [ '#type' => 'view', // @todo Allow the view to be configurable. '#name' => 'commerce_order_item_table', '#arguments' => [$order->id()], '#embed' => TRUE, ]; + + return $elements; } /** diff --git a/modules/order/src/Plugin/Field/FieldFormatter/OrderTotalSummary.php b/modules/order/src/Plugin/Field/FieldFormatter/OrderTotalSummary.php index bb88c6d977..97759e0df8 100644 --- a/modules/order/src/Plugin/Field/FieldFormatter/OrderTotalSummary.php +++ b/modules/order/src/Plugin/Field/FieldFormatter/OrderTotalSummary.php @@ -75,11 +75,17 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items, $langcode) { + /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ $order = $items->getEntity(); - return [ - '#theme' => 'commerce_order_total_summary', - '#totals' => $this->orderTotalSummary->buildTotals($order), - ]; + $elements = []; + if (!$items->isEmpty()) { + $elements[0] = [ + '#theme' => 'commerce_order_total_summary', + '#totals' => $this->orderTotalSummary->buildTotals($order), + ]; + } + + return $elements; } /** From 40c724a4ac8ede7bc568a8cfa256edb2d2e7f730 Mon Sep 17 00:00:00 2001 From: vasike Date: Wed, 7 Feb 2018 00:39:57 +0100 Subject: [PATCH 046/103] Issue #2867430 by bmcclure, vasike, bojanz, finne: Add empty text to the order items view --- modules/order/commerce_order.post_update.php | 31 +++++++++++++++ .../views.view.commerce_order_item_table.yml | 39 ++++++++++++++++++- .../tests/src/Functional/OrderAdminTest.php | 31 +++++++++++---- 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/modules/order/commerce_order.post_update.php b/modules/order/commerce_order.post_update.php index 26dd36ceba..f03caf6935 100644 --- a/modules/order/commerce_order.post_update.php +++ b/modules/order/commerce_order.post_update.php @@ -170,3 +170,34 @@ function commerce_order_post_update_6() { } } } + +/** + * Revert the 'commerce_order_item_table' view - empty text added. + */ +function commerce_order_post_update_7() { + /** @var \Drupal\commerce\Config\ConfigUpdaterInterface $config_updater */ + $config_updater = \Drupal::service('commerce.config_updater'); + + $views = [ + 'views.view.commerce_order_item_table', + ]; + $result = $config_updater->revert($views); + + $success_results = $result->getSucceeded(); + $failure_results = $result->getFailed(); + if ($success_results) { + $message = t('Succeeded:') . '
'; + foreach ($success_results as $success_message) { + $message .= $success_message . '
'; + } + $message .= '
'; + } + if ($failure_results) { + $message .= t('Failed:') . '
'; + foreach ($failure_results as $failure_message) { + $message .= $failure_message . '
'; + } + } + + return $message; +} diff --git a/modules/order/config/install/views.view.commerce_order_item_table.yml b/modules/order/config/install/views.view.commerce_order_item_table.yml index 93955666f8..d7f962ef79 100644 --- a/modules/order/config/install/views.view.commerce_order_item_table.yml +++ b/modules/order/config/install/views.view.commerce_order_item_table.yml @@ -60,6 +60,9 @@ display: description: '' columns: title: title + unit_price__number: unit_price__number + quantity: quantity + total_price__number: total_price__number info: title: sortable: false @@ -68,8 +71,29 @@ display: separator: '' empty_column: false responsive: '' + unit_price__number: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + quantity: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + total_price__number: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' default: '-1' - empty_table: false + empty_table: true row: type: fields options: @@ -347,7 +371,18 @@ display: sorts: { } header: { } footer: { } - empty: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + empty: true + tokenize: false + content: 'There are no order items yet.' + plugin_id: text_custom relationships: { } arguments: order_id: diff --git a/modules/order/tests/src/Functional/OrderAdminTest.php b/modules/order/tests/src/Functional/OrderAdminTest.php index 9fa904dfee..94760002b3 100644 --- a/modules/order/tests/src/Functional/OrderAdminTest.php +++ b/modules/order/tests/src/Functional/OrderAdminTest.php @@ -211,18 +211,12 @@ public function testUnlockOrder() { * Tests that an admin can view an order's details. */ public function testAdminOrderView() { - $order_item = $this->createEntity('commerce_order_item', [ - 'type' => 'default', - 'unit_price' => [ - 'number' => '999', - 'currency_code' => 'USD', - ], - ]); + // Start from an order without any order items. + /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ $order = $this->createEntity('commerce_order', [ 'type' => 'default', 'store_id' => $this->store->id(), 'mail' => $this->loggedInUser->getEmail(), - 'order_items' => [$order_item], 'state' => 'draft', 'uid' => $this->loggedInUser, ]); @@ -232,6 +226,10 @@ public function testAdminOrderView() { $this->assertSession()->statusCodeEquals(200); $this->assertSession()->pageTextContains($this->loggedInUser->getEmail()); + // Confirm that the order item table is showing the empty text. + $this->assertSession()->pageTextContains('There are no order items yet.'); + $this->assertSession()->pageTextNotContains('Subtotal'); + // Confirm that the transition buttons are visible and functional. $workflow = $order->getState()->getWorkflow(); $transitions = $workflow->getAllowedTransitions($order->getState()->value, $order); @@ -242,6 +240,23 @@ public function testAdminOrderView() { $this->assertSession()->buttonNotExists('Place order'); $this->assertSession()->buttonNotExists('Cancel order'); + // Add an order item, confirm that it is displayed. + $order_item = $this->createEntity('commerce_order_item', [ + 'type' => 'default', + 'unit_price' => [ + 'number' => '999', + 'currency_code' => 'USD', + ], + ]); + $order->setItems([$order_item]); + $order->save(); + + $this->drupalGet($order->toUrl()->toString()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextNotContains('There are no order items yet.'); + $this->assertSession()->pageTextContains('$999.00'); + $this->assertSession()->pageTextContains('Subtotal'); + // Logout and check that anonymous users cannot see the order admin screen // and receive a 403 error code. $this->drupalLogout(); From 97b538bb6abde4dbf063b57314a395a1294969e7 Mon Sep 17 00:00:00 2001 From: goz Date: Thu, 8 Feb 2018 11:27:08 +0100 Subject: [PATCH 047/103] Issue #2929994 by GoZ, bojanz: Clarify OrderRefreshInterface::refresh() documentation --- modules/order/src/OrderRefreshInterface.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/order/src/OrderRefreshInterface.php b/modules/order/src/OrderRefreshInterface.php index 02dd4ef3bc..2ea6d070ed 100644 --- a/modules/order/src/OrderRefreshInterface.php +++ b/modules/order/src/OrderRefreshInterface.php @@ -52,11 +52,11 @@ public function needsRefresh(OrderInterface $order); /** * Refreshes the given order. * + * Any modified order items will be automatically saved. + * The order itself will not be saved. + * * @param \Drupal\commerce_order\Entity\OrderInterface $order * The order. - * - * @return \Drupal\commerce_order\Entity\OrderInterface - * The refreshed, unsaved order. */ public function refresh(OrderInterface $order); From 034b3a9d9b62786a7e8e10960786075c1fc430db Mon Sep 17 00:00:00 2001 From: edurenye Date: Fri, 9 Feb 2018 13:57:16 +0100 Subject: [PATCH 048/103] Issue #2943525 by bojanz, edurenye: Clean up PriceCalculatedFormatter --- .../Field/FieldFormatter/PriceCalculatedFormatter.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/modules/price/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php b/modules/price/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php index f7884978b3..0234fa1f57 100644 --- a/modules/price/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php +++ b/modules/price/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php @@ -21,7 +21,7 @@ * * @FieldFormatter( * id = "commerce_price_calculated", - * label = @Translation("Calculated price"), + * label = @Translation("Calculated"), * field_types = { * "commerce_price" * } @@ -89,8 +89,8 @@ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInter parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $entity_type_manager, $number_formatter_factory); $this->chainPriceResolver = $chain_price_resolver; - $this->currentStore = $current_store; $this->currencyStorage = $entity_type_manager->getStorage('commerce_currency'); + $this->currentStore = $current_store; $this->currentUser = $current_user; } @@ -118,17 +118,16 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items, $langcode) { - $context = new Context($this->currentUser, $this->currentStore->getStore()); $elements = []; - /** @var \Drupal\commerce_price\Plugin\Field\FieldType\PriceItem $item */ - foreach ($items as $delta => $item) { + if (!$items->isEmpty()) { + $context = new Context($this->currentUser, $this->currentStore->getStore()); /** @var \Drupal\commerce\PurchasableEntityInterface $purchasable_entity */ $purchasable_entity = $items->getEntity(); $resolved_price = $this->chainPriceResolver->resolve($purchasable_entity, 1, $context); $number = $resolved_price->getNumber(); $currency = $this->currencyStorage->load($resolved_price->getCurrencyCode()); - $elements[$delta] = [ + $elements[0] = [ '#markup' => $this->numberFormatter->formatCurrency($number, $currency), '#cache' => [ 'tags' => $purchasable_entity->getCacheTags(), From d006d1951aff50cffac706691fc9206b764851eb Mon Sep 17 00:00:00 2001 From: lisastreeter Date: Thu, 18 Jan 2018 18:30:13 +0100 Subject: [PATCH 049/103] Issue #2805549 by lisastreeter, edurenye, mglaman, flocondetoile, bojanz: Expand the "Calculated price" formatter with the ability to show prices with promotions/taxes/fees --- modules/order/commerce_order.module | 14 +- modules/order/commerce_order.services.yml | 4 + .../src/CommerceOrderServiceProvider.php | 21 ++ .../Compiler/PriceCalculatorPass.php | 54 +++++ .../PriceCalculatedFormatter.php | 211 +++++++++++++++++ modules/order/src/PriceCalculator.php | 165 +++++++++++++ .../order/src/PriceCalculatorInterface.php | 60 +++++ .../commerce_order_test.services.yml | 2 +- .../src/TestAdjustmentProcessor.php | 11 + .../PriceCalculatedFormatterTest.php | 198 ++++++++++++++++ .../tests/src/Kernel/PriceCalculatorTest.php | 223 ++++++++++++++++++ .../config/schema/commerce_price.schema.yml | 7 + .../promotion/commerce_promotion.services.yml | 2 +- modules/tax/commerce_tax.services.yml | 2 +- src/Context.php | 2 +- 15 files changed, 971 insertions(+), 5 deletions(-) create mode 100644 modules/order/src/CommerceOrderServiceProvider.php create mode 100644 modules/order/src/DependencyInjection/Compiler/PriceCalculatorPass.php create mode 100644 modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php create mode 100644 modules/order/src/PriceCalculator.php create mode 100644 modules/order/src/PriceCalculatorInterface.php create mode 100644 modules/order/tests/src/Kernel/Formatter/PriceCalculatedFormatterTest.php create mode 100644 modules/order/tests/src/Kernel/PriceCalculatorTest.php diff --git a/modules/order/commerce_order.module b/modules/order/commerce_order.module index 77468d8eb0..195092dbc8 100644 --- a/modules/order/commerce_order.module +++ b/modules/order/commerce_order.module @@ -5,10 +5,11 @@ * Defines the Order entity and associated features. */ -use Drupal\entity\BundleFieldDefinition; use Drupal\commerce_order\Entity\OrderTypeInterface; +use Drupal\commerce_order\Plugin\Field\FieldFormatter\PriceCalculatedFormatter; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; +use Drupal\entity\BundleFieldDefinition; /** * Implements hook_theme(). @@ -56,6 +57,17 @@ function commerce_order_local_tasks_alter(&$definitions) { } } +/** + * Implements hook_field_formatter_info_alter(). + * + * Replaces the commerce_price PriceCalculatedFormatter with + * the expanded commerce_order one. + */ +function commerce_order_field_formatter_info_alter(array &$info) { + $info['commerce_price_calculated']['class'] = PriceCalculatedFormatter::class; + $info['commerce_price_calculated']['provider'] = 'commerce_order'; +} + /** * Implements hook_field_widget_form_alter(). * diff --git a/modules/order/commerce_order.services.yml b/modules/order/commerce_order.services.yml index 8d8e5d4914..8d7e5166cf 100644 --- a/modules/order/commerce_order.services.yml +++ b/modules/order/commerce_order.services.yml @@ -62,3 +62,7 @@ services: arguments: ['@current_route_match'] tags: - { name: commerce_store.store_resolver, priority: 100 } + + commerce_order.price_calculator: + class: Drupal\commerce_order\PriceCalculator + arguments: ['@entity_type.manager', '@commerce_price.chain_price_resolver', '@commerce_order.chain_order_type_resolver', '@request_stack'] diff --git a/modules/order/src/CommerceOrderServiceProvider.php b/modules/order/src/CommerceOrderServiceProvider.php new file mode 100644 index 0000000000..8422078d47 --- /dev/null +++ b/modules/order/src/CommerceOrderServiceProvider.php @@ -0,0 +1,21 @@ +addCompilerPass(new PriceCalculatorPass()); + } + +} diff --git a/modules/order/src/DependencyInjection/Compiler/PriceCalculatorPass.php b/modules/order/src/DependencyInjection/Compiler/PriceCalculatorPass.php new file mode 100644 index 0000000000..de4d064360 --- /dev/null +++ b/modules/order/src/DependencyInjection/Compiler/PriceCalculatorPass.php @@ -0,0 +1,54 @@ +getDefinition('commerce_order.price_calculator'); + $processor_interface = OrderProcessorInterface::class; + $processors = []; + foreach ($container->findTaggedServiceIds('commerce_order.order_processor') as $id => $attributes) { + $processor = $container->getDefinition($id); + if (!is_subclass_of($processor->getClass(), $processor_interface)) { + throw new LogicException("Service '$id' does not implement $processor_interface."); + } + $attribute = $attributes[0]; + if (empty($attribute['adjustment_type'])) { + continue; + } + + $processors[$id] = [ + 'priority' => isset($attribute['priority']) ? $attribute['priority'] : 0, + 'adjustment_type' => $attribute['adjustment_type'], + ]; + } + + // Sort the processors by priority. + uasort($processors, function ($processor1, $processor2) { + if ($processor1['priority'] == $processor2['priority']) { + return 0; + } + return ($processor1['priority'] > $processor2['priority']) ? -1 : 1; + }); + + // Add the processors to PriceCalculator. + foreach ($processors as $id => $processor) { + $arguments = [new Reference($id), $processor['adjustment_type']]; + $definition->addMethodCall('addProcessor', $arguments); + } + } + +} diff --git a/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php b/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php new file mode 100644 index 0000000000..086fb3d9b3 --- /dev/null +++ b/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php @@ -0,0 +1,211 @@ +adjustmentTypeManager = $adjustment_type_manager; + $this->currencyStorage = $entity_type_manager->getStorage('commerce_currency'); + $this->currentStore = $current_store; + $this->currentUser = $current_user; + $this->priceCalculator = $price_calculator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('entity_type.manager'), + $container->get('commerce_price.number_formatter_factory'), + $container->get('plugin.manager.commerce_adjustment_type'), + $container->get('commerce_store.current_store'), + $container->get('current_user'), + $container->get('commerce_order.price_calculator') + ); + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'adjustment_types' => [], + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $elements = parent::settingsForm($form, $form_state); + + $elements['adjustment_types'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Adjustments'), + '#options' => [], + '#default_value' => $this->getSetting('adjustment_types'), + ]; + foreach ($this->adjustmentTypeManager->getDefinitions() as $plugin_id => $definition) { + if (!in_array($plugin_id, ['custom'])) { + $label = $this->t('Apply @label to the calculated price', ['@label' => $definition['plural_label']]); + $elements['adjustment_types']['#options'][$plugin_id] = $label; + } + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + $enabled_adjustment_types = array_filter($this->getSetting('adjustment_types')); + foreach ($this->adjustmentTypeManager->getDefinitions() as $plugin_id => $definition) { + if (in_array($plugin_id, $enabled_adjustment_types)) { + $summary[] = $this->t('Apply @label to the calculated price', ['@label' => $definition['plural_label']]); + } + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + if (!$items->isEmpty()) { + $context = new Context($this->currentUser, $this->currentStore->getStore()); + /** @var \Drupal\commerce\PurchasableEntityInterface $purchasable_entity */ + $purchasable_entity = $items->getEntity(); + $adjustment_types = array_filter($this->getSetting('adjustment_types')); + $result = $this->priceCalculator->calculate($purchasable_entity, 1, $context, $adjustment_types); + /** @var \Drupal\commerce_price\Price $calculated_price */ + $calculated_price = $result['calculated_price']; + $number = $calculated_price->getNumber(); + /** @var \Drupal\commerce_price\Entity\CurrencyInterface $currency */ + $currency = $this->currencyStorage->load($calculated_price->getCurrencyCode()); + + $elements[0] = [ + '#markup' => $this->numberFormatter->formatCurrency($number, $currency), + '#cache' => [ + 'tags' => $purchasable_entity->getCacheTags(), + 'contexts' => Cache::mergeContexts($purchasable_entity->getCacheContexts(), [ + 'languages:' . LanguageInterface::TYPE_INTERFACE, + 'country', + ]), + ], + ]; + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + $entity_type = \Drupal::entityTypeManager()->getDefinition($field_definition->getTargetEntityTypeId()); + return $entity_type->entityClassImplements(PurchasableEntityInterface::class); + } + +} diff --git a/modules/order/src/PriceCalculator.php b/modules/order/src/PriceCalculator.php new file mode 100644 index 0000000000..139682f84b --- /dev/null +++ b/modules/order/src/PriceCalculator.php @@ -0,0 +1,165 @@ +entityTypeManager = $entity_type_manager; + $this->chainPriceResolver = $chain_price_resolver; + $this->orderTypeResolver = $order_type_resolver; + $this->requestStack = $request_stack; + } + + /** + * {@inheritdoc} + */ + public function addProcessor(OrderProcessorInterface $processor, $adjustment_type) { + $this->processors[$adjustment_type][] = $processor; + } + + /** + * {@inheritdoc} + */ + public function getProcessors($adjustment_type) { + if (!isset($this->processors[$adjustment_type])) { + return []; + } + return $this->processors[$adjustment_type]; + } + + /** + * {@inheritdoc} + */ + public function calculate(PurchasableEntityInterface $purchasable_entity, $quantity, Context $context, array $adjustment_types = []) { + $resolved_price = $this->chainPriceResolver->resolve($purchasable_entity, $quantity, $context); + $processors = []; + foreach ($adjustment_types as $adjustment_type) { + $processors = array_merge($processors, $this->getProcessors($adjustment_type)); + } + if (empty($adjustment_types) || empty($processors)) { + return [ + 'calculated_price' => $resolved_price, + 'base_price' => $resolved_price, + 'adjustments' => [], + ]; + } + + /** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_storage */ + $order_item_storage = $this->entityTypeManager->getStorage('commerce_order_item'); + $order_item = $order_item_storage->createFromPurchasableEntity($purchasable_entity); + $order_item->setUnitPrice($resolved_price); + $order_item->setQuantity($quantity); + $order_type_id = $this->orderTypeResolver->resolve($order_item); + + $order = $this->prepareOrder($order_type_id, $context); + $order_item->order_id = $order; + $order->setItems([$order_item]); + // Allow each selected processor to add its adjustments. + foreach ($processors as $processor) { + $processor->process($order); + } + + return [ + 'calculated_price' => $order_item->getAdjustedUnitPrice(), + 'base_price' => $resolved_price, + 'adjustments' => $order_item->getAdjustments(), + ]; + } + + /** + * Prepares an unsaved order for the given type/context. + * + * @param string $order_type_id + * The order type ID. + * @param \Drupal\commerce\Context $context + * The context. + * + * @return \Drupal\commerce_order\Entity\OrderInterface + * The order. + */ + protected function prepareOrder($order_type_id, Context $context) { + if (!isset($this->orders[$order_type_id])) { + $order_storage = $this->entityTypeManager->getStorage('commerce_order'); + $this->orders[$order_type_id] = $order_storage->create([ + 'type' => $order_type_id, + 'ip_address' => $this->requestStack->getCurrentRequest()->getClientIp(), + // Provide a flag that can be used in the order create hook/event + // to identify orders used for price calculation purposes. + 'data' => ['provider' => 'order_price_calculator'], + ]); + } + + $order = $this->orders[$order_type_id]; + // Make sure that the order data matches the data passed in the context. + $order->setStoreId($context->getStore()->id()); + $order->setCustomerId($context->getCustomer()->id()); + $order->setEmail($context->getCustomer()->getEmail()); + // Start from a clear set of adjustments each time. + $order->clearAdjustments(); + + return $order; + } + +} diff --git a/modules/order/src/PriceCalculatorInterface.php b/modules/order/src/PriceCalculatorInterface.php new file mode 100644 index 0000000000..3d11755ff0 --- /dev/null +++ b/modules/order/src/PriceCalculatorInterface.php @@ -0,0 +1,60 @@ +addAdjustment($test_adjustment); } + + // Add adjustment for PriceCalculatorTest. + if ($order->getEmail() == 'user2@example.com') { + $order_item->addAdjustment(new Adjustment([ + 'type' => 'test_adjustment_type', + 'label' => '$2.00 fee', + 'amount' => new Price('2.00', 'USD'), + ])); + } } $test_adjustments = $order->getData('test_adjustments', []); diff --git a/modules/order/tests/src/Kernel/Formatter/PriceCalculatedFormatterTest.php b/modules/order/tests/src/Kernel/Formatter/PriceCalculatedFormatterTest.php new file mode 100644 index 0000000000..bb19bcb492 --- /dev/null +++ b/modules/order/tests/src/Kernel/Formatter/PriceCalculatedFormatterTest.php @@ -0,0 +1,198 @@ +installEntitySchema('profile'); + $this->installEntitySchema('commerce_order'); + $this->installEntitySchema('commerce_order_item'); + $this->installEntitySchema('commerce_product'); + $this->installEntitySchema('commerce_product_variation'); + $this->installEntitySchema('commerce_promotion'); + $this->installConfig(['commerce_product', 'commerce_order']); + + $promotion = Promotion::create([ + 'name' => 'Promotion 1', + 'order_types' => ['default'], + 'stores' => [$this->store->id()], + 'status' => TRUE, + 'offer' => [ + 'target_plugin_id' => 'order_item_percentage_off', + 'target_plugin_configuration' => [ + 'percentage' => '0.5', + ], + ], + ]); + $promotion->save(); + + // The default store is US-WI, so imagine that the US has VAT. + TaxType::create([ + 'id' => 'us_vat', + 'label' => 'US VAT', + 'plugin' => 'custom', + 'configuration' => [ + 'display_inclusive' => TRUE, + 'rates' => [ + [ + 'id' => 'standard', + 'label' => 'Standard', + 'percentage' => '0.2', + ], + ], + 'territories' => [ + ['country_code' => 'US', 'administrative_area' => 'WI'], + ['country_code' => 'US', 'administrative_area' => 'SC'], + ], + ], + ])->save(); + + $first_variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => 'TEST_CALCULATED_PRICE', + 'status' => 1, + 'price' => new Price('3.00', 'USD'), + ]); + $first_variation->save(); + + $second_variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => 'TEST_CALCULATED_PRICE2', + 'status' => 1, + 'price' => new Price('4.00', 'USD'), + ]); + $second_variation->save(); + + $product = Product::create([ + 'type' => 'default', + 'title' => 'Default testing product', + 'stores' => [$this->store->id()], + 'variations' => [$first_variation, $second_variation], + ]); + $product->save(); + + $this->firstVariation = $this->reloadEntity($first_variation); + $this->secondVariation = $this->reloadEntity($second_variation); + + $user = $this->createUser(['mail' => 'user1@example.com']); + $this->container->get('current_user')->setAccount($user); + + $this->viewBuilder = $this->container->get('entity_type.manager')->getViewBuilder('commerce_product_variation'); + } + + /** + * Tests the rendered output. + */ + public function testRender() { + $variation_display = commerce_get_entity_display('commerce_product_variation', 'default', 'view'); + $variation_display->setComponent('price', [ + 'label' => 'above', + 'type' => 'commerce_price_calculated', + 'settings' => [], + ]); + $variation_display->save(); + + $variation_build = $this->viewBuilder->view($this->firstVariation); + $this->render($variation_build); + $this->assertEscaped('$3.00'); + + $variation_build = $this->viewBuilder->view($this->secondVariation); + $this->render($variation_build); + $this->assertEscaped('$4.00'); + + $variation_display->setComponent('price', [ + 'label' => 'above', + 'type' => 'commerce_price_calculated', + 'settings' => [ + 'adjustment_types' => [ + 'tax' => 'tax', + ], + ], + ]); + $variation_display->save(); + + $variation_build = $this->viewBuilder->view($this->firstVariation); + $this->render($variation_build); + $this->assertEscaped('$3.60'); + + $variation_build = $this->viewBuilder->view($this->secondVariation); + $this->render($variation_build); + $this->assertEscaped('$4.80'); + + $variation_display->setComponent('price', [ + 'label' => 'above', + 'type' => 'commerce_price_calculated', + 'settings' => [ + 'adjustment_types' => [ + 'tax' => 'tax', + 'promotion' => 'promotion', + ], + ], + ]); + $variation_display->save(); + + $variation_build = $this->viewBuilder->view($this->firstVariation); + $this->render($variation_build); + $this->assertEscaped('$1.80'); + + $variation_build = $this->viewBuilder->view($this->secondVariation); + $this->render($variation_build); + $this->assertEscaped('$2.40'); + } + +} diff --git a/modules/order/tests/src/Kernel/PriceCalculatorTest.php b/modules/order/tests/src/Kernel/PriceCalculatorTest.php new file mode 100644 index 0000000000..d83e13a4d6 --- /dev/null +++ b/modules/order/tests/src/Kernel/PriceCalculatorTest.php @@ -0,0 +1,223 @@ +installEntitySchema('profile'); + $this->installEntitySchema('commerce_order'); + $this->installEntitySchema('commerce_order_item'); + $this->installEntitySchema('commerce_product'); + $this->installEntitySchema('commerce_product_variation'); + $this->installEntitySchema('commerce_promotion'); + $this->installConfig(['commerce_product', 'commerce_order']); + + $promotion = Promotion::create([ + 'name' => 'Promotion 1', + 'order_types' => ['default'], + 'stores' => [$this->store->id()], + 'status' => TRUE, + 'offer' => [ + 'target_plugin_id' => 'order_item_percentage_off', + 'target_plugin_configuration' => [ + 'percentage' => '0.5', + ], + ], + ]); + $promotion->save(); + + // The default store is US-WI, so imagine that the US has VAT. + TaxType::create([ + 'id' => 'us_vat', + 'label' => 'US VAT', + 'plugin' => 'custom', + 'configuration' => [ + 'display_inclusive' => TRUE, + 'rates' => [ + [ + 'id' => 'standard', + 'label' => 'Standard', + 'percentage' => '0.2', + ], + ], + 'territories' => [ + ['country_code' => 'US', 'administrative_area' => 'WI'], + ['country_code' => 'US', 'administrative_area' => 'SC'], + ], + ], + ])->save(); + + $first_variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => 'TEST_CALCULATED_PRICE', + 'status' => 1, + 'price' => new Price('3.00', 'USD'), + ]); + $first_variation->save(); + + $second_variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => 'TEST_CALCULATED_PRICE2', + 'status' => 1, + 'price' => new Price('4.00', 'USD'), + ]); + $second_variation->save(); + + $product = Product::create([ + 'type' => 'default', + 'title' => 'Default testing product', + 'stores' => [$this->store->id()], + 'variations' => [$first_variation, $second_variation], + ]); + $product->save(); + + $this->firstVariation = $this->reloadEntity($first_variation); + $this->secondVariation = $this->reloadEntity($second_variation); + + $this->firstUser = $this->createUser(['mail' => 'user1@example.com']); + $this->secondUser = $this->createUser(['mail' => 'user2@example.com']); + + $this->priceCalculator = $this->container->get('commerce_order.price_calculator'); + } + + /** + * Tests the calculator. + */ + public function testCalculation() { + $first_context = new Context($this->firstUser, $this->store); + $second_context = new Context($this->secondUser, $this->store); + + // No adjustment types specified. + $result = $this->priceCalculator->calculate($this->firstVariation, 1, $first_context); + $this->assertEquals(new Price('3.00', 'USD'), $result['calculated_price']); + $this->assertEquals(new Price('3.00', 'USD'), $result['base_price']); + $this->assertEquals([], $result['adjustments']); + + // Unknown adjustment type specified. + $result = $this->priceCalculator->calculate($this->secondVariation, 1, $first_context, ['invalid']); + $this->assertEquals(new Price('4.00', 'USD'), $result['calculated_price']); + $this->assertEquals(new Price('4.00', 'USD'), $result['base_price']); + $this->assertEquals([], $result['adjustments']); + + // Only tax. + $result = $this->priceCalculator->calculate($this->firstVariation, 1, $first_context, ['tax']); + $this->assertEquals(new Price('3.60', 'USD'), $result['calculated_price']); + $this->assertEquals(new Price('3.00', 'USD'), $result['base_price']); + $this->assertCount(1, $result['adjustments']); + $first_adjustment = reset($result['adjustments']); + $this->assertEquals('tax', $first_adjustment->getType()); + $this->assertEquals(new Price('0.60', 'USD'), $first_adjustment->getAmount()); + + $result = $this->priceCalculator->calculate($this->secondVariation, 1, $first_context, ['tax']); + $this->assertEquals(new Price('4.80', 'USD'), $result['calculated_price']); + $this->assertEquals(new Price('4.00', 'USD'), $result['base_price']); + $this->assertCount(1, $result['adjustments']); + $first_adjustment = reset($result['adjustments']); + $this->assertEquals('tax', $first_adjustment->getType()); + $this->assertEquals(new Price('0.80', 'USD'), $first_adjustment->getAmount()); + + // Tax and promotions. + $result = $this->priceCalculator->calculate($this->firstVariation, 1, $first_context, ['tax', 'promotion']); + $this->assertEquals(new Price('1.80', 'USD'), $result['calculated_price']); + $this->assertEquals(new Price('3.00', 'USD'), $result['base_price']); + $this->assertCount(2, $result['adjustments']); + $first_adjustment = reset($result['adjustments']); + $this->assertEquals('tax', $first_adjustment->getType()); + $this->assertEquals(new Price('0.60', 'USD'), $first_adjustment->getAmount()); + $second_adjustment = end($result['adjustments']); + $this->assertEquals('promotion', $second_adjustment->getType()); + $this->assertEquals(new Price('-1.80', 'USD'), $second_adjustment->getAmount()); + + $result = $this->priceCalculator->calculate($this->secondVariation, 1, $first_context, ['tax', 'promotion']); + $this->assertEquals(new Price('2.40', 'USD'), $result['calculated_price']); + $this->assertEquals(new Price('4.00', 'USD'), $result['base_price']); + $this->assertCount(2, $result['adjustments']); + $first_adjustment = reset($result['adjustments']); + $this->assertEquals('tax', $first_adjustment->getType()); + $this->assertEquals(new Price('0.80', 'USD'), $first_adjustment->getAmount()); + $second_adjustment = end($result['adjustments']); + $this->assertEquals('promotion', $second_adjustment->getType()); + $this->assertEquals(new Price('-2.40', 'USD'), $second_adjustment->getAmount()); + + // User-specific adjustment added by TestAdjustmentProcessor. + $result = $this->priceCalculator->calculate($this->secondVariation, 1, $second_context, ['test_adjustment_type']); + $this->assertEquals(new Price('6.00', 'USD'), $result['calculated_price']); + $this->assertEquals(new Price('4.00', 'USD'), $result['base_price']); + $this->assertCount(1, $result['adjustments']); + $first_adjustment = reset($result['adjustments']); + $this->assertEquals('test_adjustment_type', $first_adjustment->getType()); + $this->assertEquals(new Price('2.00', 'USD'), $first_adjustment->getAmount()); + } + +} diff --git a/modules/price/config/schema/commerce_price.schema.yml b/modules/price/config/schema/commerce_price.schema.yml index 9c812638e1..cb2bf12bf2 100644 --- a/modules/price/config/schema/commerce_price.schema.yml +++ b/modules/price/config/schema/commerce_price.schema.yml @@ -61,3 +61,10 @@ field.formatter.settings.commerce_price_calculated: display_currency_code: type: boolean label: 'Display the currency code instead of the currency symbol' + # Needed by the commerce_order version of the formatter. + adjustment_types: + type: sequence + label: 'Adjustment types' + sequence: + type: string + label: 'Adjustment type' diff --git a/modules/promotion/commerce_promotion.services.yml b/modules/promotion/commerce_promotion.services.yml index 565cdf54d3..7997432091 100644 --- a/modules/promotion/commerce_promotion.services.yml +++ b/modules/promotion/commerce_promotion.services.yml @@ -7,7 +7,7 @@ services: class: Drupal\commerce_promotion\PromotionOrderProcessor arguments: ['@entity_type.manager'] tags: - - { name: commerce_order.order_processor, priority: 50 } + - { name: commerce_order.order_processor, priority: 50, adjustment_type: promotion } commerce_promotion.usage: class: Drupal\commerce_promotion\PromotionUsage diff --git a/modules/tax/commerce_tax.services.yml b/modules/tax/commerce_tax.services.yml index dc38e83919..5f1b4c5870 100644 --- a/modules/tax/commerce_tax.services.yml +++ b/modules/tax/commerce_tax.services.yml @@ -17,4 +17,4 @@ services: class: Drupal\commerce_tax\TaxOrderProcessor arguments: ['@entity_type.manager'] tags: - - { name: commerce_order.order_processor, priority: 100 } + - { name: commerce_order.order_processor, priority: 100, adjustment_type: tax } diff --git a/src/Context.php b/src/Context.php index 2c4d77d223..4a78949de7 100644 --- a/src/Context.php +++ b/src/Context.php @@ -62,7 +62,7 @@ public function getCustomer() { /** * Gets the store. * - * @return \Drupal\commerce_store\Entity\Store + * @return \Drupal\commerce_store\Entity\StoreInterface * The store. */ public function getStore() { From b55440ff69b600ed9e23a30c81e40cfe77234a77 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Fri, 9 Feb 2018 20:10:12 +0100 Subject: [PATCH 050/103] Issue #2943621 by bojanz: Add arithmetic methods to Adjustment --- modules/order/src/Adjustment.php | 120 ++++++++++++++++++ modules/order/src/Entity/Order.php | 11 +- .../order/tests/src/Kernel/AdjustmentTest.php | 112 +++++++++++++++- 3 files changed, 231 insertions(+), 12 deletions(-) diff --git a/modules/order/src/Adjustment.php b/modules/order/src/Adjustment.php index 07f5e4c771..5418dd839b 100644 --- a/modules/order/src/Adjustment.php +++ b/modules/order/src/Adjustment.php @@ -196,4 +196,124 @@ public function isLocked() { return $this->locked; } + /** + * Gets the array representation of the adjustment. + * + * @return array + * The array representation of the adjustment. + */ + public function toArray() { + return [ + 'type' => $this->type, + 'label' => $this->label, + 'amount' => $this->amount, + 'percentage' => $this->percentage, + 'source_id' => $this->sourceId, + 'included' => $this->included, + 'locked' => $this->locked, + ]; + } + + /** + * Adds the given adjustment to the current adjustment. + * + * @param \Drupal\commerce_order\Adjustment $adjustment + * The adjustment. + * + * @return static + * The resulting adjustment. + */ + public function add(Adjustment $adjustment) { + $this->assertSameType($adjustment); + $this->assertSameSourceId($adjustment); + $definition = [ + 'amount' => $this->amount->add($adjustment->getAmount()), + ] + $this->toArray(); + + return new static($definition); + } + + /** + * Subtracts the given adjustment from the current adjustment. + * + * @param \Drupal\commerce_order\Adjustment $adjustment + * The adjustment. + * + * @return static + * The resulting adjustment. + */ + public function subtract(Adjustment $adjustment) { + $this->assertSameType($adjustment); + $this->assertSameSourceId($adjustment); + $definition = [ + 'amount' => $this->amount->subtract($adjustment->getAmount()), + ] + $this->toArray(); + + return new static($definition); + } + + /** + * Multiplies the adjustment amount by the given number. + * + * @param string $number + * The number. + * + * @return static + * The resulting adjustment. + */ + public function multiply($number) { + $definition = [ + 'amount' => $this->amount->multiply($number), + ] + $this->toArray(); + + return new static($definition); + } + + /** + * Divides the adjustment amount by the given number. + * + * @param string $number + * The number. + * + * @return static + * The resulting adjustment. + */ + public function divide($number) { + $definition = [ + 'amount' => $this->amount->divide($number), + ] + $this->toArray(); + + return new static($definition); + } + + /** + * Asserts that the given adjustment's type matches the current one. + * + * @param \Drupal\commerce_order\Adjustment $adjustment + * The adjustment to compare. + * + * @throws \InvalidArgumentException + * Thrown when the adjustment type does not match the current one. + */ + protected function assertSameType(Adjustment $adjustment) { + if ($this->type != $adjustment->getType()) { + throw new \InvalidArgumentException(sprintf('Adjustment type "%s" does not match "%s".', $adjustment->getType(), $this->type)); + } + } + + /** + * Asserts that the given adjustment's source ID matches the current one. + * + * @param \Drupal\commerce_order\Adjustment $adjustment + * The adjustment to compare. + * + * @throws \InvalidArgumentException + * Thrown when the adjustment source ID does not match the current one. + */ + protected function assertSameSourceId(Adjustment $adjustment) { + if ($this->sourceId != $adjustment->getSourceId()) { + throw new \InvalidArgumentException(sprintf('Adjustment source ID "%s" does not match "%s".', $adjustment->getSourceId(), $this->sourceId)); + } + } + } diff --git a/modules/order/src/Entity/Order.php b/modules/order/src/Entity/Order.php index 7a1e303a26..d62ffcc924 100644 --- a/modules/order/src/Entity/Order.php +++ b/modules/order/src/Entity/Order.php @@ -324,16 +324,7 @@ public function collectAdjustments() { foreach ($order_item->getAdjustments() as $adjustment) { // Order item adjustments apply to the unit price, they // must be multiplied by quantity before they are used. - $multiplied_adjustment = new Adjustment([ - 'type' => $adjustment->getType(), - 'label' => $adjustment->getLabel(), - 'amount' => $adjustment->getAmount()->multiply($order_item->getQuantity()), - 'percentage' => $adjustment->getPercentage(), - 'source_id' => $adjustment->getSourceId(), - 'included' => $adjustment->isIncluded(), - 'locked' => $adjustment->isLocked(), - ]); - $adjustments[] = $multiplied_adjustment; + $adjustments[] = $adjustment->multiply($order_item->getQuantity()); } } foreach ($this->getAdjustments() as $adjustment) { diff --git a/modules/order/tests/src/Kernel/AdjustmentTest.php b/modules/order/tests/src/Kernel/AdjustmentTest.php index c65ab15058..b1eaeb7221 100644 --- a/modules/order/tests/src/Kernel/AdjustmentTest.php +++ b/modules/order/tests/src/Kernel/AdjustmentTest.php @@ -107,7 +107,7 @@ public function testValidAdjustmentConstruct() { } /** - * Tests methods on the adjustment object. + * Tests getters. * * @covers ::getType * @covers ::getLabel @@ -118,8 +118,9 @@ public function testValidAdjustmentConstruct() { * @covers ::getSourceId * @covers ::isIncluded * @covers ::isLocked + * @covers ::toArray */ - public function testAdjustmentMethods() { + public function testGetters() { $definition = [ 'type' => 'custom', 'label' => '10% off', @@ -141,6 +142,113 @@ public function testAdjustmentMethods() { $this->assertEquals('1', $adjustment->getSourceId()); $this->assertTrue($adjustment->isIncluded()); $this->assertTrue($adjustment->isLocked()); + $this->assertEquals($definition, $adjustment->toArray()); + } + + /** + * Tests the arithmetic operators. + * + * @covers ::add + * @covers ::subtract + * @covers ::multiply + * @covers ::divide + */ + public function testArithmetic() { + $first_adjustment = new Adjustment([ + 'type' => 'custom', + 'amount' => new Price('2.00', 'USD'), + 'label' => '10% off', + 'percentage' => '0.1', + 'source_id' => '1', + 'included' => TRUE, + 'locked' => TRUE, + ]); + $second_adjustment = new Adjustment([ + 'type' => 'custom', + 'amount' => new Price('3.00', 'USD'), + 'label' => '10% off', + 'percentage' => '0.1', + 'source_id' => '1', + 'included' => TRUE, + 'locked' => TRUE, + ]); + $third_adjustment = new Adjustment([ + 'type' => 'custom', + 'amount' => new Price('5.00', 'USD'), + 'label' => '10% off', + 'percentage' => '0.1', + 'source_id' => '1', + 'included' => TRUE, + 'locked' => TRUE, + ]); + $fourth_adjustment = new Adjustment([ + 'type' => 'custom', + 'amount' => new Price('6.00', 'USD'), + 'label' => '10% off', + 'percentage' => '0.1', + 'source_id' => '1', + 'included' => TRUE, + 'locked' => TRUE, + ]); + + $this->assertEquals($third_adjustment, $first_adjustment->add($second_adjustment)); + $this->assertEquals($second_adjustment, $third_adjustment->subtract($first_adjustment)); + $this->assertEquals($fourth_adjustment, $second_adjustment->multiply('2')); + $this->assertEquals($first_adjustment, $fourth_adjustment->divide('3')); + } + + /** + * @covers ::add + */ + public function testMismatchedTypes() { + $first_adjustment = new Adjustment([ + 'type' => 'custom', + 'amount' => new Price('2.00', 'USD'), + 'label' => '10% off', + 'percentage' => '0.1', + 'source_id' => '1', + 'included' => TRUE, + 'locked' => TRUE, + ]); + $second_adjustment = new Adjustment([ + 'type' => 'promotion', + 'amount' => new Price('3.00', 'USD'), + 'label' => '10% off', + 'percentage' => '0.1', + 'source_id' => '1', + 'included' => TRUE, + 'locked' => TRUE, + ]); + + $this->setExpectedException(\InvalidArgumentException::class, 'Adjustment type "promotion" does not match "custom".'); + $first_adjustment->add($second_adjustment); + } + + /** + * @covers ::add + */ + public function testMismatchedSourceIds() { + $first_adjustment = new Adjustment([ + 'type' => 'custom', + 'amount' => new Price('2.00', 'USD'), + 'label' => '10% off', + 'percentage' => '0.1', + 'source_id' => '1', + 'included' => TRUE, + 'locked' => TRUE, + ]); + $second_adjustment = new Adjustment([ + 'type' => 'custom', + 'amount' => new Price('3.00', 'USD'), + 'label' => '10% off', + 'percentage' => '0.1', + 'source_id' => '2', + 'included' => TRUE, + 'locked' => TRUE, + ]); + + $this->setExpectedException(\InvalidArgumentException::class, 'Adjustment source ID "2" does not match "1".'); + $first_adjustment->add($second_adjustment); } } From 7d068d5b6a98f02e890c592107067ce04206f947 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Fri, 9 Feb 2018 21:06:31 +0100 Subject: [PATCH 051/103] Issue #2943585 by bojanz: Use a value object for the result returned from commerce_order's PriceCalculator --- .../PriceCalculatedFormatter.php | 3 +- modules/order/src/PriceCalculator.php | 14 +-- .../order/src/PriceCalculatorInterface.php | 7 +- modules/order/src/PriceCalculatorResult.php | 85 +++++++++++++++++++ .../tests/src/Kernel/PriceCalculatorTest.php | 61 +++++++------ 5 files changed, 125 insertions(+), 45 deletions(-) create mode 100644 modules/order/src/PriceCalculatorResult.php diff --git a/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php b/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php index 086fb3d9b3..f3b061f6ba 100644 --- a/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php +++ b/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php @@ -179,8 +179,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { $purchasable_entity = $items->getEntity(); $adjustment_types = array_filter($this->getSetting('adjustment_types')); $result = $this->priceCalculator->calculate($purchasable_entity, 1, $context, $adjustment_types); - /** @var \Drupal\commerce_price\Price $calculated_price */ - $calculated_price = $result['calculated_price']; + $calculated_price = $result->getCalculatedPrice(); $number = $calculated_price->getNumber(); /** @var \Drupal\commerce_price\Entity\CurrencyInterface $currency */ $currency = $this->currencyStorage->load($calculated_price->getCurrencyCode()); diff --git a/modules/order/src/PriceCalculator.php b/modules/order/src/PriceCalculator.php index 139682f84b..054f856984 100644 --- a/modules/order/src/PriceCalculator.php +++ b/modules/order/src/PriceCalculator.php @@ -99,11 +99,7 @@ public function calculate(PurchasableEntityInterface $purchasable_entity, $quant $processors = array_merge($processors, $this->getProcessors($adjustment_type)); } if (empty($adjustment_types) || empty($processors)) { - return [ - 'calculated_price' => $resolved_price, - 'base_price' => $resolved_price, - 'adjustments' => [], - ]; + return new PriceCalculatorResult($resolved_price, $resolved_price); } /** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_storage */ @@ -120,12 +116,10 @@ public function calculate(PurchasableEntityInterface $purchasable_entity, $quant foreach ($processors as $processor) { $processor->process($order); } + $calculated_price = $order_item->getAdjustedUnitPrice(); + $adjustments = $order_item->getAdjustments(); - return [ - 'calculated_price' => $order_item->getAdjustedUnitPrice(), - 'base_price' => $resolved_price, - 'adjustments' => $order_item->getAdjustments(), - ]; + return new PriceCalculatorResult($calculated_price, $resolved_price, $adjustments); } /** diff --git a/modules/order/src/PriceCalculatorInterface.php b/modules/order/src/PriceCalculatorInterface.php index 3d11755ff0..7bcf1fe0ab 100644 --- a/modules/order/src/PriceCalculatorInterface.php +++ b/modules/order/src/PriceCalculatorInterface.php @@ -49,11 +49,8 @@ public function getProcessors($adjustment_type); * The adjustment types to include in the calculated price. * Examples: fee, promotion, tax. * - * @return array - * An array with the following elements: - * - calculated_price: The resolved unit price with adjustments applied. - * - base_price: The resolved unit price without any adjustments. - * - adjustments: The individual adjustments. + * @return \Drupal\commerce_order\PriceCalculatorResult + * The result. */ public function calculate(PurchasableEntityInterface $purchasable_entity, $quantity, Context $context, array $adjustment_types = []); diff --git a/modules/order/src/PriceCalculatorResult.php b/modules/order/src/PriceCalculatorResult.php new file mode 100644 index 0000000000..42042a7c02 --- /dev/null +++ b/modules/order/src/PriceCalculatorResult.php @@ -0,0 +1,85 @@ +calculatedPrice = $calculated_price; + $this->basePrice = $base_price; + $this->adjustments = $adjustments; + } + + /** + * Gets the calculated price. + * + * This is the resolved unit price with adjustments applied. + * + * @return \Drupal\commerce_price\Price + * The calculated price. + */ + public function getCalculatedPrice() { + return $this->calculatedPrice; + } + + /** + * Gets the base price. + * + * This is the resolved unit price without any adjustments. + * + * @return \Drupal\commerce_price\Price + * The base price. + */ + public function getBasePrice() { + return $this->basePrice; + } + + /** + * Gets the adjustments. + * + * @return \Drupal\commerce_order\Adjustment[] + * The adjustments. + */ + public function getAdjustments() { + return $this->adjustments; + } + +} diff --git a/modules/order/tests/src/Kernel/PriceCalculatorTest.php b/modules/order/tests/src/Kernel/PriceCalculatorTest.php index d83e13a4d6..55b3b696aa 100644 --- a/modules/order/tests/src/Kernel/PriceCalculatorTest.php +++ b/modules/order/tests/src/Kernel/PriceCalculatorTest.php @@ -160,62 +160,67 @@ public function testCalculation() { // No adjustment types specified. $result = $this->priceCalculator->calculate($this->firstVariation, 1, $first_context); - $this->assertEquals(new Price('3.00', 'USD'), $result['calculated_price']); - $this->assertEquals(new Price('3.00', 'USD'), $result['base_price']); - $this->assertEquals([], $result['adjustments']); + $this->assertEquals(new Price('3.00', 'USD'), $result->getCalculatedPrice()); + $this->assertEquals(new Price('3.00', 'USD'), $result->getBasePrice()); + $this->assertEquals([], $result->getAdjustments()); // Unknown adjustment type specified. $result = $this->priceCalculator->calculate($this->secondVariation, 1, $first_context, ['invalid']); - $this->assertEquals(new Price('4.00', 'USD'), $result['calculated_price']); - $this->assertEquals(new Price('4.00', 'USD'), $result['base_price']); - $this->assertEquals([], $result['adjustments']); + $this->assertEquals(new Price('4.00', 'USD'), $result->getCalculatedPrice()); + $this->assertEquals(new Price('4.00', 'USD'), $result->getBasePrice()); + $this->assertEquals([], $result->getAdjustments()); // Only tax. $result = $this->priceCalculator->calculate($this->firstVariation, 1, $first_context, ['tax']); - $this->assertEquals(new Price('3.60', 'USD'), $result['calculated_price']); - $this->assertEquals(new Price('3.00', 'USD'), $result['base_price']); - $this->assertCount(1, $result['adjustments']); - $first_adjustment = reset($result['adjustments']); + $this->assertEquals(new Price('3.60', 'USD'), $result->getCalculatedPrice()); + $this->assertEquals(new Price('3.00', 'USD'), $result->getBasePrice()); + $this->assertCount(1, $result->getAdjustments()); + $adjustments = $result->getAdjustments(); + $first_adjustment = reset($adjustments); $this->assertEquals('tax', $first_adjustment->getType()); $this->assertEquals(new Price('0.60', 'USD'), $first_adjustment->getAmount()); $result = $this->priceCalculator->calculate($this->secondVariation, 1, $first_context, ['tax']); - $this->assertEquals(new Price('4.80', 'USD'), $result['calculated_price']); - $this->assertEquals(new Price('4.00', 'USD'), $result['base_price']); - $this->assertCount(1, $result['adjustments']); - $first_adjustment = reset($result['adjustments']); + $this->assertEquals(new Price('4.80', 'USD'), $result->getCalculatedPrice()); + $this->assertEquals(new Price('4.00', 'USD'), $result->getBasePrice()); + $this->assertCount(1, $result->getAdjustments()); + $adjustments = $result->getAdjustments(); + $first_adjustment = reset($adjustments); $this->assertEquals('tax', $first_adjustment->getType()); $this->assertEquals(new Price('0.80', 'USD'), $first_adjustment->getAmount()); // Tax and promotions. $result = $this->priceCalculator->calculate($this->firstVariation, 1, $first_context, ['tax', 'promotion']); - $this->assertEquals(new Price('1.80', 'USD'), $result['calculated_price']); - $this->assertEquals(new Price('3.00', 'USD'), $result['base_price']); - $this->assertCount(2, $result['adjustments']); - $first_adjustment = reset($result['adjustments']); + $this->assertEquals(new Price('1.80', 'USD'), $result->getCalculatedPrice()); + $this->assertEquals(new Price('3.00', 'USD'), $result->getBasePrice()); + $this->assertCount(2, $result->getAdjustments()); + $adjustments = $result->getAdjustments(); + $first_adjustment = reset($adjustments); $this->assertEquals('tax', $first_adjustment->getType()); $this->assertEquals(new Price('0.60', 'USD'), $first_adjustment->getAmount()); - $second_adjustment = end($result['adjustments']); + $second_adjustment = end($adjustments); $this->assertEquals('promotion', $second_adjustment->getType()); $this->assertEquals(new Price('-1.80', 'USD'), $second_adjustment->getAmount()); $result = $this->priceCalculator->calculate($this->secondVariation, 1, $first_context, ['tax', 'promotion']); - $this->assertEquals(new Price('2.40', 'USD'), $result['calculated_price']); - $this->assertEquals(new Price('4.00', 'USD'), $result['base_price']); - $this->assertCount(2, $result['adjustments']); - $first_adjustment = reset($result['adjustments']); + $this->assertEquals(new Price('2.40', 'USD'), $result->getCalculatedPrice()); + $this->assertEquals(new Price('4.00', 'USD'), $result->getBasePrice()); + $this->assertCount(2, $result->getAdjustments()); + $adjustments = $result->getAdjustments(); + $first_adjustment = reset($adjustments); $this->assertEquals('tax', $first_adjustment->getType()); $this->assertEquals(new Price('0.80', 'USD'), $first_adjustment->getAmount()); - $second_adjustment = end($result['adjustments']); + $second_adjustment = end($adjustments); $this->assertEquals('promotion', $second_adjustment->getType()); $this->assertEquals(new Price('-2.40', 'USD'), $second_adjustment->getAmount()); // User-specific adjustment added by TestAdjustmentProcessor. $result = $this->priceCalculator->calculate($this->secondVariation, 1, $second_context, ['test_adjustment_type']); - $this->assertEquals(new Price('6.00', 'USD'), $result['calculated_price']); - $this->assertEquals(new Price('4.00', 'USD'), $result['base_price']); - $this->assertCount(1, $result['adjustments']); - $first_adjustment = reset($result['adjustments']); + $this->assertEquals(new Price('6.00', 'USD'), $result->getCalculatedPrice()); + $this->assertEquals(new Price('4.00', 'USD'), $result->getBasePrice()); + $this->assertCount(1, $result->getAdjustments()); + $adjustments = $result->getAdjustments(); + $first_adjustment = reset($adjustments); $this->assertEquals('test_adjustment_type', $first_adjustment->getType()); $this->assertEquals(new Price('2.00', 'USD'), $first_adjustment->getAmount()); } From 7d71e14aff254f38b134bd3da1672d1044a1d00f Mon Sep 17 00:00:00 2001 From: git Date: Sat, 10 Feb 2018 18:43:01 +0100 Subject: [PATCH 052/103] Issue #2943767 by agoradesign, pazhyn: README still contains beta notice --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index f23acbbea3..51599a11f8 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,6 @@ Drupal Commerce Drupal Commerce is the leading flexible eCommerce solution for Drupal, powering over 60,000 online stores of all sizes. -Note: 8.x-2.x is in beta. It is already usable for production websites -and upgrades between versions are supported. Bugs are still expected. Please report bugs in the [issue queue](https://www.drupal.org/project/issues/commerce?version=8.x). [Documentation](http://docs.drupalcommerce.org) From e4f01cfa3f5b35492acf540fda840966c21c4afd Mon Sep 17 00:00:00 2001 From: CameronLamb Date: Sat, 10 Feb 2018 20:08:40 +0100 Subject: [PATCH 053/103] Issue #2943187 by jafacakes2011: Check payment method for existing billing profile before assuming a new one --- .../Commerce/CheckoutPane/PaymentInformation.php | 1 + .../payment/src/PluginForm/PaymentMethodAddForm.php | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index 6cce21e053..c931b4ff83 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -126,6 +126,7 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, 'type' => $selected_option['#payment_method_type'], 'payment_gateway' => $selected_option['#payment_gateway'], 'uid' => $this->order->getCustomerId(), + 'billing_profile' => $this->order->getBillingProfile(), ]); $pane_form['add_payment_method'] = [ diff --git a/modules/payment/src/PluginForm/PaymentMethodAddForm.php b/modules/payment/src/PluginForm/PaymentMethodAddForm.php index ffefe78090..44c6c69bb1 100644 --- a/modules/payment/src/PluginForm/PaymentMethodAddForm.php +++ b/modules/payment/src/PluginForm/PaymentMethodAddForm.php @@ -55,10 +55,15 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ $payment_method = $this->entity; /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */ - $billing_profile = Profile::create([ - 'type' => 'customer', - 'uid' => $payment_method->getOwnerId(), - ]); + $billing_profile = $payment_method->getBillingProfile(); + if (!$billing_profile) { + /** @var \Drupal\profile\Entity\ProfileInterface $billing_profile */ + $billing_profile = Profile::create([ + 'type' => 'customer', + 'uid' => $payment_method->getOwnerId(), + ]); + } + if ($order = $this->routeMatch->getParameter('commerce_order')) { $store = $order->getStore(); } From 8eb2d440dffbfc8b81213114ad3ba20f39d918f2 Mon Sep 17 00:00:00 2001 From: facine Date: Sat, 10 Feb 2018 23:15:05 +0100 Subject: [PATCH 054/103] Issue #2939331 by facine, bojanz: PaymentMethod defines a bogus canonical link --- modules/payment/src/Entity/PaymentMethod.php | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/payment/src/Entity/PaymentMethod.php b/modules/payment/src/Entity/PaymentMethod.php index 6192c675bf..c8b5889b6c 100644 --- a/modules/payment/src/Entity/PaymentMethod.php +++ b/modules/payment/src/Entity/PaymentMethod.php @@ -47,7 +47,6 @@ * }, * links = { * "collection" = "/user/{user}/payment-methods", - * "canonical" = "/user/{user}/payment-methods/{commerce_payment_method}/edit", * "edit-form" = "/user/{user}/payment-methods/{commerce_payment_method}/edit", * "delete-form" = "/user/{user}/payment-methods/{commerce_payment_method}/delete", * }, From 77b25bf4889a940eed6652475c3952c36144efc0 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Sat, 10 Feb 2018 23:50:53 +0100 Subject: [PATCH 055/103] Issue #2939331 followup: Fix tests by removing the unused payment_method_url from the payment method twig template. --- modules/payment/commerce_payment.module | 1 - .../templates/commerce-payment-method--credit-card.html.twig | 1 - modules/payment/templates/commerce-payment-method.html.twig | 1 - 3 files changed, 3 deletions(-) diff --git a/modules/payment/commerce_payment.module b/modules/payment/commerce_payment.module index 66c9f62432..095c6edb72 100644 --- a/modules/payment/commerce_payment.module +++ b/modules/payment/commerce_payment.module @@ -105,7 +105,6 @@ function template_preprocess_commerce_payment_method(array &$variables) { $payment_method = $variables['elements']['#commerce_payment_method']; $variables['payment_method_entity'] = $payment_method; - $variables['payment_method_url'] = $payment_method->toUrl(); $variables['payment_method'] = [ // The label is generated dynamically, so it's not present in 'elements'. 'label' => [ diff --git a/modules/payment/templates/commerce-payment-method--credit-card.html.twig b/modules/payment/templates/commerce-payment-method--credit-card.html.twig index 5a027c71c4..67ba8ad6aa 100644 --- a/modules/payment/templates/commerce-payment-method--credit-card.html.twig +++ b/modules/payment/templates/commerce-payment-method--credit-card.html.twig @@ -14,7 +14,6 @@ * {{ payment_method|without('label') }} * @endcode * - payment_method_entity: The payment_method entity. - * - payment_method_url: The payment_method URL. * * @ingroup themeable */ diff --git a/modules/payment/templates/commerce-payment-method.html.twig b/modules/payment/templates/commerce-payment-method.html.twig index 01dc1c47f3..9bb5f3066c 100644 --- a/modules/payment/templates/commerce-payment-method.html.twig +++ b/modules/payment/templates/commerce-payment-method.html.twig @@ -14,7 +14,6 @@ * {{ payment_method|without('label') }} * @endcode * - payment_method_entity: The payment_method entity. - * - payment_method_url: The payment_method URL. * * @ingroup themeable */ From b40230f4217add8ad26fb32bc304c28060e7a059 Mon Sep 17 00:00:00 2001 From: agoradesign Date: Sat, 10 Feb 2018 23:54:46 +0100 Subject: [PATCH 056/103] Issue #2914176 by agoradesign: Improve TimestampEventSubscriber's dependency injection --- modules/order/commerce_order.services.yml | 2 +- .../src/EventSubscriber/TimestampEventSubscriber.php | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/modules/order/commerce_order.services.yml b/modules/order/commerce_order.services.yml index 8d7e5166cf..1c86111a99 100644 --- a/modules/order/commerce_order.services.yml +++ b/modules/order/commerce_order.services.yml @@ -34,7 +34,7 @@ services: commerce_order.timestamp_event_subscriber: class: Drupal\commerce_order\EventSubscriber\TimestampEventSubscriber - arguments: ['@entity_type.manager', '@datetime.time'] + arguments: ['@datetime.time'] tags: - { name: event_subscriber } diff --git a/modules/order/src/EventSubscriber/TimestampEventSubscriber.php b/modules/order/src/EventSubscriber/TimestampEventSubscriber.php index 9668e02194..03c08caec5 100644 --- a/modules/order/src/EventSubscriber/TimestampEventSubscriber.php +++ b/modules/order/src/EventSubscriber/TimestampEventSubscriber.php @@ -2,7 +2,6 @@ namespace Drupal\commerce_order\EventSubscriber; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\state_machine\Event\WorkflowTransitionEvent; use Drupal\Component\Datetime\TimeInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -10,7 +9,7 @@ class TimestampEventSubscriber implements EventSubscriberInterface { /** - * The system time. + * The time. * * @var \Drupal\Component\Datetime\TimeInterface */ @@ -19,12 +18,10 @@ class TimestampEventSubscriber implements EventSubscriberInterface { /** * Constructs a new TimestampEventSubscriber object. * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. * @param \Drupal\Component\Datetime\TimeInterface $time - * The system time. + * The time. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, TimeInterface $time) { + public function __construct(TimeInterface $time) { $this->time = $time; } From 046ff056f1f442205a491ec836335db9f699f80e Mon Sep 17 00:00:00 2001 From: Lendude Date: Sun, 11 Feb 2018 00:22:08 +0100 Subject: [PATCH 057/103] Issue #2903544 by Lendude: \Drupal\commerce_order\Plugin\views\area\OrderTotal doesn't work with commerce_order_item.order_id --- modules/order/src/Plugin/views/area/OrderTotal.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/order/src/Plugin/views/area/OrderTotal.php b/modules/order/src/Plugin/views/area/OrderTotal.php index 3ca0d6d259..0fbf2ae1a5 100644 --- a/modules/order/src/Plugin/views/area/OrderTotal.php +++ b/modules/order/src/Plugin/views/area/OrderTotal.php @@ -76,7 +76,7 @@ public function render($empty = FALSE) { if (!$argument instanceof NumericArgument) { continue; } - if ($argument->getField() !== 'commerce_order.order_id') { + if (!in_array($argument->getField(), ['commerce_order.order_id', 'commerce_order_item.order_id'])) { continue; } if ($order = $this->orderStorage->load($argument->getValue())) { From 2d5cd0ae08c96481d17058f66a71266e33d13857 Mon Sep 17 00:00:00 2001 From: edwardahan Date: Sun, 11 Feb 2018 00:31:32 +0100 Subject: [PATCH 058/103] Issue #2908419 by edwardaa: Allow for a payment gateway object when creating a payment --- modules/payment/src/PaymentStorage.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/payment/src/PaymentStorage.php b/modules/payment/src/PaymentStorage.php index f59d567fed..45f9616739 100644 --- a/modules/payment/src/PaymentStorage.php +++ b/modules/payment/src/PaymentStorage.php @@ -37,13 +37,13 @@ public function loadMultipleByOrder(OrderInterface $order) { * {@inheritdoc} */ protected function doCreate(array $values) { - if (!isset($values['payment_gateway'])) { + if (empty($values['payment_gateway'])) { throw new EntityStorageException('Missing "payment_gateway" property when creating a payment.'); } // Populate the type using the payment gateway. if (!isset($values['type'])) { $payment_gateway = $values['payment_gateway']; - if ($payment_gateway) { + if (is_string($payment_gateway)) { // The caller passed tha payment gateway ID, load the full entity. $payment_gateway_storage = $this->entityManager->getStorage('commerce_payment_gateway'); /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ From d87e374b95917f71ea327310961490138a986ba4 Mon Sep 17 00:00:00 2001 From: jsacksick Date: Sun, 11 Feb 2018 01:04:55 +0100 Subject: [PATCH 059/103] Issue #2901324 by jsacksick, bojanz, edurenye: Make the $store argument to CartProvider methods optional --- modules/cart/commerce_cart.services.yml | 2 +- modules/cart/src/CartProvider.php | 21 +++++++++++++++---- modules/cart/src/CartProviderInterface.php | 12 +++++------ .../src/Functional/CartBrowserTestBase.php | 4 +--- .../cart/tests/src/Functional/CartTest.php | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) diff --git a/modules/cart/commerce_cart.services.yml b/modules/cart/commerce_cart.services.yml index d1e6bfe70f..ab836cedee 100644 --- a/modules/cart/commerce_cart.services.yml +++ b/modules/cart/commerce_cart.services.yml @@ -11,7 +11,7 @@ services: commerce_cart.cart_provider: class: Drupal\commerce_cart\CartProvider - arguments: ['@entity_type.manager', '@current_user', '@commerce_cart.cart_session'] + arguments: ['@entity_type.manager', '@commerce_store.current_store', '@current_user', '@commerce_cart.cart_session'] commerce_cart.cart_manager: class: Drupal\commerce_cart\CartManager diff --git a/modules/cart/src/CartProvider.php b/modules/cart/src/CartProvider.php index 95bf35c8f3..db0197f60a 100644 --- a/modules/cart/src/CartProvider.php +++ b/modules/cart/src/CartProvider.php @@ -4,6 +4,7 @@ use Drupal\commerce_cart\Exception\DuplicateCartException; use Drupal\commerce_order\Entity\OrderInterface; +use Drupal\commerce_store\CurrentStoreInterface; use Drupal\commerce_store\Entity\StoreInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; @@ -20,6 +21,13 @@ class CartProvider implements CartProviderInterface { */ protected $orderStorage; + /** + * The current store. + * + * @var \Drupal\commerce_store\CurrentStoreInterface + */ + protected $currentStore; + /** * The current user. * @@ -57,13 +65,16 @@ class CartProvider implements CartProviderInterface { * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. + * @param \Drupal\commerce_store\CurrentStoreInterface $current_store + * The current store. * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. * @param \Drupal\commerce_cart\CartSessionInterface $cart_session * The cart session. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, CartSessionInterface $cart_session) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, CurrentStoreInterface $current_store, AccountInterface $current_user, CartSessionInterface $cart_session) { $this->orderStorage = $entity_type_manager->getStorage('commerce_order'); + $this->currentStore = $current_store; $this->currentUser = $current_user; $this->cartSession = $cart_session; } @@ -71,7 +82,8 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Acc /** * {@inheritdoc} */ - public function createCart($order_type, StoreInterface $store, AccountInterface $account = NULL) { + public function createCart($order_type, StoreInterface $store = NULL, AccountInterface $account = NULL) { + $store = $store ?: $this->currentStore->getStore(); $account = $account ?: $this->currentUser; $uid = $account->id(); $store_id = $store->id(); @@ -124,7 +136,7 @@ public function finalizeCart(OrderInterface $cart, $save_cart = TRUE) { /** * {@inheritdoc} */ - public function getCart($order_type, StoreInterface $store, AccountInterface $account = NULL) { + public function getCart($order_type, StoreInterface $store = NULL, AccountInterface $account = NULL) { $cart = NULL; $cart_id = $this->getCartId($order_type, $store, $account); if ($cart_id) { @@ -137,10 +149,11 @@ public function getCart($order_type, StoreInterface $store, AccountInterface $ac /** * {@inheritdoc} */ - public function getCartId($order_type, StoreInterface $store, AccountInterface $account = NULL) { + public function getCartId($order_type, StoreInterface $store = NULL, AccountInterface $account = NULL) { $cart_id = NULL; $cart_data = $this->loadCartData($account); if ($cart_data) { + $store = $store ?: $this->currentStore->getStore(); $search = [ 'type' => $order_type, 'store_id' => $store->id(), diff --git a/modules/cart/src/CartProviderInterface.php b/modules/cart/src/CartProviderInterface.php index 78881e72b2..142333c96c 100644 --- a/modules/cart/src/CartProviderInterface.php +++ b/modules/cart/src/CartProviderInterface.php @@ -19,7 +19,7 @@ interface CartProviderInterface { * @param string $order_type * The order type ID. * @param \Drupal\commerce_store\Entity\StoreInterface $store - * The store. + * The store. If empty, the current store is assumed. * @param \Drupal\Core\Session\AccountInterface $account * The user. If empty, the current user is assumed. * @@ -29,7 +29,7 @@ interface CartProviderInterface { * @throws \Drupal\commerce_cart\Exception\DuplicateCartException * When a cart with the given criteria already exists. */ - public function createCart($order_type, StoreInterface $store, AccountInterface $account = NULL); + public function createCart($order_type, StoreInterface $store = NULL, AccountInterface $account = NULL); /** * Finalizes the given cart order. @@ -51,14 +51,14 @@ public function finalizeCart(OrderInterface $cart, $save_cart = TRUE); * @param string $order_type * The order type ID. * @param \Drupal\commerce_store\Entity\StoreInterface $store - * The store. + * The store. If empty, the current store is assumed. * @param \Drupal\Core\Session\AccountInterface $account * The user. If empty, the current user is assumed. * * @return \Drupal\commerce_order\Entity\OrderInterface|null * The cart order, or NULL if none found. */ - public function getCart($order_type, StoreInterface $store, AccountInterface $account = NULL); + public function getCart($order_type, StoreInterface $store = NULL, AccountInterface $account = NULL); /** * Gets the cart order ID for the given store and user. @@ -66,14 +66,14 @@ public function getCart($order_type, StoreInterface $store, AccountInterface $ac * @param string $order_type * The order type ID. * @param \Drupal\commerce_store\Entity\StoreInterface $store - * The store. + * The store. If empty, the current store is assumed. * @param \Drupal\Core\Session\AccountInterface $account * The user. If empty, the current user is assumed. * * @return int|null * The cart order ID, or NULL if none found. */ - public function getCartId($order_type, StoreInterface $store, AccountInterface $account = NULL); + public function getCartId($order_type, StoreInterface $store = NULL, AccountInterface $account = NULL); /** * Gets all cart orders for the given user. diff --git a/modules/cart/tests/src/Functional/CartBrowserTestBase.php b/modules/cart/tests/src/Functional/CartBrowserTestBase.php index f417d06451..644a1983a6 100644 --- a/modules/cart/tests/src/Functional/CartBrowserTestBase.php +++ b/modules/cart/tests/src/Functional/CartBrowserTestBase.php @@ -66,9 +66,7 @@ protected function getAdministratorPermissions() { protected function setUp() { parent::setUp(); - $this->createStore(); - - $this->cart = \Drupal::service('commerce_cart.cart_provider')->createCart('default', $this->store); + $this->cart = \Drupal::service('commerce_cart.cart_provider')->createCart('default'); $this->cartManager = \Drupal::service('commerce_cart.cart_manager'); $this->attributeFieldManager = \Drupal::service('commerce_product.attribute_field_manager'); } diff --git a/modules/cart/tests/src/Functional/CartTest.php b/modules/cart/tests/src/Functional/CartTest.php index 301e88bebc..2222c78366 100644 --- a/modules/cart/tests/src/Functional/CartTest.php +++ b/modules/cart/tests/src/Functional/CartTest.php @@ -79,7 +79,7 @@ protected function setUp() { 'variations' => [$variation], ]); $this->variations[] = $variation; - $this->cart = \Drupal::service('commerce_cart.cart_provider')->createCart('default', $this->store); + $this->cart = \Drupal::service('commerce_cart.cart_provider')->createCart('default'); $this->cartManager = \Drupal::service('commerce_cart.cart_manager'); } From 2b266e7a8a0eb3d08bf010cf4ee6407fb8d72e12 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Mon, 12 Feb 2018 00:56:22 +0100 Subject: [PATCH 060/103] Issue #2943961: Create a AdjustmentTransformer service for combining/sorting/rounding adjustments --- modules/order/commerce_order.services.yml | 16 +- modules/order/src/AdjustmentTransformer.php | 120 ++++++++++++ .../src/AdjustmentTransformerInterface.php | 83 ++++++++ modules/order/src/Entity/OrderInterface.php | 10 +- modules/order/src/OrderTotalSummary.php | 53 ++--- .../order/src/OrderTotalSummaryInterface.php | 6 +- modules/order/src/PriceCalculator.php | 43 ++-- .../commerce-order-total-summary.html.twig | 7 +- .../src/Kernel/AdjustmentTransformerTest.php | 184 ++++++++++++++++++ .../src/Kernel/OrderTotalSummaryTest.php | 15 +- .../tests/src/Kernel/PriceCalculatorTest.php | 20 +- 11 files changed, 471 insertions(+), 86 deletions(-) create mode 100644 modules/order/src/AdjustmentTransformer.php create mode 100644 modules/order/src/AdjustmentTransformerInterface.php create mode 100644 modules/order/tests/src/Kernel/AdjustmentTransformerTest.php diff --git a/modules/order/commerce_order.services.yml b/modules/order/commerce_order.services.yml index 1c86111a99..5bbac59b4d 100644 --- a/modules/order/commerce_order.services.yml +++ b/modules/order/commerce_order.services.yml @@ -1,4 +1,8 @@ services: + plugin.manager.commerce_adjustment_type: + class: Drupal\commerce_order\AdjustmentTypeManager + arguments: ['@module_handler', '@cache.discovery'] + commerce_order.chain_order_type_resolver: class: Drupal\commerce_order\Resolver\ChainOrderTypeResolver tags: @@ -10,6 +14,10 @@ services: tags: - { name: commerce_order.order_type_resolver, priority: -100 } + commerce_order.adjustment_transformer: + class: Drupal\commerce_order\AdjustmentTransformer + arguments: ['@plugin.manager.commerce_adjustment_type', '@commerce_price.rounder'] + commerce_order.order_assignment: class: Drupal\commerce_order\OrderAssignment arguments: ['@entity_type.manager', '@event_dispatcher'] @@ -49,13 +57,9 @@ services: tags: - { name: 'event_subscriber' } - plugin.manager.commerce_adjustment_type: - class: Drupal\commerce_order\AdjustmentTypeManager - arguments: ['@module_handler', '@cache.discovery'] - commerce_order.order_total_summary: class: Drupal\commerce_order\OrderTotalSummary - arguments: ['@plugin.manager.commerce_adjustment_type'] + arguments: ['@commerce_order.adjustment_transformer'] commerce_order.order_store_resolver: class: Drupal\commerce_order\Resolver\OrderStoreResolver @@ -65,4 +69,4 @@ services: commerce_order.price_calculator: class: Drupal\commerce_order\PriceCalculator - arguments: ['@entity_type.manager', '@commerce_price.chain_price_resolver', '@commerce_order.chain_order_type_resolver', '@request_stack'] + arguments: ['@commerce_order.adjustment_transformer', '@commerce_order.chain_order_type_resolver', '@commerce_price.chain_price_resolver', '@entity_type.manager', '@request_stack'] diff --git a/modules/order/src/AdjustmentTransformer.php b/modules/order/src/AdjustmentTransformer.php new file mode 100644 index 0000000000..0930fd1791 --- /dev/null +++ b/modules/order/src/AdjustmentTransformer.php @@ -0,0 +1,120 @@ +adjustmentTypeManager = $adjustment_type_manager; + $this->rounder = $rounder; + } + + /** + * {@inheritdoc} + */ + public function processAdjustments(array $adjustments) { + $adjustments = $this->combineAdjustments($adjustments); + $adjustments = $this->sortAdjustments($adjustments); + $adjustments = $this->roundAdjustments($adjustments); + + return $adjustments; + } + + /** + * {@inheritdoc} + */ + public function combineAdjustments(array $adjustments) { + $combined_adjustments = []; + foreach ($adjustments as $index => $adjustment) { + $type = $adjustment->getType(); + $source_id = $adjustment->getSourceId(); + if (empty($source_id)) { + // Adjustments without a source ID are always shown standalone. + $key = $index; + } + else { + // Adjustments with the same type and source ID are combined. + $key = $type . '_' . $source_id; + } + + if (empty($combined_adjustments[$key])) { + $combined_adjustments[$key] = $adjustment; + } + else { + $combined_adjustments[$key] = $combined_adjustments[$key]->add($adjustment); + } + } + // The keys used for combining are irrelevant to the caller. + $combined_adjustments = array_values($combined_adjustments); + + return $combined_adjustments; + } + + /** + * {@inheritdoc} + */ + public function sortAdjustments(array $adjustments) { + $types = $this->adjustmentTypeManager->getDefinitions(); + $data = []; + foreach ($adjustments as $adjustment) { + $data[] = [ + 'adjustment' => $adjustment, + 'weight' => $types[$adjustment->getType()]['weight'], + ]; + } + uasort($data, [SortArray::class, 'sortByWeightElement']); + // Re-extract the adjustments from the sorted array. + $adjustments = array_column($data, 'adjustment'); + + return $adjustments; + } + + /** + * {@inheritdoc} + */ + public function roundAdjustments(array $adjustments, $mode = PHP_ROUND_HALF_UP) { + foreach ($adjustments as $index => $adjustment) { + $adjustments[$index] = $this->roundAdjustment($adjustment, $mode); + } + + return $adjustments; + } + + /** + * {@inheritdoc} + */ + public function roundAdjustment(Adjustment $adjustment, $mode = PHP_ROUND_HALF_UP) { + $amount = $this->rounder->round($adjustment->getAmount(), $mode); + $adjustment = new Adjustment([ + 'amount' => $amount, + ] + $adjustment->toArray()); + + return $adjustment; + } + +} diff --git a/modules/order/src/AdjustmentTransformerInterface.php b/modules/order/src/AdjustmentTransformerInterface.php new file mode 100644 index 0000000000..e671d3acd0 --- /dev/null +++ b/modules/order/src/AdjustmentTransformerInterface.php @@ -0,0 +1,83 @@ +adjustmentTypeManager = $adjustment_type_manager; + public function __construct(AdjustmentTransformerInterface $adjustment_transformer) { + $this->adjustmentTransformer = $adjustment_transformer; } /** * {@inheritdoc} */ public function buildTotals(OrderInterface $order) { - $types = $this->adjustmentTypeManager->getDefinitions(); - $adjustments = []; - foreach ($order->collectAdjustments() as $adjustment) { - $type = $adjustment->getType(); - $source_id = $adjustment->getSourceId(); - if (empty($source_id)) { - // Adjustments without a source ID are always shown standalone. - $key = count($adjustments); - } - else { - // Adjustments with the same type and source ID are combined. - $key = $type . '_' . $source_id; - } - - if (empty($adjustments[$key])) { - $adjustments[$key] = [ - 'type' => $type, - 'label' => $adjustment->getLabel(), - 'total' => $adjustment->getAmount(), - 'percentage' => $adjustment->getPercentage(), - 'weight' => $types[$type]['weight'], - ]; - } - else { - $adjustments[$key]['total'] = $adjustments[$key]['total']->add($adjustment->getAmount()); - } + $adjustments = $order->collectAdjustments(); + $adjustments = $this->adjustmentTransformer->processAdjustments($adjustments); + // Convert the adjustments to arrays. + $adjustments = array_map(function (Adjustment $adjustment) { + return $adjustment->toArray(); + }, $adjustments); + // Provide the "total" key for backwards compatibility reasons. + foreach ($adjustments as $index => $adjustment) { + $adjustments[$index]['total'] = $adjustments[$index]['amount']; } - // Sort the adjustments by weight. - uasort($adjustments, [SortArray::class, 'sortByWeightElement']); return [ 'subtotal' => $order->getSubtotalPrice(), diff --git a/modules/order/src/OrderTotalSummaryInterface.php b/modules/order/src/OrderTotalSummaryInterface.php index d95a8805b4..f45dbcd7c6 100644 --- a/modules/order/src/OrderTotalSummaryInterface.php +++ b/modules/order/src/OrderTotalSummaryInterface.php @@ -15,11 +15,11 @@ interface OrderTotalSummaryInterface { * @return array * An array of totals with the following elements: * - subtotal: The order subtotal price. - * - adjustments: An array of adjustment totals: + * - adjustments: The adjustments: * - type: The adjustment type. * - label: The adjustment label. - * - total: The adjustment total price. - * - weight: The adjustment weight, taken from the adjustment type. + * - amount: The adjustment amount. + * - percentage: The decimal adjustment percentage, when available. * - total: The order total price. */ public function buildTotals(OrderInterface $order); diff --git a/modules/order/src/PriceCalculator.php b/modules/order/src/PriceCalculator.php index 054f856984..1feec0aea4 100644 --- a/modules/order/src/PriceCalculator.php +++ b/modules/order/src/PriceCalculator.php @@ -4,7 +4,7 @@ use Drupal\commerce\Context; use Drupal\commerce\PurchasableEntityInterface; -use Drupal\commerce_order\Resolver\OrderTypeResolverInterface; +use Drupal\commerce_order\Resolver\ChainOrderTypeResolverInterface; use Drupal\commerce_price\Resolver\ChainPriceResolverInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -12,25 +12,32 @@ class PriceCalculator implements PriceCalculatorInterface { /** - * The entity type manager. + * The adjustment transformer. * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * @var \Drupal\commerce_order\AdjustmentTransformerInterface */ - protected $entityTypeManager; + protected $adjustmentTransformer; + + /** + * The chain order type resolver. + * + * @var \Drupal\commerce_order\Resolver\ChainOrderTypeResolverInterface + */ + protected $chainOrderTypeResolver; /** - * The chain base price resolver. + * The chain price resolver. * * @var \Drupal\commerce_price\Resolver\ChainPriceResolverInterface */ protected $chainPriceResolver; /** - * The order type resolver. + * The entity type manager. * - * @var \Drupal\commerce_order\Resolver\OrderTypeResolverInterface + * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ - protected $orderTypeResolver; + protected $entityTypeManager; /** * The request stack. @@ -56,19 +63,22 @@ class PriceCalculator implements PriceCalculatorInterface { /** * Constructs a new PriceCalculator object. * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. + * @param \Drupal\commerce_order\AdjustmentTransformerInterface $adjustment_transformer + * The adjustment transformer. + * @param \Drupal\commerce_order\Resolver\ChainOrderTypeResolverInterface $chain_order_type_resolver + * The chain order type resolver. * @param \Drupal\commerce_price\Resolver\ChainPriceResolverInterface $chain_price_resolver * The chain price resolver. - * @param \Drupal\commerce_order\Resolver\OrderTypeResolverInterface $order_type_resolver - * The order type resolver. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * The request stack. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, ChainPriceResolverInterface $chain_price_resolver, OrderTypeResolverInterface $order_type_resolver, RequestStack $request_stack) { - $this->entityTypeManager = $entity_type_manager; + public function __construct(AdjustmentTransformerInterface $adjustment_transformer, ChainOrderTypeResolverInterface $chain_order_type_resolver, ChainPriceResolverInterface $chain_price_resolver, EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack) { + $this->adjustmentTransformer = $adjustment_transformer; + $this->chainOrderTypeResolver = $chain_order_type_resolver; $this->chainPriceResolver = $chain_price_resolver; - $this->orderTypeResolver = $order_type_resolver; + $this->entityTypeManager = $entity_type_manager; $this->requestStack = $request_stack; } @@ -107,7 +117,7 @@ public function calculate(PurchasableEntityInterface $purchasable_entity, $quant $order_item = $order_item_storage->createFromPurchasableEntity($purchasable_entity); $order_item->setUnitPrice($resolved_price); $order_item->setQuantity($quantity); - $order_type_id = $this->orderTypeResolver->resolve($order_item); + $order_type_id = $this->chainOrderTypeResolver->resolve($order_item); $order = $this->prepareOrder($order_type_id, $context); $order_item->order_id = $order; @@ -118,6 +128,7 @@ public function calculate(PurchasableEntityInterface $purchasable_entity, $quant } $calculated_price = $order_item->getAdjustedUnitPrice(); $adjustments = $order_item->getAdjustments(); + $adjustments = $this->adjustmentTransformer->processAdjustments($adjustments); return new PriceCalculatorResult($calculated_price, $resolved_price, $adjustments); } diff --git a/modules/order/templates/commerce-order-total-summary.html.twig b/modules/order/templates/commerce-order-total-summary.html.twig index 2f857fa258..2be3907ae0 100644 --- a/modules/order/templates/commerce-order-total-summary.html.twig +++ b/modules/order/templates/commerce-order-total-summary.html.twig @@ -7,12 +7,11 @@ * - attributes: HTML attributes for the wrapper. * - totals: An array of order totals values with the following keys: * - subtotal: The order subtotal price. - * - adjustments: An array of adjustment totals: + * - adjustments: The adjustments: * - type: The adjustment type. * - label: The adjustment label. - * - total: The adjustment total price. + * - amount: The adjustment amount. * - percentage: The decimal adjustment percentage, when available. For example, "0.2" for a 20% adjustment. - * - weight: The adjustment weight, taken from the adjustment type. * - total: The order total price. * * @ingroup themeable @@ -25,7 +24,7 @@ {% for adjustment in totals.adjustments %}
- {{ adjustment.label }} {{ adjustment.total|commerce_price_format }} + {{ adjustment.label }} {{ adjustment.amount|commerce_price_format }}
{% endfor %}
diff --git a/modules/order/tests/src/Kernel/AdjustmentTransformerTest.php b/modules/order/tests/src/Kernel/AdjustmentTransformerTest.php new file mode 100644 index 0000000000..aa18335988 --- /dev/null +++ b/modules/order/tests/src/Kernel/AdjustmentTransformerTest.php @@ -0,0 +1,184 @@ +adjustmentTransformer = $this->container->get('commerce_order.adjustment_transformer'); + } + + /** + * Tests adjustment combining. + * + * @covers ::combineAdjustments + */ + public function testCombining() { + $adjustments = []; + // Adjustments 0 and 2 are supposed to be combined. + $adjustments[0] = new Adjustment([ + 'type' => 'tax', + 'label' => 'VAT', + 'amount' => new Price('10', 'USD'), + 'source_id' => 'us_vat|default|standard', + 'percentage' => '0.1', + ]); + $adjustments[1] = new Adjustment([ + 'type' => 'promotion', + 'label' => '20% off', + 'amount' => new Price('20', 'USD'), + 'percentage' => '0.2', + ]); + $adjustments[2] = new Adjustment([ + 'type' => 'tax', + 'label' => 'VAT', + 'amount' => new Price('3', 'USD'), + 'source_id' => 'us_vat|default|standard', + 'percentage' => '0.1', + ]); + $adjustments[3] = new Adjustment([ + 'type' => 'tax', + 'label' => 'VAT', + 'amount' => new Price('4', 'USD'), + 'source_id' => 'us_vat|default|reduced', + 'percentage' => '0.1', + ]); + $combined_adjustments = []; + $combined_adjustments[0] = new Adjustment([ + 'type' => 'tax', + 'label' => 'VAT', + 'amount' => new Price('13', 'USD'), + 'source_id' => 'us_vat|default|standard', + 'percentage' => '0.1', + ]); + $combined_adjustments[1] = new Adjustment([ + 'type' => 'promotion', + 'label' => '20% off', + 'amount' => new Price('20', 'USD'), + 'percentage' => '0.2', + ]); + $combined_adjustments[2] = new Adjustment([ + 'type' => 'tax', + 'label' => 'VAT', + 'amount' => new Price('4', 'USD'), + 'source_id' => 'us_vat|default|reduced', + 'percentage' => '0.1', + ]); + + $result = $this->adjustmentTransformer->combineAdjustments($adjustments); + $this->assertCount(3, $result); + $this->assertEquals($combined_adjustments, $result); + } + + /** + * Tests adjustment sorting. + * + * @covers ::sortAdjustments + */ + public function testSorting() { + $first_adjustment = new Adjustment([ + 'type' => 'tax', + 'label' => 'VAT', + 'amount' => new Price('10', 'USD'), + 'percentage' => '0.1', + ]); + $second_adjustment = new Adjustment([ + 'type' => 'promotion', + 'label' => '20% off', + 'amount' => new Price('20', 'USD'), + 'percentage' => '0.2', + ]); + + $adjustments = $this->adjustmentTransformer->sortAdjustments([$first_adjustment, $second_adjustment]); + $this->assertEquals([$second_adjustment, $first_adjustment], $adjustments); + } + + /** + * Tests adjustment rounding. + * + * @covers ::roundAdjustments + * @covers ::roundAdjustment + */ + public function testRounding() { + $first_adjustment = new Adjustment([ + 'type' => 'tax', + 'label' => 'VAT', + 'amount' => new Price('10.489', 'USD'), + 'percentage' => '0.1', + ]); + $second_adjustment = new Adjustment([ + 'type' => 'promotion', + 'label' => '20% off', + 'amount' => new Price('20.555', 'USD'), + ]); + $first_rounded_adjustment = new Adjustment([ + 'type' => 'tax', + 'label' => 'VAT', + 'amount' => new Price('10.49', 'USD'), + 'percentage' => '0.1', + ]); + $second_rounded_adjustment = new Adjustment([ + 'type' => 'promotion', + 'label' => '20% off', + 'amount' => new Price('20.56', 'USD'), + ]); + $second_rounded_down_adjustment = new Adjustment([ + 'type' => 'promotion', + 'label' => '20% off', + 'amount' => new Price('20.55', 'USD'), + ]); + + $adjustments = $this->adjustmentTransformer->roundAdjustments([$first_adjustment, $second_adjustment]); + $this->assertEquals([$first_rounded_adjustment, $second_rounded_adjustment], $adjustments); + + $adjustment = $this->adjustmentTransformer->roundAdjustment($first_adjustment); + $this->assertEquals($first_rounded_adjustment, $adjustment); + + $adjustment = $this->adjustmentTransformer->roundAdjustment($second_adjustment); + $this->assertEquals($second_rounded_adjustment, $adjustment); + + // Confirm that the $mode is passed along. + $adjustments = $this->adjustmentTransformer->roundAdjustments([$second_adjustment], PHP_ROUND_HALF_DOWN); + $this->assertEquals([$second_rounded_down_adjustment], $adjustments); + + $adjustment = $this->adjustmentTransformer->roundAdjustment($second_adjustment, PHP_ROUND_HALF_DOWN); + $this->assertEquals($second_rounded_down_adjustment, $adjustment); + } + +} diff --git a/modules/order/tests/src/Kernel/OrderTotalSummaryTest.php b/modules/order/tests/src/Kernel/OrderTotalSummaryTest.php index 68b7dcb318..987f884209 100644 --- a/modules/order/tests/src/Kernel/OrderTotalSummaryTest.php +++ b/modules/order/tests/src/Kernel/OrderTotalSummaryTest.php @@ -144,9 +144,8 @@ public function testWithOrderAdjustments() { $first = array_shift($totals['adjustments']); $this->assertEquals('promotion', $first['type']); $this->assertEquals('Back to school discount', $first['label']); - $this->assertEquals(new Price('-5', 'USD'), $first['total']); + $this->assertEquals(new Price('-5', 'USD'), $first['amount']); $this->assertEquals('0.1', $first['percentage']); - $this->assertEquals(0, $first['weight']); } /** @@ -181,9 +180,8 @@ public function testWithOrderItemAdjustments() { $first = array_shift($totals['adjustments']); $this->assertEquals('promotion', $first['type']); $this->assertEquals('Back to school discount', $first['label']); - $this->assertEquals(new Price('-1', 'USD'), $first['total']); + $this->assertEquals(new Price('-1', 'USD'), $first['amount']); $this->assertEquals('0.1', $first['percentage']); - $this->assertEquals(0, $first['weight']); } /** @@ -240,23 +238,20 @@ public function testWithAllAdjustments() { $first = array_shift($totals['adjustments']); $this->assertEquals('test_adjustment_type', $first['type']); $this->assertEquals('50 cent item fee', $first['label']); - $this->assertEquals(new Price('1', 'USD'), $first['total']); + $this->assertEquals(new Price('1', 'USD'), $first['amount']); $this->assertNull($first['percentage']); - $this->assertEquals(-1, $first['weight']); $second = array_shift($totals['adjustments']); $this->assertEquals('promotion', $second['type']); $this->assertEquals('Back to school discount', $second['label']); - $this->assertEquals(new Price('-7', 'USD'), $second['total']); + $this->assertEquals(new Price('-7', 'USD'), $second['amount']); $this->assertEquals('0.1', $second['percentage']); - $this->assertEquals(0, $second['weight']); $third = array_shift($totals['adjustments']); $this->assertEquals('custom', $third['type']); $this->assertEquals('Handling fee', $third['label']); - $this->assertEquals(new Price('10', 'USD'), $third['total']); + $this->assertEquals(new Price('10', 'USD'), $third['amount']); $this->assertNull($third['percentage']); - $this->assertEquals(10, $third['weight']); } } diff --git a/modules/order/tests/src/Kernel/PriceCalculatorTest.php b/modules/order/tests/src/Kernel/PriceCalculatorTest.php index 55b3b696aa..1f5e14fa88 100644 --- a/modules/order/tests/src/Kernel/PriceCalculatorTest.php +++ b/modules/order/tests/src/Kernel/PriceCalculatorTest.php @@ -13,6 +13,8 @@ /** * Tests the price calculator. * + * @coversDefaultClass \Drupal\commerce_order\PriceCalculator + * * @group commerce */ class PriceCalculatorTest extends CommerceKernelTestBase { @@ -153,6 +155,8 @@ protected function setUp() { /** * Tests the calculator. + * + * @covers ::calculate */ public function testCalculation() { $first_context = new Context($this->firstUser, $this->store); @@ -196,11 +200,11 @@ public function testCalculation() { $this->assertCount(2, $result->getAdjustments()); $adjustments = $result->getAdjustments(); $first_adjustment = reset($adjustments); - $this->assertEquals('tax', $first_adjustment->getType()); - $this->assertEquals(new Price('0.60', 'USD'), $first_adjustment->getAmount()); + $this->assertEquals('promotion', $first_adjustment->getType()); + $this->assertEquals(new Price('-1.80', 'USD'), $first_adjustment->getAmount()); $second_adjustment = end($adjustments); - $this->assertEquals('promotion', $second_adjustment->getType()); - $this->assertEquals(new Price('-1.80', 'USD'), $second_adjustment->getAmount()); + $this->assertEquals('tax', $second_adjustment->getType()); + $this->assertEquals(new Price('0.60', 'USD'), $second_adjustment->getAmount()); $result = $this->priceCalculator->calculate($this->secondVariation, 1, $first_context, ['tax', 'promotion']); $this->assertEquals(new Price('2.40', 'USD'), $result->getCalculatedPrice()); @@ -208,11 +212,11 @@ public function testCalculation() { $this->assertCount(2, $result->getAdjustments()); $adjustments = $result->getAdjustments(); $first_adjustment = reset($adjustments); - $this->assertEquals('tax', $first_adjustment->getType()); - $this->assertEquals(new Price('0.80', 'USD'), $first_adjustment->getAmount()); + $this->assertEquals('promotion', $first_adjustment->getType()); + $this->assertEquals(new Price('-2.40', 'USD'), $first_adjustment->getAmount()); $second_adjustment = end($adjustments); - $this->assertEquals('promotion', $second_adjustment->getType()); - $this->assertEquals(new Price('-2.40', 'USD'), $second_adjustment->getAmount()); + $this->assertEquals('tax', $second_adjustment->getType()); + $this->assertEquals(new Price('0.80', 'USD'), $second_adjustment->getAmount()); // User-specific adjustment added by TestAdjustmentProcessor. $result = $this->priceCalculator->calculate($this->secondVariation, 1, $second_context, ['test_adjustment_type']); From 3b20e5c4438c893f58b75079ba28cb7f7f847cad Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Mon, 12 Feb 2018 11:48:56 +0100 Subject: [PATCH 061/103] Issue #2944082 by bojanz: Provide a fallback singular_label/plural_label for adjustment types --- modules/order/src/AdjustmentTypeManager.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/order/src/AdjustmentTypeManager.php b/modules/order/src/AdjustmentTypeManager.php index 11be6cf03b..588450d311 100644 --- a/modules/order/src/AdjustmentTypeManager.php +++ b/modules/order/src/AdjustmentTypeManager.php @@ -4,6 +4,7 @@ use Drupal\commerce_order\Plugin\Commerce\AdjustmentType\AdjustmentType; use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; @@ -65,11 +66,20 @@ public function processDefinition(&$definition, $plugin_id) { parent::processDefinition($definition, $plugin_id); $definition['id'] = $plugin_id; - foreach (['label', 'singular_label', 'plural_label'] as $required_property) { + foreach (['label'] as $required_property) { if (empty($definition[$required_property])) { throw new PluginException(sprintf('The adjustment type %s must define the %s property.', $plugin_id, $required_property)); } } + // Provide fallback labels for contrib adjustment types defined before 2.4. + if (empty($definition['singular_label'])) { + $label = Unicode::strtolower($definition['label']); + $definition['singular_label'] = t('@label adjustment', ['@label' => $label]); + } + if (empty($definition['plural_label'])) { + $label = Unicode::strtolower($definition['label']); + $definition['plural_label'] = t('@label adjustments', ['@label' => $label]); + } } } From de2a698a21893f4c1aba6147b78aaccdfecb628d Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Mon, 12 Feb 2018 13:57:12 +0100 Subject: [PATCH 062/103] Issue #2928828 by mortona2k, bojanz: Order-level adjustment rounding is not performed --- modules/order/src/Entity/Order.php | 13 +- .../tests/src/Kernel/Entity/OrderTest.php | 120 +++++++++++------- 2 files changed, 83 insertions(+), 50 deletions(-) diff --git a/modules/order/src/Entity/Order.php b/modules/order/src/Entity/Order.php index d62ffcc924..503f66115b 100644 --- a/modules/order/src/Entity/Order.php +++ b/modules/order/src/Entity/Order.php @@ -360,9 +360,16 @@ public function recalculateTotalPrice() { $order_item_total = $order_item->getTotalPrice(); $total_price = $total_price ? $total_price->add($order_item_total) : $order_item_total; } - foreach ($this->collectAdjustments() as $adjustment) { - if (!$adjustment->isIncluded()) { - $total_price = $total_price->add($adjustment->getAmount()); + $adjustments = $this->collectAdjustments(); + if ($adjustments) { + /** @var \Drupal\commerce_order\AdjustmentTransformerInterface $adjustment_transformer */ + $adjustment_transformer = \Drupal::service('commerce_order.adjustment_transformer'); + $adjustments = $adjustment_transformer->combineAdjustments($adjustments); + $adjustments = $adjustment_transformer->roundAdjustments($adjustments); + foreach ($adjustments as $adjustment) { + if (!$adjustment->isIncluded()) { + $total_price = $total_price->add($adjustment->getAmount()); + } } } } diff --git a/modules/order/tests/src/Kernel/Entity/OrderTest.php b/modules/order/tests/src/Kernel/Entity/OrderTest.php index a7bff3d94b..c89e2762b0 100644 --- a/modules/order/tests/src/Kernel/Entity/OrderTest.php +++ b/modules/order/tests/src/Kernel/Entity/OrderTest.php @@ -181,8 +181,8 @@ public function testOrder() { $order->addItem($another_order_item); $this->assertEquals([$order_item, $another_order_item], $order->getItems()); $this->assertNotEmpty($order->hasItem($another_order_item)); - $this->assertEquals(new Price('8.00', 'USD'), $order->getTotalPrice()); + $adjustments = []; $adjustments[] = new Adjustment([ 'type' => 'custom', @@ -195,68 +195,28 @@ public function testOrder() { 'amount' => new Price('10.00', 'USD'), 'locked' => TRUE, ]); - // Included adjustments do not affect the order total. - $adjustments[] = new Adjustment([ - 'type' => 'tax', - 'label' => 'Tax', - 'amount' => new Price('12.00', 'USD'), - 'included' => TRUE, - ]); $order->addAdjustment($adjustments[0]); $order->addAdjustment($adjustments[1]); - $order->addAdjustment($adjustments[2]); $this->assertEquals($adjustments, $order->getAdjustments()); $collected_adjustments = $order->collectAdjustments(); $this->assertEquals($adjustments[0]->getAmount(), $collected_adjustments[0]->getAmount()); $this->assertEquals($adjustments[1]->getAmount(), $collected_adjustments[1]->getAmount()); - $this->assertEquals($adjustments[2]->getAmount(), $collected_adjustments[2]->getAmount()); $order->removeAdjustment($adjustments[0]); $this->assertEquals(new Price('8.00', 'USD'), $order->getSubtotalPrice()); $this->assertEquals(new Price('18.00', 'USD'), $order->getTotalPrice()); - $this->assertEquals([$adjustments[1], $adjustments[2]], $order->getAdjustments()); + $this->assertEquals([$adjustments[1]], $order->getAdjustments()); $order->setAdjustments($adjustments); $this->assertEquals($adjustments, $order->getAdjustments()); $this->assertEquals(new Price('17.00', 'USD'), $order->getTotalPrice()); - // Add an adjustment to the second order item, confirm it's a part of the - // order total, multiplied by quantity. - $order->removeItem($another_order_item); - $order_item_adjustments = []; - $order_item_adjustments[] = new Adjustment([ - 'type' => 'fee', - 'label' => 'Random fee', - 'amount' => new Price('5.00', 'USD'), - ]); - $order_item_adjustments[] = new Adjustment([ - 'type' => 'fee', - 'label' => 'Non-random fee', - 'amount' => new Price('7.00', 'USD'), - 'locked' => TRUE, - ]); - $multiplied_order_item_adjustments = []; - $multiplied_order_item_adjustments[] = new Adjustment([ + // Confirm that locked adjustments persist after clear. + // Custom adjustments are locked by default. + $order->addAdjustment(new Adjustment([ 'type' => 'fee', 'label' => 'Random fee', 'amount' => new Price('10.00', 'USD'), - ]); - $multiplied_order_item_adjustments[] = new Adjustment([ - 'type' => 'fee', - 'label' => 'Non-random fee', - 'amount' => new Price('14.00', 'USD'), - 'locked' => TRUE, - ]); - $another_order_item->setAdjustments($order_item_adjustments); - $order->addItem($another_order_item); - $this->assertEquals(new Price('41.00', 'USD'), $order->getTotalPrice()); - $collected_adjustments = $order->collectAdjustments(); - $this->assertEquals($multiplied_order_item_adjustments[0], $collected_adjustments[0]); - $this->assertEquals($multiplied_order_item_adjustments[1], $collected_adjustments[1]); - // Confirm that locked adjustments persist after clear. - // Custom adjustments are locked by default. - $order->setAdjustments($adjustments); + ])); $order->clearAdjustments(); - unset($adjustments[2]); - unset($multiplied_order_item_adjustments[0]); - $this->assertEquals(array_merge($multiplied_order_item_adjustments, $adjustments), $order->collectAdjustments()); + $this->assertEquals($adjustments, $order->getAdjustments()); $this->assertEquals('completed', $order->getState()->value); @@ -283,6 +243,72 @@ public function testOrder() { $this->assertEquals(635879900, $order->getCompletedTime()); } + /** + * Tests the order total recalculation logic. + * + * @covers ::recalculateTotalPrice + */ + public function testTotalCalculation() { + $order = Order::create([ + 'type' => 'default', + 'state' => 'completed', + ]); + $order->save(); + + /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */ + $order_item = OrderItem::create([ + 'type' => 'test', + 'quantity' => '2', + 'unit_price' => new Price('2.00', 'USD'), + ]); + $order_item->save(); + $order_item = $this->reloadEntity($order_item); + /** @var \Drupal\commerce_order\Entity\OrderItemInterface $another_order_item */ + $another_order_item = OrderItem::create([ + 'type' => 'test', + 'quantity' => '1', + 'unit_price' => new Price('3.00', 'USD'), + ]); + $another_order_item->save(); + $another_order_item = $this->reloadEntity($another_order_item); + + $adjustments = []; + $adjustments[0] = new Adjustment([ + 'type' => 'tax', + 'label' => 'Tax', + 'amount' => new Price('100.00', 'USD'), + 'included' => TRUE, + ]); + $adjustments[1] = new Adjustment([ + 'type' => 'tax', + 'label' => 'Tax', + 'amount' => new Price('2.121', 'USD'), + 'source_id' => 'us_sales_tax', + ]); + $adjustments[2] = new Adjustment([ + 'type' => 'tax', + 'label' => 'Tax', + 'amount' => new Price('5.344', 'USD'), + 'source_id' => 'us_sales_tax', + ]); + + // Included adjustments do not affect the order total. + $order->addAdjustment($adjustments[0]); + // Order item adjustments are multiplied by quantity. + $order_item->addAdjustment($adjustments[1]); + $another_order_item->addAdjustment($adjustments[2]); + $order->setItems([$order_item, $another_order_item]); + + $collected_adjustments = $order->collectAdjustments(); + $this->assertCount(3, $collected_adjustments); + $this->assertEquals($adjustments[1]->multiply('2'), $collected_adjustments[0]); + $this->assertEquals($adjustments[2], $collected_adjustments[1]); + $this->assertEquals($adjustments[0], $collected_adjustments[2]); + // The total will be correct only if the adjustments were correctly + // multiplied, combined, and rounded. + $this->assertEquals(new Price('16.59', 'USD'), $order->getTotalPrice()); + } + /** * Tests the order with order items using different currencies. * From 36926459b958f55b09c1f34f857d0f3cd4a0948c Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Mon, 12 Feb 2018 17:36:39 +0100 Subject: [PATCH 063/103] Issue #2926559 by bojanz, rszrama, lisastreeter: Apply order promotions to the subtotal, not the total --- .../Commerce/PromotionOffer/OrderFixedAmountOff.php | 13 +++++++------ .../Commerce/PromotionOffer/OrderPercentageOff.php | 4 ++-- .../tests/src/Kernel/PromotionOfferTest.php | 5 ++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderFixedAmountOff.php b/modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderFixedAmountOff.php index 45e5a07060..8a65e52c59 100644 --- a/modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderFixedAmountOff.php +++ b/modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderFixedAmountOff.php @@ -11,7 +11,7 @@ * * @CommercePromotionOffer( * id = "order_fixed_amount_off", - * label = @Translation("Fixed amount off the order total"), + * label = @Translation("Fixed amount off the order subtotal"), * entity_type = "commerce_order", * ) */ @@ -24,14 +24,15 @@ public function apply(EntityInterface $entity, PromotionInterface $promotion) { $this->assertEntity($entity); /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ $order = $entity; - $total_price = $order->getTotalPrice(); + $subtotal_price = $order->getSubTotalPrice(); $adjustment_amount = $this->getAmount(); - if ($total_price->getCurrencyCode() != $adjustment_amount->getCurrencyCode()) { + if ($subtotal_price->getCurrencyCode() != $adjustment_amount->getCurrencyCode()) { return; } - // Don't reduce the order total past zero. - if ($adjustment_amount->greaterThan($total_price)) { - $adjustment_amount = $total_price; + // The promotion amount can't be larger than the subtotal, to avoid + // potentially having a negative order total. + if ($adjustment_amount->greaterThan($subtotal_price)) { + $adjustment_amount = $subtotal_price; } $order->addAdjustment(new Adjustment([ diff --git a/modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderPercentageOff.php b/modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderPercentageOff.php index 95bb698cea..9574c1f42b 100644 --- a/modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderPercentageOff.php +++ b/modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderPercentageOff.php @@ -11,7 +11,7 @@ * * @CommercePromotionOffer( * id = "order_percentage_off", - * label = @Translation("Percentage off the order total"), + * label = @Translation("Percentage off the order subtotal"), * entity_type = "commerce_order", * ) */ @@ -24,7 +24,7 @@ public function apply(EntityInterface $entity, PromotionInterface $promotion) { $this->assertEntity($entity); /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ $order = $entity; - $adjustment_amount = $order->getTotalPrice()->multiply($this->getPercentage()); + $adjustment_amount = $order->getSubtotalPrice()->multiply($this->getPercentage()); $adjustment_amount = $this->rounder->round($adjustment_amount); $order->addAdjustment(new Adjustment([ diff --git a/modules/promotion/tests/src/Kernel/PromotionOfferTest.php b/modules/promotion/tests/src/Kernel/PromotionOfferTest.php index 489c6bc71d..281c145474 100644 --- a/modules/promotion/tests/src/Kernel/PromotionOfferTest.php +++ b/modules/promotion/tests/src/Kernel/PromotionOfferTest.php @@ -90,7 +90,6 @@ protected function setUp() { * Tests order percentage off. */ public function testOrderPercentageOff() { - // Use addOrderItem so the total is calculated. $order_item = OrderItem::create([ 'type' => 'test', 'quantity' => '2', @@ -164,12 +163,12 @@ public function testOrderFixedAmountOff() { $this->order->save(); $this->order = $this->reloadEntity($this->order); - // Offer amount larger than the order total. + // Offer amount larger than the order subtotal. $this->assertEquals(1, count($this->order->getAdjustments())); $this->assertEquals(new Price('-20.00', 'USD'), $this->order->getAdjustments()[0]->getAmount()); $this->assertEquals(new Price('0.00', 'USD'), $this->order->getTotalPrice()); - // Offer amount smaller than the order total. + // Offer amount smaller than the order subtotal. $order_item->setQuantity(2); $order_item->save(); $this->order->save(); From 1838bc92ec5bacf55d3061fb3cad0742c7e5368f Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Mon, 12 Feb 2018 20:17:44 +0100 Subject: [PATCH 064/103] Issue #2910190 by sorabh.v6, casey, harings_rob, bojanz: PriceTwigExtension::formatPrice() should accept empty values --- .../src/TwigExtension/PriceTwigExtension.php | 9 +++- .../commerce_price_test.module | 26 +----------- ...commerce-price-test-price-filter.html.twig | 2 +- .../src/Kernel/PriceTwigExtensionTest.php | 41 ++++++++++++++----- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/modules/price/src/TwigExtension/PriceTwigExtension.php b/modules/price/src/TwigExtension/PriceTwigExtension.php index 8a15f5d849..1fcda627e9 100644 --- a/modules/price/src/TwigExtension/PriceTwigExtension.php +++ b/modules/price/src/TwigExtension/PriceTwigExtension.php @@ -28,7 +28,9 @@ public function getName() { /** * Formats a price object/array. * - * Example: {{ order.getTotalPrice|commerce_price_format }} + * Examples: + * {{ order.getTotalPrice|commerce_price_format }} + * {{ order.getTotalPrice|commerce_price_format|default('N/A') }} * * @param mixed $price * Either a Price object, or an array with number and currency_code keys. @@ -39,10 +41,13 @@ public function getName() { * @throws \InvalidArgumentException */ public static function formatPrice($price) { + if (empty($price)) { + return ''; + } + if ($price instanceof Price) { $price = $price->toArray(); } - if (is_array($price) && isset($price['currency_code']) && isset($price['number'])) { $number_formatter = \Drupal::service('commerce_price.number_formatter_factory')->createInstance(); $currency_storage = \Drupal::entityTypeManager()->getStorage('commerce_currency'); diff --git a/modules/price/tests/modules/commerce_price_test/commerce_price_test.module b/modules/price/tests/modules/commerce_price_test/commerce_price_test.module index 74cbd369ab..a3d883cb5d 100644 --- a/modules/price/tests/modules/commerce_price_test/commerce_price_test.module +++ b/modules/price/tests/modules/commerce_price_test/commerce_price_test.module @@ -5,37 +5,15 @@ * Test module for Price. */ -use Drupal\commerce_price\Price; - /** * Implements hook_theme(). */ function commerce_price_test_theme($existing, $type, $theme, $path) { return [ - 'working_commerce_price' => [ - 'template' => 'commerce-price-test-price-filter', - 'variables' => [ - // Correct keys. - 'price' => [ - 'number' => '9.99', - 'currency_code' => 'USD', - ], - ], - ], - 'broken_commerce_price' => [ - 'template' => 'commerce-price-test-price-filter', - 'variables' => [ - // Incorrect keys. - 'price' => [ - 'numb' => '9.99', - 'currency_cod' => 'USD', - ], - ], - ], - 'commerce_price_object' => [ + 'commerce_price_test' => [ 'template' => 'commerce-price-test-price-filter', 'variables' => [ - 'price' => new Price('9.99', 'USD'), + 'price' => NULL, ], ], ]; diff --git a/modules/price/tests/modules/commerce_price_test/templates/commerce-price-test-price-filter.html.twig b/modules/price/tests/modules/commerce_price_test/templates/commerce-price-test-price-filter.html.twig index 6fcb1e8204..01d4f4e67b 100644 --- a/modules/price/tests/modules/commerce_price_test/templates/commerce-price-test-price-filter.html.twig +++ b/modules/price/tests/modules/commerce_price_test/templates/commerce-price-test-price-filter.html.twig @@ -1 +1 @@ -{{ price|commerce_price_format }} +{{ price|commerce_price_format|default('N/A') }} diff --git a/modules/price/tests/src/Kernel/PriceTwigExtensionTest.php b/modules/price/tests/src/Kernel/PriceTwigExtensionTest.php index 1bc456e2ef..4dc59a460b 100644 --- a/modules/price/tests/src/Kernel/PriceTwigExtensionTest.php +++ b/modules/price/tests/src/Kernel/PriceTwigExtensionTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\commerce_price\Kernel; +use Drupal\commerce_price\Price; use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase; /** @@ -19,30 +20,50 @@ class PriceTwigExtensionTest extends CommerceKernelTestBase { ]; /** - * Tests an improperly formatted price array. + * Tests passing an invalid value. */ - public function testBrokenPrice() { - $theme = ['#theme' => 'broken_commerce_price']; + public function testInvalidPrice() { + $theme = [ + '#theme' => 'commerce_price_test', + '#price' => [ + // Invalid keys. + 'numb' => '9.99', + 'currency_co' => 'USD', + ], + ]; $this->setExpectedException('InvalidArgumentException'); $this->render($theme); } /** - * Tests a properly formatted price array. + * Tests passing a valid value. */ - public function testWorkingPrice() { - $theme = ['#theme' => 'working_commerce_price']; + public function testValidPrice() { + $theme = [ + '#theme' => 'commerce_price_test', + '#price' => [ + 'number' => '9.99', + 'currency_code' => 'USD', + ], + ]; $this->render($theme); $this->assertText('$9.99'); + + $theme = [ + '#theme' => 'commerce_price_test', + '#price' => new Price('20.99', 'USD'), + ]; + $this->render($theme); + $this->assertText('$20.99'); } /** - * Tests a price object. + * Tests passing an empty value. */ - public function testPriceObject() { - $theme = ['#theme' => 'commerce_price_object']; + public function testEmptyPrice() { + $theme = ['#theme' => 'commerce_price_test']; $this->render($theme); - $this->assertText('$9.99'); + $this->assertText('N/A'); } } From 6c484405e66c599c8535aeb77ee36ac3d6da06f4 Mon Sep 17 00:00:00 2001 From: agoradesign Date: Thu, 22 Feb 2018 13:36:24 +0100 Subject: [PATCH 065/103] Issue #2946655 by agoradesign: _commerce_entity_theme_suggestions() needlessy includes the original hook in its suggestions --- commerce.module | 1 - modules/checkout/commerce_checkout.module | 1 - 2 files changed, 2 deletions(-) diff --git a/commerce.module b/commerce.module index 1c367a58cd..90a4274428 100644 --- a/commerce.module +++ b/commerce.module @@ -123,7 +123,6 @@ function _commerce_entity_theme_suggestions($entity_type_id, array $variables) { $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_'); $suggestions = []; - $suggestions[] = $original; $suggestions[] = $original . '__' . $sanitized_view_mode; $suggestions[] = $original . '__' . $entity->bundle(); $suggestions[] = $original . '__' . $entity->bundle() . '__' . $sanitized_view_mode; diff --git a/modules/checkout/commerce_checkout.module b/modules/checkout/commerce_checkout.module index af6eec7efc..6cb93de7fa 100644 --- a/modules/checkout/commerce_checkout.module +++ b/modules/checkout/commerce_checkout.module @@ -52,7 +52,6 @@ function commerce_checkout_theme() { function commerce_checkout_theme_suggestions_commerce_checkout_form(array $variables) { $original = $variables['theme_hook_original']; $suggestions = []; - $suggestions[] = $original; // If the checkout form has a sidebar, suggest the enhanced layout. if (isset($variables['form']['sidebar']) && Element::isVisibleElement($variables['form']['sidebar'])) { $suggestions[] = $original . '__with_sidebar'; From 5be779d875f4073bd616731570f1e226151f6735 Mon Sep 17 00:00:00 2001 From: agoradesign Date: Thu, 22 Feb 2018 13:40:10 +0100 Subject: [PATCH 066/103] Issue #2946659 by agoradesign: ProductForm is trying to attach an non existent library --- modules/product/src/Form/ProductForm.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/product/src/Form/ProductForm.php b/modules/product/src/Form/ProductForm.php index 7c99dda89a..f445aede62 100644 --- a/modules/product/src/Form/ProductForm.php +++ b/modules/product/src/Form/ProductForm.php @@ -169,9 +169,6 @@ public function form(array $form, FormStateInterface $form_state) { '#attributes' => [ 'class' => ['product-form-author'], ], - '#attached' => [ - 'library' => ['commerce_product/drupal.commerce_product'], - ], '#weight' => 90, '#optional' => TRUE, ]; From 9fcd817d869fb6c3785055370356c09accd60038 Mon Sep 17 00:00:00 2001 From: "robin.ingelbrecht" Date: Thu, 22 Feb 2018 14:43:44 +0100 Subject: [PATCH 067/103] Issue #2944954 by robin.ingelbrecht: AddToCartForm submit handler needs to "catch" the updated order item --- modules/cart/src/Form/AddToCartForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cart/src/Form/AddToCartForm.php b/modules/cart/src/Form/AddToCartForm.php index 5e97a0c3fa..40d79d4836 100644 --- a/modules/cart/src/Form/AddToCartForm.php +++ b/modules/cart/src/Form/AddToCartForm.php @@ -205,7 +205,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { if (!$cart) { $cart = $this->cartProvider->createCart($order_type_id, $store); } - $this->cartManager->addOrderItem($cart, $order_item, $form_state->get(['settings', 'combine'])); + $this->entity = $this->cartManager->addOrderItem($cart, $order_item, $form_state->get(['settings', 'combine'])); // Other submit handlers might need the cart ID. $form_state->set('cart_id', $cart->id()); } From 0b1f41cb79557050fd5abb24a4c74031e298118c Mon Sep 17 00:00:00 2001 From: agoradesign Date: Fri, 23 Feb 2018 18:58:43 +0100 Subject: [PATCH 068/103] Issue #2947365 by agoradesign: PromotionStorage::loadAvailable is using wrong second parameter for notExists() in query --- modules/promotion/src/PromotionStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/promotion/src/PromotionStorage.php b/modules/promotion/src/PromotionStorage.php index 7cbae1178e..ff0013ab61 100644 --- a/modules/promotion/src/PromotionStorage.php +++ b/modules/promotion/src/PromotionStorage.php @@ -83,7 +83,7 @@ public function loadAvailable(OrderInterface $order) { $query = $this->getQuery(); $or_condition = $query->orConditionGroup() ->condition('end_date', $today, '>=') - ->notExists('end_date', $today); + ->notExists('end_date'); $query ->condition('stores', [$order->getStoreId()], 'IN') ->condition('order_types', [$order->bundle()], 'IN') From 1db38f5c949711565adb7f5aa70b96eea3c9c818 Mon Sep 17 00:00:00 2001 From: osab Date: Sun, 25 Feb 2018 13:43:04 +0100 Subject: [PATCH 069/103] Issue #2917372 by osab, andypost, joachim, mglaman: incorrect use of translation in CartEventSubscriber --- modules/cart/src/EventSubscriber/CartEventSubscriber.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/cart/src/EventSubscriber/CartEventSubscriber.php b/modules/cart/src/EventSubscriber/CartEventSubscriber.php index 991c018c3a..63b05cae7e 100644 --- a/modules/cart/src/EventSubscriber/CartEventSubscriber.php +++ b/modules/cart/src/EventSubscriber/CartEventSubscriber.php @@ -7,7 +7,7 @@ use Drupal\commerce_cart\Event\CartEvents; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslationInterface; -use Drupal\Core\Link; +use Drupal\Core\Url; class CartEventSubscriber implements EventSubscriberInterface { @@ -41,9 +41,9 @@ public static function getSubscribedEvents() { */ public function displayAddToCartMessage(CartEntityAddEvent $event) { $purchased_entity = $event->getEntity(); - drupal_set_message($this->t('@entity added to @cart-link.', [ + drupal_set_message($this->t('@entity added to your cart.', [ '@entity' => $purchased_entity->label(), - '@cart-link' => Link::createFromRoute($this->t('your cart', [], ['context' => 'cart link']), 'commerce_cart.page')->toString(), + ':url' => Url::fromRoute('commerce_cart.page')->toString(), ])); } From 92891ba642210c6dab386ae55f312f7297394f1c Mon Sep 17 00:00:00 2001 From: huzooka Date: Mon, 26 Feb 2018 11:43:38 +0100 Subject: [PATCH 070/103] Issue #2940592 by mglaman, huzooka, bojanz: Inconsistent view mode of product variation fields in add to cart form --- .../AddToCartViewModeTest.php | 168 ++++++++++++++++++ .../ProductVariationWidgetBase.php | 2 +- 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 modules/cart/tests/src/FunctionalJavascript/AddToCartViewModeTest.php diff --git a/modules/cart/tests/src/FunctionalJavascript/AddToCartViewModeTest.php b/modules/cart/tests/src/FunctionalJavascript/AddToCartViewModeTest.php new file mode 100644 index 0000000000..875d1b3c8c --- /dev/null +++ b/modules/cart/tests/src/FunctionalJavascript/AddToCartViewModeTest.php @@ -0,0 +1,168 @@ +setComponent('purchased_entity', [ + 'type' => 'commerce_product_variation_title', + ]); + $order_item_form_display->save(); + + // Set up the Full view modes. + EntityViewMode::create([ + 'id' => 'commerce_product.full', + 'label' => 'Full', + 'targetEntityType' => 'commerce_product', + ])->save(); + EntityViewMode::create([ + 'id' => 'commerce_product_variation.full', + 'label' => 'Full', + 'targetEntityType' => 'commerce_product_variation', + ])->save(); + + // Use a different price widget for the two displays, to use that as + // an indicator of the right view mode being used. + $default_view_display = EntityViewDisplay::load('commerce_product_variation.default.default'); + if (!$default_view_display) { + $default_view_display = EntityViewDisplay::create([ + 'targetEntityType' => 'commerce_product_variation', + 'bundle' => 'default', + 'mode' => 'default', + 'status' => TRUE, + ]); + } + $default_view_display->setComponent('price', [ + 'type' => 'commerce_price_default', + ]); + $default_view_display->save(); + $full_view_display = EntityViewDisplay::load('commerce_product_variation.default.full'); + if (!$full_view_display) { + $full_view_display = EntityViewDisplay::create([ + 'targetEntityType' => 'commerce_product_variation', + 'bundle' => 'default', + 'mode' => 'full', + 'status' => TRUE, + ]); + } + $full_view_display->setComponent('price', [ + 'type' => 'commerce_price_plain', + ]); + $full_view_display->save(); + + $this->firstVariation = $this->createEntity('commerce_product_variation', [ + 'title' => 'First variation', + 'type' => 'default', + 'sku' => 'first-variation', + 'price' => [ + 'number' => 10, + 'currency_code' => 'USD', + ], + ]); + $this->secondVariation = $this->createEntity('commerce_product_variation', [ + 'title' => 'Second variation', + 'type' => 'default', + 'sku' => 'second-variation', + 'price' => [ + 'number' => 20, + 'currency_code' => 'USD', + ], + ]); + $this->product = $this->createEntity('commerce_product', [ + 'type' => 'default', + 'title' => 'Test product', + 'stores' => [$this->store], + 'variations' => [$this->firstVariation, $this->secondVariation], + ]); + } + + /** + * Tests changing the product variation. + * + * The `commerce_product_variation.default.full` configuration uses the + * `commerce_price_plain` formatter, but the default view mode still uses the + * `commerce_price_default` formatter. The AJAX refresh should return currency + * in the plain format of ##.00 USD and not $##.00. + */ + public function testAjaxChange() { + $this->drupalGet($this->product->toUrl()); + + $page = $this->getSession()->getPage(); + $renderer = $this->container->get('renderer'); + + $first_variation_price = [ + '#theme' => 'commerce_price_plain', + '#number' => $this->firstVariation->getPrice()->getNumber(), + '#currency' => Currency::load($this->firstVariation->getPrice()->getCurrencyCode()), + ]; + $first_variation_price = trim($renderer->renderPlain($first_variation_price)); + $second_variation_price = [ + '#theme' => 'commerce_price_plain', + '#number' => $this->secondVariation->getPrice()->getNumber(), + '#currency' => Currency::load($this->secondVariation->getPrice()->getCurrencyCode()), + ]; + $second_variation_price = trim($renderer->renderPlain($second_variation_price)); + + $price_field_selector = '.product--variation-field--variation_price__' . $this->product->id(); + + $this->assertSession()->elementExists('css', $price_field_selector); + $this->assertSession()->elementTextContains('css', $price_field_selector . ' .field__item', $first_variation_price); + $this->assertSession()->fieldValueEquals('purchased_entity[0][variation]', $this->firstVariation->id()); + $page->selectFieldOption('purchased_entity[0][variation]', $this->secondVariation->id()); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->assertSession()->elementExists('css', $price_field_selector); + $this->assertSession()->elementTextContains('css', $price_field_selector . ' .field__item', $second_variation_price); + + $page->selectFieldOption('purchased_entity[0][variation]', $this->firstVariation->id()); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->elementExists('css', $price_field_selector); + $this->assertSession()->elementTextContains('css', $price_field_selector . ' .field__item', $first_variation_price); + } + +} diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php index d2a1cbce86..c73c3f707e 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php @@ -123,7 +123,7 @@ public static function ajaxRefresh(array $form, FormStateInterface $form_state) } /** @var \Drupal\commerce_product\ProductVariationFieldRendererInterface $variation_field_renderer */ $variation_field_renderer = \Drupal::service('commerce_product.variation_field_renderer'); - $view_mode = $form_state->get('form_display')->getMode(); + $view_mode = $form_state->get('view_mode'); $variation_field_renderer->replaceRenderedFields($response, $variation, $view_mode); // Allow modules to add arbitrary ajax commands to the response. $event = new ProductVariationAjaxChangeEvent($variation, $response, $view_mode); From 4285fc91a6787fc735020e22c9beaf8ea5b9dc66 Mon Sep 17 00:00:00 2001 From: mglaman Date: Tue, 27 Feb 2018 11:32:49 +0100 Subject: [PATCH 071/103] Issue #2941834 by mglaman, Lukas von Blarer: Add to cart form doesn't use the right default translation when providing the v=ID GET parameter --- .../AddToCartMultiAttributeTest.php | 10 ++++ .../AddToCartMultilingualTest.php | 50 ++++++++++++++++++- .../MultipleCartFormsTest.php | 2 - .../ProductVariationAttributesWidget.php | 6 +-- .../ProductVariationTitleWidget.php | 6 +-- .../ProductVariationWidgetBase.php | 22 ++++++++ .../ProductVariationFieldInjectionTest.php | 31 ++++++++++++ 7 files changed, 113 insertions(+), 14 deletions(-) diff --git a/modules/cart/tests/src/FunctionalJavascript/AddToCartMultiAttributeTest.php b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultiAttributeTest.php index de50855286..4e326b6db0 100644 --- a/modules/cart/tests/src/FunctionalJavascript/AddToCartMultiAttributeTest.php +++ b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultiAttributeTest.php @@ -166,6 +166,16 @@ public function testRenderedVariationFields() { $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][attributes][attribute_color]', 'Magenta'); $this->waitForAjaxToFinish(); $this->assertSession()->pageTextContains($variation2->getSku()); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'Magenta'); + + // Load variation2 directly via the url (?v=). + $this->drupalGet($variation2->toUrl()); + $this->assertSession()->pageTextContains($variation2->getSku()); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'Magenta'); + $this->getSession()->getPage()->selectFieldOption('purchased_entity[0][attributes][attribute_color]', 'Cyan'); + $this->waitForAjaxToFinish(); + $this->assertSession()->pageTextContains($variation1->getSku()); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'Cyan'); } } diff --git a/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php index 98f9ee4899..a9fbb82a48 100644 --- a/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php +++ b/modules/cart/tests/src/FunctionalJavascript/AddToCartMultilingualTest.php @@ -179,7 +179,7 @@ public function testProductVariationAttributesWidget() { // Change the site language. $this->config('system.site')->set('default_langcode', 'fr')->save(); - drupal_flush_all_caches(); + $this->rebuildContainer(); $this->drupalGet($this->product->getTranslation('fr')->toUrl()); // Use AJAX to change the size to Medium, keeping the color on Red. @@ -207,6 +207,27 @@ public function testProductVariationAttributesWidget() { $this->assertOrderItemInOrder($this->variations[5]->getTranslation('fr'), $order_items[1]); } + /** + * Tests the attribute widget default values with a variation url (?v=). + */ + public function testProductVariationAttributesWidgetFromUrl() { + $variation = $this->variations[5]; + $this->drupalGet($variation->toUrl()); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'Blue'); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_size]', 'Medium'); + $this->getSession()->getPage()->pressButton('Add to cart'); + + // Change the site language. + $this->config('system.site')->set('default_langcode', 'fr')->save(); + $this->rebuildContainer(); + + $variation = $variation->getTranslation('fr'); + $this->drupalGet($variation->toUrl()); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_color]', 'FR Blue'); + $this->assertAttributeSelected('purchased_entity[0][attributes][attribute_size]', 'FR Medium'); + $this->getSession()->getPage()->pressButton('Add to cart'); + } + /** * Tests the title widget has translated variation title. */ @@ -224,7 +245,7 @@ public function testProductVariationTitleWidget() { // Change the site language. $this->config('system.site')->set('default_langcode', 'fr')->save(); - drupal_flush_all_caches(); + $this->rebuildContainer(); $this->drupalGet($this->product->getTranslation('fr')->toUrl()); // Use AJAX to change the size to Medium, keeping the color on Red. @@ -246,4 +267,29 @@ public function testProductVariationTitleWidget() { $this->assertOrderItemInOrder($this->variations[5]->getTranslation('fr'), $order_items[1]); } + /** + * Tests the title widget default values with a variation url (?v=). + */ + public function testProductVariationTitleWidgetFromUrl() { + $order_item_form_display = EntityFormDisplay::load('commerce_order_item.default.add_to_cart'); + $order_item_form_display->setComponent('purchased_entity', [ + 'type' => 'commerce_product_variation_title', + ]); + $order_item_form_display->save(); + + $variation = $this->variations[5]; + $this->drupalGet($variation->toUrl()); + $this->assertSession()->selectExists('purchased_entity[0][variation]'); + $this->assertAttributeSelected('purchased_entity[0][variation]', 'My Super Product - Blue, Medium'); + + // Change the site language. + $this->config('system.site')->set('default_langcode', 'fr')->save(); + $this->rebuildContainer(); + + $variation = $variation->getTranslation('fr'); + $this->drupalGet($variation->toUrl()); + $this->assertSession()->selectExists('purchased_entity[0][variation]'); + $this->assertAttributeSelected('purchased_entity[0][variation]', 'Mon super produit - FR Blue, FR Medium'); + } + } diff --git a/modules/cart/tests/src/FunctionalJavascript/MultipleCartFormsTest.php b/modules/cart/tests/src/FunctionalJavascript/MultipleCartFormsTest.php index eccac21bce..1a26cd7d13 100644 --- a/modules/cart/tests/src/FunctionalJavascript/MultipleCartFormsTest.php +++ b/modules/cart/tests/src/FunctionalJavascript/MultipleCartFormsTest.php @@ -145,8 +145,6 @@ public function testUniqueAddToCartFormIds() { /** * Tests that a page with multiple add to cart forms works properly. - * - * @group debug */ public function testMultipleRenderedProducts() { // View of rendered products, each containing an add to cart form. diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php index 98361f868b..8a347cf409 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php @@ -138,11 +138,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $selected_variation = $order_item->getPurchasedEntity(); } else { - $selected_variation = $this->variationStorage->loadFromContext($product); - // The returned variation must also be enabled. - if (!in_array($selected_variation, $variations)) { - $selected_variation = reset($variations); - } + $selected_variation = $this->getDefaultVariation($product, $variations); } } diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php index 7a431d0181..0b348d4b25 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationTitleWidget.php @@ -104,11 +104,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $selected_variation = $this->selectVariationFromUserInput($variations, $user_input); } else { - $selected_variation = $this->variationStorage->loadFromContext($product); - // The returned variation must also be enabled. - if (!in_array($selected_variation, $variations)) { - $selected_variation = reset($variations); - } + $selected_variation = $this->getDefaultVariation($product, $variations); } // Set the selected variation in the form state for our AJAX callback. diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php index c73c3f707e..71fe8d3cc3 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php @@ -133,6 +133,28 @@ public static function ajaxRefresh(array $form, FormStateInterface $form_state) return $response; } + /** + * Gets the default variation for the widget. + * + * @param \Drupal\commerce_product\Entity\ProductInterface $product + * The product. + * @param array $variations + * An array of available variations. + * + * @return \Drupal\commerce_product\Entity\ProductVariationInterface + * The default variation. + */ + protected function getDefaultVariation(ProductInterface $product, array $variations) { + $langcode = $product->language()->getId(); + $selected_variation = $this->variationStorage->loadFromContext($product); + $selected_variation = $this->entityRepository->getTranslationFromContext($selected_variation, $langcode); + // The returned variation must also be enabled. + if (!in_array($selected_variation, $variations)) { + $selected_variation = reset($variations); + } + return $selected_variation; + } + /** * Gets the enabled variations for the product. * diff --git a/modules/product/tests/src/Functional/ProductVariationFieldInjectionTest.php b/modules/product/tests/src/Functional/ProductVariationFieldInjectionTest.php index e80323d4c3..a06b301cc7 100644 --- a/modules/product/tests/src/Functional/ProductVariationFieldInjectionTest.php +++ b/modules/product/tests/src/Functional/ProductVariationFieldInjectionTest.php @@ -112,4 +112,35 @@ public function testInjectedVariationDefault() { $this->assertSession()->pageTextContains('$999.00'); } + /** + * Tests that the default injected variation respects the URL context. + */ + public function testInjectedVariationFromUrl() { + $this->drupalGet($this->product->toUrl()); + // We hide the SKU by default. + $this->assertSession()->pageTextNotContains('INJECTION-CYAN'); + + /** @var \Drupal\Core\Entity\Entity\EntityViewDisplay $variation_view_display */ + $variation_view_display = commerce_get_entity_display('commerce_product_variation', 'default', 'view'); + $variation_view_display->removeComponent('title'); + $variation_view_display->setComponent('attribute_color', [ + 'label' => 'above', + 'type' => 'entity_reference_label', + ]); + $variation_view_display->setComponent('sku', [ + 'label' => 'hidden', + 'type' => 'string', + ]); + $variation_view_display->save(); + + $this->drupalGet($this->product->toUrl()); + $this->assertSession()->pageTextContains('INJECTION-CYAN'); + + $variations = $this->product->getVariations(); + foreach ($variations as $variation) { + $this->drupalGet($variation->toUrl()); + $this->assertSession()->pageTextContains($variation->label()); + } + } + } From 8b7cc9b50dc089cd7b69e0b33750b6930e07f904 Mon Sep 17 00:00:00 2001 From: lisastreeter Date: Tue, 27 Feb 2018 16:10:32 +0100 Subject: [PATCH 072/103] Issue #2948027 by lisastreeter: Make the Promotion End Date Widget a generic Commerce widget --- .../src => src}/Plugin/Field/FieldWidget/EndDateWidget.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {modules/promotion/src => src}/Plugin/Field/FieldWidget/EndDateWidget.php (97%) diff --git a/modules/promotion/src/Plugin/Field/FieldWidget/EndDateWidget.php b/src/Plugin/Field/FieldWidget/EndDateWidget.php similarity index 97% rename from modules/promotion/src/Plugin/Field/FieldWidget/EndDateWidget.php rename to src/Plugin/Field/FieldWidget/EndDateWidget.php index 505e13af08..df2bcd336b 100644 --- a/modules/promotion/src/Plugin/Field/FieldWidget/EndDateWidget.php +++ b/src/Plugin/Field/FieldWidget/EndDateWidget.php @@ -1,6 +1,6 @@ Date: Sun, 4 Mar 2018 23:19:19 +0100 Subject: [PATCH 073/103] Issue #2923337 followup: Fix regression that broke theme suggestions for variation field templates. --- modules/product/commerce_product.module | 24 ++++++++++++++++++++++ modules/product/src/ProductViewBuilder.php | 2 -- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/modules/product/commerce_product.module b/modules/product/commerce_product.module index 2f6ad469df..a94eb1a1a7 100644 --- a/modules/product/commerce_product.module +++ b/modules/product/commerce_product.module @@ -43,6 +43,30 @@ function commerce_product_entity_view_display_update(EntityInterface $entity) { } } +/** + * Implements hook_theme_registry_alter(). + */ +function commerce_product_theme_registry_alter(&$theme_registry) { + // The preprocess function must run after quickedit_preprocess_field(). + $theme_registry['field']['preprocess functions'][] = 'commerce_product_remove_quickedit'; +} + +/** + * Turn off Quick Edit for injected variation fields, to avoid warnings. + */ +function commerce_product_remove_quickedit(&$variables) { + $entity_type_id = $variables['element']['#entity_type']; + if ($entity_type_id != 'commerce_product_variation' || empty($variables['#ajax_replace_class'])) { + return; + } + + if (isset($variables['attributes']['data-quickedit-field-id'])) { + unset($variables['attributes']['data-quickedit-field-id']); + $context_key = array_search('user.permissions', $variables['#cache']['contexts']); + unset($variables['#cache']['contexts'][$context_key]); + } +} + /** * Implements hook_theme(). */ diff --git a/modules/product/src/ProductViewBuilder.php b/modules/product/src/ProductViewBuilder.php index a78283dfd4..92cb7dc5aa 100644 --- a/modules/product/src/ProductViewBuilder.php +++ b/modules/product/src/ProductViewBuilder.php @@ -78,8 +78,6 @@ protected function alterBuild(array &$build, EntityInterface $entity, EntityView $attribute_field_names = $variation->getAttributeFieldNames(); $rendered_fields = $this->variationFieldRenderer->renderFields($variation, $view_mode); foreach ($rendered_fields as $field_name => $rendered_field) { - // Turn off Quick Edit for injected variation fields, to avoid warnings. - $rendered_field['#view_mode'] = '_custom'; // Group attribute fields to allow them to be excluded together. if (in_array($field_name, $attribute_field_names)) { $build['variation_attributes']['variation_' . $field_name] = $rendered_field; From 2726f7dfee8d12fc9ef9b155d50f712a20c79e46 Mon Sep 17 00:00:00 2001 From: lisastreeter Date: Wed, 7 Mar 2018 11:44:19 +0100 Subject: [PATCH 074/103] Issue #2902882 by lisastreeter, bojanz, mglaman: Allow for bulk creation of coupons --- modules/promotion/commerce_promotion.info.yml | 1 - .../commerce_promotion.links.action.yml | 6 + .../commerce_promotion.permissions.yml | 2 + .../promotion/commerce_promotion.routing.yml | 13 + .../promotion/commerce_promotion.services.yml | 4 + modules/promotion/src/CouponCodeGenerator.php | 109 +++++++ .../src/CouponCodeGeneratorInterface.php | 40 +++ modules/promotion/src/CouponCodePattern.php | 111 +++++++ .../promotion/src/Form/CouponGenerateForm.php | 290 ++++++++++++++++++ .../src/FunctionalJavascript/CouponTest.php | 43 +++ .../src/Kernel/CouponCodeGeneratorTest.php | 154 ++++++++++ 11 files changed, 772 insertions(+), 1 deletion(-) create mode 100644 modules/promotion/commerce_promotion.permissions.yml create mode 100644 modules/promotion/src/CouponCodeGenerator.php create mode 100644 modules/promotion/src/CouponCodeGeneratorInterface.php create mode 100644 modules/promotion/src/CouponCodePattern.php create mode 100644 modules/promotion/src/Form/CouponGenerateForm.php create mode 100644 modules/promotion/tests/src/Kernel/CouponCodeGeneratorTest.php diff --git a/modules/promotion/commerce_promotion.info.yml b/modules/promotion/commerce_promotion.info.yml index 17cabc3390..7cba720d77 100644 --- a/modules/promotion/commerce_promotion.info.yml +++ b/modules/promotion/commerce_promotion.info.yml @@ -6,5 +6,4 @@ core: 8.x dependencies: - commerce:commerce - commerce:commerce_order - - inline_entity_form:inline_entity_form - drupal:options diff --git a/modules/promotion/commerce_promotion.links.action.yml b/modules/promotion/commerce_promotion.links.action.yml index 66dda9b1e9..85149571a0 100644 --- a/modules/promotion/commerce_promotion.links.action.yml +++ b/modules/promotion/commerce_promotion.links.action.yml @@ -9,3 +9,9 @@ entity.commerce_promotion_coupon.add_form: title: 'Add coupon' appears_on: - entity.commerce_promotion_coupon.collection + +entity.commerce_promotion_coupon.generate_form: + route_name: entity.commerce_promotion_coupon.generate_form + title: 'Generate coupons' + appears_on: + - entity.commerce_promotion_coupon.collection diff --git a/modules/promotion/commerce_promotion.permissions.yml b/modules/promotion/commerce_promotion.permissions.yml new file mode 100644 index 0000000000..977c1ab7b3 --- /dev/null +++ b/modules/promotion/commerce_promotion.permissions.yml @@ -0,0 +1,2 @@ +bulk generate commerce_promotion_coupon: + title: 'Bulk generate coupons' diff --git a/modules/promotion/commerce_promotion.routing.yml b/modules/promotion/commerce_promotion.routing.yml index 0a949b02d3..594ae95717 100644 --- a/modules/promotion/commerce_promotion.routing.yml +++ b/modules/promotion/commerce_promotion.routing.yml @@ -10,3 +10,16 @@ entity.commerce_promotion_coupon.collection: type: 'entity:commerce_promotion' requirements: _entity_access: 'commerce_promotion.update' + +entity.commerce_promotion_coupon.generate_form: + path: '/promotion/{commerce_promotion}/coupons/generate' + defaults: + _form: '\Drupal\commerce_promotion\Form\CouponGenerateForm' + _title: 'Generate coupons' + options: + _admin_route: TRUE + parameters: + commerce_promotion: + type: 'entity:commerce_promotion' + requirements: + _permission: 'bulk generate commerce_promotion_coupon' diff --git a/modules/promotion/commerce_promotion.services.yml b/modules/promotion/commerce_promotion.services.yml index 7997432091..e393b0d252 100644 --- a/modules/promotion/commerce_promotion.services.yml +++ b/modules/promotion/commerce_promotion.services.yml @@ -25,3 +25,7 @@ services: arguments: ['@entity_type.manager', '@commerce_promotion.usage'] tags: - { name: event_subscriber } + + commerce_promotion.coupon_code_generator: + class: Drupal\commerce_promotion\CouponCodeGenerator + arguments: ['@database'] diff --git a/modules/promotion/src/CouponCodeGenerator.php b/modules/promotion/src/CouponCodeGenerator.php new file mode 100644 index 0000000000..feddbb4e50 --- /dev/null +++ b/modules/promotion/src/CouponCodeGenerator.php @@ -0,0 +1,109 @@ +connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function validatePattern(CouponCodePattern $pattern, $quantity = 1) { + $character_set = $this->getCharacterSet($pattern->getType()); + $combinations = pow(count($character_set), $pattern->getLength()); + return !($quantity > $combinations); + } + + /** + * {@inheritdoc} + */ + public function generateCodes(CouponCodePattern $pattern, $quantity = 1) { + $character_set = $this->getCharacterSet($pattern->getType()); + $character_set_size = count($character_set); + $length = $pattern->getLength(); + $prefix = $pattern->getPrefix(); + $suffix = $pattern->getSuffix(); + + // Generate twice the requested quantity, to improve chances of having + // the needed quantity after removing non-unique/existing codes. + $codes = []; + for ($i = 0; $i < ($quantity * 2); $i++) { + $code = ''; + while (strlen($code) < $length) { + $random_index = mt_rand(0, $character_set_size - 1); + $code .= $character_set[$random_index]; + } + $codes[] = $prefix . $code . $suffix; + } + $codes = array_unique($codes); + + // Remove codes which already exist in the database. + $result = $this->connection->select('commerce_promotion_coupon', 'c') + ->fields('c', ['code']) + ->condition('code', $codes, 'IN') + ->execute(); + $existing_codes = $result->fetchCol(); + $codes = array_diff($codes, $existing_codes); + + return array_slice($codes, 0, $quantity); + } + + /** + * Gets the character set for the given pattern type. + * + * @param string $pattern_type + * The pattern type. + * + * @return string[] + * The character set. + */ + protected function getCharacterSet($pattern_type) { + $characters = []; + switch ($pattern_type) { + // No 'I', 'O', 'i', 'l', '0', '1' to avoid recognition issues. + case CouponCodePattern::ALPHANUMERIC: + $characters = [ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', + 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '2', '3', '4', '5', '6', '7', '8', '9', + ]; + break; + + case CouponCodePattern::ALPHABETIC: + // No 'I', 'i', 'l' to avoid recognition issues. + $characters = [ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + ]; + break; + + case CouponCodePattern::NUMERIC: + $characters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + } + + return $characters; + } + +} diff --git a/modules/promotion/src/CouponCodeGeneratorInterface.php b/modules/promotion/src/CouponCodeGeneratorInterface.php new file mode 100644 index 0000000000..09e35f8fd7 --- /dev/null +++ b/modules/promotion/src/CouponCodeGeneratorInterface.php @@ -0,0 +1,40 @@ +type = $type; + $this->prefix = $prefix; + $this->suffix = $suffix; + $this->length = $length; + } + + /** + * Gets the pattern type. + * + * @return string + * The pattern type. + */ + public function getType() { + return $this->type; + } + + /** + * Gets the prefix. + * + * @return string + * The prefix. + */ + public function getPrefix() { + return $this->prefix; + } + + /** + * Gets the suffix. + * + * @return string + * The suffix. + */ + public function getSuffix() { + return $this->suffix; + } + + /** + * Gets the length. + * + * @return int + * The length. + */ + public function getLength() { + return $this->length; + } + +} diff --git a/modules/promotion/src/Form/CouponGenerateForm.php b/modules/promotion/src/Form/CouponGenerateForm.php new file mode 100644 index 0000000000..f69c712141 --- /dev/null +++ b/modules/promotion/src/Form/CouponGenerateForm.php @@ -0,0 +1,290 @@ +couponCodeGenerator = $coupon_code_generator; + $this->promotion = $current_route_match->getParameter('commerce_promotion'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('commerce_promotion.coupon_code_generator'), + $container->get('current_route_match') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'commerce_coupon_generate_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['quantity'] = [ + '#type' => 'number', + '#title' => $this->t('Number of coupons'), + '#required' => TRUE, + '#default_value' => '10', + '#min' => 1, + '#max' => 1000, + '#step' => 1, + ]; + $form['pattern'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Coupon code pattern'), + ]; + $form['pattern']['format'] = [ + '#type' => 'select', + '#title' => $this->t('Format'), + '#required' => TRUE, + '#options' => [ + 'alphanumeric' => $this->t('Alphanumeric'), + 'alphabetic' => $this->t('Alphabetic'), + 'numeric' => $this->t('Numeric'), + ], + '#default_value' => 'alphanumeric', + ]; + $form['pattern']['prefix'] = [ + '#type' => 'textfield', + '#title' => $this->t('Prefix'), + '#size' => 20, + ]; + $form['pattern']['suffix'] = [ + '#type' => 'textfield', + '#title' => $this->t('Suffix'), + '#size' => 20, + ]; + $form['pattern']['length'] = [ + '#type' => 'number', + '#title' => $this->t('Length'), + '#description' => $this->t('Length does not include prefix/suffix.'), + '#required' => TRUE, + '#default_value' => 8, + '#min' => 1, + ]; + $form['limit'] = [ + '#type' => 'radios', + '#title' => $this->t('Number of uses per coupon'), + '#options' => [ + 0 => $this->t('Unlimited'), + 1 => $this->t('Limited number of uses'), + ], + '#default_value' => 1, + ]; + $form['usage_limit'] = [ + '#type' => 'number', + '#title' => $this->t('Number of uses'), + '#title_display' => 'invisible', + '#default_value' => 1, + '#min' => 1, + '#states' => [ + 'invisible' => [ + ':input[name="limit"]' => ['value' => 0], + ], + ], + ]; + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Generate'), + '#button_type' => 'primary', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + // Make sure that the total length doesn't exceed the database limit. + $code_length = strlen($values['prefix']) + strlen($values['suffix']) + $values['length']; + if ($code_length > self::MAX_CODE_LENGTH) { + $form_state->setError($form['pattern'], $this->t('The total pattern length (@coupon_length) exceeds the maximum length allowed (@max_length).', [ + '@coupon_length' => $code_length, + '@max_length' => self::MAX_CODE_LENGTH, + ])); + } + + // Validate that pattern for the given quantity. + $quantity = $values['quantity']; + $pattern = new CouponCodePattern($values['format'], $values['prefix'], $values['suffix'], $values['length']); + if (!$this->couponCodeGenerator->validatePattern($pattern, $quantity)) { + $form_state->setError($form['pattern'], $this->t('This pattern cannot be used to generate @quantity coupons.', [ + '@quantity' => $quantity, + ])); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + $quantity = $values['quantity']; + $coupon_values = [ + 'promotion_id' => $this->promotion->id(), + 'usage_limit' => $values['usage_limit'], + ]; + $pattern = new CouponCodePattern($values['format'], $values['prefix'], $values['suffix'], $values['length']); + + $batch = [ + 'title' => $this->t('Generating coupons'), + 'progress_message' => '', + 'operations' => [ + [ + [get_class($this), 'processBatch'], + [$quantity, $coupon_values, $pattern], + ], + ], + 'finished' => [$this, 'finishBatch'], + ]; + batch_set($batch); + $form_state->setRedirect('entity.commerce_promotion_coupon.collection', [ + 'commerce_promotion' => $this->promotion->id(), + ]); + } + + /** + * Processes the batch and generates the coupons. + * + * @param int $quantity + * The number of coupons to generate. + * @param string[] $coupon_values + * The initial coupon entity values. + * @param \Drupal\commerce_promotion\CouponCodePattern $pattern + * The pattern. + * @param array $context + * The batch context information. + */ + public static function processBatch($quantity, array $coupon_values, CouponCodePattern $pattern, array &$context) { + if (empty($context['sandbox'])) { + $context['sandbox']['total_quantity'] = (int) $quantity; + $context['sandbox']['created'] = 0; + $context['results']['codes'] = []; + $context['results']['total_quantity'] = $quantity; + } + + $total_quantity = $context['sandbox']['total_quantity']; + $created = &$context['sandbox']['created']; + $remaining = $total_quantity - $created; + + $coupon_storage = \Drupal::entityTypeManager()->getStorage('commerce_promotion_coupon'); + $limit = ($remaining < self::BATCH_SIZE) ? $remaining : self::BATCH_SIZE; + $coupon_code_generator = \Drupal::service('commerce_promotion.coupon_code_generator'); + $codes = $coupon_code_generator->generateCodes($pattern, $limit); + if (!empty($codes)) { + foreach ($codes as $code) { + $coupon = $coupon_storage->create([ + 'code' => $code, + ] + $coupon_values); + $coupon->save(); + $context['results']['codes'][] = $code; + $created++; + } + $context['message'] = t('Creating coupon @created of @total_quantity', [ + '@created' => $created, + '@total_quantity' => $total_quantity, + ]); + $context['finished'] = $created / $total_quantity; + } + else { + $context['finished'] = 1; + } + } + + /** + * Batch finished callback: display batch statistics. + * + * @param bool $success + * Indicates whether the batch has completed successfully. + * @param mixed[] $results + * The array of results gathered by the batch processing. + * @param string[] $operations + * If $success is FALSE, contains the operations that remained unprocessed. + */ + public static function finishBatch($success, array $results, array $operations) { + if ($success) { + $created = count($results['codes']); + // An incomplete set of coupons was generated. + if ($created != $results['total_quantity']) { + drupal_set_message(t('Generated %created out of %total requested coupons. Consider adding a unique prefix/suffix or increasing the pattern length to improve results.', [ + '%created' => $created, + '%total' => $results['total_quantity'], + ]), 'warning'); + } + else { + drupal_set_message(\Drupal::translation()->formatPlural( + $created, + 'Generated 1 coupon.', + 'Generated @count coupons.' + )); + } + } + else { + $error_operation = reset($operations); + drupal_set_message(t('An error occurred while processing @operation with arguments: @args', [ + '@operation' => $error_operation[0], + '@args' => print_r($error_operation[0], TRUE), + ])); + } + } + +} diff --git a/modules/promotion/tests/src/FunctionalJavascript/CouponTest.php b/modules/promotion/tests/src/FunctionalJavascript/CouponTest.php index d4525040a6..e21c52e389 100644 --- a/modules/promotion/tests/src/FunctionalJavascript/CouponTest.php +++ b/modules/promotion/tests/src/FunctionalJavascript/CouponTest.php @@ -41,6 +41,7 @@ class CouponTest extends CommerceBrowserTestBase { protected function getAdministratorPermissions() { return array_merge([ 'administer commerce_promotion', + 'bulk generate commerce_promotion_coupon', ], parent::getAdministratorPermissions()); } @@ -132,4 +133,46 @@ public function testDeleteCoupon() { $this->assertFalse($coupon_exists); } + /** + * Tests bulk generation of coupons. + */ + public function testGenerateCoupons() { + $coupon_quantity = 52; + + $this->drupalGet('/promotion/' . $this->promotion->id() . '/coupons'); + $this->getSession()->getPage()->clickLink('Generate coupons'); + + // Check the integrity of the form and set values. + $this->assertSession()->fieldExists('format'); + $this->getSession()->getPage()->selectFieldOption('format', 'numeric'); + + $this->assertSession()->fieldExists('quantity'); + $this->getSession()->getPage()->fillField('quantity', (string) $coupon_quantity); + + // Use POST here to invoke batch_process() in the internal browser. + $this->drupalPostForm(NULL, [], t('Generate')); + + // Wait until the id="updateprogress" element is gone, or timeout after 3 minutes. + $this->getSession()->wait(180000, 'jQuery("#updateprogress").length === 0'); + + $this->assertSession()->pageTextContains("Generated $coupon_quantity coupons."); + $coupon_count = $this->getSession()->getPage()->findAll('xpath', '//table/tbody/tr/td[text()="0 / 1"]'); + + $this->assertEquals(count($coupon_count), $coupon_quantity, 'Coupons exist in the table.'); + + $coupons = Coupon::loadMultiple(); + $this->assertEquals(count($coupons), $coupon_quantity, 'Coupons created'); + foreach ($coupons as $id => $coupon) { + $this->assertEquals($this->promotion->id(), $coupon->getPromotionId()); + $this->assertTrue(ctype_digit($coupon->getCode())); + $this->assertEquals(strlen($coupon->getCode()), 8); + } + + \Drupal::service('entity_type.manager')->getStorage('commerce_promotion')->resetCache([$this->promotion->id()]); + $this->promotion = Promotion::load($this->promotion->id()); + foreach ($coupons as $id => $coupon) { + $this->assertTrue($this->promotion->hasCoupon($coupon)); + } + } + } diff --git a/modules/promotion/tests/src/Kernel/CouponCodeGeneratorTest.php b/modules/promotion/tests/src/Kernel/CouponCodeGeneratorTest.php new file mode 100644 index 0000000000..7e4c5cdabd --- /dev/null +++ b/modules/promotion/tests/src/Kernel/CouponCodeGeneratorTest.php @@ -0,0 +1,154 @@ +installEntitySchema('profile'); + $this->installEntitySchema('commerce_order'); + $this->installEntitySchema('commerce_order_item'); + $this->installEntitySchema('commerce_promotion'); + $this->installEntitySchema('commerce_promotion_coupon'); + $this->installConfig(['commerce_order']); + + $promotion = Promotion::create([ + 'name' => 'Promotion 1', + 'order_types' => ['default'], + 'stores' => [$this->store->id()], + 'status' => TRUE, + 'offer' => [ + 'target_plugin_id' => 'order_item_percentage_off', + 'target_plugin_configuration' => [ + 'percentage' => '0.5', + ], + ], + ]); + $promotion->save(); + + $this->numericCoupons = []; + for ($i = 0; $i < 10; $i++) { + $coupon = Coupon::create([ + 'promotion_id' => $promotion->id(), + 'code' => 'COUPON' . $i, + 'usage_limit' => 1, + 'status' => 1, + ]); + $coupon->save(); + $this->numericCoupons[] = $coupon; + } + + $this->couponCodeGenerator = $this->container->get('commerce_promotion.coupon_code_generator'); + } + + /** + * Tests the validatePattern method. + * + * @covers ::validatePattern + */ + public function testPatternValidityChecker() { + // Numeric pattern length 1 is too short for coupon quantity > 10. + $pattern = new CouponCodePattern('numeric', '', '', 1); + $result = $this->couponCodeGenerator->validatePattern($pattern, 11); + $this->assertFalse($result); + + // Numeric pattern length 1 is long enough for coupon quantity = 10. + $result = $this->couponCodeGenerator->validatePattern($pattern, 10); + $this->assertTrue($result); + + // Numeric pattern length 1 is long enough for coupon quantity < 10. + $result = $this->couponCodeGenerator->validatePattern($pattern, 9); + $this->assertTrue($result); + + // Numeric pattern length 8 is long enough for coupon quantity 1000. + $pattern = new CouponCodePattern('numeric', '', '', 8); + $result = $this->couponCodeGenerator->validatePattern($pattern, 1000); + $this->assertTrue($result); + } + + /** + * Tests the code generator. + * + * @covers ::generateCodes + */ + public function testCouponGenerator() { + // Test numeric type pattern, length 10, 1 code. + $pattern = new CouponCodePattern('numeric', '', '', 10); + $result = $this->couponCodeGenerator->generateCodes($pattern, 1); + $this->assertNotEmpty($result); + $this->assertTrue(ctype_digit($result[0])); + $this->assertEquals(strlen($result[0]), 10); + + // Test alphabetic type pattern, length 100, 10 codes. + $pattern = new CouponCodePattern('alphabetic', '', '', 100); + $result = $this->couponCodeGenerator->generateCodes($pattern, 10); + $this->assertEquals(count($result), 10); + $this->assertTrue(ctype_alpha($result[0])); + $this->assertEquals(strlen($result[0]), 100); + + // Test alphanumeric type pattern, length 50, 25 codes. + $pattern = new CouponCodePattern('alphanumeric', '', '', 50); + $result = $this->couponCodeGenerator->generateCodes($pattern, 25); + $this->assertEquals(count($result), 25); + $this->assertTrue(ctype_alnum($result[0])); + $this->assertEquals(strlen($result[0]), 50); + + // Test prefix and suffix options. + $pattern = new CouponCodePattern('numeric', 'save', 'XX', 2); + $result = $this->couponCodeGenerator->generateCodes($pattern, 1); + $this->assertNotEmpty($result); + $this->assertEquals(substr($result[0], 0, 4), 'save'); + $this->assertTrue(ctype_digit(substr($result[0], 4, 2))); + $this->assertEquals(substr($result[0], 6), 'XX'); + + // Test coupon code conflict. + $pattern = new CouponCodePattern('numeric', 'COUPON', '', 1); + $result = $this->couponCodeGenerator->generateCodes($pattern, 1); + $this->assertEmpty($result); + } + +} From c531ab3a27dac3898e15e48cb9cedfd3dd791120 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Wed, 7 Mar 2018 15:20:46 +0100 Subject: [PATCH 075/103] Issue #2950745 by bojanz: Require Drupal 8.5.x --- .travis.yml | 6 +++--- commerce.info.yml | 2 +- composer.json | 2 +- modules/checkout/commerce_checkout.module | 6 ++---- modules/order/src/Entity/Order.php | 2 +- modules/order/src/Entity/OrderItem.php | 2 +- modules/product/src/Form/ProductForm.php | 8 -------- 7 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8bd09fd5a5..6342f30984 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ env: # The environment to use, supported are: drupal-7, drupal-8 - DRUPAL_TI_ENVIRONMENT="drupal-8" - - DRUPAL_TI_CORE_BRANCH="8.4.x" + - DRUPAL_TI_CORE_BRANCH="8.5.x" # Drupal specific variables. - DRUPAL_TI_DB="drupal_travis_db" @@ -51,8 +51,8 @@ env: - DRUPAL_TI_WEBSERVER_PORT="8080" # Simpletest specific commandline arguments, the DRUPAL_TI_SIMPLETEST_GROUP is appended at the end. - - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 20 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT --types Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional" - - DRUPAL_TI_SIMPLETEST_JS_ARGS="--verbose --color --concurrency 1 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT --types PHPUnit-FunctionalJavascript" + - DRUPAL_TI_SIMPLETEST_ARGS="--verbose --color --concurrency 20 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT --types Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional --suppress-deprecations" + - DRUPAL_TI_SIMPLETEST_JS_ARGS="--verbose --color --concurrency 1 --url $DRUPAL_TI_WEBSERVER_URL:$DRUPAL_TI_WEBSERVER_PORT --types PHPUnit-FunctionalJavascript --suppress-deprecations" # === Behat specific variables. # This is relative to $TRAVIS_BUILD_DIR diff --git a/commerce.info.yml b/commerce.info.yml index 2f7353e5e2..633eb2b1da 100644 --- a/commerce.info.yml +++ b/commerce.info.yml @@ -10,4 +10,4 @@ dependencies: - datetime - inline_entity_form - views - - system (>=8.4.0) + - system (>=8.5.0) diff --git a/composer.json b/composer.json index bd53e77806..0b2014c651 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "homepage": "http://drupal.org/project/commerce", "license": "GPL-2.0+", "require": { - "drupal/core": "~8.4", + "drupal/core": "~8.5", "drupal/address": "~1.0", "drupal/entity": "~1.0", "drupal/entity_reference_revisions": "~1.0", diff --git a/modules/checkout/commerce_checkout.module b/modules/checkout/commerce_checkout.module index 6cb93de7fa..2fb3bcde0f 100644 --- a/modules/checkout/commerce_checkout.module +++ b/modules/checkout/commerce_checkout.module @@ -85,8 +85,7 @@ function commerce_checkout_entity_base_field_info(EntityTypeInterface $entity_ty ->setSetting('target_type', 'commerce_checkout_flow') ->setSetting('handler', 'default') ->setDisplayOptions('form', [ - 'type' => 'hidden', - 'weight' => 0, + 'region' => 'hidden', ]) ->setDisplayConfigurable('view', FALSE) ->setDisplayConfigurable('form', FALSE); @@ -96,8 +95,7 @@ function commerce_checkout_entity_base_field_info(EntityTypeInterface $entity_ty $fields['checkout_step'] = BaseFieldDefinition::create('string') ->setLabel(t('Checkout step')) ->setDisplayOptions('form', [ - 'type' => 'hidden', - 'weight' => 0, + 'region' => 'hidden', ]) ->setDisplayConfigurable('view', FALSE) ->setDisplayConfigurable('form', FALSE); diff --git a/modules/order/src/Entity/Order.php b/modules/order/src/Entity/Order.php index 503f66115b..e00d05c4ee 100644 --- a/modules/order/src/Entity/Order.php +++ b/modules/order/src/Entity/Order.php @@ -624,7 +624,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { 'weight' => 0, ]) ->setDisplayOptions('form', [ - 'type' => 'hidden', + 'region' => 'hidden', 'weight' => 0, ]) ->setDisplayConfigurable('form', TRUE) diff --git a/modules/order/src/Entity/OrderItem.php b/modules/order/src/Entity/OrderItem.php index 6e2333963c..2eb3862348 100644 --- a/modules/order/src/Entity/OrderItem.php +++ b/modules/order/src/Entity/OrderItem.php @@ -386,7 +386,7 @@ public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, // hidden instead. https://www.drupal.org/node/2346347#comment-10254087. $fields['purchased_entity']->setRequired(FALSE); $fields['purchased_entity']->setDisplayOptions('form', [ - 'type' => 'hidden', + 'region' => 'hidden', ]); $fields['purchased_entity']->setDisplayConfigurable('form', FALSE); $fields['purchased_entity']->setDisplayConfigurable('view', FALSE); diff --git a/modules/product/src/Form/ProductForm.php b/modules/product/src/Form/ProductForm.php index f445aede62..c2f4176eb7 100644 --- a/modules/product/src/Form/ProductForm.php +++ b/modules/product/src/Form/ProductForm.php @@ -89,14 +89,6 @@ public function form(array $form, FormStateInterface $form_state) { '#type' => 'hidden', '#default_value' => $product->getChangedTime(), ]; - - $form['footer'] = [ - '#type' => 'container', - '#weight' => 99, - '#attributes' => [ - 'class' => ['product-form-footer'], - ], - ]; $form['status']['#group'] = 'footer'; $last_saved = t('Not saved yet'); From ee6179104f9caad880f823d723ba9fe0ad60ae74 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Wed, 7 Mar 2018 16:08:25 +0100 Subject: [PATCH 076/103] Issue #2950813 by bojanz: Replace drupal_set_message() with the Messenger service --- modules/cart/commerce_cart.module | 2 +- modules/cart/commerce_cart.services.yml | 2 +- .../EventSubscriber/CartEventSubscriber.php | 18 +++++++++--- .../src/Plugin/views/area/EmptyCartButton.php | 29 ++++++++++++++----- .../src/Form/BigPipeAddToCartForm.php | 2 +- .../checkout/src/Form/CheckoutFlowForm.php | 2 +- modules/order/src/Form/OrderForm.php | 2 +- modules/order/src/Form/OrderItemTypeForm.php | 2 +- modules/order/src/Form/OrderReassignForm.php | 2 +- modules/order/src/Form/OrderTypeForm.php | 2 +- modules/order/src/Form/OrderUnlockForm.php | 2 +- .../Controller/PaymentCheckoutController.php | 18 ++++++++++-- .../src/Element/PaymentGatewayForm.php | 2 +- modules/payment/src/Form/PaymentAddForm.php | 2 +- .../payment/src/Form/PaymentGatewayForm.php | 2 +- .../payment/src/Form/PaymentMethodAddForm.php | 2 +- .../src/Form/PaymentMethodDeleteForm.php | 4 +-- .../src/Form/PaymentMethodEditForm.php | 2 +- .../payment/src/Form/PaymentOperationForm.php | 2 +- .../PaymentGateway/OffsiteRedirect.php | 1 - modules/price/src/Form/CurrencyForm.php | 2 +- modules/price/src/Form/CurrencyImportForm.php | 2 +- .../src/Form/AjaxPriceTestForm.php | 2 +- .../src/Form/NumberTestForm.php | 2 +- .../src/Form/PriceTestForm.php | 2 +- .../product/src/Form/ProductAttributeForm.php | 4 +-- modules/product/src/Form/ProductForm.php | 2 +- modules/product/src/Form/ProductTypeForm.php | 2 +- .../src/Form/ProductVariationTypeForm.php | 2 +- modules/promotion/src/Form/CouponForm.php | 2 +- .../promotion/src/Form/CouponGenerateForm.php | 8 ++--- modules/promotion/src/Form/PromotionForm.php | 2 +- modules/store/src/Form/StoreForm.php | 2 +- modules/store/src/Form/StoreTypeForm.php | 2 +- modules/tax/src/Form/TaxTypeForm.php | 2 +- 35 files changed, 86 insertions(+), 52 deletions(-) diff --git a/modules/cart/commerce_cart.module b/modules/cart/commerce_cart.module index f64d3350db..a85615566f 100644 --- a/modules/cart/commerce_cart.module +++ b/modules/cart/commerce_cart.module @@ -240,7 +240,7 @@ function commerce_cart_order_item_views_form_submit($form, FormStateInterface $f /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ $order = \Drupal::entityTypeManager()->getStorage('commerce_order')->load($order_id); $order->save(); - drupal_set_message(t('Your shopping cart has been updated.')); + \Drupal::messenger()->addMessage(t('Your shopping cart has been updated.')); } /** diff --git a/modules/cart/commerce_cart.services.yml b/modules/cart/commerce_cart.services.yml index ab836cedee..27cb87b828 100644 --- a/modules/cart/commerce_cart.services.yml +++ b/modules/cart/commerce_cart.services.yml @@ -23,7 +23,7 @@ services: commerce_cart.cart_subscriber: class: Drupal\commerce_cart\EventSubscriber\CartEventSubscriber - arguments: ['@string_translation'] + arguments: ['@messenger', '@string_translation'] tags: - { name: event_subscriber } diff --git a/modules/cart/src/EventSubscriber/CartEventSubscriber.php b/modules/cart/src/EventSubscriber/CartEventSubscriber.php index 63b05cae7e..4d18ede462 100644 --- a/modules/cart/src/EventSubscriber/CartEventSubscriber.php +++ b/modules/cart/src/EventSubscriber/CartEventSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\commerce_cart\EventSubscriber; +use Drupal\Core\Messenger\MessengerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Drupal\commerce_cart\Event\CartEntityAddEvent; use Drupal\commerce_cart\Event\CartEvents; @@ -13,13 +14,23 @@ class CartEventSubscriber implements EventSubscriberInterface { use StringTranslationTrait; + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + /** * Constructs a new CartEventSubscriber object. * + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation * The string translation. */ - public function __construct(TranslationInterface $string_translation) { + public function __construct(MessengerInterface $messenger, TranslationInterface $string_translation) { + $this->messenger = $messenger; $this->stringTranslation = $string_translation; } @@ -40,9 +51,8 @@ public static function getSubscribedEvents() { * The add to cart event. */ public function displayAddToCartMessage(CartEntityAddEvent $event) { - $purchased_entity = $event->getEntity(); - drupal_set_message($this->t('@entity added to your cart.', [ - '@entity' => $purchased_entity->label(), + $this->messenger->addMessage($this->t('@entity added to your cart.', [ + '@entity' => $event->getEntity()->label(), ':url' => Url::fromRoute('commerce_cart.page')->toString(), ])); } diff --git a/modules/cart/src/Plugin/views/area/EmptyCartButton.php b/modules/cart/src/Plugin/views/area/EmptyCartButton.php index 509275d8bd..beec14540d 100644 --- a/modules/cart/src/Plugin/views/area/EmptyCartButton.php +++ b/modules/cart/src/Plugin/views/area/EmptyCartButton.php @@ -5,6 +5,7 @@ use Drupal\commerce_cart\CartManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\views\Plugin\views\area\AreaPluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -23,11 +24,18 @@ class EmptyCartButton extends AreaPluginBase { protected $cartManager; /** - * The order storage. + * The entity manager. * - * @var \Drupal\Core\Entity\EntityStorageInterface + * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ - protected $orderStorage; + protected $entityTypeManager; + + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; /** * Constructs a new EmptyCartButton object. @@ -42,12 +50,15 @@ class EmptyCartButton extends AreaPluginBase { * The cart manager. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, CartManagerInterface $cart_manager, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, CartManagerInterface $cart_manager, EntityTypeManagerInterface $entity_type_manager, MessengerInterface $messenger) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->cartManager = $cart_manager; - $this->orderStorage = $entity_type_manager->getStorage('commerce_order'); + $this->entityTypeManager = $entity_type_manager; + $this->messenger = $messenger; } /** @@ -59,7 +70,8 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_id, $plugin_definition, $container->get('commerce_cart.cart_manager'), - $container->get('entity_type.manager') + $container->get('entity_type.manager'), + $container->get('messenger') ); } @@ -117,11 +129,12 @@ public function viewsForm(array &$form, FormStateInterface $form_state) { public function viewsFormSubmit(array &$form, FormStateInterface $form_state) { $triggering_element = $form_state->getTriggeringElement(); if (!empty($triggering_element['#empty_cart_button'])) { + $order_storage = $this->entityTypeManager->getStorage('commerce_order'); /** @var \Drupal\commerce_order\Entity\OrderInterface $cart */ - $cart = $this->orderStorage->load($this->view->argument['order_id']->getValue()); + $cart = $order_storage->load($this->view->argument['order_id']->getValue()); $this->cartManager->emptyCart($cart); - drupal_set_message($this->t('Your shopping cart has been emptied.')); + $this->messenger->addMessage($this->t('Your shopping cart has been emptied.')); } } diff --git a/modules/cart/tests/modules/commerce_cart_big_pipe/src/Form/BigPipeAddToCartForm.php b/modules/cart/tests/modules/commerce_cart_big_pipe/src/Form/BigPipeAddToCartForm.php index d5b83eafc7..dece8a4527 100644 --- a/modules/cart/tests/modules/commerce_cart_big_pipe/src/Form/BigPipeAddToCartForm.php +++ b/modules/cart/tests/modules/commerce_cart_big_pipe/src/Form/BigPipeAddToCartForm.php @@ -16,7 +16,7 @@ class BigPipeAddToCartForm extends AddToCartForm { public function buildForm(array $form, FormStateInterface $form_state) { // Sleep between 0.0 and 2.0 seconds. $load_slowdown = mt_rand(0, 20) / 10; - drupal_set_message(sprintf('Delayed form build by %s seconds', $load_slowdown)); + $this->messenger()->addMessage(sprintf('Delayed form build by %s seconds', $load_slowdown)); sleep($load_slowdown); return parent::buildForm($form, $form_state); } diff --git a/modules/checkout/src/Form/CheckoutFlowForm.php b/modules/checkout/src/Form/CheckoutFlowForm.php index 55c50d28d9..bb1fb15b2b 100644 --- a/modules/checkout/src/Form/CheckoutFlowForm.php +++ b/modules/checkout/src/Form/CheckoutFlowForm.php @@ -117,7 +117,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { */ public function save(array $form, FormStateInterface $form_state) { $status = $this->entity->save(); - drupal_set_message($this->t('Saved the %label checkout flow.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Saved the %label checkout flow.', ['%label' => $this->entity->label()])); if ($status == SAVED_UPDATED) { $form_state->setRedirect('entity.commerce_checkout_flow.collection'); } diff --git a/modules/order/src/Form/OrderForm.php b/modules/order/src/Form/OrderForm.php index 3655b86db2..2beadd96e3 100644 --- a/modules/order/src/Form/OrderForm.php +++ b/modules/order/src/Form/OrderForm.php @@ -162,7 +162,7 @@ protected function fieldAsReadOnly($label, $value) { */ public function save(array $form, FormStateInterface $form_state) { $this->entity->save(); - drupal_set_message($this->t('The order %label has been successfully saved.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('The order %label has been successfully saved.', ['%label' => $this->entity->label()])); $form_state->setRedirect('entity.commerce_order.canonical', ['commerce_order' => $this->entity->id()]); } diff --git a/modules/order/src/Form/OrderItemTypeForm.php b/modules/order/src/Form/OrderItemTypeForm.php index 53c7e41f03..84d3616fea 100644 --- a/modules/order/src/Form/OrderItemTypeForm.php +++ b/modules/order/src/Form/OrderItemTypeForm.php @@ -77,7 +77,7 @@ public function save(array $form, FormStateInterface $form_state) { $this->entity->save(); $this->submitTraitForm($form, $form_state); - drupal_set_message($this->t('Saved the %label order item type.', [ + $this->messenger()->addMessage($this->t('Saved the %label order item type.', [ '%label' => $this->entity->label(), ])); $form_state->setRedirect('entity.commerce_order_item_type.collection'); diff --git a/modules/order/src/Form/OrderReassignForm.php b/modules/order/src/Form/OrderReassignForm.php index 21db4e13f2..85b8c5452d 100644 --- a/modules/order/src/Form/OrderReassignForm.php +++ b/modules/order/src/Form/OrderReassignForm.php @@ -99,7 +99,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $this->order->setEmail($values['mail']); $this->order->setCustomerId($values['uid']); $this->order->save(); - drupal_set_message($this->t('The order %label has been assigned to customer %customer.', [ + $this->messenger()->addMessage($this->t('The order %label has been assigned to customer %customer.', [ '%label' => $this->order->label(), '%customer' => $this->order->getCustomer()->label(), ])); diff --git a/modules/order/src/Form/OrderTypeForm.php b/modules/order/src/Form/OrderTypeForm.php index 7aabb19283..99333400c2 100644 --- a/modules/order/src/Form/OrderTypeForm.php +++ b/modules/order/src/Form/OrderTypeForm.php @@ -139,7 +139,7 @@ public function save(array $form, FormStateInterface $form_state) { $status = $this->entity->save(); $this->submitTraitForm($form, $form_state); - drupal_set_message($this->t('Saved the %label order type.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Saved the %label order type.', ['%label' => $this->entity->label()])); $form_state->setRedirect('entity.commerce_order_type.collection'); if ($status == SAVED_NEW) { diff --git a/modules/order/src/Form/OrderUnlockForm.php b/modules/order/src/Form/OrderUnlockForm.php index 99d2976e33..f34b07b2b2 100644 --- a/modules/order/src/Form/OrderUnlockForm.php +++ b/modules/order/src/Form/OrderUnlockForm.php @@ -42,7 +42,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $order->unlock(); $order->save(); - drupal_set_message($this->t('The order %label has been unlocked.', [ + $this->messenger()->addMessage($this->t('The order %label has been unlocked.', [ '%label' => $order->label(), ])); $form_state->setRedirectUrl($order->toUrl('collection')); diff --git a/modules/payment/src/Controller/PaymentCheckoutController.php b/modules/payment/src/Controller/PaymentCheckoutController.php index a60605d06d..bb779711e8 100644 --- a/modules/payment/src/Controller/PaymentCheckoutController.php +++ b/modules/payment/src/Controller/PaymentCheckoutController.php @@ -8,6 +8,7 @@ use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface; use Drupal\Core\Access\AccessException; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Messenger\MessengerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -23,14 +24,24 @@ class PaymentCheckoutController implements ContainerInjectionInterface { */ protected $checkoutOrderManager; + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + /** * Constructs a new PaymentCheckoutController object. * * @param \Drupal\commerce_checkout\CheckoutOrderManagerInterface $checkout_order_manager * The checkout order manager. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. */ - public function __construct(CheckoutOrderManagerInterface $checkout_order_manager) { + public function __construct(CheckoutOrderManagerInterface $checkout_order_manager, MessengerInterface $messenger) { $this->checkoutOrderManager = $checkout_order_manager; + $this->messenger = $messenger; } /** @@ -38,7 +49,8 @@ public function __construct(CheckoutOrderManagerInterface $checkout_order_manage */ public static function create(ContainerInterface $container) { return new static( - $container->get('commerce_checkout.checkout_order_manager') + $container->get('commerce_checkout.checkout_order_manager'), + $container->get('messenger') ); } @@ -70,7 +82,7 @@ public function returnPage(OrderInterface $commerce_order, Request $request) { } catch (PaymentGatewayException $e) { \Drupal::logger('commerce_payment')->error($e->getMessage()); - drupal_set_message(t('Payment failed at the payment server. Please review your information and try again.'), 'error'); + $this->messenger->addError(t('Payment failed at the payment server. Please review your information and try again.')); $redirect_step_id = $checkout_flow_plugin->getPreviousStepId($step_id); } $checkout_flow_plugin->redirectToStep($redirect_step_id); diff --git a/modules/payment/src/Element/PaymentGatewayForm.php b/modules/payment/src/Element/PaymentGatewayForm.php index bcadea56eb..ae43de8cf8 100644 --- a/modules/payment/src/Element/PaymentGatewayForm.php +++ b/modules/payment/src/Element/PaymentGatewayForm.php @@ -99,7 +99,7 @@ public static function processForm(array $element, FormStateInterface $form_stat catch (PaymentGatewayException $e) { \Drupal::logger('commerce_payment')->error($e->getMessage()); if (!empty($element['#exception_url'])) { - drupal_set_message($element['#exception_message'], 'error'); + \Drupal::messenger()->addError($element['#exception_message']); throw new NeedsRedirectException($element['#exception_url']); } else { diff --git a/modules/payment/src/Form/PaymentAddForm.php b/modules/payment/src/Form/PaymentAddForm.php index a7f821ab93..9699e184b7 100644 --- a/modules/payment/src/Form/PaymentAddForm.php +++ b/modules/payment/src/Form/PaymentAddForm.php @@ -248,7 +248,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { elseif ($step == 'payment') { /** @var \Drupal\commerce_payment\Entity\PaymentInterface $payment */ $payment = $form_state->getValue('payment'); - drupal_set_message($this->t('Payment saved.')); + $this->messenger()->addMessage($this->t('Payment saved.')); $form_state->setRedirect('entity.commerce_payment.collection', ['commerce_order' => $payment->getOrderId()]); } } diff --git a/modules/payment/src/Form/PaymentGatewayForm.php b/modules/payment/src/Form/PaymentGatewayForm.php index f0fe44ce8e..ad8614abd5 100644 --- a/modules/payment/src/Form/PaymentGatewayForm.php +++ b/modules/payment/src/Form/PaymentGatewayForm.php @@ -163,7 +163,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { */ public function save(array $form, FormStateInterface $form_state) { $this->entity->save(); - drupal_set_message($this->t('Saved the %label payment gateway.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Saved the %label payment gateway.', ['%label' => $this->entity->label()])); $form_state->setRedirect('entity.commerce_payment_gateway.collection'); } diff --git a/modules/payment/src/Form/PaymentMethodAddForm.php b/modules/payment/src/Form/PaymentMethodAddForm.php index 3654943606..ef5182eaf7 100644 --- a/modules/payment/src/Form/PaymentMethodAddForm.php +++ b/modules/payment/src/Form/PaymentMethodAddForm.php @@ -168,7 +168,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { elseif ($step == 'payment_method') { /** @var \Drupal\commerce_payment\Entity\PaymentMethodInterface $payment_method */ $payment_method = $form_state->getValue('payment_method'); - drupal_set_message($this->t('%label saved to your payment methods.', ['%label' => $payment_method->label()])); + $this->messenger()->addMessage($this->t('%label saved to your payment methods.', ['%label' => $payment_method->label()])); $form_state->setRedirect('entity.commerce_payment_method.collection', ['user' => $payment_method->getOwnerId()]); } } diff --git a/modules/payment/src/Form/PaymentMethodDeleteForm.php b/modules/payment/src/Form/PaymentMethodDeleteForm.php index 94176d5244..a32e1149cb 100644 --- a/modules/payment/src/Form/PaymentMethodDeleteForm.php +++ b/modules/payment/src/Form/PaymentMethodDeleteForm.php @@ -24,11 +24,11 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $payment_gateway_plugin->deletePaymentMethod($payment_method); } catch (PaymentGatewayException $e) { - drupal_set_message($e->getMessage(), 'error'); + $this->messenger()->addError($e->getMessage()); return; } - drupal_set_message($this->getDeletionMessage()); + $this->messenger()->addMessage($this->getDeletionMessage()); $this->logDeletionMessage(); } diff --git a/modules/payment/src/Form/PaymentMethodEditForm.php b/modules/payment/src/Form/PaymentMethodEditForm.php index f92815061c..72d2c9e45e 100644 --- a/modules/payment/src/Form/PaymentMethodEditForm.php +++ b/modules/payment/src/Form/PaymentMethodEditForm.php @@ -37,7 +37,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { */ public function save(array $form, FormStateInterface $form_state) { // The entity was saved by the plugin form. Redirect. - drupal_set_message($this->t('Saved the %label @entity-type.', [ + $this->messenger()->addMessage($this->t('Saved the %label @entity-type.', [ '%label' => $this->entity->label(), '@entity-type' => $this->entity->getEntityType()->getLowercaseLabel(), ])); diff --git a/modules/payment/src/Form/PaymentOperationForm.php b/modules/payment/src/Form/PaymentOperationForm.php index 38ae3d207c..d418025859 100644 --- a/modules/payment/src/Form/PaymentOperationForm.php +++ b/modules/payment/src/Form/PaymentOperationForm.php @@ -51,7 +51,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { */ public function submitForm(array &$form, FormStateInterface $form_state) { if (!empty($form['payment']['#success_message'])) { - drupal_set_message($form['payment']['#success_message']); + $this->messenger()->addMessage($form['payment']['#success_message']); } $form_state->setRedirect('entity.commerce_payment.collection', ['commerce_order' => $this->entity->getOrderId()]); } diff --git a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirect.php b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirect.php index 90d452c6fe..358770c256 100644 --- a/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirect.php +++ b/modules/payment_example/src/Plugin/Commerce/PaymentGateway/OffsiteRedirect.php @@ -81,7 +81,6 @@ public function onReturn(OrderInterface $order, Request $request) { 'remote_state' => $request->query->get('payment_status'), ]); $payment->save(); - drupal_set_message('Payment was processed'); } } diff --git a/modules/price/src/Form/CurrencyForm.php b/modules/price/src/Form/CurrencyForm.php index 991cd8d2cb..a9fc31e2c6 100644 --- a/modules/price/src/Form/CurrencyForm.php +++ b/modules/price/src/Form/CurrencyForm.php @@ -139,7 +139,7 @@ public function validateNumericCode(array $element, FormStateInterface $form_sta public function save(array $form, FormStateInterface $form_state) { $currency = $this->entity; $currency->save(); - drupal_set_message($this->t('Saved the %label currency.', [ + $this->messenger()->addMessage($this->t('Saved the %label currency.', [ '%label' => $currency->label(), ])); $form_state->setRedirect('entity.commerce_currency.collection'); diff --git a/modules/price/src/Form/CurrencyImportForm.php b/modules/price/src/Form/CurrencyImportForm.php index ed4b43e3c3..c77faf7bf7 100644 --- a/modules/price/src/Form/CurrencyImportForm.php +++ b/modules/price/src/Form/CurrencyImportForm.php @@ -82,7 +82,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { foreach ($currency_codes as $currency_code) { $this->importer->import($currency_code); } - drupal_set_message($this->t('Imported the selected currencies.')); + $this->messenger()->addMessage($this->t('Imported the selected currencies.')); $form_state->setRedirect('entity.commerce_currency.collection'); } diff --git a/modules/price/tests/modules/commerce_price_test/src/Form/AjaxPriceTestForm.php b/modules/price/tests/modules/commerce_price_test/src/Form/AjaxPriceTestForm.php index cd85aa792f..a3d6c27bac 100644 --- a/modules/price/tests/modules/commerce_price_test/src/Form/AjaxPriceTestForm.php +++ b/modules/price/tests/modules/commerce_price_test/src/Form/AjaxPriceTestForm.php @@ -53,7 +53,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Create a Price to ensure the values are valid. $value = $form_state->getValue('amount'); $price = new Price($value['number'], $value['currency_code']); - drupal_set_message(t('The number is "@number" and the currency code is "@currency_code".', [ + $this->messenger()->addMessage(t('The number is "@number" and the currency code is "@currency_code".', [ '@number' => $price->getNumber(), '@currency_code' => $price->getCurrencyCode(), ])); diff --git a/modules/price/tests/modules/commerce_price_test/src/Form/NumberTestForm.php b/modules/price/tests/modules/commerce_price_test/src/Form/NumberTestForm.php index 3b0f474e8d..70627846a3 100644 --- a/modules/price/tests/modules/commerce_price_test/src/Form/NumberTestForm.php +++ b/modules/price/tests/modules/commerce_price_test/src/Form/NumberTestForm.php @@ -38,7 +38,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - drupal_set_message(t('The number is "@number".', [ + $this->messenger()->addMessage(t('The number is "@number".', [ '@number' => $form_state->getValue('number'), ])); } diff --git a/modules/price/tests/modules/commerce_price_test/src/Form/PriceTestForm.php b/modules/price/tests/modules/commerce_price_test/src/Form/PriceTestForm.php index c08f34405a..65322d70b8 100644 --- a/modules/price/tests/modules/commerce_price_test/src/Form/PriceTestForm.php +++ b/modules/price/tests/modules/commerce_price_test/src/Form/PriceTestForm.php @@ -49,7 +49,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Create a Price to ensure the values are valid. $value = $form_state->getValue('amount'); $price = new Price($value['number'], $value['currency_code']); - drupal_set_message(t('The number is "@number" and the currency code is "@currency_code".', [ + $this->messenger()->addMessage(t('The number is "@number" and the currency code is "@currency_code".', [ '@number' => $price->getNumber(), '@currency_code' => $price->getCurrencyCode(), ])); diff --git a/modules/product/src/Form/ProductAttributeForm.php b/modules/product/src/Form/ProductAttributeForm.php index c9da3da8fe..1ebcb5ba18 100644 --- a/modules/product/src/Form/ProductAttributeForm.php +++ b/modules/product/src/Form/ProductAttributeForm.php @@ -405,13 +405,13 @@ public function save(array $form, FormStateInterface $form_state) { } if ($status == SAVED_NEW) { - drupal_set_message($this->t('Created the %label product attribute.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Created the %label product attribute.', ['%label' => $this->entity->label()])); // Send the user to the edit form to create the attribute values. $form_state->setRedirectUrl($this->entity->toUrl('edit-form')); } else { $this->saveValues($form, $form_state); - drupal_set_message($this->t('Updated the %label product attribute.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Updated the %label product attribute.', ['%label' => $this->entity->label()])); $form_state->setRedirectUrl($this->entity->toUrl('collection')); } } diff --git a/modules/product/src/Form/ProductForm.php b/modules/product/src/Form/ProductForm.php index c2f4176eb7..f225c1275f 100644 --- a/modules/product/src/Form/ProductForm.php +++ b/modules/product/src/Form/ProductForm.php @@ -211,7 +211,7 @@ public function save(array $form, FormStateInterface $form_state) { /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ $product = $this->getEntity(); $product->save(); - drupal_set_message($this->t('The product %label has been successfully saved.', ['%label' => $product->label()])); + $this->messenger()->addMessage($this->t('The product %label has been successfully saved.', ['%label' => $product->label()])); $form_state->setRedirect('entity.commerce_product.canonical', ['commerce_product' => $product->id()]); } diff --git a/modules/product/src/Form/ProductTypeForm.php b/modules/product/src/Form/ProductTypeForm.php index 8d64f29c54..c3b0b82f09 100644 --- a/modules/product/src/Form/ProductTypeForm.php +++ b/modules/product/src/Form/ProductTypeForm.php @@ -156,7 +156,7 @@ public function save(array $form, FormStateInterface $form_state) { } $this->submitTraitForm($form, $form_state); - drupal_set_message($this->t('The product type %label has been successfully saved.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('The product type %label has been successfully saved.', ['%label' => $this->entity->label()])); $form_state->setRedirect('entity.commerce_product_type.collection'); if ($status == SAVED_NEW) { commerce_product_add_stores_field($this->entity); diff --git a/modules/product/src/Form/ProductVariationTypeForm.php b/modules/product/src/Form/ProductVariationTypeForm.php index 0d0344ae0c..9c072ed306 100644 --- a/modules/product/src/Form/ProductVariationTypeForm.php +++ b/modules/product/src/Form/ProductVariationTypeForm.php @@ -159,7 +159,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { */ public function save(array $form, FormStateInterface $form_state) { $this->entity->save(); - drupal_set_message($this->t('Saved the %label product variation type.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Saved the %label product variation type.', ['%label' => $this->entity->label()])); $form_state->setRedirect('entity.commerce_product_variation_type.collection'); $this->submitTraitForm($form, $form_state); diff --git a/modules/promotion/src/Form/CouponForm.php b/modules/promotion/src/Form/CouponForm.php index c5884ac73b..9406fcbed1 100644 --- a/modules/promotion/src/Form/CouponForm.php +++ b/modules/promotion/src/Form/CouponForm.php @@ -22,7 +22,7 @@ protected function prepareEntity() { */ public function save(array $form, FormStateInterface $form_state) { $this->entity->save(); - drupal_set_message($this->t('Saved the %label coupon.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Saved the %label coupon.', ['%label' => $this->entity->label()])); $form_state->setRedirectUrl($this->entity->toUrl('collection')); } diff --git a/modules/promotion/src/Form/CouponGenerateForm.php b/modules/promotion/src/Form/CouponGenerateForm.php index f69c712141..24ad5ee8ea 100644 --- a/modules/promotion/src/Form/CouponGenerateForm.php +++ b/modules/promotion/src/Form/CouponGenerateForm.php @@ -265,13 +265,13 @@ public static function finishBatch($success, array $results, array $operations) $created = count($results['codes']); // An incomplete set of coupons was generated. if ($created != $results['total_quantity']) { - drupal_set_message(t('Generated %created out of %total requested coupons. Consider adding a unique prefix/suffix or increasing the pattern length to improve results.', [ + \Drupal::messenger()->addWarning(t('Generated %created out of %total requested coupons. Consider adding a unique prefix/suffix or increasing the pattern length to improve results.', [ '%created' => $created, '%total' => $results['total_quantity'], - ]), 'warning'); + ])); } else { - drupal_set_message(\Drupal::translation()->formatPlural( + \Drupal::messenger()->addMessage(\Drupal::translation()->formatPlural( $created, 'Generated 1 coupon.', 'Generated @count coupons.' @@ -280,7 +280,7 @@ public static function finishBatch($success, array $results, array $operations) } else { $error_operation = reset($operations); - drupal_set_message(t('An error occurred while processing @operation with arguments: @args', [ + \Drupal::messenger()->addError(t('An error occurred while processing @operation with arguments: @args', [ '@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE), ])); diff --git a/modules/promotion/src/Form/PromotionForm.php b/modules/promotion/src/Form/PromotionForm.php index c7f54e1d26..172a34f4a4 100644 --- a/modules/promotion/src/Form/PromotionForm.php +++ b/modules/promotion/src/Form/PromotionForm.php @@ -114,7 +114,7 @@ protected function actions(array $form, FormStateInterface $form_state) { */ public function save(array $form, FormStateInterface $form_state) { $this->entity->save(); - drupal_set_message($this->t('Saved the %label promotion.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Saved the %label promotion.', ['%label' => $this->entity->label()])); if (!empty($form_state->getTriggeringElement()['#continue'])) { $form_state->setRedirect('entity.commerce_promotion_coupon.collection', ['commerce_promotion' => $this->entity->id()]); diff --git a/modules/store/src/Form/StoreForm.php b/modules/store/src/Form/StoreForm.php index 001516d4cd..572f5c5dbe 100644 --- a/modules/store/src/Form/StoreForm.php +++ b/modules/store/src/Form/StoreForm.php @@ -43,7 +43,7 @@ public function save(array $form, FormStateInterface $form_state) { $store_storage = $this->entityTypeManager->getStorage('commerce_store'); $store_storage->markAsDefault($this->entity); } - drupal_set_message($this->t('Saved the %label store.', [ + $this->messenger()->addMessage($this->t('Saved the %label store.', [ '%label' => $this->entity->label(), ])); $form_state->setRedirect('entity.commerce_store.collection'); diff --git a/modules/store/src/Form/StoreTypeForm.php b/modules/store/src/Form/StoreTypeForm.php index 7449ff3a21..4e7b3a0d67 100644 --- a/modules/store/src/Form/StoreTypeForm.php +++ b/modules/store/src/Form/StoreTypeForm.php @@ -73,7 +73,7 @@ public function save(array $form, FormStateInterface $form_state) { $this->entity->save(); $this->submitTraitForm($form, $form_state); - drupal_set_message($this->t('Saved the %label store type.', [ + $this->messenger()->addMessage($this->t('Saved the %label store type.', [ '%label' => $this->entity->label(), ])); $form_state->setRedirect('entity.commerce_store_type.collection'); diff --git a/modules/tax/src/Form/TaxTypeForm.php b/modules/tax/src/Form/TaxTypeForm.php index cbeae6d13f..f60a83ff59 100644 --- a/modules/tax/src/Form/TaxTypeForm.php +++ b/modules/tax/src/Form/TaxTypeForm.php @@ -132,7 +132,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { */ public function save(array $form, FormStateInterface $form_state) { $this->entity->save(); - drupal_set_message($this->t('Saved the %label tax type.', ['%label' => $this->entity->label()])); + $this->messenger()->addMessage($this->t('Saved the %label tax type.', ['%label' => $this->entity->label()])); $form_state->setRedirect('entity.commerce_tax_type.collection'); } From 4829744dc9deb759e2b0813797d768138449d9f4 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Tue, 13 Mar 2018 13:46:50 +0100 Subject: [PATCH 077/103] Issue #2952530 by bojanz: Replace drupal_set_message() in checkout panes --- .../CheckoutPane/PaymentInformation.php | 49 ++++++++++++++++- .../Commerce/CheckoutPane/PaymentProcess.php | 55 +++++++++++++++++-- 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index c931b4ff83..746f46dede 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -2,11 +2,15 @@ namespace Drupal\commerce_payment\Plugin\Commerce\CheckoutPane; +use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface; use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\SupportsStoredPaymentMethodsInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides the payment information pane. @@ -20,6 +24,49 @@ */ class PaymentInformation extends CheckoutPaneBase { + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * Constructs a new PaymentInformation object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow + * The parent checkout flow. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager, MessengerInterface $messenger) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $checkout_flow, $entity_type_manager); + + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $checkout_flow, + $container->get('entity_type.manager'), + $container->get('messenger') + ); + } + /** * {@inheritdoc} */ @@ -66,7 +113,7 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, $payment_gateways = $payment_gateway_storage->loadMultipleForOrder($this->order); // Can't proceed without any payment gateways. if (empty($payment_gateways)) { - drupal_set_message($this->noPaymentGatewayErrorMessage(), 'error'); + $this->messenger->addError($this->noPaymentGatewayErrorMessage()); return []; } diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php index 5784b57656..015f79ae26 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php @@ -3,15 +3,19 @@ namespace Drupal\commerce_payment\Plugin\Commerce\CheckoutPane; use Drupal\commerce\Response\NeedsRedirectException; +use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface; use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase; use Drupal\commerce_payment\Exception\DeclineException; use Drupal\commerce_payment\Exception\PaymentGatewayException; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\ManualPaymentGatewayInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OffsitePaymentGatewayInterface; use Drupal\commerce_payment\Plugin\Commerce\PaymentGateway\OnsitePaymentGatewayInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Link; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Url; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides the payment process pane. @@ -25,6 +29,49 @@ */ class PaymentProcess extends CheckoutPaneBase { + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * Constructs a new PaymentProcess object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow + * The parent checkout flow. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager, MessengerInterface $messenger) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $checkout_flow, $entity_type_manager); + + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $checkout_flow, + $container->get('entity_type.manager'), + $container->get('messenger') + ); + } + /** * {@inheritdoc} */ @@ -94,7 +141,7 @@ public function isVisible() { public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) { // The payment gateway is currently always required to be set. if ($this->order->get('payment_gateway')->isEmpty()) { - drupal_set_message($this->t('No payment gateway selected.'), 'error'); + $this->messenger->addError($this->t('No payment gateway selected.')); $this->redirectToPreviousStep(); } @@ -120,13 +167,13 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, } catch (DeclineException $e) { $message = $this->t('We encountered an error processing your payment method. Please verify your details and try again.'); - drupal_set_message($message, 'error'); + $this->messenger->addError($message); $this->redirectToPreviousStep(); } catch (PaymentGatewayException $e) { \Drupal::logger('commerce_payment')->error($e->getMessage()); $message = $this->t('We encountered an unexpected error processing your payment method. Please try again later.'); - drupal_set_message($message, 'error'); + $this->messenger->addError($message); $this->redirectToPreviousStep(); } } @@ -163,7 +210,7 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, catch (PaymentGatewayException $e) { \Drupal::logger('commerce_payment')->error($e->getMessage()); $message = $this->t('We encountered an unexpected error processing your payment. Please try again later.'); - drupal_set_message($message, 'error'); + $this->messenger->addError($message); $this->redirectToPreviousStep(); } } From dd2b53acc7288d46ef4dc2b33c55d20d6ac7479f Mon Sep 17 00:00:00 2001 From: mglaman Date: Tue, 13 Mar 2018 17:54:30 +0100 Subject: [PATCH 078/103] Issue #2882905 by Hubbs, mglaman: Add unique identifier for stored payment methods --- .../src/Plugin/Commerce/CheckoutPane/PaymentInformation.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index 746f46dede..e6c415ff19 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -152,6 +152,11 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, ], '#access' => count($options) > 1, ]; + // Add a class to each individual radio, to help themers. + foreach ($options as $option) { + $class_name = isset($option['payment_method']) ? 'stored' : 'new'; + $pane_form['payment_method'][$option['id']]['#attributes']['class'][] = "payment-method--$class_name"; + } // Store the values for submitPaneForm(). foreach ($options as $option_id => $option) { $pane_form['payment_method'][$option_id]['#payment_gateway'] = $option['payment_gateway']; From 845eb2860236e5894b5b79bb52b374d84e71e4f9 Mon Sep 17 00:00:00 2001 From: ctrlADel Date: Wed, 14 Mar 2018 01:28:45 +0100 Subject: [PATCH 079/103] Issue #2950989 by ctrlADel, mglaman, bojanz: Don't call toUrl() on unsaved product/promotion/store entities --- modules/product/commerce_product.module | 8 ++++++-- modules/promotion/commerce_promotion.module | 2 +- modules/store/commerce_store.module | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/product/commerce_product.module b/modules/product/commerce_product.module index a94eb1a1a7..44dc83d531 100644 --- a/modules/product/commerce_product.module +++ b/modules/product/commerce_product.module @@ -123,7 +123,7 @@ function template_preprocess_commerce_product(array &$variables) { $product = $variables['elements']['#commerce_product']; $variables['product_entity'] = $product; - $variables['product_url'] = $product->toUrl(); + $variables['product_url'] = $product->isNew() ? '' : $product->toUrl(); $variables['product'] = []; foreach (Element::children($variables['elements']) as $key) { $variables['product'][$key] = $variables['elements'][$key]; @@ -146,7 +146,11 @@ function template_preprocess_commerce_product_variation(array &$variables) { $product = $product_variation->getProduct(); $variables['product_variation_entity'] = $product_variation; - $variables['product_url'] = $product->toUrl(); + $variables['product_url'] = ''; + if ($product && !$product->isNew()) { + $variables['product_url'] = $product->toUrl(); + } + $variables['product_variation'] = []; foreach (Element::children($variables['elements']) as $key) { $variables['product_variation'][$key] = $variables['elements'][$key]; diff --git a/modules/promotion/commerce_promotion.module b/modules/promotion/commerce_promotion.module index 05c517e8c1..9395bab46f 100644 --- a/modules/promotion/commerce_promotion.module +++ b/modules/promotion/commerce_promotion.module @@ -49,7 +49,7 @@ function template_preprocess_commerce_promotion(array &$variables) { $promotion = $variables['elements']['#commerce_promotion']; $variables['promotion_entity'] = $promotion; - $variables['promotion_url'] = $promotion->toUrl(); + $variables['promotion_url'] = $promotion->isNew() ? '' : $promotion->toUrl(); $variables['promotion'] = []; foreach (Element::children($variables['elements']) as $key) { $variables['promotion'][$key] = $variables['elements'][$key]; diff --git a/modules/store/commerce_store.module b/modules/store/commerce_store.module index fe12873721..9de1282cbb 100644 --- a/modules/store/commerce_store.module +++ b/modules/store/commerce_store.module @@ -56,7 +56,7 @@ function template_preprocess_commerce_store(array &$variables) { $store = $variables['elements']['#commerce_store']; $variables['store_entity'] = $store; - $variables['store_url'] = $store->toUrl(); + $variables['store_url'] = $store->isNew() ? '' : $store->toUrl(); $variables['store'] = []; foreach (Element::children($variables['elements']) as $key) { $variables['store'][$key] = $variables['elements'][$key]; From 3641d2ec244e69cb3c05d26db01e15f8c425faed Mon Sep 17 00:00:00 2001 From: agoradesign Date: Wed, 14 Mar 2018 11:25:13 +0100 Subject: [PATCH 080/103] Issue #2945286 by agoradesign, mglaman, bojanz: The "access overview" permission is not used/respected --- modules/order/src/OrderRouteProvider.php | 2 +- modules/product/src/Entity/Product.php | 4 ++-- modules/product/src/Entity/ProductAttribute.php | 4 ++-- modules/promotion/src/Entity/Promotion.php | 4 ++-- modules/store/src/Entity/Store.php | 4 ++-- src/EntityPermissionProvider.php | 4 +++- 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/modules/order/src/OrderRouteProvider.php b/modules/order/src/OrderRouteProvider.php index bb99c7c84f..fe8380dd3c 100644 --- a/modules/order/src/OrderRouteProvider.php +++ b/modules/order/src/OrderRouteProvider.php @@ -3,7 +3,7 @@ namespace Drupal\commerce_order; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider; +use Drupal\entity\Routing\AdminHtmlRouteProvider; /** * Provides routes for the Order entity. diff --git a/modules/product/src/Entity/Product.php b/modules/product/src/Entity/Product.php index 6479e46b60..269a4c157d 100644 --- a/modules/product/src/Entity/Product.php +++ b/modules/product/src/Entity/Product.php @@ -29,7 +29,7 @@ * "event" = "Drupal\commerce_product\Event\ProductEvent", * "storage" = "Drupal\commerce\CommerceContentEntityStorage", * "access" = "Drupal\entity\EntityAccessControlHandler", - * "permission_provider" = "Drupal\commerce\EntityPermissionProvider", + * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "view_builder" = "Drupal\commerce_product\ProductViewBuilder", * "list_builder" = "Drupal\commerce_product\ProductListBuilder", * "views_data" = "Drupal\views\EntityViewsData", @@ -40,7 +40,7 @@ * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm" * }, * "route_provider" = { - * "default" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider", + * "default" = "Drupal\entity\Routing\AdminHtmlRouteProvider", * "delete-multiple" = "Drupal\entity\Routing\DeleteMultipleRouteProvider", * }, * "translation" = "Drupal\commerce_product\ProductTranslationHandler" diff --git a/modules/product/src/Entity/ProductAttribute.php b/modules/product/src/Entity/ProductAttribute.php index 677fb3c74c..37bc3513a4 100644 --- a/modules/product/src/Entity/ProductAttribute.php +++ b/modules/product/src/Entity/ProductAttribute.php @@ -20,7 +20,7 @@ * ), * handlers = { * "access" = "Drupal\entity\EntityAccessControlHandler", - * "permission_provider" = "Drupal\commerce\EntityPermissionProvider", + * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "list_builder" = "Drupal\commerce_product\ProductAttributeListBuilder", * "form" = { * "add" = "Drupal\commerce_product\Form\ProductAttributeForm", @@ -28,7 +28,7 @@ * "delete" = "Drupal\commerce_product\Form\ProductAttributeDeleteForm", * }, * "route_provider" = { - * "default" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider", + * "default" = "Drupal\entity\Routing\DefaultHtmlRouteProvider", * }, * }, * config_prefix = "commerce_product_attribute", diff --git a/modules/promotion/src/Entity/Promotion.php b/modules/promotion/src/Entity/Promotion.php index 71fb6078f2..ce394cab87 100644 --- a/modules/promotion/src/Entity/Promotion.php +++ b/modules/promotion/src/Entity/Promotion.php @@ -29,7 +29,7 @@ * "event" = "Drupal\commerce_promotion\Event\PromotionEvent", * "storage" = "Drupal\commerce_promotion\PromotionStorage", * "access" = "Drupal\entity\EntityAccessControlHandler", - * "permission_provider" = "Drupal\commerce\EntityPermissionProvider", + * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "list_builder" = "Drupal\commerce_promotion\PromotionListBuilder", * "views_data" = "Drupal\views\EntityViewsData", @@ -40,7 +40,7 @@ * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm" * }, * "route_provider" = { - * "default" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider", + * "default" = "Drupal\entity\Routing\AdminHtmlRouteProvider", * "delete-multiple" = "Drupal\entity\Routing\DeleteMultipleRouteProvider", * }, * "translation" = "Drupal\content_translation\ContentTranslationHandler" diff --git a/modules/store/src/Entity/Store.php b/modules/store/src/Entity/Store.php index df43a10327..fcea981ccc 100644 --- a/modules/store/src/Entity/Store.php +++ b/modules/store/src/Entity/Store.php @@ -28,7 +28,7 @@ * "event" = "Drupal\commerce_store\Event\StoreEvent", * "storage" = "Drupal\commerce_store\StoreStorage", * "access" = "Drupal\entity\EntityAccessControlHandler", - * "permission_provider" = "Drupal\commerce\EntityPermissionProvider", + * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "list_builder" = "Drupal\commerce_store\StoreListBuilder", * "views_data" = "Drupal\views\EntityViewsData", @@ -39,7 +39,7 @@ * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm" * }, * "route_provider" = { - * "default" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider", + * "default" = "Drupal\entity\Routing\AdminHtmlRouteProvider", * "delete-multiple" = "Drupal\entity\Routing\DeleteMultipleRouteProvider", * }, * "translation" = "Drupal\content_translation\ContentTranslationHandler" diff --git a/src/EntityPermissionProvider.php b/src/EntityPermissionProvider.php index 3d484b7a10..540d17fd8e 100644 --- a/src/EntityPermissionProvider.php +++ b/src/EntityPermissionProvider.php @@ -7,6 +7,8 @@ /** * Provides Commerce entity permissions. * - * @see \Drupal\entity\EntityPermissionProvider + * Note: This code has moved to Entity API, see the parent class. + * + * @deprecated in Commerce 2.0 */ class EntityPermissionProvider extends BaseEntityPermissionProvider {} From 445f106f69cda83cd4c1caac0e4d58a47caca061 Mon Sep 17 00:00:00 2001 From: mglaman Date: Thu, 15 Mar 2018 00:50:23 +0100 Subject: [PATCH 081/103] Issue #2856583 by mglaman, nikathone, jonnyeom, bojanz, chrisrockwell, chishah92, adanielyan: Allow free orders (checkout without payment) --- .../CheckoutFlow/CheckoutFlowBase.php | 2 +- .../Plugin/Commerce/CheckoutPane/Review.php | 2 +- .../src/Functional/CheckoutOrderTest.php | 4 +- .../CheckoutPane/PaymentInformation.php | 102 ++++++++++++------ .../Commerce/CheckoutPane/PaymentProcess.php | 12 ++- .../PaymentCheckoutTest.php | 40 +++++++ 6 files changed, 123 insertions(+), 39 deletions(-) diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php index 4df1347f59..bbf695ead0 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php @@ -152,7 +152,7 @@ public function getSteps() { ], 'complete' => [ 'label' => $this->t('Complete'), - 'next_label' => $this->t('Pay and complete purchase'), + 'next_label' => $this->t('Complete checkout'), 'has_sidebar' => FALSE, ], ]; diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/Review.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/Review.php index e391dcd56b..509a75d09e 100644 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/Review.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/Review.php @@ -33,7 +33,7 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, ]; } - $label = $pane->getDisplayLabel(); + $label = isset($summary['#title']) ? $summary['#title'] : $pane->getDisplayLabel(); if ($pane->isVisible()) { $edit_link = Link::createFromRoute($this->t('Edit'), 'commerce_checkout.form', [ 'commerce_order' => $this->order->id(), diff --git a/modules/checkout/tests/src/Functional/CheckoutOrderTest.php b/modules/checkout/tests/src/Functional/CheckoutOrderTest.php index 7ed0ccbc54..22cb2539b5 100644 --- a/modules/checkout/tests/src/Functional/CheckoutOrderTest.php +++ b/modules/checkout/tests/src/Functional/CheckoutOrderTest.php @@ -161,7 +161,7 @@ public function testGuestOrderCheckout() { $this->assertSession()->pageTextContains('Contact information'); $this->assertSession()->pageTextContains('Billing information'); $this->assertSession()->pageTextContains('Order Summary'); - $this->submitForm([], 'Pay and complete purchase'); + $this->submitForm([], 'Complete checkout'); $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.'); $this->assertSession()->pageTextContains('0 items'); // Test second order. @@ -197,7 +197,7 @@ public function testGuestOrderCheckout() { $this->getSession()->getPage()->pressButton('Continue to review'); $this->assertCheckoutProgressStep('Review'); - $this->submitForm([], 'Pay and complete purchase'); + $this->submitForm([], 'Complete checkout'); $this->assertSession()->pageTextContains('Your order number is 2. You can view your order on your account page when logged in.'); $this->assertSession()->pageTextContains('0 items'); } diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index e6c415ff19..748b63b36d 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -71,32 +71,36 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function buildPaneSummary() { + $billing_profile = $this->order->getBillingProfile(); + if ($this->order->getTotalPrice()->isZero() && $billing_profile) { + // Only the billing information was collected. + $view_builder = $this->entityTypeManager->getViewBuilder('profile'); + $summary = [ + '#title' => $this->t('Billing information'), + 'profile' => $view_builder->view($billing_profile, 'default'), + ]; + return $summary; + } + $summary = []; /** @var \Drupal\commerce_payment\Entity\PaymentGatewayInterface $payment_gateway */ $payment_gateway = $this->order->get('payment_gateway')->entity; if (!$payment_gateway) { return $summary; } - - $payment_gateway_plugin = $payment_gateway->getPlugin(); $payment_method = $this->order->get('payment_method')->entity; - if ($payment_gateway_plugin instanceof SupportsStoredPaymentMethodsInterface && $payment_method) { + if ($payment_method) { $view_builder = $this->entityTypeManager->getViewBuilder('commerce_payment_method'); $summary = $view_builder->view($payment_method, 'default'); } - else { - $billing_profile = $this->order->getBillingProfile(); - if ($billing_profile) { - $profile_view_builder = $this->entityTypeManager->getViewBuilder('profile'); - $profile_view = $profile_view_builder->view($billing_profile, 'default'); - $label = $payment_gateway->getPlugin()->getDisplayLabel(); - $summary = [ - 'label' => [ - '#markup' => $label, - ], - 'profile' => $profile_view, - ]; - } + elseif ($billing_profile) { + $view_builder = $this->entityTypeManager->getViewBuilder('profile'); + $summary = [ + 'payment_gateway' => [ + '#markup' => $payment_gateway->getPlugin()->getDisplayLabel(), + ], + 'profile' => $view_builder->view($billing_profile, 'default'), + ]; } return $summary; @@ -106,6 +110,13 @@ public function buildPaneSummary() { * {@inheritdoc} */ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) { + if ($this->order->getTotalPrice()->isZero()) { + // Free orders don't need payment, collect just the billing information. + $pane_form['#title'] = $this->t('Billing information'); + $pane_form = $this->buildBillingProfileForm($pane_form, $form_state); + return $pane_form; + } + /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */ $payment_gateway_storage = $this->entityTypeManager->getStorage('commerce_payment_gateway'); // Load the payment gateways. This fires an event for filtering the @@ -114,7 +125,7 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, // Can't proceed without any payment gateways. if (empty($payment_gateways)) { $this->messenger->addError($this->noPaymentGatewayErrorMessage()); - return []; + return $pane_form; } $options = $this->buildPaymentMethodOptions($payment_gateways); @@ -189,21 +200,7 @@ public function buildPaneForm(array $pane_form, FormStateInterface $form_state, } } else { - $store = $this->order->getStore(); - $billing_profile = $this->order->getBillingProfile(); - if (!$billing_profile) { - $billing_profile = $this->entityTypeManager->getStorage('profile')->create([ - 'uid' => $this->order->getCustomerId(), - 'type' => 'customer', - ]); - } - - $pane_form['billing_information'] = [ - '#type' => 'commerce_profile_select', - '#default_value' => $billing_profile, - '#default_country' => $store->getAddress()->getCountryCode(), - '#available_countries' => $store->getBillingCountries(), - ]; + $pane_form = $this->buildBillingProfileForm($pane_form, $form_state); } return $pane_form; @@ -368,6 +365,37 @@ protected function getDefaultPaymentMethodOption(array $options) { return $default_option; } + /** + * Builds the billing profile form. + * + * @param array $pane_form + * The pane form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state of the parent form. + * + * @return array + * The modified pane form. + */ + protected function buildBillingProfileForm(array $pane_form, FormStateInterface $form_state) { + $store = $this->order->getStore(); + $billing_profile = $this->order->getBillingProfile(); + if (!$billing_profile) { + $billing_profile = $this->entityTypeManager->getStorage('profile')->create([ + 'uid' => $this->order->getCustomerId(), + 'type' => 'customer', + ]); + } + + $pane_form['billing_information'] = [ + '#type' => 'commerce_profile_select', + '#default_value' => $billing_profile, + '#default_country' => $store->getAddress()->getCountryCode(), + '#available_countries' => $store->getBillingCountries(), + ]; + + return $pane_form; + } + /** * Ajax callback. */ @@ -381,8 +409,11 @@ public static function ajaxRefresh(array $form, FormStateInterface $form_state) * {@inheritdoc} */ public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) { - $values = $form_state->getValue($pane_form['#parents']); + if ($this->order->getTotalPrice()->isZero()) { + return; + } + $values = $form_state->getValue($pane_form['#parents']); if (!isset($values['payment_method'])) { $form_state->setError($complete_form, $this->noPaymentGatewayErrorMessage()); } @@ -392,6 +423,11 @@ public function validatePaneForm(array &$pane_form, FormStateInterface $form_sta * {@inheritdoc} */ public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) { + if ($this->order->getTotalPrice()->isZero()) { + $this->order->setBillingProfile($pane_form['billing_information']['#profile']); + return; + } + $values = $form_state->getValue($pane_form['#parents']); $selected_option = $pane_form['payment_method'][$values['payment_method']]; /** @var \Drupal\commerce_payment\PaymentGatewayStorageInterface $payment_gateway_storage */ diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php index 015f79ae26..d43bd8c03e 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentProcess.php @@ -130,9 +130,17 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s * {@inheritdoc} */ public function isVisible() { - // This pane can't be used without the PaymentInformation pane. + if ($this->order->getTotalPrice()->isZero()) { + // Hide the pane for free orders, since they don't need a payment. + return FALSE; + } $payment_info_pane = $this->checkoutFlow->getPane('payment_information'); - return $payment_info_pane->isVisible() && $payment_info_pane->getStepId() != '_disabled'; + if (!$payment_info_pane->isVisible() || $payment_info_pane->getStepId() == '_disabled') { + // Hide the pane if the PaymentInformation pane has been disabled. + return FALSE; + } + + return TRUE; } /** diff --git a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php index 31221e0d97..d2238400fe 100644 --- a/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php +++ b/modules/payment/tests/src/FunctionalJavascript/PaymentCheckoutTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\commerce_payment\FunctionalJavascript; use Drupal\commerce_checkout\Entity\CheckoutFlow; +use Drupal\commerce_order\Adjustment; use Drupal\commerce_order\Entity\Order; use Drupal\commerce_payment\Entity\Payment; use Drupal\commerce_payment\Entity\PaymentGateway; @@ -527,4 +528,43 @@ public function testCheckoutWithManual() { $this->assertEquals($payment->getAmount(), $order->getTotalPrice()); } + /** + * Tests a free order, where only the billing information is collected. + */ + public function testFreeOrder() { + $this->drupalGet($this->product->toUrl()->toString()); + $this->submitForm([], 'Add to cart'); + + // Add an adjustment to zero out the order total. + $order = Order::load(1); + $order->addAdjustment(new Adjustment([ + 'type' => 'custom', + 'label' => 'Surprise, it is free!', + 'amount' => $order->getTotalPrice()->multiply('-1'), + 'locked' => TRUE, + ])); + $order->save(); + + $this->drupalGet('checkout/1'); + $this->assertSession()->pageTextContains('Billing information'); + $this->assertSession()->pageTextNotContains('Payment information'); + $this->submitForm([ + 'payment_information[billing_information][address][0][address][given_name]' => 'Johnny', + 'payment_information[billing_information][address][0][address][family_name]' => 'Appleseed', + 'payment_information[billing_information][address][0][address][address_line1]' => '123 New York Drive', + 'payment_information[billing_information][address][0][address][locality]' => 'New York City', + 'payment_information[billing_information][address][0][address][administrative_area]' => 'NY', + 'payment_information[billing_information][address][0][address][postal_code]' => '10001', + ], 'Continue to review'); + + $this->assertSession()->pageTextContains('Billing information'); + $this->assertSession()->pageTextNotContains('Payment information'); + $this->assertSession()->pageTextContains('Example'); + $this->assertSession()->pageTextContains('Johnny Appleseed'); + $this->assertSession()->pageTextContains('123 New York Drive'); + + $this->submitForm([], 'Complete checkout'); + $this->assertSession()->pageTextContains('Your order number is 1. You can view your order on your account page when logged in.'); + } + } From 936de53fd0ac89cefdf44bcb29b7e685967890df Mon Sep 17 00:00:00 2001 From: saurabh-vijayvargiya Date: Thu, 15 Mar 2018 13:22:30 +0100 Subject: [PATCH 082/103] Issue #2927114 by sorabh.v6, mglaman, bojanz, heddn: Rework the "no payment gateways are defined" checkout error --- .../CheckoutPane/PaymentInformation.php | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php index 748b63b36d..53ba14846a 100644 --- a/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php +++ b/modules/payment/src/Plugin/Commerce/CheckoutPane/PaymentInformation.php @@ -10,6 +10,8 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -24,6 +26,13 @@ */ class PaymentInformation extends CheckoutPaneBase { + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + /** * The messenger. * @@ -44,12 +53,15 @@ class PaymentInformation extends CheckoutPaneBase { * The parent checkout flow. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. * @param \Drupal\Core\Messenger\MessengerInterface $messenger * The messenger. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager, MessengerInterface $messenger) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, MessengerInterface $messenger) { parent::__construct($configuration, $plugin_id, $plugin_definition, $checkout_flow, $entity_type_manager); + $this->currentUser = $current_user; $this->messenger = $messenger; } @@ -63,6 +75,7 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_definition, $checkout_flow, $container->get('entity_type.manager'), + $container->get('current_user'), $container->get('messenger') ); } @@ -462,13 +475,21 @@ public function submitPaneForm(array &$pane_form, FormStateInterface $form_state } /** - * Returns an error message in case there are no payment gateways. + * Returns an error message in case there are no available payment gateways. * * @return \Drupal\Core\StringTranslation\TranslatableMarkup * The error message. */ protected function noPaymentGatewayErrorMessage() { - return $this->t('No payment gateways are defined, create one first.'); + if ($this->currentUser->hasPermission('administer commerce_payment_gateway')) { + $message = $this->t('There are no payment gateways available for this order.', [ + ':url' => Url::fromRoute('entity.commerce_payment_gateway.collection')->toString(), + ]); + } + else { + $message = $this->t('There are no payment gateways available for this order. Please try again later.'); + } + return $message; } } From 2e6f96991a3e3952f27a8666077bd5398a8217ec Mon Sep 17 00:00:00 2001 From: Lendude Date: Fri, 16 Mar 2018 06:01:27 +0100 Subject: [PATCH 083/103] Issue #2952849 by Lendude: Random test fail in Drupal\Tests\commerce_promotion\FunctionalJavascript\CouponTest --- .../CouponTest.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) rename modules/promotion/tests/src/{FunctionalJavascript => Functional}/CouponTest.php (92%) diff --git a/modules/promotion/tests/src/FunctionalJavascript/CouponTest.php b/modules/promotion/tests/src/Functional/CouponTest.php similarity index 92% rename from modules/promotion/tests/src/FunctionalJavascript/CouponTest.php rename to modules/promotion/tests/src/Functional/CouponTest.php index e21c52e389..d0059590b7 100644 --- a/modules/promotion/tests/src/FunctionalJavascript/CouponTest.php +++ b/modules/promotion/tests/src/Functional/CouponTest.php @@ -1,11 +1,10 @@ assertSession()->fieldExists('quantity'); $this->getSession()->getPage()->fillField('quantity', (string) $coupon_quantity); - - // Use POST here to invoke batch_process() in the internal browser. - $this->drupalPostForm(NULL, [], t('Generate')); - - // Wait until the id="updateprogress" element is gone, or timeout after 3 minutes. - $this->getSession()->wait(180000, 'jQuery("#updateprogress").length === 0'); + $this->getSession()->getPage()->pressButton('Generate'); + $this->checkForMetaRefresh(); $this->assertSession()->pageTextContains("Generated $coupon_quantity coupons."); $coupon_count = $this->getSession()->getPage()->findAll('xpath', '//table/tbody/tr/td[text()="0 / 1"]'); From 0be04680059c31e469792be38fd0909cfd936d30 Mon Sep 17 00:00:00 2001 From: mglaman Date: Fri, 16 Mar 2018 06:10:23 +0100 Subject: [PATCH 084/103] Issue #2707721 by drugan, mglaman, skek, AndreaMaggi, ransomweaver: Incorrect display of attribute field values on the Add To Cart form --- .../AddToCartOptionalAttributeTest.php | 195 ++++++ modules/product/commerce_product.services.yml | 4 + .../ProductVariationAttributesWidget.php | 94 +-- .../src/ProductVariationAttributeMapper.php | 171 +++++ ...oductVariationAttributeMapperInterface.php | 56 ++ .../ProductVariationAttributeMapperTest.php | 601 ++++++++++++++++++ 6 files changed, 1045 insertions(+), 76 deletions(-) create mode 100644 modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php create mode 100644 modules/product/src/ProductVariationAttributeMapper.php create mode 100644 modules/product/src/ProductVariationAttributeMapperInterface.php create mode 100644 modules/product/tests/src/Kernel/ProductVariationAttributeMapperTest.php diff --git a/modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php b/modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php new file mode 100644 index 0000000000..b16200d672 --- /dev/null +++ b/modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php @@ -0,0 +1,195 @@ +variation->bundle()); + + // All attribute-groups have an 'x' value that stand for 'empty'. + $number_attributes = $this->createAttributeSet($variation_type, 'number', [ + 'one' => 'one', + 'two' => 'two', + ]); + $greek_attributes = $this->createAttributeSet($variation_type, 'greek', [ + 'alpha' => 'alpha', + 'omega' => 'omega', + ]); + + $attribute_values_matrix = [ + ['one', 'omega'], + ['two', 'alpha'], + ]; + + // Generate variations from the attribute-matrix. + $variations = []; + foreach ($attribute_values_matrix as $key => $value) { + $variation = $this->createEntity('commerce_product_variation', [ + 'type' => $variation_type->id(), + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + 'attribute_number' => $number_attributes[$value[0]], + 'attribute_greek' => $greek_attributes[$value[1]], + ]); + $variations[] = $variation; + } + $product = $this->createEntity('commerce_product', [ + 'type' => 'default', + 'title' => 'OPTIONAL_ATTRIBUTES_TEST', + 'stores' => [$this->store], + 'variations' => $variations, + ]); + + // Helper variables. + $number_selector = 'purchased_entity[0][attributes][attribute_number]'; + $greek_selector = 'purchased_entity[0][attributes][attribute_greek]'; + + + // Initial state: ['one', 'x']. + $this->drupalGet($product->toUrl()); + $this->assertAttributeSelected($number_selector, 'one'); + $this->assertAttributeSelected($greek_selector, 'omega'); + // Expect that 'number' selector can be used. + $this->assertAttributeExists($number_selector, $number_attributes['two']->id()); + // Expect that 'greek' selector cannot be used. + $this->assertAttributeDoesNotExist($greek_selector, $greek_attributes['alpha']->id()); + + + // Use AJAX to change the number-attribute to 'x'. + $this->drupalGet($product->toUrl()); + $this->getSession()->getPage()->selectFieldOption($number_selector, 'two'); + $this->waitForAjaxToFinish(); + // New state: ['x', 'alpha']. + $this->assertAttributeSelected($number_selector, 'two'); + $this->assertAttributeSelected($greek_selector, 'alpha'); + // Expect that 'number' selector can be used. + $this->assertAttributeExists($number_selector, $number_attributes['one']->id()); + // Expect that 'greek' selector cannot be used. + $this->assertAttributeDoesNotExist($greek_selector, $greek_attributes['omega']->id()); + + } + + /** + * Tests add-to-cart form where variation have mutually exclusive attributes. + * + * @group debug + * + * @see https://www.drupal.org/node/2730643 + */ + public function testMutuallyExclusiveAttributeMatrixTwoByTwobyTwo() { + /** @var \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type */ + $variation_type = ProductVariationType::load($this->variation->bundle()); + + // All attribute-groups have an 'x' value that stand for 'empty'. + $number_attributes = $this->createAttributeSet($variation_type, 'number', [ + 'one' => 'one', + 'two' => 'two', + ]); + $greek_attributes = $this->createAttributeSet($variation_type, 'greek', [ + 'alpha' => 'alpha', + 'omega' => 'omega', + ]); + $city_attributes = $this->createAttributeSet($variation_type, 'city', [ + 'milano' => 'milano', + 'pancevo' => 'pancevo', + ]); + + $attribute_values_matrix = [ + ['one', 'omega', 'pancevo'], + ['two', 'alpha', 'pancevo'], + ['two', 'omega', 'milano'], + ]; + + // Generate variations from the attribute-matrix. + $variations = []; + foreach ($attribute_values_matrix as $key => $value) { + $variation = $this->createEntity('commerce_product_variation', [ + 'type' => $variation_type->id(), + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + 'attribute_number' => $number_attributes[$value[0]], + 'attribute_greek' => $greek_attributes[$value[1]], + 'attribute_city' => $city_attributes[$value[2]], + ]); + $variations[] = $variation; + } + $product = $this->createEntity('commerce_product', [ + 'type' => 'default', + 'title' => 'OPTIONAL_ATTRIBUTES_TEST', + 'stores' => [$this->store], + 'variations' => $variations, + ]); + + // Helper variables. + $number_selector = 'purchased_entity[0][attributes][attribute_number]'; + $greek_selector = 'purchased_entity[0][attributes][attribute_greek]'; + $city_selector = 'purchased_entity[0][attributes][attribute_city]'; + + + // Initial state: ['one', 'omega', 'pancevo']. + $this->drupalGet($product->toUrl()); + $this->assertAttributeSelected($number_selector, 'one'); + $this->assertAttributeSelected($greek_selector, 'omega'); + $this->assertAttributeSelected($city_selector, 'pancevo'); + + // Use AJAX to change the number-attribute to 'two'. + $this->drupalGet($product->toUrl()); + $this->getSession()->getPage()->selectFieldOption($number_selector, 'two'); + $this->waitForAjaxToFinish(); + $this->saveHtmlOutput(); + + // New state: ['two', 'alpha', 'pancevo']. + // The top level attribute was adjusted, so the options are reset. + $this->assertAttributeSelected($number_selector, 'two'); + $this->assertAttributeSelected($greek_selector, 'alpha'); + $this->assertAttributeSelected($city_selector, 'pancevo'); + + $this->assertAttributeExists($number_selector, $number_attributes['one']->id()); + $this->assertAttributeExists($greek_selector, $greek_attributes['omega']->id()); + $this->assertAttributeExists($city_selector, $city_attributes['pancevo']->id()); + + // Use AJAX to change the number-attribute to 'two'. + $this->drupalGet($product->toUrl()); + $this->getSession()->getPage()->selectFieldOption($greek_selector, 'omega'); + $this->waitForAjaxToFinish(); + $this->saveHtmlOutput(); + + // New state: ['one', 'omega', 'pancevo']. + // The top level attribute was adjusted, so the options are reset. + $this->assertAttributeSelected($number_selector, 'one'); + $this->assertAttributeSelected($greek_selector, 'omega'); + $this->assertAttributeSelected($city_selector, 'pancevo'); + + $this->assertAttributeExists($number_selector, $number_attributes['two']->id()); + $this->assertAttributeDoesNotExist($greek_selector, $greek_attributes['alpha']->id()); + // We should not be able to change the city. + // There is one variation with "one" and "omega", which means there is only + // one city option. + $this->assertAttributeDoesNotExist($city_selector, $city_attributes['milano']->id()); + } + +} diff --git a/modules/product/commerce_product.services.yml b/modules/product/commerce_product.services.yml index 619897ee9a..f2e999e5f4 100644 --- a/modules/product/commerce_product.services.yml +++ b/modules/product/commerce_product.services.yml @@ -16,3 +16,7 @@ services: arguments: ['@current_route_match'] tags: - { name: 'context_provider' } + + commerce_product.variation_attribute_value_mapper: + class: Drupal\commerce_product\ProductVariationAttributeMapper + arguments: ['@entity_type.manager', '@entity.repository', '@commerce_product.attribute_field_manager'] diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php index 8a347cf409..e60b11c21e 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php @@ -4,6 +4,7 @@ use Drupal\commerce_product\Entity\ProductVariationInterface; use Drupal\commerce_product\ProductAttributeFieldManagerInterface; +use Drupal\commerce_product\ProductVariationAttributeMapperInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityRepositoryInterface; @@ -41,6 +42,13 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem */ protected $attributeStorage; + /** + * The variation attribute value mapper. + * + * @var \Drupal\commerce_product\ProductVariationAttributeMapperInterface + */ + protected $variationAttributeValueMapper; + /** * Constructs a new ProductVariationAttributesWidget object. * @@ -60,12 +68,15 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem * The entity repository. * @param \Drupal\commerce_product\ProductAttributeFieldManagerInterface $attribute_field_manager * The attribute field manager. + * @param \Drupal\commerce_product\ProductVariationAttributeMapperInterface $variation_attribute_value_mapper + * The variation attribute value resolver. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager, ProductVariationAttributeMapperInterface $variation_attribute_value_mapper) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $entity_type_manager, $entity_repository); $this->attributeFieldManager = $attribute_field_manager; $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); + $this->variationAttributeValueMapper = $variation_attribute_value_mapper; } /** @@ -80,7 +91,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['third_party_settings'], $container->get('entity_type.manager'), $container->get('entity.repository'), - $container->get('commerce_product.attribute_field_manager') + $container->get('commerce_product.attribute_field_manager'), + $container->get('commerce_product.variation_attribute_value_mapper') ); } @@ -224,24 +236,8 @@ public function massageFormValues(array $values, array $form, FormStateInterface * The selected variation. */ protected function selectVariationFromUserInput(array $variations, array $user_input) { - $current_variation = reset($variations); - if (!empty($user_input['attributes'])) { - $attributes = $user_input['attributes']; - foreach ($variations as $variation) { - $match = TRUE; - foreach ($attributes as $field_name => $value) { - if ($variation->getAttributeValueId($field_name) != $value) { - $match = FALSE; - } - } - if ($match) { - $current_variation = $variation; - break; - } - } - } - - return $current_variation; + $attributes = !empty($user_input['attributes']) ? $user_input['attributes'] : []; + return $this->variationAttributeValueMapper->getVariation($variations, $attributes); } /** @@ -256,48 +252,7 @@ protected function selectVariationFromUserInput(array $variations, array $user_i * The attribute information, keyed by field name. */ protected function getAttributeInfo(ProductVariationInterface $selected_variation, array $variations) { - $attributes = []; - $field_definitions = $this->attributeFieldManager->getFieldDefinitions($selected_variation->bundle()); - $field_map = $this->attributeFieldManager->getFieldMap($selected_variation->bundle()); - $field_names = array_column($field_map, 'field_name'); - $attribute_ids = array_column($field_map, 'attribute_id'); - $index = 0; - foreach ($field_names as $field_name) { - $field = $field_definitions[$field_name]; - /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */ - $attribute = $this->attributeStorage->load($attribute_ids[$index]); - // Make sure we have translation for attribute. - $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId()); - - $attributes[$field_name] = [ - 'field_name' => $field_name, - 'title' => $attribute->label(), - 'required' => $field->isRequired(), - 'element_type' => $attribute->getElementType(), - ]; - // The first attribute gets all values. Every next attribute gets only - // the values from variations matching the previous attribute value. - // For 'Color' and 'Size' attributes that means getting the colors of all - // variations, but only the sizes of variations with the selected color. - $callback = NULL; - if ($index > 0) { - $previous_field_name = $field_names[$index - 1]; - $previous_field_value = $selected_variation->getAttributeValueId($previous_field_name); - $callback = function ($variation) use ($previous_field_name, $previous_field_value) { - /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ - return $variation->getAttributeValueId($previous_field_name) == $previous_field_value; - }; - } - - $attributes[$field_name]['values'] = $this->getAttributeValues($variations, $field_name, $callback); - $index++; - } - // Filter out attributes with no values. - $attributes = array_filter($attributes, function ($attribute) { - return !empty($attribute['values']); - }); - - return $attributes; + return $this->variationAttributeValueMapper->getAttributeInfo($selected_variation, $variations); } /** @@ -314,20 +269,7 @@ protected function getAttributeInfo(ProductVariationInterface $selected_variatio * The attribute values, keyed by attribute ID. */ protected function getAttributeValues(array $variations, $field_name, callable $callback = NULL) { - $values = []; - foreach ($variations as $variation) { - if (is_null($callback) || call_user_func($callback, $variation)) { - $attribute_value = $variation->getAttributeValue($field_name); - if ($attribute_value) { - $values[$attribute_value->id()] = $attribute_value->label(); - } - else { - $values['_none'] = ''; - } - } - } - - return $values; + return $this->variationAttributeValueMapper->getAttributeValues($variations, $field_name, $callback); } } diff --git a/modules/product/src/ProductVariationAttributeMapper.php b/modules/product/src/ProductVariationAttributeMapper.php new file mode 100644 index 0000000000..7822573dd4 --- /dev/null +++ b/modules/product/src/ProductVariationAttributeMapper.php @@ -0,0 +1,171 @@ +entityRepository = $entity_repository; + $this->attributeFieldManager = $attribute_field_manager; + $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); + } + + /** + * {@inheritdoc} + */ + public function getVariation(array $variations, array $attribute_values = []) { + $current_variation = reset($variations); + if (empty($attribute_values)) { + return $current_variation; + } + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ + foreach ($variations as $variation) { + $match = TRUE; + foreach ($attribute_values as $attribute_field_name => $attribute_value) { + // If any one of the attributes do not match, this is not a valid + // candidate for the resolved variation. + if ($variation->getAttributeValueId($attribute_field_name) != $attribute_value) { + $match = FALSE; + } + } + if ($match) { + $current_variation = $variation; + break; + } + } + + return $current_variation; + } + + /** + * Gets the attribute information for the selected product variation. + * + * @param \Drupal\commerce_product\Entity\ProductVariationInterface $selected_variation + * The selected product variation. + * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations + * The available product variations. + * + * @return array[] + * The attribute information, keyed by field name. + */ + public function getAttributeInfo(ProductVariationInterface $selected_variation, array $variations) { + $attributes = []; + $field_definitions = $this->attributeFieldManager->getFieldDefinitions($selected_variation->bundle()); + $field_map = $this->attributeFieldManager->getFieldMap($selected_variation->bundle()); + $field_names = array_column($field_map, 'field_name'); + $attribute_ids = array_column($field_map, 'attribute_id'); + $index = 0; + foreach ($field_names as $field_name) { + $field = $field_definitions[$field_name]; + /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */ + $attribute = $this->attributeStorage->load($attribute_ids[$index]); + // Make sure we have translation for attribute. + $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId()); + + $attributes[$field_name] = [ + 'field_name' => $field_name, + 'title' => $attribute->label(), + 'required' => $field->isRequired(), + 'element_type' => $attribute->getElementType(), + ]; + // The first attribute gets all values. Every next attribute gets only + // the values from variations matching the previous attribute value. + // For 'Color' and 'Size' attributes that means getting the colors of all + // variations, but only the sizes of variations with the selected color. + $callback = NULL; + if ($index > 0) { + $index_limit = $index - 1; + // Get all previous field values. + $previous_variation_field_values = []; + for ($i = 0; $i <= $index_limit; $i++) { + $previous_variation_field_values[$field_names[$i]] = $selected_variation->getAttributeValueId($field_names[$i]); + } + + $callback = function (ProductVariationInterface $variation) use ($previous_variation_field_values) { + $results = []; + foreach ($previous_variation_field_values as $previous_field_name => $previous_field_value) { + $results[] = $variation->getAttributeValueId($previous_field_name) == $previous_field_value; + } + return !in_array(FALSE, $results, TRUE); + }; + } + + $attributes[$field_name]['values'] = $this->getAttributeValues($variations, $field_name, $callback); + $index++; + } + // Filter out attributes with no values. + $attributes = array_filter($attributes, function ($attribute) { + return !empty($attribute['values']); + }); + + return $attributes; + } + + /** + * Gets the attribute values of a given set of variations. + * + * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations + * The variations. + * @param string $field_name + * The field name of the attribute. + * @param callable|null $callback + * An optional callback to use for filtering the list. + * + * @return array[] + * The attribute values, keyed by attribute ID. + */ + public function getAttributeValues(array $variations, $field_name, callable $callback = NULL) { + $values = []; + foreach ($variations as $variation) { + if (is_null($callback) || call_user_func($callback, $variation)) { + $attribute_value = $variation->getAttributeValue($field_name); + if ($attribute_value) { + $values[$attribute_value->id()] = $attribute_value->label(); + } + else { + $values['_none'] = ''; + } + } + } + + return $values; + } + +} diff --git a/modules/product/src/ProductVariationAttributeMapperInterface.php b/modules/product/src/ProductVariationAttributeMapperInterface.php new file mode 100644 index 0000000000..3625c44683 --- /dev/null +++ b/modules/product/src/ProductVariationAttributeMapperInterface.php @@ -0,0 +1,56 @@ +installEntitySchema('commerce_product_variation'); + $this->installEntitySchema('commerce_product'); + $this->installEntitySchema('commerce_product_attribute'); + $this->installEntitySchema('commerce_product_attribute_value'); + $this->installConfig(['commerce_product']); + $this->attributeFieldManager = $this->container->get('commerce_product.attribute_field_manager'); + $this->mapper = $this->container->get('commerce_product.variation_attribute_value_mapper'); + + $variation_type = ProductVariationType::load('default'); + + // Create attributes. + $color_attributes = $this->createAttributeSet($variation_type, 'color', [ + 'red' => 'Red', + 'blue' => 'Blue', + ]); + $size_attributes = $this->createAttributeSet($variation_type, 'size', [ + 'small' => 'Small', + 'medium' => 'Medium', + 'large' => 'Large', + ]); + + $ram_attributes = $this->createAttributeSet($variation_type, 'ram', [ + '4gb' => '4GB', + '8gb' => '8GB', + '16gb' => '16GB', + '32gb' => '32GB', + ]); + + $disk1_attributes = $this->createAttributeSet($variation_type, 'disk1', [ + '1tb' => '1TB', + '2tb' => '2TB', + '3tb' => '3TB', + ]); + $disk2_attributes = $this->createAttributeSet($variation_type, 'disk2', [ + '1tb' => '1TB', + '2tb' => '2TB', + '3tb' => '3TB', + ]); + + $this->colorAttributes = $color_attributes; + $this->sizeAttributes = $size_attributes; + + $this->ramAttributes = $ram_attributes; + $this->disk1Attributes = $disk1_attributes; + $this->disk2Attributes = $disk2_attributes; + } + + /** + * Tests that if no attributes are passed, the default variation is returned. + */ + public function testResolveWithNoAttributes() { + $product = $this->generateThreeByTwoScenario(); + $resolved_variation = $this->mapper->getVariation($product->getVariations()); + $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); + + $resolved_variation = $this->mapper->getVariation($product->getVariations(), [ + 'attribute_color' => '', + ]); + $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); + + $resolved_variation = $this->mapper->getVariation($product->getVariations(), [ + 'attribute_color' => '', + 'attribute_size' => '', + ]); + $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); + } + + /** + * Tests that if one attribute passed, the proper variation is returned. + */ + public function testResolveWithWithOneAttribute() { + $product = $this->generateThreeByTwoScenario(); + $variations = $product->getVariations(); + + $resolved_variation = $this->mapper->getVariation($variations, [ + 'attribute_color' => $this->colorAttributes['blue']->id(), + ]); + $this->assertEquals($variations[3]->id(), $resolved_variation->id()); + + $resolved_variation = $this->mapper->getVariation($variations, [ + 'attribute_size' => $this->sizeAttributes['large']->id(), + ]); + $this->assertEquals($variations[2]->id(), $resolved_variation->id()); + } + + /** + * Tests that if two attributes are passed, the proper variation is returned. + */ + public function testResolveWithWithTwoAttributes() { + $product = $this->generateThreeByTwoScenario(); + $variations = $product->getVariations(); + + $resolved_variation = $this->mapper->getVariation($variations, [ + 'attribute_color' => $this->colorAttributes['red']->id(), + 'attribute_size' => $this->sizeAttributes['large']->id(), + ]); + $this->assertEquals($variations[2]->id(), $resolved_variation->id()); + + $resolved_variation = $this->mapper->getVariation($variations, [ + 'attribute_color' => $this->colorAttributes['blue']->id(), + 'attribute_size' => $this->sizeAttributes['large']->id(), + ]); + // An invalid arrangement was passed, so the default variation is resolved. + $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); + + $resolved_variation = $this->mapper->getVariation($variations, [ + 'attribute_color' => '', + 'attribute_size' => $this->sizeAttributes['large']->id(), + ]); + // A missing attribute was passed for first option. + $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); + + $resolved_variation = $this->mapper->getVariation($variations, [ + 'attribute_color' => $this->colorAttributes['blue']->id(), + 'attribute_size' => $this->sizeAttributes['small']->id(), + ]); + // An empty second option defaults to first variation option. + $this->assertEquals($variations[3]->id(), $resolved_variation->id()); + } + + /** + * Tests optional attributes. + */ + public function testResolveWithOptionalAttributes() { + $product = $this->generateThreeByTwoOptionalScenario(); + $variations = $product->getVariations(); + + $resolved_variation = $this->mapper->getVariation($variations, [ + 'attribute_ram' => $this->ramAttributes['16gb']->id(), + ]); + $this->assertEquals($variations[1]->id(), $resolved_variation->id()); + + $resolved_variation = $this->mapper->getVariation($variations, [ + 'attribute_ram' => $this->ramAttributes['16gb']->id(), + 'attribute_disk1' => $this->disk1Attributes['1tb']->id(), + 'attribute_disk2' => $this->disk2Attributes['1tb']->id(), + ]); + $this->assertEquals($variations[2]->id(), $resolved_variation->id()); + + $resolved_variation = $this->mapper->getVariation($variations, [ + 'attribute_ram' => $this->ramAttributes['16gb']->id(), + 'attribute_disk1' => $this->disk1Attributes['1tb']->id(), + 'attribute_disk2' => $this->disk2Attributes['2tb']->id(), + ]); + $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); + } + + /** + * Tests the getAttributeValues method. + */ + public function testGetAttributeValues() { + $product = $this->generateThreeByTwoScenario(); + $variations = $product->getVariations(); + + // With no callback, all value should be returned. + $values = $this->mapper->getAttributeValues($variations, 'attribute_color'); + foreach ($this->colorAttributes as $color_attribute) { + $this->assertTrue(in_array($color_attribute->label(), $values)); + } + + // With no callback, all value should be returned. + $values = $this->mapper->getAttributeValues($variations, 'attribute_color', function (ProductVariationInterface $variation) { + return $variation->getAttributeValueId('attribute_color') == $this->colorAttributes['blue']->id(); + }); + $this->assertTrue(in_array('Blue', $values)); + $this->assertFalse(in_array('Red', $values)); + } + + /** + * Tests the getAttributeInfo method. + */ + public function testGetAttributeInfo() { + $product = $this->generateThreeByTwoScenario(); + $variations = $product->getVariations(); + + // Test from initial variation. + $attribute_info = $this->mapper->getAttributeInfo(reset($variations), $variations); + + $color_attribute_info = $attribute_info['attribute_color']; + $this->assertEquals('select', $color_attribute_info['element_type']); + $this->assertEquals(1, $color_attribute_info['required']); + $this->assertCount(2, $color_attribute_info['values']); + + $size_attribute_info = $attribute_info['attribute_size']; + $this->assertEquals('select', $size_attribute_info['element_type']); + $this->assertEquals(1, $size_attribute_info['required']); + $this->assertCount(3, $size_attribute_info['values']); + + // Test Blue Medium. + $attribute_info = $this->mapper->getAttributeInfo($variations[4], $variations); + + $color_attribute_info = $attribute_info['attribute_color']; + $this->assertEquals('select', $color_attribute_info['element_type']); + $this->assertEquals(1, $color_attribute_info['required']); + $this->assertCount(2, $color_attribute_info['values']); + + $size_attribute_info = $attribute_info['attribute_size']; + $this->assertEquals('select', $size_attribute_info['element_type']); + $this->assertEquals(1, $size_attribute_info['required']); + $this->assertCount(2, $size_attribute_info['values']); + $this->assertFalse(in_array('Large', $size_attribute_info['values'])); + } + + /** + * Tests the getAttributeInfo method. + */ + public function testGetAttributeInfoOptional() { + $product = $this->generateThreeByTwoOptionalScenario(); + $variations = $product->getVariations(); + + // Test from initial variation. + $attribute_info = $this->mapper->getAttributeInfo(reset($variations), $variations); + + $ram_attribute_info = $attribute_info['attribute_ram']; + $this->assertEquals('select', $ram_attribute_info['element_type']); + $this->assertEquals(1, $ram_attribute_info['required']); + $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); + $this->assertCount(2, $ram_attribute_info['values']); + + $disk1_attribute_info = $attribute_info['attribute_disk1']; + $this->assertEquals('select', $disk1_attribute_info['element_type']); + $this->assertEquals(1, $disk1_attribute_info['required']); + $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + $this->assertCount(1, $disk1_attribute_info['values']); + + // @todo The Disk 2 1TB option should not show. Only "none" + // This returns disk2 [ [ '_none' => '', 13 => '1TB' ] ] + // + // The default variation is 8GB x 1TB, which does not have the Disk 2 value + // so it should only return "_none". The Disk 2 option should have only have + // this option is the 16GB RAM option is chosen. + $disk2_attribute_info = $attribute_info['attribute_disk2']; + $this->assertEquals('select', $disk2_attribute_info['element_type']); + $this->assertEquals(1, $disk2_attribute_info['required']); + $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + // There are two values. Since this is optional there is a "_none" option. + $this->assertCount(1, $disk2_attribute_info['values']); + $this->assertTrue(isset($disk2_attribute_info['values']['_none'])); + + // Test from with 16GB which has a variation with option. + $attribute_info = $this->mapper->getAttributeInfo($variations[1], $variations); + + $ram_attribute_info = $attribute_info['attribute_ram']; + $this->assertEquals('select', $ram_attribute_info['element_type']); + $this->assertEquals(1, $ram_attribute_info['required']); + $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); + $this->assertCount(2, $ram_attribute_info['values']); + + $disk1_attribute_info = $attribute_info['attribute_disk1']; + $this->assertEquals('select', $disk1_attribute_info['element_type']); + $this->assertEquals(1, $disk1_attribute_info['required']); + $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + $this->assertCount(1, $disk1_attribute_info['values']); + + $disk2_attribute_info = $attribute_info['attribute_disk2']; + $this->assertEquals('select', $disk2_attribute_info['element_type']); + $this->assertEquals(1, $disk2_attribute_info['required']); + $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + // There are two values. Since this is optional there is a "_none" option. + $this->assertCount(2, $disk2_attribute_info['values']); + $this->assertTrue(isset($disk2_attribute_info['values']['_none'])); + } + + /** + * Tests the getAttributeInfo method. + * + * @group debug + */ + public function testMutuallyExclusiveAttributeMatrixTwoByTwobyTwo() { + $product = Product::create([ + 'type' => 'default', + 'title' => $this->randomMachineName(), + 'stores' => [$this->store], + 'variations' => [], + ]); + $attribute_values_matrix = [ + ['4gb', '2tb', '2tb'], + ['8gb', '1tb', '2tb'], + ['8gb', '2tb', '1tb'], + ]; + $variations = []; + foreach ($attribute_values_matrix as $key => $value) { + $variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + 'attribute_ram' => $this->ramAttributes[$value[0]], + 'attribute_disk1' => $this->disk1Attributes[$value[1]], + 'attribute_disk2' => isset($this->disk2Attributes[$value[2]]) ? $this->disk2Attributes[$value[2]] : NULL, + ]); + $variation->save(); + $variations[] = $variation; + $product->addVariation($variation); + } + $product->save(); + + // Test from initial variation. + $attribute_info = $this->mapper->getAttributeInfo(reset($variations), $variations); + + $ram_attribute_info = $attribute_info['attribute_ram']; + $this->assertEquals('select', $ram_attribute_info['element_type']); + $this->assertEquals(1, $ram_attribute_info['required']); + $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); + $this->assertCount(2, $ram_attribute_info['values']); + + $disk1_attribute_info = $attribute_info['attribute_disk1']; + $this->assertEquals('select', $disk1_attribute_info['element_type']); + $this->assertEquals(1, $disk1_attribute_info['required']); + $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + $this->assertCount(1, $disk1_attribute_info['values']); + + $disk2_attribute_info = $attribute_info['attribute_disk2']; + $this->assertEquals('select', $disk2_attribute_info['element_type']); + $this->assertEquals(1, $disk2_attribute_info['required']); + $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + $this->assertCount(1, $disk2_attribute_info['values']); + $this->assertTrue(in_array('2TB', $disk2_attribute_info['values']), 'Only the one valid Disk 2 option is available.'); + + // Test 8GB 1TB 2TB. + $attribute_info = $this->mapper->getAttributeInfo($variations[1], $variations); + + $ram_attribute_info = $attribute_info['attribute_ram']; + $this->assertEquals('select', $ram_attribute_info['element_type']); + $this->assertEquals(1, $ram_attribute_info['required']); + $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); + $this->assertCount(2, $ram_attribute_info['values']); + + $disk1_attribute_info = $attribute_info['attribute_disk1']; + $this->assertEquals('select', $disk1_attribute_info['element_type']); + $this->assertEquals(1, $disk1_attribute_info['required']); + $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + $this->assertCount(2, $disk1_attribute_info['values']); + + $disk2_attribute_info = $attribute_info['attribute_disk2']; + $this->assertEquals('select', $disk2_attribute_info['element_type']); + $this->assertEquals(1, $disk2_attribute_info['required']); + $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + // There should only be one Disk 2 option, since the other 8GB RAM option + // has a Disk 1 value of 2TB. + $this->assertCount(1, $disk2_attribute_info['values']); + + // Test 8GB 2TB 1TB. + $attribute_info = $this->mapper->getAttributeInfo($variations[2], $variations); + + $ram_attribute_info = $attribute_info['attribute_ram']; + $this->assertEquals('select', $ram_attribute_info['element_type']); + $this->assertEquals(1, $ram_attribute_info['required']); + $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); + $this->assertCount(2, $ram_attribute_info['values']); + + $disk1_attribute_info = $attribute_info['attribute_disk1']; + $this->assertEquals('select', $disk1_attribute_info['element_type']); + $this->assertEquals(1, $disk1_attribute_info['required']); + $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + $this->assertCount(2, $disk1_attribute_info['values']); + + $disk2_attribute_info = $attribute_info['attribute_disk2']; + $this->assertEquals('select', $disk2_attribute_info['element_type']); + $this->assertEquals(1, $disk2_attribute_info['required']); + $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); + // There should only be one Disk 2 option, since the other 8GB RAM option + // has a Disk 1 value of 2TB. + $this->assertCount(1, $disk2_attribute_info['values'], print_r($disk2_attribute_info['values'], TRUE)); + } + + /** + * Generates a three by two scenario. + * + * This generates a product and variations in 3x2 scenario. There are three + * sizes and two colors. Missing one color option. + * + * [ RS, RM, RL ] + * [ BS, BM, X ] + * + * @return \Drupal\commerce_product\Entity\ProductInterface + * The product. + */ + protected function generateThreeByTwoScenario() { + $product = Product::create([ + 'type' => 'default', + 'title' => $this->randomMachineName(), + 'stores' => [$this->store], + 'variations' => [], + ]); + $attribute_values_matrix = [ + ['red', 'small'], + ['red', 'medium'], + ['red', 'large'], + ['blue', 'small'], + ['blue', 'medium'], + ]; + $variations = []; + foreach ($attribute_values_matrix as $key => $value) { + $variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + 'attribute_color' => $this->colorAttributes[$value[0]], + 'attribute_size' => $this->sizeAttributes[$value[1]], + ]); + $variation->save(); + $variations[] = $variation; + $product->addVariation($variation); + } + $product->save(); + + return $product; + } + + /** + * Generates a three by two (optional) secenario. + * + * This generates a product and variations in 3x2 scenario. + * + * https://www.drupal.org/project/commerce/issues/2730643#comment-11216983 + * + * [ 8GBx1TB, X , X ] + * [ X , 16GBx1TB , X ] + * [ X , 16GBx1TBx1TB, X ] + * + * @return \Drupal\commerce_product\Entity\ProductInterface + * The product. + */ + protected function generateThreeByTwoOptionalScenario() { + $product = Product::create([ + 'type' => 'default', + 'title' => $this->randomMachineName(), + 'stores' => [$this->store], + 'variations' => [], + ]); + $attribute_values_matrix = [ + ['8gb', '1tb', ''], + ['16gb', '1tb', ''], + ['16gb', '1tb', '1tb'], + ]; + $variations = []; + foreach ($attribute_values_matrix as $key => $value) { + $variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + 'attribute_ram' => $this->ramAttributes[$value[0]], + 'attribute_disk1' => $this->disk1Attributes[$value[1]], + 'attribute_disk2' => isset($this->disk2Attributes[$value[2]]) ? $this->disk2Attributes[$value[2]] : NULL, + ]); + $variation->save(); + $variations[] = $variation; + $product->addVariation($variation); + } + $product->save(); + + return $product; + } + + /** + * Creates an attribute field and set of attribute values. + * + * @param \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type + * The variation type. + * @param string $name + * The attribute field name. + * @param array $options + * Associative array of key name values. [red => Red]. + * + * @return \Drupal\commerce_product\Entity\ProductAttributeValueInterface[] + * Array of attribute entities. + */ + protected function createAttributeSet(ProductVariationTypeInterface $variation_type, $name, array $options) { + $attribute = ProductAttribute::create([ + 'id' => $name, + 'label' => ucfirst($name), + ]); + $attribute->save(); + $this->attributeFieldManager->createField($attribute, $variation_type->id()); + + $attribute_set = []; + foreach ($options as $key => $value) { + $attribute_set[$key] = $this->createAttributeValue($name, $value); + } + + return $attribute_set; + } + + /** + * Creates an attribute value. + * + * @param string $attribute + * The attribute ID. + * @param string $name + * The attribute value name. + * + * @return \Drupal\commerce_product\Entity\ProductAttributeValueInterface + * The attribute value entity. + */ + protected function createAttributeValue($attribute, $name) { + $attribute_value = ProductAttributeValue::create([ + 'attribute' => $attribute, + 'name' => $name, + ]); + $attribute_value->save(); + + return $attribute_value; + } + +} From 8d3f5b79ed487509fbaf99c198489f70c9999a47 Mon Sep 17 00:00:00 2001 From: git Date: Sat, 17 Mar 2018 13:19:14 +0100 Subject: [PATCH 085/103] Issue #2953865 by alianov: Move PaymentMethodAccessCheck to a different namespace --- modules/payment/commerce_payment.routing.yml | 4 ++-- modules/payment/src/{ => Access}/PaymentMethodAccessCheck.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename modules/payment/src/{ => Access}/PaymentMethodAccessCheck.php (96%) diff --git a/modules/payment/commerce_payment.routing.yml b/modules/payment/commerce_payment.routing.yml index 57327676f5..560d89497f 100644 --- a/modules/payment/commerce_payment.routing.yml +++ b/modules/payment/commerce_payment.routing.yml @@ -49,7 +49,7 @@ entity.commerce_payment_method.add_form: _form: '\Drupal\commerce_payment\Form\PaymentMethodAddForm' _title: 'Add payment method' requirements: - _custom_access: '\Drupal\commerce_payment\PaymentMethodAccessCheck::checkAccess' + _custom_access: '\Drupal\commerce_payment\Access\PaymentMethodAccessCheck::checkAccess' options: parameters: user: @@ -61,7 +61,7 @@ entity.commerce_payment_method.collection: _entity_list: 'commerce_payment_method' _title: 'Payment methods' requirements: - _custom_access: '\Drupal\commerce_payment\PaymentMethodAccessCheck::checkAccess' + _custom_access: '\Drupal\commerce_payment\Access\PaymentMethodAccessCheck::checkAccess' options: parameters: user: diff --git a/modules/payment/src/PaymentMethodAccessCheck.php b/modules/payment/src/Access/PaymentMethodAccessCheck.php similarity index 96% rename from modules/payment/src/PaymentMethodAccessCheck.php rename to modules/payment/src/Access/PaymentMethodAccessCheck.php index bf46d91248..a585c46547 100644 --- a/modules/payment/src/PaymentMethodAccessCheck.php +++ b/modules/payment/src/Access/PaymentMethodAccessCheck.php @@ -1,6 +1,6 @@ Date: Sat, 17 Mar 2018 14:53:00 +0100 Subject: [PATCH 086/103] Revert "Issue #2707721 by drugan, mglaman, skek, AndreaMaggi, ransomweaver: Incorrect display of attribute field values on the Add To Cart form" This reverts commit 0be04680059c31e469792be38fd0909cfd936d30. --- .../AddToCartOptionalAttributeTest.php | 195 ------ modules/product/commerce_product.services.yml | 4 - .../ProductVariationAttributesWidget.php | 94 ++- .../src/ProductVariationAttributeMapper.php | 171 ----- ...oductVariationAttributeMapperInterface.php | 56 -- .../ProductVariationAttributeMapperTest.php | 601 ------------------ 6 files changed, 76 insertions(+), 1045 deletions(-) delete mode 100644 modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php delete mode 100644 modules/product/src/ProductVariationAttributeMapper.php delete mode 100644 modules/product/src/ProductVariationAttributeMapperInterface.php delete mode 100644 modules/product/tests/src/Kernel/ProductVariationAttributeMapperTest.php diff --git a/modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php b/modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php deleted file mode 100644 index b16200d672..0000000000 --- a/modules/cart/tests/src/FunctionalJavascript/AddToCartOptionalAttributeTest.php +++ /dev/null @@ -1,195 +0,0 @@ -variation->bundle()); - - // All attribute-groups have an 'x' value that stand for 'empty'. - $number_attributes = $this->createAttributeSet($variation_type, 'number', [ - 'one' => 'one', - 'two' => 'two', - ]); - $greek_attributes = $this->createAttributeSet($variation_type, 'greek', [ - 'alpha' => 'alpha', - 'omega' => 'omega', - ]); - - $attribute_values_matrix = [ - ['one', 'omega'], - ['two', 'alpha'], - ]; - - // Generate variations from the attribute-matrix. - $variations = []; - foreach ($attribute_values_matrix as $key => $value) { - $variation = $this->createEntity('commerce_product_variation', [ - 'type' => $variation_type->id(), - 'sku' => $this->randomMachineName(), - 'price' => [ - 'number' => 999, - 'currency_code' => 'USD', - ], - 'attribute_number' => $number_attributes[$value[0]], - 'attribute_greek' => $greek_attributes[$value[1]], - ]); - $variations[] = $variation; - } - $product = $this->createEntity('commerce_product', [ - 'type' => 'default', - 'title' => 'OPTIONAL_ATTRIBUTES_TEST', - 'stores' => [$this->store], - 'variations' => $variations, - ]); - - // Helper variables. - $number_selector = 'purchased_entity[0][attributes][attribute_number]'; - $greek_selector = 'purchased_entity[0][attributes][attribute_greek]'; - - - // Initial state: ['one', 'x']. - $this->drupalGet($product->toUrl()); - $this->assertAttributeSelected($number_selector, 'one'); - $this->assertAttributeSelected($greek_selector, 'omega'); - // Expect that 'number' selector can be used. - $this->assertAttributeExists($number_selector, $number_attributes['two']->id()); - // Expect that 'greek' selector cannot be used. - $this->assertAttributeDoesNotExist($greek_selector, $greek_attributes['alpha']->id()); - - - // Use AJAX to change the number-attribute to 'x'. - $this->drupalGet($product->toUrl()); - $this->getSession()->getPage()->selectFieldOption($number_selector, 'two'); - $this->waitForAjaxToFinish(); - // New state: ['x', 'alpha']. - $this->assertAttributeSelected($number_selector, 'two'); - $this->assertAttributeSelected($greek_selector, 'alpha'); - // Expect that 'number' selector can be used. - $this->assertAttributeExists($number_selector, $number_attributes['one']->id()); - // Expect that 'greek' selector cannot be used. - $this->assertAttributeDoesNotExist($greek_selector, $greek_attributes['omega']->id()); - - } - - /** - * Tests add-to-cart form where variation have mutually exclusive attributes. - * - * @group debug - * - * @see https://www.drupal.org/node/2730643 - */ - public function testMutuallyExclusiveAttributeMatrixTwoByTwobyTwo() { - /** @var \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type */ - $variation_type = ProductVariationType::load($this->variation->bundle()); - - // All attribute-groups have an 'x' value that stand for 'empty'. - $number_attributes = $this->createAttributeSet($variation_type, 'number', [ - 'one' => 'one', - 'two' => 'two', - ]); - $greek_attributes = $this->createAttributeSet($variation_type, 'greek', [ - 'alpha' => 'alpha', - 'omega' => 'omega', - ]); - $city_attributes = $this->createAttributeSet($variation_type, 'city', [ - 'milano' => 'milano', - 'pancevo' => 'pancevo', - ]); - - $attribute_values_matrix = [ - ['one', 'omega', 'pancevo'], - ['two', 'alpha', 'pancevo'], - ['two', 'omega', 'milano'], - ]; - - // Generate variations from the attribute-matrix. - $variations = []; - foreach ($attribute_values_matrix as $key => $value) { - $variation = $this->createEntity('commerce_product_variation', [ - 'type' => $variation_type->id(), - 'sku' => $this->randomMachineName(), - 'price' => [ - 'number' => 999, - 'currency_code' => 'USD', - ], - 'attribute_number' => $number_attributes[$value[0]], - 'attribute_greek' => $greek_attributes[$value[1]], - 'attribute_city' => $city_attributes[$value[2]], - ]); - $variations[] = $variation; - } - $product = $this->createEntity('commerce_product', [ - 'type' => 'default', - 'title' => 'OPTIONAL_ATTRIBUTES_TEST', - 'stores' => [$this->store], - 'variations' => $variations, - ]); - - // Helper variables. - $number_selector = 'purchased_entity[0][attributes][attribute_number]'; - $greek_selector = 'purchased_entity[0][attributes][attribute_greek]'; - $city_selector = 'purchased_entity[0][attributes][attribute_city]'; - - - // Initial state: ['one', 'omega', 'pancevo']. - $this->drupalGet($product->toUrl()); - $this->assertAttributeSelected($number_selector, 'one'); - $this->assertAttributeSelected($greek_selector, 'omega'); - $this->assertAttributeSelected($city_selector, 'pancevo'); - - // Use AJAX to change the number-attribute to 'two'. - $this->drupalGet($product->toUrl()); - $this->getSession()->getPage()->selectFieldOption($number_selector, 'two'); - $this->waitForAjaxToFinish(); - $this->saveHtmlOutput(); - - // New state: ['two', 'alpha', 'pancevo']. - // The top level attribute was adjusted, so the options are reset. - $this->assertAttributeSelected($number_selector, 'two'); - $this->assertAttributeSelected($greek_selector, 'alpha'); - $this->assertAttributeSelected($city_selector, 'pancevo'); - - $this->assertAttributeExists($number_selector, $number_attributes['one']->id()); - $this->assertAttributeExists($greek_selector, $greek_attributes['omega']->id()); - $this->assertAttributeExists($city_selector, $city_attributes['pancevo']->id()); - - // Use AJAX to change the number-attribute to 'two'. - $this->drupalGet($product->toUrl()); - $this->getSession()->getPage()->selectFieldOption($greek_selector, 'omega'); - $this->waitForAjaxToFinish(); - $this->saveHtmlOutput(); - - // New state: ['one', 'omega', 'pancevo']. - // The top level attribute was adjusted, so the options are reset. - $this->assertAttributeSelected($number_selector, 'one'); - $this->assertAttributeSelected($greek_selector, 'omega'); - $this->assertAttributeSelected($city_selector, 'pancevo'); - - $this->assertAttributeExists($number_selector, $number_attributes['two']->id()); - $this->assertAttributeDoesNotExist($greek_selector, $greek_attributes['alpha']->id()); - // We should not be able to change the city. - // There is one variation with "one" and "omega", which means there is only - // one city option. - $this->assertAttributeDoesNotExist($city_selector, $city_attributes['milano']->id()); - } - -} diff --git a/modules/product/commerce_product.services.yml b/modules/product/commerce_product.services.yml index f2e999e5f4..619897ee9a 100644 --- a/modules/product/commerce_product.services.yml +++ b/modules/product/commerce_product.services.yml @@ -16,7 +16,3 @@ services: arguments: ['@current_route_match'] tags: - { name: 'context_provider' } - - commerce_product.variation_attribute_value_mapper: - class: Drupal\commerce_product\ProductVariationAttributeMapper - arguments: ['@entity_type.manager', '@entity.repository', '@commerce_product.attribute_field_manager'] diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php index e60b11c21e..8a347cf409 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php @@ -4,7 +4,6 @@ use Drupal\commerce_product\Entity\ProductVariationInterface; use Drupal\commerce_product\ProductAttributeFieldManagerInterface; -use Drupal\commerce_product\ProductVariationAttributeMapperInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityRepositoryInterface; @@ -42,13 +41,6 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem */ protected $attributeStorage; - /** - * The variation attribute value mapper. - * - * @var \Drupal\commerce_product\ProductVariationAttributeMapperInterface - */ - protected $variationAttributeValueMapper; - /** * Constructs a new ProductVariationAttributesWidget object. * @@ -68,15 +60,12 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem * The entity repository. * @param \Drupal\commerce_product\ProductAttributeFieldManagerInterface $attribute_field_manager * The attribute field manager. - * @param \Drupal\commerce_product\ProductVariationAttributeMapperInterface $variation_attribute_value_mapper - * The variation attribute value resolver. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager, ProductVariationAttributeMapperInterface $variation_attribute_value_mapper) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $entity_type_manager, $entity_repository); $this->attributeFieldManager = $attribute_field_manager; $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); - $this->variationAttributeValueMapper = $variation_attribute_value_mapper; } /** @@ -91,8 +80,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['third_party_settings'], $container->get('entity_type.manager'), $container->get('entity.repository'), - $container->get('commerce_product.attribute_field_manager'), - $container->get('commerce_product.variation_attribute_value_mapper') + $container->get('commerce_product.attribute_field_manager') ); } @@ -236,8 +224,24 @@ public function massageFormValues(array $values, array $form, FormStateInterface * The selected variation. */ protected function selectVariationFromUserInput(array $variations, array $user_input) { - $attributes = !empty($user_input['attributes']) ? $user_input['attributes'] : []; - return $this->variationAttributeValueMapper->getVariation($variations, $attributes); + $current_variation = reset($variations); + if (!empty($user_input['attributes'])) { + $attributes = $user_input['attributes']; + foreach ($variations as $variation) { + $match = TRUE; + foreach ($attributes as $field_name => $value) { + if ($variation->getAttributeValueId($field_name) != $value) { + $match = FALSE; + } + } + if ($match) { + $current_variation = $variation; + break; + } + } + } + + return $current_variation; } /** @@ -252,7 +256,48 @@ protected function selectVariationFromUserInput(array $variations, array $user_i * The attribute information, keyed by field name. */ protected function getAttributeInfo(ProductVariationInterface $selected_variation, array $variations) { - return $this->variationAttributeValueMapper->getAttributeInfo($selected_variation, $variations); + $attributes = []; + $field_definitions = $this->attributeFieldManager->getFieldDefinitions($selected_variation->bundle()); + $field_map = $this->attributeFieldManager->getFieldMap($selected_variation->bundle()); + $field_names = array_column($field_map, 'field_name'); + $attribute_ids = array_column($field_map, 'attribute_id'); + $index = 0; + foreach ($field_names as $field_name) { + $field = $field_definitions[$field_name]; + /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */ + $attribute = $this->attributeStorage->load($attribute_ids[$index]); + // Make sure we have translation for attribute. + $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId()); + + $attributes[$field_name] = [ + 'field_name' => $field_name, + 'title' => $attribute->label(), + 'required' => $field->isRequired(), + 'element_type' => $attribute->getElementType(), + ]; + // The first attribute gets all values. Every next attribute gets only + // the values from variations matching the previous attribute value. + // For 'Color' and 'Size' attributes that means getting the colors of all + // variations, but only the sizes of variations with the selected color. + $callback = NULL; + if ($index > 0) { + $previous_field_name = $field_names[$index - 1]; + $previous_field_value = $selected_variation->getAttributeValueId($previous_field_name); + $callback = function ($variation) use ($previous_field_name, $previous_field_value) { + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ + return $variation->getAttributeValueId($previous_field_name) == $previous_field_value; + }; + } + + $attributes[$field_name]['values'] = $this->getAttributeValues($variations, $field_name, $callback); + $index++; + } + // Filter out attributes with no values. + $attributes = array_filter($attributes, function ($attribute) { + return !empty($attribute['values']); + }); + + return $attributes; } /** @@ -269,7 +314,20 @@ protected function getAttributeInfo(ProductVariationInterface $selected_variatio * The attribute values, keyed by attribute ID. */ protected function getAttributeValues(array $variations, $field_name, callable $callback = NULL) { - return $this->variationAttributeValueMapper->getAttributeValues($variations, $field_name, $callback); + $values = []; + foreach ($variations as $variation) { + if (is_null($callback) || call_user_func($callback, $variation)) { + $attribute_value = $variation->getAttributeValue($field_name); + if ($attribute_value) { + $values[$attribute_value->id()] = $attribute_value->label(); + } + else { + $values['_none'] = ''; + } + } + } + + return $values; } } diff --git a/modules/product/src/ProductVariationAttributeMapper.php b/modules/product/src/ProductVariationAttributeMapper.php deleted file mode 100644 index 7822573dd4..0000000000 --- a/modules/product/src/ProductVariationAttributeMapper.php +++ /dev/null @@ -1,171 +0,0 @@ -entityRepository = $entity_repository; - $this->attributeFieldManager = $attribute_field_manager; - $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); - } - - /** - * {@inheritdoc} - */ - public function getVariation(array $variations, array $attribute_values = []) { - $current_variation = reset($variations); - if (empty($attribute_values)) { - return $current_variation; - } - /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ - foreach ($variations as $variation) { - $match = TRUE; - foreach ($attribute_values as $attribute_field_name => $attribute_value) { - // If any one of the attributes do not match, this is not a valid - // candidate for the resolved variation. - if ($variation->getAttributeValueId($attribute_field_name) != $attribute_value) { - $match = FALSE; - } - } - if ($match) { - $current_variation = $variation; - break; - } - } - - return $current_variation; - } - - /** - * Gets the attribute information for the selected product variation. - * - * @param \Drupal\commerce_product\Entity\ProductVariationInterface $selected_variation - * The selected product variation. - * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations - * The available product variations. - * - * @return array[] - * The attribute information, keyed by field name. - */ - public function getAttributeInfo(ProductVariationInterface $selected_variation, array $variations) { - $attributes = []; - $field_definitions = $this->attributeFieldManager->getFieldDefinitions($selected_variation->bundle()); - $field_map = $this->attributeFieldManager->getFieldMap($selected_variation->bundle()); - $field_names = array_column($field_map, 'field_name'); - $attribute_ids = array_column($field_map, 'attribute_id'); - $index = 0; - foreach ($field_names as $field_name) { - $field = $field_definitions[$field_name]; - /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */ - $attribute = $this->attributeStorage->load($attribute_ids[$index]); - // Make sure we have translation for attribute. - $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId()); - - $attributes[$field_name] = [ - 'field_name' => $field_name, - 'title' => $attribute->label(), - 'required' => $field->isRequired(), - 'element_type' => $attribute->getElementType(), - ]; - // The first attribute gets all values. Every next attribute gets only - // the values from variations matching the previous attribute value. - // For 'Color' and 'Size' attributes that means getting the colors of all - // variations, but only the sizes of variations with the selected color. - $callback = NULL; - if ($index > 0) { - $index_limit = $index - 1; - // Get all previous field values. - $previous_variation_field_values = []; - for ($i = 0; $i <= $index_limit; $i++) { - $previous_variation_field_values[$field_names[$i]] = $selected_variation->getAttributeValueId($field_names[$i]); - } - - $callback = function (ProductVariationInterface $variation) use ($previous_variation_field_values) { - $results = []; - foreach ($previous_variation_field_values as $previous_field_name => $previous_field_value) { - $results[] = $variation->getAttributeValueId($previous_field_name) == $previous_field_value; - } - return !in_array(FALSE, $results, TRUE); - }; - } - - $attributes[$field_name]['values'] = $this->getAttributeValues($variations, $field_name, $callback); - $index++; - } - // Filter out attributes with no values. - $attributes = array_filter($attributes, function ($attribute) { - return !empty($attribute['values']); - }); - - return $attributes; - } - - /** - * Gets the attribute values of a given set of variations. - * - * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations - * The variations. - * @param string $field_name - * The field name of the attribute. - * @param callable|null $callback - * An optional callback to use for filtering the list. - * - * @return array[] - * The attribute values, keyed by attribute ID. - */ - public function getAttributeValues(array $variations, $field_name, callable $callback = NULL) { - $values = []; - foreach ($variations as $variation) { - if (is_null($callback) || call_user_func($callback, $variation)) { - $attribute_value = $variation->getAttributeValue($field_name); - if ($attribute_value) { - $values[$attribute_value->id()] = $attribute_value->label(); - } - else { - $values['_none'] = ''; - } - } - } - - return $values; - } - -} diff --git a/modules/product/src/ProductVariationAttributeMapperInterface.php b/modules/product/src/ProductVariationAttributeMapperInterface.php deleted file mode 100644 index 3625c44683..0000000000 --- a/modules/product/src/ProductVariationAttributeMapperInterface.php +++ /dev/null @@ -1,56 +0,0 @@ -installEntitySchema('commerce_product_variation'); - $this->installEntitySchema('commerce_product'); - $this->installEntitySchema('commerce_product_attribute'); - $this->installEntitySchema('commerce_product_attribute_value'); - $this->installConfig(['commerce_product']); - $this->attributeFieldManager = $this->container->get('commerce_product.attribute_field_manager'); - $this->mapper = $this->container->get('commerce_product.variation_attribute_value_mapper'); - - $variation_type = ProductVariationType::load('default'); - - // Create attributes. - $color_attributes = $this->createAttributeSet($variation_type, 'color', [ - 'red' => 'Red', - 'blue' => 'Blue', - ]); - $size_attributes = $this->createAttributeSet($variation_type, 'size', [ - 'small' => 'Small', - 'medium' => 'Medium', - 'large' => 'Large', - ]); - - $ram_attributes = $this->createAttributeSet($variation_type, 'ram', [ - '4gb' => '4GB', - '8gb' => '8GB', - '16gb' => '16GB', - '32gb' => '32GB', - ]); - - $disk1_attributes = $this->createAttributeSet($variation_type, 'disk1', [ - '1tb' => '1TB', - '2tb' => '2TB', - '3tb' => '3TB', - ]); - $disk2_attributes = $this->createAttributeSet($variation_type, 'disk2', [ - '1tb' => '1TB', - '2tb' => '2TB', - '3tb' => '3TB', - ]); - - $this->colorAttributes = $color_attributes; - $this->sizeAttributes = $size_attributes; - - $this->ramAttributes = $ram_attributes; - $this->disk1Attributes = $disk1_attributes; - $this->disk2Attributes = $disk2_attributes; - } - - /** - * Tests that if no attributes are passed, the default variation is returned. - */ - public function testResolveWithNoAttributes() { - $product = $this->generateThreeByTwoScenario(); - $resolved_variation = $this->mapper->getVariation($product->getVariations()); - $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); - - $resolved_variation = $this->mapper->getVariation($product->getVariations(), [ - 'attribute_color' => '', - ]); - $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); - - $resolved_variation = $this->mapper->getVariation($product->getVariations(), [ - 'attribute_color' => '', - 'attribute_size' => '', - ]); - $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); - } - - /** - * Tests that if one attribute passed, the proper variation is returned. - */ - public function testResolveWithWithOneAttribute() { - $product = $this->generateThreeByTwoScenario(); - $variations = $product->getVariations(); - - $resolved_variation = $this->mapper->getVariation($variations, [ - 'attribute_color' => $this->colorAttributes['blue']->id(), - ]); - $this->assertEquals($variations[3]->id(), $resolved_variation->id()); - - $resolved_variation = $this->mapper->getVariation($variations, [ - 'attribute_size' => $this->sizeAttributes['large']->id(), - ]); - $this->assertEquals($variations[2]->id(), $resolved_variation->id()); - } - - /** - * Tests that if two attributes are passed, the proper variation is returned. - */ - public function testResolveWithWithTwoAttributes() { - $product = $this->generateThreeByTwoScenario(); - $variations = $product->getVariations(); - - $resolved_variation = $this->mapper->getVariation($variations, [ - 'attribute_color' => $this->colorAttributes['red']->id(), - 'attribute_size' => $this->sizeAttributes['large']->id(), - ]); - $this->assertEquals($variations[2]->id(), $resolved_variation->id()); - - $resolved_variation = $this->mapper->getVariation($variations, [ - 'attribute_color' => $this->colorAttributes['blue']->id(), - 'attribute_size' => $this->sizeAttributes['large']->id(), - ]); - // An invalid arrangement was passed, so the default variation is resolved. - $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); - - $resolved_variation = $this->mapper->getVariation($variations, [ - 'attribute_color' => '', - 'attribute_size' => $this->sizeAttributes['large']->id(), - ]); - // A missing attribute was passed for first option. - $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); - - $resolved_variation = $this->mapper->getVariation($variations, [ - 'attribute_color' => $this->colorAttributes['blue']->id(), - 'attribute_size' => $this->sizeAttributes['small']->id(), - ]); - // An empty second option defaults to first variation option. - $this->assertEquals($variations[3]->id(), $resolved_variation->id()); - } - - /** - * Tests optional attributes. - */ - public function testResolveWithOptionalAttributes() { - $product = $this->generateThreeByTwoOptionalScenario(); - $variations = $product->getVariations(); - - $resolved_variation = $this->mapper->getVariation($variations, [ - 'attribute_ram' => $this->ramAttributes['16gb']->id(), - ]); - $this->assertEquals($variations[1]->id(), $resolved_variation->id()); - - $resolved_variation = $this->mapper->getVariation($variations, [ - 'attribute_ram' => $this->ramAttributes['16gb']->id(), - 'attribute_disk1' => $this->disk1Attributes['1tb']->id(), - 'attribute_disk2' => $this->disk2Attributes['1tb']->id(), - ]); - $this->assertEquals($variations[2]->id(), $resolved_variation->id()); - - $resolved_variation = $this->mapper->getVariation($variations, [ - 'attribute_ram' => $this->ramAttributes['16gb']->id(), - 'attribute_disk1' => $this->disk1Attributes['1tb']->id(), - 'attribute_disk2' => $this->disk2Attributes['2tb']->id(), - ]); - $this->assertEquals($product->getDefaultVariation()->id(), $resolved_variation->id()); - } - - /** - * Tests the getAttributeValues method. - */ - public function testGetAttributeValues() { - $product = $this->generateThreeByTwoScenario(); - $variations = $product->getVariations(); - - // With no callback, all value should be returned. - $values = $this->mapper->getAttributeValues($variations, 'attribute_color'); - foreach ($this->colorAttributes as $color_attribute) { - $this->assertTrue(in_array($color_attribute->label(), $values)); - } - - // With no callback, all value should be returned. - $values = $this->mapper->getAttributeValues($variations, 'attribute_color', function (ProductVariationInterface $variation) { - return $variation->getAttributeValueId('attribute_color') == $this->colorAttributes['blue']->id(); - }); - $this->assertTrue(in_array('Blue', $values)); - $this->assertFalse(in_array('Red', $values)); - } - - /** - * Tests the getAttributeInfo method. - */ - public function testGetAttributeInfo() { - $product = $this->generateThreeByTwoScenario(); - $variations = $product->getVariations(); - - // Test from initial variation. - $attribute_info = $this->mapper->getAttributeInfo(reset($variations), $variations); - - $color_attribute_info = $attribute_info['attribute_color']; - $this->assertEquals('select', $color_attribute_info['element_type']); - $this->assertEquals(1, $color_attribute_info['required']); - $this->assertCount(2, $color_attribute_info['values']); - - $size_attribute_info = $attribute_info['attribute_size']; - $this->assertEquals('select', $size_attribute_info['element_type']); - $this->assertEquals(1, $size_attribute_info['required']); - $this->assertCount(3, $size_attribute_info['values']); - - // Test Blue Medium. - $attribute_info = $this->mapper->getAttributeInfo($variations[4], $variations); - - $color_attribute_info = $attribute_info['attribute_color']; - $this->assertEquals('select', $color_attribute_info['element_type']); - $this->assertEquals(1, $color_attribute_info['required']); - $this->assertCount(2, $color_attribute_info['values']); - - $size_attribute_info = $attribute_info['attribute_size']; - $this->assertEquals('select', $size_attribute_info['element_type']); - $this->assertEquals(1, $size_attribute_info['required']); - $this->assertCount(2, $size_attribute_info['values']); - $this->assertFalse(in_array('Large', $size_attribute_info['values'])); - } - - /** - * Tests the getAttributeInfo method. - */ - public function testGetAttributeInfoOptional() { - $product = $this->generateThreeByTwoOptionalScenario(); - $variations = $product->getVariations(); - - // Test from initial variation. - $attribute_info = $this->mapper->getAttributeInfo(reset($variations), $variations); - - $ram_attribute_info = $attribute_info['attribute_ram']; - $this->assertEquals('select', $ram_attribute_info['element_type']); - $this->assertEquals(1, $ram_attribute_info['required']); - $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); - $this->assertCount(2, $ram_attribute_info['values']); - - $disk1_attribute_info = $attribute_info['attribute_disk1']; - $this->assertEquals('select', $disk1_attribute_info['element_type']); - $this->assertEquals(1, $disk1_attribute_info['required']); - $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - $this->assertCount(1, $disk1_attribute_info['values']); - - // @todo The Disk 2 1TB option should not show. Only "none" - // This returns disk2 [ [ '_none' => '', 13 => '1TB' ] ] - // - // The default variation is 8GB x 1TB, which does not have the Disk 2 value - // so it should only return "_none". The Disk 2 option should have only have - // this option is the 16GB RAM option is chosen. - $disk2_attribute_info = $attribute_info['attribute_disk2']; - $this->assertEquals('select', $disk2_attribute_info['element_type']); - $this->assertEquals(1, $disk2_attribute_info['required']); - $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - // There are two values. Since this is optional there is a "_none" option. - $this->assertCount(1, $disk2_attribute_info['values']); - $this->assertTrue(isset($disk2_attribute_info['values']['_none'])); - - // Test from with 16GB which has a variation with option. - $attribute_info = $this->mapper->getAttributeInfo($variations[1], $variations); - - $ram_attribute_info = $attribute_info['attribute_ram']; - $this->assertEquals('select', $ram_attribute_info['element_type']); - $this->assertEquals(1, $ram_attribute_info['required']); - $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); - $this->assertCount(2, $ram_attribute_info['values']); - - $disk1_attribute_info = $attribute_info['attribute_disk1']; - $this->assertEquals('select', $disk1_attribute_info['element_type']); - $this->assertEquals(1, $disk1_attribute_info['required']); - $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - $this->assertCount(1, $disk1_attribute_info['values']); - - $disk2_attribute_info = $attribute_info['attribute_disk2']; - $this->assertEquals('select', $disk2_attribute_info['element_type']); - $this->assertEquals(1, $disk2_attribute_info['required']); - $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - // There are two values. Since this is optional there is a "_none" option. - $this->assertCount(2, $disk2_attribute_info['values']); - $this->assertTrue(isset($disk2_attribute_info['values']['_none'])); - } - - /** - * Tests the getAttributeInfo method. - * - * @group debug - */ - public function testMutuallyExclusiveAttributeMatrixTwoByTwobyTwo() { - $product = Product::create([ - 'type' => 'default', - 'title' => $this->randomMachineName(), - 'stores' => [$this->store], - 'variations' => [], - ]); - $attribute_values_matrix = [ - ['4gb', '2tb', '2tb'], - ['8gb', '1tb', '2tb'], - ['8gb', '2tb', '1tb'], - ]; - $variations = []; - foreach ($attribute_values_matrix as $key => $value) { - $variation = ProductVariation::create([ - 'type' => 'default', - 'sku' => $this->randomMachineName(), - 'price' => [ - 'number' => 999, - 'currency_code' => 'USD', - ], - 'attribute_ram' => $this->ramAttributes[$value[0]], - 'attribute_disk1' => $this->disk1Attributes[$value[1]], - 'attribute_disk2' => isset($this->disk2Attributes[$value[2]]) ? $this->disk2Attributes[$value[2]] : NULL, - ]); - $variation->save(); - $variations[] = $variation; - $product->addVariation($variation); - } - $product->save(); - - // Test from initial variation. - $attribute_info = $this->mapper->getAttributeInfo(reset($variations), $variations); - - $ram_attribute_info = $attribute_info['attribute_ram']; - $this->assertEquals('select', $ram_attribute_info['element_type']); - $this->assertEquals(1, $ram_attribute_info['required']); - $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); - $this->assertCount(2, $ram_attribute_info['values']); - - $disk1_attribute_info = $attribute_info['attribute_disk1']; - $this->assertEquals('select', $disk1_attribute_info['element_type']); - $this->assertEquals(1, $disk1_attribute_info['required']); - $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - $this->assertCount(1, $disk1_attribute_info['values']); - - $disk2_attribute_info = $attribute_info['attribute_disk2']; - $this->assertEquals('select', $disk2_attribute_info['element_type']); - $this->assertEquals(1, $disk2_attribute_info['required']); - $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - $this->assertCount(1, $disk2_attribute_info['values']); - $this->assertTrue(in_array('2TB', $disk2_attribute_info['values']), 'Only the one valid Disk 2 option is available.'); - - // Test 8GB 1TB 2TB. - $attribute_info = $this->mapper->getAttributeInfo($variations[1], $variations); - - $ram_attribute_info = $attribute_info['attribute_ram']; - $this->assertEquals('select', $ram_attribute_info['element_type']); - $this->assertEquals(1, $ram_attribute_info['required']); - $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); - $this->assertCount(2, $ram_attribute_info['values']); - - $disk1_attribute_info = $attribute_info['attribute_disk1']; - $this->assertEquals('select', $disk1_attribute_info['element_type']); - $this->assertEquals(1, $disk1_attribute_info['required']); - $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - $this->assertCount(2, $disk1_attribute_info['values']); - - $disk2_attribute_info = $attribute_info['attribute_disk2']; - $this->assertEquals('select', $disk2_attribute_info['element_type']); - $this->assertEquals(1, $disk2_attribute_info['required']); - $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - // There should only be one Disk 2 option, since the other 8GB RAM option - // has a Disk 1 value of 2TB. - $this->assertCount(1, $disk2_attribute_info['values']); - - // Test 8GB 2TB 1TB. - $attribute_info = $this->mapper->getAttributeInfo($variations[2], $variations); - - $ram_attribute_info = $attribute_info['attribute_ram']; - $this->assertEquals('select', $ram_attribute_info['element_type']); - $this->assertEquals(1, $ram_attribute_info['required']); - $this->assertNotCount(4, $ram_attribute_info['values'], 'Out of the four available attribute values, only the two used are returned.'); - $this->assertCount(2, $ram_attribute_info['values']); - - $disk1_attribute_info = $attribute_info['attribute_disk1']; - $this->assertEquals('select', $disk1_attribute_info['element_type']); - $this->assertEquals(1, $disk1_attribute_info['required']); - $this->assertNotCount(3, $disk1_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - $this->assertCount(2, $disk1_attribute_info['values']); - - $disk2_attribute_info = $attribute_info['attribute_disk2']; - $this->assertEquals('select', $disk2_attribute_info['element_type']); - $this->assertEquals(1, $disk2_attribute_info['required']); - $this->assertNotCount(3, $disk2_attribute_info['values'], 'Out of the three available attribute values, only the one used is returned.'); - // There should only be one Disk 2 option, since the other 8GB RAM option - // has a Disk 1 value of 2TB. - $this->assertCount(1, $disk2_attribute_info['values'], print_r($disk2_attribute_info['values'], TRUE)); - } - - /** - * Generates a three by two scenario. - * - * This generates a product and variations in 3x2 scenario. There are three - * sizes and two colors. Missing one color option. - * - * [ RS, RM, RL ] - * [ BS, BM, X ] - * - * @return \Drupal\commerce_product\Entity\ProductInterface - * The product. - */ - protected function generateThreeByTwoScenario() { - $product = Product::create([ - 'type' => 'default', - 'title' => $this->randomMachineName(), - 'stores' => [$this->store], - 'variations' => [], - ]); - $attribute_values_matrix = [ - ['red', 'small'], - ['red', 'medium'], - ['red', 'large'], - ['blue', 'small'], - ['blue', 'medium'], - ]; - $variations = []; - foreach ($attribute_values_matrix as $key => $value) { - $variation = ProductVariation::create([ - 'type' => 'default', - 'sku' => $this->randomMachineName(), - 'price' => [ - 'number' => 999, - 'currency_code' => 'USD', - ], - 'attribute_color' => $this->colorAttributes[$value[0]], - 'attribute_size' => $this->sizeAttributes[$value[1]], - ]); - $variation->save(); - $variations[] = $variation; - $product->addVariation($variation); - } - $product->save(); - - return $product; - } - - /** - * Generates a three by two (optional) secenario. - * - * This generates a product and variations in 3x2 scenario. - * - * https://www.drupal.org/project/commerce/issues/2730643#comment-11216983 - * - * [ 8GBx1TB, X , X ] - * [ X , 16GBx1TB , X ] - * [ X , 16GBx1TBx1TB, X ] - * - * @return \Drupal\commerce_product\Entity\ProductInterface - * The product. - */ - protected function generateThreeByTwoOptionalScenario() { - $product = Product::create([ - 'type' => 'default', - 'title' => $this->randomMachineName(), - 'stores' => [$this->store], - 'variations' => [], - ]); - $attribute_values_matrix = [ - ['8gb', '1tb', ''], - ['16gb', '1tb', ''], - ['16gb', '1tb', '1tb'], - ]; - $variations = []; - foreach ($attribute_values_matrix as $key => $value) { - $variation = ProductVariation::create([ - 'type' => 'default', - 'sku' => $this->randomMachineName(), - 'price' => [ - 'number' => 999, - 'currency_code' => 'USD', - ], - 'attribute_ram' => $this->ramAttributes[$value[0]], - 'attribute_disk1' => $this->disk1Attributes[$value[1]], - 'attribute_disk2' => isset($this->disk2Attributes[$value[2]]) ? $this->disk2Attributes[$value[2]] : NULL, - ]); - $variation->save(); - $variations[] = $variation; - $product->addVariation($variation); - } - $product->save(); - - return $product; - } - - /** - * Creates an attribute field and set of attribute values. - * - * @param \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type - * The variation type. - * @param string $name - * The attribute field name. - * @param array $options - * Associative array of key name values. [red => Red]. - * - * @return \Drupal\commerce_product\Entity\ProductAttributeValueInterface[] - * Array of attribute entities. - */ - protected function createAttributeSet(ProductVariationTypeInterface $variation_type, $name, array $options) { - $attribute = ProductAttribute::create([ - 'id' => $name, - 'label' => ucfirst($name), - ]); - $attribute->save(); - $this->attributeFieldManager->createField($attribute, $variation_type->id()); - - $attribute_set = []; - foreach ($options as $key => $value) { - $attribute_set[$key] = $this->createAttributeValue($name, $value); - } - - return $attribute_set; - } - - /** - * Creates an attribute value. - * - * @param string $attribute - * The attribute ID. - * @param string $name - * The attribute value name. - * - * @return \Drupal\commerce_product\Entity\ProductAttributeValueInterface - * The attribute value entity. - */ - protected function createAttributeValue($attribute, $name) { - $attribute_value = ProductAttributeValue::create([ - 'attribute' => $attribute, - 'name' => $name, - ]); - $attribute_value->save(); - - return $attribute_value; - } - -} From 6353a0d3765d71834bc26fb8b972a21fcaf2f9dc Mon Sep 17 00:00:00 2001 From: agoradesign Date: Sat, 17 Mar 2018 16:37:43 +0100 Subject: [PATCH 087/103] Issue #2953599 by agoradesign: Wrong return type in DocBlock for getPluginConfiguration() of payment gateways and tax types --- modules/payment/src/Entity/PaymentGatewayInterface.php | 2 +- modules/tax/src/Entity/TaxTypeInterface.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/payment/src/Entity/PaymentGatewayInterface.php b/modules/payment/src/Entity/PaymentGatewayInterface.php index 6a20e95f1b..cfc4e58201 100644 --- a/modules/payment/src/Entity/PaymentGatewayInterface.php +++ b/modules/payment/src/Entity/PaymentGatewayInterface.php @@ -60,7 +60,7 @@ public function setPluginId($plugin_id); /** * Gets the payment gateway plugin configuration. * - * @return string + * @return array * The payment gateway plugin configuration. */ public function getPluginConfiguration(); diff --git a/modules/tax/src/Entity/TaxTypeInterface.php b/modules/tax/src/Entity/TaxTypeInterface.php index 9438ca7e09..a80f8b5bc6 100644 --- a/modules/tax/src/Entity/TaxTypeInterface.php +++ b/modules/tax/src/Entity/TaxTypeInterface.php @@ -41,7 +41,7 @@ public function setPluginId($plugin_id); /** * Gets the tax type plugin configuration. * - * @return string + * @return array * The tax type plugin configuration. */ public function getPluginConfiguration(); From 05042bc6ea24f3902f0f21dc7b9221da6acbc525 Mon Sep 17 00:00:00 2001 From: git Date: Sat, 17 Mar 2018 16:40:08 +0100 Subject: [PATCH 088/103] Issue #2949406 by alianov, bojanz, Kate Heinlein: Make CountryRepository exchangeable in CreateStoreCommand class --- modules/store/src/Command/CreateStoreCommand.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/store/src/Command/CreateStoreCommand.php b/modules/store/src/Command/CreateStoreCommand.php index e5e09054d1..982c0d32b5 100644 --- a/modules/store/src/Command/CreateStoreCommand.php +++ b/modules/store/src/Command/CreateStoreCommand.php @@ -13,7 +13,7 @@ use Drupal\Console\Annotations\DrupalCommand; use Drupal\commerce_price\CurrencyImporter; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\address\Repository\CountryRepository; +use CommerceGuys\Intl\Country\CountryRepositoryInterface; use Drupal\Core\Render\MetadataBubblingUrlGenerator; use Egulias\EmailValidator\EmailValidator; use Symfony\Component\Console\Question\Question; @@ -50,7 +50,7 @@ class CreateStoreCommand extends Command { /** * The country repository. * - * @var \Drupal\address\Repository\CountryRepository + * @var \CommerceGuys\Intl\Country\CountryRepositoryInterface */ protected $countryRepository; @@ -75,17 +75,17 @@ class CreateStoreCommand extends Command { * The currency importer. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. - * @param \Drupal\address\Repository\CountryRepository $address_country_repository + * @param \CommerceGuys\Intl\Country\CountryRepositoryInterface $country_repository * The country repository. * @param \Drupal\Core\Render\MetadataBubblingUrlGenerator $url_generator * The URL generator. * @param \Egulias\EmailValidator\EmailValidator $email_validator * The email validator. */ - public function __construct(CurrencyImporter $commerce_price_currency_importer, EntityTypeManagerInterface $entity_type_manager, CountryRepository $address_country_repository, MetadataBubblingUrlGenerator $url_generator, EmailValidator $email_validator) { + public function __construct(CurrencyImporter $commerce_price_currency_importer, EntityTypeManagerInterface $entity_type_manager, CountryRepositoryInterface $country_repository, MetadataBubblingUrlGenerator $url_generator, EmailValidator $email_validator) { $this->currencyImporter = $commerce_price_currency_importer; $this->entityTypeManager = $entity_type_manager; - $this->countryRepository = $address_country_repository; + $this->countryRepository = $country_repository; $this->urlGenerator = $url_generator; $this->emailValidator = $email_validator; parent::__construct(); From 8d7728d34525856a08600fa90070328e2f91255b Mon Sep 17 00:00:00 2001 From: quietone Date: Tue, 20 Mar 2018 18:02:57 +0100 Subject: [PATCH 089/103] Issue #2953843 by quietone: Make product body field translatable --- modules/product/commerce_product.module | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/product/commerce_product.module b/modules/product/commerce_product.module index 44dc83d531..fcc7ddbc4b 100644 --- a/modules/product/commerce_product.module +++ b/modules/product/commerce_product.module @@ -221,6 +221,7 @@ function commerce_product_add_body_field(ProductTypeInterface $product_type, $la ->setTargetBundle($product_type->id()) ->setName('body') ->setLabel($label) + ->setTranslatable(TRUE) ->setSetting('display_summary', FALSE) ->setDisplayOptions('form', [ 'type' => 'text_textarea_with_summary', From e27d48db104ffe277b150286056042f2511944f8 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Wed, 21 Mar 2018 18:31:19 +0100 Subject: [PATCH 090/103] Issue #2861983 by trigdog, joe1987uk, drugan, shabana.navas, sorabh.v6, bojanz: Improve cart behavior when the quantity is 0 or an empty string --- .../src/Plugin/views/field/EditQuantity.php | 25 +++++++++++++++---- .../cart/tests/src/Functional/CartTest.php | 12 ++++++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/modules/cart/src/Plugin/views/field/EditQuantity.php b/modules/cart/src/Plugin/views/field/EditQuantity.php index f3f11d03dc..3c35c0b986 100644 --- a/modules/cart/src/Plugin/views/field/EditQuantity.php +++ b/modules/cart/src/Plugin/views/field/EditQuantity.php @@ -133,6 +133,7 @@ public function viewsForm(array &$form, FormStateInterface $form_state) { '#min' => 0, '#max' => 9999, '#step' => $step, + '#required' => TRUE, ]; } // Replace the form submit button label. @@ -150,15 +151,29 @@ public function viewsForm(array &$form, FormStateInterface $form_state) { public function viewsFormSubmit(array &$form, FormStateInterface $form_state) { $quantities = $form_state->getValue($this->options['id'], []); foreach ($quantities as $row_index => $quantity) { + if (!is_numeric($quantity) || $quantity < 0) { + // The input might be invalid if the #required or #min attributes + // were removed by an alter hook. + continue; + } /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */ $order_item = $this->getEntity($this->view->result[$row_index]); - if ($order_item->getQuantity() != $quantity) { + if ($order_item->getQuantity() == $quantity) { + // The quantity hasn't changed. + continue; + } + + if ($quantity > 0) { $order_item->setQuantity($quantity); - $order = $order_item->getOrder(); - $this->cartManager->updateOrderItem($order, $order_item, FALSE); - // Tells commerce_cart_order_item_views_form_submit() to save the order. - $form_state->set('quantity_updated', TRUE); + $this->cartManager->updateOrderItem($order_item->getOrder(), $order_item, FALSE); } + else { + // Treat quantity "0" as a request for deletion. + $this->cartManager->removeOrderItem($order_item->getOrder(), $order_item, FALSE); + } + + // Tells commerce_cart_order_item_views_form_submit() to save the order. + $form_state->set('quantity_updated', TRUE); } } diff --git a/modules/cart/tests/src/Functional/CartTest.php b/modules/cart/tests/src/Functional/CartTest.php index 2222c78366..d87aab8618 100644 --- a/modules/cart/tests/src/Functional/CartTest.php +++ b/modules/cart/tests/src/Functional/CartTest.php @@ -114,10 +114,20 @@ public function testCartPage() { $this->assertSession()->elementTextContains('css', '.order-total-line', 'Total'); $this->assertSession()->pageTextContains('$3,048.00'); + // Confirm that setting the quantity to 0 removes an item. + $values = [ + 'edit_quantity[0]' => 0, + 'edit_quantity[1]' => 3, + ]; + $this->submitForm($values, t('Update cart')); + $this->assertSession()->pageTextContains(t('Your shopping cart has been updated.')); + $this->assertSession()->fieldExists('edit-edit-quantity-0'); + $this->assertSession()->fieldNotExists('edit-edit-quantity-1'); + $this->assertSession()->pageTextContains('$1,050.00'); + // Confirm the presence and functioning of the Remove button. $this->assertSession()->buttonExists('Remove'); $this->submitForm([], t('Remove')); - $this->submitForm([], t('Remove')); $this->assertSession()->pageTextContains(t('Your shopping cart is empty.')); // Test that cart is denied for user without permission. From 8062c3f7305c28f5142df0db74ed53552d255ac3 Mon Sep 17 00:00:00 2001 From: karlos007 Date: Wed, 21 Mar 2018 19:22:29 +0100 Subject: [PATCH 091/103] Issue #2954572 by karlos007: Update composer.json to indicate the minimum required Entity API version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0b2014c651..65f7eeb79e 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "require": { "drupal/core": "~8.5", "drupal/address": "~1.0", - "drupal/entity": "~1.0", + "drupal/entity": "^1.0-beta3", "drupal/entity_reference_revisions": "~1.0", "drupal/inline_entity_form": "~1.0", "drupal/profile": "~1.0", From ee6aa054b9dd8c0b92230a209b82834aba940a99 Mon Sep 17 00:00:00 2001 From: agoradesign Date: Wed, 21 Mar 2018 19:30:43 +0100 Subject: [PATCH 092/103] Issue #2953937 by agoradesign: Clean up PriceCalculatedFormatter variables --- .../Field/FieldFormatter/PriceCalculatedFormatter.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php b/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php index f3b061f6ba..7096142f30 100644 --- a/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php +++ b/modules/order/src/Plugin/Field/FieldFormatter/PriceCalculatedFormatter.php @@ -33,13 +33,6 @@ class PriceCalculatedFormatter extends PriceDefaultFormatter implements Containe */ protected $adjustmentTypeManager; - /** - * The currency storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $currencyStorage; - /** * The current user. * @@ -95,7 +88,6 @@ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInter parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $entity_type_manager, $number_formatter_factory); $this->adjustmentTypeManager = $adjustment_type_manager; - $this->currencyStorage = $entity_type_manager->getStorage('commerce_currency'); $this->currentStore = $current_store; $this->currentUser = $current_user; $this->priceCalculator = $price_calculator; From fb6167f4ff294fc0c89961f3699267ffbdf35c8e Mon Sep 17 00:00:00 2001 From: abramm Date: Wed, 21 Mar 2018 19:50:50 +0100 Subject: [PATCH 093/103] Issue #2954205 by abramm: Missing $order_item->getTotalPrice() return value check --- modules/order/src/Entity/Order.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/order/src/Entity/Order.php b/modules/order/src/Entity/Order.php index e00d05c4ee..4fdc00f94d 100644 --- a/modules/order/src/Entity/Order.php +++ b/modules/order/src/Entity/Order.php @@ -342,8 +342,9 @@ public function getSubtotalPrice() { $subtotal_price = NULL; if ($this->hasItems()) { foreach ($this->getItems() as $order_item) { - $order_item_total = $order_item->getTotalPrice(); - $subtotal_price = $subtotal_price ? $subtotal_price->add($order_item_total) : $order_item_total; + if ($order_item_total = $order_item->getTotalPrice()) { + $subtotal_price = $subtotal_price ? $subtotal_price->add($order_item_total) : $order_item_total; + } } } return $subtotal_price; @@ -357,8 +358,9 @@ public function recalculateTotalPrice() { $total_price = NULL; if ($this->hasItems()) { foreach ($this->getItems() as $order_item) { - $order_item_total = $order_item->getTotalPrice(); - $total_price = $total_price ? $total_price->add($order_item_total) : $order_item_total; + if ($order_item_total = $order_item->getTotalPrice()) { + $total_price = $total_price ? $total_price->add($order_item_total) : $order_item_total; + } } $adjustments = $this->collectAdjustments(); if ($adjustments) { From fd9826aec061744a02ff21c3d7b75a3788de14f1 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Thu, 22 Mar 2018 10:36:58 +0100 Subject: [PATCH 094/103] Issue #2944402 by bojanz, agoradesign, tce: Cart totals are not updated when clicking the "Checkout" button --- modules/cart/commerce_cart.module | 29 ---------- .../src/Plugin/views/area/EmptyCartButton.php | 2 +- .../src/Plugin/views/field/EditQuantity.php | 53 ++++++++++++++++--- .../cart/tests/src/Functional/CartTest.php | 37 ++++++++++--- modules/checkout/commerce_checkout.module | 2 + 5 files changed, 79 insertions(+), 44 deletions(-) diff --git a/modules/cart/commerce_cart.module b/modules/cart/commerce_cart.module index a85615566f..af08a096a5 100644 --- a/modules/cart/commerce_cart.module +++ b/modules/cart/commerce_cart.module @@ -10,7 +10,6 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AnonymousUserSession; -use Drupal\views\Form\ViewsForm; /** * Implements hook_menu_links_discovered_alter(). @@ -215,34 +214,6 @@ function commerce_cart_form_commerce_order_type_form_alter(array &$form, FormSta $form['actions']['submit']['#submit'][] = 'commerce_cart_order_type_form_submit'; } -/** - * Implements hook_form_alter(). - */ -function commerce_cart_form_alter(&$form, FormStateInterface $form_state, $form_id) { - if ($form_state->getFormObject() instanceof ViewsForm) { - /** @var \Drupal\views\ViewExecutable $view */ - $view = reset($form_state->getBuildInfo()['args']); - if ($view->storage->get('tag') == 'commerce_cart_form' && !empty($view->result)) { - $form['actions']['submit']['#submit'] = array_merge($form['#submit'], ['commerce_cart_order_item_views_form_submit']); - $form['actions']['submit']['#order_id'] = $view->argument['order_id']->value[0]; - } - } -} - -/** - * Form submission handler for 'views_form_commerce_cart_form_'. - */ -function commerce_cart_order_item_views_form_submit($form, FormStateInterface $form_state) { - if (!$form_state->has('quantity_updated')) { - return; - } - $order_id = $form_state->getTriggeringElement()['#order_id']; - /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ - $order = \Drupal::entityTypeManager()->getStorage('commerce_order')->load($order_id); - $order->save(); - \Drupal::messenger()->addMessage(t('Your shopping cart has been updated.')); -} - /** * Form submission handler for 'commerce_order_type_form'. */ diff --git a/modules/cart/src/Plugin/views/area/EmptyCartButton.php b/modules/cart/src/Plugin/views/area/EmptyCartButton.php index beec14540d..39aea69fda 100644 --- a/modules/cart/src/Plugin/views/area/EmptyCartButton.php +++ b/modules/cart/src/Plugin/views/area/EmptyCartButton.php @@ -24,7 +24,7 @@ class EmptyCartButton extends AreaPluginBase { protected $cartManager; /** - * The entity manager. + * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ diff --git a/modules/cart/src/Plugin/views/field/EditQuantity.php b/modules/cart/src/Plugin/views/field/EditQuantity.php index 3c35c0b986..fcd0e962ef 100644 --- a/modules/cart/src/Plugin/views/field/EditQuantity.php +++ b/modules/cart/src/Plugin/views/field/EditQuantity.php @@ -3,7 +3,9 @@ namespace Drupal\commerce_cart\Plugin\views\field; use Drupal\commerce_cart\CartManagerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\views\Plugin\views\field\FieldPluginBase; use Drupal\views\Plugin\views\field\UncacheableFieldHandlerTrait; use Drupal\views\ResultRow; @@ -25,6 +27,20 @@ class EditQuantity extends FieldPluginBase { */ protected $cartManager; + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + /** * Constructs a new EditQuantity object. * @@ -36,11 +52,17 @@ class EditQuantity extends FieldPluginBase { * The plugin implementation definition. * @param \Drupal\commerce_cart\CartManagerInterface $cart_manager * The cart manager. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, CartManagerInterface $cart_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, CartManagerInterface $cart_manager, EntityTypeManagerInterface $entity_type_manager, MessengerInterface $messenger) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->cartManager = $cart_manager; + $this->entityTypeManager = $entity_type_manager; + $this->messenger = $messenger; } /** @@ -51,7 +73,9 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('commerce_cart.cart_manager') + $container->get('commerce_cart.cart_manager'), + $container->get('entity_type.manager'), + $container->get('messenger') ); } @@ -136,6 +160,8 @@ public function viewsForm(array &$form, FormStateInterface $form_state) { '#required' => TRUE, ]; } + $form['actions']['submit']['#update_cart'] = TRUE; + $form['actions']['submit']['#show_update_message'] = TRUE; // Replace the form submit button label. $form['actions']['submit']['#value'] = $this->t('Update cart'); } @@ -149,7 +175,17 @@ public function viewsForm(array &$form, FormStateInterface $form_state) { * The current state of the form. */ public function viewsFormSubmit(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + if (empty($triggering_element['#update_cart'])) { + // Don't run when the "Remove" or "Empty cart" buttons are pressed. + return; + } + + $order_storage = $this->entityTypeManager->getStorage('commerce_order'); + /** @var \Drupal\commerce_order\Entity\OrderInterface $cart */ + $cart = $order_storage->load($this->view->argument['order_id']->getValue()); $quantities = $form_state->getValue($this->options['id'], []); + $save_cart = FALSE; foreach ($quantities as $row_index => $quantity) { if (!is_numeric($quantity) || $quantity < 0) { // The input might be invalid if the #required or #min attributes @@ -165,15 +201,20 @@ public function viewsFormSubmit(array &$form, FormStateInterface $form_state) { if ($quantity > 0) { $order_item->setQuantity($quantity); - $this->cartManager->updateOrderItem($order_item->getOrder(), $order_item, FALSE); + $this->cartManager->updateOrderItem($cart, $order_item, FALSE); } else { // Treat quantity "0" as a request for deletion. - $this->cartManager->removeOrderItem($order_item->getOrder(), $order_item, FALSE); + $this->cartManager->removeOrderItem($cart, $order_item, FALSE); } + $save_cart = TRUE; + } - // Tells commerce_cart_order_item_views_form_submit() to save the order. - $form_state->set('quantity_updated', TRUE); + if ($save_cart) { + $cart->save(); + if (!empty($triggering_element['#show_update_message'])) { + $this->messenger->addMessage($this->t('Your shopping cart has been updated.')); + } } } diff --git a/modules/cart/tests/src/Functional/CartTest.php b/modules/cart/tests/src/Functional/CartTest.php index d87aab8618..a00dbb52f2 100644 --- a/modules/cart/tests/src/Functional/CartTest.php +++ b/modules/cart/tests/src/Functional/CartTest.php @@ -41,7 +41,7 @@ class CartTest extends OrderBrowserTestBase { */ public static $modules = [ 'commerce_cart', - 'node', + 'commerce_checkout', ]; /** @@ -81,18 +81,17 @@ protected function setUp() { $this->variations[] = $variation; $this->cart = \Drupal::service('commerce_cart.cart_provider')->createCart('default'); $this->cartManager = \Drupal::service('commerce_cart.cart_manager'); - } - - /** - * Test the cart page. - */ - public function testCartPage() { - $this->drupalLogin($this->adminUser); + // Add both variations to the cart. foreach ($this->variations as $variation) { $this->cartManager->addEntity($this->cart, $variation); } + } + /** + * Test the basic functioning of the cart page. + */ + public function testCartPage() { $this->drupalGet('cart'); // Confirm the presence of the order total summary. $this->assertSession()->elementTextContains('css', '.order-total-line', 'Subtotal'); @@ -139,4 +138,26 @@ public function testCartPage() { $this->assertSession()->statusCodeEquals(403); } + /** + * Tests the Checkout button added by commerce_checkout. + */ + public function testCheckoutButton() { + $this->drupalGet('cart'); + // Confirm that the "Checkout" button redirects and updates the cart. + $this->assertSession()->buttonExists('Checkout'); + $values = [ + 'edit_quantity[0]' => 2, + 'edit_quantity[1]' => 3, + ]; + $this->submitForm($values, t('Checkout')); + $this->assertSession()->addressEquals('checkout/1/order_information'); + $this->assertSession()->pageTextNotContains(t('Your shopping cart has been updated.')); + + $this->drupalGet('cart'); + $this->assertSession()->fieldValueEquals('edit-edit-quantity-0', 2); + $this->assertSession()->fieldValueEquals('edit-edit-quantity-1', 3); + $this->assertSession()->elementTextContains('css', '.order-total-line', 'Total'); + $this->assertSession()->pageTextContains('$3,048.00'); + } + } diff --git a/modules/checkout/commerce_checkout.module b/modules/checkout/commerce_checkout.module index 2fb3bcde0f..37e7cfa17e 100644 --- a/modules/checkout/commerce_checkout.module +++ b/modules/checkout/commerce_checkout.module @@ -177,6 +177,8 @@ function commerce_checkout_form_alter(&$form, FormStateInterface $form_state, $f '#access' => \Drupal::currentUser()->hasPermission('access checkout'), '#submit' => array_merge($form['#submit'], ['commerce_checkout_order_item_views_form_submit']), '#order_id' => $view->argument['order_id']->value[0], + '#update_cart' => TRUE, + '#show_update_message' => FALSE, ]; } } From 0054dd4f411831e055ee0a53e90a3ff70cc8a736 Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Thu, 22 Mar 2018 11:03:54 +0100 Subject: [PATCH 095/103] Issue #2897813 by waspper, bojanz: Remove the 'access cart' permission because it is unused and not granular enough --- modules/cart/commerce_cart.install | 17 ----------------- modules/cart/commerce_cart.permissions.yml | 3 --- modules/cart/commerce_cart.routing.yml | 2 +- modules/cart/tests/src/Functional/CartTest.php | 10 ---------- 4 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 modules/cart/commerce_cart.install delete mode 100644 modules/cart/commerce_cart.permissions.yml diff --git a/modules/cart/commerce_cart.install b/modules/cart/commerce_cart.install deleted file mode 100644 index 645fbdb95b..0000000000 --- a/modules/cart/commerce_cart.install +++ /dev/null @@ -1,17 +0,0 @@ -assertSession()->buttonExists('Remove'); $this->submitForm([], t('Remove')); $this->assertSession()->pageTextContains(t('Your shopping cart is empty.')); - - // Test that cart is denied for user without permission. - Role::load(RoleInterface::ANONYMOUS_ID) - ->revokePermission('access cart') - ->save(); - $this->drupalLogout(); - $this->drupalGet('cart'); - $this->assertSession()->statusCodeEquals(403); } /** From 26bb76f14f5498b897d8025a0e1ae26c4278cded Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Thu, 22 Mar 2018 14:38:15 +0100 Subject: [PATCH 096/103] Issue #2951181 by joachim, bojanz: QuantityWidget allows "0" as a valid value --- .../cart/tests/src/Functional/AddToCartFormTest.php | 12 +++++++++--- .../FunctionalJavascript/MultipleCartFormsTest.php | 2 +- .../src/Plugin/Field/FieldWidget/QuantityWidget.php | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/modules/cart/tests/src/Functional/AddToCartFormTest.php b/modules/cart/tests/src/Functional/AddToCartFormTest.php index a99e45c58b..4234fd3069 100644 --- a/modules/cart/tests/src/Functional/AddToCartFormTest.php +++ b/modules/cart/tests/src/Functional/AddToCartFormTest.php @@ -113,17 +113,23 @@ public function testExposedOrderItemFields() { /** @var \Drupal\Core\Entity\Entity\EntityFormDisplay $order_item_form_display */ $order_item_form_display = EntityFormDisplay::load('commerce_order_item.default.add_to_cart'); $order_item_form_display->setComponent('quantity', [ - 'type' => 'number', + 'type' => 'commerce_quantity', ]); $order_item_form_display->save(); - // Get the existing product page and submit Add to cart form. + + // Confirm that the given quantity was accepted and saved. $this->postAddToCart($this->variation->getProduct(), [ 'quantity[0][value]' => 3, ]); - // Check if the quantity was increased for the existing order item. $this->cart = Order::load($this->cart->id()); $order_items = $this->cart->getItems(); $this->assertOrderItemInOrder($this->variation, $order_items[0], 3); + + // Confirm that a zero quantity isn't accepted. + $this->postAddToCart($this->variation->getProduct(), [ + 'quantity[0][value]' => 0, + ]); + $this->assertSession()->pageTextContains('Quantity must be higher than or equal to 1.'); } /** diff --git a/modules/cart/tests/src/FunctionalJavascript/MultipleCartFormsTest.php b/modules/cart/tests/src/FunctionalJavascript/MultipleCartFormsTest.php index 1a26cd7d13..f7de1eaadd 100644 --- a/modules/cart/tests/src/FunctionalJavascript/MultipleCartFormsTest.php +++ b/modules/cart/tests/src/FunctionalJavascript/MultipleCartFormsTest.php @@ -56,7 +56,7 @@ protected function setUp() { /** @var \Drupal\Core\Entity\Entity\EntityFormDisplay $order_item_form_display */ $order_item_form_display = EntityFormDisplay::load('commerce_order_item.default.add_to_cart'); $order_item_form_display->setComponent('quantity', [ - 'type' => 'number', + 'type' => 'commerce_quantity', ]); $order_item_form_display->save(); diff --git a/modules/order/src/Plugin/Field/FieldWidget/QuantityWidget.php b/modules/order/src/Plugin/Field/FieldWidget/QuantityWidget.php index b65ecc6117..f0027ccb86 100644 --- a/modules/order/src/Plugin/Field/FieldWidget/QuantityWidget.php +++ b/modules/order/src/Plugin/Field/FieldWidget/QuantityWidget.php @@ -104,6 +104,7 @@ public function settingsSummary() { public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { $element = parent::formElement($items, $delta, $element, $form, $form_state); $element['value']['#step'] = $this->getSetting('step'); + $element['value']['#min'] = $this->getSetting('step'); return $element; } From def442dbc491c25296c6c668f1ae690c9f572c4c Mon Sep 17 00:00:00 2001 From: Bojan Zivanovic Date: Thu, 22 Mar 2018 17:18:48 +0100 Subject: [PATCH 097/103] Issue #2955241: Plugin label 'order item table' should be in sentence case --- .../order/src/Plugin/Field/FieldFormatter/OrderItemTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/order/src/Plugin/Field/FieldFormatter/OrderItemTable.php b/modules/order/src/Plugin/Field/FieldFormatter/OrderItemTable.php index 0a7f9aa3cf..3fc8a7ea17 100644 --- a/modules/order/src/Plugin/Field/FieldFormatter/OrderItemTable.php +++ b/modules/order/src/Plugin/Field/FieldFormatter/OrderItemTable.php @@ -11,7 +11,7 @@ * * @FieldFormatter( * id = "commerce_order_item_table", - * label = @Translation("order item table"), + * label = @Translation("Order item table"), * field_types = { * "entity_reference", * }, From ec32a7b985a57db5f1bec4e93ad903410a00aa7c Mon Sep 17 00:00:00 2001 From: Lendude Date: Thu, 22 Mar 2018 18:09:10 +0100 Subject: [PATCH 098/103] Issue #2882374 by xSDx, Lendude, drugan, bojanz: Views filters are broken for price fields ("number" column) --- modules/log/src/Entity/Log.php | 2 +- modules/order/src/Entity/Order.php | 2 +- modules/order/src/OrderItemViewsData.php | 4 +-- modules/payment/src/Entity/Payment.php | 2 +- modules/product/src/Entity/Product.php | 2 +- .../src/Entity/ProductAttributeValue.php | 2 +- .../product/src/Entity/ProductVariation.php | 2 +- modules/promotion/src/Entity/Coupon.php | 2 +- modules/promotion/src/Entity/Promotion.php | 2 +- modules/store/src/Entity/Store.php | 2 +- src/CommerceEntityViewsData.php | 31 +++++++++++++++++++ 11 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 src/CommerceEntityViewsData.php diff --git a/modules/log/src/Entity/Log.php b/modules/log/src/Entity/Log.php index a790812b17..ec42cc80d9 100644 --- a/modules/log/src/Entity/Log.php +++ b/modules/log/src/Entity/Log.php @@ -23,7 +23,7 @@ * "list_builder" = "Drupal\commerce_log\LogListBuilder", * "storage" = "Drupal\commerce_log\LogStorage", * "view_builder" = "Drupal\commerce_log\LogViewBuilder", - * "views_data" = "Drupal\views\EntityViewsData", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * }, * base_table = "commerce_log", * entity_keys = { diff --git a/modules/order/src/Entity/Order.php b/modules/order/src/Entity/Order.php index 4fdc00f94d..d0b6039aa2 100644 --- a/modules/order/src/Entity/Order.php +++ b/modules/order/src/Entity/Order.php @@ -32,7 +32,7 @@ * "access" = "Drupal\commerce_order\OrderAccessControlHandler", * "permission_provider" = "Drupal\commerce_order\OrderPermissionProvider", * "list_builder" = "Drupal\commerce_order\OrderListBuilder", - * "views_data" = "Drupal\views\EntityViewsData", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * "form" = { * "default" = "Drupal\commerce_order\Form\OrderForm", * "add" = "Drupal\commerce_order\Form\OrderForm", diff --git a/modules/order/src/OrderItemViewsData.php b/modules/order/src/OrderItemViewsData.php index eebb8fe774..7ed769ee3e 100644 --- a/modules/order/src/OrderItemViewsData.php +++ b/modules/order/src/OrderItemViewsData.php @@ -2,12 +2,12 @@ namespace Drupal\commerce_order; -use Drupal\views\EntityViewsData; +use Drupal\commerce\CommerceEntityViewsData; /** * Provides views data for order items. */ -class OrderItemViewsData extends EntityViewsData { +class OrderItemViewsData extends CommerceEntityViewsData { /** * {@inheritdoc} diff --git a/modules/payment/src/Entity/Payment.php b/modules/payment/src/Entity/Payment.php index 5d0bf05538..5a5213eba7 100644 --- a/modules/payment/src/Entity/Payment.php +++ b/modules/payment/src/Entity/Payment.php @@ -32,7 +32,7 @@ * "operation" = "Drupal\commerce_payment\Form\PaymentOperationForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * }, - * "views_data" = "Drupal\views\EntityViewsData", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * "route_provider" = { * "default" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider", * }, diff --git a/modules/product/src/Entity/Product.php b/modules/product/src/Entity/Product.php index 269a4c157d..9a7fe8ba7e 100644 --- a/modules/product/src/Entity/Product.php +++ b/modules/product/src/Entity/Product.php @@ -32,7 +32,7 @@ * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "view_builder" = "Drupal\commerce_product\ProductViewBuilder", * "list_builder" = "Drupal\commerce_product\ProductListBuilder", - * "views_data" = "Drupal\views\EntityViewsData", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * "form" = { * "default" = "Drupal\commerce_product\Form\ProductForm", * "add" = "Drupal\commerce_product\Form\ProductForm", diff --git a/modules/product/src/Entity/ProductAttributeValue.php b/modules/product/src/Entity/ProductAttributeValue.php index a87596c677..8ffb36c008 100644 --- a/modules/product/src/Entity/ProductAttributeValue.php +++ b/modules/product/src/Entity/ProductAttributeValue.php @@ -25,7 +25,7 @@ * "storage" = "Drupal\commerce_product\ProductAttributeValueStorage", * "access" = "Drupal\commerce\EmbeddedEntityAccessControlHandler", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", - * "views_data" = "Drupal\views\EntityViewsData", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * "translation" = "Drupal\content_translation\ContentTranslationHandler" * }, * admin_permission = "administer commerce_product_attribute", diff --git a/modules/product/src/Entity/ProductVariation.php b/modules/product/src/Entity/ProductVariation.php index 4e2b10dbe5..6a9e00ef84 100644 --- a/modules/product/src/Entity/ProductVariation.php +++ b/modules/product/src/Entity/ProductVariation.php @@ -32,7 +32,7 @@ * "storage" = "Drupal\commerce_product\ProductVariationStorage", * "access" = "Drupal\commerce\EmbeddedEntityAccessControlHandler", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", - * "views_data" = "Drupal\views\EntityViewsData", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * "form" = { * "default" = "Drupal\Core\Entity\ContentEntityForm", * }, diff --git a/modules/promotion/src/Entity/Coupon.php b/modules/promotion/src/Entity/Coupon.php index 5b3f0efc49..640e21bd82 100644 --- a/modules/promotion/src/Entity/Coupon.php +++ b/modules/promotion/src/Entity/Coupon.php @@ -25,7 +25,7 @@ * "list_builder" = "Drupal\commerce_promotion\CouponListBuilder", * "storage" = "Drupal\commerce_promotion\CouponStorage", * "access" = "Drupal\commerce_promotion\CouponAccessControlHandler", - * "views_data" = "Drupal\views\EntityViewsData", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * "form" = { * "add" = "Drupal\commerce_promotion\Form\CouponForm", * "edit" = "Drupal\commerce_promotion\Form\CouponForm", diff --git a/modules/promotion/src/Entity/Promotion.php b/modules/promotion/src/Entity/Promotion.php index ce394cab87..4d21bd8f18 100644 --- a/modules/promotion/src/Entity/Promotion.php +++ b/modules/promotion/src/Entity/Promotion.php @@ -32,7 +32,7 @@ * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "list_builder" = "Drupal\commerce_promotion\PromotionListBuilder", - * "views_data" = "Drupal\views\EntityViewsData", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * "form" = { * "default" = "Drupal\commerce_promotion\Form\PromotionForm", * "add" = "Drupal\commerce_promotion\Form\PromotionForm", diff --git a/modules/store/src/Entity/Store.php b/modules/store/src/Entity/Store.php index fcea981ccc..edd5f78037 100644 --- a/modules/store/src/Entity/Store.php +++ b/modules/store/src/Entity/Store.php @@ -31,7 +31,7 @@ * "permission_provider" = "Drupal\entity\EntityPermissionProvider", * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "list_builder" = "Drupal\commerce_store\StoreListBuilder", - * "views_data" = "Drupal\views\EntityViewsData", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * "form" = { * "default" = "Drupal\commerce_store\Form\StoreForm", * "add" = "Drupal\commerce_store\Form\StoreForm", diff --git a/src/CommerceEntityViewsData.php b/src/CommerceEntityViewsData.php new file mode 100644 index 0000000000..b52059d474 --- /dev/null +++ b/src/CommerceEntityViewsData.php @@ -0,0 +1,31 @@ + Date: Thu, 22 Mar 2018 22:55:45 +0100 Subject: [PATCH 099/103] Issue #2900772 by FatherShawn, abramm, bojanz: The payment method entity type is not exposed to Views --- modules/payment/src/Entity/PaymentMethod.php | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/payment/src/Entity/PaymentMethod.php b/modules/payment/src/Entity/PaymentMethod.php index c8b5889b6c..685fb14a98 100644 --- a/modules/payment/src/Entity/PaymentMethod.php +++ b/modules/payment/src/Entity/PaymentMethod.php @@ -30,6 +30,7 @@ * "access" = "Drupal\commerce_payment\PaymentMethodAccessControlHandler", * "list_builder" = "Drupal\commerce_payment\PaymentMethodListBuilder", * "storage" = "Drupal\commerce_payment\PaymentMethodStorage", + * "views_data" = "Drupal\commerce\CommerceEntityViewsData", * "form" = { * "edit" = "Drupal\commerce_payment\Form\PaymentMethodEditForm", * "delete" = "Drupal\commerce_payment\Form\PaymentMethodDeleteForm" From ac5cb9bd81a27ea9e642366c03f9b8f100adcfd1 Mon Sep 17 00:00:00 2001 From: lisastreeter Date: Thu, 22 Mar 2018 23:10:47 +0100 Subject: [PATCH 100/103] Issue #2853277 by lisastreeter: Populate OrderItem backreference to Order before OrderItem is saved --- modules/cart/src/CartManager.php | 1 + modules/cart/tests/src/Kernel/CartManagerTest.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/modules/cart/src/CartManager.php b/modules/cart/src/CartManager.php index 18f4c40ed9..c0de845a01 100644 --- a/modules/cart/src/CartManager.php +++ b/modules/cart/src/CartManager.php @@ -113,6 +113,7 @@ public function addOrderItem(OrderInterface $cart, OrderItemInterface $order_ite $saved_order_item = $matching_order_item; } else { + $order_item->set('order_id', $cart->id()); $order_item->save(); $cart->addItem($order_item); $saved_order_item = $order_item; diff --git a/modules/cart/tests/src/Kernel/CartManagerTest.php b/modules/cart/tests/src/Kernel/CartManagerTest.php index 8c447e4206..b7254a87bf 100644 --- a/modules/cart/tests/src/Kernel/CartManagerTest.php +++ b/modules/cart/tests/src/Kernel/CartManagerTest.php @@ -118,6 +118,7 @@ public function testCartManager() { $order_item1 = $this->reloadEntity($order_item1); $this->assertNotEmpty($cart->hasItem($order_item1)); $this->assertEquals(1, $order_item1->getQuantity()); + $this->assertEquals($cart->id(), $order_item1->getOrderId()); $this->assertEquals(new Price('1.00', 'USD'), $cart->getTotalPrice()); $order_item1->setQuantity(2); @@ -131,6 +132,7 @@ public function testCartManager() { $this->assertNotEmpty($cart->hasItem($order_item1)); $this->assertNotEmpty($cart->hasItem($order_item2)); $this->assertEquals(3, $order_item2->getQuantity()); + $this->assertEquals($cart->id(), $order_item2->getOrderId()); $this->assertEquals(new Price('8.00', 'USD'), $cart->getTotalPrice()); $this->cartManager->removeOrderItem($cart, $order_item1); From afd53f65e32aba3cd87bfda228bd66f019f777c4 Mon Sep 17 00:00:00 2001 From: Miroslav Date: Thu, 22 Mar 2018 23:47:54 +0100 Subject: [PATCH 101/103] Issue #2865324 by Dom., bojanz: Make the commerce_order_item "created" field configurable on view displays --- modules/order/src/Entity/OrderItem.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/order/src/Entity/OrderItem.php b/modules/order/src/Entity/OrderItem.php index 2eb3862348..7ba54a8ccb 100644 --- a/modules/order/src/Entity/OrderItem.php +++ b/modules/order/src/Entity/OrderItem.php @@ -358,7 +358,8 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { 'type' => 'timestamp', 'weight' => 0, ]) - ->setDisplayConfigurable('form', TRUE); + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); $fields['changed'] = BaseFieldDefinition::create('changed') ->setLabel(t('Changed')) From 9276e9ab05bffc4f70642ffa42add257690ef5ae Mon Sep 17 00:00:00 2001 From: mglaman Date: Sat, 24 Mar 2018 12:49:43 +0100 Subject: [PATCH 102/103] Issue #2707721 by mglaman, drugan, skek, AndreaMaggi, ransomweaver, ndf, Londova, bojanz: Incorrect display of attribute field values on the Add To Cart form --- modules/product/commerce_product.services.yml | 4 + .../ProductVariationAttributesWidget.php | 183 +---- modules/product/src/PreparedAttribute.php | 136 ++++ .../src/ProductVariationAttributeMapper.php | 160 ++++ ...oductVariationAttributeMapperInterface.php | 51 ++ .../ProductVariationAttributeMapperTest.php | 686 ++++++++++++++++++ 6 files changed, 1071 insertions(+), 149 deletions(-) create mode 100644 modules/product/src/PreparedAttribute.php create mode 100644 modules/product/src/ProductVariationAttributeMapper.php create mode 100644 modules/product/src/ProductVariationAttributeMapperInterface.php create mode 100644 modules/product/tests/src/Kernel/ProductVariationAttributeMapperTest.php diff --git a/modules/product/commerce_product.services.yml b/modules/product/commerce_product.services.yml index 619897ee9a..e709e17227 100644 --- a/modules/product/commerce_product.services.yml +++ b/modules/product/commerce_product.services.yml @@ -16,3 +16,7 @@ services: arguments: ['@current_route_match'] tags: - { name: 'context_provider' } + + commerce_product.variation_attribute_mapper: + class: Drupal\commerce_product\ProductVariationAttributeMapper + arguments: ['@commerce_product.attribute_field_manager', '@entity_type.manager', '@entity.repository'] diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php index 8a347cf409..1dee09b4e9 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php @@ -2,8 +2,8 @@ namespace Drupal\commerce_product\Plugin\Field\FieldWidget; -use Drupal\commerce_product\Entity\ProductVariationInterface; use Drupal\commerce_product\ProductAttributeFieldManagerInterface; +use Drupal\commerce_product\ProductVariationAttributeMapperInterface; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityRepositoryInterface; @@ -28,18 +28,18 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implements ContainerFactoryPluginInterface { /** - * The attribute field manager. + * The product attribute field manager. * * @var \Drupal\commerce_product\ProductAttributeFieldManagerInterface */ protected $attributeFieldManager; /** - * The product attribute storage. + * The product variation attribute mapper. * - * @var \Drupal\Core\Entity\EntityStorageInterface + * @var \Drupal\commerce_product\ProductVariationAttributeMapperInterface */ - protected $attributeStorage; + protected $variationAttributeMapper; /** * Constructs a new ProductVariationAttributesWidget object. @@ -59,13 +59,15 @@ class ProductVariationAttributesWidget extends ProductVariationWidgetBase implem * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * The entity repository. * @param \Drupal\commerce_product\ProductAttributeFieldManagerInterface $attribute_field_manager - * The attribute field manager. + * The product attribute field manager. + * @param \Drupal\commerce_product\ProductVariationAttributeMapperInterface $variation_attribute_mapper + * The product variation attribute mapper. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ProductAttributeFieldManagerInterface $attribute_field_manager, ProductVariationAttributeMapperInterface $variation_attribute_mapper) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $entity_type_manager, $entity_repository); $this->attributeFieldManager = $attribute_field_manager; - $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); + $this->variationAttributeMapper = $variation_attribute_mapper; } /** @@ -80,7 +82,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['third_party_settings'], $container->get('entity_type.manager'), $container->get('entity.repository'), - $container->get('commerce_product.attribute_field_manager') + $container->get('commerce_product.attribute_field_manager'), + $container->get('commerce_product.variation_attribute_mapper') ); } @@ -126,9 +129,9 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen // If an operation caused the form to rebuild, select the variation from // the user's current input. if ($form_state->isRebuilding()) { - $parents = array_merge($element['#field_parents'], [$items->getName(), $delta]); - $user_input = (array) NestedArray::getValue($form_state->getUserInput(), $parents); - $selected_variation = $this->selectVariationFromUserInput($variations, $user_input); + $parents = array_merge($element['#field_parents'], [$items->getName(), $delta, 'attributes']); + $attribute_values = (array) NestedArray::getValue($form_state->getUserInput(), $parents); + $selected_variation = $this->variationAttributeMapper->selectVariation($variations, $attribute_values); } // Otherwise load from the current context. else { @@ -155,12 +158,12 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen 'class' => ['attribute-widgets'], ], ]; - foreach ($this->getAttributeInfo($selected_variation, $variations) as $field_name => $attribute) { - $element['attributes'][$field_name] = [ - '#type' => $attribute['element_type'], - '#title' => $attribute['title'], - '#options' => $attribute['values'], - '#required' => $attribute['required'], + foreach ($this->variationAttributeMapper->prepareAttributes($selected_variation, $variations) as $field_name => $attribute) { + $attribute_element = [ + '#type' => $attribute->getElementType(), + '#title' => $attribute->getLabel(), + '#options' => $attribute->getValues(), + '#required' => $attribute->isRequired(), '#default_value' => $selected_variation->getAttributeValueId($field_name), '#limit_validation_errors' => [], '#ajax' => [ @@ -169,25 +172,27 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen ], ]; // Convert the _none option into #empty_value. - if (isset($element['attributes'][$field_name]['#options']['_none'])) { - if (!$element['attributes'][$field_name]['#required']) { - $element['attributes'][$field_name]['#empty_value'] = ''; + if (isset($attribute_element['#options']['_none'])) { + if (!$attribute_element['#required']) { + $attribute_element['#empty_value'] = ''; } - unset($element['attributes'][$field_name]['#options']['_none']); + unset($attribute_element['#options']['_none']); } // 1 required value -> Disable the element to skip unneeded ajax calls. - if ($attribute['required'] && count($attribute['values']) === 1) { - $element['attributes'][$field_name]['#disabled'] = TRUE; + if ($attribute_element['#required'] && count($attribute->getValues()) === 1) { + $attribute_element['#disabled'] = TRUE; } // Optimize the UX of optional attributes: // - Hide attributes that have no values. // - Require attributes that have a value on each variation. - if (empty($element['attributes'][$field_name]['#options'])) { - $element['attributes'][$field_name]['#access'] = FALSE; + if (empty($attribute_element['#options'])) { + $attribute_element['#access'] = FALSE; } if (!isset($element['attributes'][$field_name]['#empty_value'])) { - $element['attributes'][$field_name]['#required'] = TRUE; + $attribute_element['#required'] = TRUE; } + + $element['attributes'][$field_name] = $attribute_element; } return $element; @@ -202,132 +207,12 @@ public function massageFormValues(array $values, array $form, FormStateInterface $variations = $this->variationStorage->loadEnabled($product); foreach ($values as &$value) { - $selected_variation = $this->selectVariationFromUserInput($variations, $value); + $attribute_values = isset($value['attributes']) ? $value['attributes'] : []; + $selected_variation = $this->variationAttributeMapper->selectVariation($variations, $attribute_values); $value['variation'] = $selected_variation->id(); } return parent::massageFormValues($values, $form, $form_state); } - /** - * Selects a product variation from user input. - * - * If there's no user input (form viewed for the first time), the default - * variation is returned. - * - * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations - * An array of product variations. - * @param array $user_input - * The user input. - * - * @return \Drupal\commerce_product\Entity\ProductVariationInterface - * The selected variation. - */ - protected function selectVariationFromUserInput(array $variations, array $user_input) { - $current_variation = reset($variations); - if (!empty($user_input['attributes'])) { - $attributes = $user_input['attributes']; - foreach ($variations as $variation) { - $match = TRUE; - foreach ($attributes as $field_name => $value) { - if ($variation->getAttributeValueId($field_name) != $value) { - $match = FALSE; - } - } - if ($match) { - $current_variation = $variation; - break; - } - } - } - - return $current_variation; - } - - /** - * Gets the attribute information for the selected product variation. - * - * @param \Drupal\commerce_product\Entity\ProductVariationInterface $selected_variation - * The selected product variation. - * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations - * The available product variations. - * - * @return array[] - * The attribute information, keyed by field name. - */ - protected function getAttributeInfo(ProductVariationInterface $selected_variation, array $variations) { - $attributes = []; - $field_definitions = $this->attributeFieldManager->getFieldDefinitions($selected_variation->bundle()); - $field_map = $this->attributeFieldManager->getFieldMap($selected_variation->bundle()); - $field_names = array_column($field_map, 'field_name'); - $attribute_ids = array_column($field_map, 'attribute_id'); - $index = 0; - foreach ($field_names as $field_name) { - $field = $field_definitions[$field_name]; - /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */ - $attribute = $this->attributeStorage->load($attribute_ids[$index]); - // Make sure we have translation for attribute. - $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId()); - - $attributes[$field_name] = [ - 'field_name' => $field_name, - 'title' => $attribute->label(), - 'required' => $field->isRequired(), - 'element_type' => $attribute->getElementType(), - ]; - // The first attribute gets all values. Every next attribute gets only - // the values from variations matching the previous attribute value. - // For 'Color' and 'Size' attributes that means getting the colors of all - // variations, but only the sizes of variations with the selected color. - $callback = NULL; - if ($index > 0) { - $previous_field_name = $field_names[$index - 1]; - $previous_field_value = $selected_variation->getAttributeValueId($previous_field_name); - $callback = function ($variation) use ($previous_field_name, $previous_field_value) { - /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ - return $variation->getAttributeValueId($previous_field_name) == $previous_field_value; - }; - } - - $attributes[$field_name]['values'] = $this->getAttributeValues($variations, $field_name, $callback); - $index++; - } - // Filter out attributes with no values. - $attributes = array_filter($attributes, function ($attribute) { - return !empty($attribute['values']); - }); - - return $attributes; - } - - /** - * Gets the attribute values of a given set of variations. - * - * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations - * The variations. - * @param string $field_name - * The field name of the attribute. - * @param callable|null $callback - * An optional callback to use for filtering the list. - * - * @return array[] - * The attribute values, keyed by attribute ID. - */ - protected function getAttributeValues(array $variations, $field_name, callable $callback = NULL) { - $values = []; - foreach ($variations as $variation) { - if (is_null($callback) || call_user_func($callback, $variation)) { - $attribute_value = $variation->getAttributeValue($field_name); - if ($attribute_value) { - $values[$attribute_value->id()] = $attribute_value->label(); - } - else { - $values['_none'] = ''; - } - } - } - - return $values; - } - } diff --git a/modules/product/src/PreparedAttribute.php b/modules/product/src/PreparedAttribute.php new file mode 100644 index 0000000000..4f26e06816 --- /dev/null +++ b/modules/product/src/PreparedAttribute.php @@ -0,0 +1,136 @@ +id = $definition['id']; + $this->label = $definition['label']; + $this->elementType = $definition['element_type']; + $this->required = $definition['required']; + $this->values = $definition['values']; + } + + /** + * Gets the ID. + * + * @return string + * The ID. + */ + public function getId() { + return $this->id; + } + + /** + * Gets the label. + * + * @return string + * The label. + */ + public function getLabel() { + return $this->label; + } + + /** + * Gets the element type. + * + * @return string + * The element type. + */ + public function getElementType() { + return $this->elementType; + } + + /** + * Gets whether the attribute is required. + * + * @return bool + * TRUE if the attribute is required, FALSE otherwise. + */ + public function isRequired() { + return $this->required; + } + + /** + * Gets the attribute values. + * + * @return string[] + * The attribute values. + */ + public function getValues() { + return $this->values; + } + + /** + * Gets the array representation of the prepared attribute. + * + * @return array + * The array representation of the prepared attribute. + */ + public function toArray() { + return [ + 'id' => $this->id, + 'label' => $this->label, + 'element_type' => $this->elementType, + 'required' => $this->required, + 'values' => $this->values, + ]; + } + +} diff --git a/modules/product/src/ProductVariationAttributeMapper.php b/modules/product/src/ProductVariationAttributeMapper.php new file mode 100644 index 0000000000..95896c99b0 --- /dev/null +++ b/modules/product/src/ProductVariationAttributeMapper.php @@ -0,0 +1,160 @@ +attributeFieldManager = $attribute_field_manager; + $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); + $this->entityRepository = $entity_repository; + } + + /** + * {@inheritdoc} + */ + public function selectVariation(array $variations, array $attribute_values = []) { + $selected_variation = reset($variations); + // Select the first variation that matches the most attribute values. + // Start with all attribute values, reduce them by 1 until a match is found. + while (!empty($attribute_values)) { + foreach ($variations as $variation) { + $match = TRUE; + foreach ($attribute_values as $field_name => $attribute_value_id) { + if ($variation->getAttributeValueId($field_name) != $attribute_value_id) { + $match = FALSE; + } + } + if ($match) { + $selected_variation = $variation; + break 2; + } + } + array_pop($attribute_values); + } + + return $selected_variation; + } + + /** + * {@inheritdoc} + */ + public function prepareAttributes(ProductVariationInterface $selected_variation, array $variations) { + $attributes = []; + $field_definitions = $this->attributeFieldManager->getFieldDefinitions($selected_variation->bundle()); + $field_map = $this->attributeFieldManager->getFieldMap($selected_variation->bundle()); + $field_names = array_column($field_map, 'field_name'); + $attribute_ids = array_column($field_map, 'attribute_id'); + $index = 0; + foreach ($field_names as $field_name) { + $field = $field_definitions[$field_name]; + /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute */ + $attribute = $this->attributeStorage->load($attribute_ids[$index]); + // Make sure we have translation for attribute. + $attribute = $this->entityRepository->getTranslationFromContext($attribute, $selected_variation->language()->getId()); + + $definition = [ + 'id' => $attribute->id(), + 'label' => $attribute->label(), + 'element_type' => $attribute->getElementType(), + 'required' => $field->isRequired(), + ]; + // The first attribute gets all values. Every next attribute gets only + // the values from variations matching the previous attribute value. + // For 'Color' and 'Size' attributes that means getting the colors of all + // variations, but only the sizes of variations with the selected color. + $callback = NULL; + if ($index > 0) { + $index_limit = $index - 1; + // Get all previous field values. + $previous_variation_field_values = []; + for ($i = 0; $i <= $index_limit; $i++) { + $previous_variation_field_values[$field_names[$i]] = $selected_variation->getAttributeValueId($field_names[$i]); + } + + $callback = function (ProductVariationInterface $variation) use ($previous_variation_field_values) { + $results = []; + foreach ($previous_variation_field_values as $previous_field_name => $previous_field_value) { + $results[] = $variation->getAttributeValueId($previous_field_name) == $previous_field_value; + } + return !in_array(FALSE, $results, TRUE); + }; + } + $definition['values'] = $this->getAttributeValues($variations, $field_name, $callback); + // Return the attribute only if it has at least one value. + if (!empty($definition['values'])) { + $attributes[$field_name] = new PreparedAttribute($definition); + } + + $index++; + } + + return $attributes; + } + + /** + * Gets the attribute values of a given set of variations. + * + * @param \Drupal\commerce_product\Entity\ProductVariationInterface[] $variations + * The variations. + * @param string $field_name + * The field name of the attribute. + * @param callable|null $callback + * An optional callback to use for filtering the list. + * + * @return array[] + * The attribute values, keyed by attribute ID. + */ + protected function getAttributeValues(array $variations, $field_name, callable $callback = NULL) { + $values = []; + foreach ($variations as $variation) { + if (is_null($callback) || call_user_func($callback, $variation)) { + $attribute_value = $variation->getAttributeValue($field_name); + if ($attribute_value) { + $values[$attribute_value->id()] = $attribute_value->label(); + } + else { + $values['_none'] = ''; + } + } + } + + return $values; + } + +} diff --git a/modules/product/src/ProductVariationAttributeMapperInterface.php b/modules/product/src/ProductVariationAttributeMapperInterface.php new file mode 100644 index 0000000000..f9659ffb14 --- /dev/null +++ b/modules/product/src/ProductVariationAttributeMapperInterface.php @@ -0,0 +1,51 @@ +installEntitySchema('commerce_product_variation'); + $this->installEntitySchema('commerce_product'); + $this->installEntitySchema('commerce_product_attribute'); + $this->installEntitySchema('commerce_product_attribute_value'); + $this->installConfig(['commerce_product']); + + $this->attributeFieldManager = $this->container->get('commerce_product.attribute_field_manager'); + $this->mapper = $this->container->get('commerce_product.variation_attribute_mapper'); + $variation_type = ProductVariationType::load('default'); + + $this->colorAttributes = $this->createAttributeSet($variation_type, 'color', [ + 'black' => 'Black', + 'blue' => 'Blue', + 'green' => 'Green', + 'red' => 'Red', + 'white' => 'White', + 'yellow' => 'Yellow', + ]); + $this->sizeAttributes = $this->createAttributeSet($variation_type, 'size', [ + 'small' => 'Small', + 'medium' => 'Medium', + 'large' => 'Large', + ]); + $this->ramAttributes = $this->createAttributeSet($variation_type, 'ram', [ + '4gb' => '4GB', + '8gb' => '8GB', + '16gb' => '16GB', + '32gb' => '32GB', + ]); + $this->disk1Attributes = $this->createAttributeSet($variation_type, 'disk1', [ + '1tb' => '1TB', + '2tb' => '2TB', + '3tb' => '3TB', + ]); + $this->disk2Attributes = $this->createAttributeSet($variation_type, 'disk2', [ + '1tb' => '1TB', + '2tb' => '2TB', + '3tb' => '3TB', + ]); + } + + /** + * Tests selecting a variation. + * + * @covers ::selectVariation + */ + public function testSelect() { + $product = $this->generateThreeByTwoScenario(); + $variations = $product->getVariations(); + + // No attribute values. + $selected_variation = $this->mapper->selectVariation($product->getVariations()); + $this->assertEquals($product->getDefaultVariation()->id(), $selected_variation->id()); + + // Empty attribute values. + $selected_variation = $this->mapper->selectVariation($product->getVariations(), [ + 'attribute_color' => '', + 'attribute_size' => '', + ]); + $this->assertEquals($product->getDefaultVariation()->id(), $selected_variation->id()); + + // Single attribute value. + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_color' => $this->colorAttributes['blue']->id(), + ]); + $this->assertEquals($variations[3]->id(), $selected_variation->id()); + + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_size' => $this->sizeAttributes['large']->id(), + ]); + $this->assertEquals($variations[2]->id(), $selected_variation->id()); + + // Two attribute values. + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_color' => $this->colorAttributes['red']->id(), + 'attribute_size' => $this->sizeAttributes['large']->id(), + ]); + $this->assertEquals($variations[2]->id(), $selected_variation->id()); + + // Invalid attribute combination. + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_color' => $this->colorAttributes['blue']->id(), + 'attribute_size' => $this->sizeAttributes['large']->id(), + ]); + $this->assertEquals($variations[3]->id(), $selected_variation->id()); + $this->assertEquals('Blue', $selected_variation->getAttributeValue('attribute_color')->label()); + $this->assertEquals('Small', $selected_variation->getAttributeValue('attribute_size')->label()); + + // Missing first attribute. + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_color' => '', + 'attribute_size' => $this->sizeAttributes['large']->id(), + ]); + $this->assertEquals($product->getDefaultVariation()->id(), $selected_variation->id()); + } + + /** + * Tests selecting a variation when there are optional attributes. + * + * @covers ::selectVariation + */ + public function testSelectWithOptionalAttributes() { + $product = $this->generateThreeByTwoOptionalScenario(); + $variations = $product->getVariations(); + + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_ram' => $this->ramAttributes['16gb']->id(), + ]); + $this->assertEquals($variations[1]->id(), $selected_variation->id()); + + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_ram' => $this->ramAttributes['16gb']->id(), + 'attribute_disk1' => $this->disk1Attributes['1tb']->id(), + 'attribute_disk2' => $this->disk2Attributes['1tb']->id(), + ]); + $this->assertEquals($variations[2]->id(), $selected_variation->id()); + + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_ram' => $this->ramAttributes['16gb']->id(), + 'attribute_disk1' => $this->disk1Attributes['1tb']->id(), + 'attribute_disk2' => $this->disk2Attributes['2tb']->id(), + ]); + // Falls back to 16GBx1TB, 16GBx1TBx2TB is invalid. + $this->assertEquals($variations[1]->id(), $selected_variation->id()); + } + + /** + * Tests preparing attributes. + * + * @covers ::prepareAttributes + */ + public function testPrepareAttributes() { + $product = $this->generateThreeByTwoScenario(); + $variations = $product->getVariations(); + + // Test from the initial variation. + $attributes = $this->mapper->prepareAttributes(reset($variations), $variations); + + $color_attribute = $attributes['attribute_color']; + $this->assertEquals('color', $color_attribute->getId()); + $this->assertEquals('Color', $color_attribute->getLabel()); + $this->assertEquals('select', $color_attribute->getElementType()); + $this->assertTrue($color_attribute->isRequired()); + $this->assertEquals(['2' => 'Blue', '4' => 'Red'], $color_attribute->getValues()); + + $size_attribute = $attributes['attribute_size']; + $this->assertEquals('size', $size_attribute->getId()); + $this->assertEquals('Size', $size_attribute->getLabel()); + $this->assertEquals('select', $size_attribute->getElementType()); + $this->assertTrue($size_attribute->isRequired()); + $this->assertEquals(['7' => 'Small', '8' => 'Medium', '9' => 'Large'], $size_attribute->getValues()); + + // Test Blue Medium. + $attributes = $this->mapper->prepareAttributes($variations[4], $variations); + + $color_attribute = $attributes['attribute_color']; + $this->assertEquals('color', $color_attribute->getId()); + $this->assertEquals('Color', $color_attribute->getLabel()); + $this->assertEquals('select', $color_attribute->getElementType()); + $this->assertTrue($color_attribute->isRequired()); + $this->assertEquals(['2' => 'Blue', '4' => 'Red'], $color_attribute->getValues()); + + $size_attribute = $attributes['attribute_size']; + $this->assertEquals('size', $size_attribute->getId()); + $this->assertEquals('Size', $size_attribute->getLabel()); + $this->assertEquals('select', $size_attribute->getElementType()); + $this->assertTrue($size_attribute->isRequired()); + $this->assertEquals(['7' => 'Small', '8' => 'Medium'], $size_attribute->getValues()); + } + + /** + * Tests preparing attributes when there are optional attributes. + * + * @covers ::prepareAttributes + */ + public function testPrepareAttributesOptional() { + $product = $this->generateThreeByTwoOptionalScenario(); + $variations = $product->getVariations(); + + // Test from the initial variation. + $attributes = $this->mapper->prepareAttributes(reset($variations), $variations); + + $ram_attribute = $attributes['attribute_ram']; + $this->assertEquals('ram', $ram_attribute->getId()); + $this->assertEquals('Ram', $ram_attribute->getLabel()); + $this->assertEquals('select', $ram_attribute->getElementType()); + $this->assertTrue($ram_attribute->isRequired()); + $this->assertEquals(['11' => '8GB', '12' => '16GB'], $ram_attribute->getValues()); + + $disk1_attribute = $attributes['attribute_disk1']; + $this->assertEquals('disk1', $disk1_attribute->getId()); + $this->assertEquals('Disk1', $disk1_attribute->getLabel()); + $this->assertEquals('select', $disk1_attribute->getElementType()); + $this->assertTrue($disk1_attribute->isRequired()); + $this->assertEquals(['14' => '1TB'], $disk1_attribute->getValues()); + + // The Disk 2 1TB option should not show. Only "none". + // The default variation is 8GB x 1TB, which does not have the Disk 2 value + // so it should only return "_none". The Disk 2 option should have only have + // this option is the 16GB RAM option is chosen. + $disk2_attribute = $attributes['attribute_disk2']; + $this->assertEquals('disk2', $disk2_attribute->getId()); + $this->assertEquals('Disk2', $disk2_attribute->getLabel()); + $this->assertEquals('select', $disk2_attribute->getElementType()); + $this->assertTrue($disk2_attribute->isRequired()); + $this->assertEquals(['_none' => ''], $disk2_attribute->getValues()); + + // Test from the 16GB x 1TB x None variation. + $attributes = $this->mapper->prepareAttributes($variations[1], $variations); + + $ram_attribute = $attributes['attribute_ram']; + $this->assertEquals('ram', $ram_attribute->getId()); + $this->assertEquals('Ram', $ram_attribute->getLabel()); + $this->assertEquals('select', $ram_attribute->getElementType()); + $this->assertTrue($ram_attribute->isRequired()); + $this->assertEquals(['11' => '8GB', '12' => '16GB'], $ram_attribute->getValues()); + + $disk1_attribute = $attributes['attribute_disk1']; + $this->assertEquals('disk1', $disk1_attribute->getId()); + $this->assertEquals('Disk1', $disk1_attribute->getLabel()); + $this->assertEquals('select', $disk1_attribute->getElementType()); + $this->assertTrue($disk1_attribute->isRequired()); + $this->assertEquals(['14' => '1TB'], $disk1_attribute->getValues()); + + $disk2_attribute = $attributes['attribute_disk2']; + $this->assertEquals('disk2', $disk2_attribute->getId()); + $this->assertEquals('Disk2', $disk2_attribute->getLabel()); + $this->assertEquals('select', $disk2_attribute->getElementType()); + $this->assertTrue($disk2_attribute->isRequired()); + $this->assertEquals(['_none' => '', '17' => '1TB'], $disk2_attribute->getValues()); + } + + /** + * Tests preparing attributes when the values are mutually exclusive. + * + * @covers ::prepareAttributes + */ + public function testMutuallyExclusiveAttributeMatrixTwoByTwoByTwo() { + $product = Product::create([ + 'type' => 'default', + 'title' => $this->randomMachineName(), + 'stores' => [$this->store], + 'variations' => [], + ]); + $attribute_values_matrix = [ + ['4gb', '2tb', '2tb'], + ['8gb', '1tb', '2tb'], + ['8gb', '2tb', '1tb'], + ]; + $variations = []; + foreach ($attribute_values_matrix as $key => $value) { + $variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + 'attribute_ram' => $this->ramAttributes[$value[0]], + 'attribute_disk1' => $this->disk1Attributes[$value[1]], + 'attribute_disk2' => isset($this->disk2Attributes[$value[2]]) ? $this->disk2Attributes[$value[2]] : NULL, + ]); + $variation->save(); + $variations[] = $variation; + $product->addVariation($variation); + } + $product->save(); + + // Test from the initial variation. + $attributes = $this->mapper->prepareAttributes(reset($variations), $variations); + + $ram_attribute = $attributes['attribute_ram']; + $this->assertEquals('ram', $ram_attribute->getId()); + $this->assertEquals('Ram', $ram_attribute->getLabel()); + $this->assertEquals('select', $ram_attribute->getElementType()); + $this->assertTrue($ram_attribute->isRequired()); + $this->assertEquals(['11' => '8GB', '10' => '4GB'], $ram_attribute->getValues()); + + $disk1_attribute = $attributes['attribute_disk1']; + $this->assertEquals('disk1', $disk1_attribute->getId()); + $this->assertEquals('Disk1', $disk1_attribute->getLabel()); + $this->assertEquals('select', $disk1_attribute->getElementType()); + $this->assertTrue($disk1_attribute->isRequired()); + $this->assertNotCount(3, $disk1_attribute->getValues(), 'Out of the three available attribute values, only the one used is returned.'); + $this->assertCount(1, $disk1_attribute->getValues()); + $this->assertEquals(['15' => '2TB'], $disk1_attribute->getValues()); + + $disk2_attribute = $attributes['attribute_disk2']; + $this->assertEquals('disk2', $disk2_attribute->getId()); + $this->assertEquals('Disk2', $disk2_attribute->getLabel()); + $this->assertEquals('select', $disk2_attribute->getElementType()); + $this->assertTrue($disk2_attribute->isRequired()); + $this->assertEquals(['18' => '2TB'], $disk2_attribute->getValues()); + + // Test 8GB x 1TB x 2TB. + $attributes = $this->mapper->prepareAttributes($variations[1], $variations); + + $ram_attribute = $attributes['attribute_ram']; + $this->assertEquals('ram', $ram_attribute->getId()); + $this->assertEquals('Ram', $ram_attribute->getLabel()); + $this->assertEquals('select', $ram_attribute->getElementType()); + $this->assertTrue($ram_attribute->isRequired()); + $this->assertNotCount(4, $ram_attribute->getValues(), 'Out of the four available attribute values, only the two used are returned.'); + $this->assertCount(2, $ram_attribute->getValues()); + $this->assertEquals(['11' => '8GB', '10' => '4GB'], $ram_attribute->getValues()); + + $disk1_attribute = $attributes['attribute_disk1']; + $this->assertEquals('disk1', $disk1_attribute->getId()); + $this->assertEquals('Disk1', $disk1_attribute->getLabel()); + $this->assertEquals('select', $disk1_attribute->getElementType()); + $this->assertTrue($disk1_attribute->isRequired()); + $this->assertEquals(['15' => '2TB', '14' => '1TB'], $disk1_attribute->getValues()); + + $disk2_attribute = $attributes['attribute_disk2']; + $this->assertEquals('disk2', $disk2_attribute->getId()); + $this->assertEquals('Disk2', $disk2_attribute->getLabel()); + $this->assertEquals('select', $disk2_attribute->getElementType()); + $this->assertTrue($disk2_attribute->isRequired()); + // There should only be one Disk 2 option, since the other 8GB RAM option + // has a Disk 1 value of 2TB. + $this->assertEquals(['18' => '2TB'], $disk2_attribute->getValues()); + + // Test 8GB x 2TB x 1TB. + $attributes = $this->mapper->prepareAttributes($variations[2], $variations); + + $ram_attribute = $attributes['attribute_ram']; + $this->assertEquals('ram', $ram_attribute->getId()); + $this->assertEquals('Ram', $ram_attribute->getLabel()); + $this->assertEquals('select', $ram_attribute->getElementType()); + $this->assertTrue($ram_attribute->isRequired()); + $this->assertEquals(['11' => '8GB', '10' => '4GB'], $ram_attribute->getValues()); + + $disk1_attribute = $attributes['attribute_disk1']; + $this->assertEquals('disk1', $disk1_attribute->getId()); + $this->assertEquals('Disk1', $disk1_attribute->getLabel()); + $this->assertEquals('select', $disk1_attribute->getElementType()); + $this->assertTrue($disk1_attribute->isRequired()); + $this->assertEquals(['15' => '2TB', '14' => '1TB'], $disk1_attribute->getValues()); + + $disk2_attribute = $attributes['attribute_disk2']; + $this->assertEquals('disk2', $disk2_attribute->getId()); + $this->assertEquals('Disk2', $disk2_attribute->getLabel()); + $this->assertEquals('select', $disk2_attribute->getElementType()); + $this->assertTrue($disk2_attribute->isRequired()); + // There should only be one Disk 2 option, since the other 8GB RAM option + // has a Disk 1 value of 2TB. + $this->assertEquals(['17' => '1TB'], $disk2_attribute->getValues()); + } + + /** + * Tests having three attributes and six variations. + * + * @covers ::selectVariation + * @covers ::prepareAttributes + */ + public function testThreeAttributesSixVariations() { + $variation_type = ProductVariationType::load('default'); + + $pack = $this->createAttributeSet($variation_type, 'pack', [ + 'one' => '1', + 'twenty' => '20', + 'hundred' => '100', + 'twohundred' => '200', + ]); + + $product = Product::create([ + 'type' => 'default', + 'title' => $this->randomMachineName(), + 'stores' => [$this->store], + 'variations' => [], + ]); + $product->save(); + + // The Size attribute needs a lighter weight than Color for this scenario. + // @todo This is an undocumented item, where the order of the attributes on + // the form display correlate to how they display in the widget / returned + // values. + $form_display = commerce_get_entity_display('commerce_product_variation', $variation_type->id(), 'form'); + $form_display->setComponent('attribute_size', ['weight' => 0] + $form_display->getComponent('attribute_size')); + $form_display->setComponent('attribute_color', ['weight' => 1] + $form_display->getComponent('attribute_color')); + $form_display->setComponent('attribute_pack', ['weight' => 2] + $form_display->getComponent('attribute_pack')); + $form_display->save(); + + $attribute_values_matrix = [ + ['small', 'black', 'one'], + ['small', 'blue', 'twenty'], + ['medium', 'green', 'hundred'], + ['medium', 'red', 'twohundred'], + ['large', 'white', 'hundred'], + ['large', 'yellow', 'twenty'], + ]; + $variations = []; + foreach ($attribute_values_matrix as $key => $value) { + $variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + 'attribute_size' => $this->sizeAttributes[$value[0]], + 'attribute_color' => $this->colorAttributes[$value[1]], + 'attribute_pack' => $pack[$value[2]], + ]); + $variation->save(); + $variations[] = $variation; + $product->addVariation($variation); + } + $product->save(); + + /** @var \Drupal\commerce_product\Entity\ProductVariation[] $variations */ + $variations = $product->getVariations(); + $selected_variation = $this->mapper->selectVariation($product->getVariations()); + $this->assertEquals($product->getDefaultVariation()->id(), $selected_variation->id()); + $this->assertEquals('Small', $selected_variation->getAttributeValue('attribute_size')->label()); + $this->assertEquals('Black', $selected_variation->getAttributeValue('attribute_color')->label()); + $this->assertEquals('1', $selected_variation->getAttributeValue('attribute_pack')->label()); + + // Verify available attribute selections. + $attributes = $this->mapper->prepareAttributes($selected_variation, $product->getVariations()); + $size_attribute = $attributes['attribute_size']; + $this->assertEquals(['7' => 'Small', '8' => 'Medium', '9' => 'Large'], $size_attribute->getValues()); + $color_attribute = $attributes['attribute_color']; + $this->assertEquals(['2' => 'Blue', '1' => 'Black'], $color_attribute->getValues()); + $pack_attribute = $attributes['attribute_pack']; + // The resolved variation is Small -> Black -> 1, cannot choose 20 for the + // pack size, since that is Small -> Blue -> 20. + $this->assertEquals(['20' => '1'], $pack_attribute->getValues()); + + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_size' => $this->sizeAttributes['small']->id(), + 'attribute_color' => $this->colorAttributes['blue']->id(), + ]); + $this->assertEquals($variations[1]->id(), $selected_variation->id()); + + // Medium only has Green & Red as color, so selecting this size should + // cause the color to reset. + $selected_variation = $this->mapper->selectVariation($variations, [ + 'attribute_size' => $this->sizeAttributes['medium']->id(), + 'attribute_color' => $this->colorAttributes['blue']->id(), + ]); + $this->assertEquals($variations[2]->id(), $selected_variation->id()); + $this->assertEquals('Medium', $selected_variation->getAttributeValue('attribute_size')->label()); + $this->assertEquals('Green', $selected_variation->getAttributeValue('attribute_color')->label()); + $this->assertEquals('100', $selected_variation->getAttributeValue('attribute_pack')->label()); + + // Verify available attribute selections. + $attributes = $this->mapper->prepareAttributes($selected_variation, $product->getVariations()); + $size_attribute = $attributes['attribute_size']; + $this->assertEquals(['7' => 'Small', '8' => 'Medium', '9' => 'Large'], $size_attribute->getValues()); + $color_attribute = $attributes['attribute_color']; + $this->assertEquals(['3' => 'Green', '4' => 'Red'], $color_attribute->getValues()); + $pack_attribute = $attributes['attribute_pack']; + // The resolved variation is Medium -> Green -> 100, cannot choose 200 for + // the pack size, since that is Medium -> Red -> 200. + $this->assertEquals(['22' => '100'], $pack_attribute->getValues()); + } + + /** + * Generates a three by two scenario. + * + * There are three sizes and two colors. Missing one color option. + * Generated product variations: + * [ RS, RM, RL ] + * [ BS, BM, X ] + * + * @return \Drupal\commerce_product\Entity\ProductInterface + * The product. + */ + protected function generateThreeByTwoScenario() { + $product = Product::create([ + 'type' => 'default', + 'title' => $this->randomMachineName(), + 'stores' => [$this->store], + 'variations' => [], + ]); + $attribute_values_matrix = [ + ['red', 'small'], + ['red', 'medium'], + ['red', 'large'], + ['blue', 'small'], + ['blue', 'medium'], + ]; + $variations = []; + foreach ($attribute_values_matrix as $key => $value) { + $variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + 'attribute_color' => $this->colorAttributes[$value[0]], + 'attribute_size' => $this->sizeAttributes[$value[1]], + ]); + $variation->save(); + $variations[] = $variation; + $product->addVariation($variation); + } + $product->save(); + + return $product; + } + + /** + * Generates a three by two (optional) scenario. + * + * Generated product variations: + * [ 8GBx1TB, X , X ] + * [ X , 16GBx1TB , X ] + * [ X , 16GBx1TBx1TB, X ] + * + * @return \Drupal\commerce_product\Entity\ProductInterface + * The product. + */ + protected function generateThreeByTwoOptionalScenario() { + $product = Product::create([ + 'type' => 'default', + 'title' => $this->randomMachineName(), + 'stores' => [$this->store], + 'variations' => [], + ]); + $attribute_values_matrix = [ + ['8gb', '1tb', ''], + ['16gb', '1tb', ''], + ['16gb', '1tb', '1tb'], + ]; + $variations = []; + foreach ($attribute_values_matrix as $key => $value) { + $variation = ProductVariation::create([ + 'type' => 'default', + 'sku' => $this->randomMachineName(), + 'price' => [ + 'number' => 999, + 'currency_code' => 'USD', + ], + 'attribute_ram' => $this->ramAttributes[$value[0]], + 'attribute_disk1' => $this->disk1Attributes[$value[1]], + 'attribute_disk2' => isset($this->disk2Attributes[$value[2]]) ? $this->disk2Attributes[$value[2]] : NULL, + ]); + $variation->save(); + $variations[] = $variation; + $product->addVariation($variation); + } + $product->save(); + + return $product; + } + + /** + * Creates an attribute field and set of attribute values. + * + * @param \Drupal\commerce_product\Entity\ProductVariationTypeInterface $variation_type + * The variation type. + * @param string $name + * The attribute field name. + * @param array $options + * Associative array of key name values. [red => Red]. + * + * @return \Drupal\commerce_product\Entity\ProductAttributeValueInterface[] + * Array of attribute entities. + */ + protected function createAttributeSet(ProductVariationTypeInterface $variation_type, $name, array $options) { + $attribute = ProductAttribute::create([ + 'id' => $name, + 'label' => ucfirst($name), + ]); + $attribute->save(); + $this->attributeFieldManager->createField($attribute, $variation_type->id()); + + $attribute_set = []; + foreach ($options as $key => $value) { + $attribute_set[$key] = $this->createAttributeValue($name, $value); + } + + return $attribute_set; + } + + /** + * Creates an attribute value. + * + * @param string $attribute + * The attribute ID. + * @param string $name + * The attribute value name. + * + * @return \Drupal\commerce_product\Entity\ProductAttributeValueInterface + * The attribute value entity. + */ + protected function createAttributeValue($attribute, $name) { + $attribute_value = ProductAttributeValue::create([ + 'attribute' => $attribute, + 'name' => $name, + ]); + $attribute_value->save(); + + return $attribute_value; + } + +} From 8565b2ffdbcd86b9a45a0f9194b88680f7c7d44b Mon Sep 17 00:00:00 2001 From: Steve Oliver Date: Thu, 29 Sep 2016 10:52:42 -0700 Subject: [PATCH 103/103] Issue #2808813: New user account should own previous guest checkout order(s). --- modules/order/commerce_order.module | 37 +++++++++++++++++++ .../tests/src/Functional/OrderAccountTest.php | 33 +++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 modules/order/tests/src/Functional/OrderAccountTest.php diff --git a/modules/order/commerce_order.module b/modules/order/commerce_order.module index 195092dbc8..8a7e95ccb4 100644 --- a/modules/order/commerce_order.module +++ b/modules/order/commerce_order.module @@ -5,11 +5,14 @@ * Defines the Order entity and associated features. */ +use Drupal\commerce_order\Entity\Order; +use Drupal\commerce_order\Entity\OrderInterface; use Drupal\commerce_order\Entity\OrderTypeInterface; use Drupal\commerce_order\Plugin\Field\FieldFormatter\PriceCalculatedFormatter; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; use Drupal\entity\BundleFieldDefinition; +use Drupal\Core\Session\AccountInterface; /** * Implements hook_theme(). @@ -68,6 +71,40 @@ function commerce_order_field_formatter_info_alter(array &$info) { $info['commerce_price_calculated']['provider'] = 'commerce_order'; } +/** + * Implements hook_ENTITY_TYPE_insert() for user entities. + * + * - Assigns any previous guest orders to new user account. + */ +function commerce_order_user_insert(AccountInterface $user) { + $order_ids = \Drupal::entityQuery('commerce_order') + ->condition('mail', $user->getEmail()) + ->execute(); + if ($order_ids) { + $orders = Order::loadMultiple($order_ids); + foreach ($orders as $order) { + $order->setCustomer($user); + $order->save(); + } + } +} + +/** + * Implements hook_ENTITY_TYPE_presave(). + */ +function commerce_order_commerce_order_presave(OrderInterface $order) { + $place_transition = $order->getState()->getWorkflow()->getTransition('place'); + if (!$order->isNew() && empty($order->getPlacedTime()) && $place_transition) { + $from_states = $place_transition->getFromStates(); + $from_state = reset($from_states); + $from_state = $from_state->getId(); + $to_state = $place_transition->getToState()->getId(); + if ($order->original->state->value == $from_state && $order->state->value == $to_state) { + $order->setPlacedTime(REQUEST_TIME); + } + } +} + /** * Implements hook_field_widget_form_alter(). * diff --git a/modules/order/tests/src/Functional/OrderAccountTest.php b/modules/order/tests/src/Functional/OrderAccountTest.php new file mode 100644 index 0000000000..c965e56e46 --- /dev/null +++ b/modules/order/tests/src/Functional/OrderAccountTest.php @@ -0,0 +1,33 @@ +createEntity('commerce_order_item', [ + 'type' => 'product_variation', + ]); + $order = $this->createEntity('commerce_order', [ + 'type' => 'default', + 'uid' => 0, + 'mail' => 'guest@example.com', + 'order_items' => [$order_item], + ]); + $this->assertEmpty($order->getOwnerId(), 'The guest order has no owner account.'); + $user = $this->createUser([], 'guest'); + $order = Order::load($order->id()); + $this->assertEquals($user->id(), $order->getOwnerId(), 'New user account owns previous guest order.'); + } + +}