You are here

public function BuyXGetY::apply in Commerce Core 8.2

Applies the offer to the given entity.

Parameters

\Drupal\Core\Entity\EntityInterface $entity: The entity.

\Drupal\commerce_promotion\Entity\PromotionInterface $promotion: THe parent promotion.

Overrides PromotionOfferInterface::apply

File

modules/promotion/src/Plugin/Commerce/PromotionOffer/BuyXGetY.php, line 341

Class

BuyXGetY
Provides the "Buy X Get Y" offer for orders.

Namespace

Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer

Code

public function apply(EntityInterface $entity, PromotionInterface $promotion) {
  $this
    ->assertEntity($entity);

  /** @var \Drupal\commerce_order\Entity\OrderInterface $order */
  $order = $entity;
  $order_items = $order
    ->getItems();
  $buy_conditions = $this
    ->buildConditionGroup($this->configuration['buy_conditions']);
  $buy_order_items = $this
    ->selectOrderItems($order_items, $buy_conditions, 'DESC');
  $buy_quantities = array_map(function (OrderItemInterface $order_item) {
    return $order_item
      ->getQuantity();
  }, $buy_order_items);
  if (array_sum($buy_quantities) < $this->configuration['buy_quantity']) {
    return;
  }
  $get_conditions = $this
    ->buildConditionGroup($this->configuration['get_conditions']);
  if ($this->configuration['get_auto_add'] && ($get_purchasable_entity = $this
    ->findSinglePurchasableEntity($get_conditions))) {
    $order_item = $this
      ->findOrCreateOrderItem($get_purchasable_entity, $order_items);
    $expected_get_quantity = $this
      ->calculateExpectedGetQuantity($buy_quantities, $order_item);

    // If the expected get quantity is non-zero, we need to update the
    // quantity of the 'get' order item accordingly.
    if (Calculator::compare($expected_get_quantity, '0') !== 0) {
      if (Calculator::compare($order_item
        ->getQuantity(), $expected_get_quantity) === -1) {
        $order_item
          ->setQuantity($expected_get_quantity);

        // Ensure that order items which are 'touched' by this promotion can
        // not be edited by the customer, either by changing their quantity or
        // removing them from the cart.
        $order_item
          ->lock();
      }

      // Keep track of the quantity that was auto-added to this order item so
      // we can subtract it (or remove the order item completely) if the buy
      // conditions are no longer satisfied on the next order refresh.
      $order_item
        ->setData("promotion:{$promotion->id()}:auto_add_quantity", $expected_get_quantity);
      $time = $order
        ->getCalculationDate()
        ->format('U');
      $context = new Context($order
        ->getCustomer(), $order
        ->getStore(), $time);
      $unit_price = $this->chainPriceResolver
        ->resolve($get_purchasable_entity, $order_item
        ->getQuantity(), $context);
      $order_item
        ->setUnitPrice($unit_price);
      if ($order_item
        ->isNew()) {
        $order_item
          ->set('order_id', $order
          ->id());
        $order_item
          ->save();
        $order
          ->addItem($order_item);
        $order_items = $order
          ->getItems();
      }
    }
  }
  $get_order_items = $this
    ->selectOrderItems($order_items, $get_conditions, 'ASC');
  $get_quantities = array_map(function (OrderItemInterface $order_item) {
    return $order_item
      ->getQuantity();
  }, $get_order_items);
  if (empty($get_quantities)) {
    return;
  }

  // It is possible for $buy_quantities and $get_quantities to overlap (have
  // the same order item IDs). For example, in a "Buy 3 Get 1" scenario with
  // a single T-shirt order item of quantity: 8, there are 6 bought and 2
  // discounted products, in this order: 3, 1, 3, 1. To ensure the specified
  // results, $buy_quantities must be processed group by group, with any
  // overlaps immediately removed from the $get_quantities (and vice-versa).
  // Additionally, ensure that any items from $buy_quantities that overlap
  // with $get_quantities are processed last, in order to accommodate the case
  // when $buy_conditions (or the lack thereof) are satisfied by the other
  // (non-overlapping) $buy_quantity items.
  foreach ($buy_quantities as $id => $quantity) {
    if (isset($get_quantities[$id])) {
      unset($buy_quantities[$id]);
      $buy_quantities[$id] = $quantity;
    }
  }
  $final_quantities = [];
  $i = 0;
  while (!empty($buy_quantities)) {
    $selected_buy_quantities = $this
      ->sliceQuantities($buy_quantities, $this->configuration['buy_quantity']);
    if (array_sum($selected_buy_quantities) < $this->configuration['buy_quantity']) {
      break;
    }
    $get_quantities = $this
      ->removeQuantities($get_quantities, $selected_buy_quantities);
    $selected_get_quantities = $this
      ->sliceQuantities($get_quantities, $this->configuration['get_quantity']);
    $buy_quantities = $this
      ->removeQuantities($buy_quantities, $selected_get_quantities);

    // Merge the selected get quantities into a final list, to ensure that
    // each order item only gets a single adjustment.
    $final_quantities = $this
      ->mergeQuantities($final_quantities, $selected_get_quantities);

    // Determine whether the offer reached its limit.
    if ($this->configuration['offer_limit'] == ++$i) {
      break;
    }
  }
  foreach ($final_quantities as $order_item_id => $quantity) {
    $order_item = $get_order_items[$order_item_id];
    $adjustment_amount = $this
      ->buildAdjustmentAmount($order_item, $quantity);
    $order_item
      ->addAdjustment(new Adjustment([
      'type' => 'promotion',
      'label' => $promotion
        ->getDisplayName() ?: $this
        ->t('Discount'),
      'amount' => $adjustment_amount
        ->multiply('-1'),
      'source_id' => $promotion
        ->id(),
    ]));
  }
}