Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds product/save to message queue #144

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
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
69 changes: 9 additions & 60 deletions Cron/EventsTopic.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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))
);
}
}
104 changes: 104 additions & 0 deletions Cron/ProductsTopic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace Klaviyo\Reclaim\Cron;

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

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

/**
* Klaviyo helper for mapping category ids to names
* @var CategoryMapper $categoryMapperactory
*/
protected $_categoryMapper;

/**
* 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
* @param CategoryMapper $categoryMapper
*/
public function __construct(
Logger $klaviyoLogger,
Products $klProduct,
CategoryMapper $categoryMapper,
SyncsFactory $klSyncFactory,
CollectionFactory $klProductCollectionFactory
)
{
$this->_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);
}
}
107 changes: 107 additions & 0 deletions Helper/CategoryMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

namespace Klaviyo\Reclaim\Helper;

use Magento\Catalog\Model\CategoryFactory;

class CategoryMapper extends \Magento\Framework\App\Helper\AbstractHelper
{
/**
* Magento Category Factory
* @var CategoryFactory
*/
protected $_categoryFactory;

/**
* Category Map of ids to names
* @var array
*/
protected $categoryMap = [];

/**
* @param CategoryFactory $categoryFactory
*/
public function __construct(
CategoryFactory $categoryFactory
){
$this->_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;
}
}
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()],
];
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 value in returning topics that aren't enabled? Thinking about other platforms for which we subscribe to certain webhooks (Shopify, WooCommerce), I'm pretty sure the endpoints that return this info only return the topics to which we are registered rather than a comprehensive list of all available.

I think it might be cleaner (and more easily extended in the future) to format this as an indexed array of associative arrays, similar to what those other platforms return. ex:

[
    [
        'topic' => 'product/delete',
        'enabled' => $this->getProductDeleteWebhookSetting(),
    ],
    [
        'topic' => 'product/save',
        'enabled' => $this-> getProductSaveWebhookSetting(),
    ]
]

Copy link
Contributor

Choose a reason for hiding this comment

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

We could also allow for a param in this request to optionally fetch all available webhooks vs those that are enabled

Copy link
Contributor Author

@jordanallain jordanallain Nov 17, 2021

Choose a reason for hiding this comment

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

I'd be fine with only sending back the enabled webhooks if we want to go that direction. In the app code that hits this endpoint I essentially filter out the ones that aren't enabled anyway. If we're only going to return the enabled ones then I'm not sure we need anything other than a list of the topics though?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yea, I guess since this is only for an inspector task it's probably fine however. Is there any other info stored about this? like when this setting was last updated for example? That could be useful for troubleshooting but may not be available

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is actually used for more than the inspector task, it is used in setup_webhooks to update the integration blob setting registered_webhooks. Then in the product sync I have it check that before making any queries for product info.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh, forgot about that. In that case if it's used in two different scenarios, I'd let the app code contain the logic to process the response/format the value to be saved and return as much info here for troubleshooting purposes which can be accessed via the inspector task.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd love to see this return an indexed array containing an associative array for each webhook topic. Makes the logic in the app way easier to read instead of relying on the index for the topic and enabled boolean.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry for the confusion around this! should be updated now though to what you were describing.

}

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);
}

}
Loading