Automation.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. <?php
  2. namespace Dotdigitalgroup\Email\Model\Sync;
  3. /**
  4. * Sync automation by type.
  5. *
  6. * @SuppressWarnings(PHPMD.TooManyFields)
  7. */
  8. class Automation
  9. {
  10. const AUTOMATION_TYPE_NEW_CUSTOMER = 'customer_automation';
  11. const AUTOMATION_TYPE_NEW_SUBSCRIBER = 'subscriber_automation';
  12. const AUTOMATION_TYPE_NEW_ORDER = 'order_automation';
  13. const AUTOMATION_TYPE_NEW_GUEST_ORDER = 'guest_order_automation';
  14. const AUTOMATION_TYPE_NEW_REVIEW = 'review_automation';
  15. const AUTOMATION_TYPE_NEW_WISHLIST = 'wishlist_automation';
  16. const AUTOMATION_STATUS_PENDING = 'pending';
  17. const ORDER_STATUS_AUTOMATION = 'order_automation_';
  18. const AUTOMATION_TYPE_CUSTOMER_FIRST_ORDER = 'first_order_automation';
  19. const CONTACT_STATUS_PENDING = "PendingOptIn";
  20. const CONTACT_STATUS_CONFIRMED = "Confirmed";
  21. const CONTACT_STATUS_EXPIRED = "Expired";
  22. /**
  23. * @var array
  24. */
  25. public $automationTypes = [
  26. self::AUTOMATION_TYPE_NEW_CUSTOMER =>
  27. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_AUTOMATION_STUDIO_CUSTOMER,
  28. self::AUTOMATION_TYPE_NEW_SUBSCRIBER =>
  29. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_AUTOMATION_STUDIO_SUBSCRIBER,
  30. self::AUTOMATION_TYPE_NEW_ORDER =>
  31. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_AUTOMATION_STUDIO_ORDER,
  32. self::AUTOMATION_TYPE_NEW_GUEST_ORDER =>
  33. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_AUTOMATION_STUDIO_GUEST_ORDER,
  34. self::AUTOMATION_TYPE_NEW_REVIEW =>
  35. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_AUTOMATION_STUDIO_REVIEW,
  36. self::AUTOMATION_TYPE_NEW_WISHLIST =>
  37. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_AUTOMATION_STUDIO_WISHLIST,
  38. self::AUTOMATION_TYPE_CUSTOMER_FIRST_ORDER =>
  39. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_AUTOMATION_STUDIO_FIRST_ORDER
  40. ];
  41. /**
  42. * @var int
  43. */
  44. private $limit = 100;
  45. /**
  46. * @var string
  47. */
  48. private $typeId;
  49. /**
  50. * @var string
  51. */
  52. private $storeName;
  53. /**
  54. * @var string
  55. */
  56. private $programId;
  57. /**
  58. * @var string
  59. */
  60. private $programStatus = 'Active';
  61. /**
  62. * @var string
  63. */
  64. private $programMessage;
  65. /**
  66. * @var \Dotdigitalgroup\Email\Helper\Data
  67. */
  68. private $helper;
  69. /**
  70. * @var \Magento\Framework\App\ResourceConnection
  71. */
  72. private $resource;
  73. /**
  74. * @var \Magento\Framework\Stdlib\DateTime
  75. */
  76. private $dateTime;
  77. /**
  78. * @var \Dotdigitalgroup\Email\Model\ResourceModel\Automation\CollectionFactory
  79. */
  80. private $automationFactory;
  81. /**
  82. * @var \Dotdigitalgroup\Email\Model\ResourceModel\Automation
  83. */
  84. private $automationResource;
  85. /**
  86. * @var \Magento\Sales\Model\OrderFactory
  87. */
  88. private $orderFactory;
  89. /**
  90. * @var \Dotdigitalgroup\Email\Model\DateIntervalFactory
  91. */
  92. private $dateIntervalFactory;
  93. /**
  94. * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface
  95. */
  96. private $timeZone;
  97. /**
  98. * Automation constructor.
  99. *
  100. * @param \Dotdigitalgroup\Email\Model\ResourceModel\Automation\CollectionFactory $automationFactory
  101. * @param \Magento\Framework\App\ResourceConnection $resource
  102. * @param \Dotdigitalgroup\Email\Helper\Data $helper
  103. * @param \Magento\Framework\Stdlib\DateTime $dateTime
  104. * @param \Magento\Sales\Model\OrderFactory $orderFactory
  105. * @param \Dotdigitalgroup\Email\Model\ResourceModel\Automation $automationResource
  106. * @param \Dotdigitalgroup\Email\Model\DateIntervalFactory $dateIntervalFactory,
  107. * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timeZone
  108. */
  109. public function __construct(
  110. \Dotdigitalgroup\Email\Model\ResourceModel\Automation\CollectionFactory $automationFactory,
  111. \Magento\Framework\App\ResourceConnection $resource,
  112. \Dotdigitalgroup\Email\Helper\Data $helper,
  113. \Magento\Framework\Stdlib\DateTime $dateTime,
  114. \Magento\Sales\Model\OrderFactory $orderFactory,
  115. \Dotdigitalgroup\Email\Model\ResourceModel\Automation $automationResource,
  116. \Dotdigitalgroup\Email\Model\DateIntervalFactory $dateIntervalFactory,
  117. \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timeZone
  118. ) {
  119. $this->automationFactory = $automationFactory;
  120. $this->helper = $helper;
  121. $this->resource = $resource;
  122. $this->dateTime = $dateTime;
  123. $this->orderFactory = $orderFactory;
  124. $this->automationResource = $automationResource;
  125. $this->dateIntervalFactory = $dateIntervalFactory;
  126. $this->timeZone = $timeZone;
  127. }
  128. /**
  129. * Sync.
  130. *
  131. * @throws \Magento\Framework\Exception\LocalizedException
  132. *
  133. * @return null
  134. */
  135. public function sync()
  136. {
  137. $this->checkStatusForPendingContacts();
  138. $this->setupAutomationTypes();
  139. //send the campaign by each types
  140. foreach ($this->automationTypes as $type => $config) {
  141. $contacts = $this->buildFirstDimensionOfContactsArray($type, $config);
  142. //get collection from type
  143. $automationCollection = $this->automationFactory->create()
  144. ->getCollectionByType($type, $this->limit);
  145. foreach ($automationCollection as $automation) {
  146. $type = $automation->getAutomationType();
  147. $email = $automation->getEmail();
  148. $this->typeId = $automation->getTypeId();
  149. $websiteId = $automation->getWebsiteId();
  150. $this->storeName = $automation->getStoreName();
  151. $typeDouble = $type;
  152. //Set type to generic automation status if type contains constant value
  153. if (strpos($typeDouble, self::ORDER_STATUS_AUTOMATION) !== false) {
  154. $typeDouble = self::ORDER_STATUS_AUTOMATION;
  155. }
  156. $contact = $this->helper->getContact($email, $websiteId);
  157. //contact id is valid, can update datafields
  158. if ($contact && isset($contact->id)) {
  159. if ($contact->status === self::CONTACT_STATUS_PENDING) {
  160. $automation->setEnrolmentStatus(self::CONTACT_STATUS_PENDING);
  161. $this->automationResource->save($automation);
  162. continue;
  163. }
  164. //need to update datafields
  165. $this->updateDatafieldsByType(
  166. $typeDouble,
  167. $email,
  168. $websiteId
  169. );
  170. $contacts[$automation->getWebsiteId()]['contacts'][$automation->getId()] = $contact->id;
  171. } else {
  172. // the contact is suppressed or the request failed
  173. $automation->setEnrolmentStatus('Suppressed');
  174. $this->automationResource->save($automation);
  175. }
  176. }
  177. $this->sendAutomationEnrolements($contacts, $type);
  178. }
  179. }
  180. /**
  181. * check automation entries for pending contacts
  182. */
  183. private function checkStatusForPendingContacts()
  184. {
  185. $updatedAt = $this->dateTime->formatDate(true);
  186. if ($this->isItTimeToCheckPendingContact()) {
  187. $collection = $this->automationFactory->create()
  188. ->getCollectionByPendingStatus();
  189. $idsToUpdateStatus = [];
  190. $idsToUpdateDate = [];
  191. foreach ($collection as $item) {
  192. $contact = $this->helper->getContact($item->getEmail(), $item->getWebsiteId());
  193. if (isset($contact->id) && $contact->status !== self::CONTACT_STATUS_PENDING) {
  194. //add to array for update status
  195. $idsToUpdateStatus[] = $item->getId();
  196. } else {
  197. //add to array for update date
  198. $idsToUpdateDate[] = $item->getId();
  199. }
  200. }
  201. if (! empty($idsToUpdateStatus)) {
  202. $this->automationResource
  203. ->update(
  204. $idsToUpdateStatus,
  205. $updatedAt,
  206. self::CONTACT_STATUS_CONFIRMED
  207. );
  208. }
  209. if (! empty($idsToUpdateDate)) {
  210. $this->automationResource
  211. ->update(
  212. $idsToUpdateDate,
  213. $updatedAt
  214. );
  215. }
  216. }
  217. //Get pending with 24 house delay and expire it
  218. $collection = $this->automationFactory->create()
  219. ->getCollectionByPendingStatus($this->getDateTimeForExpiration());
  220. $ids = $collection->getColumnValues('id');
  221. if (! empty($ids)) {
  222. $this->automationResource
  223. ->update(
  224. $ids,
  225. $updatedAt,
  226. self::CONTACT_STATUS_EXPIRED
  227. );
  228. }
  229. }
  230. /**
  231. * @return string
  232. */
  233. private function getDateTimeForExpiration()
  234. {
  235. $hours = (int) $this->helper->getWebsiteConfig(
  236. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_AC_AUTOMATION_EXPIRE_TIME
  237. );
  238. $interval = $this->dateIntervalFactory->create(
  239. ['interval_spec' => sprintf('PT%sH', $hours)]
  240. );
  241. $dateTime = $this->timeZone->date();
  242. $dateTime->sub($interval);
  243. return $dateTime->format('Y-m-d H:i:s');
  244. }
  245. /**
  246. * @return boolean
  247. */
  248. private function isItTimeToCheckPendingContact()
  249. {
  250. $dateTimeFromDb = $this->automationFactory->create()->getLastPendingStatusCheckTime();
  251. if (! $dateTimeFromDb) {
  252. return false;
  253. }
  254. $lastCheckTime = $this->timeZone->date($dateTimeFromDb);
  255. $interval = $this->dateIntervalFactory->create(['interval_spec' => 'PT30M']);
  256. $lastCheckTime->add($interval);
  257. $now = $this->timeZone->date();
  258. return ($now->format('Y-m-d H:i:s') > $lastCheckTime->format('Y-m-d H:i:s'));
  259. }
  260. /**
  261. * Update single contact datafields for this automation type.
  262. *
  263. * @param string $type
  264. * @param string $email
  265. * @param int $websiteId
  266. *
  267. * @return null
  268. * @throws \Magento\Framework\Exception\LocalizedException
  269. */
  270. private function updateDatafieldsByType($type, $email, $websiteId)
  271. {
  272. switch ($type) {
  273. case self::AUTOMATION_TYPE_NEW_ORDER:
  274. case self::AUTOMATION_TYPE_NEW_GUEST_ORDER:
  275. case self::ORDER_STATUS_AUTOMATION:
  276. case self::AUTOMATION_TYPE_CUSTOMER_FIRST_ORDER:
  277. $this->updateNewOrderDatafields($websiteId);
  278. break;
  279. default:
  280. $this->updateDefaultDatafields($email, $websiteId);
  281. break;
  282. }
  283. }
  284. /**
  285. * Update config datafield.
  286. *
  287. * @param string $email
  288. * @param int $websiteId
  289. *
  290. * @return null
  291. * @throws \Magento\Framework\Exception\LocalizedException
  292. */
  293. private function updateDefaultDatafields($email, $websiteId)
  294. {
  295. $website = $this->helper->storeManager->getWebsite($websiteId);
  296. $this->helper->updateDataFields($email, $website, $this->storeName);
  297. }
  298. /**
  299. * Update new order default datafields.
  300. *
  301. * @param int $websiteId
  302. *
  303. * @return null
  304. * @throws \Magento\Framework\Exception\LocalizedException
  305. */
  306. private function updateNewOrderDatafields($websiteId)
  307. {
  308. $website = $this->helper->storeManager->getWebsite($websiteId);
  309. $orderModel = $this->orderFactory->create()
  310. ->loadByIncrementId($this->typeId);
  311. //data fields
  312. if ($lastOrderId = $website->getConfig(
  313. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_CUSTOMER_LAST_ORDER_ID
  314. )
  315. ) {
  316. $data[] = [
  317. 'Key' => $lastOrderId,
  318. 'Value' => $orderModel->getId(),
  319. ];
  320. }
  321. if ($orderIncrementId = $website->getConfig(
  322. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_CUSTOMER_LAST_ORDER_INCREMENT_ID
  323. )
  324. ) {
  325. $data[] = [
  326. 'Key' => $orderIncrementId,
  327. 'Value' => $orderModel->getIncrementId(),
  328. ];
  329. }
  330. if ($storeName = $website->getConfig(
  331. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_CUSTOMER_STORE_NAME
  332. )
  333. ) {
  334. $data[] = [
  335. 'Key' => $storeName,
  336. 'Value' => $this->storeName,
  337. ];
  338. }
  339. if ($websiteName = $website->getConfig(
  340. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_CUSTOMER_WEBSITE_NAME
  341. )
  342. ) {
  343. $data[] = [
  344. 'Key' => $websiteName,
  345. 'Value' => $website->getName(),
  346. ];
  347. }
  348. if ($lastOrderDate = $website->getConfig(
  349. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_CUSTOMER_LAST_ORDER_DATE
  350. )
  351. ) {
  352. $data[] = [
  353. 'Key' => $lastOrderDate,
  354. 'Value' => $orderModel->getCreatedAt(),
  355. ];
  356. }
  357. if (($customerId = $website->getConfig(
  358. \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_CUSTOMER_ID
  359. ))
  360. && $orderModel->getCustomerId()
  361. ) {
  362. $data[] = [
  363. 'Key' => $customerId,
  364. 'Value' => $orderModel->getCustomerId(),
  365. ];
  366. }
  367. if (!empty($data)) {
  368. //update data fields
  369. $client = $this->helper->getWebsiteApiClient($website);
  370. $client->updateContactDatafieldsByEmail(
  371. $orderModel->getCustomerEmail(),
  372. $data
  373. );
  374. }
  375. }
  376. /**
  377. * Program check if is valid and active.
  378. *
  379. * @param int $programId
  380. * @param int $websiteId
  381. *
  382. * @return bool
  383. * @throws \Exception
  384. */
  385. private function checkCampignEnrolmentActive($programId, $websiteId)
  386. {
  387. //program is not set
  388. if (!$programId) {
  389. return false;
  390. }
  391. $client = $this->helper->getWebsiteApiClient($websiteId);
  392. $program = $client->getProgramById($programId);
  393. //program status
  394. if (isset($program->status)) {
  395. $this->programStatus = $program->status;
  396. }
  397. if (isset($program->status) && $program->status == 'Active') {
  398. return true;
  399. }
  400. return false;
  401. }
  402. /**
  403. * Enrol contacts for a program.
  404. *
  405. * @param array $contacts
  406. * @param int $websiteId
  407. *
  408. * @return mixed
  409. * @throws \Exception
  410. */
  411. private function sendContactsToAutomation($contacts, $websiteId)
  412. {
  413. $client = $this->helper->getWebsiteApiClient($websiteId);
  414. $data = [
  415. 'Contacts' => $contacts,
  416. 'ProgramId' => $this->programId,
  417. 'AddressBooks' => [],
  418. ];
  419. //api add contact to automation enrolment
  420. $result = $client->postProgramsEnrolments($data);
  421. return $result;
  422. }
  423. /**
  424. * Setup automation types
  425. *
  426. * @return null
  427. */
  428. private function setupAutomationTypes()
  429. {
  430. $statusTypes = $this->automationFactory->create()
  431. ->getAutomationStatusType();
  432. foreach ($statusTypes as $type) {
  433. $this->automationTypes[$type]
  434. = \Dotdigitalgroup\Email\Helper\Config::XML_PATH_CONNECTOR_AUTOMATION_STUDIO_ORDER_STATUS;
  435. }
  436. }
  437. /**
  438. * @param string $type
  439. * @param string $config
  440. *
  441. * @return array
  442. */
  443. private function buildFirstDimensionOfContactsArray($type, $config)
  444. {
  445. $contacts = [];
  446. $websites = $this->helper->getWebsites(true);
  447. foreach ($websites as $website) {
  448. if (strpos($type, self::ORDER_STATUS_AUTOMATION) !== false) {
  449. $configValue = $this->helper->serializer->unserialize(
  450. $this->helper->getWebsiteConfig($config, $website)
  451. );
  452. if (is_array($configValue) && !empty($configValue)) {
  453. foreach ($configValue as $one) {
  454. if (strpos($type, $one['status']) !== false) {
  455. $contacts[$website->getId()]['programId']
  456. = $one['automation'];
  457. }
  458. }
  459. }
  460. } else {
  461. $contacts[$website->getId()]['programId']
  462. = $this->helper->getWebsiteConfig($config, $website);
  463. }
  464. }
  465. return $contacts;
  466. }
  467. /**
  468. * @param array $contactsArray
  469. * @param int $websiteId
  470. *
  471. * @return null
  472. * @throws \Exception
  473. */
  474. private function sendSubscribedContactsToAutomation($contactsArray, $websiteId)
  475. {
  476. if (!empty($contactsArray) &&
  477. $this->checkCampignEnrolmentActive($this->programId, $websiteId)
  478. ) {
  479. $result = $this->sendContactsToAutomation(
  480. array_values($contactsArray),
  481. $websiteId
  482. );
  483. //check for error message
  484. if (isset($result->message)) {
  485. $this->programStatus = 'Failed';
  486. $this->programMessage = $result->message;
  487. }
  488. //program is not active
  489. } elseif ($this->programMessage
  490. == 'Error: ERROR_PROGRAM_NOT_ACTIVE '
  491. ) {
  492. $this->programStatus = 'Deactivated';
  493. }
  494. }
  495. /**
  496. * @param $contacts
  497. * @param $type
  498. */
  499. private function sendAutomationEnrolements($contacts, $type)
  500. {
  501. foreach ($contacts as $websiteId => $websiteContacts) {
  502. if (isset($websiteContacts['contacts'])) {
  503. $this->programId = $websiteContacts['programId'];
  504. $contactsArray = $websiteContacts['contacts'];
  505. //only for subscribed contacts
  506. $this->sendSubscribedContactsToAutomation($contactsArray, $websiteId);
  507. //update contacts with the new status, and log the error message if fails
  508. $contactIds = array_keys($contactsArray);
  509. $updatedAt = $this->dateTime->formatDate(true);
  510. $this->automationResource
  511. ->updateStatus(
  512. $contactIds,
  513. $this->programStatus,
  514. $this->programMessage,
  515. $updatedAt,
  516. $type
  517. );
  518. }
  519. }
  520. }
  521. }