Skip to content
advanced Phase 87 · Integration Patterns

Payment Gateway Integration

Payment gateway integration - Stripe, PayPal, custom gateways, webhook handling

45m
0 problems
Topic Progress 0%

Payment Gateway Architecture

Gateway Abstraction Layer

┌─────────────────┐
│   Magento 2     │
│   Payment       │
│   Methods       │
└────────┬────────┘
         │
┌────────▼────────┐
│  Gateway        │
│  Aggregator     │
│  Interface      │
└────────┬────────┘
    ┌────┴────┬──────────┬──────────┐
    â–¼         â–¼          â–¼          â–¼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│Stripe │ │PayPal │ │Custom │ │Braintree│
└───────┘ └───────┘ └───────┘ └───────┘

Gateway Interface

namespace Vendor\Payment\Gateway;

interface GatewayInterface
{
    /**
     * Authorize payment
     */
    public function authorize(AuthorizeRequest $request): PaymentResponse;

    /**
     * Capture authorized payment
     */
    public function capture(CaptureRequest $request): PaymentResponse;

    /**
     * Void/cancel authorization
     */
    public function void(VoidRequest $request): PaymentResponse;

    /**
     * Process refund
     */
    public function refund(RefundRequest $request): PaymentResponse;
}

Stripe Adapter

namespace Vendor\Payment\Gateway\Stripe;

class StripeGateway implements GatewayInterface
{
    private StripeClient $stripeClient;
    private LoggerInterface $logger;

    public function authorize(AuthorizeRequest $request): PaymentResponse
    {
        try {
            $charge = $this->stripeClient->charges->create([
                'amount' => (int) ($request->getAmount() * 100),
                'currency' => strtolower($request->getCurrency()),
                'source' => $request->getToken(),
                'capture' => false,
                'metadata' => [
                    'order_id' => $request->getOrderId(),
                    'store_id' => $request->getStoreId(),
                ],
            ]);

            return new PaymentResponse([
                'success' => true,
                'transaction_id' => $charge->id,
                'amount' => $request->getAmount(),
                'status' => $charge->status,
            ]);
        } catch (StripeException $e) {
            $this->logger->error('Stripe auth failed', [
                'error' => $e->getMessage(),
                'order_id' => $request->getOrderId(),
            ]);
            return new PaymentResponse([
                'success' => false,
                'error' => $e->getMessage(),
            ]);
        }
    }

    public function capture(CaptureRequest $request): PaymentResponse
    {
        $charge = $this->stripeClient->charges->capture(
            $request->getTransactionId(),
            ['amount' => (int) ($request->getAmount() * 100)]
        );

        return new PaymentResponse([
            'success' => true,
            'transaction_id' => $charge->id,
        ]);
    }
}

PayPal Integration

PayPal Integration

namespace Vendor\Payment\Gateway\PayPal;

class PayPalGateway implements GatewayInterface
{
    private PayPalClient $paypalClient;

    public function authorize(AuthorizeRequest $request): PaymentResponse
    {
        // Step 1: Create order
        $order = $this->paypalClient->createOrder([
            'intent' => 'AUTHORIZE',
            'purchase_units' => [[
                'amount' => [
                    'currency_code' => $request->getCurrency(),
                    'value' => number_format($request->getAmount(), 2, '.', ''),
                ],
                'reference_id' => $request->getOrderId(),
            ]],
            'application_context' => [
                'return_url' => $request->getReturnUrl(),
                'cancel_url' => $request->getCancelUrl(),
            ],
        ]);

        // Step 2: Get approval URL
        $approvalUrl = collect($order['links'])->firstWhere('rel', 'approve')['href'];

        return new PaymentResponse([
            'success' => true,
            'transaction_id' => $order['id'],
            'approval_url' => $approvalUrl,
            'requires_redirect' => true,
        ]);
    }

    public function capture(CaptureRequest $request): PaymentResponse
    {
        $result = $this->paypalClient->captureOrder(
            $request->getTransactionId()
        );

        return new PaymentResponse([
            'success' => $result['status'] === 'COMPLETED',
            'transaction_id' => $result['id'],
        ]);
    }
}

Webhook Handling

Payment Webhook Controller

namespace Vendor\Payment\Controller\Webhook;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\Controller\Result\JsonFactory;

class Stripe extends Action
{
    private WebhookVerifierInterface $verifier;
    private WebhookProcessorInterface $processor;
    private JsonFactory $jsonFactory;

    public function __construct(
        Context $context,
        WebhookVerifierInterface $verifier,
        WebhookProcessorInterface $processor,
        JsonFactory $jsonFactory
    ) {
        parent::__construct($context);
        $this->verifier = $verifier;
        $this->processor = $processor;
        $this->jsonFactory = $jsonFactory;
    }

    public function execute()
    {
        $payload = file_get_contents('php://input');
        $signature = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';

        // Verify signature
        if (!$this->verifier->verify($payload, $signature)) {
            $result = $this->jsonFactory->create()->setHttpResponseCode(401);
            $result->setData(['error' => 'Invalid signature']);
            return $result;
        }

        $event = json_decode($payload, true);

        // Process asynchronously
        $this->processor->process($event);

        $result = $this->jsonFactory->create()->setHttpResponseCode(200);
        $result->setData(['received' => true]);
        return $result;
    }
}

Webhook Signature Verification

namespace Vendor\Payment\Gateway\Webhook;

class StripeWebhookVerifier implements WebhookVerifierInterface
{
    private string $secret;

    public function verify(string $payload, string $signatureHeader): bool
    {
        $elements = [];
        foreach (explode(',', $signatureHeader) as $pair) {
            [$key, $value] = explode('=', $pair, 2);
            $elements[$key] = $value;
        }

        $timestamp = $elements['t'] ?? '';
        $expectedSig = $elements['v1'] ?? '';

        $signedPayload = $timestamp . '.' . $payload;
        $computedSig = hash_hmac('sha256', $signedPayload, $this->secret);

        return hash_equals($expectedSig, $computedSig);
    }
}

Gateway Configuration

gateway.xml Configuration

<!-- app/code/Vendor/Payment/etc/gateway.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Payment:etc/gateway.xsd">

    <gateway name="stripe">
        <title>Stripe</title>
        <model>Vendor\Payment\Gateway\Stripe\StripeGateway</model>
        <actions>
            <action name="authorize">
                <handler class="Vendor\Payment\Gateway\Handler\AuthorizeHandler"/>
            </action>
            <action name="capture">
                <handler class="Vendor\Payment\Gateway\Handler\CaptureHandler"/>
            </action>
            <action name="void">
                <handler class="Vendor\Payment\Gateway\Handler\VoidHandler"/>
            </action>
        </actions>
    </gateway>
</config>

Gateway Config Provider

namespace Vendor\Payment\Model\ConfigProvider;

class StripeConfigProvider implements ConfigProviderInterface
{
    public function getConfig(): array
    {
        return [
            'payment' => [
                'stripe' => [
                    'title' => $this->scopeConfig->getValue('payment/stripe/title'),
                    'isActive' => $this->scopeConfig->isFlagSet('payment/stripe/active'),
                    'publishableKey' => $this->scopeConfig->getValue('payment/stripe/publishable_key'),
                    'paymentAction' => $this->scopeConfig->getValue('payment/stripe/payment_action'),
                ],
            ],
        ];
    }
}

Quiz

1. What does capture() do after authorize()?

Question 1 options

2. Why verify webhook signatures?

Question 2 options

3. What is the purpose of gateway.xml?

Question 3 options

Flashcards

Question

What is authorize/capture flow?

Answer

Authorize holds funds, capture collects them

Question

How to verify Stripe webhooks?

Answer

Use HMAC-SHA256 signature verification

Question

What is gateway.xml?

Answer

Maps gateway actions to handler classes

Question

What is a payment token?

Answer

A secure reference to payment method data (never raw card numbers)

Revision Notes

Key Takeaways

  • 1. Gateway abstraction enables multiple payment provider support
  • 2. Authorize holds funds, capture collects them later
  • 3. Webhook signatures must be verified to prevent spoofing
  • 4. Gateway configuration maps actions to handler classes
  • 5. PCI compliance requires tokenization, never storing card data

Interview Tips

  • Explain the difference between authorize and capture
  • Describe how webhook verification prevents fraud
  • Discuss the payment gateway abstraction pattern
  • Talk about handling payment failures gracefully

Cheat Sheet

Payment Gateway:
  authorize() → hold funds
  capture()   → collect funds
  void()      → cancel hold
  refund()    → return funds

Webhook Security:
  Verify signature (HMAC-SHA256)
  Validate timestamp (prevent replay)
  Process asynchronously

Gateway Config:
  gateway.xml → action→handler mapping
  di.xml → gateway class registration