BoomPay/docs

Magento 2 / Adobe Commerce

A standard offsite-redirect payment module: AbstractMethod + OrderPlaceRedirectInterface, a CSRF-exempt callback controller, and the usual invoice-on-payment flow.

PHPOffsite redirect moduleMagento 2 / Adobe Commerce

This follows the same redirect-method shape Magento has used for offsite gateways since the PayPal Standard era: a payment model that points checkout at a controller instead of processing a card directly, a redirect controller that talks to BoomPay, and a callback controller that invoices the order once the signed return checks out.

Module files

Module declaration

registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'BoomPay_Payment',
    __DIR__
);
etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="BoomPay_Payment" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Sales"/>
            <module name="Magento_Payment"/>
            <module name="Magento_Checkout"/>
        </sequence>
    </module>
</config>

Configuration

etc/config.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/config.xsd">
    <default>
        <payment>
            <boompay>
                <active>0</active>
                <model>BoomPay\Payment\Model\BoomPay</model>
                <title>Pay with Boomcoin (BoomPay)</title>
                <sandbox>1</sandbox>
                <allow_multiple_address>1</allow_multiple_address>
                <group>offline</group>
            </boompay>
        </payment>
    </default>
</config>
etc/adminhtml/system.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <section id="payment">
            <group id="boompay" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1">
                <label>BoomPay (Boomcoin)</label>
                <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1">
                    <label>Enabled</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="title" translate="label" type="text" sortOrder="2" showInDefault="1" showInWebsite="1">
                    <label>Title</label>
                </field>
                <field id="sandbox" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="1">
                    <label>Sandbox mode</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="api_key" translate="label" type="obscure" sortOrder="4" showInDefault="1" showInWebsite="1">
                    <label>Live API key</label>
                    <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
                </field>
                <field id="sandbox_api_key" translate="label" type="obscure" sortOrder="5" showInDefault="1" showInWebsite="1">
                    <label>Sandbox API key</label>
                    <backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
                </field>
            </group>
        </section>
    </system>
</config>
etc/payment.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/payment.xsd">
    <methods>
        <method name="boompay">
            <allow_multiple_address>1</allow_multiple_address>
        </method>
    </methods>
</config>

Routing

etc/frontend/routes.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="boompay" frontName="boompay">
            <module name="BoomPay_Payment"/>
        </route>
    </router>
</config>

BoomPay client

Helper/Client.php
<?php
namespace BoomPay\Payment\Helper;

use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\HTTP\Client\Curl;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\Framework\Exception\LocalizedException;

class Client extends AbstractHelper
{
    const SANDBOX_BASE = 'https://sapi.boom.market';
    const LIVE_BASE = 'https://api.boom.market';

    private $curl;
    private $json;

    public function __construct(
        \Magento\Framework\App\Helper\Context $context,
        Curl $curl,
        Json $json
    ) {
        parent::__construct($context);
        $this->curl = $curl;
        $this->json = $json;
    }

    public function createIntent($apiKey, $sandbox, $amount, $successUrl, $failureUrl, $label, $metadata = [])
    {
        return $this->request($apiKey, $sandbox, 'POST', '/v1/boompay/paymentIntent', [
            'amount' => $amount,
            'successUrl' => $successUrl,
            'failureUrl' => $failureUrl,
            'label' => $label,
            'metadata' => $metadata,
        ]);
    }

    public function getPayment($apiKey, $sandbox, $id)
    {
        return $this->request($apiKey, $sandbox, 'GET', '/v1/boompay/paymentIntent/' . rawurlencode($id));
    }

    /** signature = base64(HMAC-SHA1(apiKey, paymentIntentId)) -- see API Reference > Verifying the return */
    public function verifySignature($apiKey, $paymentIntentId, $signature)
    {
        $expected = base64_encode(hash_hmac('sha1', $paymentIntentId, $apiKey, true));
        $received = str_replace(' ', '+', (string) $signature);
        return hash_equals($expected, $received);
    }

    private function request($apiKey, $sandbox, $method, $path, $body = null)
    {
        $base = $sandbox ? self::SANDBOX_BASE : self::LIVE_BASE;
        $this->curl->addHeader('x-api-key', $apiKey);
        $this->curl->addHeader('Content-Type', 'application/json');

        if ($method === 'POST') {
            $this->curl->post($base . $path, $this->json->serialize($body));
        } else {
            $this->curl->get($base . $path);
        }

        $status = $this->curl->getStatus();
        $data = $this->json->unserialize($this->curl->getBody());

        if ($status < 200 || $status >= 300) {
            $message = $data['message'] ?? 'Unknown BoomPay API error';
            throw new LocalizedException(__('BoomPay API error (%1): %2', $status, $message));
        }

        return $data;
    }
}

Payment method model

Model/BoomPay.php
<?php
namespace BoomPay\Payment\Model;

use Magento\Payment\Model\Method\AbstractMethod;
use Magento\Payment\Model\Method\OrderPlaceRedirectInterface;

class BoomPay extends AbstractMethod implements OrderPlaceRedirectInterface
{
    protected $_code = 'boompay';
    protected $_isOffline = false;
    protected $_canCapture = false;
    protected $_canAuthorize = false;
    protected $_canRefund = false;

    private $urlBuilder;

    public function __construct(
        \Magento\Framework\Model\Context $context,
        \Magento\Framework\Registry $registry,
        \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory,
        \Magento\Framework\Api\AttributeValueFactory $customAttributeFactory,
        \Magento\Payment\Helper\Data $paymentData,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Payment\Model\Method\Logger $logger,
        \Magento\Framework\UrlInterface $urlBuilder,
        array $data = []
    ) {
        parent::__construct(
            $context, $registry, $extensionFactory, $customAttributeFactory,
            $paymentData, $scopeConfig, $logger, null, null, $data
        );
        $this->urlBuilder = $urlBuilder;
    }

    public function getOrderPlaceRedirectUrl()
    {
        return $this->urlBuilder->getUrl('boompay/payment/redirect');
    }

    public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null)
    {
        return parent::isAvailable($quote) && (bool) $this->getConfigData('active');
    }
}

Controllers

Controller/Payment/Redirect.php
<?php
namespace BoomPay\Payment\Controller\Payment;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Checkout\Model\Session as CheckoutSession;

class Redirect extends Action
{
    private $checkoutSession;
    private $client;
    private $scopeConfig;
    private $orderRepository;

    public function __construct(
        Context $context,
        CheckoutSession $checkoutSession,
        \BoomPay\Payment\Helper\Client $client,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Sales\Api\OrderRepositoryInterface $orderRepository
    ) {
        parent::__construct($context);
        $this->checkoutSession = $checkoutSession;
        $this->client = $client;
        $this->scopeConfig = $scopeConfig;
        $this->orderRepository = $orderRepository;
    }

    public function execute()
    {
        $order = $this->checkoutSession->getLastRealOrder();
        $sandbox = (bool) $this->scopeConfig->getValue('payment/boompay/sandbox');
        $apiKey = $this->scopeConfig->getValue(
            $sandbox ? 'payment/boompay/sandbox_api_key' : 'payment/boompay/api_key'
        );

        $intent = $this->client->createIntent(
            $apiKey,
            $sandbox,
            (float) $order->getGrandTotal(),
            $this->_url->getUrl('boompay/payment/callback', ['status' => 'success', 'order_id' => $order->getId()]),
            $this->_url->getUrl('boompay/payment/callback', ['status' => 'failure', 'order_id' => $order->getId()]),
            sprintf('Order #%s', $order->getIncrementId()),
            ['order_id' => $order->getId()]
        );

        $order->getPayment()->setAdditionalInformation('boompay_intent_id', $intent['id']);
        $this->orderRepository->save($order);

        return $this->resultRedirectFactory->create()->setUrl($intent['link']);
    }
}
Controller/Payment/Callback.php
<?php
namespace BoomPay\Payment\Controller\Payment;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\CsrfAwareActionInterface;
use Magento\Framework\App\Request\InvalidRequestException;
use Magento\Framework\App\RequestInterface;
use Magento\Sales\Model\Order;

class Callback extends Action implements CsrfAwareActionInterface
{
    private $orderRepository;
    private $client;
    private $scopeConfig;
    private $invoiceService;

    public function __construct(
        Context $context,
        \Magento\Sales\Api\OrderRepositoryInterface $orderRepository,
        \BoomPay\Payment\Helper\Client $client,
        \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
        \Magento\Sales\Model\Service\InvoiceService $invoiceService
    ) {
        parent::__construct($context);
        $this->orderRepository = $orderRepository;
        $this->client = $client;
        $this->scopeConfig = $scopeConfig;
        $this->invoiceService = $invoiceService;
    }

    // BoomPay's browser redirect arrives with no Magento form key, so this
    // controller has to opt out of the framework's default CSRF check.
    public function createCsrfValidationException(RequestInterface $request): ?InvalidRequestException
    {
        return null;
    }

    public function validateForCsrfRequest(RequestInterface $request): ?bool
    {
        return true;
    }

    public function execute()
    {
        $orderId = (int) $this->getRequest()->getParam('order_id');
        $intentId = (string) $this->getRequest()->getParam('paymentIntentId');
        $signature = (string) $this->getRequest()->getParam('X-Boom-Signature');
        $status = (string) $this->getRequest()->getParam('status');

        $order = $this->orderRepository->get($orderId);
        $sandbox = (bool) $this->scopeConfig->getValue('payment/boompay/sandbox');
        $apiKey = $this->scopeConfig->getValue(
            $sandbox ? 'payment/boompay/sandbox_api_key' : 'payment/boompay/api_key'
        );

        if (!$this->client->verifySignature($apiKey, $intentId, $signature)) {
            return $this->_redirect('checkout/cart', ['_query' => ['boompay_error' => 'signature']]);
        }

        $payment = $this->client->getPayment($apiKey, $sandbox, $intentId);

        if ($status === 'success' && !empty($payment['paidAt'])) {
            $invoice = $this->invoiceService->prepareInvoice($order);
            $invoice->register();
            $order->addRelatedObject($invoice);
            $order->setState(Order::STATE_PROCESSING)->setStatus('processing');
            $this->orderRepository->save($order);
            return $this->_redirect('checkout/onepage/success');
        }

        $order->setState(Order::STATE_CANCELED)->setStatus('canceled');
        $this->orderRepository->save($order);
        return $this->_redirect('checkout/cart');
    }
}
!

The callback controller implements CsrfAwareActionInterface and opts out of form-key validation. This is necessary and correct here — the request arrives from BoomPay’s redirect, not from a form on your own site — but it also means the signature check above it is the only thing standing between this route and a forged request. Don’t relax it.

Install

Place the files under app/code/BoomPay/Payment/ matching the paths above, then from the Magento root:

bash
bin/magento module:enable BoomPay_Payment
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush

Configure it under Stores → Configuration → Sales → Payment Methods → BoomPay (Boomcoin).

Testing in sandbox

With sandbox mode enabled and a sandbox API key set, place an order through checkout selecting BoomPay. Confirm you land on a sapi.boom.market page, approve the test payment, and check that the order appears under Sales → Orders as Processing with an invoice attached. Then deliberately let a second test payment expire or cancel it, and confirm the order moves to Canceled rather than sitting in limbo.

Go-live checklist

  • Run setup:di:compile again after any code change in production mode — Magento will silently serve stale generated code otherwise.
  • Confirm the live API key is stored via the encrypted backend model (it is, in the config above) so it doesn’t sit in plaintext in core_config_data.
  • BoomPay has no FX endpoint — convert your store’s base currency to a BMC amount before calling createIntent if you don’t price directly in BMC.
  • Test the cancel/failure path specifically, not just the happy path — offsite redirects are where abandoned-cart orders most often get stuck in the wrong state.