diff --git a/modules/order/src/OrderRefresh.php b/modules/order/src/OrderRefresh.php index 3eac869d..d5cbd937 100644 --- a/modules/order/src/OrderRefresh.php +++ b/modules/order/src/OrderRefresh.php @@ -3,6 +3,7 @@ namespace Drupal\commerce_order; use Drupal\commerce\Context; +use Drupal\commerce_price\Calculator; use Drupal\Component\Datetime\TimeInterface; use Drupal\commerce_order\Entity\OrderInterface; use Drupal\commerce_order\Entity\OrderType; @@ -166,11 +167,17 @@ class OrderRefresh implements OrderRefreshInterface { foreach ($order->getItems() as $order_item) { if ($order_item->hasTranslationChanges()) { - // Remove the order that was set above, to avoid - // crashes during the entity save process. - $order_item->order_id->entity = NULL; - $order_item->setChangedTime($current_time); - $order_item->save(); + // Remove order items which had their quantities set to 0. + if (Calculator::compare($order_item->getQuantity(), '0') === 0) { + $order->removeItem($order_item); + } + else { + // Remove the order that was set above, to avoid + // crashes during the entity save process. + $order_item->order_id->entity = NULL; + $order_item->setChangedTime($current_time); + $order_item->save(); + } } } } diff --git a/modules/promotion/commerce_promotion.services.yml b/modules/promotion/commerce_promotion.services.yml index b25e6d43..ee0442b1 100644 --- a/modules/promotion/commerce_promotion.services.yml +++ b/modules/promotion/commerce_promotion.services.yml @@ -3,6 +3,12 @@ services: class: Drupal\commerce_promotion\PromotionOfferManager arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@entity_type.manager'] + commerce_promotion.early_order_processor: + class: Drupal\commerce_promotion\EarlyOrderProcessor + arguments: ['@entity_type.manager'] + tags: + - { name: commerce_order.order_processor, priority: 1000} + commerce_promotion.promotion_order_processor: class: Drupal\commerce_promotion\PromotionOrderProcessor arguments: ['@entity_type.manager', '@language_manager'] diff --git a/modules/promotion/src/Entity/Promotion.php b/modules/promotion/src/Entity/Promotion.php index 9f8c885c..242b54a1 100644 --- a/modules/promotion/src/Entity/Promotion.php +++ b/modules/promotion/src/Entity/Promotion.php @@ -563,10 +563,12 @@ class Promotion extends CommerceContentEntityBase implements PromotionInterface */ public function apply(OrderInterface $order) { $offer = $this->getOffer(); + $order_items = $order->getItems(); + if ($offer instanceof OrderItemPromotionOfferInterface) { $offer_conditions = new ConditionGroup($offer->getConditions(), $offer->getConditionOperator()); // Apply the offer to order items that pass the conditions. - foreach ($order->getItems() as $order_item) { + foreach ($order_items as $order_item) { if ($offer_conditions->evaluate($order_item)) { $offer->apply($order_item, $this); } diff --git a/modules/promotion/src/Plugin/Commerce/PromotionOffer/BuyXGetY.php b/modules/promotion/src/Plugin/Commerce/PromotionOffer/BuyXGetY.php index 8aadf9ea..b1ca9ca7 100644 --- a/modules/promotion/src/Plugin/Commerce/PromotionOffer/BuyXGetY.php +++ b/modules/promotion/src/Plugin/Commerce/PromotionOffer/BuyXGetY.php @@ -117,7 +117,6 @@ class BuyXGetY extends OrderPromotionOfferBase { 'get_quantity' => 1, 'get_conditions' => [], 'get_auto_add' => FALSE, - 'get_auto_remove' => FALSE, 'offer_type' => 'percentage', 'offer_percentage' => '0', 'offer_amount' => NULL, @@ -187,15 +186,6 @@ class BuyXGetY extends OrderPromotionOfferBase { 'visible' => [$states], ], ]; - $form['get']['auto_remove'] = [ - '#type' => 'checkbox', - '#title' => $this->t("Automatically remove the offer product to the cart if the buy conditions are no longer met"), - '#default_value' => $this->configuration['auto_remove'], - '#states' => [ - 'visible' => [$states], - ], - ]; - $parents = array_merge($form['#parents'], ['offer', 'type']); $selected_offer_type = NestedArray::getValue($form_state->getUserInput(), $parents); $selected_offer_type = $selected_offer_type ?: $this->configuration['offer_type']; @@ -308,7 +298,6 @@ class BuyXGetY extends OrderPromotionOfferBase { // selected. if (!$has_valid_condition) { $values['get']['auto_add'] = FALSE; - $values['get']['auto_remove'] = FALSE; $form_state->setValue($form['#parents'], $values); return; } @@ -333,7 +322,6 @@ class BuyXGetY extends OrderPromotionOfferBase { $this->configuration['get_quantity'] = $values['get']['quantity']; $this->configuration['get_conditions'] = $values['get']['conditions']; $this->configuration['get_auto_add'] = $values['get']['auto_add']; - $this->configuration['get_auto_remove'] = $values['get']['auto_remove']; $this->configuration['offer_type'] = $values['offer']['type']; if ($this->configuration['offer_type'] == 'percentage') { $this->configuration['offer_percentage'] = Calculator::divide((string) $values['offer']['percentage'], '100'); @@ -479,10 +467,6 @@ class BuyXGetY extends OrderPromotionOfferBase { public function clear(EntityInterface $entity, PromotionInterface $promotion) { parent::clear($entity, $promotion); - // If not configured to auto remove the auto-added get product, stop here. - if (empty($this->configuration['get_auto_remove'])) { - return; - } $this->assertEntity($entity); /** @var \Drupal\commerce_order\Entity\OrderInterface $order */ $order = $entity; @@ -490,9 +474,13 @@ class BuyXGetY extends OrderPromotionOfferBase { // Remove any leftover order items that were auto-added. foreach ($order_items as $order_item) { + $order_item->setAdjustments(array_filter($order_item->getAdjustments(), function (Adjustment $adjustment) use ($promotion) { + return $adjustment->getSourceId() !== $promotion->id(); + })); + if ($order_item->getData("promotion:{$promotion->id()}:auto_add_quantity")) { - $order->removeItem($order_item); - $order_item->delete(); + $new_quantity = Calculator::subtract($order_item->getQuantity(), $order_item->getData("promotion:{$promotion->id()}:auto_add_quantity")); + $order_item->setQuantity($new_quantity); } } } diff --git a/modules/promotion/src/PromotionOrderProcessor.php b/modules/promotion/src/PromotionOrderProcessor.php index 7ecb20dd..dc9a4ebf 100644 --- a/modules/promotion/src/PromotionOrderProcessor.php +++ b/modules/promotion/src/PromotionOrderProcessor.php @@ -2,7 +2,6 @@ namespace Drupal\commerce_promotion; -use Drupal\commerce_order\Adjustment; use Drupal\commerce_order\Entity\OrderInterface; use Drupal\commerce_order\OrderProcessorInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -61,7 +60,7 @@ class PromotionOrderProcessor implements OrderProcessorInterface { $promotions_to_apply = []; foreach ($coupons as $index => $coupon) { $promotion = $coupon->getPromotion(); - $promotions_to_apply[$promotion->id()] = $promotion; + $promotion->apply($order); } // Non-coupon promotions are loaded and applied separately. @@ -77,38 +76,6 @@ class PromotionOrderProcessor implements OrderProcessorInterface { if ($promotion->hasTranslation($content_langcode)) { $promotion = $promotion->getTranslation($content_langcode); } - $promotions_to_apply[$key] = $promotion; - } - - $promotions_to_clear = []; - // Prior to applying promotions, we first need to clear out any "locked" - // promotion adjustment, that are not expected by promotion offers. - // We also need to collect the promotion IDS that are no longer applicable - // in order to clear any potential modifications made to the order. - foreach ($order->getItems() as $order_item) { - foreach ($order_item->getAdjustments(['promotion']) as $adjustment) { - // Skip non locked adjustments. - if (!$adjustment->isLocked()) { - continue; - } - $order_item->removeAdjustment($adjustment); - $source_id = $adjustment->getSourceId(); - // Collect the promotion IDS to clear (i.e the ones that no longer - // apply). - if (!empty($source_id) && !isset($promotions_to_apply[$source_id])) { - $promotions_to_clear[] = $source_id; - } - } - } - - if ($promotions_to_clear) { - $promotions = $promotion_storage->loadMultiple($promotions_to_clear); - foreach ($promotions as $promotion) { - $promotion->clear($order); - } - } - - foreach ($promotions_to_apply as $promotion) { $promotion->apply($order); } } diff --git a/modules/promotion/tests/src/Kernel/Plugin/Commerce/PromotionOffer/BuyXGetYTest.php b/modules/promotion/tests/src/Kernel/Plugin/Commerce/PromotionOffer/BuyXGetYTest.php index 2ae71375..a729ec97 100644 --- a/modules/promotion/tests/src/Kernel/Plugin/Commerce/PromotionOffer/BuyXGetYTest.php +++ b/modules/promotion/tests/src/Kernel/Plugin/Commerce/PromotionOffer/BuyXGetYTest.php @@ -710,31 +710,6 @@ class BuyXGetYTest extends OrderKernelTestBase { $this->assertCount(1, $second_order_item->getAdjustments()); $this->assertAdjustmentPrice($second_order_item->getAdjustments()[0], '-30'); - // When reducing the quantity, if the offer is not configured to autoremove - // the get order item, the promotion adjustments should be cleared out but - // the get order item should remain. - $first_order_item->setQuantity('1'); - $first_order_item->save(); - $this->container->get('commerce_order.order_refresh')->refresh($this->order); - $this->assertCount(2, $this->order->getItems()); - list($first_order_item, $second_order_item) = $this->order->getItems(); - $this->assertCount(0, $second_order_item->getAdjustments()); - $this->assertEquals(new Price('40', 'USD'), $this->order->getTotalPrice()); - - // Now test the auto remove feature. - $offer_configuration['get_auto_remove'] = TRUE; - $offer->setConfiguration($offer_configuration); - $this->promotion->setOffer($offer); - $this->promotion->save(); - $first_order_item->setQuantity('2'); - $first_order_item->save(); - $this->order->setItems([$first_order_item]); - $this->container->get('commerce_order.order_refresh')->refresh($this->order); - list($first_order_item, $second_order_item) = $this->order->getItems(); - $this->assertEquals(2, $first_order_item->getQuantity()); - $this->assertEquals(2, $second_order_item->getQuantity()); - $this->assertCount(1, $second_order_item->getAdjustments()); - $this->assertAdjustmentPrice($second_order_item->getAdjustments()[0], '-30'); $first_order_item->setQuantity('1'); $first_order_item->save(); $this->container->get('commerce_order.order_refresh')->refresh($this->order);