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.
Veri Kaynağı: enrollments ve earnings
Section titled “Veri Kaynağı: enrollments ve earnings”enrollments — Öğrenci tarafı satış kaydı:
| Alan | Açı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):
| Alan | Açıklama |
|---|---|
| total_price | Satışın brüt tutarı (minor); webhook’ta session.amount_total ile aynı olmalı (bölgesel fiyatlandırma ile gerçek ödeme). |
| currency | Satış para birimi (try, eur, usd). |
| rate_at_sale | Satış 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_total→paidAmountMinor(enrollments) vetotalPrice(earnings),session.currency→paidCurrency/currency.rateAtSaleise getCachedRates ile satış anı kurundan alınır. - Stripe Payment Intent (webhook veya GraphQL buyWithSavedCard):
paymentIntent.amount/ bölgesel fiyat →paidAmountMinor,paymentIntent.currency/finalCurrency→paidCurrency.
Loader ve UI
Section titled “Loader ve UI”- Loader, her enrollment için
paidAmountMinorvepaidCurrencyokur; her satın alma kartınacurrencyveamountMinor(yoksa kurs/bundle fiyatı +usdfallback) 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.ts — requestRefund 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.stripeCheckoutSessionIdveyaenrollment.stripePaymentIntentId(Payment Intent ile satışta earnings’testripeCheckoutSessionIdalanına PI id yazılır).courseId = enrollment.courseId.
- İlerleme suistimali kalkanı: İade öncesi
enrollments.maxProgressPercentageve 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,toggleLessonCompletionvelearn.$slug.tsxiçindekicurrentProgressdeğ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, yoksagetCachedRates(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.
Admin Dashboard – Finans Üssü Tablosu
Section titled “Admin Dashboard – Finans Üssü Tablosu”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.ts — checkout.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/refundStatustemizlenir,stripeCheckoutSessionId,stripePaymentIntentId,paidAmountMinor,paidCurrency,enrolledAtyeni ö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.ts — charge.refunded / payment_intent.refunded (paket iadesi), checkout.session.completed (paket yeniden satın alma)
Paket iadesi
Section titled “Paket iadesi”- İade event’inde metadata üzerinden
bundle_idveyabundleId(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ı
refundedAtverefundStatus = '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
stripeCheckoutSessionIdvebundleIdile tek satışı hedefler.
Paket yeniden satın alma
Section titled “Paket yeniden satın alma”enrollmentstablosunda 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/refundStatustemizlenir,stripeCheckoutSessionId,stripePaymentIntentId,paidAmountMinor,paidCurrencyyeni ödemeye göre güncellenir. - Yoksa (örn. pakete yeni kurs eklenmişse) INSERT ile yeni kayıt eklenir.
- İade edilmiş enrollment varsa UPDATE yapılır:
- 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.affiliateShareolarak yazılır;earnings.currency(try/eur/usd) ve satış anı kuruearnings.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: heraffiliate_commissionsatı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ı
rateAtSaleile USD’ye çevrilip toplanır; bekleyen ve ödenen talepler (zaten USD) düşülerekavailableBalance(USD) hesaplanır. Talep tutarı API’ye USD olarak gönderilir.
Layout’ta exchangeRates ve user.country
Section titled “Layout’ta exchangeRates ve user.country”Dosya: app/routes/account.tsx
- Loader’da
getCachedRates(env)çağrılır;user.countrykullanıcı kaydından döndürülür. - Dönüş:
exchangeRates: { rates, updatedAt }veuser: { ..., country };<Outlet context={{ user, bunnyConfig, exchangeRates }} />ile profile sayfasına iletilir.
Profile’da gösterim
Section titled “Profile’da gösterim”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 >= 100ise 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.”
Özet Tablo
Section titled “Özet Tablo”| Konu | Açıklama |
|---|---|
| Satın alma geçmişi | enrollments.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 listesi | Toplam: 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 bakiye | Kazançlar satış para biriminde + rateAtSale ile saklanır; GraphQL her satırı USD’ye çevirip toplar; profile’da getDisplayCurrencyFromCountry + formatInDisplayCurrency (TRY/€/$). |
| Affiliate min çekim | 100 USD veya eşdeğer; UI’da yerel para birimi ile girdi, API’ye USD. |
İlgili Dosyalar
Section titled “İlgili Dosyalar”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ı.