Skip to content

Commit 06497cd

Browse files
committed
Issue #2980700 by bojanz: Introduce offer conditions
1 parent f4c04e4 commit 06497cd

File tree

10 files changed

+360
-121
lines changed

10 files changed

+360
-121
lines changed

modules/promotion/commerce_promotion.module

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ use Drupal\Core\Field\BaseFieldDefinition;
1010
use Drupal\Core\Form\FormStateInterface;
1111
use Drupal\Core\Render\Element;
1212

13+
/**
14+
* Implements hook_commerce_condition_info_alter().
15+
*/
16+
function commerce_promotion_commerce_condition_info_alter(&$definitions) {
17+
foreach ($definitions as &$definition) {
18+
// Force all order item conditions to have the same category.
19+
// This prevents them from accidentally showing in vertical tabs
20+
// in the promotion offer UI.
21+
if ($definition['entity_type'] == 'commerce_order_item') {
22+
$definition['category'] = t('Products');
23+
}
24+
}
25+
}
26+
1327
/**
1428
* Implements hook_theme().
1529
*/

modules/promotion/commerce_promotion.post_update.php

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
use Drupal\commerce_promotion\Entity\PromotionInterface;
9+
use Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\OrderItemPromotionOfferInterface;
910
use Drupal\Core\Field\BaseFieldDefinition;
1011

1112
/**
@@ -233,3 +234,153 @@ function commerce_promotion_post_update_8(&$sandbox = NULL) {
233234
$sandbox['#finished'] = ($sandbox['total_count'] - $sandbox['current_count']) / $sandbox['total_count'];
234235
}
235236
}
237+
238+
/**
239+
* Update offers and conditions.
240+
*/
241+
function commerce_promotion_post_update_9(&$sandbox = NULL) {
242+
$promotion_storage = \Drupal::entityTypeManager()->getStorage('commerce_promotion');
243+
if (!isset($sandbox['current_count'])) {
244+
$query = $promotion_storage->getQuery();
245+
$sandbox['total_count'] = $query->count()->execute();
246+
$sandbox['current_count'] = 0;
247+
$sandbox['disabled_offers'] = [];
248+
$sandbox['disabled_conditions'] = [];
249+
250+
if (empty($sandbox['total_count'])) {
251+
$sandbox['#finished'] = 1;
252+
return;
253+
}
254+
}
255+
256+
$query = $promotion_storage->getQuery();
257+
$query->range($sandbox['current_count'], 25);
258+
$result = $query->execute();
259+
if (empty($result)) {
260+
$sandbox['#finished'] = 1;
261+
return;
262+
}
263+
264+
/** @var \Drupal\commerce_promotion\Entity\PromotionInterface[] $promotions */
265+
$promotions = $promotion_storage->loadMultiple($result);
266+
foreach ($promotions as $promotion) {
267+
$needs_save = FALSE;
268+
$needs_disable = FALSE;
269+
270+
$conditions = $promotion->getConditions();
271+
$order_item_conditions = array_filter($conditions, function ($condition) {
272+
/** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
273+
return $condition->getEntityTypeId() == 'commerce_order_item' && $condition->getPluginId() != 'order_item_quantity';
274+
});
275+
$condition_map = [
276+
'order_item_product' => 'order_product',
277+
'order_item_product_type' => 'order_product_type',
278+
'order_item_variation_type' => 'order_variation_type',
279+
];
280+
$condition_items = $promotion->get('conditions')->getValue();
281+
282+
$known_order_item_offers = [
283+
'order_item_fixed_amount_off',
284+
'order_item_percentage_off',
285+
];
286+
$offer = $promotion->getOffer();
287+
$offer_item = $promotion->get('offer')->first()->getValue();
288+
289+
if ($offer->getEntityTypeId() == 'commerce_order_item') {
290+
$needs_save = TRUE;
291+
// Transfer order item conditions to the offer.
292+
// Modify the offer item directly to be able to upgrade offers that
293+
// haven't yet been converted to extend OfferItemPromotionOfferBase.
294+
$offer_item['target_plugin_configuration']['conditions'] = [];
295+
foreach ($order_item_conditions as $condition) {
296+
$offer_item['target_plugin_configuration']['conditions'][] = [
297+
'plugin' => $condition->getPluginId(),
298+
'configuration' => $condition->getConfiguration(),
299+
];
300+
}
301+
302+
// The promotion is using a custom offer which hasn't been updated yet,
303+
// disable it so that it can get updated without crashing everything.
304+
if (!in_array($offer->getPluginId(), $known_order_item_offers)) {
305+
if (!($offer instanceof OrderItemPromotionOfferInterface)) {
306+
$needs_disable = TRUE;
307+
$sandbox['disabled_offers'][] = $promotion->label();
308+
}
309+
}
310+
}
311+
312+
// Convert known order item conditions to order conditions.
313+
if ($order_item_conditions) {
314+
foreach ($condition_items as $index => $condition_item) {
315+
if (array_key_exists($condition_item['target_plugin_id'], $condition_map)) {
316+
$condition_items[$index]['target_plugin_id'] = $condition_map[$condition_item['target_plugin_id']];
317+
$needs_save = TRUE;
318+
}
319+
}
320+
$promotion->set('conditions', $condition_items);
321+
}
322+
323+
// Drop unknown order item conditions.
324+
$conditions = $promotion->getConditions();
325+
$order_item_conditions = array_filter($conditions, function ($condition) {
326+
/** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
327+
return $condition->getEntityTypeId() == 'commerce_order_item' && $condition->getPluginId() != 'order_item_quantity';
328+
});
329+
foreach ($order_item_conditions as $condition) {
330+
foreach ($condition_items as $index => $condition_item) {
331+
if ($condition_item['target_plugin_id'] == $condition->getPluginId()) {
332+
unset($condition_items[$index]);
333+
$needs_save = TRUE;
334+
// An unrecognized offer was dropped, but because the offer applies
335+
// to the order, wasn't transferred there. Disable the promotion
336+
// to allow the merchant to double check the new configuration.
337+
if ($offer->getEntityTypeId() == 'commerce_order') {
338+
$needs_disable = TRUE;
339+
$sandbox['disabled_conditions'][$promotion->id()] = [$promotion->label(), $condition->getPluginId()];
340+
}
341+
}
342+
}
343+
}
344+
345+
if ($needs_disable) {
346+
$promotion->setEnabled(FALSE);
347+
}
348+
if ($needs_save) {
349+
$promotion->set('offer', $offer_item);
350+
$promotion->set('conditions', array_values($condition_items));
351+
$promotion->save();
352+
}
353+
}
354+
355+
$sandbox['current_count'] += 25;
356+
if ($sandbox['current_count'] >= $sandbox['total_count']) {
357+
$sandbox['#finished'] = 1;
358+
}
359+
else {
360+
$sandbox['#finished'] = ($sandbox['total_count'] - $sandbox['current_count']) / $sandbox['total_count'];
361+
}
362+
363+
if ($sandbox['#finished']) {
364+
$message = '';
365+
if ($sandbox['disabled_offers']) {
366+
$message .= 'These promotions have been disabled because their offers need to be updated for Commerce 2.8: <br>';
367+
foreach ($sandbox['disabled_offers'] as $promotion_title) {
368+
$message .= '- ' . $promotion_title . '<br>';
369+
}
370+
}
371+
if ($sandbox['disabled_conditions']) {
372+
$message .= 'These promotions have been disabled because their conditions need to be updated for Commerce 2.8: <br>';
373+
foreach ($sandbox['disabled_conditions'] as $item) {
374+
$message .= '- ' . $item[0] . ' (Condition: ' . $item[1] . ') <br>';
375+
}
376+
}
377+
if ($message) {
378+
$message .= 'Please see https://www.drupal.org/node/2982334 for more information.';
379+
}
380+
else {
381+
$message .= 'Successfully updated all promotions';
382+
}
383+
384+
return $message;
385+
}
386+
}

modules/promotion/src/Entity/Promotion.php

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Drupal\commerce\Entity\CommerceContentEntityBase;
77
use Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface;
88
use Drupal\commerce_order\Entity\OrderInterface;
9+
use Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\OrderItemPromotionOfferInterface;
910
use Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\PromotionOfferInterface;
1011
use Drupal\Core\Datetime\DrupalDateTime;
1112
use Drupal\Core\Entity\EntityStorageInterface;
@@ -456,66 +457,34 @@ public function applies(OrderInterface $order) {
456457
// Promotions without conditions always apply.
457458
return TRUE;
458459
}
459-
$order_conditions = array_filter($conditions, function ($condition) {
460+
// Filter the conditions just in case there are leftover order item
461+
// conditions (which have been moved to offer conditions).
462+
$conditions = array_filter($conditions, function ($condition) {
460463
/** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
461464
return $condition->getEntityTypeId() == 'commerce_order';
462465
});
463-
$order_item_conditions = array_filter($conditions, function ($condition) {
464-
/** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
465-
return $condition->getEntityTypeId() == 'commerce_order_item';
466-
});
467-
$condition_operator = $this->getConditionOperator();
468-
$order_conditions = new ConditionGroup($order_conditions, $condition_operator);
469-
$order_item_conditions = new ConditionGroup($order_item_conditions, $condition_operator);
470-
471-
$order_conditions_apply = $order_conditions->evaluate($order);
472-
if ($condition_operator == 'AND' && !$order_conditions_apply) {
473-
return FALSE;
474-
}
475-
476-
$order_item_conditions_apply = FALSE;
477-
foreach ($order->getItems() as $order_item) {
478-
// Order item conditions must match at least one order item.
479-
if ($order_item_conditions->evaluate($order_item)) {
480-
$order_item_conditions_apply = TRUE;
481-
break;
482-
}
483-
}
484-
485-
if ($condition_operator == 'AND') {
486-
return $order_conditions_apply && $order_item_conditions_apply;
487-
}
488-
elseif ($condition_operator == 'OR') {
489-
// Empty condition groups are TRUE by default, which leads to incorrect
490-
// logic with ORed groups due to false positives.
491-
$order_conditions_apply = $order_conditions->getConditions() && $order_conditions_apply;
492-
$order_item_conditions_apply = $order_item_conditions->getConditions() && $order_item_conditions_apply;
466+
$condition_group = new ConditionGroup($conditions, $this->getConditionOperator());
493467

494-
return $order_conditions_apply || $order_item_conditions_apply;
495-
}
468+
return $condition_group->evaluate($order);
496469
}
497470

498471
/**
499472
* {@inheritdoc}
500473
*/
501474
public function apply(OrderInterface $order) {
502475
$offer = $this->getOffer();
503-
if ($offer->getEntityTypeId() == 'commerce_order') {
504-
$offer->apply($order, $this);
505-
}
506-
elseif ($offer->getEntityTypeId() == 'commerce_order_item') {
507-
$order_item_conditions = array_filter($this->getConditions(), function ($condition) {
508-
/** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
509-
return $condition->getEntityTypeId() == 'commerce_order_item';
510-
});
511-
$order_item_conditions = new ConditionGroup($order_item_conditions, $this->getConditionOperator());
476+
if ($offer instanceof OrderItemPromotionOfferInterface) {
477+
$offer_conditions = new ConditionGroup($offer->getConditions(), 'OR');
512478
// Apply the offer to order items that pass the conditions.
513479
foreach ($order->getItems() as $order_item) {
514-
if ($order_item_conditions->evaluate($order_item)) {
480+
if ($offer_conditions->evaluate($order_item)) {
515481
$offer->apply($order_item, $this);
516482
}
517483
}
518484
}
485+
else {
486+
$offer->apply($order, $this);
487+
}
519488
}
520489

521490
/**
@@ -634,7 +603,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
634603
'type' => 'commerce_conditions',
635604
'weight' => 3,
636605
'settings' => [
637-
'entity_types' => ['commerce_order', 'commerce_order_item'],
606+
'entity_types' => ['commerce_order'],
638607
],
639608
]);
640609

modules/promotion/src/EventSubscriber/FilterConditionsEventSubscriber.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ public function onFilterConditions(FilterConditionsEvent $event) {
3131
$definitions = $event->getDefinitions();
3232
unset($definitions['order_store']);
3333
unset($definitions['order_type']);
34-
// Remove until #2980700 lands.
35-
unset($definitions['order_product']);
36-
unset($definitions['order_product_type']);
37-
unset($definitions['order_variation_type']);
3834
$event->setDefinitions($definitions);
3935
}
4036
}

modules/promotion/src/Plugin/Commerce/PromotionOffer/FixedAmountOffTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function defaultConfiguration() {
2323
* {@inheritdoc}
2424
*/
2525
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
26-
$form += parent::buildConfigurationForm($form, $form_state);
26+
$form = parent::buildConfigurationForm($form, $form_state);
2727

2828
$amount = $this->configuration['amount'];
2929
// A bug in the plugin_select form element causes $amount to be incomplete.

modules/promotion/src/Plugin/Commerce/PromotionOffer/OrderItemPromotionOfferBase.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,78 @@
22

33
namespace Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer;
44

5+
use Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface;
6+
use Drupal\Core\Form\FormStateInterface;
7+
58
/**
69
* Provides the base class for order item offers.
710
*/
811
abstract class OrderItemPromotionOfferBase extends PromotionOfferBase implements OrderItemPromotionOfferInterface {
912

13+
/**
14+
* {@inheritdoc}
15+
*/
16+
public function defaultConfiguration() {
17+
return [
18+
'conditions' => [],
19+
] + parent::defaultConfiguration();
20+
}
21+
22+
/**
23+
* {@inheritdoc}
24+
*/
25+
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
26+
$form = parent::buildConfigurationForm($form, $form_state);
27+
28+
$form['conditions'] = [
29+
'#type' => 'commerce_conditions',
30+
'#title' => $this->t('Applies to'),
31+
'#parent_entity_type' => 'commerce_promotion',
32+
'#entity_types' => ['commerce_order_item'],
33+
'#default_value' => $this->configuration['conditions'],
34+
];
35+
36+
return $form;
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
43+
parent::submitConfigurationForm($form, $form_state);
44+
45+
if (!$form_state->getErrors()) {
46+
$values = $form_state->getValue($form['#parents']);
47+
$this->configuration['conditions'] = $values['conditions'];
48+
}
49+
}
50+
51+
/**
52+
* {@inheritdoc}
53+
*/
54+
public function getConditions() {
55+
$plugin_manager = \Drupal::service('plugin.manager.commerce_condition');
56+
$conditions = [];
57+
foreach ($this->configuration['conditions'] as $condition) {
58+
$conditions[] = $plugin_manager->createInstance($condition['plugin'], $condition['configuration']);
59+
}
60+
return $conditions;
61+
}
62+
63+
/**
64+
* {@inheritdoc}
65+
*/
66+
public function setConditions(array $conditions) {
67+
$this->configuration['conditions'] = [];
68+
foreach ($conditions as $condition) {
69+
if ($condition instanceof ConditionInterface) {
70+
$this->configuration['conditions'][] = [
71+
'plugin' => $condition->getPluginId(),
72+
'configuration' => $condition->getConfiguration(),
73+
];
74+
}
75+
}
76+
return $this;
77+
}
78+
1079
}

0 commit comments

Comments
 (0)