İçeriğe geç
Mert Yılmaz
CVEnglishİletişim
Tüm yazılar

NestJS ile Stripe Checkout ve abonelikler: önemli olan webhook'lar

Yayın

Kısa cevap

Checkout oturumunu sunucuda oluştur, müşteriyi Stripe'a gönder ve gerçeğin kaynağı olarak yönlendirmeyi değil webhook'u kullan. Webhook imzasını ham istek gövdesiyle doğrula, Stripe müşteri ve abonelik kimliklerini sakla, checkout.session.completed, customer.subscription.updated, customer.subscription.deleted ve invoice.payment_failed olaylarını işle ve işleyicileri tekrar çalışsa da zarar vermeyecek şekilde yaz. Qrispy'de NestJS ile Stripe ödeme ve abonelik akışlarını geliştirdim.

Akışın özeti

  1. Kullanıcı bir plan seçer, ön yüz sunucundan bir Checkout oturumu ister.
  2. Sunucu oturumu oluşturur ve Stripe'ın ödeme sayfasının adresini döndürür.
  3. Kullanıcı Stripe'ta öder ve sitene geri yönlendirilir.
  4. Stripe, sunucundaki webhook adresine olayları gönderir ve veritabanını bu olaylara göre güncellersin.

Başarı sayfasına dönülmüş olması ödemenin tamamlandığını kanıtlamaz. Kullanıcı sekmeyi kapatabilir ya da adresi elle yazabilir. Abonelik durumunu yalnızca webhook ile güncelle.

Checkout oturumu oluşturma

const session = await this.stripe.checkout.sessions.create({
  mode: "subscription",
  customer: customerId,
  line_items: [{ price: priceId, quantity: 1 }],
  success_url: `${appUrl}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${appUrl}/billing`,
});

return { url: session.url };

NestJS'te webhook ve imza doğrulama

Stripe imzayı ham istek gövdesi üzerinden hesaplar. Gövde JSON olarak ayrıştırılırsa imza tutmaz. NestJS'te uygulamayı rawBody seçeneğiyle oluştur ve işleyicide ham gövdeyi kullan.

// main.ts
const app = await NestFactory.create(AppModule, { rawBody: true });

// stripe.controller.ts
@Post("webhooks/stripe")
async handle(
  @Req() req: RawBodyRequest<Request>,
  @Headers("stripe-signature") signature: string,
) {
  const event = this.stripe.webhooks.constructEvent(
    req.rawBody as Buffer,
    signature,
    process.env.STRIPE_WEBHOOK_SECRET as string,
  );
  await this.billing.process(event);
  return { received: true };
}

İşlemen gereken olaylar

  • checkout.session.completed: müşteri ve abonelik kimliklerini kullanıcıya bağla.
  • customer.subscription.updated: plan ya da durum değişikliğini kaydet.
  • customer.subscription.deleted: erişimi kapat.
  • invoice.payment_failed: kullanıcıyı bilgilendir ve ödeme yöntemini güncellemesi için yönlendir.

Idempotency ve yerel test

Stripe aynı olayı birden fazla kez gönderebilir. İşlediğin olay kimliklerini sakla ve abonelikleri abonelik kimliğine göre upsert et, böylece tekrar gelen olay hiçbir şeyi bozmaz.

Yerelde Stripe CLI ile olayları kendi makinene yönlendirebilirsin. Test kartı olarak 4242 4242 4242 4242 numarasını kullanabilirsin.

stripe listen --forward-to localhost:3000/webhooks/stripe

Sık sorulanlar

Başarı sayfasına dönülmesi ödemenin tamamlandığını gösterir mi?
Hayır. Kullanıcı o sayfaya hiç dönmeyebilir ya da adresi elle açabilir. Gerçek durumu webhook ile öğren.
Webhook imza doğrulaması neden başarısız oluyor?
Genellikle gövde JSON olarak ayrıştırıldığı için. İmzayı ham gövdeyle doğrulaman gerekir; NestJS'te rawBody seçeneğini aç.
Aynı webhook iki kez gelirse ne olur?
Stripe olayı yeniden gönderebilir. İşlenen olay kimliklerini sakla ve veritabanı güncellemelerini tekrar çalışsa da aynı sonucu verecek şekilde yaz.

Bu konuda çalışıyorum

Yazan: Mert Yılmaz, freelance fullstack geliştirici.