Skip to content

Commit

Permalink
Merge pull request #13 from code4business/develop
Browse files Browse the repository at this point in the history
Fix for #9 and #12. New feature: apply gift for each subset of products, multiple gifts per rule
  • Loading branch information
domeglic authored Feb 8, 2019
2 parents 52804c2 + 04f46a1 commit 8c746b8
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Observer/RemoveGiftItems.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function __construct(CheckoutSession $checkoutSession)
/**
* Delete all gift items. They will be re-added by SalesRule (If possible).
*
* @event sales_quote_address_collect_totals_before
* @event sales_quote_remove_item
* @param Observer $observer
* @return void
*/
Expand Down
90 changes: 65 additions & 25 deletions Observer/ResetGiftItems.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace C4B\FreeProduct\Observer;

use C4B\FreeProduct\SalesRule\Action\ForeachGiftAction;
use C4B\FreeProduct\SalesRule\Action\GiftAction;

use Magento\Framework\Event\Observer;
Expand All @@ -10,9 +11,19 @@
use Magento\Quote\Model\Quote;

/**
* Observer for resetting gift cart items
* Observer for resetting gift cart items.
* When quote totals are collected, all gifts are removed and are later re-added by Discount total collector.
* It is triggered by two events:
* - quote collect before: for normal quote operations (adding items, changing qty, removing item)
* - address collect before: When shipping is estimated the above event is not triggered.
*
* There is some weird handling of quote items. There are two ways to get them: getItems() and getItemsCollection()
* New quote items are added into the collection, but not into getItems. This is apparently how it should be because
* otherwise newly added quote items are added again since they don't have an item_id yet and in case of bundle items this would fail.
* So quote->setItems should not be used here:
* @see \Magento\Quote\Model\QuoteRepository\SaveHandler::save
* @see \Magento\Quote\Model\Quote\Item\CartItemPersister::save
*
* @category C4B
* @package C4B_FreeProduct
* @author Dominik Meglič <[email protected]>
* @copyright code4business Software GmbH
Expand All @@ -21,45 +32,77 @@
class ResetGiftItems implements ObserverInterface
{
/**
* Delete all gift items. They will be re-added by SalesRule (If possible).
*
* @var bool
*/
private $areGiftItemsReset = false;

/**
* @event sales_quote_collect_totals_before
* @event sales_quote_address_collect_totals_before
* @param Observer $observer
* @return void
* @throws \Exception
*/
public function execute(\Magento\Framework\Event\Observer $observer)
public function execute(Observer $observer)
{
/** @var ShippingAssignmentInterface $shippingAssignment */
$shippingAssignment = $observer->getEvent()->getData('shipping_assignment');
/** @var Quote $quote */
$quote = $observer->getEvent()->getData('quote');
/** @var Quote\Address $address */
$address = $shippingAssignment->getShipping()->getAddress();
/** @var ShippingAssignmentInterface $shippingAssignment */
$shippingAssignment = $observer->getEvent()->getData('shipping_assignment');

if ($shippingAssignment->getItems() == null || $address->getAddressType() != Quote\Address::TYPE_SHIPPING)
if ($quote->getItems() == null || $this->areGiftItemsReset)
{
return;
}

$newShippingAssignmentItems = $this->removeOldGiftQuoteItems($shippingAssignment);
if ($shippingAssignment instanceof ShippingAssignmentInterface)
{
/** @var Quote\Address $address */
$address = $shippingAssignment->getShipping()->getAddress();

$shippingAssignment->setItems($newShippingAssignmentItems);
if ($address->getAddressType() != Quote\Address::ADDRESS_TYPE_SHIPPING)
{
return;
}
}
else
{
$address = $quote->getShippingAddress();
}

$realQuoteItems = $this->removeOldGiftQuoteItems($quote->getItemsCollection());
$this->areGiftItemsReset = true;
$address->unsetData(GiftAction::APPLIED_FREEPRODUCT_RULE_IDS);
$address->unsetData('cached_items_all');

$this->updateExtensionAttributes($quote, $shippingAssignment);
if ($shippingAssignment instanceof ShippingAssignmentInterface)
{
$shippingAssignment->setItems($realQuoteItems);
$this->updateExtensionAttributes($quote, $shippingAssignment);
}
}

/**
* @param ShippingAssignmentInterface $shippingAssignment
* @return array
* A new gift item was added so if cart totals are collected again, all gift items will be reset.
*
* @return void
*/
public function reportGiftItemAdded()
{
$this->areGiftItemsReset = false;
}

/**
* @param \Magento\Quote\Model\ResourceModel\Quote\Item\Collection|\Magento\Framework\Data\Collection $quoteItemsCollection
* @return Quote\Item[]
* @throws \Exception
*/
protected function removeOldGiftQuoteItems($shippingAssignment): array
protected function removeOldGiftQuoteItems($quoteItemsCollection)
{
$newShippingAssignment = [];
$realQuoteItems = [];

/** @var Quote\Item $quoteItem */
foreach ($shippingAssignment->getItems() as $quoteItem)
foreach ($quoteItemsCollection->getItems() as $key => $quoteItem)
{
if ($quoteItem->isDeleted())
{
Expand All @@ -78,15 +121,12 @@ protected function removeOldGiftQuoteItems($shippingAssignment): array
}
} else
{
/**
* Reset shipping assignment to prevent others from working on old items
* @see \Magento\Tax\Model\Sales\Total\Quote\CommonTaxCollector::processAppliedTaxes
* @see \Magento\Tax\Model\Plugin\OrderSave::saveOrderTax
*/
$newShippingAssignment[] = $quoteItem;
$quoteItem->unsetData(ForeachGiftAction::APPLIED_FREEPRODUCT_RULE_IDS);
$realQuoteItems[$key] = $quoteItem;
}
}
return $newShippingAssignment;

return $realQuoteItems;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions Plugin/SalesRule/Model/MetadataValueProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace C4B\FreeProduct\Plugin\SalesRule\Model;

use C4B\FreeProduct\SalesRule\Action\GiftAction;
use C4B\FreeProduct\SalesRule\Action\ForeachGiftAction;

use \Magento\SalesRule\Model\Rule\Metadata\ValueProvider as Source;

/**
Expand All @@ -28,6 +30,9 @@ public function afterGetMetadataValues(Source $subject, $resultMetadataValues)
$resultMetadataValues['actions']['children']['simple_action']['arguments']['data']['config']['options'][] = [
'label' => __('Add a Gift'), 'value' => GiftAction::ACTION
];
$resultMetadataValues['actions']['children']['simple_action']['arguments']['data']['config']['options'][] = [
'label' => __('Add a Gift (For each cart item)'), 'value' => ForeachGiftAction::ACTION
];

return $resultMetadataValues;
}
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The development and the function of the original Magento1 extension is described

Requirements
-------
- PHP >= 7.0.0
- PHP >= 7.1
- Magento >= 2.2

Supported Product Types
Expand All @@ -35,8 +35,13 @@ Configuration
-------
Sales rules for carts are configured in _Marketing->Cart Price Rules_:
- In the Actions tab, the Apply field should be set to Add a Gift
- Gift SKU: Product that will be added. Only simple and virtual products without (required) custom options are supported
- Discount Amount: The qty of added gifts
- Gift SKU: Product that will be added. Only simple and virtual products without (required) custom options are supported. Multiple comma-separated SKUs can be specified
- Discount Amount: The qty of added gifts
- The gift item is added once for the whole cart

Action **Add a Gift (for each cart item)** works similarly but will add the gift item for each product in cart. The qty of said product is also taken into consideration.

This action usually needs conditions to match only specific items *(Apply the rule only to cart items matching the following conditions)*.

Limitations:
-------
Expand Down
117 changes: 117 additions & 0 deletions SalesRule/Action/ForeachGiftAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace C4B\FreeProduct\SalesRule\Action;

use C4B\FreeProduct\Observer\ResetGiftItems;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Customer\Model\Address;
use Magento\Quote\Model\Quote\Item\AbstractItem;
use Magento\SalesRule\Model\Rule\Action\Discount;
use Psr\Log\LoggerInterface;

/**
* Adds a gift for each cart item that meets criteria. It is also multiplied by the qty of said cart item.
* Ex. Add one gift for each product from category Sales.
*
* @package C4B_FreeProduct
* @author Dominik Meglič <[email protected]>
* @copyright code4business Software GmbH
* @license http://opensource.org/licenses/osl-3.0.php
*/
class ForeachGiftAction extends GiftAction
{
const ACTION = 'add_gift_foreach';
/**
* @var ResetGiftItems
*/
private $resetGiftItems;
/**
* @var LoggerInterface
*/
private $logger;


/**
* @param Discount\DataFactory $discountDataFactory
* @param ProductRepositoryInterface $productRepository
* @param ResetGiftItems $resetGiftItems
* @param LoggerInterface $logger
*/
public function __construct(Discount\DataFactory $discountDataFactory,
ProductRepositoryInterface $productRepository,
ResetGiftItems $resetGiftItems,
LoggerInterface $logger)
{
parent::__construct($discountDataFactory, $productRepository, $resetGiftItems, $logger);
$this->resetGiftItems = $resetGiftItems;
$this->logger = $logger;
}

/**
* @param \Magento\SalesRule\Model\Rule $rule
* @param AbstractItem $item
* @param float $qty
* @return Discount\Data
*/
public function calculate($rule, $item, $qty)
{
$appliedRuleIds = $item->getData(static::APPLIED_FREEPRODUCT_RULE_IDS);

if ($item->getAddress()->getAddressType() != Address::TYPE_SHIPPING
|| ($appliedRuleIds != null && isset($appliedRuleIds[$rule->getId()])))
{
return $this->getDiscountData($item);
}

$skus = explode(',', $rule->getData(static::RULE_DATA_KEY_SKU));
$isRuleAdded = false;

foreach ($skus as $sku)
{
try
{
$quoteItem = $item->getQuote()->addProduct($this->getGiftProduct($sku), $rule->getDiscountAmount() * $qty);
$item->getQuote()->setItemsCount($item->getQuote()->getItemsCount() + 1);
$item->getQuote()->setItemsQty((float)$item->getQuote()->getItemsQty() + $quoteItem->getQty());
$this->resetGiftItems->reportGiftItemAdded();

if (is_string($quoteItem))
{
throw new \Exception($quoteItem);
}

$isRuleAdded = true;
} catch (\Exception $e)
{
$this->logger->error(
sprintf('Exception occurred while adding gift product %s to cart. Rule: %d, Exception: %s', implode(',', $skus), $rule->getId(), $e->getMessage()),
[__METHOD__]
);
}
}
if ($isRuleAdded)
{
$this->addAppliedRuleIdToItem($rule->getRuleId(), $item);
}

return $this->getDiscountData($item);
}

/**
* @param int $ruleId
* @param AbstractItem $quoteItem
*/
protected function addAppliedRuleIdToItem(int $ruleId, AbstractItem $quoteItem)
{
$appliedRules = $quoteItem->getData(static::APPLIED_FREEPRODUCT_RULE_IDS);

if ($appliedRules == null)
{
$appliedRules = [];
}

$appliedRules[$ruleId] = $ruleId;

$quoteItem->setData(static::APPLIED_FREEPRODUCT_RULE_IDS, $appliedRules);
}
}
Loading

0 comments on commit 8c746b8

Please sign in to comment.