Quote.php 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. <?php
  2. namespace Dotdigitalgroup\Email\Model\Sales;
  3. use Dotdigitalgroup\Email\Model\AbandonedCart\PendingContactUpdater;
  4. use Dotdigitalgroup\Email\Model\ResourceModel\Campaign;
  5. /**
  6. * Customer and guest Abandoned Carts.
  7. *
  8. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  9. */
  10. class Quote
  11. {
  12. //customer
  13. const XML_PATH_LOSTBASKET_CUSTOMER_ENABLED_1 = 'abandoned_carts/customers/enabled_1';
  14. const XML_PATH_LOSTBASKET_CUSTOMER_ENABLED_2 = 'abandoned_carts/customers/enabled_2';
  15. const XML_PATH_LOSTBASKET_CUSTOMER_ENABLED_3 = 'abandoned_carts/customers/enabled_3';
  16. const XML_PATH_LOSTBASKET_CUSTOMER_INTERVAL_1 = 'abandoned_carts/customers/send_after_1';
  17. const XML_PATH_LOSTBASKET_CUSTOMER_INTERVAL_2 = 'abandoned_carts/customers/send_after_2';
  18. const XML_PATH_LOSTBASKET_CUSTOMER_INTERVAL_3 = 'abandoned_carts/customers/send_after_3';
  19. const XML_PATH_LOSTBASKET_CUSTOMER_CAMPAIGN_1 = 'abandoned_carts/customers/campaign_1';
  20. const XML_PATH_LOSTBASKET_CUSTOMER_CAMPAIGN_2 = 'abandoned_carts/customers/campaign_2';
  21. const XML_PATH_LOSTBASKET_CUSTOMER_CAMPAIGN_3 = 'abandoned_carts/customers/campaign_3';
  22. //guest
  23. const XML_PATH_LOSTBASKET_GUEST_ENABLED_1 = 'abandoned_carts/guests/enabled_1';
  24. const XML_PATH_LOSTBASKET_GUEST_ENABLED_2 = 'abandoned_carts/guests/enabled_2';
  25. const XML_PATH_LOSTBASKET_GUEST_ENABLED_3 = 'abandoned_carts/guests/enabled_3';
  26. const XML_PATH_LOSTBASKET_GUEST_INTERVAL_1 = 'abandoned_carts/guests/send_after_1';
  27. const XML_PATH_LOSTBASKET_GUEST_INTERVAL_2 = 'abandoned_carts/guests/send_after_2';
  28. const XML_PATH_LOSTBASKET_GUEST_INTERVAL_3 = 'abandoned_carts/guests/send_after_3';
  29. const XML_PATH_LOSTBASKET_GUEST_CAMPAIGN_1 = 'abandoned_carts/guests/campaign_1';
  30. const XML_PATH_LOSTBASKET_GUEST_CAMPAIGN_2 = 'abandoned_carts/guests/campaign_2';
  31. const XML_PATH_LOSTBASKET_GUEST_CAMPAIGN_3 = 'abandoned_carts/guests/campaign_3';
  32. const CUSTOMER_LOST_BASKET_ONE = 1;
  33. const CUSTOMER_LOST_BASKET_TWO = 2;
  34. const CUSTOMER_LOST_BASKET_THREE = 3;
  35. const GUEST_LOST_BASKET_ONE = 1;
  36. const GUEST_LOST_BASKET_TWO = 2;
  37. const GUEST_LOST_BASKET_THREE = 3;
  38. const STATUS_PENDING = 'PendingOptIn';
  39. const STATUS_CONFIRMED = 'Confirmed';
  40. const STATUS_SENT = 'Sent';
  41. const STATUS_EXPIRED = 'Expired';
  42. /**
  43. * @var \Dotdigitalgroup\Email\Model\AbandonedFactory
  44. */
  45. public $abandonedFactory;
  46. /**
  47. * @var \Dotdigitalgroup\Email\Model\ResourceModel\Abandoned\CollectionFactory
  48. */
  49. public $abandonedCollectionFactory;
  50. /**
  51. * @var \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory
  52. */
  53. public $quoteCollectionFactory;
  54. /**
  55. * @var Campaign
  56. */
  57. private $campaignResource;
  58. /**
  59. * @var \Dotdigitalgroup\Email\Helper\Data
  60. */
  61. private $helper;
  62. /**
  63. * @var \Magento\Framework\App\Config\ScopeConfigInterface
  64. */
  65. private $scopeConfig;
  66. /**
  67. * @var \Dotdigitalgroup\Email\Model\ResourceModel\Order\CollectionFactory
  68. */
  69. private $orderCollection;
  70. /**
  71. * @var \Dotdigitalgroup\Email\Model\CampaignFactory
  72. */
  73. private $campaignFactory;
  74. /**
  75. * @var \Dotdigitalgroup\Email\Model\ResourceModel\Campaign\CollectionFactory
  76. */
  77. private $campaignCollection;
  78. /**
  79. * @var \Dotdigitalgroup\Email\Model\RulesFactory
  80. */
  81. private $rulesFactory;
  82. /**
  83. * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface
  84. */
  85. private $timeZone;
  86. /**
  87. * Total number of customers found.
  88. * @var int
  89. */
  90. public $totalCustomers = 0;
  91. /**
  92. * Total number of guest found.
  93. * @var int
  94. */
  95. public $totalGuests = 0;
  96. /**
  97. * @var \Dotdigitalgroup\Email\Model\ResourceModel\Abandoned
  98. */
  99. private $abandonedResource;
  100. /**
  101. * @var \Dotdigitalgroup\Email\Model\DateIntervalFactory
  102. */
  103. private $dateIntervalFactory;
  104. /**
  105. * @var PendingContactUpdater
  106. */
  107. private $acPendingContactUpdater;
  108. /**
  109. * Quote constructor.
  110. *
  111. * @param \Dotdigitalgroup\Email\Model\AbandonedFactory $abandonedFactory
  112. * @param \Dotdigitalgroup\Email\Model\RulesFactory $rulesFactory
  113. * @param Campaign $campaignResource
  114. * @param \Dotdigitalgroup\Email\Model\CampaignFactory $campaignFactory
  115. * @param \Dotdigitalgroup\Email\Model\ResourceModel\Abandoned $abandonedResource
  116. * @param \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $quoteCollectionFactory
  117. * @param \Dotdigitalgroup\Email\Model\ResourceModel\Order\CollectionFactory $collectionFactory
  118. * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone
  119. * @param \Dotdigitalgroup\Email\Model\DateIntervalFactory $dateIntervalFactory
  120. * @param PendingContactUpdater $pendingContactUpdater
  121. */
  122. public function __construct(
  123. \Dotdigitalgroup\Email\Model\AbandonedFactory $abandonedFactory,
  124. \Dotdigitalgroup\Email\Model\RulesFactory $rulesFactory,
  125. Campaign $campaignResource,
  126. \Dotdigitalgroup\Email\Model\CampaignFactory $campaignFactory,
  127. \Dotdigitalgroup\Email\Model\ResourceModel\Abandoned $abandonedResource,
  128. \Magento\Quote\Model\ResourceModel\Quote\CollectionFactory $quoteCollectionFactory,
  129. \Dotdigitalgroup\Email\Model\ResourceModel\Order\CollectionFactory $collectionFactory,
  130. \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone,
  131. \Dotdigitalgroup\Email\Model\DateIntervalFactory $dateIntervalFactory,
  132. PendingContactUpdater $pendingContactUpdater
  133. ) {
  134. $this->timeZone = $timezone;
  135. $this->rulesFactory = $rulesFactory;
  136. $this->campaignFactory = $campaignFactory;
  137. $this->helper = $abandonedResource->helper;
  138. $this->abandonedFactory = $abandonedFactory;
  139. $this->campaignResource = $campaignResource;
  140. $this->orderCollection = $collectionFactory;
  141. $this->abandonedResource = $abandonedResource;
  142. $this->dateIntervalFactory = $dateIntervalFactory;
  143. $this->scopeConfig = $this->helper->getScopeConfig();
  144. $this->quoteCollectionFactory = $quoteCollectionFactory;
  145. $this->campaignCollection = $campaignFactory->create()->campaignCollection;
  146. $this->abandonedCollectionFactory = $abandonedFactory->create()->abandonedCollectionFactory;
  147. $this->acPendingContactUpdater = $pendingContactUpdater;
  148. }
  149. /**
  150. * Process abandoned carts.
  151. *
  152. * @return array
  153. */
  154. public function processAbandonedCarts()
  155. {
  156. $result = [];
  157. $stores = $this->helper->getStores();
  158. $this->acPendingContactUpdater->update();
  159. foreach ($stores as $store) {
  160. $storeId = $store->getId();
  161. $websiteId = $store->getWebsiteId();
  162. $result = $this->processAbandonedCartsForCustomers($storeId, $websiteId, $result);
  163. $result = $this->processAbandonedCartsForGuests($storeId, $websiteId, $result);
  164. }
  165. return $result;
  166. }
  167. /**
  168. * Process abandoned carts for customer
  169. *
  170. * @param int $storeId
  171. * @param int $websiteId
  172. * @param array $result
  173. *
  174. * @return array
  175. */
  176. private function processAbandonedCartsForCustomers($storeId, $websiteId, $result)
  177. {
  178. $secondCustomerEnabled = $this->isLostBasketCustomerEnabled(self::CUSTOMER_LOST_BASKET_TWO, $storeId);
  179. $thirdCustomerEnabled = $this->isLostBasketCustomerEnabled(self::CUSTOMER_LOST_BASKET_THREE, $storeId);
  180. //first customer
  181. if ($this->isLostBasketCustomerEnabled(self::CUSTOMER_LOST_BASKET_ONE, $storeId) ||
  182. $secondCustomerEnabled ||
  183. $thirdCustomerEnabled
  184. ) {
  185. $result[$storeId]['firstCustomer'] = $this->processCustomerFirstAbandonedCart($storeId);
  186. }
  187. //second customer
  188. if ($secondCustomerEnabled) {
  189. $result[$storeId]['secondCustomer'] = $this->processExistingAbandonedCart(
  190. $this->getLostBasketCustomerCampaignId(self::CUSTOMER_LOST_BASKET_TWO, $storeId),
  191. $storeId,
  192. $websiteId,
  193. self::CUSTOMER_LOST_BASKET_TWO
  194. );
  195. }
  196. //third customer
  197. if ($thirdCustomerEnabled) {
  198. $result[$storeId]['thirdCustomer'] = $this->processExistingAbandonedCart(
  199. $this->getLostBasketCustomerCampaignId(self::CUSTOMER_LOST_BASKET_THREE, $storeId),
  200. $storeId,
  201. $websiteId,
  202. self::CUSTOMER_LOST_BASKET_THREE
  203. );
  204. }
  205. return $result;
  206. }
  207. /**
  208. * Process abandoned carts for guests
  209. *
  210. * @param int $storeId
  211. * @param int $websiteId
  212. * @param array $result
  213. *
  214. * @return array
  215. */
  216. private function processAbandonedCartsForGuests($storeId, $websiteId, $result)
  217. {
  218. $secondGuestEnabled = $this->isLostBasketGuestEnabled(self::GUEST_LOST_BASKET_TWO, $storeId);
  219. $thirdGuestEnabled = $this->isLostBasketGuestEnabled(self::GUEST_LOST_BASKET_THREE, $storeId);
  220. //first guest
  221. if ($this->isLostBasketGuestEnabled(self::GUEST_LOST_BASKET_ONE, $storeId) ||
  222. $secondGuestEnabled ||
  223. $thirdGuestEnabled
  224. ) {
  225. $result[$storeId]['firstGuest'] = $this->processGuestFirstAbandonedCart($storeId);
  226. }
  227. //second guest
  228. if ($secondGuestEnabled) {
  229. $result[$storeId]['secondGuest'] = $this->processExistingAbandonedCart(
  230. $this->getLostBasketGuestCampaignId(self::GUEST_LOST_BASKET_TWO, $storeId),
  231. $storeId,
  232. $websiteId,
  233. self::GUEST_LOST_BASKET_TWO,
  234. true
  235. );
  236. }
  237. //third guest
  238. if ($thirdGuestEnabled) {
  239. $result[$storeId]['thirdGuest'] = $this->processExistingAbandonedCart(
  240. $this->getLostBasketGuestCampaignId(self::GUEST_LOST_BASKET_THREE, $storeId),
  241. $storeId,
  242. $websiteId,
  243. self::GUEST_LOST_BASKET_THREE,
  244. true
  245. );
  246. }
  247. return $result;
  248. }
  249. /**
  250. * @param int $num
  251. * @param int $storeId
  252. *
  253. * @return bool
  254. */
  255. public function isLostBasketCustomerEnabled($num, $storeId)
  256. {
  257. return $this->scopeConfig->isSetFlag(
  258. constant('self::XML_PATH_LOSTBASKET_CUSTOMER_ENABLED_' . $num),
  259. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  260. $storeId
  261. );
  262. }
  263. /**
  264. * @param int $num
  265. * @param int $storeId
  266. *
  267. * @return mixed
  268. */
  269. public function getLostBasketCustomerInterval($num, $storeId)
  270. {
  271. return $this->scopeConfig->getValue(
  272. constant('self::XML_PATH_LOSTBASKET_CUSTOMER_INTERVAL_' . $num),
  273. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  274. $storeId
  275. );
  276. }
  277. /**
  278. * @param string|null $from
  279. * @param string|null $to
  280. * @param bool $guest
  281. * @param int $storeId
  282. * @return \Magento\Quote\Model\ResourceModel\Quote\Collection|\Magento\Sales\Model\ResourceModel\Order\Collection
  283. * @throws \Magento\Framework\Exception\NoSuchEntityException
  284. */
  285. public function getStoreQuotes($from = null, $to = null, $guest = false, $storeId = 0)
  286. {
  287. $updated = [
  288. 'from' => $from,
  289. 'to' => $to,
  290. 'date' => true,
  291. ];
  292. $salesCollection = $this->orderCollection->create()
  293. ->getStoreQuotes($storeId, $updated, $guest);
  294. //process rules on collection
  295. $ruleModel = $this->rulesFactory->create();
  296. $websiteId = $this->helper->storeManager->getStore($storeId)
  297. ->getWebsiteId();
  298. $salesCollection = $ruleModel->process(
  299. $salesCollection,
  300. \Dotdigitalgroup\Email\Model\Rules::ABANDONED,
  301. $websiteId
  302. );
  303. return $salesCollection;
  304. }
  305. /**
  306. * @param int $num
  307. * @param int $storeId
  308. *
  309. * @return mixed
  310. */
  311. public function getLostBasketCustomerCampaignId($num, $storeId)
  312. {
  313. return $this->scopeConfig->getValue(
  314. constant('self::XML_PATH_LOSTBASKET_CUSTOMER_CAMPAIGN_' . $num),
  315. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  316. $storeId
  317. );
  318. }
  319. /**
  320. * Send email only if the interval limit passed, no emails during this interval.
  321. * Return false for any found for this period.
  322. *
  323. * @param string $email
  324. * @param int $storeId
  325. *
  326. * @return bool
  327. */
  328. public function isIntervalCampaignFound($email, $storeId)
  329. {
  330. $cartLimit = $this->scopeConfig->getValue(
  331. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_ABANDONED_CART_LIMIT,
  332. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  333. $storeId
  334. );
  335. //no limit is set skip
  336. if (! $cartLimit) {
  337. return false;
  338. }
  339. $fromTime = $this->timeZone->scopeDate($storeId, 'now', true);
  340. $toTime = clone $fromTime;
  341. $interval = $this->dateIntervalFactory->create(
  342. ['interval_spec' => sprintf('PT%sH', $cartLimit)]
  343. );
  344. $fromTime->sub($interval);
  345. $fromDate = $fromTime->getTimestamp();
  346. $toDate = $toTime->getTimestamp();
  347. $updated = [
  348. 'from' => $fromDate,
  349. 'to' => $toDate,
  350. 'date' => true,
  351. ];
  352. //total campaigns sent for this interval of time
  353. $campaignLimit = $this->campaignCollection->create()
  354. ->getNumberOfCampaignsForContactByInterval($email, $updated);
  355. //found campaign
  356. if ($campaignLimit) {
  357. return true;
  358. }
  359. return false;
  360. }
  361. /**
  362. * @param int $num
  363. * @param int $storeId
  364. *
  365. * @return bool
  366. */
  367. public function isLostBasketGuestEnabled($num, $storeId)
  368. {
  369. return $this->scopeConfig->isSetFlag(
  370. constant('self::XML_PATH_LOSTBASKET_GUEST_ENABLED_' . $num),
  371. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  372. $storeId
  373. );
  374. }
  375. /**
  376. * @param int $num
  377. * @param int $storeId
  378. *
  379. * @return mixed
  380. */
  381. public function getLostBasketSendAfterForGuest($num, $storeId)
  382. {
  383. return $this->scopeConfig->getValue(
  384. constant('self::XML_PATH_LOSTBASKET_GUEST_INTERVAL_' . $num),
  385. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  386. $storeId
  387. );
  388. }
  389. /**
  390. * @param int $num
  391. * @param int $storeId
  392. *
  393. * @return mixed
  394. */
  395. public function getLostBasketGuestCampaignId($num, $storeId)
  396. {
  397. return $this->scopeConfig->getValue(
  398. constant('self::XML_PATH_LOSTBASKET_GUEST_CAMPAIGN_' . $num),
  399. \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
  400. $storeId
  401. );
  402. }
  403. /**
  404. * @param int $storeId
  405. * @param int $num
  406. *
  407. * @return \DateInterval
  408. */
  409. private function getInterval($storeId, $num)
  410. {
  411. if ($num == 1) {
  412. $minutes = $this->getLostBasketCustomerInterval($num, $storeId);
  413. $interval = $this->dateIntervalFactory->create(
  414. ['interval_spec' => sprintf('PT%sM', $minutes)]
  415. );
  416. } else {
  417. $hours = (int)$this->getLostBasketCustomerInterval($num, $storeId);
  418. $interval = $this->dateIntervalFactory->create(
  419. ['interval_spec' => sprintf('PT%sH', $hours)]
  420. );
  421. }
  422. return $interval;
  423. }
  424. /**
  425. * @param int $storeId
  426. * @param int $num
  427. *
  428. * @return \DateInterval
  429. */
  430. protected function getSendAfterIntervalForGuest($storeId, $num)
  431. {
  432. $timeInterval = $this->getLostBasketSendAfterForGuest($num, $storeId);
  433. //for the first cart which use the minutes
  434. if ($num == 1) {
  435. $interval = $this->dateIntervalFactory->create(
  436. ['interval_spec' => sprintf('PT%sM', $timeInterval)]
  437. );
  438. } else {
  439. $interval = $this->dateIntervalFactory->create(
  440. ['interval_spec' => sprintf('PT%sH', $timeInterval)]
  441. );
  442. }
  443. return $interval;
  444. }
  445. /**
  446. * @param int $storeId
  447. * @return int|string
  448. */
  449. private function processCustomerFirstAbandonedCart($storeId)
  450. {
  451. $abandonedNum = 1;
  452. $interval = $this->getInterval($storeId, $abandonedNum);
  453. $fromTime = new \DateTime('now', new \DateTimezone('UTC'));
  454. $fromTime->sub($interval);
  455. $toTime = clone $fromTime;
  456. $fromTime->sub($this->dateIntervalFactory->create(['interval_spec' => 'PT5M']));
  457. $fromDate = $fromTime->format('Y-m-d H:i:s');
  458. $toDate = $toTime->format('Y-m-d H:i:s');
  459. //active quotes
  460. $quoteCollection = $this->getStoreQuotes($fromDate, $toDate, false, $storeId);
  461. //found abandoned carts
  462. if ($quoteCollection->getSize()) {
  463. $this->helper->log('Customer AC 1 ' . $fromDate . ' - ' . $toDate);
  464. }
  465. //campaign id for customers
  466. $campaignId = $this->getLostBasketCustomerCampaignId($abandonedNum, $storeId);
  467. $result = $this->createCustomerFirstAbandonedCart($quoteCollection, $storeId, $campaignId);
  468. $result += $this->processConfirmedCustomerAbandonedCart($storeId, $campaignId);
  469. return $result;
  470. }
  471. /**
  472. * @param $quoteCollection
  473. * @param $storeId
  474. * @param $campaignId
  475. * @param $result
  476. *
  477. * @return int
  478. * @throws \Magento\Framework\Exception\NoSuchEntityException
  479. */
  480. private function createCustomerFirstAbandonedCart($quoteCollection, $storeId, $campaignId)
  481. {
  482. $result = 0;
  483. foreach ($quoteCollection as $quote) {
  484. $websiteId = $this->helper->storeManager->getStore($storeId)->getWebsiteId();
  485. if (! $this->updateDataFieldAndCreateAc($quote, $websiteId)) {
  486. continue;
  487. }
  488. //send campaign; check if valid to be sent
  489. if ($this->isLostBasketCustomerEnabled(self::CUSTOMER_LOST_BASKET_ONE, $storeId)) {
  490. $this->sendEmailCampaign(
  491. $quote->getCustomerEmail(),
  492. $quote,
  493. $campaignId,
  494. self::CUSTOMER_LOST_BASKET_ONE,
  495. $websiteId
  496. );
  497. }
  498. $this->totalCustomers++;
  499. $result = $this->totalCustomers;
  500. }
  501. return $result;
  502. }
  503. /**
  504. * @param \Magento\Quote\Model\Quote $quote
  505. * @param int $websiteId
  506. *
  507. * @return bool
  508. */
  509. private function updateDataFieldAndCreateAc($quote, $websiteId)
  510. {
  511. $quoteId = $quote->getId();
  512. $items = $quote->getAllItems();
  513. $email = $quote->getCustomerEmail();
  514. $itemIds = $this->getQuoteItemIds($items);
  515. $abandonedModel = $this->abandonedFactory->create()
  516. ->loadByQuoteId($quoteId);
  517. $contact = $this->helper->getContact($email, $websiteId);
  518. if (!$contact) {
  519. return false;
  520. }
  521. if ($contact->status === self::STATUS_PENDING) {
  522. $this->createAbandonedCart($abandonedModel, $quote, $itemIds, self::STATUS_PENDING);
  523. return false;
  524. }
  525. if ($this->abandonedCartAlreadyExists($abandonedModel) &&
  526. $this->shouldNotSendACAgain($abandonedModel, $quote) &&
  527. $this->isNotAConfirmedContact($abandonedModel)
  528. ) {
  529. if ($this->shouldDeleteAbandonedCart($quote)) {
  530. $this->deleteAbandonedCart($abandonedModel);
  531. }
  532. return false;
  533. } else {
  534. if ($mostExpensiveItem = $this->getMostExpensiveItems($items)) {
  535. $this->helper->updateAbandonedProductName(
  536. $mostExpensiveItem->getName(),
  537. $email,
  538. $websiteId
  539. );
  540. }
  541. $this->createAbandonedCart($abandonedModel, $quote, $itemIds, self::STATUS_SENT);
  542. return true;
  543. }
  544. }
  545. /**
  546. * @param array $allItemsIds
  547. * @return array
  548. */
  549. private function getQuoteItemIds($allItemsIds)
  550. {
  551. $itemIds = [];
  552. foreach ($allItemsIds as $item) {
  553. $itemIds[] = $item->getProductId();
  554. }
  555. return $itemIds;
  556. }
  557. /**
  558. * @param array $items
  559. * @return bool|\Magento\Quote\Model\Quote\Item
  560. */
  561. private function getMostExpensiveItems($items)
  562. {
  563. $mostExpensiveItem = false;
  564. foreach ($items as $item) {
  565. /** @var $item \Magento\Quote\Model\Quote\Item */
  566. if ($mostExpensiveItem == false) {
  567. $mostExpensiveItem = $item;
  568. } elseif ($item->getPrice() > $mostExpensiveItem->getPrice()) {
  569. $mostExpensiveItem = $item;
  570. }
  571. }
  572. return $mostExpensiveItem;
  573. }
  574. /**
  575. * @param \Magento\Quote\Model\Quote $quote
  576. * @param \Dotdigitalgroup\Email\Model\Abandoned $abandonedModel
  577. * @return bool
  578. */
  579. private function isItemsChanged($quote, $abandonedModel)
  580. {
  581. if ($quote->getItemsCount() != $abandonedModel->getItemsCount()) {
  582. return true;
  583. } else {
  584. //number of items matches
  585. $quoteItemIds = $this->getQuoteItemIds($quote->getAllItems());
  586. $abandonedItemIds = explode(',', $abandonedModel->getItemsIds());
  587. //quote items not same
  588. if (! $this->isItemsIdsSame($quoteItemIds, $abandonedItemIds)) {
  589. return true;
  590. }
  591. return false;
  592. }
  593. }
  594. /**
  595. * @param \Dotdigitalgroup\Email\Model\Abandoned $abandonedModel
  596. * @param \Magento\Quote\Model\Quote $quote
  597. * @param array $itemIds
  598. * @param string $status
  599. *
  600. * @throws \Magento\Framework\Exception\LocalizedException
  601. */
  602. private function createAbandonedCart($abandonedModel, $quote, $itemIds, $status)
  603. {
  604. $abandonedModel->setStoreId($quote->getStoreId())
  605. ->setCustomerId($quote->getCustomerId())
  606. ->setEmail($quote->getCustomerEmail())
  607. ->setQuoteId($quote->getId())
  608. ->setQuoteUpdatedAt($quote->getUpdatedAt())
  609. ->setAbandonedCartNumber(1)
  610. ->setItemsCount($quote->getItemsCount())
  611. ->setItemsIds(implode(',', $itemIds))
  612. ->setStatus($status)
  613. ->save();
  614. }
  615. /**
  616. * @param string $email
  617. * @param \Magento\Quote\Model\Quote $quote
  618. * @param string $campaignId
  619. * @param int $number
  620. * @param int $websiteId
  621. */
  622. private function sendEmailCampaign($email, $quote, $campaignId, $number, $websiteId)
  623. {
  624. $storeId = $quote->getStoreId();
  625. //interval campaign found
  626. if ($this->isIntervalCampaignFound($email, $storeId) || ! $campaignId) {
  627. return;
  628. }
  629. $customerId = $quote->getCustomerId();
  630. $message = ($customerId)? 'Abandoned Cart ' . $number : 'Guest Abandoned Cart ' . $number;
  631. $campaign = $this->campaignFactory->create()
  632. ->setEmail($email)
  633. ->setCustomerId($customerId)
  634. ->setEventName(\Dotdigitalgroup\Email\Model\Campaign::CAMPAIGN_EVENT_LOST_BASKET)
  635. ->setQuoteId($quote->getId())
  636. ->setMessage($message)
  637. ->setCampaignId($campaignId)
  638. ->setStoreId($storeId)
  639. ->setWebsiteId($websiteId)
  640. ->setSendStatus(\Dotdigitalgroup\Email\Model\Campaign::PENDING);
  641. $this->campaignResource->save($campaign);
  642. }
  643. /**
  644. * @param int $storeId
  645. * @return int
  646. */
  647. private function processGuestFirstAbandonedCart($storeId)
  648. {
  649. $abandonedNum = 1;
  650. $sendAfter = $this->getSendAfterIntervalForGuest($storeId, $abandonedNum);
  651. $fromTime = new \DateTime('now', new \DateTimezone('UTC'));
  652. $fromTime->sub($sendAfter);
  653. $toTime = clone $fromTime;
  654. $fromTime->sub($this->dateIntervalFactory->create(['interval_spec' => 'PT5M']));
  655. //format time
  656. $fromDate = $fromTime->format('Y-m-d H:i:s');
  657. $toDate = $toTime->format('Y-m-d H:i:s');
  658. $quoteCollection = $this->getStoreQuotes($fromDate, $toDate, true, $storeId);
  659. if ($quoteCollection->getSize()) {
  660. $this->helper->log('Guest AC 1 ' . $fromDate . ' - ' . $toDate);
  661. }
  662. $guestCampaignId = $this->getLostBasketGuestCampaignId($abandonedNum, $storeId);
  663. $result = $this->createGuestFirstAbandonedCart($quoteCollection, $storeId, $guestCampaignId);
  664. $result += $this->processConfirmedGuestAbandonedCart($storeId, $guestCampaignId);
  665. return $result;
  666. }
  667. /**
  668. * @param $quoteCollection
  669. * @param $storeId
  670. * @param $guestCampaignId
  671. * @param $result
  672. *
  673. * @return int
  674. */
  675. private function createGuestFirstAbandonedCart($quoteCollection, $storeId, $guestCampaignId)
  676. {
  677. $result = 0;
  678. foreach ($quoteCollection as $quote) {
  679. $websiteId = $this->helper->storeManager->getStore($storeId)->getWebsiteId();
  680. if (! $this->updateDataFieldAndCreateAc($quote, $websiteId)) {
  681. continue;
  682. }
  683. //send campaign; check if still valid to be sent
  684. if ($this->isLostBasketGuestEnabled(self::GUEST_LOST_BASKET_ONE, $storeId)) {
  685. $this->sendEmailCampaign(
  686. $quote->getCustomerEmail(),
  687. $quote,
  688. $guestCampaignId,
  689. self::GUEST_LOST_BASKET_ONE,
  690. $websiteId
  691. );
  692. }
  693. $this->totalGuests++;
  694. $result = $this->totalGuests;
  695. }
  696. return $result;
  697. }
  698. /**
  699. * @param \Dotdigitalgroup\Email\Model\Abandoned $abandonedModel
  700. *
  701. * @return mixed
  702. */
  703. private function abandonedCartAlreadyExists($abandonedModel)
  704. {
  705. return $abandonedModel->getId();
  706. }
  707. /**
  708. * @param \Dotdigitalgroup\Email\Model\Abandoned $abandonedModel
  709. * @param \Magento\Quote\Model\Quote $quote
  710. * @return bool
  711. */
  712. private function shouldNotSendACAgain($abandonedModel, $quote)
  713. {
  714. return
  715. !$quote->getIsActive() ||
  716. $quote->getItemsCount() == 0 ||
  717. !$this->isItemsChanged($quote, $abandonedModel);
  718. }
  719. /**
  720. * @param \Magento\Quote\Model\Quote $quote
  721. * @return bool
  722. */
  723. private function shouldDeleteAbandonedCart($quote)
  724. {
  725. return !$quote->getIsActive() || $quote->getItemsCount() == 0;
  726. }
  727. /**
  728. * @param \Dotdigitalgroup\Email\Model\Abandoned $abandonedModel
  729. * @throws \Exception
  730. */
  731. private function deleteAbandonedCart($abandonedModel)
  732. {
  733. $this->abandonedResource->delete($abandonedModel);
  734. }
  735. /**
  736. * @param int $campaignId
  737. * @param int $storeId
  738. * @param int $websiteId
  739. * @param int $number
  740. * @param bool $guest
  741. *
  742. * @return int
  743. */
  744. private function processExistingAbandonedCart($campaignId, $storeId, $websiteId, $number, $guest = false)
  745. {
  746. $result = 0;
  747. $fromTime = new \DateTime('now', new \DateTimezone('UTC'));
  748. if ($guest) {
  749. $interval = $this->getSendAfterIntervalForGuest($storeId, $number);
  750. $message = 'Guest';
  751. } else {
  752. $interval = $this->getInterval($storeId, $number);
  753. $message = 'Customer';
  754. }
  755. $fromTime->sub($interval);
  756. $toTime = clone $fromTime;
  757. $fromTime->sub($this->dateIntervalFactory->create(['interval_spec' => 'PT5M']));
  758. $fromDate = $fromTime->format('Y-m-d H:i:s');
  759. $toDate = $toTime->format('Y-m-d H:i:s');
  760. //get abandoned carts already sent
  761. $abandonedCollection = $this->getAbandonedCartsForStore(
  762. $number,
  763. $fromDate,
  764. $toDate,
  765. $storeId,
  766. $guest
  767. );
  768. //quote collection based on the updated date from abandoned cart table
  769. $quoteIds = $abandonedCollection->getColumnValues('quote_id');
  770. if (empty($quoteIds)) {
  771. return $result;
  772. }
  773. $quoteCollection = $this->getProcessedQuoteByIds($quoteIds, $storeId);
  774. //found abandoned carts
  775. if ($quoteCollection->getSize()) {
  776. $this->helper->log(
  777. $message . ' Abandoned Cart ' . $number . ',from ' . $fromDate . ' : ' . $toDate . ', storeId '
  778. . $storeId
  779. );
  780. }
  781. foreach ($quoteCollection as $quote) {
  782. $quoteId = $quote->getId();
  783. $email = $quote->getCustomerEmail();
  784. if ($mostExpensiveItem = $this->getMostExpensiveItems($quote->getAllItems())) {
  785. $this->helper->updateAbandonedProductName(
  786. $mostExpensiveItem->getName(),
  787. $email,
  788. $websiteId
  789. );
  790. }
  791. $abandonedModel = $this->abandonedFactory->create()
  792. ->loadByQuoteId($quoteId);
  793. //number of items changed or not active anymore
  794. if ($this->isItemsChanged($quote, $abandonedModel)) {
  795. if ($this->shouldDeleteAbandonedCart($quote)) {
  796. $this->deleteAbandonedCart($abandonedModel);
  797. }
  798. continue;
  799. }
  800. $abandonedModel->setAbandonedCartNumber($number)
  801. ->setQuoteUpdatedAt($quote->getUpdatedAt())
  802. ->save();
  803. $this->sendEmailCampaign($email, $quote, $campaignId, $number, $websiteId);
  804. $result++;
  805. }
  806. return $result;
  807. }
  808. /**
  809. * @param int $number
  810. * @param string $from
  811. * @param string $to
  812. * @param int $storeId
  813. * @param bool $guest
  814. *
  815. * @return mixed
  816. */
  817. private function getAbandonedCartsForStore($number, $from, $to, $storeId, $guest = false)
  818. {
  819. $updated = [
  820. 'from' => $from,
  821. 'to' => $to,
  822. 'date' => true
  823. ];
  824. return $this->abandonedCollectionFactory->create()->getAbandonedCartsForStore(
  825. --$number,
  826. $storeId,
  827. $updated,
  828. self::STATUS_SENT,
  829. $this->helper->isOnlySubscribersForAC($storeId),
  830. $guest
  831. );
  832. }
  833. /**
  834. * @param array $quoteIds
  835. * @param int $storeId
  836. * @return mixed
  837. */
  838. private function getProcessedQuoteByIds($quoteIds, $storeId)
  839. {
  840. $quoteCollection = $this->quoteCollectionFactory->create()
  841. ->addFieldToFilter('entity_id', ['in' => $quoteIds]);
  842. //process rules on collection
  843. $ruleModel = $this->rulesFactory->create();
  844. $quoteCollection = $ruleModel->process(
  845. $quoteCollection,
  846. \Dotdigitalgroup\Email\Model\Rules::ABANDONED,
  847. $this->helper->storeManager->getStore($storeId)->getWebsiteId()
  848. );
  849. return $quoteCollection;
  850. }
  851. /**
  852. * Compare items ids.
  853. *
  854. * @param array $quoteItemIds
  855. * @param array $abandonedItemIds
  856. * @return bool
  857. */
  858. private function isItemsIdsSame($quoteItemIds, $abandonedItemIds)
  859. {
  860. return $quoteItemIds == $abandonedItemIds;
  861. }
  862. /**
  863. * @param $storeId
  864. * @param $guestCampaignId
  865. *
  866. * @return int
  867. */
  868. private function processConfirmedGuestAbandonedCart($storeId, $guestCampaignId)
  869. {
  870. $ac1QuoteIdsWithConfirmedContacts = $this->abandonedCollectionFactory->create()
  871. ->getCollectionByConfirmedStatus($storeId, true)
  872. ->getColumnValues('quote_id');
  873. $quoteCollectionFromIds = $this->orderCollection->create()
  874. ->getStoreQuotesFromQuoteIds($ac1QuoteIdsWithConfirmedContacts, $storeId);
  875. return $this->createGuestFirstAbandonedCart(
  876. $quoteCollectionFromIds,
  877. $storeId,
  878. $guestCampaignId
  879. );
  880. }
  881. /**
  882. * @param $storeId
  883. * @param $campaignId
  884. * @param $result
  885. *
  886. * @return int
  887. * @throws \Magento\Framework\Exception\NoSuchEntityException
  888. */
  889. private function processConfirmedCustomerAbandonedCart($storeId, $campaignId)
  890. {
  891. $ac1QuoteIdsWithConfirmedContacts = $this->abandonedCollectionFactory->create()
  892. ->getCollectionByConfirmedStatus($storeId)
  893. ->getColumnValues('quote_id');
  894. $quoteCollectionFromIds = $this->orderCollection->create()
  895. ->getStoreQuotesFromQuoteIds($ac1QuoteIdsWithConfirmedContacts, $storeId);
  896. return $this->createCustomerFirstAbandonedCart(
  897. $quoteCollectionFromIds,
  898. $storeId,
  899. $campaignId
  900. );
  901. }
  902. private function isNotAConfirmedContact($abandonedModel)
  903. {
  904. return $abandonedModel->getStatus() !== self::STATUS_CONFIRMED;
  905. }
  906. }