abandon cart campaign

This commit is contained in:
O K
2025-09-04 14:57:28 +03:00
parent ee75ace7cd
commit b834851889
3 changed files with 207 additions and 14 deletions

View File

@@ -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 = '';
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);
if (Tools::isSubmit('submit' . $this->name)) {
$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'));

View File

@@ -0,0 +1,3 @@
<script type="application/ld+json">
{$json_ld_data|@json_encode nofilter}
</script>

View 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>