Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Api/ReclaimInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
123 changes: 123 additions & 0 deletions Cron/ProductsTopic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace Klaviyo\Reclaim\Cron;

use Klaviyo\Reclaim\Helper\Logger;
use Klaviyo\Reclaim\Model\SyncsFactory;
use Klaviyo\Reclaim\Model\ResourceModel\Products;
use Klaviyo\Reclaim\Model\ResourceModel\Products\CollectionFactory;

use Magento\Catalog\Model\CategoryFactory;

class ProductsTopic
{
/**
* Klaviyo Logger
* @var Logger
*/
protected $_klaviyoLogger;

/**
* Magento product category helper
* @var CategoryFactory $categoryFactory
*/
protected $_categoryFactory;

/**
* Klaviyo Products Resource Model
* @var Products
*/
protected $_klProduct;

/**
* Klaviyo Products Collection
* @var CollectionFactory
*/
protected $_klProductCollectionFactory;

/**
* Klaviyo Syncs Model
* @var SyncsFactory
*/
protected $_klSyncFactory;

/**
* @param Logger $klaviyoLogger
* @param Products $klProduct
* @param SyncsFactory $klSyncFactory
* @param CollectionFactory $klProductCollectionFactory
*/
public function __construct(
Logger $klaviyoLogger,
Products $klProduct,
CategoryFactory $categoryFactory,
SyncsFactory $klSyncFactory,
CollectionFactory $klProductCollectionFactory
)
{
$this->_klaviyoLogger = $klaviyoLogger;
$this->_klProduct = $klProduct;
$this->_categoryFactory = $categoryFactory;
$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->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);
}

/**
* Helper function to associate category names with their ids
* @param string $payload
* @return array
*/
public function addCategoryNames(string $payload): array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Over here, we will be iterating over each categoryId for all payloads passed, so we will be running the for loop 500 times, if categoryIds, exist and possibly loading duplicate categroyModels since it is likely more than one product will have a category repeated. Can we make it such that we create a class level attribute to have Ids and categoryNames associated so don't duplicate the effort?

#142 (comment) > as advised here?

Or do you think it is worth having a method in the helper class which can be used by both the Cron?

{
$decoded_payload = json_decode($payload, true);
$category_ids = $decoded_payload['product']['categories'];
if (empty($category_ids)) {return $decoded_payload;}
$decoded_payload['product']['categories'] = [];
$category_factory = $this->_categoryFactory->create();
foreach ($category_ids as $category_id) {
$category = $category_factory->load($category_id);
$decoded_payload['product']['categories'][$category_id] = $category->getName();
}
return $decoded_payload;
}
}
25 changes: 19 additions & 6 deletions Helper/ScopeSetting.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -143,6 +144,14 @@ public function getWebhookSecret($storeId = null)
return $this->getScopeSetting(self::WEBHOOK_SECRET, $storeId);
}

public function getWebhooks()
{
return $registeredWebhooks = [
['product/delete' => $this->getProductDeleteWebhookSetting()],
['product/save' => $this->getProductSaveWebhookSetting()],
];
}

public function isEnabled($storeId = null)
{
return $this->getScopeSetting(self::ENABLE, $storeId);
Expand Down Expand Up @@ -221,7 +230,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);
Expand Down Expand Up @@ -259,10 +268,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);
}

}
18 changes: 8 additions & 10 deletions Helper/Webhook.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -51,12 +50,12 @@ 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(
'Content-Type: application/json',
'Magento-two-signature: ' . $this->createWebhookSecurity($data),
'Content-Length: '. strlen(json_encode($data)),
'Content-Length: '. strlen($data),
'Topic: ' . $webhookType
),
]);
Expand All @@ -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(sprintf("Unable to send webhook to $url with data: $data"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit:

Suggested change
$this->_klaviyoLogger->log(sprintf("Unable to send webhook to $url with data: $data"));
$this->_klaviyoLogger->log("Unable to send webhook to $url with data: $data");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoops haha!

}

// Close cURL session handle
curl_close($curl);

return $response;
}

/**
* @param array data
* @param string data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a description to this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work?

* @param string $data json payload used to create hmac signature

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param string data
* Returns an HMAC signature for webhooks
* @param string data

* @return string
* @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);
}
}

5 changes: 5 additions & 0 deletions Model/Reclaim.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public function getWebhookSecret()
return $this->_klaviyoScopeSetting->getWebhookSecret();
}

public function getWebhooks()
{
return $this->_klaviyoScopeSetting->getWebhooks();
}

/**
* Returns the Klaviyo log file
*
Expand Down
2 changes: 1 addition & 1 deletion Observer/ProductDeleteBefore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
116 changes: 116 additions & 0 deletions Observer/ProductSaveAfter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace Klaviyo\Reclaim\Observer;

use Exception;
use Klaviyo\Reclaim\Helper\ScopeSetting;
use Klaviyo\Reclaim\Model\ProductsFactory;

use Magento\CatalogInventory\Api\StockRegistryInterface;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;


class ProductSaveAfter implements ObserverInterface
{
/**
* Klaviyo scope setting helper
* @var ScopeSetting $klaviyoScopeSetting
*/
protected $_klaviyoScopeSetting;

/**
* Klaviyo product factory
* @var klProductFactory
*/
protected $_klProductFactory;

/**
* Magento stock registry api interface
* @var $stockRegistry
*/
protected $_stockRegistry;

/**
* @param ScopeSetting $klaviyoScopeSetting
* @param CategoryFactory $categoryFactory
* @param ProductsFactory $klProductFactory
* @param StockRegistryInterface $stockRegistry
*/
public function __construct(
ScopeSetting $klaviyoScopeSetting,
ProductsFactory $klProductFactory,
StockRegistryInterface $stockRegistry
) {
$this->_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 = array(
'store_ids' => $product->getStoreIds(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all array_keys supposed to start lower case/upper case? Could we standardize across the payload?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only use uppercase letters on some of the properties because Klaviyo wants them that way so this avoids normalizing the payload in the app. otherwise I user lowercase where I can. Let me know if that's alright or not!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any restriction on making other start upper case too? Just looking for standardize keys. it's a nitpick so if it is too much work to accommodate this, we can skip it

'product' => array(
'ID' => $product_id,
'TypeID' => $product->getTypeId(),
'Name' => $product->getName(),
'qty' => $this->_stockRegistry->getStockItem($product_id)->getQty(),
'Visibility' => $product->getVisibility(),
'IsInStock' => $product->isInStock(),
'Status' => $product->getStatus(),
'CreatedAt' => $product->getCreatedAt(),
'UpdatedAt' => $product->getUpdatedAt(),
'FirstImageURL' => $product->getImage(),
'ThumbnailImageURL' => $product->getThumbnail(),
'metadata' => array(
'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;
}
}
Loading