Rules.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. <?php
  2. namespace Dotdigitalgroup\Email\Model;
  3. use Dotdigitalgroup\Email\Model\Config\Json;
  4. class Rules extends \Magento\Framework\Model\AbstractModel
  5. {
  6. /**
  7. * Exclusion Rule for Abandoned Cart.
  8. */
  9. const ABANDONED = 1;
  10. /**
  11. * Exclusion Rule for Product Review.
  12. */
  13. const REVIEW = 2;
  14. /**
  15. * Condition combination all.
  16. */
  17. const COMBINATION_TYPE_ALL = 1;
  18. /**
  19. * Condition combination any.
  20. */
  21. const COMBINATION_TYPE_ANY = 2;
  22. /**
  23. * @var int
  24. */
  25. public $ruleType;
  26. /**
  27. * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory
  28. */
  29. private $quoteCollectionFactory;
  30. /**
  31. * @var ResourceModel\Rules
  32. */
  33. private $rulesResource;
  34. /**
  35. * @var array
  36. */
  37. private $conditionMap;
  38. /**
  39. * @var array
  40. */
  41. private $defaultOptions;
  42. /**
  43. * @var array
  44. */
  45. public $attributeMapForQuote;
  46. /**
  47. * @var array
  48. */
  49. private $attributeMapForOrder;
  50. /**
  51. * @var array
  52. */
  53. private $productAttribute;
  54. /**
  55. * @var array
  56. */
  57. private $used = [];
  58. /**
  59. * @var Adminhtml\Source\Rules\Type
  60. */
  61. private $rulesType;
  62. /**
  63. * @var \Magento\Eav\Model\Config
  64. */
  65. private $config;
  66. /**
  67. * @var Json
  68. */
  69. private $serializer;
  70. /**
  71. * Rules constructor.
  72. * @param \Magento\Framework\Model\Context $context
  73. * @param \Magento\Framework\Registry $registry
  74. * @param \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $quoteCollectionFactory
  75. * @param Adminhtml\Source\Rules\Type $rulesType
  76. * @param \Magento\Eav\Model\Config $config
  77. * @param Json $serializer
  78. * @param ResourceModel\Rules $rulesResource
  79. * @param array $data
  80. * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource
  81. * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection
  82. */
  83. public function __construct(
  84. \Magento\Framework\Model\Context $context,
  85. \Magento\Framework\Registry $registry,
  86. \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $quoteCollectionFactory,
  87. \Dotdigitalgroup\Email\Model\Adminhtml\Source\Rules\Type $rulesType,
  88. \Magento\Eav\Model\Config $config,
  89. \Dotdigitalgroup\Email\Model\Config\Json $serializer,
  90. \Dotdigitalgroup\Email\Model\ResourceModel\Rules $rulesResource,
  91. array $data = [],
  92. \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
  93. \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null
  94. ) {
  95. $this->serializer = $serializer;
  96. $this->config = $config;
  97. $this->rulesType = $rulesType;
  98. $this->rulesResource = $rulesResource;
  99. $this->quoteCollectionFactory = $quoteCollectionFactory;
  100. parent::__construct(
  101. $context,
  102. $registry,
  103. $resource,
  104. $resourceCollection,
  105. $data
  106. );
  107. }
  108. /**
  109. * Construct.
  110. *
  111. * @return null
  112. */
  113. public function _construct()
  114. {
  115. $this->defaultOptions = $this->rulesType->defaultOptions();
  116. $this->conditionMap = [
  117. 'eq' => 'neq',
  118. 'neq' => 'eq',
  119. 'gteq' => 'lteq',
  120. 'lteq' => 'gteq',
  121. 'gt' => 'lt',
  122. 'lt' => 'gt',
  123. 'like' => 'nlike',
  124. 'nlike' => 'like',
  125. ];
  126. $this->attributeMapForQuote = [
  127. 'method' => 'method',
  128. 'shipping_method' => 'shipping_method',
  129. 'country_id' => 'country_id',
  130. 'city' => 'city',
  131. 'region_id' => 'region_id',
  132. 'customer_group_id' => 'main_table.customer_group_id',
  133. 'coupon_code' => 'main_table.coupon_code',
  134. 'subtotal' => 'main_table.subtotal',
  135. 'grand_total' => 'main_table.grand_total',
  136. 'items_qty' => 'main_table.items_qty',
  137. 'customer_email' => 'main_table.customer_email',
  138. ];
  139. $this->attributeMapForOrder = [
  140. 'method' => 'method',
  141. 'shipping_method' => 'main_table.shipping_method',
  142. 'country_id' => 'country_id',
  143. 'city' => 'city',
  144. 'region_id' => 'region_id',
  145. 'customer_group_id' => 'main_table.customer_group_id',
  146. 'coupon_code' => 'main_table.coupon_code',
  147. 'subtotal' => 'main_table.subtotal',
  148. 'grand_total' => 'main_table.grand_total',
  149. 'items_qty' => 'items_qty',
  150. 'customer_email' => 'main_table.customer_email',
  151. ];
  152. parent::_construct();
  153. $this->_init(\Dotdigitalgroup\Email\Model\ResourceModel\Rules::class);
  154. }
  155. /**
  156. * @return $this
  157. */
  158. public function beforeSave()
  159. {
  160. parent::beforeSave();
  161. if ($this->isObjectNew()) {
  162. $this->setCreatedAt(time());
  163. } else {
  164. $this->setUpdatedAt(time());
  165. }
  166. $this->setConditions($this->serializer->serialize($this->getConditions()));
  167. $this->setWebsiteIds(implode(',', $this->getWebsiteIds()));
  168. return $this;
  169. }
  170. /**
  171. * Check if rule already exist for website.
  172. *
  173. * @param int $websiteId
  174. * @param string $type
  175. * @param bool $ruleId
  176. *
  177. * @return bool
  178. */
  179. public function checkWebsiteBeforeSave($websiteId, $type, $ruleId = false)
  180. {
  181. return $this->getCollection()
  182. ->hasCollectionAnyItemsByWebsiteAndType($websiteId, $type, $ruleId);
  183. }
  184. /**
  185. * Get rule for website.
  186. *
  187. * @param string $type
  188. * @param int $websiteId
  189. *
  190. * @return array|\Dotdigitalgroup\Email\Model\Rules
  191. */
  192. public function getActiveRuleForWebsite($type, $websiteId)
  193. {
  194. return $this->getCollection()
  195. ->getActiveRuleByWebsiteAndType($type, $websiteId);
  196. }
  197. /**
  198. * Process rule on collection.
  199. *
  200. * @param \Magento\Sales\Model\ResourceModel\Order\Collection|
  201. * \Magento\Quote\Model\ResourceModel\Quote\Collection $collection
  202. * @param string $type
  203. * @param int $websiteId
  204. *
  205. * @return \Magento\Sales\Model\ResourceModel\Order\Collection|
  206. * \Magento\Quote\Model\ResourceModel\Quote\Collection $collection
  207. */
  208. public function process($collection, $type, $websiteId)
  209. {
  210. $this->ruleType = $type;
  211. $emailRules = $this->getActiveRuleForWebsite($type, $websiteId);
  212. //if no rule or condition then return the collection untouched
  213. if (empty($emailRules)) {
  214. return $collection;
  215. }
  216. $condition = $this->serializer->unserialize($emailRules->getConditions());
  217. if (empty($condition)) {
  218. return $collection;
  219. }
  220. //process rule on collection according to combination
  221. $combination = $emailRules->getCombination();
  222. //join tables to collection according to type
  223. $collection = $this->rulesResource->joinTablesOnCollectionByType($collection, $type);
  224. if ($combination == self::COMBINATION_TYPE_ALL) {
  225. $collection = $this->processAndCombination($collection, $condition);
  226. }
  227. if ($combination == self::COMBINATION_TYPE_ANY) {
  228. $collection = $this->processOrCombination($collection, $condition);
  229. }
  230. return $collection;
  231. }
  232. /**
  233. * Process And combination on collection.
  234. *
  235. * @param \Magento\Sales\Model\ResourceModel\Order\Collection|
  236. * \Magento\Quote\Model\ResourceModel\Quote\Collectio $collection
  237. * @param array $conditions
  238. *
  239. * @return \Magento\Sales\Model\ResourceModel\Order\Collection|
  240. * \Magento\Quote\Model\ResourceModel\Quote\Collection $collection
  241. */
  242. public function processAndCombination($collection, $conditions)
  243. {
  244. foreach ($conditions as $condition) {
  245. $attribute = $condition['attribute'];
  246. $cond = $condition['conditions'];
  247. $value = $condition['cvalue'];
  248. //ignore condition if value is null or empty
  249. if ($value == '' || $value == null) {
  250. continue;
  251. }
  252. //ignore conditions for already used attribute
  253. if (in_array($attribute, $this->used)) {
  254. continue;
  255. }
  256. //set used to check later
  257. $this->used[] = $attribute;
  258. //product review
  259. if ($this->ruleType == self::REVIEW && isset($this->attributeMapForQuote[$attribute])) {
  260. $attribute = $this->attributeMapForOrder[$attribute];
  261. //abandoned cart
  262. } elseif ($this->ruleType == self::ABANDONED && isset($this->attributeMapForOrder[$attribute])) {
  263. $attribute = $this->attributeMapForQuote[$attribute];
  264. } else {
  265. $this->productAttribute[] = $condition;
  266. continue;
  267. }
  268. $collection = $this->processProcessAndCombinationCondition($collection, $cond, $value, $attribute);
  269. }
  270. return $this->processProductAttributes($collection);
  271. }
  272. /**
  273. * @param \Magento\Sales\Model\ResourceModel\Order\Collection|
  274. * \Magento\Quote\Model\ResourceModel\Quote\Collection $collection
  275. * @param string $cond
  276. * @param string $value
  277. * @param string $attribute
  278. *
  279. * @return null
  280. */
  281. private function processProcessAndCombinationCondition($collection, $cond, $value, $attribute)
  282. {
  283. if ($cond == 'null') {
  284. if ($value == '1') {
  285. $condition = ['notnull' => true];
  286. } elseif ($value == '0') {
  287. $condition = [$cond => true];
  288. }
  289. } else {
  290. if ($cond == 'like' or $cond == 'nlike') {
  291. $value = '%' . $value . '%';
  292. }
  293. //condition with null values can't be filter using sting, inlude to filter null values
  294. $conditionMap = [$this->conditionMap[$cond] => $value];
  295. if ($cond == 'eq' || $cond == 'neq') {
  296. $conditionMap[] = ['null' => true];
  297. }
  298. $condition = $conditionMap;
  299. }
  300. //filter by quote attribute
  301. if ($attribute == 'items_qty' && $this->ruleType == self::REVIEW) {
  302. $collection = $this->filterCollectionByQuoteAttribute($collection, $attribute, $condition);
  303. } else {
  304. $collection->addFieldToFilter($attribute, $condition);
  305. }
  306. return $collection;
  307. }
  308. /**
  309. * process Or combination on collection.
  310. *
  311. * @param \Magento\Sales\Model\ResourceModel\Order\Collection|
  312. * \Magento\Quote\Model\ResourceModel\Quote\Collectio $collection
  313. * @param array $conditions
  314. * @param string $type
  315. *
  316. * @return \Magento\Sales\Model\ResourceModel\Order\Collection|
  317. * \Magento\Quote\Model\ResourceModel\Quote\Collection $collection
  318. *
  319. * @SuppressWarnings(PHPMD.NPathComplexity)
  320. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  321. */
  322. public function processOrCombination($collection, $conditions)
  323. {
  324. $fieldsConditions = [];
  325. $multiFieldsConditions = [];
  326. foreach ($conditions as $condition) {
  327. $attribute = $condition['attribute'];
  328. $cond = $condition['conditions'];
  329. $value = $condition['cvalue'];
  330. //ignore condition if value is null or empty
  331. if ($value == '' or $value == null) {
  332. continue;
  333. }
  334. if ($this->ruleType == self::REVIEW && isset($this->attributeMapForQuote[$attribute])) {
  335. $attribute = $this->attributeMapForOrder[$attribute];
  336. } elseif ($this->ruleType == self::ABANDONED && isset($this->attributeMapForOrder[$attribute])) {
  337. $attribute = $this->attributeMapForQuote[$attribute];
  338. } else {
  339. $this->productAttribute[] = $condition;
  340. continue;
  341. }
  342. if ($cond == 'null') {
  343. if ($value == '1') {
  344. if (isset($fieldsConditions[$attribute])) {
  345. $multiFieldsConditions[$attribute][]
  346. = ['notnull' => true];
  347. continue;
  348. }
  349. $fieldsConditions[$attribute] = ['notnull' => true];
  350. } elseif ($value == '0') {
  351. if (isset($fieldsConditions[$attribute])) {
  352. $multiFieldsConditions[$attribute][]
  353. = [$cond => true];
  354. continue;
  355. }
  356. $fieldsConditions[$attribute] = [$cond => true];
  357. }
  358. } else {
  359. if ($cond == 'like' or $cond == 'nlike') {
  360. $value = '%' . $value . '%';
  361. }
  362. if (isset($fieldsConditions[$attribute])) {
  363. $multiFieldsConditions[$attribute][]
  364. = [$this->conditionMap[$cond] => $value];
  365. continue;
  366. }
  367. $fieldsConditions[$attribute]
  368. = [$this->conditionMap[$cond] => $value];
  369. }
  370. }
  371. //all rules condition will be with or combination
  372. if (!empty($fieldsConditions)) {
  373. $column = $cond = [];
  374. foreach ($fieldsConditions as $key => $fieldsCondition) {
  375. $column[] = (string)$key;
  376. $cond[] = $fieldsCondition;
  377. if (!empty($multiFieldsConditions[$key])) {
  378. foreach ($multiFieldsConditions[$key] as $multiFieldsCondition) {
  379. $column[] = (string)$key;
  380. $cond[] = $multiFieldsCondition;
  381. }
  382. }
  383. }
  384. $collection->addFieldToFilter(
  385. $column,
  386. $cond
  387. );
  388. }
  389. return $this->processProductAttributes($collection);
  390. }
  391. /**
  392. * Process product attributes on collection.
  393. *
  394. * @param \Magento\Sales\Model\ResourceModel\Order\Collection|
  395. * \Magento\Quote\Model\ResourceModel\Quote\Collectio $collection
  396. *
  397. * @return \Magento\Sales\Model\ResourceModel\Order\Collection|
  398. * \Magento\Quote\Model\ResourceModel\Quote\Collection $collection
  399. */
  400. private function processProductAttributes($collection)
  401. {
  402. //if no product attribute or collection empty return collection
  403. if (empty($this->productAttribute) || !$collection->getSize()) {
  404. return $collection;
  405. }
  406. $collection = $this->processProductAttributesInCollection($collection);
  407. return $collection;
  408. }
  409. /**
  410. * Evaluate two values against condition.
  411. *
  412. * @param string $varOne
  413. * @param string $op
  414. * @param string $varTwo
  415. *
  416. * @return bool
  417. */
  418. public function _evaluate($varOne, $op, $varTwo)
  419. {
  420. switch ($op) {
  421. case 'eq':
  422. return $varOne == $varTwo;
  423. case 'neq':
  424. return $varOne != $varTwo;
  425. case 'gteq':
  426. return $varOne >= $varTwo;
  427. case 'lteq':
  428. return $varOne <= $varTwo;
  429. case 'gt':
  430. return $varOne > $varTwo;
  431. case 'lt':
  432. return $varOne < $varTwo;
  433. }
  434. return false;
  435. }
  436. /**
  437. * Process product attributes on collection.
  438. *
  439. * @param \Magento\Sales\Model\ResourceModel\Order\Collection|
  440. * \Magento\Quote\Model\ResourceModel\Quote\Collectio $collection
  441. *
  442. * @return \Magento\Sales\Model\ResourceModel\Order\Collection|
  443. * \Magento\Quote\Model\ResourceModel\Quote\Collection $collection
  444. *
  445. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  446. */
  447. private function processProductAttributesInCollection($collection)
  448. {
  449. foreach ($collection as $collectionItem) {
  450. $items = $collectionItem->getAllItems();
  451. foreach ($items as $item) {
  452. $product = $item->getProduct();
  453. $attributes = $this->getAttributesArrayFromLoadedProduct($product);
  454. foreach ($this->productAttribute as $productAttribute) {
  455. $attribute = $productAttribute['attribute'];
  456. $cond = $productAttribute['conditions'];
  457. $value = $productAttribute['cvalue'];
  458. if ($cond == 'null') {
  459. if ($value == '0') {
  460. $cond = 'neq';
  461. } elseif ($value == '1') {
  462. $cond = 'eq';
  463. }
  464. $value = '';
  465. }
  466. //if attribute is in product's attributes array
  467. if (in_array($attribute, $attributes)) {
  468. $attr = $this->config->getAttribute('catalog_product', $attribute);
  469. //frontend type
  470. $frontType = $attr->getFrontend()->getInputType();
  471. //if type is select
  472. if ($frontType == 'select' or $frontType
  473. == 'multiselect'
  474. ) {
  475. $attributeValue = $product->getAttributeText(
  476. $attribute
  477. );
  478. //evaluate conditions on values. if true then unset item from collection
  479. if ($this->_evaluate(
  480. $value,
  481. $cond,
  482. $attributeValue
  483. )
  484. ) {
  485. $collection->removeItemByKey(
  486. $collectionItem->getId()
  487. );
  488. continue 3;
  489. }
  490. } else {
  491. $getter = 'get';
  492. $exploded = explode('_', $attribute);
  493. foreach ($exploded as $one) {
  494. $getter .= ucfirst($one);
  495. }
  496. $attributeValue = call_user_func(
  497. [$product, $getter]
  498. );
  499. //if retrieved value is an array then loop through all array values.
  500. // example can be categories
  501. if (is_array($attributeValue)) {
  502. foreach ($attributeValue as $attrValue) {
  503. //evaluate conditions on values. if true then unset item from collection
  504. if ($this->_evaluate(
  505. $value,
  506. $cond,
  507. $attrValue
  508. )
  509. ) {
  510. $collection->removeItemByKey(
  511. $collectionItem->getId()
  512. );
  513. continue 3;
  514. }
  515. }
  516. } else {
  517. //evaluate conditions on values. if true then unset item from collection
  518. if ($this->_evaluate(
  519. $value,
  520. $cond,
  521. $attributeValue
  522. )
  523. ) {
  524. $collection->removeItemByKey(
  525. $collectionItem->getId()
  526. );
  527. continue 3;
  528. }
  529. }
  530. }
  531. }
  532. }
  533. }
  534. }
  535. return $collection;
  536. }
  537. /**
  538. * @param \Magento\Catalog\Model\Product $product
  539. *
  540. * @return array
  541. */
  542. private function getAttributesArrayFromLoadedProduct($product)
  543. {
  544. //attributes array from loaded product
  545. $attributes = $this->config->getEntityAttributes(
  546. \Magento\Catalog\Model\Product::ENTITY,
  547. $product
  548. );
  549. return array_keys($attributes);
  550. }
  551. /**
  552. * @param \Magento\Sales\Model\ResourceModel\Order\Collection $collection
  553. * @param string $attribute
  554. * @param array $condition
  555. * @return \Magento\Sales\Model\ResourceModel\Order\Collection
  556. */
  557. private function filterCollectionByQuoteAttribute($collection, $attribute, array $condition)
  558. {
  559. $originalCollection = clone $collection;
  560. $quoteCollection = $this->quoteCollectionFactory->create();
  561. $quoteIds = $originalCollection->getColumnValues('quote_id');
  562. if ($quoteIds) {
  563. $quoteCollection->addFieldToFilter('entity_id', ['in' => $quoteIds])
  564. ->addFieldToFilter($attribute, $condition);
  565. //no need for empty check - because should include the null result, it should work like exclusion filter!
  566. $collection->addFieldToFilter('quote_id', ['in' => $quoteCollection->getAllIds()]);
  567. }
  568. return $collection;
  569. }
  570. }