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()?
2. Why verify webhook signatures?
3. What is the purpose of gateway.xml?
Flashcards
Question
What is authorize/capture flow?
Click to reveal answer
Answer
Authorize holds funds, capture collects them
Question
How to verify Stripe webhooks?
Click to reveal answer
Answer
Use HMAC-SHA256 signature verification
Question
What is gateway.xml?
Click to reveal answer
Answer
Maps gateway actions to handler classes
Question
What is a payment token?
Click to reveal answer
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