Türkiye (TR) Özel Payout Akışı
Stripe kısıtlamalarını aşmak için Payoneer/Cenoa/IBAN toplama, KYC doğrulama, admin onay ve payout_requests tablosu.
Türkiye’de Stripe Connect’in sınırlı olması nedeniyle Achidemy, TR eğitmenleri için Stripe yerine Payoneer, Cenoa veya manuel banka havalesi akışı sunar. Bu sayfa, bu mantığın nasıl çalıştığını, KYC doğrulama sürecini ve admin onay mekanizmasını açıklar.
Neden TR Özel Akış?
Section titled “Neden TR Özel Akış?”- Stripe Connect, Türkiye’de tam desteklenmediği veya kısıtlamaları olduğu için, TR’deki eğitmenlere Stripe Transfer ile ödeme yapılamıyor.
- Çözüm: Eğitmen Payoneer veya Cenoa bilgisini platforma girer; KYC doğrulaması tamamlandıktan sonra ödeme talebi oluşturabilir; admin talebi onayladıktan sonra manuel havale/EFT ile ödeme yapar.
Doğrulama Yöntemleri Karşılaştırması
Section titled “Doğrulama Yöntemleri Karşılaştırması”| Özellik | Stripe Connect | Payoneer (KYC) | Cenoa (KYC) |
|---|---|---|---|
| Desteklenen Ülkeler | ABD, AB, UK, vb. | Tüm ülkeler | Tüm ülkeler |
| Doğrulama Süreci | Stripe Onboarding | Manuel KYC (admin onayı) | Manuel KYC (admin onayı) |
| Ödeme İşlemi | Otomatik Stripe Transfer | Manuel havale/EFT | Manuel Cenoa transfer |
| İletişim Bilgisi | Stripe tarafından alınır | E-posta adresi | Telefon numarası |
| Kimlik Belgesi | Stripe tarafından alınır | Platform üzerinden yüklenir | Platform üzerinden yüklenir |
| Admin Müdahalesi | Sadece onay butonu | Onay + manuel ödeme | Onay + manuel ödeme |
TR Eğitmen Akışı
Section titled “TR Eğitmen Akışı”┌─────────────────────────────────────────────────────────────────────────┐│ TR EĞİTMEN ÖDEME AKIŞI │├─────────────────────────────────────────────────────────────────────────┤│ ││ 1. Eğitmen /instructor/payouts?tab=payment sayfasına gider ││ ↓ ││ 2. "Payoneer" veya "Cenoa" seçeneğini seçer ││ ↓ ││ 3. KYC formu doldurur: ││ - Yasal ad/soyad ││ - TC Kimlik No ││ - Payoneer e-posta VEYA Cenoa telefon numarası ││ - Kimlik belgesi (ön + arka yüz) ││ ↓ ││ 4. Kimlik belgeleri Bunny Storage'a yüklenir ││ ↓ ││ 5. kycStatus = "pending", payoutMethod = "payoneer"/"cenoa" ││ ↓ ││ 6. Admin /admin/kyc-requests panelinde başvuruyu inceler ││ ↓ ││ 7. Admin onaylar → kycStatus = "approved" ││ (veya reddeder → kycStatus = "rejected") ││ ↓ ││ 8. Eğitmen artık ödeme talebi oluşturabilir ││ ↓ ││ 9. Ödeme talebi /admin/payouts paneline düşer ││ ↓ ││ 10. Admin Payoneer/Cenoa bilgilerini görür ve manuel ödeme yapar ││ ↓ ││ 11. Admin talebi "Tamamlandı" olarak işaretler ││ │└─────────────────────────────────────────────────────────────────────────┘Veritabanı Yapısı
Section titled “Veritabanı Yapısı”Users Tablosu - KYC Alanları
Section titled “Users Tablosu - KYC Alanları”| Alan | Tip | Açıklama |
|---|---|---|
kycStatus | enum | none / pending / approved / rejected |
legalName | text | Kimlik belgesindeki yasal ad |
taxId | text | TC Kimlik No |
idDocumentUrl | text | Kimlik ön yüz URL’i (Bunny Storage) |
idDocumentBackUrl | text | Kimlik arka yüz URL’i (Bunny Storage) |
manualPayoutDetails | text | Payoneer e-posta veya Cenoa telefon numarası |
kycRejectionReason | text | Red sebebi (admin notu) |
payoutMethod | text | 'payoneer' veya 'cenoa' veya 'bank_transfer' |
Payout Requests Tablosu
Section titled “Payout Requests Tablosu”| Alan | Tip | Açıklama |
|---|---|---|
id | UUID | Primary key |
instructor_id | UUID | Talep sahibi eğitmen |
amount | numeric | Talep edilen tutar (USD) |
status | text | pending / processing / completed / rejected |
method | text | 'stripe_connect' veya 'bank_transfer' |
destination | text | Stripe Connect ID veya Payoneer/Cenoa bilgisi |
processed_at | timestamp | İşlenme zamanı |
stripe_transfer_id | text | Stripe Transfer ID (sadece Stripe için) |
Eğitmen Tarafı
Section titled “Eğitmen Tarafı”Ödeme Yöntemi Seçimi
Section titled “Ödeme Yöntemi Seçimi”TR eğitmenleri için “Payoneer” ve “Cenoa” seçenekleri gösterilir (Stripe devre dışı):
const isTurkey = user?.country === "TR";const [selectedMethod, setSelectedMethod] = useState<"stripe" | "payoneer" | "cenoa">( useStripe ? "stripe" : "payoneer");Ödeme Yöntemi Seçim UI’ı (Logo ile)
Section titled “Ödeme Yöntemi Seçim UI’ı (Logo ile)”{/* Payoneer Seçeneği */}<div className={`cursor-pointer rounded-2xl border-2 p-6 transition-all ${ selectedMethod === "payoneer" ? "border-orange-500 bg-orange-50/50" : "border-slate-200 hover:border-slate-300"}`}> <img src="/images/payoneer.png" alt="Payoneer" className="h-8 w-auto object-contain" /></div>
{/* Cenoa Seçeneği */}<div className={`cursor-pointer rounded-2xl border-2 p-6 transition-all ${ selectedMethod === "cenoa" ? "border-[#0052FF] bg-blue-50/50" : "border-slate-200 hover:border-slate-300"}`}> <img src="/images/cenoa.png" alt="Cenoa" className="h-8 w-auto object-contain" /></div>KYC Doğrulama Kontrolü
Section titled “KYC Doğrulama Kontrolü”// Hangi ödeme yöntemi ile doğrulama yapıldığını belirleconst isStripeVerified = user?.isConnectOnboardingCompleted ?? false;const isPayoneerVerified = user?.kycStatus === "approved" && !isStripeVerified && user?.payoutMethod === "payoneer";const isCenoaVerified = user?.kycStatus === "approved" && !isStripeVerified && user?.payoutMethod === "cenoa";
// Genel onay durumuconst isApproved = isPayoneerVerified || isCenoaVerified || isStripeVerified;Ödeme Talebi Oluşturma
Section titled “Ödeme Talebi Oluşturma”Payoneer veya Cenoa ile doğrulanan eğitmen için UI (logo + “ile doğrulandı”):
{(isPayoneerVerified || isCenoaVerified) && ( <div className="bg-white border-2 border-slate-100 rounded-3xl p-8 space-y-6"> <div className="flex items-center gap-3 mb-2"> <div className={`p-2 rounded-lg ${isCenoaVerified ? "bg-blue-50" : "bg-orange-50"}`}> <CheckCircle2 className={isCenoaVerified ? "text-blue-600" : "text-orange-600"} /> </div> <div> <h4>Ödeme Talebi Oluştur</h4> <div className="flex items-center gap-2 mt-1"> <img src={isCenoaVerified ? "/images/cenoa.png" : "/images/payoneer.png"} alt={isCenoaVerified ? "Cenoa" : "Payoneer"} className="h-4 w-auto object-contain" /> <span className={`text-xs font-bold ${isCenoaVerified ? "text-blue-600" : "text-orange-600"}`}> ile doğrulandı </span> </div> </div> </div> {/* Tutar girişi ve gönder butonu */} </div>)}Alıcı Hesap Bilgisi (Logo ile)
Section titled “Alıcı Hesap Bilgisi (Logo ile)”{(isPayoneerVerified || isCenoaVerified || isStripeVerified) && ( <div className="text-right"> <p className="text-[10px] font-black text-blue-600 uppercase"> Alıcı Hesap </p> <div className="flex items-center justify-end gap-2"> <img src={isStripeVerified ? "/images/stripe.webp" : isCenoaVerified ? "/images/cenoa.png" : "/images/payoneer.png"} alt={isStripeVerified ? "Stripe" : isCenoaVerified ? "Cenoa" : "Payoneer"} className="h-4 w-auto object-contain" /> {!isStripeVerified && ( <span className="text-xs font-bold text-blue-700 truncate max-w-[150px]"> {user?.manualPayoutDetails || "-"} </span> )} </div> </div>)}Admin Tarafı
Section titled “Admin Tarafı”KYC Onay Paneli
Section titled “KYC Onay Paneli”URL: /admin/kyc-requests
TR eğitmenlerinin KYC başvuruları bu panelde listelenir:
- Yasal ad, ülke, e-posta
- Vergi/TC No
- Ödeme yöntemi (logo ile gösterilir)
- Payoneer e-posta veya Cenoa telefon
- Kimlik belgeleri (ön + arka yüz)
Detay Sayfası: /admin/kyc/:userId/preview
- Tüm KYC bilgileri
- Kimlik belgesi görüntüleme (modal)
- Onay/Red butonları
Ödeme Talepleri Paneli
Section titled “Ödeme Talepleri Paneli”URL: /admin/payouts
Ödeme Yöntemi Gösterimi (Logo ile)
Section titled “Ödeme Yöntemi Gösterimi (Logo ile)”<div className="flex items-center gap-2"> {payout.isPayoneerVerified ? ( <> <img src="/images/payoneer.png" alt="Payoneer" className="h-5 w-auto object-contain" /> <span className="bg-emerald-100 text-emerald-700 border-emerald-200"> <CheckCircle2 size={10} className="mr-1" /> Doğrulandı </span> </> ) : payout.isCenoaVerified ? ( <> <img src="/images/cenoa.png" alt="Cenoa" className="h-5 w-auto object-contain" /> <span className="bg-blue-100 text-blue-700 border-blue-200"> <CheckCircle2 size={10} className="mr-1" /> Doğrulandı </span> </> ) : null}</div>Payoneer Bilgileri Kutusu
Section titled “Payoneer Bilgileri Kutusu”{payout.isPayoneerVerified && payout.instructor?.manualPayoutDetails && ( <div className="mt-2 p-3 bg-emerald-50 border border-emerald-200 rounded-lg"> <p className="text-[10px] font-bold text-emerald-700 uppercase mb-1"> Payoneer / IBAN Bilgileri </p> <p className="text-xs text-emerald-800 font-mono whitespace-pre-wrap break-all"> {payout.instructor.manualPayoutDetails} </p> </div>)}Cenoa Bilgileri Kutusu
Section titled “Cenoa Bilgileri Kutusu”{payout.isCenoaVerified && payout.instructor?.manualPayoutDetails && ( <div className="mt-2 p-3 bg-blue-50 border border-blue-200 rounded-lg"> <p className="text-[10px] font-bold text-blue-700 uppercase mb-1"> Cenoa Telefon Numarası </p> <p className="text-xs text-blue-800 font-mono whitespace-pre-wrap break-all"> {payout.instructor.manualPayoutDetails} </p> </div>)}Manuel Ödeme Uyarıları
Section titled “Manuel Ödeme Uyarıları”{/* Payoneer uyarısı */}{payout.isPayoneerVerified && payout.status === "pending" && ( <div className="mt-3 bg-emerald-50 border border-emerald-200 rounded-lg p-3"> <div className="flex items-start gap-2"> <AlertCircle className="text-emerald-600 mt-0.5" size={14} /> <p className="text-xs text-emerald-800"> <strong>Manuel Ödeme Gerekli:</strong>{" "} Bu eğitmen Payoneer ile doğrulandı. Yukarıdaki Payoneer/IBAN bilgilerine manuel olarak ödeme yapmanız gerekmektedir. </p> </div> </div>)}
{/* Cenoa uyarısı */}{payout.isCenoaVerified && payout.status === "pending" && ( <div className="mt-3 bg-blue-50 border border-blue-200 rounded-lg p-3"> <div className="flex items-start gap-2"> <AlertCircle className="text-blue-600 mt-0.5" size={14} /> <p className="text-xs text-blue-800"> <strong>Manuel Ödeme Gerekli:</strong>{" "} Bu eğitmen Cenoa ile doğrulandı. Yukarıdaki telefon numarasına Cenoa üzerinden manuel olarak ödeme yapmanız gerekmektedir. </p> </div> </div>)}Admin Onay İşlemi
Section titled “Admin Onay İşlemi”Admin /admin/payouts panelinde ödeme talebini onaylarken, sistem eğitmenin doğrulama yöntemini kontrol eder:
// app/routes/admin.payouts.tsx - actionif (intent === "approve") { const [instructor] = await db .select({ country: users.country, stripeConnectId: users.stripeConnectId, manualPayoutDetails: users.manualPayoutDetails, kycStatus: users.kycStatus, isConnectOnboardingCompleted: users.isConnectOnboardingCompleted, payoutMethod: users.payoutMethod, }) .from(users) .where(eq(users.id, payoutRow.instructorId));
// Hangi ödeme yöntemi ile doğrulama yapıldığını belirle const isStripeVerified = instructor?.isConnectOnboardingCompleted ?? false; const isPayoneerVerified = instructor?.kycStatus === "approved" && !isStripeVerified && instructor?.payoutMethod === "payoneer"; const isCenoaVerified = instructor?.kycStatus === "approved" && !isStripeVerified && instructor?.payoutMethod === "cenoa";
let stripeTransferId: string | null = null;
if (isStripeVerified && instructor?.stripeConnectId) { // STRIPE CONNECT: Otomatik transfer const transfer = await stripe.transfers.create({ amount: Math.round(Number(payoutRow.amount) * 100), currency: "usd", destination: instructor.stripeConnectId, }); stripeTransferId = transfer.id; } else if (isPayoneerVerified) { // PAYONEER: Manuel ödeme - Stripe Transfer YAPILMAZ console.log("Payoneer doğrulamalı eğitmen için manuel ödeme onaylandı"); } else if (isCenoaVerified) { // CENOA: Manuel ödeme - Stripe Transfer YAPILMAZ console.log("Cenoa doğrulamalı eğitmen için manuel ödeme onaylandı"); }
// Durumu güncelle (her durumda) await db.update(payoutRequests).set({ status: "completed", processedAt: now, ...(stripeTransferId ? { stripeTransferId } : {}), }).where(eq(payoutRequests.id, payoutId));}Önemli: Payoneer veya Cenoa doğrulamalı eğitmenler için “Onayla” butonuna basıldığında Stripe API çağrısı yapılmaz. Admin önce ilgili ödeme bilgilerine manuel ödeme yapar, ardından “Onayla” ile talebi tamamlandı olarak işaretler.
TR Eğitmen Bakiye ve Kazanç Gösterimi
Section titled “TR Eğitmen Bakiye ve Kazanç Gösterimi”Eğitmen ödemeler sayfasında bakiye ve kazançlar yerel para biriminde (TRY / ₺) gösterilir:
// Döviz kuru çevrimiconst displayCurrency = getDisplayCurrencyFromCountry(user?.country); // "TRY"const localAmount = convertUsdToCurrency(amountUsd, displayCurrency, rates);const localSymbol = DISPLAY_SYMBOLS[displayCurrency]; // "₺"Kurlar Döviz Kuru API ile alınır (open.er-api.com, Cloudflare KV önbelleği).
Renk Temaları
Section titled “Renk Temaları”| Ödeme Yöntemi | Ana Renk | Tema |
|---|---|---|
| Payoneer | Turuncu (#F97316) | bg-orange-50, text-orange-600, border-orange-200 |
| Cenoa | Mavi (#0052FF) | bg-blue-50, text-blue-600, border-blue-200 |
Özet Tablo
Section titled “Özet Tablo”| Adım | Açıklama |
|---|---|
| 1 | TR eğitmeni KYC başvurusu yapar (yasal ad, TC no, Payoneer e-posta veya Cenoa telefon, kimlik belgesi). |
| 2 | Admin KYC başvurusunu /admin/kyc-requests panelinde inceler ve onaylar. |
| 3 | kycStatus = "approved" → isPayoneerVerified veya isCenoaVerified = true |
| 4 | Eğitmen ödeme talebi oluşturur (tutar girer, gönderir). |
| 5 | payout_requests tablosuna kayıt eklenir. |
| 6 | Admin /admin/payouts panelinde talebi görür. |
| 7 | Admin Payoneer/Cenoa bilgilerini görür ve manuel ödeme yapar. |
| 8 | Admin talebi “Onayla” ile “Tamamlandı” olarak işaretler. |
| 9 | Eğitmene bildirim gönderilir. |
İlgili Dosyalar
Section titled “İlgili Dosyalar”| Dosya | Açıklama |
|---|---|
app/routes/api.instructor.submit-kyc.ts | KYC başvuru API endpoint’i |
app/routes/admin.kyc-requests.tsx | Admin KYC liste paneli |
app/routes/admin.kyc.$userId.preview.tsx | Admin KYC detay/onay sayfası |
app/routes/admin.payouts.tsx | Admin ödeme talepleri paneli |
app/routes/instructor.payouts.tsx | Eğitmen ödemeler sayfası |
app/lib/exchange-rates.ts | Döviz kuru fonksiyonları |
public/images/payoneer.png | Payoneer logosu |
public/images/cenoa.png | Cenoa logosu |
İlgili Dokümantasyon
Section titled “İlgili Dokümantasyon”- KYC Onboarding — KYC sistemi detaylı dokümantasyonu
- Stripe Connect — Stripe Connect entegrasyonu
- Döviz Kuru API (Exchange Rates) — Kurların nasıl alındığı
- Checkout & Webhooks — Ödeme webhook’ları