Skip to content

Commit 4e9a025

Browse files
istavrosbojanz
istavros
authored andcommitted
Issue #2929103 by istavros, bojanz: Promotions have incorrect condition logic when the operator is "OR"
1 parent 5b9e0e0 commit 4e9a025

File tree

3 files changed

+74
-22
lines changed

3 files changed

+74
-22
lines changed

modules/promotion/src/Entity/Promotion.php

+21-6
Original file line numberDiff line numberDiff line change
@@ -464,20 +464,35 @@ public function applies(OrderInterface $order) {
464464
/** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
465465
return $condition->getEntityTypeId() == 'commerce_order_item';
466466
});
467-
$order_conditions = new ConditionGroup($order_conditions, $this->getConditionOperator());
468-
$order_item_conditions = new ConditionGroup($order_item_conditions, $this->getConditionOperator());
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);
469470

470-
if (!$order_conditions->evaluate($order)) {
471+
$order_conditions_apply = $order_conditions->evaluate($order);
472+
if ($condition_operator == 'AND' && !$order_conditions_apply) {
471473
return FALSE;
472474
}
475+
476+
$order_item_conditions_apply = FALSE;
473477
foreach ($order->getItems() as $order_item) {
474478
// Order item conditions must match at least one order item.
475479
if ($order_item_conditions->evaluate($order_item)) {
476-
return TRUE;
480+
$order_item_conditions_apply = TRUE;
481+
break;
477482
}
478483
}
479484

480-
return FALSE;
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;
493+
494+
return $order_conditions_apply || $order_item_conditions_apply;
495+
}
481496
}
482497

483498
/**
@@ -493,7 +508,7 @@ public function apply(OrderInterface $order) {
493508
/** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
494509
return $condition->getEntityTypeId() == 'commerce_order_item';
495510
});
496-
$order_item_conditions = new ConditionGroup($order_item_conditions, 'AND');
511+
$order_item_conditions = new ConditionGroup($order_item_conditions, $this->getConditionOperator());
497512
// Apply the offer to order items that pass the conditions.
498513
foreach ($order->getItems() as $order_item) {
499514
if ($order_item_conditions->evaluate($order_item)) {

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

+52-15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Drupal\commerce_order\Entity\OrderItem;
66
use Drupal\commerce_order\Entity\OrderItemType;
77
use Drupal\commerce_order\Entity\Order;
8+
use Drupal\commerce_price\Price;
89
use Drupal\commerce_promotion\Entity\Promotion;
910
use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
1011

@@ -141,12 +142,17 @@ public function testOrderCondition() {
141142
$this->order->save();
142143
$result = $promotion->applies($this->order);
143144
$this->assertTrue($result);
145+
146+
// No order total can satisfy both conditions.
147+
$promotion->setConditionOperator('AND');
148+
$result = $promotion->applies($this->order);
149+
$this->assertFalse($result);
144150
}
145151

146152
/**
147-
* Tests promotions with an order item condition.
153+
* Tests promotions with both order and order item conditions.
148154
*/
149-
public function testOrderItemCondition() {
155+
public function testMixedCondition() {
150156
// Starts now, enabled. No end time.
151157
$promotion = Promotion::create([
152158
'name' => 'Promotion 1',
@@ -165,7 +171,7 @@ public function testOrderItemCondition() {
165171
'target_plugin_configuration' => [
166172
'operator' => '>',
167173
'amount' => [
168-
'number' => '10.00',
174+
'number' => '30.00',
169175
'currency_code' => 'USD',
170176
],
171177
],
@@ -174,7 +180,7 @@ public function testOrderItemCondition() {
174180
'target_plugin_id' => 'order_item_quantity',
175181
'target_plugin_configuration' => [
176182
'operator' => '>',
177-
'quantity' => 2,
183+
'quantity' => 1,
178184
],
179185
],
180186
],
@@ -184,31 +190,62 @@ public function testOrderItemCondition() {
184190

185191
$order_item = OrderItem::create([
186192
'type' => 'test',
187-
'quantity' => 2,
193+
'quantity' => 4,
188194
'unit_price' => [
189-
'number' => '20.00',
195+
'number' => '10.00',
190196
'currency_code' => 'USD',
191197
],
192198
]);
193199
$order_item->save();
200+
201+
// AND: Both conditions apply.
194202
$this->order->addItem($order_item);
195203
$this->order->save();
204+
$result = $promotion->applies($this->order);
205+
$this->assertTrue($result);
206+
207+
// OR: Both conditions apply.
208+
$promotion->setConditionOperator('OR');
209+
$result = $promotion->applies($this->order);
210+
$this->assertTrue($result);
196211

212+
// AND: Neither condition applies.
213+
$order_item->setQuantity(1);
214+
$order_item->save();
215+
$this->order->save();
216+
$promotion->setConditionOperator('AND');
197217
$result = $promotion->applies($this->order);
198218
$this->assertFalse($result);
199219

200-
$order_item = OrderItem::create([
201-
'type' => 'test',
202-
'quantity' => 4,
203-
'unit_price' => [
204-
'number' => '20.00',
205-
'currency_code' => 'USD',
206-
],
207-
]);
220+
// OR: Neither condition applies.
221+
$promotion->setConditionOperator('OR');
222+
$result = $promotion->applies($this->order);
223+
$this->assertFalse($result);
224+
225+
// AND: Order condition fails, order item condition passes.
226+
$order_item->setQuantity(2);
227+
$order_item->save();
228+
$this->order->save();
229+
$promotion->setConditionOperator('AND');
230+
$result = $promotion->applies($this->order);
231+
$this->assertFalse($result);
232+
233+
// OR: Order condition fails, order item condition passes.
234+
$promotion->setConditionOperator('OR');
235+
$result = $promotion->applies($this->order);
236+
$this->assertTrue($result);
237+
238+
// AND: Order condition passes, order item condition fails.
239+
$order_item->setUnitPrice(new Price('40', 'USD'));
240+
$order_item->setQuantity(1);
208241
$order_item->save();
209-
$this->order->addItem($order_item);
210242
$this->order->save();
243+
$promotion->setConditionOperator('AND');
244+
$result = $promotion->applies($this->order);
245+
$this->assertFalse($result);
211246

247+
// OR: Order condition passes, order item condition fails.
248+
$promotion->setConditionOperator('OR');
212249
$result = $promotion->applies($this->order);
213250
$this->assertTrue($result);
214251
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public function testOrderTotal() {
120120
]);
121121
$promotion->save();
122122

123-
$this->assertNotEmpty($promotion->applies($this->order));
123+
$this->assertTrue($promotion->applies($this->order));
124124
$this->container->get('commerce_order.order_refresh')->refresh($this->order);
125125

126126
$this->assertEquals(1, count($this->order->getAdjustments()));

0 commit comments

Comments
 (0)