Skip to content

Commit ea01f37

Browse files
authored
Issue #2855762 by mglaman: Create a coupon redemption element
1 parent 7ca6964 commit ea01f37

5 files changed

Lines changed: 379 additions & 1 deletion

File tree

modules/cart/src/Plugin/views/field/EditQuantity.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public function viewsForm(array &$form, FormStateInterface $form_state) {
115115
* The current state of the form.
116116
*/
117117
public function viewsFormSubmit(array &$form, FormStateInterface $form_state) {
118-
$quantities = $form_state->getValue($this->options['id']);
118+
$quantities = $form_state->getValue($this->options['id'], []);
119119
foreach ($quantities as $row_index => $quantity) {
120120
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
121121
$order_item = $this->getEntity($this->view->result[$row_index]);
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
<?php
2+
3+
namespace Drupal\commerce_promotion\Element;
4+
5+
use Drupal\commerce\Element\CommerceElementBase;
6+
use Drupal\commerce_order\Entity\OrderInterface;
7+
use Drupal\commerce_promotion\Entity\CouponInterface;
8+
use Drupal\commerce_promotion\Entity\PromotionInterface;
9+
use Drupal\Core\Form\FormStateInterface;
10+
11+
/**
12+
* Provides a form element for embedding the coupon redemption form.
13+
*
14+
* Usage example:
15+
* @code
16+
* $form['coupons'] = [
17+
* '#type' => 'commerce_coupon_redemption_form',
18+
* // The order to which the coupon will be applied to.
19+
* '#order' => $order,
20+
* ];
21+
* @endcode
22+
*
23+
* @RenderElement("commerce_coupon_redemption_form")
24+
*/
25+
class CouponRedemptionForm extends CommerceElementBase {
26+
27+
/**
28+
* {@inheritdoc}
29+
*/
30+
public function getInfo() {
31+
$class = get_class($this);
32+
return [
33+
// The order to which the coupon will be applied to.
34+
'#order' => NULL,
35+
'#process' => [
36+
[$class, 'attachElementSubmit'],
37+
[$class, 'processForm'],
38+
],
39+
'#element_validate' => [
40+
[$class, 'validateElementSubmit'],
41+
[$class, 'validateForm'],
42+
],
43+
'#commerce_element_submit' => [
44+
[$class, 'submitForm'],
45+
],
46+
'#theme_wrappers' => ['container'],
47+
];
48+
}
49+
50+
/**
51+
* Builds the coupon redemption form.
52+
*
53+
* @param array $element
54+
* The form element being processed.
55+
* @param \Drupal\Core\Form\FormStateInterface $form_state
56+
* The current state of the form.
57+
* @param array $complete_form
58+
* The complete form structure.
59+
*
60+
* @throws \InvalidArgumentException
61+
* Thrown when the #order property is empty or invalid entity.
62+
*
63+
* @return array
64+
* The processed form element.
65+
*/
66+
public static function processForm(array $element, FormStateInterface $form_state, array &$complete_form) {
67+
if (empty($element['#order'])) {
68+
throw new \InvalidArgumentException('The commerce_coupon_redemption_form element requires the #order property.');
69+
}
70+
if (!$element['#order'] instanceof OrderInterface) {
71+
throw new \InvalidArgumentException('The commerce_coupon_redemption_form #order property must be an order entity.');
72+
}
73+
74+
$id_prefix = implode('-', $element['#parents']);
75+
// @todo We cannot use unique IDs, or multiple elements on a page currently.
76+
// @see https://www.drupal.org/node/2675688
77+
// $wrapper_id = Html::getUniqueId($id_prefix . '-ajax-wrapper');
78+
$wrapper_id = $id_prefix . '-ajax-wrapper';
79+
80+
$form_state->set('order', $element['#order']);
81+
$element = [
82+
'#tree' => TRUE,
83+
'#prefix' => '<div id="' . $wrapper_id . '">',
84+
'#suffix' => '</div>',
85+
// Pass the id along to other methods.
86+
'#wrapper_id' => $wrapper_id,
87+
] + $element;
88+
$element['code'] = [
89+
'#type' => 'textfield',
90+
'#title' => t('Coupon code'),
91+
'#description' => t('Enter your coupon code here.'),
92+
];
93+
$element['apply'] = [
94+
'#type' => 'submit',
95+
'#value' => t('Apply coupon'),
96+
'#name' => 'apply_coupon',
97+
'#limit_validation_errors' => [
98+
array_merge($element['#parents'], ['code']),
99+
],
100+
];
101+
102+
return $element;
103+
}
104+
105+
/**
106+
* Validates the coupon redemption form.
107+
*
108+
* @param array $element
109+
* The form element.
110+
* @param \Drupal\Core\Form\FormStateInterface $form_state
111+
* The current state of the form.
112+
*/
113+
public static function validateForm(array &$element, FormStateInterface $form_state) {
114+
$coupon_parents = array_merge($element['#parents'], ['code']);
115+
$coupon_code = $form_state->getValue($coupon_parents);
116+
if (empty($coupon_code)) {
117+
return;
118+
}
119+
$code_path = implode('][', $coupon_parents);
120+
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
121+
$order = $form_state->get('order');
122+
$entity_type_manager = \Drupal::entityTypeManager();
123+
124+
/** @var \Drupal\commerce_promotion\CouponStorageInterface $coupon_storage */
125+
$coupon_storage = $entity_type_manager->getStorage('commerce_promotion_coupon');
126+
$coupon = $coupon_storage->loadByCode($coupon_code);
127+
if (empty($coupon)) {
128+
$form_state->setErrorByName($code_path, t('Coupon is invalid'));
129+
return;
130+
}
131+
132+
foreach ($order->get('coupons') as $item) {
133+
if ($item->target_id == $coupon->id()) {
134+
$form_state->setErrorByName($code_path, t('Coupon has already been redeemed'));
135+
return;
136+
}
137+
}
138+
139+
$order_type_storage = $entity_type_manager->getStorage('commerce_order_type');
140+
/** @var \Drupal\commerce_promotion\PromotionStorageInterface $promotion_storage */
141+
$promotion_storage = $entity_type_manager->getStorage('commerce_promotion');
142+
143+
/** @var \Drupal\commerce_order\Entity\OrderTypeInterface $order_type */
144+
$order_type = $order_type_storage->load($order->bundle());
145+
$promotion = $promotion_storage->loadByCoupon($order_type, $order->getStore(), $coupon);
146+
if (empty($promotion)) {
147+
$form_state->setErrorByName($code_path, t('Coupon is invalid'));
148+
return;
149+
}
150+
151+
if (!self::couponApplies($order, $promotion, $coupon)) {
152+
$form_state->setErrorByName($code_path, t('Coupon is invalid'));
153+
return;
154+
}
155+
}
156+
157+
/**
158+
* Submits the coupon redemption form.
159+
*
160+
* @param array $element
161+
* The form element.
162+
* @param \Drupal\Core\Form\FormStateInterface $form_state
163+
* The current state of the form.
164+
*/
165+
public static function submitForm(array &$element, FormStateInterface $form_state) {
166+
$coupon_parents = array_merge($element['#parents'], ['code']);
167+
$coupon_code = $form_state->getValue($coupon_parents);
168+
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
169+
$order = $form_state->get('order');
170+
$entity_type_manager = \Drupal::entityTypeManager();
171+
$order_type_storage = $entity_type_manager->getStorage('commerce_order_type');
172+
/** @var \Drupal\commerce_promotion\PromotionStorageInterface $promotion_storage */
173+
$promotion_storage = $entity_type_manager->getStorage('commerce_promotion');
174+
/** @var \Drupal\commerce_promotion\CouponStorageInterface $coupon_storage */
175+
$coupon_storage = $entity_type_manager->getStorage('commerce_promotion_coupon');
176+
177+
$coupon = $coupon_storage->loadByCode($coupon_code);
178+
/** @var \Drupal\commerce_order\Entity\OrderTypeInterface $order_type */
179+
$order_type = $order_type_storage->load($order->bundle());
180+
$promotion = $promotion_storage->loadByCoupon($order_type, $order->getStore(), $coupon);
181+
182+
if (self::couponApplies($order, $promotion, $coupon)) {
183+
$order->get('coupons')->appendItem($coupon);
184+
$order->save();
185+
drupal_set_message(t('Coupon applied'));
186+
}
187+
}
188+
189+
/**
190+
* Checks if a coupon applies.
191+
*
192+
* @param \Drupal\commerce_order\Entity\OrderInterface $order
193+
* The order.
194+
* @param \Drupal\commerce_promotion\Entity\PromotionInterface $promotion
195+
* The promotion.
196+
* @param \Drupal\commerce_promotion\Entity\CouponInterface $coupon
197+
* The coupon.
198+
*
199+
* @return bool
200+
* Returns TRUE if the coupon applies, FALSE otherwise.
201+
*/
202+
protected static function couponApplies(OrderInterface $order, PromotionInterface $promotion, CouponInterface $coupon) {
203+
if ($promotion->applies($order)) {
204+
return TRUE;
205+
}
206+
else {
207+
foreach ($order->getItems() as $orderItem) {
208+
if ($promotion->applies($orderItem)) {
209+
return TRUE;
210+
}
211+
}
212+
}
213+
214+
return FALSE;
215+
}
216+
217+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: Commerce Promotion Test
2+
type: module
3+
description: Provides items for testing Commerce Promotion.
4+
package: Testing
5+
core: 8.x
6+
dependencies:
7+
- commerce_cart
8+
- commerce_promotion
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* Provides items for testing Commerce Promotion.
6+
*/
7+
8+
/**
9+
* Implements hook_form_BASE_FORM_ID_alter().
10+
*/
11+
function commerce_promotion_test_form_views_form_commerce_cart_form_default_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
12+
// We know that view forms are build on the base ID plus arguments.
13+
$order_id = substr($form_id, strlen('views_form_commerce_cart_form_default_'));
14+
$order = \Drupal::entityTypeManager()->getStorage('commerce_order')->load($order_id);
15+
16+
$form['coupons'] = [
17+
'#type' => 'commerce_coupon_redemption_form',
18+
'#order' => $order,
19+
];
20+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
namespace Drupal\Tests\commerce_promotion\Functional;
4+
5+
use Drupal\Core\Url;
6+
use Drupal\Tests\commerce\Functional\CommerceBrowserTestBase;
7+
8+
/**
9+
* Tests the coupon redemption form element.
10+
*
11+
* @group commerce
12+
*/
13+
class CouponRedemptionTest extends CommerceBrowserTestBase {
14+
15+
/**
16+
* The cart order to test against.
17+
*
18+
* @var \Drupal\commerce_order\Entity\OrderInterface
19+
*/
20+
protected $cart;
21+
22+
/**
23+
* The cart manager.
24+
*
25+
* @var \Drupal\commerce_cart\CartManagerInterface
26+
*/
27+
protected $cartManager;
28+
29+
/**
30+
* The variation to test against.
31+
*
32+
* @var \Drupal\commerce_product\Entity\ProductVariation
33+
*/
34+
protected $variation;
35+
36+
/**
37+
* The promotion for testing.
38+
*
39+
* @var \Drupal\commerce_promotion\Entity\PromotionInterface
40+
*/
41+
protected $promotion;
42+
43+
/**
44+
* Modules to enable.
45+
*
46+
* @var array
47+
*/
48+
public static $modules = [
49+
'block',
50+
'commerce_cart',
51+
'commerce_promotion',
52+
'commerce_promotion_test',
53+
];
54+
55+
/**
56+
* {@inheritdoc}
57+
*/
58+
protected function setUp() {
59+
parent::setUp();
60+
61+
$this->cart = $this->container->get('commerce_cart.cart_provider')->createCart('default', $this->store, $this->adminUser);
62+
$this->cartManager = $this->container->get('commerce_cart.cart_manager');
63+
64+
// Create a product variation.
65+
$this->variation = $this->createEntity('commerce_product_variation', [
66+
'type' => 'default',
67+
'sku' => $this->randomMachineName(),
68+
'price' => [
69+
'number' => 999,
70+
'currency_code' => 'USD',
71+
],
72+
]);
73+
$this->cartManager->addEntity($this->cart, $this->variation);
74+
75+
// We need a product too otherwise tests complain about the missing
76+
// backreference.
77+
$this->createEntity('commerce_product', [
78+
'type' => 'default',
79+
'title' => $this->randomMachineName(),
80+
'stores' => [$this->store],
81+
'variations' => [$this->variation],
82+
]);
83+
84+
// Starts now, enabled. No end time.
85+
$this->promotion = $this->createEntity('commerce_promotion', [
86+
'name' => 'Promotion (with coupon)',
87+
'order_types' => ['default'],
88+
'stores' => [$this->store->id()],
89+
'status' => TRUE,
90+
'offer' => [
91+
'target_plugin_id' => 'commerce_promotion_order_percentage_off',
92+
'target_plugin_configuration' => [
93+
'amount' => '0.10',
94+
],
95+
],
96+
'conditions' => [],
97+
]);
98+
99+
$coupon = $this->createEntity('commerce_promotion_coupon', [
100+
'code' => $this->randomString(),
101+
'status' => TRUE,
102+
]);
103+
$coupon->save();
104+
$this->promotion->get('coupons')->appendItem($coupon);
105+
$this->promotion->save();
106+
}
107+
108+
/**
109+
* Tests redeeming coupon on the cart form.
110+
*
111+
* @see commerce_promotion_test_form_views_form_commerce_cart_form_default_alter
112+
*/
113+
public function testCouponRedemption() {
114+
$this->drupalGet(Url::fromRoute('commerce_cart.page'));
115+
116+
/** @var \Drupal\commerce_promotion\Entity\CouponInterface $existing_coupon */
117+
$existing_coupon = $this->promotion->get('coupons')->first()->entity;
118+
119+
// Test entering an invalid coupon.
120+
$this->getSession()->getPage()->fillField('Coupon code', $this->randomString());
121+
$this->getSession()->getPage()->pressButton('Apply coupon');
122+
$this->assertSession()->pageTextContains('Coupon is invalid');
123+
124+
$this->getSession()->getPage()->fillField('coupons[code]', $existing_coupon->getCode());
125+
$this->getSession()->getPage()->pressButton('Apply coupon');
126+
$this->assertSession()->pageTextContains('Coupon applied');
127+
128+
$this->getSession()->getPage()->fillField('coupons[code]', $existing_coupon->getCode());
129+
$this->getSession()->getPage()->pressButton('Apply coupon');
130+
$this->assertSession()->pageTextContains('Coupon has already been redeemed');
131+
}
132+
133+
}

0 commit comments

Comments
 (0)