PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Custom payment method – authorize/capture/refund, JS renderer, PCI DSS

by Henryk Tews / Tuesday, 09 August 2022 / Published in Magento 2

Building a custom payment method in Magento 2 is one of the more complex integrations you can do on the platform. It touches frontend JavaScript, backend PHP, REST API configuration, and security requirements. I walk through the complete implementation – authorize, capture, refund, the JS renderer for checkout, and PCI DSS considerations.

Module structure

Vendor/Payment/
  etc/
    config.xml          - default configuration values
    adminhtml/system.xml - admin settings UI
    di.xml
    module.xml
  Model/
    Payment/
      Method.php        - main payment method class (extends AbstractMethod)
    Api/
      Client.php        - HTTP client for the gateway API
    Config.php          - configuration helper
  view/
    frontend/
      web/js/view/payment/
        method-renderer/
          vendor-payment.js    - Knockout JS component
      web/template/payment/
        vendor-payment.html    - checkout form template

config.xml – default values

<config>
    <default>
        <payment>
            <vendor_payment>
                <active>0</active>
                <model>Vendor\Payment\Model\Payment\Method</model>
                <order_status>pending_payment</order_status>
                <payment_action>authorize</payment_action>
                <title>Credit Card</title>
                <allowspecific>0</allowspecific>
                <can_use_checkout>1</can_use_checkout>
                <can_authorize>1</can_authorize>
                <can_capture>1</can_capture>
                <can_refund>1</can_refund>
                <can_void>1</can_void>
            </vendor_payment>
        </payment>
    </default>
</config>

Payment Method class

<?php

declare(strict_types=1);

namespace Vendor\Payment\Model\Payment;

use Magento\Payment\Model\Method\AbstractMethod;

class Method extends AbstractMethod
{
    protected $_code                    = 'vendor_payment';
    protected $_isGateway               = true;
    protected $_canAuthorize            = true;
    protected $_canCapture              = true;
    protected $_canCapturePartial       = false;
    protected $_canRefund               = true;
    protected $_canRefundInvoicePartial = true;
    protected $_canVoid                 = true;
    protected $_canUseCheckout          = true;
    protected $_isInitializeNeeded      = false;

    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,
        private \Vendor\Payment\Model\Api\Client $apiClient,
        array $data = []
    ) {
        parent::__construct($context, $registry, $extensionFactory, $customAttributeFactory,
            $paymentData, $scopeConfig, $logger, null, null, $data);
    }

    public function authorize(\Magento\Payment\Model\InfoInterface $payment, $amount): static
    {
        try {
            $token    = $payment->getAdditionalInformation('payment_token');
            $order    = $payment->getOrder();
            $response = $this->apiClient->authorize([
                'amount'   => (int) round($amount * 100),
                'currency' => $order->getBaseCurrencyCode(),
                'token'    => $token,
                'order_id' => $order->getIncrementId(),
            ]);

            $payment->setTransactionId($response['transaction_id']);
            $payment->setIsTransactionClosed(false);
            $payment->setAdditionalInformation('auth_code', $response['auth_code']);

        } catch (\Exception $e) {
            $this->_logger->error('Authorize failed: ' . $e->getMessage());
            throw new \Magento\Framework\Exception\LocalizedException(
                __('Payment authorization failed. Please try again.')
            );
        }

        return $this;
    }

    public function capture(\Magento\Payment\Model\InfoInterface $payment, $amount): static
    {
        try {
            $authTxnId = $payment->getParentTransactionId();
            $response  = $this->apiClient->capture([
                'auth_transaction_id' => $authTxnId,
                'amount'              => (int) round($amount * 100),
            ]);

            $payment->setTransactionId($response['capture_id']);
            $payment->setIsTransactionClosed(true);

        } catch (\Exception $e) {
            throw new \Magento\Framework\Exception\LocalizedException(
                __('Payment capture failed.')
            );
        }

        return $this;
    }

    public function refund(\Magento\Payment\Model\InfoInterface $payment, $amount): static
    {
        $captureTxnId = $payment->getParentTransactionId();

        if (!$captureTxnId) {
            throw new \Magento\Framework\Exception\LocalizedException(
                __('Cannot refund without a capture transaction.')
            );
        }

        $response = $this->apiClient->refund([
            'capture_id' => $captureTxnId,
            'amount'     => (int) round($amount * 100),
        ]);

        $payment->setTransactionId($response['refund_id'] . '-refund');
        $payment->setIsTransactionClosed(true);

        return $this;
    }
}

Checkout JS renderer (Knockout)

// view/frontend/web/js/view/payment/method-renderer/vendor-payment.js
define([
    'Magento_Checkout/js/view/payment/default',
    'ko',
    'mage/translate'
], function (Component, ko, $t) {
    'use strict';

    return Component.extend({
        defaults: {
            template: 'Vendor_Payment/payment/vendor-payment',
            paymentToken: ko.observable(''),
        },

        initObservable: function () {
            this._super().observe(['paymentToken']);
            return this;
        },

        // Send token (never raw card data) to backend
        getData: function () {
            return {
                method: this.item.method,
                additional_data: {
                    payment_token: this.paymentToken()
                }
            };
        },

        // Called before order is placed - tokenise card via gateway SDK
        beforePlaceOrder: function () {
            // Example: Stripe.js tokenisation
            const cardElement = this.getCardElement();
            window.gatewaySDK.createToken(cardElement).then((token) => {
                this.paymentToken(token.id);
                this.placeOrder();
            }).catch((error) => {
                alert($t('Card tokenisation failed: ') + error.message);
            });
        },

        validate: function () {
            return !!this.paymentToken();
        },
    });
});

PCI DSS – key requirement

Never pass raw card numbers through Magento. The flow must be:

  1. Card data entered in the browser
  2. Gateway SDK tokenises directly from browser to gateway (card data never touches your server)
  3. Token sent to Magento backend
  4. Magento uses the token to call the gateway API

This approach keeps you in the simplest PCI DSS SAQ-A scope. Sending raw card data through Magento requires SAQ-D – a much heavier compliance burden.

Summary

A custom payment method touches more layers of Magento than almost any other integration. The PHP side is straightforward once you understand the authorize/capture/refund contract. The JS renderer handles the checkout UI. The critical security requirement is tokenisation – card data should never reach your server. Done correctly, the implementation is clean and the compliance scope is minimal.

About Henryk Tews

What you can read next

Xdebug – configuration, PHPStorm, debugging Magento plugins
Strategy pattern in PHP – and how Magento 2 uses it in pricing

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}