How to Implement a Custom "Order Approval Workflow" for B2B Stores

How to Implement a Custom "Order Approval Workflow" for B2B Stores

Running a B2B store on Magento 2? You probably deal with complex purchasing processes where orders need approval before processing. Unlike B2C, B2B transactions often involve multiple decision-makers—managers, procurement teams, or finance departments—who need to review and approve purchases before they’re finalized.

Magento 2’s default setup doesn’t include a built-in order approval workflow, but with some customization, you can create a seamless approval process tailored to your business needs. En esta guía,’ll walk through cómo implement a custom order approval workflow paso a paso.

Why You Need an Order Approval Workflow

Antes de diving into the code, let’s understand why this feature is crucial for B2B stores:

  • Prevents Unauthorized Purchases: Ensures only approved orders proceed.
  • Multi-Level Approvals: Allows different roles (e.g., managers, finance) to review orders.
  • Budget Control: Restricts spending beyond predefined limits.
  • Audit Trail: Keeps track of who approved what and when.

Step 1: Setting Up the Database Structure

Primero, we need a way to store approval requests. We’ll create a new tabla de base de datos to track order approvals.


// File: app/code/YourVendor/OrderApproval/Setup/InstallSchema.php

use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

class InstallSchema implements InstallSchemaInterface
{
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;
        $installer->startSetup();

        $table = $installer->getConnection()->newTable(
            $installer->getTable('order_approval_requests')
        )->addColumn(
            'approval_id',
            Table::TYPE_INTEGER,
            null,
            ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
            'Approval ID'
        )->addColumn(
            'order_id',
            Table::TYPE_INTEGER,
            null,
            ['unsigned' => true, 'nullable' => false],
            'Order ID'
        )->addColumn(
            'status',
            Table::TYPE_TEXT,
            20,
            ['nullable' => false, 'default' => 'pending'],
            'Approval Status (pending/approved/rejected)'
        )->addColumn(
            'approver_id',
            Table::TYPE_INTEGER,
            null,
            ['unsigned' => true, 'nullable' => true],
            'Admin User ID who approved/rejected'
        )->addColumn(
            'comments',
            Table::TYPE_TEXT,
            '64k',
            ['nullable' => true],
            'Approver Comments'
        )->addColumn(
            'created_at',
            Table::TYPE_TIMESTAMP,
            null,
            ['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
            'Creation Time'
        )->addColumn(
            'updated_at',
            Table::TYPE_TIMESTAMP,
            null,
            ['nullable' => false, 'default' => Table::TIMESTAMP_INIT_UPDATE],
            'Update Time'
        )->addForeignKey(
            $installer->getFkName(
                'order_approval_requests',
                'order_id',
                'sales_order',
                'entity_id'
            ),
            'order_id',
            $installer->getTable('sales_order'),
            'entity_id',
            Table::ACTION_CASCADE
        )->setComment(
            'Order Approval Requests'
        );

        $installer->getConnection()->createTable($table);
        $installer->endSetup();
    }
}

Step 2: Creating the Approval Logic

A continuación, we’ll create an observer that triggers when an order is placed and creates an approval request.


// File: app/code/YourVendor/OrderApproval/Observer/OrderPlaceAfter.php

namespace YourVendor\OrderApproval\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use YourVendor\OrderApproval\Model\ApprovalFactory;

class OrderPlaceAfter implements ObserverInterface
{
    protected $approvalFactory;

    public function __construct(ApprovalFactory $approvalFactory)
    {
        $this->approvalFactory = $approvalFactory;
    }

    public function execute(Observer $observer)
    {
        $order = $observer->getEvent()->getOrder();
        
        // Skip approval for certain customer groups or small orders
        if ($order->getCustomerGroupId() == 1 || $order->getGrandTotal() < 1000) {
            return;
        }

        $approval = $this->approvalFactory->create();
        $approval->setOrderId($order->getId())
                ->setStatus('pending')
                ->save();

        // Send email notification to approvers
        $this->sendApprovalEmail($order);
    }

    protected function sendApprovalEmail($order)
    {
        // Email logic here
    }
}

Step 3: Building the Admin Approval Interface

Now we need a way for admins to review and approve orders. Vamos a add a new grid in the Magento admin.


// File: app/code/YourVendor/OrderApproval/Block/Adminhtml/Approval/Grid.php

namespace YourVendor\OrderApproval\Block\Adminhtml\Approval;

class Grid extends \Magento\Backend\Block\Widget\Grid\Extended
{
    protected $_approvalCollectionFactory;

    public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Backend\Helper\Data $backendHelper,
        \YourVendor\OrderApproval\Model\ResourceModel\Approval\CollectionFactory $approvalCollectionFactory,
        array $data = []
    ) {
        $this->_approvalCollectionFactory = $approvalCollectionFactory;
        parent::__construct($context, $backendHelper, $data);
    }

    protected function _construct()
    {
        parent::_construct();
        $this->setId('approvalGrid');
        $this->setDefaultSort('created_at');
        $this->setDefaultDir('DESC');
        $this->setSaveParametersInSession(true);
        $this->setUseAjax(true);
    }

    protected function _prepareCollection()
    {
        $collection = $this->_approvalCollectionFactory->create();
        $this->setCollection($collection);
        return parent::_prepareCollection();
    }

    protected function _prepareColumns()
    {
        $this->addColumn('approval_id', [
            'header' => __('ID'),
            'index' => 'approval_id',
            'type' => 'number'
        ]);

        $this->addColumn('order_id', [
            'header' => __('Order #'),
            'index' => 'order_id',
            'renderer' => \YourVendor\OrderApproval\Block\Adminhtml\Approval\Renderer\Order::class
        ]);

        $this->addColumn('status', [
            'header' => __('Status'),
            'index' => 'status',
            'type' => 'options',
            'options' => [
                'pending' => __('Pending'),
                'approved' => __('Approved'),
                'rejected' => __('Rejected')
            ]
        ]);

        // Add more columns as needed

        return parent::_prepareColumns();
    }

    public function getGridUrl()
    {
        return $this->getUrl('*/*/grid', ['_current' => true]);
    }
}

Step 4: Implementing Approval Actions

Now let's create the controller that handles approval/rejection actions:


// File: app/code/YourVendor/OrderApproval/Controller/Adminhtml/Approval/Approve.php

namespace YourVendor\OrderApproval\Controller\Adminhtml\Approval;

class Approve extends \Magento\Backend\App\Action
{
    protected $approvalFactory;
    protected $orderRepository;
    protected $messageManager;

    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \YourVendor\OrderApproval\Model\ApprovalFactory $approvalFactory,
        \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
    ) {
        parent::__construct($context);
        $this->approvalFactory = $approvalFactory;
        $this->orderRepository = $orderRepository;
        $this->messageManager = $context->getMessageManager();
    }

    public function execute()
    {
        $approvalId = $this->getRequest()->getParam('id');
        $approval = $this->approvalFactory->create()->load($approvalId);
        
        if (!$approval->getId()) {
            $this->messageManager->addErrorMessage(__('Approval request not found.'));
            return $this->_redirect('*/*/');
        }

        try {
            $approval->setStatus('approved')
                     ->setApproverId($this->_auth->getUser()->getId())
                     ->save();

            $order = $this->orderRepository->get($approval->getOrderId());
            $order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING)
                  ->setStatus('processing')
                  ->save();

            $this->messageManager->addSuccessMessage(__('Order has been approved.'));
        } catch (\Exception $e) {
            $this->messageManager->addErrorMessage($e->getMessage());
        }

        return $this->_redirect('*/*/');
    }
}

Step 5: Customizing the Customer Experience

Let's modify the frontend to show estado del pedido and approval information to customers:


// File: app/code/YourVendor/OrderApproval/Block/Order/ApprovalStatus.php

namespace YourVendor\OrderApproval\Block\Order;

class ApprovalStatus extends \Magento\Framework\View\Element\Template
{
    protected $approvalFactory;

    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context,
        \YourVendor\OrderApproval\Model\ApprovalFactory $approvalFactory,
        array $data = []
    ) {
        $this->approvalFactory = $approvalFactory;
        parent::__construct($context, $data);
    }

    public function getApprovalStatus($orderId)
    {
        $approval = $this->approvalFactory->create()
            ->getCollection()
            ->addFieldToFilter('order_id', $orderId)
            ->getFirstItem();

        if ($approval->getId()) {
            return $approval->getStatus();
        }

        return 'approved'; // Default if no approval needed
    }
}

Step 6: Adding Email Notifications

Communication is key in approval workflows. Let's set up email notifications:


// File: app/code/YourVendor/OrderApproval/Model/Email/Sender.php

namespace YourVendor\OrderApproval\Model\Email;

class Sender
{
    protected $transportBuilder;
    protected $storeManager;
    protected $inlineTranslation;

    public function __construct(
        \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation
    ) {
        $this->transportBuilder = $transportBuilder;
        $this->storeManager = $storeManager;
        $this->inlineTranslation = $inlineTranslation;
    }

    public function sendApprovalRequestEmail($order, $approvers)
    {
        $this->inlineTranslation->suspend();
        
        $storeId = $this->storeManager->getStore()->getId();
        $templateVars = [
            'order' => $order,
            'approve_url' => $this->getApprovalUrl($order->getId())
        ];

        foreach ($approvers as $approver) {
            $transport = $this->transportBuilder
                ->setTemplateIdentifier('order_approval_request')
                ->setTemplateOptions(['area' => 'frontend', 'store' => $storeId])
                ->setTemplateVars($templateVars)
                ->setFrom('general')
                ->addTo($approver->getEmail(), $approver->getName())
                ->getTransport();

            $transport->sendMessage();
        }

        $this->inlineTranslation->resume();
    }
}

Advanced Customizations

Una vez you have the basic workflow working, consider these enhancements:

  • Multi-Level Approvals: Create sequential approval steps for different departments.
  • Approval Rules: Set rules basado en order amount, customer group, or product category.
  • Partial Approvals: Allow approvers to modify quantities before approval.
  • Escalation Rules: Automatically escalate if approval takes too long.

Testing Your Workflow

Antes de going live, thoroughly test:

  1. Place orders that should and shouldn't require approval
  2. Test approval/rejection from admin
  3. Verify email notifications
  4. Check estado del pedido updates
  5. Test with multiple approvers if applicable

Conclusión

Implementing a custom order approval workflow in Magento 2 requires several components working together: database tracking, admin interfaces, frontend displays, and email notifications. Mientras this guide covers the fundamentals, you can extend it further to match your specific business processes.

For businesses that prefer ready-made solutions, check out Magefine's Order Approval Extension which offers these features and more listo para usar.

Have you implemented an approval workflow in your Magento store? Share your experiences in the comments!