diff --git a/mauticconnect.php b/mauticconnect.php index 3d19340..40d6fe0 100644 --- a/mauticconnect.php +++ b/mauticconnect.php @@ -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 = ''; + + // 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')); diff --git a/views/templates/mail/json_ld_data.tpl b/views/templates/mail/json_ld_data.tpl new file mode 100644 index 0000000..4e5cdc8 --- /dev/null +++ b/views/templates/mail/json_ld_data.tpl @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/views/templates/mail/product_list_table.tpl b/views/templates/mail/product_list_table.tpl new file mode 100644 index 0000000..e0e1df6 --- /dev/null +++ b/views/templates/mail/product_list_table.tpl @@ -0,0 +1,12 @@ + + {foreach from=$products item=product} + + + + + + {/foreach} +
{$product.product_name}{$product.product_name}
{$product.product_quantity} x + {$product.unit_price_tax_incl} {$product.currency_iso_code}
{$product.total_price_tax_incl} {$product.currency_iso_code}
+