diff --git a/Api/ReclaimInterface.php b/Api/ReclaimInterface.php index 6bbca46..aeba458 100644 --- a/Api/ReclaimInterface.php +++ b/Api/ReclaimInterface.php @@ -27,6 +27,14 @@ public function reclaim(); */ public function getWebhookSecret(); + /** + * Returns the registered webhooks + * + * @return mixed[] + * @api + */ + public function getWebhooks(); + /** * Returns the Klaviyo log file * diff --git a/Cron/EventsTopic.php b/Cron/EventsTopic.php index 0b4546f..0dce1ac 100644 --- a/Cron/EventsTopic.php +++ b/Cron/EventsTopic.php @@ -2,6 +2,7 @@ namespace Klaviyo\Reclaim\Cron; +use Klaviyo\Reclaim\Helper\CategoryMapper; use Klaviyo\Reclaim\Helper\Logger; use Klaviyo\Reclaim\Model\SyncsFactory; use Klaviyo\Reclaim\Model\Quote\QuoteIdMask; @@ -36,37 +37,31 @@ class EventsTopic protected $_klSyncFactory; /** - * Magento Category Factory - * @var CategoryFactory + * Klaviyo helper for mapping category ids to names + * @var CategoryMapper $categoryMapperactory */ - protected $_categoryFactory; - - /** - * Category Map of ids to names - * @var array - */ - protected $categoryMap = []; - + protected $_categoryMapper; /** * @param Logger $klaviyoLogger * @param CollectionFactory $eventsCollectionFactory * @param SyncsFactory $klSyncFactory * @param QuoteIdMask $quoteIdMaskResource * @param CategoryFactory $categoryFactory + * @param CategoryMapper $categoryMapper */ public function __construct( Logger $klaviyoLogger, CollectionFactory $eventsCollectionFactory, SyncsFactory $klSyncFactory, QuoteIdMask $quoteIdMaskResource, - CategoryFactory $categoryFactory + CategoryMapper $categoryMapper ) { $this->_klaviyoLogger = $klaviyoLogger; $this->_eventsCollectionFactory = $eventsCollectionFactory; $this->_klSyncFactory = $klSyncFactory; $this->_quoteIdMaskResource = $quoteIdMaskResource; - $this->_categoryFactory = $categoryFactory; + $this->_categoryMapper = $categoryMapper; } /** @@ -89,7 +84,7 @@ public function moveRowsToSync() // Capture all events that have been moved and add data to Sync table foreach ($eventsData as $event){ if ($event['event'] == 'Added To Cart') { - $event['payload'] = json_encode($this->replaceQuoteIdAndCategoryIds($event['payload'])); + $event['payload'] = json_encode($this->_categoryMapper->replaceQuoteIdAndCategoryIds($event['payload'])); } //TODO: This can probably be done as one bulk update instead of individual inserts @@ -137,54 +132,8 @@ public function replaceQuoteIdAndCategoryIds(string $payload): array unset($decoded_payload['QuoteId']); // Replace CategoryIds for Added Item, Quote Items with resp. CategoryNames - $decoded_payload = $this->replaceCategoryIdsWithNames($decoded_payload); + $decoded_payload = $this->_categoryMapper->replaceCategoryIdsWithNames($decoded_payload); return $decoded_payload; } - - /** - * Replace all CategoryIds in event payload with their respective names - * @param $payload - * @return array - */ - public function replaceCategoryIdsWithNames(array $payload): array - { - $this->updateCategoryMap($payload['Categories']); - - foreach ($payload['Items'] as &$item){ - $item['Categories'] = $this->searchCategoryMapAndReturnNames($item['Categories']); - } - - $payload['AddedItemCategories'] = $this->searchCategoryMapAndReturnNames($payload['AddedItemCategories']); - $payload['Categories'] = $this->searchCategoryMapAndReturnNames($payload['Categories']); - - return $payload; - } - - /** - * Retrieve categoryNames using categoryIds - * @param array $categoryIds - */ - public function updateCategoryMap(array $categoryIds) - { - $categoryFactory = $this->_categoryFactory->create(); - - foreach($categoryIds as $categoryId){ - if (!in_array($categoryId, $this->categoryMap)){ - $this->categoryMap[$categoryId] = $categoryFactory->load($categoryId)->getName(); - } - } - } - - /** - * Return array of CategoryNames from CategoryMap using ids - * @param array $categoryIds - * @return array - */ - public function searchCategoryMapAndReturnNames(array $categoryIds): array - { - return array_values( - array_intersect_key($this->categoryMap, array_flip($categoryIds)) - ); - } } diff --git a/Cron/ProductsTopic.php b/Cron/ProductsTopic.php new file mode 100644 index 0000000..56e41b1 --- /dev/null +++ b/Cron/ProductsTopic.php @@ -0,0 +1,104 @@ +_klaviyoLogger = $klaviyoLogger; + $this->_klProduct = $klProduct; + $this->_categoryMapper = $categoryMapper; + $this->_klSyncFactory = $klSyncFactory; + $this->_klProductCollectionFactory = $klProductCollectionFactory; + } + + public function queueKlProductsForSync() + { + $klProductsCollection = $this->_klProductCollectionFactory->create(); + $klProductsToSync = $klProductsCollection->getRowsForSync('NEW') + ->addFieldToSelect(['id','payload','status','topic', 'klaviyo_id']) + ->getData(); + + if (empty($klProductsToSync)) {return;} + + $idsToUpdate = []; + + foreach ($klProductsToSync as $klProductToSync) + { + $klProductToSync['payload'] = json_encode($this->_categoryMapper->addCategoryNames($klProductToSync['payload'])); + $klSync = $this->_klSyncFactory->create(); + $klSync->setData([ + 'payload'=> $klProductToSync['payload'], + 'topic'=> $klProductToSync['topic'], + 'klaviyo_id'=>$klProductToSync['klaviyo_id'], + 'status'=> 'NEW' + ]); + try { + $klSync->save(); + array_push($idsToUpdate, $klProductToSync['id']); + } catch (\Exception $e) { + $this->_klaviyoLogger->log(sprintf('Unable to move row: %s', $e->getMessage())); + } + } + + $klProductsCollection->updateRowStatus($idsToUpdate, 'MOVED'); + } + + public function clean() + { + $klProductsCollection = $this->_klProductCollectionFactory->create(); + $idsToDelete = $klProductsCollection->getIdsToDelete('MOVED'); + + $klProductsCollection->deleteRows($idsToDelete); + } +} diff --git a/Helper/CategoryMapper.php b/Helper/CategoryMapper.php new file mode 100644 index 0000000..f4c1bd3 --- /dev/null +++ b/Helper/CategoryMapper.php @@ -0,0 +1,107 @@ +_categoryFactory = $categoryFactory; + } + + /** + * Replace all CategoryIds in payload with their respective names + * @param $payload + * @return array + */ + public function replaceCategoryIdsWithNames(array $payload): array + { + $this->updateCategoryMap($payload['Categories']); + + foreach ($payload['Items'] as &$item){ + $item['Categories'] = $this->searchCategoryMapAndReturnNames($item['Categories']); + } + + $payload['AddedItemCategories'] = $this->searchCategoryMapAndReturnNames($payload['AddedItemCategories']); + $payload['Categories'] = $this->searchCategoryMapAndReturnNames($payload['Categories']); + + return $payload; + } + + /** + * Adds all category names in payload to their respective ids + * @param $payload json encoded string of the payload + * @return array + */ + public function addCategoryNames(string $payload): array + { + $decoded_payload = json_decode($payload, true); + $this->updateCategoryMap($decoded_payload['product']['Categories']); + + $decoded_payload['product']['Categories'] = $this-> + searchCategoryMapAndReturnIdsAndNames( + $decoded_payload['product']['Categories'] + ); + + return $decoded_payload; + } + + /** + * Retrieve categoryNames using categoryIds + * @param array $categoryIds + */ + public function updateCategoryMap(array $categoryIds) + { + $categoryFactory = $this->_categoryFactory->create(); + + foreach($categoryIds as $categoryId){ + if (!in_array($categoryId, $this->categoryMap)){ + $this->categoryMap[$categoryId] = $categoryFactory->load($categoryId)->getName(); + } + } + } + + /** + * Return array of CategoryNames from CategoryMap using ids + * @param array $categoryIds + * @return array + */ + public function searchCategoryMapAndReturnNames(array $categoryIds): array + { + return array_values( + array_intersect_key($this->categoryMap, array_flip($categoryIds)) + ); + } + + /** + * Return array of arrays mapping category ids to their names + * @param array $categoryIds + * @return array + */ + public function searchCategoryMapAndReturnIdsAndNames(array $categoryIds): array + { + $categoryIdsAndNames = []; + foreach ($categoryIds as $categoryId){ + $categoryIdsAndNames[$categoryId] = $this->categoryMap[$categoryId]; + } + return $categoryIdsAndNames; + } +} diff --git a/Helper/ScopeSetting.php b/Helper/ScopeSetting.php index 61c8b99..5d4286f 100755 --- a/Helper/ScopeSetting.php +++ b/Helper/ScopeSetting.php @@ -29,8 +29,9 @@ class ScopeSetting extends \Magento\Framework\App\Helper\AbstractHelper const KLAVIYO_NAME_DEFAULT = 'klaviyo'; const WEBHOOK_SECRET = 'klaviyo_reclaim_webhook/klaviyo_webhooks/webhook_secret'; - const PRODUCT_DELETE_BEFORE = 'klaviyo_reclaim_webhook/klaviyo_webhooks/using_product_delete_before_webhook'; - + const PRODUCT_DELETE_WEBHOOK = 'klaviyo_reclaim_webhook/klaviyo_webhooks/using_product_delete_webhook'; + const PRODUCT_SAVE_WEBHOOK = 'klaviyo_reclaim_webhook/klaviyo_webhooks/using_product_save_webhook'; + const KLAVIYO_OAUTH_NAME = 'klaviyo_reclaim_oauth/klaviyo_oauth/integration_name'; protected $_scopeConfig; @@ -143,6 +144,19 @@ public function getWebhookSecret($storeId = null) return $this->getScopeSetting(self::WEBHOOK_SECRET, $storeId); } + public function getWebhooks() + { + return $registeredWebhooks = [ + [ + 'topic' => 'product/delete', + 'enabled' => $this->getProductDeleteWebhookSetting() + ], + [ + 'topic' => 'product/save', + 'enabled' => $this->getProductSaveWebhookSetting()], + ]; + } + public function isEnabled($storeId = null) { return $this->getScopeSetting(self::ENABLE, $storeId); @@ -221,7 +235,7 @@ public function getConsentAtCheckoutSMSListId($storeId = null) { return $this->getScopeSetting(self::CONSENT_AT_CHECKOUT_SMS_LIST_ID, $storeId); } - + public function getConsentAtCheckoutSMSConsentText($storeId = null) { return $this->getScopeSetting(self::CONSENT_AT_CHECKOUT_SMS_CONSENT_TEXT, $storeId); @@ -259,10 +273,14 @@ public function getStoreIdKlaviyoAccountSetMap($storeIds) return $storeMap; } - public function getProductDeleteBeforeSetting($storeId = null) + public function getProductDeleteWebhookSetting($storeId = null) { - return $this->getScopeSetting(self::PRODUCT_DELETE_BEFORE, $storeId); + return $this->getScopeSetting(self::PRODUCT_DELETE_WEBHOOK, $storeId); } -} + public function getProductSaveWebhookSetting($storeId = null) + { + return $this->getScopeSetting(self::PRODUCT_SAVE_WEBHOOK, $storeId); + } +} diff --git a/Helper/Webhook.php b/Helper/Webhook.php index b475265..b6bdc09 100755 --- a/Helper/Webhook.php +++ b/Helper/Webhook.php @@ -33,14 +33,13 @@ public function __construct( /** * @param string $webhookType - * @param array $data + * @param string $data json payload to be sent in the body of the request * @param string $klaviyoId * @return string * @throws Exception */ public function makeWebhookRequest($webhookType, $data, $klaviyoId=null) { - if (!$klaviyoId) { $klaviyoId = $this->_klaviyoScopeSetting->getPublicApiKey(); } @@ -51,14 +50,14 @@ public function makeWebhookRequest($webhookType, $data, $klaviyoId=null) CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'POST', - CURLOPT_POSTFIELDS => json_encode($data), + CURLOPT_POSTFIELDS => $data, CURLOPT_USERAGENT => self::USER_AGENT, - CURLOPT_HTTPHEADER => array( + CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Magento-two-signature: ' . $this->createWebhookSecurity($data), - 'Content-Length: '. strlen(json_encode($data)), + 'Content-Length: '. strlen($data), 'Topic: ' . $webhookType - ), + ], ]); // Submit the request @@ -66,24 +65,23 @@ public function makeWebhookRequest($webhookType, $data, $klaviyoId=null) $err = curl_errno($curl); if ($err) { - $this->_klaviyoLogger->log(sprintf('Unable to send webhook to %s with data: %s', $url, json_encode($data))); + $this->_klaviyoLogger->log("Unable to send webhook to $url with data: $data"); } // Close cURL session handle curl_close($curl); + return $response; } /** - * @param array data - * @return string + * @param string $data json payload used to create hmac signature + * @return string an HMAC signature for webhooks * @throws Exception */ - private function createWebhookSecurity(array $data) + private function createWebhookSecurity(string $data) { $webhookSecret = $this->_klaviyoScopeSetting->getWebhookSecret(); - return hash_hmac('sha256', json_encode($data), $webhookSecret); - + return hash_hmac('sha256', $data, $webhookSecret); } } - diff --git a/Model/Reclaim.php b/Model/Reclaim.php index bcfa05a..d87e5b2 100644 --- a/Model/Reclaim.php +++ b/Model/Reclaim.php @@ -73,6 +73,11 @@ public function getWebhookSecret() return $this->_klaviyoScopeSetting->getWebhookSecret(); } + public function getWebhooks() + { + return $this->_klaviyoScopeSetting->getWebhooks(); + } + /** * Returns the Klaviyo log file * diff --git a/Observer/ProductDeleteBefore.php b/Observer/ProductDeleteBefore.php index 9f25d59..82a2190 100644 --- a/Observer/ProductDeleteBefore.php +++ b/Observer/ProductDeleteBefore.php @@ -67,7 +67,7 @@ public function execute(Observer $observer) 'store_ids' => $storeIds, 'product_id' => $product->getId(), ); - $this->_webhookHelper->makeWebhookRequest('product/delete', $data, $klaviyoId); + $this->_webhookHelper->makeWebhookRequest('product/delete', json_encode($data), $klaviyoId); } } } diff --git a/Observer/ProductSaveAfter.php b/Observer/ProductSaveAfter.php new file mode 100644 index 0000000..86b4c14 --- /dev/null +++ b/Observer/ProductSaveAfter.php @@ -0,0 +1,115 @@ +_klaviyoScopeSetting = $klaviyoScopeSetting; + $this->_klProductFactory = $klProductFactory; + $this->_stockRegistry = $stockRegistry; + } + + /** + * customer register event handler + * + * @param Observer $observer + * @return void + * @throws Exception + */ + public function execute(Observer $observer) + { + $product = $observer->getEvent()->getProduct(); + $storeIds = $product->getStoreIds(); + $storeIdKlaviyoMap = $this->_klaviyoScopeSetting->getStoreIdKlaviyoAccountSetMap($storeIds); + + foreach ($storeIdKlaviyoMap as $klaviyoId => $storeIds) { + if (empty($storeIds)) {continue;} + + if ($this->_klaviyoScopeSetting->getWebhookSecret() && $this->_klaviyoScopeSetting->getProductSaveWebhookSetting($storeIds[0])) { + $normalizedProduct = $this->normalizeProduct($product); + $data = [ + 'status'=>'NEW', + 'topic'=>'product/save', + 'klaviyo_id'=>$klaviyoId, + 'payload'=>json_encode($normalizedProduct) + ]; + $klProduct = $this->_klProductFactory->create(); + $klProduct->setData($data); + $klProduct->save(); + } + } + } + + private function normalizeProduct($product=null) + { + if ($product == null) {return;} + + $product_id = $product->getId(); + + $product_info = [ + 'store_ids' => $product->getStoreIds(), + 'product' => [ + 'ID' => $product_id, + 'TypeID' => $product->getTypeId(), + 'Name' => $product->getName(), + 'Visibility' => $product->getVisibility(), + 'IsInStock' => $product->isInStock(), + 'Status' => $product->getStatus(), + 'CreatedAt' => $product->getCreatedAt(), + 'UpdatedAt' => $product->getUpdatedAt(), + 'FirstImageURL' => $product->getImage(), + 'ThumbnailImageURL' => $product->getThumbnail(), + 'Metadata' => [ + 'price' => $product->getPrice(), + 'sku' => $product->getSku() + ], + 'Categories' => $product->getCategoryIds() + ] + ]; + + if ($product->getSpecialPrice()) { + $product_info['Metadata']['special_price'] = $product->getSpecialPrice(); + $product_info['Metadata']['special_from_date'] = $product->getSpecialFromDate(); + $product_info['Metadata']['special_to_date'] = $product->getSpecialToDate(); + } + return $product_info; + } +} diff --git a/Observer/SaveOrderMarketingConsent.php b/Observer/SaveOrderMarketingConsent.php index 7fa0be2..297062f 100644 --- a/Observer/SaveOrderMarketingConsent.php +++ b/Observer/SaveOrderMarketingConsent.php @@ -92,7 +92,7 @@ public function execute(Observer $observer) } if (count($data["data"]) > 0) { - $this->_webhookHelper->makeWebhookRequest('custom/consent', $data); + $this->_webhookHelper->makeWebhookRequest('custom/consent', json_encode($data)); } return $this; diff --git a/etc/adminhtml/events.xml b/etc/adminhtml/events.xml index 1dc2601..c454f9d 100644 --- a/etc/adminhtml/events.xml +++ b/etc/adminhtml/events.xml @@ -5,6 +5,9 @@ + + + diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 1f1a02b..13995e0 100755 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -153,11 +153,16 @@ Magento\Config\Model\Config\Backend\Encrypted - + Magento\Config\Model\Config\Source\Yesno This will remove deleted products from the Klaviyo catalog. + + + Magento\Config\Model\Config\Source\Yesno + This will update or create saved products in the Klaviyo catalog. + diff --git a/etc/crontab.xml b/etc/crontab.xml index 0ce9711..dd2cd29 100644 --- a/etc/crontab.xml +++ b/etc/crontab.xml @@ -1,10 +1,10 @@ - + */5 * * * * - + 59 23 * * * @@ -16,5 +16,11 @@ 59 23 * * * + + */5 * * * * + + + 59 23 * * * + diff --git a/etc/webapi.xml b/etc/webapi.xml index b40b57c..f1012d1 100644 --- a/etc/webapi.xml +++ b/etc/webapi.xml @@ -54,6 +54,12 @@ + + + + + +