Skip to content

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.

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`;
}
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).
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ı.