Validator.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\SalesRule\Model;
  7. use Magento\Quote\Model\Quote\Address;
  8. use Magento\Quote\Model\Quote\Item\AbstractItem;
  9. /**
  10. * SalesRule Validator Model
  11. *
  12. * Allows dispatching before and after events for each controller action
  13. *
  14. * @method mixed getCouponCode()
  15. * @method Validator setCouponCode($code)
  16. * @method mixed getWebsiteId()
  17. * @method Validator setWebsiteId($id)
  18. * @method mixed getCustomerGroupId()
  19. * @method Validator setCustomerGroupId($id)
  20. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  21. */
  22. class Validator extends \Magento\Framework\Model\AbstractModel
  23. {
  24. /**
  25. * Rule source collection
  26. *
  27. * @var \Magento\SalesRule\Model\ResourceModel\Rule\Collection
  28. */
  29. protected $_rules;
  30. /**
  31. * Defines if method \Magento\SalesRule\Model\Validator::reset() wasn't called
  32. * Used for clearing applied rule ids in Quote and in Address
  33. *
  34. * @var bool
  35. */
  36. protected $_isFirstTimeResetRun = true;
  37. /**
  38. * Information about item totals for rules
  39. *
  40. * @var array
  41. */
  42. protected $_rulesItemTotals = [];
  43. /**
  44. * Skip action rules validation flag
  45. *
  46. * @var bool
  47. */
  48. protected $_skipActionsValidation = false;
  49. /**
  50. * Catalog data
  51. *
  52. * @var \Magento\Catalog\Helper\Data|null
  53. */
  54. protected $_catalogData = null;
  55. /**
  56. * @var \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory
  57. */
  58. protected $_collectionFactory;
  59. /**
  60. * @var \Magento\SalesRule\Model\Utility
  61. */
  62. protected $validatorUtility;
  63. /**
  64. * @var \Magento\SalesRule\Model\RulesApplier
  65. */
  66. protected $rulesApplier;
  67. /**
  68. * @var \Magento\Framework\Pricing\PriceCurrencyInterface
  69. */
  70. protected $priceCurrency;
  71. /**
  72. * @var Validator\Pool
  73. */
  74. protected $validators;
  75. /**
  76. * @var \Magento\Framework\Message\ManagerInterface
  77. */
  78. protected $messageManager;
  79. /**
  80. * Counter is used for assigning temporary id to quote address
  81. *
  82. * @var int
  83. */
  84. protected $counter = 0;
  85. /**
  86. * @param \Magento\Framework\Model\Context $context
  87. * @param \Magento\Framework\Registry $registry
  88. * @param \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory $collectionFactory
  89. * @param \Magento\Catalog\Helper\Data $catalogData
  90. * @param Utility $utility
  91. * @param RulesApplier $rulesApplier
  92. * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
  93. * @param Validator\Pool $validators
  94. * @param \Magento\Framework\Message\ManagerInterface $messageManager
  95. * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
  96. * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
  97. * @param array $data
  98. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  99. */
  100. public function __construct(
  101. \Magento\Framework\Model\Context $context,
  102. \Magento\Framework\Registry $registry,
  103. \Magento\SalesRule\Model\ResourceModel\Rule\CollectionFactory $collectionFactory,
  104. \Magento\Catalog\Helper\Data $catalogData,
  105. \Magento\SalesRule\Model\Utility $utility,
  106. \Magento\SalesRule\Model\RulesApplier $rulesApplier,
  107. \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
  108. \Magento\SalesRule\Model\Validator\Pool $validators,
  109. \Magento\Framework\Message\ManagerInterface $messageManager,
  110. \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
  111. \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
  112. array $data = []
  113. ) {
  114. $this->_collectionFactory = $collectionFactory;
  115. $this->_catalogData = $catalogData;
  116. $this->validatorUtility = $utility;
  117. $this->rulesApplier = $rulesApplier;
  118. $this->priceCurrency = $priceCurrency;
  119. $this->validators = $validators;
  120. $this->messageManager = $messageManager;
  121. parent::__construct($context, $registry, $resource, $resourceCollection, $data);
  122. }
  123. /**
  124. * Init validator
  125. * Init process load collection of rules for specific website,
  126. * customer group and coupon code
  127. *
  128. * @param int $websiteId
  129. * @param int $customerGroupId
  130. * @param string $couponCode
  131. * @return $this
  132. */
  133. public function init($websiteId, $customerGroupId, $couponCode)
  134. {
  135. $this->setWebsiteId($websiteId)->setCustomerGroupId($customerGroupId)->setCouponCode($couponCode);
  136. return $this;
  137. }
  138. /**
  139. * Get rules collection for current object state
  140. *
  141. * @param Address|null $address
  142. * @return \Magento\SalesRule\Model\ResourceModel\Rule\Collection
  143. */
  144. protected function _getRules(Address $address = null)
  145. {
  146. $addressId = $this->getAddressId($address);
  147. $key = $this->getWebsiteId() . '_'
  148. . $this->getCustomerGroupId() . '_'
  149. . $this->getCouponCode() . '_'
  150. . $addressId;
  151. if (!isset($this->_rules[$key])) {
  152. $this->_rules[$key] = $this->_collectionFactory->create()
  153. ->setValidationFilter(
  154. $this->getWebsiteId(),
  155. $this->getCustomerGroupId(),
  156. $this->getCouponCode(),
  157. null,
  158. $address
  159. )
  160. ->addFieldToFilter('is_active', 1)
  161. ->load();
  162. }
  163. return $this->_rules[$key];
  164. }
  165. /**
  166. * @param Address $address
  167. * @return string
  168. */
  169. protected function getAddressId(Address $address)
  170. {
  171. if ($address == null) {
  172. return '';
  173. }
  174. if (!$address->hasData('address_sales_rule_id')) {
  175. if ($address->hasData('address_id')) {
  176. $address->setData('address_sales_rule_id', $address->getData('address_id'));
  177. } else {
  178. $type = $address->getAddressType();
  179. $tempId = $type . $this->counter++;
  180. $address->setData('address_sales_rule_id', $tempId);
  181. }
  182. }
  183. return $address->getData('address_sales_rule_id');
  184. }
  185. /**
  186. * Set skip actions validation flag
  187. *
  188. * @param bool $flag
  189. * @return $this
  190. */
  191. public function setSkipActionsValidation($flag)
  192. {
  193. $this->_skipActionsValidation = $flag;
  194. return $this;
  195. }
  196. /**
  197. * Can apply rules check
  198. *
  199. * @param AbstractItem $item
  200. * @return bool
  201. */
  202. public function canApplyRules(AbstractItem $item)
  203. {
  204. $address = $item->getAddress();
  205. foreach ($this->_getRules($address) as $rule) {
  206. if (!$this->validatorUtility->canProcessRule($rule, $address) || !$rule->getActions()->validate($item)) {
  207. return false;
  208. }
  209. }
  210. return true;
  211. }
  212. /**
  213. * Reset quote and address applied rules
  214. *
  215. * @param Address $address
  216. * @return $this
  217. */
  218. public function reset(Address $address)
  219. {
  220. $this->validatorUtility->resetRoundingDeltas();
  221. if ($this->_isFirstTimeResetRun) {
  222. $address->setAppliedRuleIds('');
  223. $address->getQuote()->setAppliedRuleIds('');
  224. $this->_isFirstTimeResetRun = false;
  225. }
  226. return $this;
  227. }
  228. /**
  229. * Quote item discount calculation process
  230. *
  231. * @param AbstractItem $item
  232. * @return $this
  233. */
  234. public function process(AbstractItem $item)
  235. {
  236. $item->setDiscountAmount(0);
  237. $item->setBaseDiscountAmount(0);
  238. $item->setDiscountPercent(0);
  239. $itemPrice = $this->getItemPrice($item);
  240. if ($itemPrice < 0) {
  241. return $this;
  242. }
  243. $appliedRuleIds = $this->rulesApplier->applyRules(
  244. $item,
  245. $this->_getRules($item->getAddress()),
  246. $this->_skipActionsValidation,
  247. $this->getCouponCode()
  248. );
  249. $this->rulesApplier->setAppliedRuleIds($item, $appliedRuleIds);
  250. return $this;
  251. }
  252. /**
  253. * Apply discounts to shipping amount
  254. *
  255. * @param Address $address
  256. * @return $this
  257. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  258. */
  259. public function processShippingAmount(Address $address)
  260. {
  261. $shippingAmount = $address->getShippingAmountForDiscount();
  262. if ($shippingAmount !== null) {
  263. $baseShippingAmount = $address->getBaseShippingAmountForDiscount();
  264. } else {
  265. $shippingAmount = $address->getShippingAmount();
  266. $baseShippingAmount = $address->getBaseShippingAmount();
  267. }
  268. $quote = $address->getQuote();
  269. $appliedRuleIds = [];
  270. foreach ($this->_getRules($address) as $rule) {
  271. /* @var \Magento\SalesRule\Model\Rule $rule */
  272. if (!$rule->getApplyToShipping() || !$this->validatorUtility->canProcessRule($rule, $address)) {
  273. continue;
  274. }
  275. $discountAmount = 0;
  276. $baseDiscountAmount = 0;
  277. $rulePercent = min(100, $rule->getDiscountAmount());
  278. switch ($rule->getSimpleAction()) {
  279. case \Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION:
  280. $rulePercent = max(0, 100 - $rule->getDiscountAmount());
  281. // break is intentionally omitted
  282. case \Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION:
  283. $discountAmount = ($shippingAmount - $address->getShippingDiscountAmount()) * $rulePercent / 100;
  284. $baseDiscountAmount = ($baseShippingAmount -
  285. $address->getBaseShippingDiscountAmount()) * $rulePercent / 100;
  286. $discountPercent = min(100, $address->getShippingDiscountPercent() + $rulePercent);
  287. $address->setShippingDiscountPercent($discountPercent);
  288. break;
  289. case \Magento\SalesRule\Model\Rule::TO_FIXED_ACTION:
  290. $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore());
  291. $discountAmount = $shippingAmount - $quoteAmount;
  292. $baseDiscountAmount = $baseShippingAmount - $rule->getDiscountAmount();
  293. break;
  294. case \Magento\SalesRule\Model\Rule::BY_FIXED_ACTION:
  295. $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore());
  296. $discountAmount = $quoteAmount;
  297. $baseDiscountAmount = $rule->getDiscountAmount();
  298. break;
  299. case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION:
  300. $cartRules = $address->getCartFixedRules();
  301. if (!isset($cartRules[$rule->getId()])) {
  302. $cartRules[$rule->getId()] = $rule->getDiscountAmount();
  303. }
  304. if ($cartRules[$rule->getId()] > 0) {
  305. $quoteAmount = $this->priceCurrency->convert($cartRules[$rule->getId()], $quote->getStore());
  306. $discountAmount = min($shippingAmount - $address->getShippingDiscountAmount(), $quoteAmount);
  307. $baseDiscountAmount = min(
  308. $baseShippingAmount - $address->getBaseShippingDiscountAmount(),
  309. $cartRules[$rule->getId()]
  310. );
  311. $cartRules[$rule->getId()] -= $baseDiscountAmount;
  312. }
  313. $address->setCartFixedRules($cartRules);
  314. break;
  315. }
  316. $discountAmount = min($address->getShippingDiscountAmount() + $discountAmount, $shippingAmount);
  317. $baseDiscountAmount = min(
  318. $address->getBaseShippingDiscountAmount() + $baseDiscountAmount,
  319. $baseShippingAmount
  320. );
  321. $address->setShippingDiscountAmount($discountAmount);
  322. $address->setBaseShippingDiscountAmount($baseDiscountAmount);
  323. $appliedRuleIds[$rule->getRuleId()] = $rule->getRuleId();
  324. $this->rulesApplier->maintainAddressCouponCode($address, $rule, $this->getCouponCode());
  325. $this->rulesApplier->addDiscountDescription($address, $rule);
  326. if ($rule->getStopRulesProcessing()) {
  327. break;
  328. }
  329. }
  330. $address->setAppliedRuleIds($this->validatorUtility->mergeIds($address->getAppliedRuleIds(), $appliedRuleIds));
  331. $quote->setAppliedRuleIds($this->validatorUtility->mergeIds($quote->getAppliedRuleIds(), $appliedRuleIds));
  332. return $this;
  333. }
  334. /**
  335. * Calculate quote totals for each rule and save results
  336. *
  337. * @param mixed $items
  338. * @param Address $address
  339. * @return $this
  340. */
  341. public function initTotals($items, Address $address)
  342. {
  343. $address->setCartFixedRules([]);
  344. if (!$items) {
  345. return $this;
  346. }
  347. /** @var \Magento\SalesRule\Model\Rule $rule */
  348. foreach ($this->_getRules($address) as $rule) {
  349. if (\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION == $rule->getSimpleAction()
  350. && $this->validatorUtility->canProcessRule($rule, $address)
  351. ) {
  352. $ruleTotalItemsPrice = 0;
  353. $ruleTotalBaseItemsPrice = 0;
  354. $validItemsCount = 0;
  355. foreach ($items as $item) {
  356. //Skipping child items to avoid double calculations
  357. if ($item->getParentItemId()) {
  358. continue;
  359. }
  360. if (!$rule->getActions()->validate($item)) {
  361. continue;
  362. }
  363. if (!$this->canApplyDiscount($item)) {
  364. continue;
  365. }
  366. $qty = $this->validatorUtility->getItemQty($item, $rule);
  367. $ruleTotalItemsPrice += $this->getItemPrice($item) * $qty;
  368. $ruleTotalBaseItemsPrice += $this->getItemBasePrice($item) * $qty;
  369. $validItemsCount++;
  370. }
  371. $this->_rulesItemTotals[$rule->getId()] = [
  372. 'items_price' => $ruleTotalItemsPrice,
  373. 'base_items_price' => $ruleTotalBaseItemsPrice,
  374. 'items_count' => $validItemsCount,
  375. ];
  376. }
  377. }
  378. return $this;
  379. }
  380. /**
  381. * Return item price
  382. *
  383. * @param AbstractItem $item
  384. * @return float
  385. */
  386. public function getItemPrice($item)
  387. {
  388. $price = $item->getDiscountCalculationPrice();
  389. $calcPrice = $item->getCalculationPrice();
  390. return $price === null ? $calcPrice : $price;
  391. }
  392. /**
  393. * Return item original price
  394. *
  395. * @param AbstractItem $item
  396. * @return float
  397. */
  398. public function getItemOriginalPrice($item)
  399. {
  400. return $this->_catalogData->getTaxPrice($item, $item->getOriginalPrice(), true);
  401. }
  402. /**
  403. * Return item base price
  404. *
  405. * @param AbstractItem $item
  406. * @return float
  407. */
  408. public function getItemBasePrice($item)
  409. {
  410. $price = $item->getDiscountCalculationPrice();
  411. return $price !== null ? $item->getBaseDiscountCalculationPrice() : $item->getBaseCalculationPrice();
  412. }
  413. /**
  414. * Return item base original price
  415. *
  416. * @param AbstractItem $item
  417. * @return float
  418. */
  419. public function getItemBaseOriginalPrice($item)
  420. {
  421. return $this->_catalogData->getTaxPrice($item, $item->getBaseOriginalPrice(), true);
  422. }
  423. /**
  424. * Convert address discount description array to string
  425. *
  426. * @param Address $address
  427. * @param string $separator
  428. * @return $this
  429. */
  430. public function prepareDescription($address, $separator = ', ')
  431. {
  432. $descriptionArray = $address->getDiscountDescriptionArray();
  433. if (!$descriptionArray && $address->getQuote()->getItemVirtualQty() > 0) {
  434. $descriptionArray = $address->getQuote()->getBillingAddress()->getDiscountDescriptionArray();
  435. }
  436. $description = $descriptionArray && is_array(
  437. $descriptionArray
  438. ) ? implode(
  439. $separator,
  440. array_unique($descriptionArray)
  441. ) : '';
  442. $address->setDiscountDescription($description);
  443. return $this;
  444. }
  445. /**
  446. * Return items list sorted by possibility to apply prioritized rules
  447. *
  448. * @param array $items
  449. * @param Address $address
  450. * @return array $items
  451. */
  452. public function sortItemsByPriority($items, Address $address = null)
  453. {
  454. $itemsSorted = [];
  455. /** @var $rule \Magento\SalesRule\Model\Rule */
  456. foreach ($this->_getRules($address) as $rule) {
  457. foreach ($items as $itemKey => $itemValue) {
  458. if ($rule->getActions()->validate($itemValue)) {
  459. unset($items[$itemKey]);
  460. $itemsSorted[] = $itemValue;
  461. }
  462. }
  463. }
  464. if (!empty($itemsSorted)) {
  465. $items = array_merge($itemsSorted, $items);
  466. }
  467. return $items;
  468. }
  469. /**
  470. * @param int $key
  471. * @return array
  472. * @throws \Magento\Framework\Exception\LocalizedException
  473. */
  474. public function getRuleItemTotalsInfo($key)
  475. {
  476. if (empty($this->_rulesItemTotals[$key])) {
  477. throw new \Magento\Framework\Exception\LocalizedException(__('Item totals are not set for the rule.'));
  478. }
  479. return $this->_rulesItemTotals[$key];
  480. }
  481. /**
  482. * @param int $key
  483. * @return $this
  484. */
  485. public function decrementRuleItemTotalsCount($key)
  486. {
  487. $this->_rulesItemTotals[$key]['items_count']--;
  488. return $this;
  489. }
  490. /**
  491. * Check if we can apply discount to current QuoteItem
  492. *
  493. * @param AbstractItem $item
  494. * @return bool
  495. */
  496. public function canApplyDiscount(AbstractItem $item)
  497. {
  498. $result = true;
  499. /** @var \Zend_Validate_Interface $validator */
  500. foreach ($this->validators->getValidators('discount') as $validator) {
  501. $result = $validator->isValid($item);
  502. if (!$result) {
  503. break;
  504. }
  505. }
  506. return $result;
  507. }
  508. }