diff --git a/Model/Carrier/BobGo.php b/Model/Carrier/BobGo.php index 2809b13f9ba6364acd2667ee292656b11e410329..1260bbfd8508776e36eb0c735a41770cbac89e93 100644 --- a/Model/Carrier/BobGo.php +++ b/Model/Carrier/BobGo.php @@ -680,6 +680,11 @@ class BobGo extends AbstractCarrierOnline implements \Magento\Shipping\Model\Car return UData::RATES_ENDPOINT; } + private function getWebhookUrl(): string + { + return UData::WEBHOOK_URL; + } + /** * Perform API Request to Bob Go API and return response. * @@ -1041,4 +1046,67 @@ class BobGo extends AbstractCarrierOnline implements \Magento\Shipping\Model\Car } return false; } + + public function isWebhookEnabled(): bool + { + $enabled = $this->scopeConfig->getValue( + 'carriers/bobgo/enable_webhooks', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + + // Cast the value to a boolean + return filter_var($enabled, FILTER_VALIDATE_BOOLEAN); + } + + + public function triggerWebhookTest(): bool + { + $webhookKey = $this->scopeConfig->getValue( + 'carriers/bobgo/webhook_key', + \Magento\Store\Model\ScopeInterface::SCOPE_STORE + ); + + // Convert the string to a boolean value + $isEnabled = $this->isWebhookEnabled(); + + $storeId = strval($this->_storeManager->getStore()->getId()); + + $payload = [ + 'event' => 'webhook_validation', + 'channel_identifier' => $this->getBaseUrl(), + 'store_id' => $storeId, + 'webhooks_enabled' => $isEnabled, + ]; + + try { + $this->encodeWebhookAndPostRequest($this->getWebhookUrl(), $payload, $storeId, $webhookKey); + $statusCode = $this->curl->getStatus(); + $responseBody = $this->curl->getBody(); + + if ($statusCode != 200) { + throw new LocalizedException(__('Status code from BobGo: %1', $statusCode)); + } + } catch (\Exception $e) { + return false; + } + return true; + } + + public function encodeWebhookAndPostRequest($url, $data, $storeId, $webhookKey) { + // Generate the HMAC-SHA256 hash as raw binary data + $rawSignature = hash_hmac('sha256', $storeId, $webhookKey, true); + // Encode the binary data in Base64 + $signature = base64_encode($rawSignature); + // Set headers and post the data + $this->curl->addHeader('Content-Type', 'application/json'); + $this->curl->addHeader('x-m-webhook-signature', $signature); + + $payloadJson = json_encode($data); + if ($payloadJson === false) { + throw new \RuntimeException('Failed to encode payload to JSON.'); + } + + $this->curl->addHeader('Content-Type', 'application/json'); + $this->curl->post($url, $payloadJson); + } } diff --git a/Model/Carrier/UData.php b/Model/Carrier/UData.php index d173baaf9bf68790a4b4e7490eff6e79b408105e..adf83a27d10e5dafbef7fa9c7fb864e37759dc56 100644 --- a/Model/Carrier/UData.php +++ b/Model/Carrier/UData.php @@ -20,4 +20,11 @@ class UData * @var string */ public const RATES_ENDPOINT = 'https://api.dev.bobgo.co.za/rates-at-checkout/magento'; + + /** + * Order create/update webhook URL + * + * @var string + */ + public const WEBHOOK_URL = 'https://api.dev.bobgo.co.za/webhook/channel/magento'; } diff --git a/Observer/ConfigChangeObserver.php b/Observer/ConfigChangeObserver.php index 1b6c07214501db7cd9ba19dc325fe3bbade643a5..efc0179a7bd1288e9f61931fae30f25f63597569 100644 --- a/Observer/ConfigChangeObserver.php +++ b/Observer/ConfigChangeObserver.php @@ -52,6 +52,7 @@ class ConfigChangeObserver implements ObserverInterface { $changedPaths = $observer->getEvent()->getData('changed_paths'); + // Test for rates at checkout if (is_array($changedPaths) && in_array('carriers/bobgo/active', $changedPaths)) { if ($this->bobGo->isActive()) { $result = $this->bobGo->triggerRatesTest(); @@ -70,5 +71,23 @@ class ConfigChangeObserver implements ObserverInterface } } } + + // Test for webhooks + if ((is_array($changedPaths) && in_array('carriers/bobgo/enable_webhooks', $changedPaths)) || (is_array($changedPaths) && in_array('carriers/bobgo/webhook_key', $changedPaths))) { + $result = $this->bobGo->triggerWebhookTest(); + + if ($this->bobGo->isWebhookEnabled()) { + if ($result) { + $this->messageManager->addSuccessMessage( + __('Webhook validation successful.') + ); + } else { + $this->messageManager->addErrorMessage( + __('Webhook validation failed. Please check your internet connection + and use your Bob Go integration consumer secret key for webhook validation.') + ); + } + } + } } } diff --git a/Observer/ModifyShippingDescription.php b/Observer/ModifyShippingDescription.php new file mode 100644 index 0000000000000000000000000000000000000000..2e443aff7dc7af482ec5ae4630129244e080b95c --- /dev/null +++ b/Observer/ModifyShippingDescription.php @@ -0,0 +1,58 @@ +<?php + +namespace BobGroup\BobGo\Observer; + +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\Event\Observer; +use Psr\Log\LoggerInterface; + +class ModifyShippingDescription implements ObserverInterface +{ + public const CODE = 'bobgo'; + + protected $logger; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function execute(Observer $observer) + { + // Get the order object from the event + $order = $observer->getEvent()->getOrder(); + + // Get the current shipping description + $shippingDescription = $order->getShippingDescription(); + + // Get the method title from the shipping description + $methodTitle = $this->extractMethodTitle($shippingDescription); + + // Set the new shipping description based only on MethodTitle + $newDescription = $methodTitle; + + // Update the shipping description in the order + $order->setShippingDescription($newDescription); + } + + /** + * Helper function to extract the method title from the original shipping description + * + * @param string $shippingDescription + * @return string + */ + private function extractMethodTitle($shippingDescription) + { + // Find the position of the last dash in the string + $lastDashPosition = strrpos($shippingDescription, ' - '); + + // If a dash is found, extract the part after the last dash + if ($lastDashPosition !== false) { + return trim(substr($shippingDescription, $lastDashPosition + 3)); // +3 to skip the ' - ' part + } + + // If no dash is found, return the full description (fallback) + return $shippingDescription; + } + +} diff --git a/Observer/OrderCreateWebhook.php b/Observer/OrderCreateWebhook.php new file mode 100644 index 0000000000000000000000000000000000000000..74f1ce4520155ee9b072cd2b6d0d2c6e00ea89a8 --- /dev/null +++ b/Observer/OrderCreateWebhook.php @@ -0,0 +1,19 @@ +<?php + +namespace BobGroup\BobGo\Observer; + +use Magento\Framework\Event\Observer; + +class OrderCreateWebhook extends OrderWebhookBase +{ + public function execute(Observer $observer) + { + $order = $observer->getEvent()->getOrder(); + if (!$order) { + return; + } + + // Extract order data and send to the webhook URL + $this->sendWebhook($order); + } +} diff --git a/Observer/OrderWebhookBase.php b/Observer/OrderWebhookBase.php new file mode 100644 index 0000000000000000000000000000000000000000..62988c46be67f0066d8724530b3028040fa9c117 --- /dev/null +++ b/Observer/OrderWebhookBase.php @@ -0,0 +1,69 @@ +<?php + +namespace BobGroup\BobGo\Observer; + +use BobGroup\BobGo\Model\Carrier\UData; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\ObserverInterface; +use Magento\Framework\HTTP\Client\Curl; +use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; +use BobGroup\BobGo\Model\Carrier\BobGo; + +abstract class OrderWebhookBase implements ObserverInterface +{ + protected Curl $curl; + protected LoggerInterface $logger; + protected StoreManagerInterface $storeManager; + protected ScopeConfigInterface $scopeConfig; + protected $bobGo; + + public function __construct(LoggerInterface $logger, Curl $curl, StoreManagerInterface $storeManager, ScopeConfigInterface $scopeConfig, BobGo $bobGo) + { + $this->logger = $logger; + $this->curl = $curl; + $this->storeManager = $storeManager; + $this->scopeConfig = $scopeConfig; + $this->bobGo = $bobGo; + } + + protected function sendWebhook($order) + { + // Return early if not enabled + if (!$this->bobGo->isWebhookEnabled()) { + return; + } + + // Webhook URL + $url = $this->getWebhookUrl(); + + $storeId = $this->getStoreId(); + + $orderId = $order->getId(); + + // Prepare payload + $data = [ + 'event' => 'order_updated', + 'order_id' => $orderId, + 'channel_identifier' => $this->bobGo->getBaseUrl(), + 'store_id' => $storeId, + 'webhooks_enabled' => true, // If we get to this point webhooks are enabled + ]; + + // Generate the signature using the webhook key saved in config + $webhookKey = $this->scopeConfig->getValue('carriers/bobgo/webhook_key', \Magento\Store\Model\ScopeInterface::SCOPE_STORE); + // Send the webhook + $this->bobGo->encodeWebhookAndPostRequest($url, $data, $storeId, $webhookKey); + } + + private function getWebhookUrl(): string + { + return UData::WEBHOOK_URL; + } + + private function getStoreId(): string + { + $storeId = $this->storeManager->getStore()->getId(); + return $storeId; + } +} diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 263bc8b6ea4907d1838e777473e854f13cbb0d41..16f3d833cb34abc3a45eb903f116cfe301606dcf 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -28,13 +28,28 @@ <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment>Displays the delivery timeframe and additional service level description, as configured on Bob Go.</comment> </field> - <field id="enable_track_order" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> - <label>Enable Track My Order</label> - <comment>When this setting is enabled, your customers will be presented with a page to track orders.</comment> - <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - </field> + + <!-- Enable Webhooks Checkbox --> + <field id="enable_webhooks" translate="label" type="select" sortOrder="8" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Enable webhooks</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>Enable or disable the webhook functionality for Bob Go.</comment> + </field> + + <!-- Webhook Key Input Field --> + <field id="webhook_key" translate="label" type="text" sortOrder="9" showInDefault="1" showInWebsite="1" showInStore="0"> + <label>Webhook key</label> + <comment>Enter Bob Go integration consumer secret key for webhook authentication.</comment> + <depends> + <field id="enable_webhooks">1</field> + </depends> + </field> + <field id="enable_track_order" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>Enable Track My Order</label> + <comment>When this setting is enabled, your customers will be presented with a page to track orders.</comment> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> </section> - </system> </config> diff --git a/etc/events.xml b/etc/events.xml index bfb5e9669522f329879ecc96da2ca4a571ea8b2e..a6fab5c7c81c3fe6dd4331d91aaa71bd88848158 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -1,3 +1,11 @@ <?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="sales_order_save_after"> + <observer name="order_create_webhook" instance="BobGroup\BobGo\Observer\OrderCreateWebhook"/> + </event> + <!-- app/code/Vendor/Module/etc/events.xml --> + <event name="sales_order_place_before"> + <observer name="modify_shipping_description" instance="BobGroup\BobGo\Observer\ModifyShippingDescription"/> + </event> + </config> diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml index bc1aaebb43890432e80d8e9d27bfd5838b7c8e8b..25a80a9bc5c7b63aad2741c99339161717ede977 100644 --- a/etc/frontend/di.xml +++ b/etc/frontend/di.xml @@ -13,4 +13,12 @@ <argument name="logger" xsi:type="object">Psr\Log\LoggerInterface</argument> </arguments> </type> + <type name="BobGroup\BobGo\Observer\OrderCreateWebhook"> + <arguments> + <argument name="logger" xsi:type="object">Psr\Log\LoggerInterface</argument> + <argument name="curl" xsi:type="object">Magento\Framework\HTTP\Client\Curl</argument> + <argument name="storeManager" xsi:type="object">Magento\Store\Model\StoreManagerInterface</argument> + </arguments> + </type> + </config>