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 @@
+
+
+
+
+
+