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.
Veritabanı Şeması
Section titled “Veritabanı Şeması”Tablo: coupons
Migration: drizzle/0033_coupons_schema.sql
Şema: app/db/schema.ts — couponTypeEnum, coupons pgTable
| Alan | Açıklama |
|---|---|
| id | UUID, primary key. |
| code | Kupon kodu (unique, uppercase kayıt). Örn. YAZ50, REACT100. |
| instructor_id | Eğitmen; kupon bu eğitmene aittir. |
| course_id | Opsiyonel. Null ise genel indirim (sepete uygulanır); dolu ise sadece o kursta geçerli. |
| bundle_id | Opsiyonel; paket kapsamı (ileride kullanım). |
| discount_type | percentage | fixed_amount | free. |
| discount_value | Yüzde ise 1–100; sabit tutar ise USD cinsinden (örn. 9.99); free ise 0. |
| max_uses | Null = sınırsız; dolu ise maksimum kullanım sayısı. |
| used_count | Kaç kez kullanıldığı (webhook’ta artırılır). |
| expires_at | Opsiyonel bitiş tarihi. |
| is_active | Aktif/pasif (eğitmen toggle edebilir). |
| created_at | Oluş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.
Kupon Motoru (coupon-engine)
Section titled “Kupon Motoru (coupon-engine)”Dosya: app/lib/coupon-engine.ts
calculateDiscountedPrice
Section titled “calculateDiscountedPrice”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:
- Kodu uppercase ile
couponstablosunda arar. - Geçerlilik:
isActive,expiresAt,maxUsesvs.usedCountkontrolü. - Kapsam:
courseIddolu kupon sadece o kursa;courseIdnull kupon “genel” (sepete uygulanır).instructorId/bundleIdeşleşmesi. - İndirim türü:
- percentage:
originalPrice * (discountValue / 100)indirim. - free:
finalPrice = 0. - fixed_amount:
discountValueUSD;convertAmountToCurrency(..., "usd", currency, rates)ile hedef para birimine çevrilir;finalPrice = min(originalPrice, targetLocalPrice).
- percentage:
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.
Eğitmen Tarafı: Kupon Yönetimi
Section titled “Eğitmen Tarafı: Kupon Yönetimi”Dashboard’da Kuponlar Sekmesi
Section titled “Dashboard’da Kuponlar Sekmesi”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ıcouponstablosundan (kurs adı içincoursesleft join) çekilir.
Eski URL Yönlendirmesi
Section titled “Eski URL Yönlendirmesi”Dosya: app/routes/instructor.coupons.tsx
URL: /:lang/instructor/coupons → redirect ile /:lang/instructor?tab=coupons yönlendirilir (loader’da dil parametresi ile redirect çağrılır).
Kurs Bazlı Promosyon Sayfası
Section titled “Kurs Bazlı Promosyon Sayfası”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.
Sepet ve Checkout’ta Kupon
Section titled “Sepet ve Checkout’ta Kupon”Sepet (cart.tsx)
Section titled “Sepet (cart.tsx)”- 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
calculateDiscountedPriceile 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).
Checkout (payment.checkout.tsx)
Section titled “Checkout (payment.checkout.tsx)”- 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_ownedvb. eklenir; webhook bu metadata’yı kullanır.
Stripe Webhook’ta Kupon
Section titled “Stripe Webhook’ta Kupon”Dosya: app/routes/api.stripe.webhook.ts
- checkout.session.completed ve payment_intent.succeeded (kayıtlı kart ile ödeme) işlenirken:
- metadata:
applied_couponokunur;normalizeCouponCodeile uppercase normalize edilir. - Enrollment / earnings:
couponCodealanına bu kod yazılır (iade ve raporlama için). - Kullanım sayacı: Ödeme başarıyla tamamlandıktan sonra
coupons.used_countartırılır:
UPDATE coupons SET used_count = used_count + 1 WHERE code = ?.
- metadata:
- 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.
Çok Dilli Metinler (i18n)
Section titled “Çok Dilli Metinler (i18n)”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).
Akış Özeti
Section titled “Akış Özeti”| Aşama | Açıklama |
|---|---|
| 1 | Eğ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. |
| 3 | Stripe ödeme tamamlanır → webhook metadata’dan applied_coupon okur; enrollment/earnings’e couponCode yazar; coupons.used_count +1. |
| 4 | Kupon max_uses’a ulaştıysa veya süresi dolduysa bir sonraki sepette geçersiz döner. |
İlgili Dosyalar
Section titled “İlgili Dosyalar”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.