Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
165 changes: 107 additions & 58 deletions modules/promotion/src/Element/CouponRedemptionForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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'],
];
}
Expand All @@ -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' => '<div id="' . $wrapper_id . '">',
Expand All @@ -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.
Expand All @@ -119,19 +200,19 @@ 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);
if (empty($coupon)) {
$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'));
Expand All @@ -142,55 +223,23 @@ 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);
if (empty($promotion)) {
$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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
];
}
10 changes: 7 additions & 3 deletions modules/promotion/tests/src/Functional/CouponRedemptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

}