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. In this guide, we’ll walk through how to implement a custom order approval workflow step by step.
Why You Need an Order Approval Workflow
Before 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
First, we need a way to store approval requests. We’ll create a new database table 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
Next, 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. We'll 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 order status 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
Once you have the basic workflow working, consider these enhancements:
- Multi-Level Approvals: Create sequential approval steps for different departments.
- Approval Rules: Set rules based on 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
Before going live, thoroughly test:
- Place orders that should and shouldn't require approval
- Test approval/rejection from admin
- Verify email notifications
- Check order status updates
- Test with multiple approvers if applicable
Conclusion
Implementing a custom order approval workflow in Magento 2 requires several components working together: database tracking, admin interfaces, frontend displays, and email notifications. While 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 out of the box.
Have you implemented an approval workflow in your Magento store? Share your experiences in the comments!