Implementacja własnej metody płatności w Magento 2 to jeden z trudniejszych tematów – łączy PHP, XML, JavaScript i znajomość procesu checkout. Większość tutoriali zatrzymuje się na „hello world” który pojawia się na liście metod. Pokazuję kompletną implementację: od struktury modułu, przez walidację, po integrację z zewnętrznym gateway’em płatniczym.
Architektura płatności w Magento 2
Każda metoda płatności w Magento 2 składa się z kilku warstw:
- Model PHP – logika biznesowa (autoryzacja, capture, refund)
- Konfiguracja XML – rejestracja metody i ustawienia domyślne
- JavaScript Mixin – komponent checkout (knockout.js)
- Renderer – widok metody na liście w checkout
Struktura modułu
Vendor/PaymentGateway/
etc/
config.xml - wartości domyślne
adminhtml/
system.xml - konfiguracja w panelu admina
module.xml
payment.xml - rejestracja metody
Model/
Payment.php - główny model metody
Api/
PaymentGatewayClient.php - klient HTTP do gateway'a
Gateway/
Request/
AuthorizationRequest.php
CaptureRequest.php
Response/
AuthorizationHandler.php
Validator/
ResponseValidator.php
view/
frontend/
layout/
checkout_index_index.xml
web/
js/
view/
payment/
method-renderer/
gateway-method.js
template/
payment/
gateway-form.html
registration.php
Model płatności – PHP
<?php
declare(strict_types=1);
namespace Vendor\PaymentGateway\Model;
use Magento\Payment\Model\Method\AbstractMethod;
use Magento\Framework\Exception\LocalizedException;
use Magento\Payment\Model\InfoInterface;
class Payment extends AbstractMethod
{
// Unikalny kod metody - używany wszędzie w XML
public const CODE = 'vendor_payment_gateway';
protected $_code = self::CODE;
// Możliwości metody
protected $_isGateway = true;
protected $_canCapture = true;
protected $_canCapturePartial = false;
protected $_canRefund = true;
protected $_canRefundInvoicePartial = false;
protected $_canAuthorize = true;
protected $_canVoid = true;
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\PaymentGateway\Model\Api\PaymentGatewayClient $gatewayClient,
array $data = []
) {
parent::__construct(
$context, $registry, $extensionFactory, $customAttributeFactory,
$paymentData, $scopeConfig, $logger, null, null, $data
);
}
// Walidacja przed wyświetleniem metody - np. sprawdź walutę
public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null): bool
{
if ($quote === null) {
return false;
}
// Metoda dostępna tylko dla PLN i EUR
$currency = $quote->getQuoteCurrencyCode();
if (!in_array($currency, ['PLN', 'EUR'], true)) {
return false;
}
return parent::isAvailable($quote);
}
// Autoryzacja płatności
public function authorize(InfoInterface $payment, $amount): static
{
if (!$this->canAuthorize()) {
throw new LocalizedException(__('Authorize action is not available.'));
}
$order = $payment->getOrder();
$customerId = $order->getCustomerEmail();
try {
$response = $this->gatewayClient->authorize([
'amount' => (int) round($amount * 100), // w groszach
'currency' => $order->getOrderCurrencyCode(),
'customer' => $customerId,
'order_id' => $order->getIncrementId(),
'description' => "Zamówienie #{$order->getIncrementId()}",
]);
if (!$response->isSuccess()) {
throw new LocalizedException(
__('Płatność odrzucona: %1', $response->getErrorMessage())
);
}
// Zapisz token transakcji do późniejszego capture/refund
$payment->setTransactionId($response->getTransactionId());
$payment->setIsTransactionClosed(false);
$payment->setAdditionalInformation('gateway_token', $response->getToken());
} catch (\Exception $e) {
$this->logger->critical($e);
throw new LocalizedException(__('Błąd podczas autoryzacji płatności.'));
}
return $this;
}
// Capture - obciążenie karty po autoryzacji
public function capture(InfoInterface $payment, $amount): static
{
if (!$this->canCapture()) {
throw new LocalizedException(__('Capture action is not available.'));
}
$transactionId = $payment->getParentTransactionId();
$token = $payment->getAdditionalInformation('gateway_token');
try {
$response = $this->gatewayClient->capture([
'transaction_id' => $transactionId,
'token' => $token,
'amount' => (int) round($amount * 100),
]);
if (!$response->isSuccess()) {
throw new LocalizedException(
__('Capture odrzucony: %1', $response->getErrorMessage())
);
}
$payment->setTransactionId($response->getCaptureId());
$payment->setIsTransactionClosed(true);
} catch (\Exception $e) {
$this->logger->critical($e);
throw new LocalizedException(__('Błąd podczas pobierania płatności.'));
}
return $this;
}
// Zwrot płatności
public function refund(InfoInterface $payment, $amount): static
{
$captureId = $payment->getParentTransactionId();
try {
$response = $this->gatewayClient->refund([
'capture_id' => $captureId,
'amount' => (int) round($amount * 100),
'reason' => 'customer_request',
]);
if (!$response->isSuccess()) {
throw new LocalizedException(
__('Zwrot odrzucony: %1', $response->getErrorMessage())
);
}
$payment->setTransactionId($response->getRefundId());
$payment->setIsTransactionClosed(true);
} catch (\Exception $e) {
$this->logger->critical($e);
throw new LocalizedException(__('Błąd podczas zwrotu płatności.'));
}
return $this;
}
}
Konfiguracja – config.xml i system.xml
<!-- etc/config.xml -->
<?xml version="1.0"?>
<config>
<default>
<payment>
<vendor_payment_gateway>
<active>0</active>
<title>Płatność kartą (Vendor Gateway)</title>
<order_status>pending</order_status>
<payment_action>authorize</payment_action>
<allowspecific>0</allowspecific>
<model>Vendor\PaymentGateway\Model\Payment</model>
<api_url>https://api.gateway.example.com/v1</api_url>
<debug>0</debug>
</vendor_payment_gateway>
</payment>
</default>
</config>
<!-- etc/adminhtml/system.xml -->
<?xml version="1.0"?>
<config>
<system>
<section id="payment">
<group id="vendor_payment_gateway" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Vendor Payment Gateway</label>
<field id="active" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1">
<label>Aktywna</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="title" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1">
<label>Tytuł</label>
</field>
<field id="api_key" translate="label" type="obscure" sortOrder="30" showInDefault="1" showInWebsite="1">
<label>Klucz API</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
</field>
<field id="payment_action" translate="label" type="select" sortOrder="40" showInDefault="1" showInWebsite="1">
<label>Akcja płatności</label>
<source_model>Magento\Payment\Model\Source\PaymentAction</source_model>
</field>
<field id="debug" translate="label" type="select" sortOrder="50" showInDefault="1" showInWebsite="1">
<label>Tryb debug</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
</group>
</section>
</system>
</config>
Komponent JavaScript – knockout.js
// view/frontend/web/js/view/payment/method-renderer/gateway-method.js
define([
'Magento_Checkout/js/view/payment/default',
'mage/translate'
], function (Component, $t) {
'use strict';
return Component.extend({
defaults: {
template: 'Vendor_PaymentGateway/payment/gateway-form',
cardNumber: '',
cardExpiry: '',
cardCvv: ''
},
// Kod metody - musi zgadzać się z Payment::CODE
getCode: function () {
return 'vendor_payment_gateway';
},
// Dane do przesłania do Magento przy złożeniu zamówienia
getData: function () {
return {
method: this.getCode(),
additional_data: {
// Nigdy nie przesyłaj surowych danych karty do backendu!
// Tokenizuj przez SDK gateway'a po stronie JS
card_token: this.getCardToken()
}
};
},
getCardToken: function () {
// Tu wywołujesz SDK gateway'a (np. Stripe.js, Przelewy24 SDK)
// które tokenizuje dane karty po stronie klienta
// Backend nigdy nie widzi surowego numeru karty - PCI DSS compliance
return window.gatewayToken || '';
},
validate: function () {
if (!this.getCardToken()) {
this.messageContainer.addErrorMessage({
message: $t('Proszę wprowadzić dane karty.')
});
return false;
}
return true;
}
});
});
Rejestracja renderera w layout XML
<!-- view/frontend/layout/checkout_index_index.xml -->
<?xml version="1.0"?>
<page>
<body>
<referenceBlock name="checkout.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="steps" xsi:type="array">
<item name="children" xsi:type="array">
<item name="billing-step" xsi:type="array">
<item name="children" xsi:type="array">
<item name="payment" xsi:type="array">
<item name="children" xsi:type="array">
<item name="renders" xsi:type="array">
<item name="children" xsi:type="array">
<item name="vendor-payment-gateway" xsi:type="array">
<item name="component" xsi:type="string">Vendor_PaymentGateway/js/view/payment/gateway-methods</item>
<item name="methods" xsi:type="array">
<item name="vendor_payment_gateway" xsi:type="array">
<item name="isBillingAddressRequired" xsi:type="boolean">true</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
</body>
</page>
Najważniejsze zasady przy implementacji płatności
- Nigdy nie przechowuj surowych danych karty – tokenizuj przez SDK gateway’a po stronie JS (PCI DSS)
- Szyfruj klucze API – używaj
Magento\Config\Model\Config\Backend\Encrypteddla pól wrażliwych - Loguj wszystko – szczególnie odpowiedzi z gateway’a, z maskowaniem danych wrażliwych
- Obsługuj wyjątki przez LocalizedException – inne wyjątki mogą ujawniać szczegóły techniczne klientowi
- Zawsze testuj scenariusze negatywne – odrzucona karta, timeout, błąd sieci
Podsumowanie
Własna metoda płatności w Magento 2 to jeden z bardziej złożonych komponentów do zaimplementowania – łączy PHP, XML, JavaScript i zewnętrzne API. Kluczem jest dobre zrozumienie przepływu authorize-capture-refund i świadome podejście do bezpieczeństwa (tokenizacja, szyfrowanie kluczy). Gotowe rozwiązania jak Stripe czy Przelewy24 warto traktować jako wzorzec – ich moduły to świetna lektura przed pisaniem własnego.
