Importer.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. <?php
  2. namespace Dotdigitalgroup\Email\Model;
  3. /**
  4. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  5. */
  6. class Importer extends \Magento\Framework\Model\AbstractModel
  7. {
  8. const NOT_IMPORTED = 0;
  9. const IMPORTING = 1;
  10. const IMPORTED = 2;
  11. const FAILED = 3;
  12. //import mode
  13. const MODE_BULK = 'Bulk';
  14. const MODE_SINGLE = 'Single';
  15. const MODE_SINGLE_DELETE = 'Single_Delete';
  16. const MODE_CONTACT_DELETE = 'Contact_Delete';
  17. const MODE_SUBSCRIBER_UPDATE = 'Subscriber_Update';
  18. const MODE_CONTACT_EMAIL_UPDATE = 'Contact_Email_Update';
  19. const MODE_SUBSCRIBER_RESUBSCRIBED = 'Subscriber_Resubscribed';
  20. //import type
  21. const IMPORT_TYPE_GUEST = 'Guest';
  22. const IMPORT_TYPE_ORDERS = 'Orders';
  23. const IMPORT_TYPE_CONTACT = 'Contact';
  24. const IMPORT_TYPE_REVIEWS = 'Reviews';
  25. const IMPORT_TYPE_WISHLIST = 'Wishlist';
  26. const IMPORT_TYPE_CONTACT_UPDATE = 'Contact';
  27. const IMPORT_TYPE_SUBSCRIBERS = 'Subscriber';
  28. const IMPORT_TYPE_SUBSCRIBER_UPDATE = 'Subscriber';
  29. const IMPORT_TYPE_SUBSCRIBER_RESUBSCRIBED = 'Subscriber';
  30. //sync limits
  31. const SYNC_SINGLE_LIMIT_NUMBER = 100;
  32. /**
  33. * @var ResourceModel\Importer
  34. */
  35. private $importerResource;
  36. /**
  37. * @var \Dotdigitalgroup\Email\Helper\Data
  38. */
  39. private $helper;
  40. /**
  41. * @var array
  42. */
  43. public $reasons
  44. = [
  45. 'Globally Suppressed',
  46. 'Blocked',
  47. 'Unsubscribed',
  48. 'Hard Bounced',
  49. 'Isp Complaints',
  50. 'Domain Suppressed',
  51. 'Failures',
  52. 'Invalid Entries',
  53. 'Mail Blocked',
  54. 'Suppressed by you',
  55. ];
  56. /**
  57. * @var array
  58. */
  59. public $importStatuses
  60. = [
  61. 'RejectedByWatchdog',
  62. 'InvalidFileFormat',
  63. 'Unknown',
  64. 'Failed',
  65. 'ExceedsAllowedContactLimit',
  66. 'NotAvailableInThisVersion',
  67. ];
  68. /**
  69. * @var array
  70. */
  71. public $bulkPriority;
  72. /**
  73. * @var array
  74. */
  75. public $singlePriority;
  76. /**
  77. * @var int
  78. */
  79. public $totalItems;
  80. /**
  81. * @var int
  82. */
  83. public $bulkSyncLimit;
  84. /**
  85. * @var \Magento\Framework\Stdlib\DateTime
  86. */
  87. public $dateTime;
  88. /**
  89. * @var \Magento\Framework\ObjectManagerInterface
  90. */
  91. public $objectManager;
  92. /**
  93. * @var Config\Json
  94. */
  95. public $serializer;
  96. /**
  97. * Importer constructor.
  98. * @param \Magento\Framework\Model\Context $context
  99. * @param \Magento\Framework\Registry $registry
  100. * @param \Dotdigitalgroup\Email\Helper\Data $helper
  101. * @param ResourceModel\Importer $importerResource
  102. * @param \Magento\Framework\ObjectManagerInterface $objectManager
  103. * @param \Magento\Framework\Stdlib\DateTime $dateTime
  104. * @param Config\Json $serializer
  105. * @param array $data
  106. * @param \Magento\Framework\Model\ResourceModel\AbstractResource|null $resource
  107. * @param \Magento\Framework\Data\Collection\AbstractDb|null $resourceCollection
  108. */
  109. public function __construct(
  110. \Magento\Framework\Model\Context $context,
  111. \Magento\Framework\Registry $registry,
  112. \Dotdigitalgroup\Email\Helper\Data $helper,
  113. \Dotdigitalgroup\Email\Model\ResourceModel\Importer $importerResource,
  114. \Magento\Framework\ObjectManagerInterface $objectManager,
  115. \Magento\Framework\Stdlib\DateTime $dateTime,
  116. \Dotdigitalgroup\Email\Model\Config\Json $serializer,
  117. array $data = [],
  118. \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
  119. \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null
  120. ) {
  121. $this->helper = $helper;
  122. $this->dateTime = $dateTime;
  123. $this->serializer = $serializer;
  124. $this->objectManager = $objectManager;
  125. $this->importerResource = $importerResource;
  126. parent::__construct($context, $registry, $resource, $resourceCollection, $data);
  127. }
  128. /**
  129. * Constructor.
  130. *
  131. * @return null
  132. */
  133. public function _construct()
  134. {
  135. $this->_init(\Dotdigitalgroup\Email\Model\ResourceModel\Importer::class);
  136. }
  137. /**
  138. * @return $this
  139. */
  140. public function beforeSave()
  141. {
  142. parent::beforeSave();
  143. if ($this->isObjectNew()) {
  144. $this->setCreatedAt($this->dateTime->formatDate(true));
  145. }
  146. $this->setUpdatedAt($this->dateTime->formatDate(true));
  147. return $this;
  148. }
  149. /**
  150. * Register import in queue.
  151. *
  152. * @param string $importType
  153. * @param array|string|null $importData
  154. * @param string $importMode
  155. * @param int $websiteId
  156. * @param bool $file
  157. *
  158. * @return bool
  159. */
  160. public function registerQueue(
  161. $importType,
  162. $importData,
  163. $importMode,
  164. $websiteId,
  165. $file = false
  166. ) {
  167. try {
  168. if (! empty($importData)) {
  169. $importData = $this->serializer->serialize($importData);
  170. }
  171. if ($file) {
  172. $this->setImportFile($file);
  173. }
  174. if ($importData || $file) {
  175. $this->setImportType($importType)
  176. ->setImportData($importData)
  177. ->setWebsiteId($websiteId)
  178. ->setImportMode($importMode);
  179. $this->importerResource->save($this);
  180. return true;
  181. }
  182. } catch (\Exception $e) {
  183. $this->helper->debug((string)$e, []);
  184. }
  185. if ($this->serializer->jsonError) {
  186. $jle = $this->serializer->jsonError;
  187. $format = "Json error ($jle) for Import type ($importType) / mode ($importMode) for website ($websiteId)";
  188. $this->helper->log($format);
  189. }
  190. return false;
  191. }
  192. /**
  193. * Proccess the data from queue.
  194. *
  195. * @return null
  196. */
  197. public function processQueue()
  198. {
  199. //Set items to 0
  200. $this->totalItems = 0;
  201. //Set bulk sync limit
  202. $this->bulkSyncLimit = 5;
  203. //Set priority
  204. $this->_setPriority();
  205. //Check previous import status
  206. $this->_checkImportStatus();
  207. //Bulk priority. Process group 1 first
  208. foreach ($this->bulkPriority as $bulk) {
  209. if ($this->totalItems < $bulk['limit']) {
  210. $collection = $this->_getQueue(
  211. $bulk['type'],
  212. $bulk['mode'],
  213. $bulk['limit'] - $this->totalItems
  214. );
  215. if ($collection->getSize()) {
  216. $this->totalItems += $collection->getSize();
  217. $bulkModel = $this->objectManager->create($bulk['model']);
  218. $bulkModel->sync($collection);
  219. }
  220. }
  221. }
  222. //reset total items to 0
  223. $this->totalItems = 0;
  224. //Single/Update priority.
  225. foreach ($this->singlePriority as $single) {
  226. if ($this->totalItems < $single['limit']) {
  227. $collection = $this->_getQueue(
  228. $single['type'],
  229. $single['mode'],
  230. $single['limit'] - $this->totalItems
  231. );
  232. if ($collection->getSize()) {
  233. $this->totalItems += $collection->getSize();
  234. $singleModel = $this->objectManager->create(
  235. $single['model']
  236. );
  237. $singleModel->sync($collection);
  238. }
  239. }
  240. }
  241. }
  242. /**
  243. * Set importing priority.
  244. *
  245. * @return null
  246. *
  247. * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
  248. */
  249. public function _setPriority()
  250. {
  251. /*
  252. * Bulk
  253. */
  254. $defaultBulk = [
  255. 'model' => '',
  256. 'mode' => self::MODE_BULK,
  257. 'type' => '',
  258. 'limit' => $this->bulkSyncLimit,
  259. ];
  260. //Contact Bulk
  261. $contact = $defaultBulk;
  262. $contact['model'] = \Dotdigitalgroup\Email\Model\Sync\Contact\Bulk::class;
  263. $contact['type'] = [
  264. self::IMPORT_TYPE_CONTACT,
  265. self::IMPORT_TYPE_GUEST,
  266. self::IMPORT_TYPE_SUBSCRIBERS,
  267. ];
  268. //Bulk Order
  269. $order = $defaultBulk;
  270. $order['model'] = \Dotdigitalgroup\Email\Model\Sync\Td\Bulk::class;
  271. $order['type'] = self::IMPORT_TYPE_ORDERS;
  272. //Bulk Other TD
  273. $other = $defaultBulk;
  274. $other['model'] = \Dotdigitalgroup\Email\Model\Sync\Td\Bulk::class;
  275. $other['type'] = [
  276. 'Catalog',
  277. self::IMPORT_TYPE_REVIEWS,
  278. self::IMPORT_TYPE_WISHLIST,
  279. ];
  280. /*
  281. * Update
  282. */
  283. $defaultSingleUpdate = [
  284. 'model' => \Dotdigitalgroup\Email\Model\Sync\Contact\Update::class,
  285. 'mode' => '',
  286. 'type' => '',
  287. 'limit' => self::SYNC_SINGLE_LIMIT_NUMBER,
  288. ];
  289. //Subscriber resubscribe
  290. $subscriberResubscribe = $defaultSingleUpdate;
  291. $subscriberResubscribe['mode'] = self::MODE_SUBSCRIBER_RESUBSCRIBED;
  292. $subscriberResubscribe['type'] = self::IMPORT_TYPE_SUBSCRIBER_RESUBSCRIBED;
  293. //Subscriber update/suppressed
  294. $subscriberUpdate = $defaultSingleUpdate;
  295. $subscriberUpdate['mode'] = self::MODE_SUBSCRIBER_UPDATE;
  296. $subscriberUpdate['type'] = self::IMPORT_TYPE_SUBSCRIBER_UPDATE;
  297. //Email Change
  298. $emailChange = $defaultSingleUpdate;
  299. $emailChange['mode'] = self::MODE_CONTACT_EMAIL_UPDATE;
  300. $emailChange['type'] = self::IMPORT_TYPE_CONTACT_UPDATE;
  301. //Order Update
  302. $orderUpdate = $defaultSingleUpdate;
  303. $orderUpdate['model'] = \Dotdigitalgroup\Email\Model\Sync\Td\Update::class;
  304. $orderUpdate['mode'] = self::MODE_SINGLE;
  305. $orderUpdate['type'] = self::IMPORT_TYPE_ORDERS;
  306. //Update Other TD
  307. $updateOtherTd = $defaultSingleUpdate;
  308. $updateOtherTd['model'] = \Dotdigitalgroup\Email\Model\Sync\Td\Update::class;
  309. $updateOtherTd['mode'] = self::MODE_SINGLE;
  310. $updateOtherTd['type'] = [
  311. 'Catalog',
  312. self::IMPORT_TYPE_WISHLIST,
  313. ];
  314. /*
  315. * Delete
  316. */
  317. $defaultSingleDelete = [
  318. 'model' => '',
  319. 'mode' => '',
  320. 'type' => '',
  321. 'limit' => self::SYNC_SINGLE_LIMIT_NUMBER,
  322. ];
  323. //Contact Delete
  324. $contactDelete = $defaultSingleDelete;
  325. $contactDelete['model'] = \Dotdigitalgroup\Email\Model\Sync\Contact\Delete::class;
  326. $contactDelete['mode'] = self::MODE_CONTACT_DELETE;
  327. $contactDelete['type'] = self::IMPORT_TYPE_CONTACT;
  328. //TD Delete
  329. $tdDelete = $defaultSingleDelete;
  330. $tdDelete['model'] = \Dotdigitalgroup\Email\Model\Sync\Td\Delete::class;
  331. $tdDelete['mode'] = self::MODE_SINGLE_DELETE;
  332. $tdDelete['type'] = [
  333. 'Catalog',
  334. self::IMPORT_TYPE_REVIEWS,
  335. self::IMPORT_TYPE_WISHLIST,
  336. self::IMPORT_TYPE_ORDERS,
  337. ];
  338. //Bulk Priority
  339. $this->bulkPriority = [
  340. $contact,
  341. $order,
  342. $other,
  343. ];
  344. $this->singlePriority = [
  345. $subscriberResubscribe,
  346. $subscriberUpdate,
  347. $emailChange,
  348. $orderUpdate,
  349. $updateOtherTd,
  350. $contactDelete,
  351. $tdDelete,
  352. ];
  353. }
  354. /**
  355. * Check importing status for pending import.
  356. *
  357. * @return null
  358. */
  359. public function _checkImportStatus()
  360. {
  361. if ($items = $this->_getImportingItems($this->bulkSyncLimit)) {
  362. foreach ($items as $item) {
  363. $websiteId = $item->getWebsiteId();
  364. $client = false;
  365. if ($this->helper->isEnabled($websiteId)) {
  366. $client = $this->helper->getWebsiteApiClient(
  367. $websiteId
  368. );
  369. }
  370. if ($client) {
  371. try {
  372. if ($item->getImportType() == self::IMPORT_TYPE_CONTACT ||
  373. $item->getImportType() == self::IMPORT_TYPE_SUBSCRIBERS ||
  374. $item->getImportType() == self::IMPORT_TYPE_GUEST
  375. ) {
  376. $response = $client->getContactsImportByImportId($item->getImportId());
  377. } else {
  378. $response = $client->getContactsTransactionalDataImportByImportId(
  379. $item->getImportId()
  380. );
  381. }
  382. } catch (\Exception $e) {
  383. $item->setMessage($e->getMessage())
  384. ->setImportStatus(self::FAILED);
  385. $this->saveItem($item);
  386. continue;
  387. }
  388. $this->processResponse($response, $item, $websiteId);
  389. }
  390. }
  391. }
  392. }
  393. /**
  394. * @param Object $response
  395. * @param \Dotdigitalgroup\Email\Model\Importer $item
  396. * @param int $websiteId
  397. *
  398. * @return null
  399. */
  400. private function processResponse($response, $item, $websiteId)
  401. {
  402. if (isset($response->message)) {
  403. $item->setImportStatus(self::FAILED)
  404. ->setMessage($response->message);
  405. } else {
  406. if ($response->status == 'Finished') {
  407. $item = $this->processFinishedItem($item, $websiteId);
  408. } elseif (in_array($response->status, $this->importStatuses)) {
  409. $item->setImportStatus(self::FAILED)
  410. ->setMessage('Import failed with status ' . $response->status);
  411. } else {
  412. //Not finished
  413. $this->totalItems += 1;
  414. }
  415. }
  416. //Save item
  417. $this->saveItem($item);
  418. }
  419. /**
  420. * @param Importer $item
  421. * @param int $websiteId
  422. *
  423. * @return $this
  424. *
  425. * @throws \Magento\Framework\Exception\LocalizedException
  426. */
  427. private function processFinishedItem($item, $websiteId)
  428. {
  429. $now = gmdate('Y-m-d H:i:s');
  430. $item->setImportStatus(self::IMPORTED)
  431. ->setImportFinished($now)
  432. ->setMessage('');
  433. if ($item->getImportType() == self::IMPORT_TYPE_CONTACT ||
  434. $item->getImportType() == self::IMPORT_TYPE_SUBSCRIBERS ||
  435. $item->getImportType() == self::IMPORT_TYPE_GUEST
  436. ) {
  437. //if file
  438. if ($file = $item->getImportFile()) {
  439. //remove the consent data for contacts before arhiving the file
  440. $log = $this->helper->fileHelper->cleanProcessedConsent(
  441. $this->helper->fileHelper->getFilePathWithFallback($file)
  442. );
  443. if ($log) {
  444. $this->helper->log($log);
  445. }
  446. if (! $this->helper->fileHelper->isFileAlreadyArchived($file)) {
  447. $this->helper->fileHelper->archiveCSV($file);
  448. }
  449. }
  450. if ($item->getImportId()) {
  451. $this->_processContactImportReportFaults($item->getImportId(), $websiteId);
  452. }
  453. }
  454. return $item;
  455. }
  456. /**
  457. * @param \Dotdigitalgroup\Email\Model\Importer $itemToSave
  458. *
  459. * @return null
  460. */
  461. private function saveItem($itemToSave)
  462. {
  463. $this->importerResource->save($itemToSave);
  464. }
  465. /**
  466. * Get imports marked as importing.
  467. *
  468. * @param int $limit
  469. *
  470. * @return \Dotdigitalgroup\Email\Model\ResourceModel\Importer\Collection|bool
  471. */
  472. public function _getImportingItems($limit)
  473. {
  474. return $this->getCollection()
  475. ->getItemsWithImportingStatus($limit);
  476. }
  477. /**
  478. * Get report info for contacts sync.
  479. *
  480. * @param int $id
  481. * @param int $websiteId
  482. *
  483. * @throws \Magento\Framework\Exception\LocalizedException
  484. *
  485. * @return null
  486. */
  487. public function _processContactImportReportFaults($id, $websiteId)
  488. {
  489. $client = $this->helper->getWebsiteApiClient($websiteId);
  490. $report = $client->getContactImportReportFaults($id);
  491. if ($report) {
  492. $reportData = explode(PHP_EOL, $this->_removeUtf8Bom($report));
  493. //unset header
  494. unset($reportData[0]);
  495. //no data in report
  496. if (! empty($reportData)) {
  497. $contacts = [];
  498. foreach ($reportData as $row) {
  499. $row = explode(',', $row);
  500. //reason
  501. if (in_array($row[0], $this->reasons)) {
  502. //email
  503. $contacts[] = $row[1];
  504. }
  505. }
  506. //unsubscribe from email contact and newsletter subscriber tables
  507. $this->helper->contactResource->unsubscribe($contacts);
  508. }
  509. }
  510. }
  511. /**
  512. * Convert utf8 data.
  513. *
  514. * @param string $text
  515. *
  516. * @return string
  517. */
  518. public function _removeUtf8Bom($text)
  519. {
  520. $bom = pack('H*', 'EFBBBF');
  521. $text = preg_replace("/^$bom/", '', $text);
  522. return $text;
  523. }
  524. /**
  525. * Get the imports by type.
  526. *
  527. * @param string $importType
  528. * @param string $importMode
  529. * @param int $limit
  530. *
  531. * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
  532. */
  533. public function _getQueue($importType, $importMode, $limit)
  534. {
  535. return $this->getCollection()
  536. ->getQueueByTypeAndMode($importType, $importMode, $limit);
  537. }
  538. }