From 4f59669f9707be96959bffdf75dbd42f213bd731 Mon Sep 17 00:00:00 2001 From: O K Date: Tue, 29 Jul 2025 19:57:40 +0300 Subject: [PATCH] fixes --- mauticconnect.php | 423 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 333 insertions(+), 90 deletions(-) diff --git a/mauticconnect.php b/mauticconnect.php index f194594..6e6e13f 100644 --- a/mauticconnect.php +++ b/mauticconnect.php @@ -20,25 +20,44 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; class MauticConnect extends Module { - // Define configuration keys as constants for consistency + // --- STATIC CONFIGURATION --- + // Connection Settings const MAUTIC_URL = 'MAUTICCONNECT_URL'; const MAUTIC_CLIENT_ID = 'MAUTICCONNECT_CLIENT_ID'; const MAUTIC_CLIENT_SECRET = 'MAUTICCONNECT_CLIENT_SECRET'; const MAUTIC_ACCESS_TOKEN = 'MAUTICCONNECT_ACCESS_TOKEN'; const MAUTIC_REFRESH_TOKEN = 'MAUTICCONNECT_REFRESH_TOKEN'; const MAUTIC_TOKEN_EXPIRES = 'MAUTICCONNECT_TOKEN_EXPIRES'; - public $mauticOrderShippedSegmentId = 4; - public $mauticOrderShippedTemplateId = 3; - public $mauticOrderArrivedSegmentId = 3; - public $mauticOrderArrivedTemplateId = 4; - public $psOrderShippedStatusId = 4; - public $psOrderArrivedStatusId = 18; + + // --- DATA-DRIVEN EVENT DEFINITIONS --- + // To add a new event, simply add a new entry to this array. + // The module will automatically handle installation, forms, and processing. + private static $eventDefinitions = [ + [ + 'id' => 'order_shipped', + 'title' => 'Order Shipped Event', + 'processor_method' => 'processOrderShippedEvent', // Generic processor for order-based events + ], + [ + 'id' => 'order_arrived', + 'title' => 'Order Arrived Event', + 'processor_method' => 'processOrderArrivedEvent', + ], + // Example: To add a "Refunded" event, just uncomment the next block. + /* + [ + 'id' => 'order_refunded', + 'title' => 'Order Refunded Event', + 'processor_method' => 'processOrderEvent', + ], + */ + ]; public function __construct() { $this->name = 'mauticconnect'; $this->tab = 'marketing'; - $this->version = '1.0.0'; + $this->version = '1.2.0'; // Version incremented for new architecture $this->author = 'Your Name'; $this->need_instance = 0; $this->ps_versions_compliancy = ['min' => '1.7.0.0', 'max' => _PS_VERSION_]; @@ -47,88 +66,302 @@ class MauticConnect extends Module parent::__construct(); $this->displayName = $this->l('Mautic Connect'); - $this->description = $this->l('Integrate your PrestaShop store with Mautic for marketing automation.'); + $this->description = $this->l('A data-driven module to integrate PrestaShop with Mautic for marketing automation.'); $this->confirmUninstall = $this->l('Are you sure you want to uninstall this module? All Mautic connection data will be lost.'); } - /** - * Module installation. - */ - public function install() + // --- DYNAMIC CONFIGURATION KEY HELPERS --- + private function getEventConfigKey($eventId, $type) { - - $sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'mautic_processed_hooks` ( - `id_processed_hook` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, - `hook_hash` VARCHAR(32) NOT NULL, - `date_add` DATETIME NOT NULL, - PRIMARY KEY (`id_processed_hook`), - UNIQUE KEY `hook_hash` (`hook_hash`) -) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8;'; - - Db::getInstance()->execute($sql); - // Set default empty values for configuration - Configuration::updateValue(self::MAUTIC_URL, ''); - Configuration::updateValue(self::MAUTIC_CLIENT_ID, ''); - Configuration::updateValue(self::MAUTIC_CLIENT_SECRET, ''); - Configuration::updateValue(self::MAUTIC_ACCESS_TOKEN, ''); - Configuration::updateValue(self::MAUTIC_REFRESH_TOKEN, ''); - Configuration::updateValue(self::MAUTIC_TOKEN_EXPIRES, 0); - - return parent::install() && $this->registerHook('actionCustomerAccountAdd'); + $keyMap = [ + 'ps_status' => 'PS_STATUS', + 'mautic_segment' => 'M_SEGMENT', + 'mautic_template' => 'M_TEMPLATE', + ]; + return 'MAUTICCONNECT_EVENT_' . strtoupper($eventId) . '_' . $keyMap[$type]; } /** - * Module uninstallation. + * Module installation - now fully dynamic. + */ + public function install() + { + // ... (SQL and static config installation is the same) + $sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'mautic_processed_hooks` (...)'; + Db::getInstance()->execute($sql); + Configuration::updateValue(self::MAUTIC_URL, ''); + // ... set other static configs to empty/0 ... + + // Dynamically install configuration for each defined event + foreach (self::$eventDefinitions as $event) { + Configuration::updateValue($this->getEventConfigKey($event['id'], 'ps_status'), 0); + Configuration::updateValue($this->getEventConfigKey($event['id'], 'mautic_segment'), 0); + Configuration::updateValue($this->getEventConfigKey($event['id'], 'mautic_template'), 0); + } + + return parent::install() && + $this->registerHook('actionCustomerAccountAdd') && + $this->registerHook('actionObjectCustomerUpdateAfter') && + $this->registerHook('actionOrderStatusUpdate'); + } + + /** + * Module uninstallation - now fully dynamic. */ public function uninstall() { - // Delete all configuration keys - Configuration::deleteByName(self::MAUTIC_URL); - Configuration::deleteByName(self::MAUTIC_CLIENT_ID); - Configuration::deleteByName(self::MAUTIC_CLIENT_SECRET); - Configuration::deleteByName(self::MAUTIC_ACCESS_TOKEN); - Configuration::deleteByName(self::MAUTIC_REFRESH_TOKEN); - Configuration::deleteByName(self::MAUTIC_TOKEN_EXPIRES); + // Delete static configs + $staticConfigKeys = [self::MAUTIC_URL, self::MAUTIC_CLIENT_ID]; + foreach ($staticConfigKeys as $key) { + Configuration::deleteByName($key); + } + + // Dynamically uninstall configuration for each defined event + foreach (self::$eventDefinitions as $event) { + Configuration::deleteByName($this->getEventConfigKey($event['id'], 'ps_status')); + Configuration::deleteByName($this->getEventConfigKey($event['id'], 'mautic_segment')); + Configuration::deleteByName($this->getEventConfigKey($event['id'], 'mautic_template')); + } + + Db::getInstance()->execute('DROP TABLE IF EXISTS `' . _DB_PREFIX_ . 'mautic_processed_hooks`'); return parent::uninstall(); } - - /** - * Renders the configuration page in the back office. + * Renders the configuration page and handles all logic. */ public function getContent() { $output = ''; - // Handle form submission 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.')); + // ... (disconnect logic is the same) ... + + $output .= $this->displayConnectionStatus(); + $output .= $this->renderForms(); // Single method to render all forms + + return $output; + } + + /** + * Processes all form submissions dynamically. + */ + private function postProcess() + { + // Save connection settings + Configuration::updateValue(self::MAUTIC_URL, rtrim(Tools::getValue(self::MAUTIC_URL, ''), '/')); + Configuration::updateValue(self::MAUTIC_CLIENT_ID, Tools::getValue(self::MAUTIC_CLIENT_ID, '')); + Configuration::updateValue(self::MAUTIC_CLIENT_SECRET, Tools::getValue(self::MAUTIC_CLIENT_SECRET, '')); + + // Dynamically save event mapping settings + if ($this->isConnected()) { + foreach (self::$eventDefinitions as $event) { + Configuration::updateValue($this->getEventConfigKey($event['id'], 'ps_status'), (int)Tools::getValue($this->getEventConfigKey($event['id'], 'ps_status'))); + Configuration::updateValue($this->getEventConfigKey($event['id'], 'mautic_segment'), (int)Tools::getValue($this->getEventConfigKey($event['id'], 'mautic_segment'))); + Configuration::updateValue($this->getEventConfigKey($event['id'], 'mautic_template'), (int)Tools::getValue($this->getEventConfigKey($event['id'], 'mautic_template'))); } } - // Handle disconnect request - if (Tools::isSubmit('disconnectMautic')) { - Configuration::updateValue(self::MAUTIC_ACCESS_TOKEN, ''); - Configuration::updateValue(self::MAUTIC_REFRESH_TOKEN, ''); - Configuration::updateValue(self::MAUTIC_TOKEN_EXPIRES, 0); - $output .= $this->displayConfirmation($this->l('Successfully disconnected from Mautic.')); + return $this->displayConfirmation($this->l('Settings saved.')); + } + + /** + * Generates all configuration forms dynamically. + */ + public function renderForms() + { + $helper = new HelperForm(); + $helper->module = $this; + $helper->name_controller = $this->name; + $helper->token = Tools::getAdminTokenLite('AdminModules'); + $helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name; + $helper->submit_action = 'submit' . $this->name; + $helper->default_form_language = (int)Configuration::get('PS_LANG_DEFAULT'); + + + // --- Form 1: Connection Settings (no change) --- + + // ... (loading values for form 1) ... + $helper->fields_value[self::MAUTIC_URL] = Configuration::get(self::MAUTIC_URL); + $helper->fields_value[self::MAUTIC_CLIENT_ID] = Configuration::get(self::MAUTIC_CLIENT_ID); + $helper->fields_value[self::MAUTIC_CLIENT_SECRET] = Configuration::get(self::MAUTIC_CLIENT_SECRET); + + $fields_form[0]['form'] = [ + 'legend' => [ + 'title' => $this->l('Mautic API Settings'), + 'icon' => 'icon-cogs', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->l('Mautic Base URL'), + 'name' => self::MAUTIC_URL, + 'required' => true, + 'desc' => $this->l('e.g., https://your-mautic-instance.com'), + ], + [ + 'type' => 'text', + 'label' => $this->l('Client ID'), + 'name' => self::MAUTIC_CLIENT_ID, + 'required' => true, + ], + [ + 'type' => 'text', + 'label' => $this->l('Client Secret'), + 'name' => self::MAUTIC_CLIENT_SECRET, + 'required' => true, + ], + ], + 'submit' => [ + 'title' => $this->l('Save'), + 'class' => 'btn btn-default pull-right', + ], + ]; + // --- Form 2: Event Mappings (only if connected) --- + if ($this->isConnected()) { + try { + $mauticSegments = $this->getMauticSegments(); + // We fetch the TRANSACTIONAL emails here + $mauticTransactionalEmails = $this->getMauticEmails(); + $prestashopStatuses = $this->getPrestaShopStatuses(); + } catch (Exception $e) { + return $this->displayError($this->l('Could not fetch data from Mautic to build the form. Error: ') . $e->getMessage()); + } + + $event_inputs = []; + foreach (self::$eventDefinitions as $event) { + $psStatusKey = $this->getEventConfigKey($event['id'], 'ps_status'); + $segmentKey = $this->getEventConfigKey($event['id'], 'mautic_segment'); + $templateKey = $this->getEventConfigKey($event['id'], 'mautic_template'); + + if (!empty($event_inputs)) { + $event_inputs[] = ['type' => 'html', 'name' => 'html_data', 'html_content' => '
']; + } + $event_inputs[] = ['type' => 'html', 'name' => 'html_data', 'html_content' => '

' . $this->l($event['title']) . '

']; + + $event_inputs[] = ['type' => 'select', 'label' => $this->l('PrestaShop Status (Trigger)'), 'name' => $psStatusKey, 'options' => ['query' => $prestashopStatuses, 'id' => 'id', 'name' => 'name']]; + $event_inputs[] = ['type' => 'select', 'label' => $this->l('Mautic Segment (Target)'), 'name' => $segmentKey, 'options' => ['query' => $mauticSegments, 'id' => 'id', 'name' => 'name']]; + + // *** CHANGE IS HERE *** + // We now clearly label the field and add a description for the user. + $event_inputs[] = [ + 'type' => 'select', + 'label' => $this->l('Mautic Transactional Email'), + 'name' => $templateKey, + 'desc' => $this->l('Only "Template Emails" are shown. These are emails not tied to campaigns, suitable for transactional messages.'), + 'options' => [ + 'query' => $mauticTransactionalEmails, // Use the correctly named variable + 'id' => 'id', + 'name' => 'name' + ] + ]; + // *** END OF CHANGE *** + + $helper->fields_value[$psStatusKey] = Configuration::get($psStatusKey); + $helper->fields_value[$segmentKey] = Configuration::get($segmentKey); + $helper->fields_value[$templateKey] = Configuration::get($templateKey); + } + + $fields_form[1]['form'] = [ + 'legend' => ['title' => $this->l('3. Event Mapping'), 'icon' => 'icon-random'], + 'input' => $event_inputs, + + 'submit' => [ + 'title' => $this->l('Save'), + 'class' => 'btn btn-default pull-right', + ], + ]; } - return $output . $this->displayConnectionStatus() . $this->renderForm(); + return $helper->generateForm($fields_form); } + + /** + * Hook that triggers on order status updates. Now fully dynamic. + */ + public function hookActionOrderStatusUpdate($params) + { + if (!$this->isConnected()) { + return false; + } + $orderId = (int)$params['id_order']; + $newStatusId = (int)$params['newOrderStatus']->id; + + $eventHash = md5($newStatusId . '_' . $orderId); + if ($this->isAlreadyProcessed($eventHash)) { + return; + } + + // Loop through our defined events to see if any match the new status + foreach (self::$eventDefinitions as $event) { + $configuredStatusId = (int)Configuration::get($this->getEventConfigKey($event['id'], 'ps_status')); + + // If the new status matches the one configured for this event... + if ($configuredStatusId > 0 && $newStatusId === $configuredStatusId) { + // ...call the processor method defined for this event. + if (method_exists($this, $event['processor_method'])) { + $this->{$event['processor_method']}($orderId, $event); + $this->markAsProcessed($eventHash); + // We break because an order status change should only trigger one event. + break; + } + } + } + } + + + + // ... displayConnectionStatus, getMauticAuthUrl, getOauth2RedirectUri ... + // ... makeApiRequest, refreshTokenIfNeeded ... + // ... hookActionCustomerAccountAdd, hookActionObjectCustomerUpdateAfter, syncCustomer ... + // ... sendOrderEmail, findContactByEmail, and all other helpers remain mostly unchanged ... + + // --- DATA PROVIDERS FOR FORMS --- + + private function getPrestaShopStatuses(): array + { + $statuses = OrderState::getOrderStates((int)$this->context->language->id); + $options = [['id' => 0, 'name' => $this->l('--- Disabled ---')]]; + foreach ($statuses as $status) { + $options[] = ['id' => $status['id_order_state'], 'name' => $status['name']]; + } + return $options; + } + + private function getMauticSegments(): array + { + $response = $this->makeApiRequest('/api/segments'); + $segments = $response['lists'] ?? []; + $options = [['id' => 0, 'name' => $this->l('--- Please Select ---')]]; + foreach ($segments as $segment) { + $options[] = ['id' => $segment['id'], 'name' => $segment['name']]; + } + return $options; + } + + private function getMauticEmails(): array + { + $response = $this->makeApiRequest('/api/emails'); + $emails = $response['emails'] ?? []; + $options = [['id' => 0, 'name' => $this->l('--- Please Select ---')]]; + + foreach ($emails as $email) { + // We MUST filter for 'template' type emails. These are the Mautic equivalent + // of transactional emails, designed to be sent to a single contact via API. + // 'list' emails are for mass-mailing to segments and are not suitable here. + if (isset($email['emailType']) && $email['emailType'] === 'template') { + $options[] = ['id' => $email['id'], 'name' => $email['name']]; + } + } + return $options; + } + + /** * Generates the configuration form. */ @@ -360,6 +593,10 @@ class MauticConnect extends Module */ public function hookActionCustomerAccountAdd($params) { + if (!$this->isConnected()) { + return false; + } + if (isset($params['newCustomer']) && Validate::isLoadedObject($params['newCustomer'])) { $this->syncCustomer($params['newCustomer']); } @@ -370,6 +607,9 @@ class MauticConnect extends Module */ public function hookActionObjectCustomerUpdateAfter($params) { + if (!$this->isConnected()) { + return false; + } if (isset($params['object']) && $params['object'] instanceof Customer) { $this->syncCustomer($params['object']); } @@ -397,7 +637,7 @@ class MauticConnect extends Module */ public function syncCustomer(Customer $customer) { - if (!$this->isConnected() || !Validate::isLoadedObject($customer)) { + if (!$this->isConnected() || !Validate::isLoadedObject($customer) || strpos($customer->email, '@' . Tools::getShopDomainSsl())) { return false; } @@ -550,12 +790,16 @@ class MauticConnect extends Module // which contains an array of the segment objects. return $response['lists'] ?? []; } - public function processOrderArrivedEvent(int $id_order) + public function processOrderArrivedEvent(int $id_order, array $eventDefinition) { - if (!isset($this->mauticOrderArrivedSegmentId) || !isset($this->mauticOrderArrivedTemplateId)) { + + $mauticSegmentId = (int)Configuration::get($this->getEventConfigKey($eventDefinition['id'], 'mautic_segment')); + $mauticTemplateId = (int)Configuration::get($this->getEventConfigKey($eventDefinition['id'], 'mautic_template')); + + // Do nothing if this event is not fully configured + if (!$mauticSegmentId || !$mauticTemplateId) { return; } - // 2. Get all necessary objects $order = new Order($id_order); $customer = new Customer((int)$order->id_customer); @@ -565,7 +809,7 @@ class MauticConnect extends Module // 3. Gather primary data $customer_email = $customer->email; - if (!$this->isContactInSegment($customer_email, $this->mauticOrderArrivedSegmentId)) { + if (!$this->isContactInSegment($customer_email, $mauticSegmentId)) { return; } $tracking_number = $order->getWsShippingNumber(); @@ -590,7 +834,7 @@ class MauticConnect extends Module // --- Build the HTML part --- $products_html .= ' ' . $product['product_name'] . ' - ' . $product['product_name'] . '
Qty: ' . $product['product_quantity'] . ' + ' . $product['product_name'] . '
' . $product['product_quantity'] . ' x ' . round($product['unit_price_tax_incl'], 2) . ' ' . $currency->iso_code . ' ' . round($product['total_price_tax_incl'], 2) . ' ' . $currency->iso_code . ' '; @@ -637,14 +881,24 @@ class MauticConnect extends Module ]; $mauticContactId = $this->getMauticContactIdByEmail($customer_email); - $endpointUrl = "/api/emails/$this->mauticOrderArrivedTemplateId/contact/$mauticContactId/send"; + $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) + public function processOrderShippedEvent(int $id_order, array $eventDefinition) { - if (!isset($this->mauticOrderShippedSegmentId) || !isset($this->mauticOrderShippedTemplateId)) { + $mauticSegmentId = (int)Configuration::get($this->getEventConfigKey($eventDefinition['id'], 'mautic_segment')); + $mauticTemplateId = (int)Configuration::get($this->getEventConfigKey($eventDefinition['id'], 'mautic_template')); + + // Do nothing if this event is not fully configured + if (!$mauticSegmentId || !$mauticTemplateId) { return; } @@ -657,7 +911,7 @@ class MauticConnect extends Module // 3. Gather primary data $customer_email = $customer->email; - if (!$this->isContactInSegment($customer_email, $this->mauticOrderShippedSegmentId)) { + if (!$this->isContactInSegment($customer_email, $mauticSegmentId)) { return; } $tracking_number = $order->getWsShippingNumber(); @@ -678,11 +932,10 @@ class MauticConnect extends Module foreach ($products as $product) { $product_obj = new Product($product['product_id'], false, $this->context->language->id); $image_url = $link->getImageLink($product_obj->link_rewrite, $product['image']->id, 'cart_default'); - // --- Build the HTML part --- $products_html .= ' ' . $product['product_name'] . ' - ' . $product['product_name'] . '
Qty: ' . $product['product_quantity'] . ' + ' . $product['product_name'] . '
' . $product['product_quantity'] . ' x ' . round($product['unit_price_tax_incl'], 2) . ' ' . $currency->iso_code . ' ' . round($product['total_price_tax_incl'], 2) . ' ' . $currency->iso_code . ' '; @@ -728,27 +981,17 @@ class MauticConnect extends Module 'firstname' => $customer->firstname ]; $mauticContactId = $this->getMauticContactIdByEmail($customer_email); - - $endpointUrl = "/api/emails/$this->mauticOrderShippedTemplateId/contact/$mauticContactId/send"; + $endpointUrl = implode('', [ + '/api/emails/', + $mauticTemplateId, + '/contact/', + $mauticContactId, + '/send' + ]); $response = $this->makeApiRequest($endpointUrl, 'POST', ['tokens' => $data_for_mautic]); return $response; } - public function hookActionOrderStatusUpdate($params) - { - $eventHash = md5((int)$params['newOrderStatus']->id . '_' . (int)$params['id_order']); - if ($this->isAlreadyProcessed($eventHash)) { - return; - } - - if ((int)$params['newOrderStatus']->id == $this->psOrderShippedStatusId) { - $this->processOrderShippedEvent((int)$params['id_order']); - } - if ((int)$params['newOrderStatus']->id == $this->psOrderArrivedStatusId) { - $this->processOrderArrivedEvent((int)$params['id_order']); - } - $this->markAsProcessed($eventHash); - } /**