Skip to content

Affiliate (Satış Ortaklığı) Programı

Referans takip algoritması, coursio_affiliate_id cookie yönetimi, adil kazanç mantığı ve komisyon hesaplama.

Achidemy’de 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.

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`;

Paket vitrin sayfası: app/routes/bundle.$slug.tsx loader’ında da aynı ref parametresi okunur; varsa coursio_affiliate HttpOnly cookie (30 gün) Set-Cookie ile set edilir — kurs detay (course.$id.tsx) ile tutarlıdır. Böylece /:lang/bundle/:slug?ref=KOD paylaşım linkleri affiliate takibini destekler.

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`;
}
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çin false.

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:

  1. URL parametresi (?ref=... veya affiliateCode parametresi).
  2. Cookie (coursio_affiliate).
  3. Metadata’dan (session.metadata.affiliateCode).

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: enrollmentsreferred_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ış:

  1. Kayıt: Kullanıcı ?ref=ABC123 linkiyle gelir; cookie’ye coursio_affiliate=ABC123 yazılır.
  2. Satın alma: Checkout sırasında cookie’den affiliate kodu okunur; users tablosunda affiliateCode = 'ABC123' olan kullanıcı bulunur; bu kullanıcının id değeri alınır.
  3. Enrollment: Webhook (checkout.session.completed) tarafında enrollment oluşturulurken referredBy alanı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.tshandleCourseSale fonksiyonu

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).

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 cinsinden
const 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.

Eğitmen ve affiliate bakiyeleri, earnings tablosundan status = 'completed' ve ilgili instructorId (veya affiliate ID) ile sorgulanarak hesaplanır:

  • Eğitmen bakiyesi: instructorShare toplamı (kendi kursları için).
  • Affiliate bakiyesi: affiliateShare toplamı (saleType = 'affiliate_commission' ve instructorId = affiliateId olan kayıtlar). Affiliate kazançları satış para biriminde (cent) ve satış anı kuru (rateAtSale: 1 USD = X birim) ile saklanır; bakiye hesaplanırken her satır (affiliateShare/100) / (rateAtSale || 1) ile USD’ye çevrilip toplanır (konum bazlı dinamik döviz). Detay için Döviz Gösterimi ve İade Akışları ve Döviz Kuru API sayfalarına bakın.

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

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

Dosyalar: app/routes/account.tsx (layout), app/routes/account.profile.tsx (Affiliate bölümü)

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 (rateAtSale) ile USD’ye çevrilerek toplanır; ekranda affiliate üyesinin konumuna göre (ülke → para birimi) yerel para biriminde (₺, €, $) gösterilir. Minimum çekim 100 USD veya eşdeğer döviz uygulanır.

  • Backend (GraphQL): myAffiliateEarningsTotal ve myAffiliateBalance her affiliate_commission satırını (affiliateShare/100) / (rateAtSale || 1) ile USD’ye çevirip toplar. requestAffiliatePayout çekilebilir bakiye kontrolünde aynı çevrim kullanılır.
  • Layout: account.tsx loader’da getCachedRates(env) çağrılır; user.country döndürülür; <Outlet context={{ user, bunnyConfig, exchangeRates }} /> ile profile sayfasına exchangeRates ve user iletilir.
  • Profil sayfası: getDisplayCurrencyFromCountry(user?.country) ile gösterim para birimi (TR → TRY, Euro bölgesi → EUR, diğer → USD). Toplam kazanç, çekilebilir bakiye, bekleyen talep ve ödeme geçmişi tutarları formatInDisplayCurrency(amountUsd) ile yerel para biriminde gösterilir.
  • Ödeme talebi: Minimum 100 USD (veya eşdeğer ₺/€). Form görünürlüğü availableBalance >= 100; kullanıcı kendi para biriminde tutar girer; gönderimde USD’ye çevrilip requestAffiliatePayout(amountUsd) çağrılır. GraphQL requestAffiliatePayout mutation’ında minimum tutar 100 USD; aksi halde hata: “Affiliate ödeme talebi için minimum tutar 100$ (veya eşdeğer ₺/€) olmalıdır.”

Detay için Döviz Gösterimi ve İade Akışları sayfasına bakın.

AdımAçıklama
1Kullanıcı ?ref=ABC123 linkiyle gelir → $lang.tsx loader cookie’ye yazar (30 gün).
2Satın alma sırasında cookie’den affiliate kodu okunur; users.affiliateCode ile eşleştirilir.
3Webhook’ta enrollment oluşturulurken referredBy alanına affiliate ID yazılır.
4Komisyon 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.

  • 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ı.