diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php
index 98cfca23c7..774d9c366b 100644
--- a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php
+++ b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowBase.php
@@ -246,11 +246,17 @@ public function getFormId() {
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $step_id = NULL) {
- // The $step_id argument is optional only because PHP disallows adding required
- // arguments to an existing interface's method.
+ // The $step_id argument is optional only because PHP disallows adding
+ // required arguments to an existing interface's method.
if (empty($step_id)) {
throw new \InvalidArgumentException('The $step_id cannot be empty.');
}
+ if ($form_state->isRebuilding()) {
+ // Ensure a fresh order, in case an ajax submit has modified it.
+ $order_storage = $this->entityTypeManager->getStorage('commerce_order');
+ $this->order = $order_storage->load($this->order->id());
+ }
+
$steps = $this->getVisibleSteps();
$form['#tree'] = TRUE;
$form['#step_id'] = $step_id;
diff --git a/modules/promotion/src/Element/CouponRedemptionForm.php b/modules/promotion/src/Element/CouponRedemptionForm.php
index 42b7683818..2d4798c879 100644
--- a/modules/promotion/src/Element/CouponRedemptionForm.php
+++ b/modules/promotion/src/Element/CouponRedemptionForm.php
@@ -6,21 +6,27 @@
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_promotion\Entity\CouponInterface;
use Drupal\commerce_promotion\Entity\PromotionInterface;
+use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
/**
- * Provides a form element for embedding the coupon redemption form.
+ * Provides a form element for redeeming a coupon.
*
* Usage example:
* @code
- * $form['coupons'] = [
+ * $form['coupon'] = [
* '#type' => 'commerce_coupon_redemption_form',
- * // The order to which the coupon will be applied to.
- * '#order' => $order,
+ * '#title' => t('Coupon code'),
+ * '#default_value' => $coupon_id,
+ * '#order_id' => $order_id,
* ];
* @endcode
+ * The element value ($form_state->getValue('coupon')) will be the
+ * coupon ID. Note that the order is not saved if the element was
+ * submitted as a result of the main form being submitted. It is the
+ * responsibility of the caller to update the order in that case.
*
- * @RenderElement("commerce_coupon_redemption_form")
+ * @FormElement("commerce_coupon_redemption_form")
*/
class CouponRedemptionForm extends CommerceElementBase {
@@ -30,22 +36,20 @@ class CouponRedemptionForm extends CommerceElementBase {
public function getInfo() {
$class = get_class($this);
return [
- // The order to which the coupon will be applied to.
- '#order' => NULL,
'#title' => t('Coupon code'),
'#description' => t('Enter your coupon code here.'),
'#submit_title' => t('Apply coupon'),
+ '#submit_message' => t('Coupon applied'),
+ '#remove_title' => t('Remove coupon'),
+ // The coupon ID.
+ '#default_value' => NULL,
+ '#order_id' => NULL,
'#process' => [
- [$class, 'attachElementSubmit'],
[$class, 'processForm'],
],
'#element_validate' => [
- [$class, 'validateElementSubmit'],
[$class, 'validateForm'],
],
- '#commerce_element_submit' => [
- [$class, 'submitForm'],
- ],
'#theme_wrappers' => ['container'],
];
}
@@ -61,26 +65,28 @@ public function getInfo() {
* The complete form structure.
*
* @throws \InvalidArgumentException
- * Thrown when the #order property is empty or invalid entity.
+ * Thrown when the #order_id property is empty or invalid.
*
* @return array
* The processed form element.
*/
public static function processForm(array $element, FormStateInterface $form_state, array &$complete_form) {
- if (empty($element['#order'])) {
- throw new \InvalidArgumentException('The commerce_coupon_redemption_form element requires the #order property.');
+ if (empty($element['#order_id'])) {
+ throw new \InvalidArgumentException('The commerce_coupon_redemption_form element requires the #order_id property.');
}
- if (!$element['#order'] instanceof OrderInterface) {
- throw new \InvalidArgumentException('The commerce_coupon_redemption_form #order property must be an order entity.');
+ $order_storage = \Drupal::entityTypeManager()->getStorage('commerce_order');
+ $order = $order_storage->load($element['#order_id']);
+ if (!$order instanceof OrderInterface) {
+ throw new \InvalidArgumentException('The commerce_coupon_redemption #order_id must be a valid order ID.');
}
+ $has_coupons = !$order->get('coupons')->isEmpty();
$id_prefix = implode('-', $element['#parents']);
// @todo We cannot use unique IDs, or multiple elements on a page currently.
// @see https://www.drupal.org/node/2675688
// $wrapper_id = Html::getUniqueId($id_prefix . '-ajax-wrapper');
$wrapper_id = $id_prefix . '-ajax-wrapper';
- $form_state->set('order', $element['#order']);
$element = [
'#tree' => TRUE,
'#prefix' => '
',
@@ -92,21 +98,96 @@ public static function processForm(array $element, FormStateInterface $form_stat
'#type' => 'textfield',
'#title' => $element['#title'],
'#description' => $element['#description'],
+ '#access' => !$has_coupons,
];
$element['apply'] = [
'#type' => 'submit',
'#value' => $element['#submit_title'],
'#name' => 'apply_coupon',
'#limit_validation_errors' => [
- array_merge($element['#parents'], ['code']),
+ $element['#parents'],
+ ],
+ '#submit' => [
+ [get_called_class(), 'applyCoupon'],
],
+ '#ajax' => [
+ 'callback' => [get_called_class(), 'ajaxRefresh'],
+ 'wrapper' => $element['#wrapper_id'],
+ ],
+ '#access' => !$has_coupons,
+ ];
+ $element['remove'] = [
+ '#type' => 'submit',
+ '#value' => $element['#remove_title'],
+ '#name' => 'remove_coupon',
+ '#ajax' => [
+ 'callback' => [get_called_class(), 'ajaxRefresh'],
+ 'wrapper' => $element['#wrapper_id'],
+ ],
+ '#weight' => 50,
+ '#limit_validation_errors' => [
+ $element['#parents'],
+ ],
+ '#submit' => [
+ [get_called_class(), 'removeCoupon'],
+ ],
+ '#access' => $has_coupons,
];
return $element;
}
/**
- * Validates the coupon redemption form.
+ * Ajax callback.
+ */
+ public static function ajaxRefresh(array $form, FormStateInterface $form_state) {
+ $parents = $form_state->getTriggeringElement()['#parents'];
+ array_pop($parents);
+ return NestedArray::getValue($form, $parents);
+ }
+
+ /**
+ * Apply coupon submit callback.
+ */
+ public static function applyCoupon(array $form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ $parents = $triggering_element['#parents'];
+ array_pop($parents);
+ $element = NestedArray::getValue($form, $parents);
+
+ $entity_type_manager = \Drupal::entityTypeManager();
+ $order_storage = $entity_type_manager->getStorage('commerce_order');
+ /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
+ $order = $order_storage->load($element['#order_id']);
+
+ $coupon = $form_state->getValue($parents);
+ $order->get('coupons')->appendItem($coupon);
+ $order->save();
+ $form_state->setRebuild();
+ drupal_set_message($element['#submit_message']);
+ }
+
+ /**
+ * Remove coupon submit callback.
+ */
+ public static function removeCoupon(array $form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ $parents = $triggering_element['#parents'];
+ array_pop($parents);
+ $element = NestedArray::getValue($form, $parents);
+
+ $entity_type_manager = \Drupal::entityTypeManager();
+ $order_storage = $entity_type_manager->getStorage('commerce_order');
+ /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
+ $order = $order_storage->load($element['#order_id']);
+
+ $order->get('coupons')->setValue([]);
+ $order->save();
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Validates the coupon redemption element.
*
* @param array $element
* The form element.
@@ -119,11 +200,12 @@ public static function validateForm(array &$element, FormStateInterface $form_st
if (empty($coupon_code)) {
return;
}
- $code_path = implode('][', $coupon_parents);
- /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
- $order = $form_state->get('order');
$entity_type_manager = \Drupal::entityTypeManager();
+ $code_path = implode('][', $coupon_parents);
+ $order_storage = $entity_type_manager->getStorage('commerce_order');
+ /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
+ $order = $order_storage->load($element['#order_id']);
/** @var \Drupal\commerce_promotion\CouponStorageInterface $coupon_storage */
$coupon_storage = $entity_type_manager->getStorage('commerce_promotion_coupon');
$coupon = $coupon_storage->loadByCode($coupon_code);
@@ -131,7 +213,6 @@ public static function validateForm(array &$element, FormStateInterface $form_st
$form_state->setErrorByName($code_path, t('Coupon is invalid'));
return;
}
-
foreach ($order->get('coupons') as $item) {
if ($item->target_id == $coupon->id()) {
$form_state->setErrorByName($code_path, t('Coupon has already been redeemed'));
@@ -142,7 +223,6 @@ public static function validateForm(array &$element, FormStateInterface $form_st
$order_type_storage = $entity_type_manager->getStorage('commerce_order_type');
/** @var \Drupal\commerce_promotion\PromotionStorageInterface $promotion_storage */
$promotion_storage = $entity_type_manager->getStorage('commerce_promotion');
-
/** @var \Drupal\commerce_order\Entity\OrderTypeInterface $order_type */
$order_type = $order_type_storage->load($order->bundle());
$promotion = $promotion_storage->loadByCoupon($order_type, $order->getStore(), $coupon);
@@ -150,47 +230,16 @@ public static function validateForm(array &$element, FormStateInterface $form_st
$form_state->setErrorByName($code_path, t('Coupon is invalid'));
return;
}
-
if (!self::couponApplies($order, $promotion, $coupon)) {
$form_state->setErrorByName($code_path, t('Coupon is invalid'));
return;
}
- }
-
- /**
- * Submits the coupon redemption form.
- *
- * @param array $element
- * The form element.
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- * The current state of the form.
- */
- public static function submitForm(array &$element, FormStateInterface $form_state) {
- $coupon_parents = array_merge($element['#parents'], ['code']);
- $coupon_code = $form_state->getValue($coupon_parents);
- /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
- $order = $form_state->get('order');
- $entity_type_manager = \Drupal::entityTypeManager();
- $order_type_storage = $entity_type_manager->getStorage('commerce_order_type');
- /** @var \Drupal\commerce_promotion\PromotionStorageInterface $promotion_storage */
- $promotion_storage = $entity_type_manager->getStorage('commerce_promotion');
- /** @var \Drupal\commerce_promotion\CouponStorageInterface $coupon_storage */
- $coupon_storage = $entity_type_manager->getStorage('commerce_promotion_coupon');
- $coupon = $coupon_storage->loadByCode($coupon_code);
- /** @var \Drupal\commerce_order\Entity\OrderTypeInterface $order_type */
- $order_type = $order_type_storage->load($order->bundle());
- $promotion = $promotion_storage->loadByCoupon($order_type, $order->getStore(), $coupon);
-
- if (self::couponApplies($order, $promotion, $coupon)) {
- $order->get('coupons')->appendItem($coupon);
- $order->save();
- drupal_set_message(t('Coupon applied'));
- }
+ $form_state->setValueForElement($element, $coupon);
}
/**
- * Checks if a coupon applies.
+ * Checks whether a coupon applies.
*
* @param \Drupal\commerce_order\Entity\OrderInterface $order
* The order.
diff --git a/modules/promotion/tests/modules/commerce_promotion_test/commerce_promotion_test.module b/modules/promotion/tests/modules/commerce_promotion_test/commerce_promotion_test.module
index 531a063800..ea6554a826 100644
--- a/modules/promotion/tests/modules/commerce_promotion_test/commerce_promotion_test.module
+++ b/modules/promotion/tests/modules/commerce_promotion_test/commerce_promotion_test.module
@@ -13,13 +13,13 @@ use Drupal\Core\Form\FormStateInterface;
function commerce_promotion_test_form_views_form_commerce_cart_form_default_alter(&$form, FormStateInterface $form_state, $form_id) {
// We know that view forms are build on the base ID plus arguments.
$order_id = substr($form_id, strlen('views_form_commerce_cart_form_default_'));
- $order = \Drupal::entityTypeManager()->getStorage('commerce_order')->load($order_id);
$form['coupons'] = [
'#type' => 'commerce_coupon_redemption_form',
- '#order' => $order,
+ '#order_id' => $order_id,
'#title' => t('Promotion code'),
'#description' => t('Enter your promotion code to redeem a discount.'),
'#submit_title' => t('Apply'),
+ '#remove_title' => t('Remove promotion'),
];
}
diff --git a/modules/promotion/tests/src/Functional/CouponRedemptionTest.php b/modules/promotion/tests/src/Functional/CouponRedemptionTest.php
index 6e95e3cf96..010e75ee3e 100644
--- a/modules/promotion/tests/src/Functional/CouponRedemptionTest.php
+++ b/modules/promotion/tests/src/Functional/CouponRedemptionTest.php
@@ -125,11 +125,15 @@ public function testCouponRedemption() {
$this->getSession()->getPage()->fillField('coupons[code]', $existing_coupon->getCode());
$this->getSession()->getPage()->pressButton('Apply');
+
$this->assertSession()->pageTextContains('Coupon applied');
- $this->getSession()->getPage()->fillField('coupons[code]', $existing_coupon->getCode());
- $this->getSession()->getPage()->pressButton('Apply');
- $this->assertSession()->pageTextContains('Coupon has already been redeemed');
+ $this->assertSession()->fieldNotExists('coupons[code]');
+ $this->assertSession()->buttonNotExists('Apply');
+ $this->getSession()->getPage()->pressButton('Remove promotion');
+
+ $this->assertSession()->fieldExists('coupons[code]');
+ $this->assertSession()->buttonExists('Apply');
}
}