Maka
Maka

Reputation: 663

VirtueMart send notifications on update order status externally

I am building Joomla webservice plugin that can change status of VirtueMart order. Plugin can update status depending on data sent in POST request to it's endpoint. Status update works fine, issue is that it does not trigger internal logic for sending email notifications.

Virtueamart does have internal methods which are usually triggered via Admin panel or Payment plugin. First one is updateOrderStatus which is used by Admin panel and updateStatusForOneOrder used by payment plugins.

Usually would be required to call oderModel and function like this:

$result = $orderModel->safeUpdateOrderStatus($orderId, $orderStatus, ["P", "U", ""]);

But this does not trigger internal methods for kicking off notification system. Here is full example:

Plugin

<?php
defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Router\ApiRouter;
use Joomla\CMS\Factory;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Dispatcher\EventDispatcher;

if (!defined('VMPATH_ADMIN')) {
    define('VMPATH_ADMIN', JPATH_ADMINISTRATOR . '/components/com_virtuemart');
    
}
if (!defined('JPATH_COMPONENT')) {
    define('JPATH_COMPONENT', JPATH_SITE . '/components/com_virtuemart');
}

class PlgWebservicesVmorderstatusapi extends CMSPlugin
{
    protected $autoloadLanguage = true;

    public function onBeforeApiRoute(&$router)
    {
        // Ensure that the log file path is a string and correct
        $logFile = JPATH_ROOT . '/logs/api-debug.log';
        if (!is_string($logFile)) {
            // Handle if the log file path is not a string
            $this->sendJsonResponse(['error' => 'Invalid log file path'], 500);
            return;
        }

        try {
            $app = Factory::getApplication();
            if (!$app) {
                $this->sendJsonResponse(['error' => 'Application context not initialized'], 500);
                return;
            }
            $requestPath = trim(parse_url($app->input->server->getString('REQUEST_URI'), PHP_URL_PATH), '/');

            // Log all headers
            $headers = getallheaders(); // Get all headers
            // Check for API token in the headers
            $apiToken = isset($headers['Authorization']) ? $headers['Authorization'] : null; // Retrieve the Authorization header

            if (!$this->isValidApiToken($apiToken)) {
                $this->sendJsonResponse(['error' => 'Unauthorized'], 401);
                return; // Stop further processing
            }

            $rawData = file_get_contents('php://input');
            $inputData = json_decode($rawData, true);

            // Check if data is valid
            if (!isset($inputData['order_id']) || !isset($inputData['status'])) {
                $this->sendJsonResponse(['error' => 'Order ID and status are required'], 400);
                return; // Stop further processing
            }

            // Proceed with your logic (e.g., update order status)
            if ($requestPath === 'api/index.php/v1/vmorderstatusapi/updateOrderStatus') {
                $this->handleUpdateOrderStatus($app, $inputData);
                return; // Stop further processing
            }

            // If no matching route found
            $this->sendJsonResponse(['error' => 'Resource not found'], 404);

        } catch (\Exception $e) {
            file_put_contents($logFile, "\nError occurred:\n", FILE_APPEND);
            file_put_contents($logFile, $e->getMessage() . "\n", FILE_APPEND);
            file_put_contents($logFile, $e->getTraceAsString() . "\n", FILE_APPEND);
            $this->sendJsonResponse(['error' => 'Internal Server Error'], 500);
        }
    }

    private function handleUpdateOrderStatus($app, $data)
    {
        // Ensure the application context is valid
        if (!$app) {
            $this->sendJsonResponse(['error' => 'Application context not initialized'], 500);
            return;
        }
        
        // Check if VirtueMart and web asset manager are initialized
        if (method_exists($app, 'getWebAssetManager')) {
            $webAssetManager = $app->getWebAssetManager();
            // Continue....
        }
        
        // Include VirtueMart dependencies
        $this->includeVirtueMartDependencies();
        $orderModel = VmModel::getModel('orders');
        
        // C - Confirmed
        // D - Denied
        // F - Completed
        // P - Pending - LOCKED
        // R - Refunded
        // S - Shipped - LOCKED
        // U - Confirmed by Shopper
        // X - Cancelled - LOCKED

        // Get parameters from the request body
        $input          = $app->input;
        $orderId        = 127;                      // $input->getInt('order_id');
        $orderStatus    = "P";                      // $input->getString('status');
        $comments       = "Updating status... ";    // $input->getString('comments');
        $notify         = 1;                        // $input->getInt('notify', 1);

        // Version 1: 
        // Prepare the order data for update
        // $orderData = [
        //     'virtuemart_order_id' => $orderId,
        //     'order_status' => $orderStatus,
        //     'customer_notified' => $notify,
        //     'comments' => $comments,
        // ];
        
        // // Update the order status
        // $result = $orderModel->updateStatusForOneOrder($orderId, $orderData, true);
        

        // Version 2: 
        // This under the hood calls updateStatusForOneOrder method
        // Important: This updates order status but does not send any notification. It does sents response true | false which is good
        $result = $orderModel->safeUpdateOrderStatus($orderId, $orderStatus, ["P", "U", ""]);
 
        if ($result) {
            // Trigger the order status change event
            $dispatcher = Factory::getApplication()->getDispatcher();
            $dispatcher->trigger('onVmOrderStatusChange', [$orderId, $orderStatus, $comments]);
            $this->sendJsonResponse(['message' => 'Order status updated successfully']);
        } else {
            $this->sendJsonResponse(['error' => 'Failed to update order status'], 500);
        }
    }

    private function includeVirtueMartDependencies()
    {
        // Include the vObject class
        if (!class_exists('vObject')) {
            require_once JPATH_ADMINISTRATOR . '/components/com_virtuemart/helpers/vobject.php';
        }
        // Include the base VirtueMart model
        if (!class_exists('VmModel')) {
            require_once JPATH_ADMINISTRATOR . '/components/com_virtuemart/helpers/vmmodel.php';
        }
        if (!class_exists('VmTable')) {
            require_once JPATH_ADMINISTRATOR . '/components/com_virtuemart/helpers/vmtable.php';
        }
        if (!class_exists('VmConfig')) {
            require_once JPATH_ADMINISTRATOR . '/components/com_virtuemart/helpers/config.php';
            // JLoader::register('VmConfig', JPATH_ADMINISTRATOR . '/components/com_virtuemart/helpers/config.php');
        }
        // Include the VirtueMart order model
        if (!class_exists('VirtueMartModelOrders')) {
            require_once JPATH_ADMINISTRATOR . '/components/com_virtuemart/models/orders.php';
        }
        VmConfig::loadConfig();
    }

    private function sendJsonResponse($data, $status = 200)
    {
        Factory::getApplication()->setHeader('Content-Type', 'application/json', true);
        http_response_code($status);
        echo json_encode($data);
        Factory::getApplication()->close();
    }

    private function isValidApiToken($apiToken)
    {
        // Check if the token is present
        if (empty($apiToken)) {
            return false; // Token is missing
        }
        // Remove "Bearer " prefix if present
        if (strpos($apiToken, 'Bearer ') === 0) {
            $apiToken = substr($apiToken, 7);
        } else {
            return false; // Invalid token format
        }
        $validToken = "my-magic-secure-token"; // Replace with actual method to fetch token
        return $apiToken === $validToken;
    }
}

Does anyone have any ideas how to VM's internals are triggering notifications?

If there is a way to setup CronJob to scan orders and send emails for those that changed status, that would do as well if such method exists.

Thx!

Upvotes: 0

Views: 25

Answers (0)

Related Questions