Skip to content

Döviz Gösterimi ve İade Akışları

Satın alma geçmişi para birimi, iade eşlemesi (earnings), admin iade listesi ve affiliate bölgesel bakiye.

Bu sayfa, bölgesel fiyatlandırma ile uyumlu olarak satın alma geçmişinde tutar/para birimi gösterimi, iade sonrası doğru earnings eşlemesi, admin iade listesinde döviz kullanımı ve affiliate bakiye/ödeme talebinin yerel para biriminde gösterilmesini açıklar.

Satın Alma Geçmişi – Ödenen Tutar ve Para Birimi

Section titled “Satın Alma Geçmişi – Ödenen Tutar ve Para Birimi”

Dosya: app/routes/account.purchase-history.tsx
URL: /:lang/account/purchase-history

Öğrenci hangi para birimi ile ödediyse (€, ₺, $) satın alma kartında aynı para birimi ve tutar gösterilir. Böylece bölgesel fiyatlandırma ile tutarlı bir deneyim sağlanır.

enrollments — Öğrenci tarafı satış kaydı:

AlanAçıklama
paid_amount_minorÖdenen tutar (cent/kuruş cinsinden integer).
paid_currencyÖdeme para birimi (örn. try, eur, usd).

earnings — Eğitmen/platform kazanç satırı (admin ve eğitmen panelleri bu tabloyu okur):

AlanAçıklama
total_priceSatışın brüt tutarı (minor); webhook’ta session.amount_total ile aynı olmalı (bölgesel fiyatlandırma ile gerçek ödeme).
currencySatış para birimi (try, eur, usd).
rate_at_saleSatış anındaki kur: 1 USD = X birim; raporlamada USD’ye çevrim için kullanılır.

Bu alanlar kayıt oluşturulurken doldurulur:

  • Stripe Checkout (webhook): session.amount_totalpaidAmountMinor (enrollments) ve totalPrice (earnings), session.currencypaidCurrency / currency. rateAtSale ise getCachedRates ile satış anı kurundan alınır.
  • Stripe Payment Intent (webhook veya GraphQL buyWithSavedCard): paymentIntent.amount / bölgesel fiyat → paidAmountMinor, paymentIntent.currency / finalCurrencypaidCurrency.
  • Loader, her enrollment için paidAmountMinor ve paidCurrency okur; her satın alma kartına currency ve amountMinor (yoksa kurs/bundle fiyatı + usd fallback) geçer.
  • UI’da formatPriceWithCurrency(amountMinor, currency) (app/lib/pricing.ts) kullanılır: para birimi sembolü (₺, €, $) ve locale’e uygun sayı formatı (tr-TR, de-DE, en-US).

Sonuç: Avrupa’da EUR, Türkiye’de TRY, ABD’de USD ile yapılan ödemeler satın alma geçmişinde doğru döviz ve tutarla görünür.

İade Talebi – Stripe, Earnings ve İlerleme Kalkanı

Section titled “İade Talebi – Stripe, Earnings ve İlerleme Kalkanı”

Dosya: app/graphql/schema.tsrequestRefund mutation

  • Stripe iade: stripe.refunds.create({ payment_intent, amount: paymentIntent.amount }) — öğrencinin ödediği tutar ve para birimi ile iade yapılır (Stripe otomatik aynı dövizi kullanır).
  • Earnings eşlemesi: İade sonrası ilgili earnings kayıtları (eğitmen payı, platform payı, varsa affiliate payı) status: 'refunded' yapılır. Eşleme tek satışı hedefleyecek şekilde yapılır:
    • stripeCheckoutSessionId = enrollment.stripeCheckoutSessionId veya enrollment.stripePaymentIntentId (Payment Intent ile satışta earnings’te stripeCheckoutSessionId alanına PI id yazılır).
    • courseId = enrollment.courseId.
  • İlerleme suistimali kalkanı: İade öncesi enrollments.maxProgressPercentage ve sertifika durumu kontrol edilir; öğrenci kursun %25’inden fazlasına ulaştıysa veya sertifika aldıysa otomatik iade engellenir ve hata mesajı döner. Bu alan, toggleLessonCompletion ve learn.$slug.tsx içindeki currentProgress değeriyle sadece yukarı yönlü güncellenir.

İade e-postası: Tutar, formatPriceWithCurrency(paymentIntent.amount, paymentIntent.currency) ile öğrencinin ödediği para biriminde formatlanır. E-posta gönderimi requestRefund resolver’ında Cloudflare Workers ctx.waitUntil ile arka planda tetiklenir; API yanıtı e-posta beklemeden döner.

Admin İade Listesi – Döviz Kullanımı

Section titled “Admin İade Listesi – Döviz Kullanımı”

Dosya: app/routes/admin.refunds.tsx
URL: /admin/refunds

  • Toplam İade Tutarı (kart): Tüm iadelerin USD karşılığı toplanır. Admin dashboard’daki gelir mantığı ile uyumlu: satış anındaki kur (earnings.rateAtSale) varsa kullanılır, yoksa getCachedRates(env) ile güncel kur kullanılır (convertMinorToUsd).
  • İade kartları (her satır): İade edilen kursun orijinal para biriminde tutar gösterilir: formatPriceWithCurrency(refund.paidAmountMinor, refund.paidCurrency). Örn. TRY ile alınıp iade edildiyse ₺, EUR ise €, USD ise $.

Loader’da enrollments.paidAmountMinor, enrollments.paidCurrency ve refunded earnings (rateAtSale) kullanılır; toplam USD hesaplanır, kartlarda orijinal döviz formatlanır.

Dosya: app/routes/admin._index.tsx

Satışların döviz dağılımı tablosunda:

  • Platform net (USD) sütunu: Platformun o para birimindeki satışlardan elde ettiği payın USD karşılığı (direkt satışlarda %5, organikte %55 vb.). Önceden “Net (USD)” eğitmen payını (~%95) gösteriyordu; artık platform net geliri gösteriliyor.

Webhook – Tek Kurs İadesi Sonrası Yeniden Satın Alma

Section titled “Webhook – Tek Kurs İadesi Sonrası Yeniden Satın Alma”

Dosya: app/routes/api.stripe.webhook.tscheckout.session.completed, tek kurs yolu (courseId && userId)

enrollments tablosunda (user_id, course_id) için tek satır olabilir (unique constraint: user_course_unique). Kullanıcı bir kursu iade ettikten sonra aynı kursu tekrar satın alırsa:

  • INSERT yapılmaz (aynı (userId, courseId) ikinci kez eklenemez).
  • 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 hatası (duplicate key) oluşmaz ve öğrenci tekrar kursa kayıtlı olur.

Webhook – Kurs Paketi (Bundle) İade ve Yeniden Satın Alma

Section titled “Webhook – Kurs Paketi (Bundle) İade ve Yeniden Satın Alma”

Dosya: app/routes/api.stripe.webhook.tscharge.refunded / payment_intent.refunded (paket iadesi), checkout.session.completed (paket yeniden satın alma)

  • İade event’inde metadata üzerinden bundle_id veya bundleId (camelCase) okunur; session / payment intent / charge metadata’da her iki anahtar da desteklenir.
  • bundleId varsa paket iadesi yapılır: ilgili kullanıcının bu paketten tüm enrollment’ları refundedAt ve refundStatus = 'completed' ile güncellenir; aynı satışa ait earnings kayıtları status: 'refunded' yapılır.
  • İade, satın alınan dövizde (Stripe üzerinden) yapılır; earnings eşlemesi stripeCheckoutSessionId ve bundleId ile tek satışı hedefler.
  • enrollments tablosunda her kurs için (user_id, course_id) tek satırdır (user_course_unique). Paket iade edildikten sonra aynı paket tekrar satın alındığında yeni INSERT unique ihlali verir.
  • Webhook (checkout.session.completed): Önce bu kullanıcı + paket için iade edilmiş enrollment’lar sorgulanır. Her paket kursu için:
    • İade edilmiş enrollment varsa UPDATE yapılır: refundedAt / refundStatus temizlenir, stripeCheckoutSessionId, stripePaymentIntentId, paidAmountMinor, paidCurrency yeni ödemeye göre güncellenir.
    • Yoksa (örn. pakete yeni kurs eklenmişse) INSERT ile yeni kayıt eklenir.
  • Böylece ödeme Stripe’a aktarılır ve öğrenci “Öğren” ekranında paketteki kursları tekrar görür.

Affiliate Bakiye ve Ödeme Talebi – Konum Bazlı Dinamik Döviz

Section titled “Affiliate Bakiye ve Ödeme Talebi – Konum Bazlı Dinamik Döviz”

Dosya: app/routes/account.profile.tsx (Affiliate bölümü), app/routes/account.tsx (layout), app/graphql/schema.ts (myAffiliateBalance, myAffiliateEarningsTotal, requestAffiliatePayout)

Affiliate gelirleri, admin ve eğitmen panelindeki döviz sistemiyle uyumlu çalışır: satışlar hangi para biriminde yapılırsa yapılsın (₺, €, $), kazançlar satış anındaki kur ile USD’ye çevrilerek toplanır; ekranda ise affiliate üyesinin konumuna göre (ülke → para birimi) yerel para biriminde (₺, €, $) gösterilir. Minimum çekim 100 USD veya eşdeğer döviz (eğitmen ödeme talebi ile aynı mantık) uygulanır.

Backend: Kazançların satış para biriminde saklanması ve USD’ye çevrimi

Section titled “Backend: Kazançların satış para biriminde saklanması ve USD’ye çevrimi”
  • Webhook: Her satışta affiliate komisyonu satış para biriminde (cent) earnings.affiliateShare olarak yazılır; earnings.currency (try/eur/usd) ve satış anı kuru earnings.rateAtSale (1 USD = X birim) birlikte saklanır.
  • GraphQL – myAffiliateEarningsTotal / myAffiliateBalance: Toplam bakiye tek bir sum(affiliateShare) ile değil, satır bazında hesaplanır: her affiliate_commission satırı için (affiliateShare/100) / (rateAtSale || 1) ile tutar USD’ye çevrilir ve toplanır. Böylece TRY, EUR ve USD ile yapılan satışlardan gelen komisyonlar doğru USD toplamına dönüşür (örn. 750₺ satış anı kurundan ~17,44 USD; 4,5$ = 4,5 USD).
  • requestAffiliatePayout: Çekilebilir bakiye kontrolünde aynı mantık kullanılır: kazanç satırları rateAtSale ile USD’ye çevrilip toplanır; bekleyen ve ödenen talepler (zaten USD) düşülerek availableBalance (USD) hesaplanır. Talep tutarı API’ye USD olarak gönderilir.

Dosya: app/routes/account.tsx

  • Loader’da getCachedRates(env) çağrılır; user.country kullanıcı kaydından döndürülür.
  • Dönüş: exchangeRates: { rates, updatedAt } ve user: { ..., country }; <Outlet context={{ user, bunnyConfig, exchangeRates }} /> ile profile sayfasına iletilir.
  • getDisplayCurrencyFromCountry(user?.country) ile gösterim para birimi: TR → TRY, Euro bölgesi → EUR, diğer → USD.
  • formatInDisplayCurrency(amountUsd): convertUsdToCurrency(amountUsd, displayCurrency, rates) + sembol + locale (tr-TR / de-DE / en-US).
  • Toplam kazanç, çekilebilir bakiye, bekleyen talep: Hepsi formatInDisplayCurrency(...) ile yerel para biriminde gösterilir (GraphQL’den gelen tüm tutarlar USD’dir; anlık kur ile kullanıcı para birimine çevrilir).
  • Ödeme geçmişi listesi: Her talep tutarı (payout.amount, USD) formatInDisplayCurrency(parseFloat(payout.amount)) ile kullanıcının para biriminde (₺/€/$) gösterilir.

Ödeme talebi – Min 100 USD veya eşdeğer

Section titled “Ödeme talebi – Min 100 USD veya eşdeğer”
  • Minimum tutar: 100 USD (minPayoutUsd = 100). Form, affiliateBalance.availableBalance >= 100 ise gösterilir.
  • Girdi: Kullanıcı kendi para biriminde tutar girer (placeholder ve min, o para biriminde; örn. TR’de ~₺3.450).
  • Gönderim: Girilen tutar USD’ye çevrilir: amountUsd = displayCurrency === "USD" ? num : num / (rates[displayCurrency] ?? 1); API’ye USD gönderilir. Backend requestAffiliatePayout mutation’ında minimum 100 USD kontrolü yapar.

GraphQL: requestAffiliatePayout(amount: Float!)amount < 100 ise hata: “Affiliate ödeme talebi için minimum tutar 100$ (veya eşdeğer ₺/€) olmalıdır.”

KonuAçıklama
Satın alma geçmişienrollments.paidAmountMinor / paidCurrency; formatPriceWithCurrency; bölgesel döviz ile tutar.
İade (Stripe)paymentIntent.amount ile aynı dövizde iade.
İade (earnings)stripeCheckoutSessionId / PI id + courseId ile tek satışı hedefle; earnings status = refunded.
Admin iade listesiToplam: USD (rate at sale veya güncel kur). Kartlar: orijinal döviz (formatPriceWithCurrency).
Admin dashboard tablo”Platform net (USD)” = platform payının $ karşılığı.
Webhook tek kursİade sonrası yeniden satın almada INSERT yerine UPDATE enrollment (user_course_unique).
Webhook paketİade: metadata bundle_id/bundleId ile tüm paket enrollment’ları refunded; earnings refunded. Yeniden satın alma: iade edilmiş enrollment’ları UPDATE, yoksa INSERT.
Affiliate bakiyeKazançlar satış para biriminde + rateAtSale ile saklanır; GraphQL her satırı USD’ye çevirip toplar; profile’da getDisplayCurrencyFromCountry + formatInDisplayCurrency (TRY/€/$).
Affiliate min çekim100 USD veya eşdeğer; UI’da yerel para birimi ile girdi, API’ye USD.
  • app/routes/account.purchase-history.tsx — Satın alma geçmişi loader ve formatPriceWithCurrency.
  • app/lib/pricing.ts — getCurrencySymbol, formatPriceWithCurrency.
  • app/graphql/schema.ts — requestRefund (earnings eşlemesi), myAffiliateBalance / myAffiliateEarningsTotal (satış para biriminden USD’ye çevrim), requestAffiliatePayout (min 100).
  • app/routes/api.stripe.webhook.ts — Enrollment paidAmountMinor/paidCurrency; tek kurs UPDATE; paket iade (charge.refunded / payment_intent.refunded) ve paket yeniden satın alma (checkout.session.completed’da iade edilmiş enrollment UPDATE).
  • app/routes/admin.refunds.tsx — Toplam USD, kartlarda orijinal döviz.
  • app/routes/admin._index.tsx — Platform net (USD) sütunu.
  • app/routes/account.tsx — exchangeRates, user.country.
  • app/routes/account.profile.tsx — Affiliate bölgesel bakiye ve ödeme talebi (min 100$).
  • app/lib/exchange-rates.ts — getCachedRates, convertUsdToCurrency, getDisplayCurrencyFromCountry.
  • drizzle/0032_enrollments_paid_amount_currency.sql — paid_amount_minor, paid_currency kolonları.