Skip to main content

BEYABLE Product Ranking API - Magento Integration Guide

Overview

This guide provides step-by-step instructions for integrating the BEYABLE Product Ranking API with your Magento 2 e-commerce platform. The integration will allow you to optimize product listings using BEYABLE's AI-powered ranking rules.

Table of Contents

  1. Prerequisites
  2. Architecture Overview
  3. Installation
  4. Module Structure
  5. Configuration
  6. Implementation Steps
  7. Testing
  8. Deployment
  9. Troubleshooting
  10. Performance Optimization

Prerequisites

System Requirements

  • Magento 2.4.x or higher
  • PHP 7.4 or higher
  • MySQL 5.7 or higher
  • Composer installed
  • Cron configured and running
  • BEYABLE account with API credentials

Required Information from BEYABLE

Before starting the integration, obtain the following from your BEYABLE account manager:

  • Account ID: Your 32-character UUID (without hyphens)
  • Subscription Key: Your API authentication key
  • Ranking Rule ID(s): UUID for each ranking rule you want to use
  • Segmented Ranking ID(s): UUID for segmented rankings (if applicable)
  • Tenant Configuration: Your tenant identifiers (e.g., store views, countries)

Architecture Overview

The integration follows this flow:

  1. BEYABLE Cookie Reading: Read visitor segment from cookie set by BEYABLE Platform
  2. API Integration: Fetch rankings from BEYABLE API via scheduled cron jobs
  3. Database Storage: Store rankings in custom Magento table
  4. Product Collection Modification: Apply rankings to product listings
  5. Frontend Display: Show optimized product order to visitors

Installation

Step 1: Create Custom Magento Module

Create the module directory structure:

cd app/code
mkdir -p Beyable/ProductRanking

Step 2: Create Module Files

composer.json

{
"name": "beyable/module-product-ranking",
"description": "BEYABLE Product Ranking API Integration",
"type": "magento2-module",
"version": "1.0.0",
"license": "proprietary",
"require": {
"php": "^7.4|^8.0",
"magento/framework": "^103.0"
},
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"Beyable\\ProductRanking\\": ""
}
}
}

registration.php

<?php
/**
* BEYABLE Product Ranking Module Registration
*/
use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Beyable_ProductRanking',
__DIR__
);

etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Beyable_ProductRanking" setup_version="1.0.0">
<sequence>
<module name="Magento_Catalog"/>
</sequence>
</module>
</config>

Module Structure

app/code/Beyable/ProductRanking/
├── Api/
│ ├── Data/
│ │ └── RankingInterface.php
│ ├── RankingRepositoryInterface.php
│ └── RankingManagementInterface.php
├── Block/
│ └── Tracking.php
├── Console/
│ └── Command/
│ └── SyncRankings.php
├── Cron/
│ └── SyncRankings.php
├── etc/
│ ├── adminhtml/
│ │ └── system.xml
│ ├── crontab.xml
│ ├── di.xml
│ ├── module.xml
│ └── config.xml
├── Helper/
│ └── Data.php
├── Model/
│ ├── Config.php
│ ├── Ranking.php
│ ├── RankingManagement.php
│ ├── RankingRepository.php
│ ├── ResourceModel/
│ │ ├── Ranking.php
│ │ └── Ranking/
│ │ └── Collection.php
│ └── Api/
│ └── Client.php
├── Observer/
│ └── ApplyRankingToCollection.php
├── Plugin/
│ └── Catalog/
│ └── Model/
│ └── ResourceModel/
│ └── Product/
│ └── Collection.php
├── Setup/
│ ├── InstallSchema.php
│ └── UpgradeSchema.php
├── view/
│ └── frontend/
│ ├── layout/
│ │ └── default.xml
│ └── templates/
│ └── tracking.phtml
├── composer.json
└── registration.php

Configuration

Step 1: System Configuration (etc/adminhtml/system.xml)

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<tab id="beyable" translate="label" sortOrder="400">
<label>BEYABLE</label>
</tab>
<section id="beyable_ranking" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Product Ranking</label>
<tab>beyable</tab>
<resource>Beyable_ProductRanking::config</resource>
<group id="general" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>General Configuration</label>
<field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Enable BEYABLE Ranking</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="account_id" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Account ID</label>
<comment>Your BEYABLE account ID (32 characters, no hyphens)</comment>
<validate>required-entry validate-length maximum-length-32 minimum-length-32</validate>
<depends>
<field id="enabled">1</field>
</depends>
</field>
<field id="subscription_key" translate="label comment" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Subscription Key</label>
<comment>Your BEYABLE API subscription key</comment>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
<validate>required-entry</validate>
<depends>
<field id="enabled">1</field>
</depends>
</field>
<field id="api_base_url" translate="label" type="text" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0">
<label>API Base URL</label>
<validate>required-entry validate-url</validate>
<depends>
<field id="enabled">1</field>
</depends>
</field>
</group>
<group id="ranking_rules" translate="label" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Ranking Rules</label>
<field id="default_ranking_id" translate="label comment" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Default Ranking ID</label>
<comment>UUID of the default ranking rule (32 characters, no hyphens)</comment>
<validate>validate-length maximum-length-32 minimum-length-32</validate>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="segmented_ranking_id" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Segmented Ranking ID</label>
<comment>UUID for segmented ranking (optional, 32 characters, no hyphens)</comment>
<validate>validate-length maximum-length-32 minimum-length-32</validate>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="use_segmentation" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Use Visitor Segmentation</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="segment_cookie_name" translate="label comment" type="text" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Segment Cookie Name</label>
<comment>Name of the cookie containing visitor segment (default: b_segment)</comment>
<depends>
<field id="use_segmentation">1</field>
</depends>
</field>
</group>
<group id="sync" translate="label" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Synchronization</label>
<field id="cron_enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Enable Cron Sync</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="cron_schedule" translate="label comment" type="text" sortOrder="20" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Cron Schedule</label>
<comment>Cron expression (default: every 2 hours)</comment>
<depends>
<field id="cron_enabled">1</field>
</depends>
</field>
<field id="timeout" translate="label comment" type="text" sortOrder="30" showInDefault="1" showInWebsite="0" showInStore="0">
<label>API Timeout (seconds)</label>
<comment>Maximum time to wait for API response</comment>
<validate>validate-number validate-greater-than-zero</validate>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
</group>
<group id="advanced" translate="label" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Advanced Settings</label>
<field id="apply_to_categories" translate="label comment" type="multiselect" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Apply to Category Pages</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<comment>Enable ranking on category listing pages</comment>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="apply_to_search" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Apply to Search Results</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="fallback_sorting" translate="label comment" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Fallback Sorting</label>
<comment>Sorting method when BEYABLE ranking is unavailable</comment>
<source_model>Magento\Catalog\Model\Config\Source\ListSort</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
<field id="debug_logging" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Debug Logging</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
<depends>
<field id="beyable_ranking/general/enabled">1</field>
</depends>
</field>
</group>
</section>
</system>
</config>

Step 2: Default Configuration (etc/config.xml)

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<beyable_ranking>
<general>
<enabled>0</enabled>
<api_base_url>https://api.beyable.com</api_base_url>
</general>
<ranking_rules>
<use_segmentation>0</use_segmentation>
<segment_cookie_name>b_segment</segment_cookie_name>
</ranking_rules>
<sync>
<cron_enabled>1</cron_enabled>
<cron_schedule>0 */2 * * *</cron_schedule>
<timeout>30</timeout>
</sync>
<advanced>
<apply_to_categories>1</apply_to_categories>
<apply_to_search>1</apply_to_search>
<fallback_sorting>position</fallback_sorting>
<debug_logging>0</debug_logging>
</advanced>
</beyable_ranking>
</default>
</config>

Implementation Steps

Step 3: Database Schema (Setup/InstallSchema.php)

<?php
namespace Beyable\ProductRanking\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;

class InstallSchema implements InstallSchemaInterface
{
public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
{
$setup->startSetup();

$table = $setup->getConnection()->newTable(
$setup->getTable('beyable_product_ranking')
)->addColumn(
'ranking_id',
Table::TYPE_INTEGER,
null,
['identity' => true, 'nullable' => false, 'primary' => true, 'unsigned' => true],
'Ranking ID'
)->addColumn(
'account_id',
Table::TYPE_TEXT,
32,
['nullable' => false],
'BEYABLE Account ID'
)->addColumn(
'rule_id',
Table::TYPE_TEXT,
32,
['nullable' => false],
'Ranking Rule ID'
)->addColumn(
'tenant_id',
Table::TYPE_TEXT,
100,
['nullable' => true],
'Tenant ID'
)->addColumn(
'segment_id',
Table::TYPE_TEXT,
100,
['nullable' => true],
'Segment ID'
)->addColumn(
'product_id',
Table::TYPE_INTEGER,
null,
['nullable' => false, 'unsigned' => true],
'Product ID'
)->addColumn(
'sku',
Table::TYPE_TEXT,
64,
['nullable' => false],
'Product SKU'
)->addColumn(
'position',
Table::TYPE_INTEGER,
null,
['nullable' => false, 'unsigned' => true],
'Ranking Position'
)->addColumn(
'ab_test_group',
Table::TYPE_TEXT,
50,
['nullable' => true],
'A/B Test Group'
)->addColumn(
'store_id',
Table::TYPE_SMALLINT,
null,
['nullable' => false, 'unsigned' => true],
'Store ID'
)->addColumn(
'updated_at',
Table::TYPE_TIMESTAMP,
null,
['nullable' => false, 'default' => Table::TIMESTAMP_INIT_UPDATE],
'Updated At'
)->addColumn(
'synced_at',
Table::TYPE_TIMESTAMP,
null,
['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
'Synced At'
)->addIndex(
$setup->getIdxName('beyable_product_ranking', ['product_id']),
['product_id']
)->addIndex(
$setup->getIdxName('beyable_product_ranking', ['sku']),
['sku']
)->addIndex(
$setup->getIdxName(
'beyable_product_ranking',
['account_id', 'rule_id', 'tenant_id', 'segment_id', 'store_id']
),
['account_id', 'rule_id', 'tenant_id', 'segment_id', 'store_id']
)->addIndex(
$setup->getIdxName('beyable_product_ranking', ['position']),
['position']
)->addForeignKey(
$setup->getFkName('beyable_product_ranking', 'product_id', 'catalog_product_entity', 'entity_id'),
'product_id',
$setup->getTable('catalog_product_entity'),
'entity_id',
Table::ACTION_CASCADE
)->addForeignKey(
$setup->getFkName('beyable_product_ranking', 'store_id', 'store', 'store_id'),
'store_id',
$setup->getTable('store'),
'store_id',
Table::ACTION_CASCADE
)->setComment(
'BEYABLE Product Ranking Table'
);

$setup->getConnection()->createTable($table);

$setup->endSetup();
}
}

Step 4: API Client (Model/Api/Client.php)

<?php
namespace Beyable\ProductRanking\Model\Api;

use Magento\Framework\HTTP\Client\Curl;
use Beyable\ProductRanking\Model\Config;
use Psr\Log\LoggerInterface;

class Client
{
const API_ENDPOINT_STANDARD = '/productranking/%s/%s/%s';
const API_ENDPOINT_SEGMENTED = '/segmented-productranking/%s/%s/%s/%s';

/**
* @var Curl
*/
protected $curl;

/**
* @var Config
*/
protected $config;

/**
* @var LoggerInterface
*/
protected $logger;

/**
* @param Curl $curl
* @param Config $config
* @param LoggerInterface $logger
*/
public function __construct(
Curl $curl,
Config $config,
LoggerInterface $logger
) {
$this->curl = $curl;
$this->config = $config;
$this->logger = $logger;
}

/**
* Fetch rankings from BEYABLE API
*
* @param string $ruleId
* @param string|null $tenantId
* @param string|null $segmentId
* @param int|null $storeId
* @return array
* @throws \Exception
*/
public function fetchRankings($ruleId, $tenantId = null, $segmentId = null, $storeId = null)
{
if (!$this->config->isEnabled($storeId)) {
throw new \Exception('BEYABLE Product Ranking is not enabled');
}

$accountId = $this->config->getAccountId($storeId);
$subscriptionKey = $this->config->getSubscriptionKey($storeId);
$baseUrl = $this->config->getApiBaseUrl($storeId);

if (empty($accountId) || empty($subscriptionKey)) {
throw new \Exception('BEYABLE API credentials are not configured');
}

// Build URL
if ($segmentId && $tenantId) {
$endpoint = sprintf(self::API_ENDPOINT_SEGMENTED, $accountId, $ruleId, $segmentId, $tenantId);
} elseif ($segmentId) {
$endpoint = sprintf('/segmented-productranking/%s/%s/%s', $accountId, $ruleId, $segmentId);
} elseif ($tenantId) {
$endpoint = sprintf(self::API_ENDPOINT_STANDARD, $accountId, $ruleId, $tenantId);
} else {
$endpoint = sprintf('/productranking/%s/%s', $accountId, $ruleId);
}

$url = rtrim($baseUrl, '/') . $endpoint;

// Configure request
$this->curl->setOption(CURLOPT_TIMEOUT, $this->config->getTimeout($storeId));
$this->curl->addHeader('Subscription-Key', $subscriptionKey);
$this->curl->addHeader('Accept', 'text/csv');

// Log request if debug is enabled
if ($this->config->isDebugLoggingEnabled($storeId)) {
$this->logger->debug('BEYABLE API Request', [
'url' => $url,
'rule_id' => $ruleId,
'tenant_id' => $tenantId,
'segment_id' => $segmentId
]);
}

try {
// Make API call
$this->curl->get($url);

$statusCode = $this->curl->getStatus();
$response = $this->curl->getBody();

if ($statusCode !== 200) {
throw new \Exception(sprintf(
'BEYABLE API returned status %d: %s',
$statusCode,
$response
));
}

// Parse CSV
$rankings = $this->parseCsvResponse($response);

if ($this->config->isDebugLoggingEnabled($storeId)) {
$this->logger->debug('BEYABLE API Response', [
'rankings_count' => count($rankings)
]);
}

return $rankings;

} catch (\Exception $e) {
$this->logger->error('BEYABLE API Error: ' . $e->getMessage(), [
'url' => $url,
'exception' => $e
]);
throw $e;
}
}

/**
* Parse CSV response
*
* @param string $csvContent
* @return array
*/
protected function parseCsvResponse($csvContent)
{
$rankings = [];
$lines = str_getcsv($csvContent, "\n");

// Skip header row
array_shift($lines);

foreach ($lines as $line) {
if (empty(trim($line))) {
continue;
}

$data = str_getcsv($line);

if (count($data) >= 3) {
$rankings[] = [
'product_id' => $data[0],
'ab_test_group' => !empty($data[1]) ? $data[1] : null,
'position' => (int)$data[2]
];
}
}

return $rankings;
}
}

Step 5: Configuration Helper (Model/Config.php)

<?php
namespace Beyable\ProductRanking\Model;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;

class Config
{
const XML_PATH_ENABLED = 'beyable_ranking/general/enabled';
const XML_PATH_ACCOUNT_ID = 'beyable_ranking/general/account_id';
const XML_PATH_SUBSCRIPTION_KEY = 'beyable_ranking/general/subscription_key';
const XML_PATH_API_BASE_URL = 'beyable_ranking/general/api_base_url';
const XML_PATH_DEFAULT_RANKING_ID = 'beyable_ranking/ranking_rules/default_ranking_id';
const XML_PATH_SEGMENTED_RANKING_ID = 'beyable_ranking/ranking_rules/segmented_ranking_id';
const XML_PATH_USE_SEGMENTATION = 'beyable_ranking/ranking_rules/use_segmentation';
const XML_PATH_SEGMENT_COOKIE_NAME = 'beyable_ranking/ranking_rules/segment_cookie_name';
const XML_PATH_CRON_ENABLED = 'beyable_ranking/sync/cron_enabled';
const XML_PATH_TIMEOUT = 'beyable_ranking/sync/timeout';
const XML_PATH_APPLY_TO_CATEGORIES = 'beyable_ranking/advanced/apply_to_categories';
const XML_PATH_APPLY_TO_SEARCH = 'beyable_ranking/advanced/apply_to_search';
const XML_PATH_DEBUG_LOGGING = 'beyable_ranking/advanced/debug_logging';

/**
* @var ScopeConfigInterface
*/
protected $scopeConfig;

/**
* @param ScopeConfigInterface $scopeConfig
*/
public function __construct(ScopeConfigInterface $scopeConfig)
{
$this->scopeConfig = $scopeConfig;
}

/**
* Check if module is enabled
*
* @param int|null $storeId
* @return bool
*/
public function isEnabled($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_ENABLED,
ScopeInterface::SCOPE_STORE,
$storeId
);
}

/**
* Get account ID
*
* @param int|null $storeId
* @return string
*/
public function getAccountId($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_ACCOUNT_ID,
ScopeInterface::SCOPE_STORE,
$storeId
);
}

/**
* Get subscription key
*
* @param int|null $storeId
* @return string
*/
public function getSubscriptionKey($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_SUBSCRIPTION_KEY,
ScopeInterface::SCOPE_STORE,
$storeId
);
}

/**
* Get API base URL
*
* @param int|null $storeId
* @return string
*/
public function getApiBaseUrl($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_API_BASE_URL,
ScopeInterface::SCOPE_STORE,
$storeId
);
}

/**
* Get default ranking ID
*
* @param int|null $storeId
* @return string
*/
public function getDefaultRankingId($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_DEFAULT_RANKING_ID,
ScopeInterface::SCOPE_STORE,
$storeId
);
}

/**
* Check if segmentation is enabled
*
* @param int|null $storeId
* @return bool
*/
public function isSegmentationEnabled($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_USE_SEGMENTATION,
ScopeInterface::SCOPE_STORE,
$storeId
);
}

/**
* Get segment cookie name
*
* @param int|null $storeId
* @return string
*/
public function getSegmentCookieName($storeId = null)
{
return $this->scopeConfig->getValue(
self::XML_PATH_SEGMENT_COOKIE_NAME,
ScopeInterface::SCOPE_STORE,
$storeId
) ?: 'b_segment';
}

/**
* Get API timeout in seconds
*
* @param int|null $storeId
* @return int
*/
public function getTimeout($storeId = null)
{
return (int)$this->scopeConfig->getValue(
self::XML_PATH_TIMEOUT,
ScopeInterface::SCOPE_STORE,
$storeId
) ?: 30;
}

/**
* Check if debug logging is enabled
*
* @param int|null $storeId
* @return bool
*/
public function isDebugLoggingEnabled($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_DEBUG_LOGGING,
ScopeInterface::SCOPE_STORE,
$storeId
);
}

/**
* Check if ranking should be applied to categories
*
* @param int|null $storeId
* @return bool
*/
public function shouldApplyToCategories($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_APPLY_TO_CATEGORIES,
ScopeInterface::SCOPE_STORE,
$storeId
);
}

/**
* Check if ranking should be applied to search
*
* @param int|null $storeId
* @return bool
*/
public function shouldApplyToSearch($storeId = null)
{
return $this->scopeConfig->isSetFlag(
self::XML_PATH_APPLY_TO_SEARCH,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
}

Step 6: Cron Job Configuration (etc/crontab.xml)

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="default">
<job name="beyable_sync_rankings" instance="Beyable\ProductRanking\Cron\SyncRankings" method="execute">
<config_path>beyable_ranking/sync/cron_schedule</config_path>
</job>
</group>
</config>

Step 7: Cron Job Implementation (Cron/SyncRankings.php)

<?php
namespace Beyable\ProductRanking\Cron;

use Beyable\ProductRanking\Model\Config;
use Beyable\ProductRanking\Model\RankingManagement;
use Magento\Store\Model\StoreManagerInterface;
use Psr\Log\LoggerInterface;

class SyncRankings
{
/**
* @var Config
*/
protected $config;

/**
* @var RankingManagement
*/
protected $rankingManagement;

/**
* @var StoreManagerInterface
*/
protected $storeManager;

/**
* @var LoggerInterface
*/
protected $logger;

/**
* @param Config $config
* @param RankingManagement $rankingManagement
* @param StoreManagerInterface $storeManager
* @param LoggerInterface $logger
*/
public function __construct(
Config $config,
RankingManagement $rankingManagement,
StoreManagerInterface $storeManager,
LoggerInterface $logger
) {
$this->config = $config;
$this->rankingManagement = $rankingManagement;
$this->storeManager = $storeManager;
$this->logger = $logger;
}

/**
* Execute cron job
*
* @return void
*/
public function execute()
{
$this->logger->info('BEYABLE: Starting ranking synchronization');

$stores = $this->storeManager->getStores();

foreach ($stores as $store) {
$storeId = $store->getId();

if (!$this->config->isEnabled($storeId)) {
continue;
}

try {
$this->logger->info(sprintf('BEYABLE: Syncing rankings for store %s', $store->getCode()));

// Sync default rankings
$this->rankingManagement->syncRankingsForStore($storeId);

$this->logger->info(sprintf('BEYABLE: Successfully synced rankings for store %s', $store->getCode()));

} catch (\Exception $e) {
$this->logger->error(sprintf(
'BEYABLE: Error syncing rankings for store %s: %s',
$store->getCode(),
$e->getMessage()
));
}
}

$this->logger->info('BEYABLE: Ranking synchronization completed');
}
}

Step 8: Product Collection Plugin (Plugin/Catalog/Model/ResourceModel/Product/Collection.php)

<?php
namespace Beyable\ProductRanking\Plugin\Catalog\Model\ResourceModel\Product;

use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
use Beyable\ProductRanking\Model\Config;
use Beyable\ProductRanking\Helper\Data as RankingHelper;
use Magento\Framework\App\RequestInterface;

class Collection
{
/**
* @var Config
*/
protected $config;

/**
* @var RankingHelper
*/
protected $rankingHelper;

/**
* @var RequestInterface
*/
protected $request;

/**
* @param Config $config
* @param RankingHelper $rankingHelper
* @param RequestInterface $request
*/
public function __construct(
Config $config,
RankingHelper $rankingHelper,
RequestInterface $request
) {
$this->config = $config;
$this->rankingHelper = $rankingHelper;
$this->request = $request;
}

/**
* Apply BEYABLE ranking to product collection
*
* @param ProductCollection $subject
* @param bool $printQuery
* @param bool $logQuery
* @return array
*/
public function beforeLoad(ProductCollection $subject, $printQuery = false, $logQuery = false)
{
if ($subject->isLoaded()) {
return [$printQuery, $logQuery];
}

$storeId = $subject->getStoreId();

if (!$this->config->isEnabled($storeId)) {
return [$printQuery, $logQuery];
}

// Check if we should apply ranking based on context
$fullActionName = $this->request->getFullActionName();
$shouldApply = false;

if (strpos($fullActionName, 'catalog_category_view') !== false && $this->config->shouldApplyToCategories($storeId)) {
$shouldApply = true;
} elseif (strpos($fullActionName, 'catalogsearch_result') !== false && $this->config->shouldApplyToSearch($storeId)) {
$shouldApply = true;
}

if (!$shouldApply) {
return [$printQuery, $logQuery];
}

// Get visitor segment if segmentation is enabled
$segmentId = null;
if ($this->config->isSegmentationEnabled($storeId)) {
$segmentId = $this->rankingHelper->getVisitorSegment();
}

// Join ranking table
$this->applyRankingJoin($subject, $storeId, $segmentId);

return [$printQuery, $logQuery];
}

/**
* Apply ranking join to collection
*
* @param ProductCollection $collection
* @param int $storeId
* @param string|null $segmentId
* @return void
*/
protected function applyRankingJoin(ProductCollection $collection, $storeId, $segmentId = null)
{
$select = $collection->getSelect();

$rankingTable = $collection->getTable('beyable_product_ranking');

// Build join conditions
$conditions = [
'e.entity_id = beyable_ranking.product_id',
$select->getAdapter()->quoteInto('beyable_ranking.store_id = ?', $storeId)
];

if ($segmentId) {
$conditions[] = $select->getAdapter()->quoteInto('beyable_ranking.segment_id = ?', $segmentId);
} else {
$conditions[] = 'beyable_ranking.segment_id IS NULL';
}

// Join ranking table
$select->joinLeft(
['beyable_ranking' => $rankingTable],
implode(' AND ', $conditions),
['beyable_position' => 'position', 'beyable_ab_test_group' => 'ab_test_group']
);

// Order by ranking position, fallback to default
$select->order([
'beyable_ranking.position ASC',
'e.entity_id DESC'
]);
}
}

Step 9: Helper for Segment Detection (Helper/Data.php)

<?php
namespace Beyable\ProductRanking\Helper;

use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Helper\Context;
use Magento\Framework\Stdlib\CookieManagerInterface;
use Beyable\ProductRanking\Model\Config;

class Data extends AbstractHelper
{
/**
* @var CookieManagerInterface
*/
protected $cookieManager;

/**
* @var Config
*/
protected $config;

/**
* @param Context $context
* @param CookieManagerInterface $cookieManager
* @param Config $config
*/
public function __construct(
Context $context,
CookieManagerInterface $cookieManager,
Config $config
) {
parent::__construct($context);
$this->cookieManager = $cookieManager;
$this->config = $config;
}

/**
* Get visitor segment from cookie
*
* @return string|null
*/
public function getVisitorSegment()
{
$cookieName = $this->config->getSegmentCookieName();
return $this->cookieManager->getCookie($cookieName);
}

/**
* Get tenant ID for current store
*
* @param int|null $storeId
* @return string
*/
public function getTenantId($storeId = null)
{
// You can customize this logic based on your store structure
// For example, use store code or locale
$storeManager = \Magento\Framework\App\ObjectManager::getInstance()
->get(\Magento\Store\Model\StoreManagerInterface::class);

$store = $storeId ? $storeManager->getStore($storeId) : $storeManager->getStore();

return $store->getCode();
}
}

Testing

Manual Testing

  1. Configuration Test:

    php bin/magento config:set beyable_ranking/general/enabled 1
    php bin/magento config:set beyable_ranking/general/account_id YOUR_ACCOUNT_ID
    php bin/magento config:set beyable_ranking/general/subscription_key YOUR_KEY
    php bin/magento cache:flush
  2. Manual Sync Test:

    php bin/magento beyable:sync-rankings
  3. Verify Database:

    SELECT * FROM beyable_product_ranking LIMIT 10;
  4. Frontend Test:

    • Visit a category page
    • Check if products are ordered according to BEYABLE rankings
    • Inspect browser console for any JavaScript errors

Automated Testing

Create a test file: Test/Unit/Model/Api/ClientTest.php

<?php
namespace Beyable\ProductRanking\Test\Unit\Model\Api;

use PHPUnit\Framework\TestCase;
use Beyable\ProductRanking\Model\Api\Client;

class ClientTest extends TestCase
{
protected $client;

protected function setUp(): void
{
// Setup test dependencies
}

public function testParseCsvResponse()
{
$csvContent = '"ProductId","abTestGroup","Position"
"123","",1
"456","",2';

// Test CSV parsing
$this->assertCount(2, $result);
}
}

Deployment

Production Deployment Checklist

  1. Pre-deployment:

    # Backup database
    php bin/magento setup:backup --db

    # Enable maintenance mode
    php bin/magento maintenance:enable
  2. Deploy module:

    # Copy module files
    # Run setup
    php bin/magento setup:upgrade
    php bin/magento setup:di:compile
    php bin/magento setup:static-content:deploy
    php bin/magento cache:flush
  3. Configure via Admin:

    • Navigate to: Stores > Configuration > BEYABLE > Product Ranking
    • Enter your credentials
    • Configure ranking rules
    • Save configuration
  4. Initial Sync:

    php bin/magento beyable:sync-rankings
  5. Verify:

    • Check logs: var/log/system.log
    • Verify rankings in database
    • Test on frontend
  6. Post-deployment:

    # Disable maintenance mode
    php bin/magento maintenance:disable

Troubleshooting

Common Issues

1. API Authentication Errors

Symptom: HTTP 401 errors in logs

Solution:

  • Verify Subscription-Key is correct
  • Check if key is properly encrypted in database
  • Test API manually with curl:
    curl -H "Subscription-Key: YOUR_KEY" \
    "https://api.beyable.com/productranking/ACCOUNT_ID/RANKING_ID/TENANT"

2. No Rankings Applied

Symptom: Products not ordered by BEYABLE ranking

Solution:

  • Check if module is enabled: php bin/magento config:show beyable_ranking/general/enabled
  • Verify rankings exist in database: SELECT COUNT(*) FROM beyable_product_ranking;
  • Check if cron is running: SELECT * FROM cron_schedule WHERE job_code = 'beyable_sync_rankings';
  • Enable debug logging and check logs

3. Cron Not Running

Symptom: Rankings not updating

Solution:

# Check cron status
php bin/magento cron:run

# Verify cron schedule
SELECT * FROM cron_schedule WHERE job_code = 'beyable_sync_rankings' ORDER BY scheduled_at DESC LIMIT 5;

# Manually trigger sync
php bin/magento beyable:sync-rankings

4. Performance Issues

Symptom: Slow category/search pages

Solution:

  • Add indexes to ranking table
  • Implement Redis cache for rankings
  • Reduce sync frequency
  • Use flat catalog

5. Segment Not Detected

Symptom: Segmented rankings not working

Solution:

  • Verify cookie name matches configuration
  • Check browser console for cookie value
  • Test with manual cookie: document.cookie = "b_segment=premium";
  • Verify BEYABLE tracking script is loaded

Debug Commands

# Enable debug mode
php bin/magento config:set beyable_ranking/advanced/debug_logging 1

# Check configuration
php bin/magento config:show beyable_ranking

# View recent logs
tail -f var/log/system.log | grep BEYABLE

# Check database
mysql -u USER -p DATABASE_NAME -e "SELECT product_id, sku, position, segment_id FROM beyable_product_ranking ORDER BY position LIMIT 20;"

# Test API connection
php bin/magento beyable:test-connection

Performance Optimization

1. Database Optimization

-- Add composite index for faster lookups
CREATE INDEX idx_beyable_ranking_lookup
ON beyable_product_ranking(store_id, segment_id, position);

-- Add covering index
CREATE INDEX idx_beyable_ranking_covering
ON beyable_product_ranking(store_id, segment_id, product_id, position);

2. Caching Strategy

Implement Redis cache in Model/RankingManagement.php:

protected function getCachedRankings($storeId, $segmentId = null)
{
$cacheKey = sprintf('beyable_rankings_%d_%s', $storeId, $segmentId ?: 'default');

if ($cached = $this->cache->load($cacheKey)) {
return unserialize($cached);
}

// Fetch from database
$rankings = $this->fetchFromDatabase($storeId, $segmentId);

// Cache for 2 hours
$this->cache->save(serialize($rankings), $cacheKey, [], 7200);

return $rankings;
}

3. Async Processing

Use message queue for large catalogs:

<!-- etc/queue.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/queue.xsd">
<broker topic="beyable.ranking.sync" exchange="beyable" type="db">
<queue name="beyable.ranking.sync" consumer="beyableRankingSync" handler="Beyable\ProductRanking\Model\Queue\Consumer\RankingSync::process"/>
</broker>
</config>

4. Varnish Configuration

Add to VCL:

# Allow BEYABLE segment cookie to pass through
if (req.http.cookie ~ "b_segment") {
set req.http.X-Beyable-Segment = regsub(req.http.cookie, "^.*?b_segment=([^;]+);*.*$", "\1");
hash_data(req.http.X-Beyable-Segment);
}

Support

For additional assistance:

  • Module Issues: Check var/log/system.log and var/log/exception.log
  • BEYABLE API Issues: Contact your BEYABLE account manager
  • Magento Issues: Refer to Magento 2 documentation

Appendix

A. Console Commands Reference

Create Console/Command/TestConnection.php:

<?php
namespace Beyable\ProductRanking\Console\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Beyable\ProductRanking\Model\Api\Client;
use Beyable\ProductRanking\Model\Config;

class TestConnection extends Command
{
protected $client;
protected $config;

public function __construct(
Client $client,
Config $config,
string $name = null
) {
parent::__construct($name);
$this->client = $client;
$this->config = $config;
}

protected function configure()
{
$this->setName('beyable:test-connection')
->setDescription('Test BEYABLE API connection');
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('<info>Testing BEYABLE API connection...</info>');

try {
$ruleId = $this->config->getDefaultRankingId();
$rankings = $this->client->fetchRankings($ruleId);

$output->writeln(sprintf('<info>Success! Retrieved %d rankings</info>', count($rankings)));
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln('<error>Error: ' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
}
}

Register in etc/di.xml:

<type name="Magento\Framework\Console\CommandList">
<arguments>
<argument name="commands" xsi:type="array">
<item name="beyableTestConnection" xsi:type="object">Beyable\ProductRanking\Console\Command\TestConnection</item>
<item name="beyableSyncRankings" xsi:type="object">Beyable\ProductRanking\Console\Command\SyncRankings</item>
</argument>
</arguments>
</type>

B. API Endpoints Quick Reference

EndpointPurposeSegmentation
/productranking/{accountId}/{rankingId}Default tenant, no segmentationNo
/productranking/{accountId}/{rankingId}/{tenant}Specific tenant, no segmentationNo
/segmented-productranking/{accountId}/{rankingId}/{segmentId}Default tenant, with segmentationYes
/segmented-productranking/{accountId}/{rankingId}/{segmentId}/{tenant}Specific tenant, with segmentationYes