Ödeme Entegrasyonu Sadece Bir API Çağrısı Değildir
Her geliştiricinin ilk ödeme entegrasyonu aynı görünür: API'yi çağır, yanıtı kontrol et, işlemi kaydet. Yayınla.
Sonra gerçek dünya devreye girer -- ağ zaman aşımları, mükerrer ödemeler, webhook teslim hataları, para birimi yuvarlama hataları, kısmi iadeler ve "Öde" düğmesine on yedi kez tıklayan o müşteri.
Bu rehber, kırılgan bir ödeme entegrasyonunu üretime hazır bir entegrasyondan ayıran desenleri kapsar.
Strategy Pattern: Birden Fazla Ödeme Sağlayıcısı Desteği
Çoğu uygulama eninde sonunda birden fazla ödeme sağlayıcısını desteklemek zorunda kalır. Strategy pattern bunu temiz bir şekilde çözer:
interface PaymentGateway
{
public function charge(Money $amount, PaymentMethod $method): PaymentResult;
public function refund(string $transactionId, ?Money $amount = null): RefundResult;
public function supportsMethod(string $method): bool;
}
class StripeGateway implements PaymentGateway
{
public function __construct(private readonly StripeClient $client) {}
public function charge(Money $amount, PaymentMethod $method): PaymentResult
{
try {
$intent = $this->client->paymentIntents->create([
'amount' => $amount->inCents(),
'currency' => strtolower($amount->currency),
'payment_method' => $method->token,
'confirm' => true,
'idempotency_key' => $method->idempotencyKey,
]);
return PaymentResult::success(
transactionId: $intent->id,
amount: $amount,
);
} catch (CardException $e) {
return PaymentResult::failed($e->getMessage(), $e->getDeclineCode());
}
}
// ...
}
class PayPalGateway implements PaymentGateway
{
// Same interface, different implementation
}
// Resolution
class PaymentGatewayFactory
{
public function make(string $provider): PaymentGateway
{
return match($provider) {
'stripe' => app(StripeGateway::class),
'paypal' => app(PayPalGateway::class),
default => throw new UnsupportedGatewayException($provider),
};
}
}
Idempotency: Mükerrer Ödemeleri Önleme
Bu, ödeme işlemlerindeki en önemli desendir. Kullanıcı çift tıklarsa, sunucunuz zaman aşımı sonrası yeniden denerse, bir webhook iki kez tetiklenirse -- müşteriden yalnızca bir kez ücret alınmalıdır.
class ProcessPayment
{
public function execute(PaymentRequest $request): PaymentResult
{
// Generate a deterministic idempotency key
$idempotencyKey = $this->generateKey($request);
// Check if we've already processed this payment
$existing = Payment::where('idempotency_key', $idempotencyKey)->first();
if ($existing) {
return PaymentResult::fromExisting($existing);
}
// Create a pending record BEFORE calling the gateway
$payment = Payment::create([
'idempotency_key' => $idempotencyKey,
'amount' => $request->amount,
'status' => PaymentStatus::Pending,
'gateway' => $request->gateway,
]);
try {
$result = $this->gateway->charge($request->amount, $request->method);
$payment->update([
'status' => $result->success ? PaymentStatus::Completed : PaymentStatus::Failed,
'transaction_id' => $result->transactionId,
'gateway_response' => $result->rawResponse,
]);
return $result;
} catch (Throwable $e) {
$payment->update(['status' => PaymentStatus::Error, 'error' => $e->getMessage()]);
throw $e;
}
}
private function generateKey(PaymentRequest $request): string
{
return hash('sha256', implode('|', [
$request->orderId,
$request->amount->inCents(),
$request->amount->currency,
$request->userId,
]));
}
}
Webhook Yönetimi: Ödeme Durumunun Omurgası
İstemci tarafındaki ödeme onayına asla tek başına güvenmeyin. Webhook'lar doğruluğun kaynağıdır.
class StripeWebhookController extends Controller
{
public function handle(Request $request): Response
{
// 1. Verify the webhook signature
try {
$event = Webhook::constructEvent(
$request->getContent(),
$request->header('Stripe-Signature'),
config('services.stripe.webhook_secret')
);
} catch (SignatureVerificationException $e) {
return response('Invalid signature', 400);
}
// 2. Check for duplicate delivery
if (WebhookLog::where('event_id', $event->id)->exists()) {
return response('Already processed', 200);
}
// 3. Log before processing
WebhookLog::create([
'event_id' => $event->id,
'type' => $event->type,
'payload' => $event->toArray(),
]);
// 4. Dispatch to handler
match($event->type) {
'payment_intent.succeeded' => $this->handlePaymentSuccess($event),
'payment_intent.payment_failed' => $this->handlePaymentFailure($event),
'charge.refunded' => $this->handleRefund($event),
default => null, // Ignore unhandled events
};
// 5. Always return 200 to prevent retries
return response('OK', 200);
}
}
Hata Kurtarma Desenleri
Ödemeler ilginç şekillerde başarısız olabilir. En yaygın senaryoları nasıl ele alacağınız:
- Ödeme ağ geçidi zaman aşımı: Körü körüne yeniden denemeyin. Önce ödeme durumunu kontrol edin, ardından yalnızca orijinal ücretlendirme gerçekleşmediyse yeniden deneyin
- Kısmi başarısızlık: Ücretlendirme başarılı olup veritabanı yazımı başarısız olursa, webhook durumu uzlaştıracaktır
- Reddedilen kartlar: Red kodlarından eşleştirilmiş kullanıcı dostu mesajlar döndürün
- Para birimi uyumsuzluğu: Para birimini giriş katmanında doğrulayın, asla ödeme ağ geçidi katmanında değil
Ödeme Entegrasyonlarını Test Etme
class ProcessPaymentTest extends TestCase
{
public function test_idempotent_payment_does_not_double_charge(): void
{
$gateway = Mockery::mock(PaymentGateway::class);
$gateway->shouldReceive('charge')
->once() // Critical: should only be called ONCE
->andReturn(PaymentResult::success('txn_123', new Money(1000)));
$processor = new ProcessPayment($gateway);
$request = new PaymentRequest(orderId: 1, amount: new Money(1000));
$result1 = $processor->execute($request);
$result2 = $processor->execute($request);
$this->assertTrue($result1->success);
$this->assertTrue($result2->success);
$this->assertEquals(1, Payment::count()); // Only one record
}
}
Yayına Almadan Önce Kontrol Listesi
- Webhook imza doğrulaması etkinleştirildi
- Tüm ücretlendirmeler için idempotency key'ler oluşturuluyor
- Tüm parasal tutarlar ondalık sayı değil, tam sayı cent olarak kullanılıyor
- Webhook endpoint'i işlemeden önce 200 döndürüyor (kuyruklanmış job'lar kullanın)
- Başarısız webhook teslimatları izleniyor ve uyarılar yapılandırılmış
- PCI uyumluluk gereksinimleri anlaşılmış (barındırılan ödeme formları kullanın)
- İade akışları uçtan uca test edildi