Skip to content

Checkout & Webhooks

payment.checkout.tsx loader'ı ve Stripe checkout.session.completed olaylarının işlenmesi.

Achidemy’de ödeme akışı Stripe Checkout ile tamamlanır; tamamlanan oturumlar webhook ile işlenir. Bu sayfa, payment.checkout.tsx loader’ının rolünü ve Stripe’tan gelen checkout.session.completed sinyallerinin nasıl işlendiğini açıklar.

payment.checkout.tsx — Loader’ın Rolü

Section titled “payment.checkout.tsx — Loader’ın Rolü”

Dosya: app/routes/payment.checkout.tsx

Checkout sayfasına girildiğinde loader şunları yapar:

  1. Kimlik doğrulama: Oturum kontrolü; yoksa /${lang}/login’e yönlendirme.
  2. Sepet / tek ürün: URL’den courseId veya bundleId (veya sepet için birden fazla ürün) alınır; mükerrer satın alma kontrolü (paket veya kurs zaten alınmış mı).
  3. Veritabanı: Sepet (getCart), tek kurs (getCourseById), tek paket (getBundleById) paralel çekilir.
  4. Bölgesel fiyat: getCountryFromRequest(request, env, context?.cloudflare) ile ülke; getRegionalPriceFromRequest(db, priceTierId, request) ile fiyat ve para birimi alınır. Geliştirme için ?test_country=TR ile ülke taklidi yapılabilir.
  5. Stripe Payment Methods: Kullanıcının kayıtlı kartları stripe.paymentMethods.list ile listelenir; varsayılan kart bilgisi de gelir.
  6. Dönüş: Loader, checkout sayfasında kullanılacak veriyi döner: sepet içeriği, toplam tutar, bölgesel fiyat, para birimi sembolü, kayıtlı ödeme yöntemleri, tek kurs/paket bilgisi vb.

Checkout sayfası bu veriyle ödeme formunu ve (gerekirse) Stripe Checkout Session oluşturma / yönlendirme adımını sunar. Session oluşturulurken metadata içine userId, courseId/courseIds, bundleId/bundleIds, affiliateCode, saleType, instructorRef, applied_coupon (uygulanan kupon kodu), coupon_id, coupon_is_instructor_owned vb. eklenir; webhook tarafında bu metadata kullanılır. Kupon sistemi detayı için Kupon Sistemi (Promosyonlar) sayfasına bakın.

Stripe Webhook — checkout.session.completed

Section titled “Stripe Webhook — checkout.session.completed”

Dosya: app/routes/api.stripe.webhook.ts

Stripe, ödeme tamamlandığında checkout.session.completed event’ini webhook URL’ine (örn. /api/stripe/webhook) POST eder.

  • Gelen gövde ve Stripe-Signature header’ı, STRIPE_WEBHOOK_SECRET ile doğrulanır: stripe.webhooks.constructEventAsync(body, sig, webhookSecret).
  • Doğrulama başarısızsa istek reddedilir; böylece sahte ödeme bildirimi engellenir.
  1. metadata: userId, courseId / courseIds, bundleId / bundleIds, saleType, affiliateCode, applied_coupon (kupon kodu) vb. okunur.
  2. B2B (Company) Branch: Eğer session.metadata.type === "b2b_subscription" ve organizationId varsa, event kurumsal abonelik olarak ele alınır. Bu durumda:
    • organizations.stripeCustomerId ve organizations.stripeSubscriptionId güncellenir
    • organizations.isActive = true yapılarak demo/soft-lock kaldırılır
    • Sonrasında handler erken döner (B2C enrollment/earnings akışına girmez)
  3. Kupon: Varsa applied_coupon normalize edilir; enrollment ve earnings kayıtlarına couponCode alanı yazılır. Ödeme başarıyla tamamlandıktan sonra coupons.used_count ilgili kupon için +1 artırılır (max_uses aşımı engellenir).
  4. Mod kontrolü:
    • subscription: Abonelik satın alma; kullanıcı abone yapılır (abonelik güncelleme fonksiyonu çağrılır).
    • payment: Tek seferlik ödeme (kurs veya sepet).
  5. Payment modunda:
    • Idempotency: Aynı session.id için daha önce enrollment oluşturulmuşsa tekrar oluşturulmaz.
    • Enrollment: Satın alınan her kurs için enrollments tablosuna kayıt: userId, courseId, bundleId (paket satışıysa), stripeCheckoutSessionId, stripePaymentIntentId, paidAmountMinor (session.amount_total — Stripe’ın tahsil ettiği gerçek tutar, minor birim), paidCurrency (session.currency — try/eur/usd), couponCode (metadata.applied_coupon varsa).
    • Komisyon motoru: handleCourseSale (veya paket için handleBundlePurchase) çağrılır. Earnings tablosuna yazılan değerler bölgesel fiyatlandırma ile uyumludur:
      • totalPrice: session.amount_total (ödenen brüt tutar, cent/kuruş).
      • instructorShare, platformShare, affiliateShare: Aynı tutar üzerinden oranlarla hesaplanır (minor birim).
      • currency: session.currency (try, eur, usd).
      • rateAtSale: Satış anındaki kur — 1 USD = X birim (örn. TRY için ~44). USD ise 1; diğer para birimlerinde getCachedRates(env).rates[currency] ile alınır. Raporlamada (admin/eğitmen paneli) bu kur ile minor tutar USD’ye çevrilir.
    • Böylece hem enrollments hem earnings gerçek ödeme tutarı ve para birimi ile kaydedilir; admin ve eğitmen panellerinde satışın yapıldığı döviz ve kur üzerinden doğru gösterim yapılır. Detay için Döviz Kuru API ve Döviz Gösterimi ve İade sayfalarına bakın.
  6. Fatura / e-posta: Satın alma makbuzu/fatura e-postası sendPurchaseReceiptEmailWithInvoice(..., locale) ile gönderilir; dil session.locale (örn. en-USen) ile belirlenir. İşlemler Cloudflare Workers ctx.waitUntil ile arka planda çalıştırılır.

Böylece checkout.session.completed tek bir event ile hem kayıt (enrollment) hem kazanç dağılımı (earnings) hem de (varsa) abonelik güncellemesi ve bildirimler yapılır.

Tek kurs – iade sonrası yeniden satın alma

Section titled “Tek kurs – iade sonrası yeniden satın alma”

Aynı kullanıcı aynı kursu iade ettikten sonra tekrar satın alırsa, enrollments tablosunda (user_id, course_id) için zaten bir satır vardır (unique constraint: user_course_unique). Bu durumda yeni INSERT yapılmaz; mevcut iade edilmiş enrollment satırı UPDATE edilir: refundedAt / refundStatus temizlenir, stripeCheckoutSessionId, stripePaymentIntentId, paidAmountMinor, paidCurrency, enrolledAt yeni ödemeye göre güncellenir. Böylece webhook 500 (duplicate key) hatası oluşmaz.

Kurs paketi (bundle) – iade sonrası yeniden satın alma

Section titled “Kurs paketi (bundle) – iade sonrası yeniden satın alma”

Aynı mantık paket için de uygulanır: Paket iade edildikten sonra aynı paket tekrar satın alındığında, her paket kursu için (user_id, course_id) zaten iade edilmiş satır vardır. Webhook bu satırları UPDATE eder (INSERT değil); böylece öğrenci “Öğren” ekranında paketteki kursları tekrar görür. Paket iadesi ise charge.refunded / payment_intent.refunded event’lerinde metadata’daki bundle_id / bundleId ile işlenir. Detay için Döviz Gösterimi ve İade Akışları sayfasına bakın.

AşamaAçıklama
1Kullanıcı checkout sayfasına girer → loader sepeti, fiyatı (bölgesel), ödeme yöntemlerini hazırlar.
2Kullanıcı ödemeyi tamamlar → Stripe Checkout Session tamamlanır.
3Stripe checkout.session.completed webhook’u gönderir → api.stripe.webhook imzayı doğrular.
4Enrollment’lar oluşturulur; komisyon motoru earnings kayıtlarını yazar; abonelik/fatura/e-posta işlemleri yapılır.

Diğer event’ler (örn. payment_intent.succeeded, abonelik iptali) de aynı webhook dosyasında idempotency ve enrollment/earnings kurallarına uygun şekilde işlenir; ana “ödeme tamamlandı” sinyali checkout.session.completed’dır.

Sepette uygulanan kupon kodu checkout metadata’da applied_coupon olarak Stripe’a gider. Webhook’ta:

  • Enrollment ve earnings kayıtlarına couponCode yazılır (iade/raporlama için).
  • Ödeme başarılı olduktan sonra coupons tablosunda ilgili kod için used_count +1 artırılır.

Detaylı kupon akışı, eğitmen UI ve coupon-engine için Kupon Sistemi (Promosyonlar) sayfasına bakın.

  • app/routes/api.stripe.webhook.ts — Stripe webhook; checkout.session.completed ve diğer event’lerin işlenmesi; applied_coupon ve coupons.used_count.
  • app/routes/payment.checkout.tsx — Checkout sayfası loader ve Stripe Session oluşturma.
  • app/routes/cart.tsx — Sepet sayfası; kupon girişi ve indirimli fiyat.
  • app/lib/coupon-engine.ts — Kupon doğrulama ve indirimli fiyat hesaplama.
  • app/lib/stripe-connect-payout.ts — Connect transfer (admin onayı sonrası processPayoutTransfer).
  • app/lib/payout-engine.ts — Payout talebi ve earnings mantığı.

Stripe geliştirmeleri ve daha fazla bilgi için: Stripe Dokümantasyonu