abandon cart campaign
This commit is contained in:
@@ -43,6 +43,12 @@ class MauticConnect extends Module
|
||||
'title' => 'Order Arrived Event',
|
||||
'processor_method' => 'processOrderArrivedEvent',
|
||||
],
|
||||
|
||||
[
|
||||
'id' => 'cart_abandon',
|
||||
'title' => 'Abandon Cart Event',
|
||||
'processor_method' => 'processAbandonCartEvent',
|
||||
],
|
||||
// Example: To add a "Refunded" event, just uncomment the next block.
|
||||
/*
|
||||
[
|
||||
@@ -139,22 +145,15 @@ class MauticConnect extends Module
|
||||
public function getContent()
|
||||
{
|
||||
$output = '';
|
||||
|
||||
$mauticUrl = Tools::getValue(self::MAUTIC_URL);
|
||||
$clientId = Tools::getValue(self::MAUTIC_CLIENT_ID);
|
||||
$clientSecret = Tools::getValue(self::MAUTIC_CLIENT_SECRET);
|
||||
if (Tools::isSubmit('submit' . $this->name)) {
|
||||
$mauticUrl = Tools::getValue(self::MAUTIC_URL);
|
||||
$clientId = Tools::getValue(self::MAUTIC_CLIENT_ID);
|
||||
$clientSecret = Tools::getValue(self::MAUTIC_CLIENT_SECRET);
|
||||
|
||||
$output .= $this->postProcess();
|
||||
}
|
||||
|
||||
if ($mauticUrl && $clientId && $clientSecret) {
|
||||
Configuration::updateValue(self::MAUTIC_URL, rtrim($mauticUrl, '/'));
|
||||
Configuration::updateValue(self::MAUTIC_CLIENT_ID, $clientId);
|
||||
Configuration::updateValue(self::MAUTIC_CLIENT_SECRET, $clientSecret);
|
||||
$output .= $this->displayConfirmation($this->l('Settings updated. Please connect to Mautic if you haven\'t already.'));
|
||||
} else {
|
||||
$output .= $this->displayError($this->l('Mautic URL, Client ID, and Client Secret are required.'));
|
||||
}
|
||||
|
||||
$output .= $this->displayConnectionStatus();
|
||||
$output .= $this->renderForms(); // Single method to render all forms
|
||||
|
||||
@@ -176,6 +175,16 @@ class MauticConnect extends Module
|
||||
Configuration::updateValue(self::MAUTIC_REFRESH_TOKEN, '');
|
||||
Configuration::updateValue(self::MAUTIC_TOKEN_EXPIRES, 0);
|
||||
}
|
||||
$mauticUrl = Tools::getValue(self::MAUTIC_URL);
|
||||
$clientId = Tools::getValue(self::MAUTIC_CLIENT_ID);
|
||||
$clientSecret = Tools::getValue(self::MAUTIC_CLIENT_SECRET);
|
||||
if ($mauticUrl && $clientId && $clientSecret) {
|
||||
Configuration::updateValue(self::MAUTIC_URL, rtrim($mauticUrl, '/'));
|
||||
Configuration::updateValue(self::MAUTIC_CLIENT_ID, $clientId);
|
||||
Configuration::updateValue(self::MAUTIC_CLIENT_SECRET, $clientSecret);
|
||||
} else {
|
||||
return $this->displayError($this->l('Mautic URL, Client ID, and Client Secret are required.'));
|
||||
}
|
||||
// Dynamically save event mapping settings
|
||||
if ($this->isConnected()) {
|
||||
foreach (self::$eventDefinitions as $event) {
|
||||
@@ -334,8 +343,45 @@ class MauticConnect extends Module
|
||||
}
|
||||
}
|
||||
}
|
||||
public function runAbandonCartCampaign()
|
||||
{
|
||||
$cartCollection = new PrestaShopCollection('Cart');
|
||||
$cartCollection->where('id_customer', '!=', 0);
|
||||
$cartCollection->where('date_add', '>', date('Y-m-d', time() - 60 * 60 * 24 * 1));
|
||||
$cartCollection->where('date_add', '<', date('Y-m-d'));
|
||||
//@var Cart $cart
|
||||
foreach ($cartCollection as $cart) {
|
||||
|
||||
if (!Order::getIdByCartId($cart->id)) {
|
||||
$this->processAbandonCart($cart->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function processAbandonCart(int $id_cart)
|
||||
{
|
||||
if (!$this->isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$eventHash = md5('abandon_cart' . '_' . $id_cart);
|
||||
if ($this->isAlreadyProcessed($eventHash)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Loop through our defined events to see if any match the new status
|
||||
foreach (self::$eventDefinitions as $event) {
|
||||
if ($event['id'] === 'cart_abandon') {
|
||||
// ...call the processor method defined for this event.
|
||||
if (method_exists($this, $event['processor_method'])) {
|
||||
$this->{$event['processor_method']}($id_cart, $event);
|
||||
$this->markAsProcessed($eventHash);
|
||||
// We break because an order status change should only trigger one event.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ... displayConnectionStatus, getMauticAuthUrl, getOauth2RedirectUri ...
|
||||
// ... makeApiRequest, refreshTokenIfNeeded ...
|
||||
@@ -354,7 +400,7 @@ class MauticConnect extends Module
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function getMauticSegments(): array
|
||||
public function getMauticSegments(): array
|
||||
{
|
||||
$response = $this->makeApiRequest('/api/segments');
|
||||
$segments = $response['lists'] ?? [];
|
||||
@@ -664,7 +710,7 @@ class MauticConnect extends Module
|
||||
*/
|
||||
public function syncCustomer(Customer $customer)
|
||||
{
|
||||
if (!$this->isConnected() || !Validate::isLoadedObject($customer) || strpos($customer->email, '@' . Tools::getShopDomainSsl())) {
|
||||
if (!$this->isConnected() || !Validate::isLoadedObject($customer) || strpos($customer->email, '@' . Tools::getShopDomainSsl()) || $customer->email == 'anonymous@psgdpr.com') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -920,6 +966,138 @@ class MauticConnect extends Module
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
public function processAbandonCartEvent(int $id_cart, array $eventDefinition)
|
||||
{
|
||||
|
||||
$mauticSegmentId = (int)Configuration::get($this->getEventConfigKey($eventDefinition['id'], 'mautic_segment'));
|
||||
$mauticTemplateId = (int)Configuration::get($this->getEventConfigKey($eventDefinition['id'], 'mautic_template'));
|
||||
$smarty = new Smarty();
|
||||
// Do nothing if this event is not fully configured
|
||||
if (!$mauticSegmentId || !$mauticTemplateId) {
|
||||
return;
|
||||
}
|
||||
// 2. Get all necessary objects
|
||||
$cart = new Cart($id_cart);
|
||||
if (!$cart->id_customer) {
|
||||
return;
|
||||
}
|
||||
$customer = new Customer((int)$cart->id_customer);
|
||||
|
||||
$currency = new Currency((int)$cart->id_currency);
|
||||
$link = new Link(); // Needed for generating image URLs
|
||||
|
||||
// 3. Gather primary data
|
||||
$customer_email = $customer->email;
|
||||
if (!$this->isContactInSegment($customer_email, $mauticSegmentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$action_url = $link->getPageLink('cart', true, null, [
|
||||
'action' => 'show',
|
||||
|
||||
]);
|
||||
$products = $cart->getProducts();
|
||||
if (!count($products)) {
|
||||
return;
|
||||
}
|
||||
$abandoned_cart_items_for_json = [];
|
||||
$abandoned_cart_items_for_html = [];
|
||||
foreach ($products as $product) {
|
||||
|
||||
$product_obj = new Product($product['id_product'], false, $this->context->language->id);
|
||||
$product_url = $product_obj->getLink();
|
||||
$cover_img = Product::getCover($product_obj->id);
|
||||
$image_url = $link->getImageLink($product_obj->link_rewrite, $cover_img['id_image'], 'cart_default');
|
||||
|
||||
$abandoned_cart_items_for_html[] = [
|
||||
'image_url' => $image_url,
|
||||
'product_name' => $product['name'],
|
||||
'product_quantity' => $product['cart_quantity'],
|
||||
'product_url' => $product_url,
|
||||
'unit_price_tax_incl' => round($product['price_with_reduction'], 2),
|
||||
'total_price_tax_incl' => round($product['price_with_reduction'] * $product['cart_quantity'], 2),
|
||||
|
||||
'currency_iso_code' => $currency->iso_code,
|
||||
];
|
||||
$abandoned_cart_items_for_json[] = [
|
||||
"@type" => "Offer",
|
||||
"itemOffered" => [
|
||||
"@type" => "Product",
|
||||
"name" => $product['name'],
|
||||
"sku" => $product['reference'],
|
||||
// Only include 'gtin' if it's consistently available and a valid EAN/UPC/ISBN
|
||||
"gtin" => $product['ean13'],
|
||||
"image" => 'https://' . $image_url, // Ensure this is a full, valid URL
|
||||
"url" => $product_url // Link directly to the product page
|
||||
],
|
||||
"price" => round($product['price_with_reduction'], 2),
|
||||
"priceCurrency" => $currency->iso_code,
|
||||
"itemCondition" => "http://schema.org/NewCondition"
|
||||
];
|
||||
}
|
||||
|
||||
$ldData = [
|
||||
"@context" => "http://schema.org",
|
||||
"@type" => "EmailMessage", // This is an email about an abandoned cart
|
||||
"potentialAction" => [
|
||||
"@type" => "ReserveAction", // Or "BuyAction" if it's a direct purchase flow, "ViewAction" if just to see cart.
|
||||
"name" => "Завершіть Замовлення",
|
||||
"target" => [
|
||||
"@type" => "EntryPoint",
|
||||
"urlTemplate" => $action_url, // The dynamic URL to complete the order
|
||||
"actionPlatform" => [
|
||||
"http://schema.org/DesktopWebPlatform",
|
||||
"http://schema.org/MobileWebPlatform"
|
||||
]
|
||||
]
|
||||
],
|
||||
"about" => [ // What this email is about: the abandoned cart items
|
||||
"@type" => "OfferCatalog", // A collection of offers/products
|
||||
"name" => "Неоформлене замовлення",
|
||||
"description" => "Ви, можливо, забули придбати ці товари на exclusion-ua.shop.",
|
||||
// Optionally, add a general image for the catalog/brand
|
||||
// "image": "https://exclusion-ua.shop/logo.png",
|
||||
"merchant" => [
|
||||
"@type" => "Organization",
|
||||
"name" => "exclusion-ua.shop",
|
||||
"url" => "https://exclusion-ua.shop" // URL of your store
|
||||
],
|
||||
"itemListElement" => $abandoned_cart_items_for_json // The list of products
|
||||
]
|
||||
];
|
||||
|
||||
// Convert the PHP array to a clean JSON string.
|
||||
// Use JSON_UNESCAPED_SLASHES for clean URLs and JSON_PRE
|
||||
|
||||
$abandoned_cart_json_string = '<script type="application/ld+json">' . json_encode($ldData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . '</script>';
|
||||
|
||||
// 5. Prepare the final payload for the Mautic API
|
||||
$smarty->assign([
|
||||
'products' => $abandoned_cart_items_for_html,
|
||||
'json_ld_data' => $ldData,
|
||||
|
||||
]);
|
||||
$data_for_mautic = [
|
||||
'action_url' => $action_url,
|
||||
'html_data' => $smarty->fetch($this->local_path . 'views/templates/mail/product_list_table.tpl'),
|
||||
'json_ld_data' => $smarty->fetch($this->local_path . 'views/templates/mail/json_ld_data.tpl'),
|
||||
];
|
||||
$mauticContactId = $this->getMauticContactIdByEmail($customer_email);
|
||||
|
||||
$endpointUrl = implode('', [
|
||||
'/api/emails/',
|
||||
$mauticTemplateId,
|
||||
'/contact/',
|
||||
$mauticContactId,
|
||||
'/send'
|
||||
]);
|
||||
$response = $this->makeApiRequest($endpointUrl, 'POST', ['tokens' => $data_for_mautic]);
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
public function processOrderShippedEvent(int $id_order, array $eventDefinition)
|
||||
{
|
||||
$mauticSegmentId = (int)Configuration::get($this->getEventConfigKey($eventDefinition['id'], 'mautic_segment'));
|
||||
|
||||
3
views/templates/mail/json_ld_data.tpl
Normal file
3
views/templates/mail/json_ld_data.tpl
Normal file
@@ -0,0 +1,3 @@
|
||||
<script type="application/ld+json">
|
||||
{$json_ld_data|@json_encode nofilter}
|
||||
</script>
|
||||
12
views/templates/mail/product_list_table.tpl
Normal file
12
views/templates/mail/product_list_table.tpl
Normal file
@@ -0,0 +1,12 @@
|
||||
<table width="100%" cellpadding="10" cellspacing="0" style="border-collapse: collapse; margin-top: 20px;">
|
||||
{foreach from=$products item=product}
|
||||
<tr style="border-bottom: 1px solid #eee;">
|
||||
<td width="80"><img src="https://{$product.image_url}" alt="{$product.product_name}" width="70"
|
||||
style="border: 1px solid #ddd;"></td>
|
||||
<td><a href="{$product.product_url}">{$product.product_name}</a><br><small>{$product.product_quantity} x
|
||||
{$product.unit_price_tax_incl} {$product.currency_iso_code}</small></td>
|
||||
<td align="right">{$product.total_price_tax_incl} {$product.currency_iso_code}</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user