Skip to content

Commit 9bddff6

Browse files
committed
Issue #2862945 by mglaman: Prevent a promotion from applying if other promotions present
1 parent 1c07f4f commit 9bddff6

File tree

6 files changed

+249
-12
lines changed

6 files changed

+249
-12
lines changed

modules/promotion/commerce_promotion.info.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ description: 'Provides a UI for managing promotions.'
44
package: Commerce
55
core: 8.x
66
dependencies:
7+
- options
78
- inline_entity_form
89
- commerce
910
- commerce:commerce_order

modules/promotion/commerce_promotion.post_update.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Post update functions for Promotion.
66
*/
77

8+
use Drupal\commerce_promotion\Entity\PromotionInterface;
89
use Drupal\Core\Field\BaseFieldDefinition;
910

1011
/**
@@ -56,3 +57,30 @@ function commerce_promotion_post_update_3() {
5657
}
5758
$coupon_storage->delete($delete_coupons);
5859
}
60+
61+
/**
62+
* Add the compatibility field to promotions.
63+
*/
64+
function commerce_promotion_post_update_4() {
65+
$storage_definition = BaseFieldDefinition::create('list_string')
66+
->setLabel(t('Compatibility with other promotions'))
67+
->setSetting('allowed_values_function', ['\Drupal\commerce_promotion\Entity\Promotion', 'getCompatibilityOptions'])
68+
->setRequired(TRUE)
69+
->setDefaultValue(PromotionInterface::COMPATIBLE_ANY)
70+
->setDisplayOptions('form', [
71+
'type' => 'options_select',
72+
'weight' => 4,
73+
]);
74+
75+
$entity_definition_update = \Drupal::entityDefinitionUpdateManager();
76+
$entity_definition_update->installFieldStorageDefinition('compatibility', 'commerce_promotion', 'commerce_promotion', $storage_definition);
77+
78+
/** @var \Drupal\commerce_promotion\PromotionStorageInterface $promotion_storage */
79+
$promotion_storage = \Drupal::service('entity_type.manager')->getStorage('commerce_promotion');
80+
/** @var \Drupal\commerce_promotion\Entity\PromotionInterface[] $promotions */
81+
$promotions = $promotion_storage->loadMultiple();
82+
foreach ($promotions as $promotion) {
83+
$promotion->setCompatibility(PromotionInterface::COMPATIBLE_ANY);
84+
$promotion->save();
85+
}
86+
}

modules/promotion/src/Entity/Promotion.php

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace Drupal\commerce_promotion\Entity;
44

5+
use Drupal\commerce_order\EntityAdjustableInterface;
56
use Drupal\Core\Datetime\DrupalDateTime;
67
use Drupal\Core\Entity\ContentEntityBase;
7-
use Drupal\Core\Entity\EntityInterface;
88
use Drupal\Core\Entity\EntityStorageInterface;
99
use Drupal\Core\Entity\EntityTypeInterface;
1010
use Drupal\Core\Field\BaseFieldDefinition;
@@ -300,6 +300,24 @@ public function setEndDate(DrupalDateTime $end_date) {
300300
return $this;
301301
}
302302

303+
/**
304+
* {@inheritdoc}
305+
*/
306+
public function getCompatibility() {
307+
return $this->get('compatibility')->value;
308+
}
309+
310+
/**
311+
* {@inheritdoc}
312+
*/
313+
public function setCompatibility($compatibility) {
314+
if (!in_array($compatibility, [self::COMPATIBLE_NONE, self::COMPATIBLE_ANY])) {
315+
throw new \InvalidArgumentException('Invalid compatibility type');
316+
}
317+
$this->get('compatibility')->value = $compatibility;
318+
return $this;
319+
}
320+
303321
/**
304322
* {@inheritdoc}
305323
*/
@@ -333,7 +351,7 @@ public function setWeight($weight) {
333351
/**
334352
* {@inheritdoc}
335353
*/
336-
public function applies(EntityInterface $entity) {
354+
public function applies(EntityAdjustableInterface $entity) {
337355
$entity_type_id = $entity->getEntityTypeId();
338356

339357
/** @var \Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\PromotionOfferInterface $offer */
@@ -342,6 +360,22 @@ public function applies(EntityInterface $entity) {
342360
return FALSE;
343361
}
344362

363+
// Check compatibility.
364+
// @todo port remaining strategies from Commerce Discount #2762997.
365+
switch ($this->getCompatibility()) {
366+
case self::COMPATIBLE_NONE:
367+
// If there are any existing promotions, then this cannot apply.
368+
foreach ($entity->getAdjustments() as $adjustment) {
369+
if ($adjustment->getType() == 'promotion') {
370+
return FALSE;
371+
}
372+
}
373+
break;
374+
375+
case self::COMPATIBLE_ANY:
376+
break;
377+
}
378+
345379
// @todo should whatever invokes this method be providing the context?
346380
$context = new Context(new ContextDefinition('entity:' . $entity_type_id), $entity);
347381

@@ -362,7 +396,7 @@ public function applies(EntityInterface $entity) {
362396
/**
363397
* {@inheritdoc}
364398
*/
365-
public function apply(EntityInterface $entity) {
399+
public function apply(EntityAdjustableInterface $entity) {
366400
$entity_type_id = $entity->getEntityTypeId();
367401
// @todo should whatever invokes this method be providing the context?
368402
$context = new Context(new ContextDefinition('entity:' . $entity_type_id), $entity);
@@ -539,6 +573,16 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
539573
'weight' => 6,
540574
]);
541575

576+
$fields['compatibility'] = BaseFieldDefinition::create('list_string')
577+
->setLabel(t('Compatibility with other promotions'))
578+
->setSetting('allowed_values_function', ['\Drupal\commerce_promotion\Entity\Promotion', 'getCompatibilityOptions'])
579+
->setRequired(TRUE)
580+
->setDefaultValue(self::COMPATIBLE_ANY)
581+
->setDisplayOptions('form', [
582+
'type' => 'options_buttons',
583+
'weight' => 4,
584+
]);
585+
542586
$fields['status'] = BaseFieldDefinition::create('boolean')
543587
->setLabel(t('Enabled'))
544588
->setDescription(t('Whether the promotion is enabled.'))
@@ -615,4 +659,17 @@ public static function sort(PromotionInterface $a, PromotionInterface $b) {
615659
return ($a_weight < $b_weight) ? -1 : 1;
616660
}
617661

662+
/**
663+
* Gets the allowed values for the 'compatibility' base field.
664+
*
665+
* @return array
666+
* The allowed values.
667+
*/
668+
public static function getCompatibilityOptions() {
669+
return [
670+
self::COMPATIBLE_ANY => t('Any promotion'),
671+
self::COMPATIBLE_NONE => t('Not with any other promotions'),
672+
];
673+
}
674+
618675
}

modules/promotion/src/Entity/PromotionInterface.php

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@
22

33
namespace Drupal\commerce_promotion\Entity;
44

5+
use Drupal\commerce_order\EntityAdjustableInterface;
56
use Drupal\commerce_store\Entity\EntityStoresInterface;
67
use Drupal\Core\Datetime\DrupalDateTime;
78
use Drupal\Core\Entity\ContentEntityInterface;
8-
use Drupal\Core\Entity\EntityInterface;
99

1010
/**
1111
* Defines the interface for promotions.
1212
*/
1313
interface PromotionInterface extends ContentEntityInterface, EntityStoresInterface {
1414

15+
const COMPATIBLE_ANY = 'any';
16+
const COMPATIBLE_NONE = 'none';
17+
1518
/**
1619
* Gets the promotion name.
1720
*
@@ -226,6 +229,24 @@ public function getEndDate();
226229
*/
227230
public function setEndDate(DrupalDateTime $end_date);
228231

232+
/**
233+
* Gets the promotion compatibility.
234+
*
235+
* @return string
236+
* The compatibility.
237+
*/
238+
public function getCompatibility();
239+
240+
/**
241+
* Sets the promotion compatibility.
242+
*
243+
* @param string $compatibility
244+
* The compatibility.
245+
*
246+
* @return $this
247+
*/
248+
public function setCompatibility($compatibility);
249+
229250
/**
230251
* Get whether the promotion is enabled.
231252
*
@@ -265,20 +286,20 @@ public function setWeight($weight);
265286
/**
266287
* Checks whether the promotion entity can be applied.
267288
*
268-
* @param \Drupal\Core\Entity\EntityInterface $entity
269-
* The entity.
289+
* @param \Drupal\commerce_order\EntityAdjustableInterface $entity
290+
* The adjustable entity.
270291
*
271292
* @return bool
272293
* TRUE if promotion can be applied, or false if conditions failed.
273294
*/
274-
public function applies(EntityInterface $entity);
295+
public function applies(EntityAdjustableInterface $entity);
275296

276297
/**
277298
* Apply the promotion to an entity.
278299
*
279-
* @param \Drupal\Core\Entity\EntityInterface $entity
280-
* The entity.
300+
* @param \Drupal\commerce_order\EntityAdjustableInterface $entity
301+
* The adjustable entity.
281302
*/
282-
public function apply(EntityInterface $entity);
303+
public function apply(EntityAdjustableInterface $entity);
283304

284305
}

modules/promotion/tests/src/Kernel/CouponValidationTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
use Drupal\commerce_promotion\Entity\Coupon;
66
use Drupal\Component\Render\FormattableMarkup;
7-
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
7+
use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
88

99
/**
1010
* Tests coupon validation constraints.
1111
*
1212
* @group commerce
1313
*/
14-
class CouponValidationTest extends EntityKernelTestBase {
14+
class CouponValidationTest extends CommerceKernelTestBase {
1515

1616
/**
1717
* Modules to enable.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
namespace Drupal\Tests\commerce_promotion\Kernel;
4+
5+
use Drupal\commerce_order\Entity\Order;
6+
use Drupal\commerce_order\Entity\OrderItemType;
7+
use Drupal\commerce_order\Entity\OrderType;
8+
use Drupal\commerce_price\Price;
9+
use Drupal\commerce_promotion\Entity\Promotion;
10+
use Drupal\commerce_promotion\Entity\PromotionInterface;
11+
use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
12+
13+
/**
14+
* Tests promotion compatibility options.
15+
*
16+
* @group commerce
17+
* @group commerce_promotion
18+
*/
19+
class PromotionCompatibilityTest extends CommerceKernelTestBase {
20+
21+
/**
22+
* The test order.
23+
*
24+
* @var \Drupal\commerce_order\Entity\OrderInterface
25+
*/
26+
protected $order;
27+
28+
/**
29+
* Modules to enable.
30+
*
31+
* @var array
32+
*/
33+
public static $modules = [
34+
'entity_reference_revisions',
35+
'profile',
36+
'state_machine',
37+
'commerce_order',
38+
'commerce_promotion',
39+
];
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
protected function setUp() {
45+
parent::setUp();
46+
47+
$this->installEntitySchema('profile');
48+
$this->installEntitySchema('commerce_order');
49+
$this->installEntitySchema('commerce_order_type');
50+
$this->installEntitySchema('commerce_promotion');
51+
$this->installEntitySchema('commerce_promotion_coupon');
52+
$this->installConfig([
53+
'profile',
54+
'commerce_order',
55+
'commerce_promotion',
56+
]);
57+
58+
// An order item type that doesn't need a purchasable entity, for simplicity.
59+
OrderItemType::create([
60+
'id' => 'test',
61+
'label' => 'Test',
62+
'orderType' => 'default',
63+
])->save();
64+
65+
$this->order = Order::create([
66+
'type' => 'default',
67+
'state' => 'completed',
68+
'mail' => '[email protected]',
69+
'ip_address' => '127.0.0.1',
70+
'order_number' => '6',
71+
'store_id' => $this->store,
72+
'order_items' => [],
73+
'total_price' => new Price('100.00', 'USD'),
74+
'uid' => $this->createUser()->id(),
75+
]);
76+
}
77+
78+
/**
79+
* Tests the compatibility setting.
80+
*/
81+
public function testCompatibility() {
82+
$order_type = OrderType::load('default');
83+
84+
// Starts now, enabled. No end time.
85+
$promotion1 = Promotion::create([
86+
'name' => 'Promotion 1',
87+
'order_types' => [$order_type],
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+
]);
97+
$this->assertEquals(SAVED_NEW, $promotion1->save());
98+
99+
$promotion2 = Promotion::create([
100+
'name' => 'Promotion 2',
101+
'order_types' => [$order_type],
102+
'stores' => [$this->store->id()],
103+
'status' => TRUE,
104+
'offer' => [
105+
'target_plugin_id' => 'commerce_promotion_order_percentage_off',
106+
'target_plugin_configuration' => [
107+
'amount' => '0.10',
108+
],
109+
],
110+
]);
111+
$this->assertEquals(SAVED_NEW, $promotion2->save());
112+
113+
$this->assertTrue($promotion1->applies($this->order));
114+
$this->assertTrue($promotion2->applies($this->order));
115+
116+
$promotion1->setWeight(-10);
117+
$promotion1->save();
118+
119+
$promotion2->setWeight(10);
120+
$promotion2->setCompatibility(PromotionInterface::COMPATIBLE_NONE);
121+
$promotion2->save();
122+
123+
$promotion1->apply($this->order);
124+
$this->assertFalse($promotion2->applies($this->order));
125+
126+
$this->container->get('commerce_order.order_refresh')->refresh($this->order);
127+
$this->assertEquals(1, count($this->order->collectAdjustments()));
128+
}
129+
130+
}

0 commit comments

Comments
 (0)