Affiliate (Satış Ortaklığı) Programı
Referans takip algoritması, coursio_affiliate_id cookie yönetimi, adil kazanç mantığı ve komisyon hesaplama.
Coursio’da Affiliate Programı, kullanıcıların benzersiz referans kodlarıyla satış yapmalarını ve komisyon kazanmalarını sağlar. Bu sayfa, referans takip algoritması (ref parametresi yakalama, cookie yönetimi), adil kazanç mantığı (yalnızca yeni kayıt olan kullanıcıların ilk satın alımından komisyon) ve webhook tarafında komisyonun ayrıştırılması ve eğitmen/affiliate bakiyelerine dağıtımını açıklar.
Referans Takip Algoritması
Section titled “Referans Takip Algoritması”ref Parametresinin Yakalanması
Section titled “ref Parametresinin Yakalanması”Dosya: app/routes/$lang.tsx (loader)
URL’deki ?ref=... parametresi loader seviyesinde yakalanır:
const url = request ? new URL(request.url) : null;const refCode = url?.searchParams.get("ref");Eğer refCode varsa, HttpOnly cookie olarak coursio_affiliate adıyla 30 gün boyunca saklanır:
headers["Set-Cookie"] = `coursio_affiliate=${encodeURIComponent(refCode.trim())}; Path=/; Max-Age=${60 * 60 * 24 * 30}; HttpOnly; SameSite=Lax`;Client-side yedek: LangLayout component’i içinde useSearchParams ile ref parametresi tekrar kontrol edilir ve client-side cookie olarak da ayarlanır (HttpOnly olmayan, tarayıcı erişimi için):
function setAffiliateCookieClientSide(refCode: string) { const maxAge = 60 * 60 * 24 * 30; document.cookie = `coursio_affiliate=${encodeURIComponent(refCode)}; path=/; max-age=${maxAge}; samesite=lax`;}Misafir Kullanıcılar için coursio_affiliate_id Cookie Yönetimi
Section titled “Misafir Kullanıcılar için coursio_affiliate_id Cookie Yönetimi”Cookie adı: coursio_affiliate (kullanıcı dokümantasyonunda bazen coursio_affiliate_id olarak geçer).
Özellikler:
- Süre: 30 gün (2592000 saniye).
- Path:
/(tüm sayfalarda geçerli). - SameSite:
Lax(güvenlik ve CSRF koruması). - HttpOnly: Server-side cookie için
true(JavaScript erişimi yok); client-side yedek içinfalse.
Cookie okuma: Route’larda (örn. payment.checkout.tsx, GraphQL resolver’lar) cookie header’ından okunur:
const getAffiliateCookie = async (cookieHeader: string | null): Promise<string> => { if (!cookieHeader) return ""; const match = cookieHeader.match(/coursio_affiliate=([^;]+)/); return match ? decodeURIComponent(match[1]?.trim() || "") : "";};Öncelik sırası: Checkout session oluşturulurken affiliate kodu belirlenirken:
- URL parametresi (
?ref=...veyaaffiliateCodeparametresi). - Cookie (
coursio_affiliate). - Metadata’dan (
session.metadata.affiliateCode).
Adil Kazanç Mantığı
Section titled “Adil Kazanç Mantığı”Yalnızca Yeni Kayıt Olan Kullanıcıların İlk Satın Alımından Komisyon
Section titled “Yalnızca Yeni Kayıt Olan Kullanıcıların İlk Satın Alımından Komisyon”Mantık: Affiliate komisyonu, yalnızca yeni kayıt olan kullanıcıların ilk satın alımı için verilir. Bu, sistemin adil çalışmasını ve affiliate’lerin gerçekten yeni müşteri getirdiğini garanti eder.
Veritabanındaki referredById Eşleşmesi:
Tablo: enrollments — referred_by alanı
- Alan:
referred_by(text, FK →users.id). - Açıklama: Satın almayı yönlendiren affiliate kullanıcı ID’si (referans linki ile gelen kayıt).
Akış:
- Kayıt: Kullanıcı
?ref=ABC123linkiyle gelir; cookie’yecoursio_affiliate=ABC123yazılır. - Satın alma: Checkout sırasında cookie’den affiliate kodu okunur;
userstablosundaaffiliateCode = 'ABC123'olan kullanıcı bulunur; bu kullanıcınıniddeğeri alınır. - Enrollment: Webhook (
checkout.session.completed) tarafında enrollment oluşturulurkenreferredByalanına affiliate kullanıcı ID’si yazılır:
await db.insert(enrollments).values({ userId, courseId, referredBy: affiliateUser?.id ?? null, // Affiliate kullanıcı ID'si stripeCheckoutSessionId: session.id, stripePaymentIntentId: paymentIntentId,});İlk satın alım kontrolü: Sistem, kullanıcının daha önce aktif enrollment’ı olup olmadığını kontrol eder. Eğer kullanıcının bu kurs için aktif (iade edilmemiş) enrollment’ı varsa, yeni enrollment oluşturulmaz ve komisyon hesaplanmaz. Bu sayede sadece ilk satın alım için komisyon verilir.
Kazanç Hesaplama: Webhook Tarafında Komisyon Ayrıştırması
Section titled “Kazanç Hesaplama: Webhook Tarafında Komisyon Ayrıştırması”Dosya: app/routes/api.stripe.webhook.ts — handleCourseSale fonksiyonu
Komisyon Oranları
Section titled “Komisyon Oranları”Affiliate satışı: Eğer affiliate varsa ve eğitmen koruması yoksa:
- Eğitmen: %40 (
instructorRate = 0.40). - Affiliate: %15 (
affiliateRate = 0.15). - Platform: %45 (
platformRate = 0.45). - Satış türü:
saleType = "affiliate".
Eğitmen koruması: Eğer instructor_ref === "instructor" veya eğitmenin kendi affiliate kodu kullanılıyorsa, affiliate komisyonu verilmez (%95 eğitmen, %5 platform).
Komisyon Hesaplama ve Dağıtım
Section titled “Komisyon Hesaplama ve Dağıtım”1. Affiliate ID tespiti:
let affiliateId: string | null = null;if (meta.affiliate_id) { affiliateId = meta.affiliate_id as string;} else if (meta.affiliateCode) { const [affiliateUser] = await db .select({ id: users.id }) .from(users) .where(eq(users.affiliateCode, meta.affiliateCode as string)) .limit(1); if (affiliateUser) { affiliateId = affiliateUser.id; }}2. Tutar hesaplama (cent cinsinden):
const amountTotal = session.amount_total ?? 0; // Cent cinsindenconst instructorAmount = Math.round(amountTotal * instructorRate);const platformAmount = Math.round(amountTotal * platformRate);const affiliateAmount = affiliateRate > 0 ? Math.round(amountTotal * affiliateRate) : 0;3. Earnings kaydı (eğitmen için):
await db.insert(earnings).values({ instructorId: finalInstructorId, courseId: courseId, totalPrice: amountTotal, instructorShare: instructorAmount, platformShare: platformAmount, affiliateShare: affiliateAmount, affiliateId: affiliateId, saleType: saleType, status: "completed", currency: currency, stripeCheckoutSessionId: session.id,});4. Affiliate için ayrı earnings kaydı:
Affiliate komisyonu, affiliate kullanıcının kendi bakiyesine yazılması için ayrı bir earnings kaydı oluşturulur:
if (affiliateAmount > 0 && affiliateId) { await db.insert(earnings).values({ instructorId: affiliateId, // Affiliate kullanıcı ID'si courseId: courseId, totalPrice: amountTotal, instructorShare: 0, platformShare: 0, affiliateShare: affiliateAmount, affiliateId: affiliateId, saleType: "affiliate_commission", status: "completed", currency: currency, stripeCheckoutSessionId: session.id, });}Not: instructorId alanı burada affiliate kullanıcı ID’si olarak kullanılır (şema uyumluluğu için); saleType = "affiliate_commission" ile ayırt edilir.
Bakiye Hesaplama
Section titled “Bakiye Hesaplama”Eğitmen ve affiliate bakiyeleri, earnings tablosundan status = 'completed' ve ilgili instructorId (veya affiliate ID) ile sorgulanarak hesaplanır:
- Eğitmen bakiyesi:
instructorSharetoplamı (kendi kursları için). - Affiliate bakiyesi:
affiliateSharetoplamı (saleType = 'affiliate_commission'veinstructorId = affiliateIdolan kayıtlar).
| Adım | Açıklama |
|---|---|
| 1 | Kullanıcı ?ref=ABC123 linkiyle gelir → $lang.tsx loader cookie’ye yazar (30 gün). |
| 2 | Satın alma sırasında cookie’den affiliate kodu okunur; users.affiliateCode ile eşleştirilir. |
| 3 | Webhook’ta enrollment oluşturulurken referredBy alanına affiliate ID yazılır. |
| 4 | Komisyon motoru (handleCourseSale) çalışır: %40 eğitmen, %15 affiliate, %45 platform. |
| 5 | İki earnings kaydı oluşturulur: eğitmen için (toplam paylar) ve affiliate için (affiliate_commission). |
Detaylı komisyon mantığı ve eğitmen koruması için Checkout & Webhooks sayfasına bakın.
İlgili Dosyalar
Section titled “İlgili Dosyalar”app/routes/$lang.tsx— ref parametresi yakalama ve coursio_affiliate cookie.app/routes/affiliate.tsx— Affiliate program sayfası.app/routes/api.stripe.webhook.ts— Satış webhook’unda affiliate komisyon ve earnings.app/lib/db-queries.ts— Bakiye ve earnings sorguları.