Skip to content

Kupon Sistemi (Promosyonlar)

Eğitmen kuponları, sepette kupon uygulama, checkout metadata ve webhook'ta usedCount güncellemesi.

Achidemy’de eğitmenler kupon kodu oluşturur; öğrenciler sepette veya checkout’ta kodu girerek indirim alır. Bu sayfa veritabanı şeması, kupon motoru, eğitmen UI (dashboard sekmesi ve kurs bazlı promosyon sayfası), sepet/checkout entegrasyonu ve Stripe webhook’ta kupon kullanım sayacının güncellenmesini açıklar.

Tablo: coupons
Migration: drizzle/0033_coupons_schema.sql
Şema: app/db/schema.tscouponTypeEnum, coupons pgTable

AlanAçıklama
idUUID, primary key.
codeKupon kodu (unique, uppercase kayıt). Örn. YAZ50, REACT100.
instructor_idEğitmen; kupon bu eğitmene aittir.
course_idOpsiyonel. Null ise genel indirim (sepete uygulanır); dolu ise sadece o kursta geçerli.
bundle_idOpsiyonel; paket kapsamı (ileride kullanım).
discount_typepercentage | fixed_amount | free.
discount_valueYüzde ise 1–100; sabit tutar ise USD cinsinden (örn. 9.99); free ise 0.
max_usesNull = sınırsız; dolu ise maksimum kullanım sayısı.
used_countKaç kez kullanıldığı (webhook’ta artırılır).
expires_atOpsiyonel bitiş tarihi.
is_activeAktif/pasif (eğitmen toggle edebilir).
created_atOluşturulma zamanı.

İlişkili alanlar (diğer tablolarda):

  • enrollments: coupon_code — satın almada kullanılan kupon (iade/raporlama için).
  • earnings: coupon_code — satışta kullanılan kupon (eğitmen raporu için).
  • cart_items: applied_coupon — sepette bu öğe için girilen kupon kodu.

Dosya: app/lib/coupon-engine.ts

Tek bir kupon kodu için indirimli fiyatı hesaplar.

Parametreler: db, originalPrice, currency, couponCode, courseId?, instructorId?, bundleId?, rates? (döviz kurları; sabit tutar kuponu için gerekli).

Mantık:

  1. Kodu uppercase ile coupons tablosunda arar.
  2. Geçerlilik: isActive, expiresAt, maxUses vs. usedCount kontrolü.
  3. Kapsam: courseId dolu kupon sadece o kursa; courseId null kupon “genel” (sepete uygulanır). instructorId / bundleId eşleşmesi.
  4. İndirim türü:
    • percentage: originalPrice * (discountValue / 100) indirim.
    • free: finalPrice = 0.
    • fixed_amount: discountValue USD; convertAmountToCurrency(..., "usd", currency, rates) ile hedef para birimine çevrilir; finalPrice = min(originalPrice, targetLocalPrice).

Dönüş: { finalPrice, discountAmount, isValid, error? }.

Sepet ve checkout bu fonksiyonu çağırarak indirimli tutarı ve hata mesajını kullanır; Stripe’a gönderilen tutar indirimli fiyata göre hesaplanır.

URL: /:lang/instructor?tab=coupons
Dosya: app/routes/instructor._index.tsx

  • Eğitmen paneli ana sayfasında Courses, Course Bundles, Coupons, Trash sekmeleri vardır.
  • Coupons sekmesi: kupon listesi (tablo) + yeni kupon oluşturma formu.
  • Form alanları: kupon kodu, kapsam (genel indirim / belirli kurs), indirim türü (yüzde / sabit tutar / ücretsiz), indirim değeri, maks. kullanım, bitiş tarihi, aktif (Switch).
  • Tabloda: Kod, Kapsam, Tür, Değer, Kullanım, Max, Bitiş, Aktif; işlemler: Aç/Kapat (toggle), Sil.
  • Action: create_coupon, toggle_coupon, delete_coupon; loader’da eğitmenin kuponları coupons tablosundan (kurs adı için courses left join) çekilir.

Dosya: app/routes/instructor.coupons.tsx
URL: /:lang/instructor/couponsredirect ile /:lang/instructor?tab=coupons yönlendirilir (loader’da dil parametresi ile redirect çağrılır).

URL: /:lang/instructor/course/:slug/manage/promotions
Dosya: app/routes/instructor.course.$slug.manage.promotions.tsx
Sidebar: CourseEditSidebar’da “Promosyonlar ve Kuponlar” linki (promotions sayfası).

  • Bu kursa özel kupon oluşturma ve listeleme (courseId dolu kuponlar).
  • Aynı form ve tablo mantığı; kapsam varsayılan olarak ilgili kurs seçili.
  • Kupon uygulama giriş gerektirir; misafir kullanıcı sepeti görebilir ancak kuponu uygulamak ve ödemeye geçmek için giriş yapmalıdır.
  • Giriş yapan kullanıcı kupon kodu girer; coupon-engine calculateDiscountedPrice ile doğrulama ve indirimli fiyat hesaplanır.
  • Sepet satırlarında/eski fiyat üstü çizili, indirimli fiyat gösterilir; toplam indirimli tutar kullanılır.
  • Girilen kupon kodu, checkout’a iletilir (Stripe metadata’ya yazılır).
  • Loader: sepet/ürün fiyatları + uygulanan kupon ile indirimli toplam hesaplanır (calculateDiscountedPrice / sepet toplamı için benzer mantık).
  • Stripe Checkout Session veya Payment Intent oluşturulurken metadata içine applied_coupon, coupon_id (varsa), coupon_is_instructor_owned vb. eklenir; webhook bu metadata’yı kullanır.

Dosya: app/routes/api.stripe.webhook.ts

  • checkout.session.completed ve payment_intent.succeeded (kayıtlı kart ile ödeme) işlenirken:
    • metadata: applied_coupon okunur; normalizeCouponCode ile uppercase normalize edilir.
    • Enrollment / earnings: couponCode alanına bu kod yazılır (iade ve raporlama için).
    • Kullanım sayacı: Ödeme başarıyla tamamlandıktan sonra coupons.used_count artırılır:
      UPDATE coupons SET used_count = used_count + 1 WHERE code = ?.
  • Böylece aynı kupon max_uses’a ulaştığında bir sonraki sepette geçersiz sayılır.

Detaylı checkout ve webhook akışı için Checkout & Webhooks sayfasına bakın.

Anahtarlar: courseManage.promotions.*, nav.instructorDashboardSection.coupons

  • courseManage.promotions: title, description, descriptionDashboard (dashboard sekmesi açıklaması), codeLabel, codePlaceholder, scopeLabel, scopeGeneral, scopeGeneralHint, scopeCourseHint, discountTypeLabel, discountTypePercentage / discountTypeFixed / discountTypeFree, discountValueLabel, discountValuePlaceholder, maxUsesLabel, maxUsesPlaceholder, expiresAtLabel, expiresAtPlaceholder, isActiveLabel, submitCreate, createdSuccess, noCoupons, tableCode, tableType, tableValue, tableUsed, tableMax, tableExpires, tableActive, tableActions, toggleActive.
  • Desteklenen diller: tr, en, es, de, fr, ja (app/locales/*.json).
AşamaAçıklama
1Eğitmen dashboard’da veya kurs promosyon sayfasında kupon oluşturur (kod, kapsam, indirim türü/değer, max uses, bitiş).
2Öğrenci sepette kupon kodu girer → calculateDiscountedPrice ile doğrulama ve indirimli fiyat; checkout’a indirimli tutar ve applied_coupon gider.
3Stripe ödeme tamamlanır → webhook metadata’dan applied_coupon okur; enrollment/earnings’e couponCode yazar; coupons.used_count +1.
4Kupon max_uses’a ulaştıysa veya süresi dolduysa bir sonraki sepette geçersiz döner.
  • app/db/schema.ts — coupons tablosu, couponTypeEnum; enrollments/earnings/cart_items’ta coupon alanları.
  • app/lib/coupon-engine.ts — calculateDiscountedPrice.
  • app/routes/instructor._index.tsx — Dashboard coupons sekmesi (create/toggle/delete).
  • app/routes/instructor.coupons.tsx — Redirect /instructor/coupons → ?tab=coupons.
  • app/routes/instructor.course.$slug.manage.promotions.tsx — Kurs bazlı promosyon sayfası.
  • app/routes/cart.tsx — Sepette kupon girişi ve indirimli fiyat gösterimi.
  • app/routes/payment.checkout.tsx — Checkout’ta indirimli toplam ve Stripe metadata.
  • app/routes/api.stripe.webhook.ts — applied_coupon metadata, couponCode kaydı, used_count güncellemesi.
  • app/locales/*.json — courseManage.promotions ve nav.instructorDashboardSection.coupons çevirileri.