123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- <?php
- /**
- * Copyright © Magento, Inc. All rights reserved.
- * See COPYING.txt for license details.
- */
- namespace Magento\SalesRule\Model;
- use Magento\Quote\Model\Quote\Address;
- use Magento\Quote\Model\Quote\Item\AbstractItem;
- /**
- * SalesRule Validator Model
- *
- * Allows dispatching before and after events for each controller action
- *
- * @method mixed getCouponCode()
- * @method Validator setCouponCode($code)
- * @method mixed getWebsiteId()
- * @method Validator setWebsiteId($id)
- * @method mixed getCustomerGroupId()
- * @method Validator setCustomerGroupId($id)
- * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
- */
- class Validator extends \Magento\Framework\Model\AbstractModel
- {
- /**
- * Rule source collection
- *
- * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection
- */
- protected $_rules;
- /**
- * Defines if method \Magento\SalesRule\Model\Validator::reset() wasn't called
- * Used for clearing applied rule ids in Quote and in Address
- *
- * @var bool
- */
- protected $_isFirstTimeResetRun = true;
- /**
- * Information about item totals for rules
- *
- * @var array
- */
- protected $_rulesItemTotals = [];
- /**
- * Skip action rules validation flag
- *
- * @var bool
- */
- protected $_skipActionsValidation = false;
- /**
- * Catalog data
- *
- * @var \Magento\Catalog\Helper\Data|null
- */
- protected $_catalogData = null;
- /**
- * @var \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory
- */
- protected $_collectionFactory;
- /**
- * @var \Magento\SalesRule\Model\Utility
- */
- protected $validatorUtility;
- /**
- * @var \Magento\SalesRule\Model\RulesApplier
- */
- protected $rulesApplier;
- /**
- * @var \Magento\Framework\Pricing\PriceCurrencyInterface
- */
- protected $priceCurrency;
- /**
- * @var Validator\Pool
- */
- protected $validators;
- /**
- * @var \Magento\Framework\Message\ManagerInterface
- */
- protected $messageManager;
- /**
- * Counter is used for assigning temporary id to quote address
- *
- * @var int
- */
- protected $counter = 0;
- /**
- * @param \Magento\Framework\Model\Context $context
- * @param \Magento\Framework\Registry $registry
- * @param \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory $collectionFactory
- * @param \Magento\Catalog\Helper\Data $catalogData
- * @param Utility $utility
- * @param RulesApplier $rulesApplier
- * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
- * @param Validator\Pool $validators
- * @param \Magento\Framework\Message\ManagerInterface $messageManager
- * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
- * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
- * @param array $data
- * @SuppressWarnings(PHPMD.ExcessiveParameterList)
- */
- public function __construct(
- \Magento\Framework\Model\Context $context,
- \Magento\Framework\Registry $registry,
- \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory $collectionFactory,
- \Magento\Catalog\Helper\Data $catalogData,
- \Magento\SalesRule\Model\Utility $utility,
- \Magento\SalesRule\Model\RulesApplier $rulesApplier,
- \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
- \Magento\SalesRule\Model\Validator\Pool $validators,
- \Magento\Framework\Message\ManagerInterface $messageManager,
- \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
- \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
- array $data = []
- ) {
- $this->_collectionFactory = $collectionFactory;
- $this->_catalogData = $catalogData;
- $this->validatorUtility = $utility;
- $this->rulesApplier = $rulesApplier;
- $this->priceCurrency = $priceCurrency;
- $this->validators = $validators;
- $this->messageManager = $messageManager;
- parent::__construct($context, $registry, $resource, $resourceCollection, $data);
- }
- /**
- * Init validator
- * Init process load collection of rules for specific website,
- * customer group and coupon code
- *
- * @param int $websiteId
- * @param int $customerGroupId
- * @param string $couponCode
- * @return $this
- */
- public function init($websiteId, $customerGroupId, $couponCode)
- {
- $this->setWebsiteId($websiteId)->setCustomerGroupId($customerGroupId)->setCouponCode($couponCode);
- return $this;
- }
- /**
- * Get rules collection for current object state
- *
- * @param Address|null $address
- * @return \Magento\SalesRule\Model\ResourceModel\Rule\Collection
- */
- protected function _getRules(Address $address = null)
- {
- $addressId = $this->getAddressId($address);
- $key = $this->getWebsiteId() . '_'
- . $this->getCustomerGroupId() . '_'
- . $this->getCouponCode() . '_'
- . $addressId;
- if (!isset($this->_rules[$key])) {
- $this->_rules[$key] = $this->_collectionFactory->create()
- ->setValidationFilter(
- $this->getWebsiteId(),
- $this->getCustomerGroupId(),
- $this->getCouponCode(),
- null,
- $address
- )
- ->addFieldToFilter('is_active', 1)
- ->load();
- }
- return $this->_rules[$key];
- }
- /**
- * @param Address $address
- * @return string
- */
- protected function getAddressId(Address $address)
- {
- if ($address == null) {
- return '';
- }
- if (!$address->hasData('address_sales_rule_id')) {
- if ($address->hasData('address_id')) {
- $address->setData('address_sales_rule_id', $address->getData('address_id'));
- } else {
- $type = $address->getAddressType();
- $tempId = $type . $this->counter++;
- $address->setData('address_sales_rule_id', $tempId);
- }
- }
- return $address->getData('address_sales_rule_id');
- }
- /**
- * Set skip actions validation flag
- *
- * @param bool $flag
- * @return $this
- */
- public function setSkipActionsValidation($flag)
- {
- $this->_skipActionsValidation = $flag;
- return $this;
- }
- /**
- * Can apply rules check
- *
- * @param AbstractItem $item
- * @return bool
- */
- public function canApplyRules(AbstractItem $item)
- {
- $address = $item->getAddress();
- foreach ($this->_getRules($address) as $rule) {
- if (!$this->validatorUtility->canProcessRule($rule, $address) || !$rule->getActions()->validate($item)) {
- return false;
- }
- }
- return true;
- }
- /**
- * Reset quote and address applied rules
- *
- * @param Address $address
- * @return $this
- */
- public function reset(Address $address)
- {
- $this->validatorUtility->resetRoundingDeltas();
- if ($this->_isFirstTimeResetRun) {
- $address->setAppliedRuleIds('');
- $address->getQuote()->setAppliedRuleIds('');
- $this->_isFirstTimeResetRun = false;
- }
- return $this;
- }
- /**
- * Quote item discount calculation process
- *
- * @param AbstractItem $item
- * @return $this
- */
- public function process(AbstractItem $item)
- {
- $item->setDiscountAmount(0);
- $item->setBaseDiscountAmount(0);
- $item->setDiscountPercent(0);
- $itemPrice = $this->getItemPrice($item);
- if ($itemPrice < 0) {
- return $this;
- }
- $appliedRuleIds = $this->rulesApplier->applyRules(
- $item,
- $this->_getRules($item->getAddress()),
- $this->_skipActionsValidation,
- $this->getCouponCode()
- );
- $this->rulesApplier->setAppliedRuleIds($item, $appliedRuleIds);
- return $this;
- }
- /**
- * Apply discounts to shipping amount
- *
- * @param Address $address
- * @return $this
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- */
- public function processShippingAmount(Address $address)
- {
- $shippingAmount = $address->getShippingAmountForDiscount();
- if ($shippingAmount !== null) {
- $baseShippingAmount = $address->getBaseShippingAmountForDiscount();
- } else {
- $shippingAmount = $address->getShippingAmount();
- $baseShippingAmount = $address->getBaseShippingAmount();
- }
- $quote = $address->getQuote();
- $appliedRuleIds = [];
- foreach ($this->_getRules($address) as $rule) {
- /* @var \Magento\SalesRule\Model\Rule $rule */
- if (!$rule->getApplyToShipping() || !$this->validatorUtility->canProcessRule($rule, $address)) {
- continue;
- }
- $discountAmount = 0;
- $baseDiscountAmount = 0;
- $rulePercent = min(100, $rule->getDiscountAmount());
- switch ($rule->getSimpleAction()) {
- case \Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION:
- $rulePercent = max(0, 100 - $rule->getDiscountAmount());
- // break is intentionally omitted
- case \Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION:
- $discountAmount = ($shippingAmount - $address->getShippingDiscountAmount()) * $rulePercent / 100;
- $baseDiscountAmount = ($baseShippingAmount -
- $address->getBaseShippingDiscountAmount()) * $rulePercent / 100;
- $discountPercent = min(100, $address->getShippingDiscountPercent() + $rulePercent);
- $address->setShippingDiscountPercent($discountPercent);
- break;
- case \Magento\SalesRule\Model\Rule::TO_FIXED_ACTION:
- $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore());
- $discountAmount = $shippingAmount - $quoteAmount;
- $baseDiscountAmount = $baseShippingAmount - $rule->getDiscountAmount();
- break;
- case \Magento\SalesRule\Model\Rule::BY_FIXED_ACTION:
- $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore());
- $discountAmount = $quoteAmount;
- $baseDiscountAmount = $rule->getDiscountAmount();
- break;
- case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION:
- $cartRules = $address->getCartFixedRules();
- if (!isset($cartRules[$rule->getId()])) {
- $cartRules[$rule->getId()] = $rule->getDiscountAmount();
- }
- if ($cartRules[$rule->getId()] > 0) {
- $quoteAmount = $this->priceCurrency->convert($cartRules[$rule->getId()], $quote->getStore());
- $discountAmount = min($shippingAmount - $address->getShippingDiscountAmount(), $quoteAmount);
- $baseDiscountAmount = min(
- $baseShippingAmount - $address->getBaseShippingDiscountAmount(),
- $cartRules[$rule->getId()]
- );
- $cartRules[$rule->getId()] -= $baseDiscountAmount;
- }
- $address->setCartFixedRules($cartRules);
- break;
- }
- $discountAmount = min($address->getShippingDiscountAmount() + $discountAmount, $shippingAmount);
- $baseDiscountAmount = min(
- $address->getBaseShippingDiscountAmount() + $baseDiscountAmount,
- $baseShippingAmount
- );
- $address->setShippingDiscountAmount($discountAmount);
- $address->setBaseShippingDiscountAmount($baseDiscountAmount);
- $appliedRuleIds[$rule->getRuleId()] = $rule->getRuleId();
- $this->rulesApplier->maintainAddressCouponCode($address, $rule, $this->getCouponCode());
- $this->rulesApplier->addDiscountDescription($address, $rule);
- if ($rule->getStopRulesProcessing()) {
- break;
- }
- }
- $address->setAppliedRuleIds($this->validatorUtility->mergeIds($address->getAppliedRuleIds(), $appliedRuleIds));
- $quote->setAppliedRuleIds($this->validatorUtility->mergeIds($quote->getAppliedRuleIds(), $appliedRuleIds));
- return $this;
- }
- /**
- * Calculate quote totals for each rule and save results
- *
- * @param mixed $items
- * @param Address $address
- * @return $this
- */
- public function initTotals($items, Address $address)
- {
- $address->setCartFixedRules([]);
- if (!$items) {
- return $this;
- }
- /** @var \Magento\SalesRule\Model\Rule $rule */
- foreach ($this->_getRules($address) as $rule) {
- if (\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION == $rule->getSimpleAction()
- && $this->validatorUtility->canProcessRule($rule, $address)
- ) {
- $ruleTotalItemsPrice = 0;
- $ruleTotalBaseItemsPrice = 0;
- $validItemsCount = 0;
- foreach ($items as $item) {
- //Skipping child items to avoid double calculations
- if ($item->getParentItemId()) {
- continue;
- }
- if (!$rule->getActions()->validate($item)) {
- continue;
- }
- if (!$this->canApplyDiscount($item)) {
- continue;
- }
- $qty = $this->validatorUtility->getItemQty($item, $rule);
- $ruleTotalItemsPrice += $this->getItemPrice($item) * $qty;
- $ruleTotalBaseItemsPrice += $this->getItemBasePrice($item) * $qty;
- $validItemsCount++;
- }
- $this->_rulesItemTotals[$rule->getId()] = [
- 'items_price' => $ruleTotalItemsPrice,
- 'base_items_price' => $ruleTotalBaseItemsPrice,
- 'items_count' => $validItemsCount,
- ];
- }
- }
- return $this;
- }
- /**
- * Return item price
- *
- * @param AbstractItem $item
- * @return float
- */
- public function getItemPrice($item)
- {
- $price = $item->getDiscountCalculationPrice();
- $calcPrice = $item->getCalculationPrice();
- return $price === null ? $calcPrice : $price;
- }
- /**
- * Return item original price
- *
- * @param AbstractItem $item
- * @return float
- */
- public function getItemOriginalPrice($item)
- {
- return $this->_catalogData->getTaxPrice($item, $item->getOriginalPrice(), true);
- }
- /**
- * Return item base price
- *
- * @param AbstractItem $item
- * @return float
- */
- public function getItemBasePrice($item)
- {
- $price = $item->getDiscountCalculationPrice();
- return $price !== null ? $item->getBaseDiscountCalculationPrice() : $item->getBaseCalculationPrice();
- }
- /**
- * Return item base original price
- *
- * @param AbstractItem $item
- * @return float
- */
- public function getItemBaseOriginalPrice($item)
- {
- return $this->_catalogData->getTaxPrice($item, $item->getBaseOriginalPrice(), true);
- }
- /**
- * Convert address discount description array to string
- *
- * @param Address $address
- * @param string $separator
- * @return $this
- */
- public function prepareDescription($address, $separator = ', ')
- {
- $descriptionArray = $address->getDiscountDescriptionArray();
- if (!$descriptionArray && $address->getQuote()->getItemVirtualQty() > 0) {
- $descriptionArray = $address->getQuote()->getBillingAddress()->getDiscountDescriptionArray();
- }
- $description = $descriptionArray && is_array(
- $descriptionArray
- ) ? implode(
- $separator,
- array_unique($descriptionArray)
- ) : '';
- $address->setDiscountDescription($description);
- return $this;
- }
- /**
- * Return items list sorted by possibility to apply prioritized rules
- *
- * @param array $items
- * @param Address $address
- * @return array $items
- */
- public function sortItemsByPriority($items, Address $address = null)
- {
- $itemsSorted = [];
- /** @var $rule \Magento\SalesRule\Model\Rule */
- foreach ($this->_getRules($address) as $rule) {
- foreach ($items as $itemKey => $itemValue) {
- if ($rule->getActions()->validate($itemValue)) {
- unset($items[$itemKey]);
- $itemsSorted[] = $itemValue;
- }
- }
- }
- if (!empty($itemsSorted)) {
- $items = array_merge($itemsSorted, $items);
- }
- return $items;
- }
- /**
- * @param int $key
- * @return array
- * @throws \Magento\Framework\Exception\LocalizedException
- */
- public function getRuleItemTotalsInfo($key)
- {
- if (empty($this->_rulesItemTotals[$key])) {
- throw new \Magento\Framework\Exception\LocalizedException(__('Item totals are not set for the rule.'));
- }
- return $this->_rulesItemTotals[$key];
- }
- /**
- * @param int $key
- * @return $this
- */
- public function decrementRuleItemTotalsCount($key)
- {
- $this->_rulesItemTotals[$key]['items_count']--;
- return $this;
- }
- /**
- * Check if we can apply discount to current QuoteItem
- *
- * @param AbstractItem $item
- * @return bool
- */
- public function canApplyDiscount(AbstractItem $item)
- {
- $result = true;
- /** @var \Zend_Validate_Interface $validator */
- foreach ($this->validators->getValidators('discount') as $validator) {
- $result = $validator->isValid($item);
- if (!$result) {
- break;
- }
- }
- return $result;
- }
- }
|