Abonelik modeli SaaS ürünlerinin en yaygın gelir biçimi. Ama tekrarlayan faturalama, bir kartı bir kez çekmekten çok daha fazlası: faturalama döngüleri, başarısız ödemeler, vergi, plan değişikliğinde kıstelyevm (proration) hesabı, iptaller ve müşterinin yaşam döngüsü boyunca olan her şey doğru ve tekrar edilebilir biçimde yürümeli.
Bu yazıda ölçeklenebilir bir abonelik ve tekrarlayan faturalama sistemi tasarlamanın temel ilkelerini ve pratik kalıplarını ele alıyoruz.
Abonelik sistemleri neden karmaşık
Tek seferlik satışta ödeme bir olaydır. Abonelikte ise birden fazla hareketli parçayı aynı anda yönetirsiniz:
- Tekrarlayan tahsilat: farklı saat dilimleri ve faturalama tarihleri
- Başarısız ödeme yönetimi: yeniden deneme mantığı ve müşteri bildirimleri
- Abonelik durumu: aktif, duraklatılmış, iptal edilmiş, süresi dolmuş
- Vergi ve uyum: ülkeye göre değişen kurallar
- Tahsilat takibi (dunning): başarısız ödemeleri geri kazanmak
- Kıstelyevm hesabı: dönem ortasında plan değiştiğinde
- Mutabakat: ödeme sağlayıcısı ile iç kayıtlar arasında
Faturalama mantığındaki tek bir hata ya gelir kaybına ya da müşteri şikâyetine dönüşür; ikisi de pahalıdır.
Temel mimari bileşenler
1. Abonelik veri modeli
Sistemin merkezinde iyi tasarlanmış bir veri modeli durur:
class Subscription:
subscription_id: str
customer_id: str
plan_id: str
status: str # active, paused, canceled, expired
billing_cycle: str # monthly, quarterly, yearly
current_period_start: datetime
current_period_end: datetime
next_billing_date: datetime
amount_minor: int # kuruş cinsinden; para asla float tutulmaz
currency: str # 'TRY', 'EUR', 'USD'
payment_method_id: str
auto_renew: bool
created_at: datetime
canceled_at: datetime | None = None
cancellation_reason: str | None = None
status alanı kritik: faturalamanın yapılıp yapılmayacağını, müşterinin hizmete erişip erişemeyeceğini ve hangi işlemlerin mümkün olduğunu o belirler. Tutarı kuruş cinsinden tam sayı olarak tutmak da yuvarlama hatalarının önüne geçer.
2. Olay güdümlü mimari
Faturalama mantığını uygulamaya sıkı bağlamak yerine olay yayınlayın ve ilgili servisler kendi işini yapsın:
# Abonelik oluşturulduğunda yayınlanan olay
event = {
"event_type": "subscription.created",
"subscription_id": "sub_12345",
"customer_id": "cust_67890",
"plan_id": "plan_monthly",
"billing_date": "2026-01-15",
"amount_minor": 29900,
"currency": "TRY",
"occurred_at": "2026-01-01T10:00:00Z"
}
# Bu olay şunları tetikler:
# - bildirim servisi
# - analitik
# - faturalama zamanlayıcısı
# - muhasebe ve e-fatura entegrasyonu
Olayı veritabanı işlemiyle aynı transaction’da bir outbox tablosuna yazmak, “kayıt oluştu ama olay kayboldu” durumunu ortadan kaldırır.
3. Faturalama zamanlayıcısı
Tekrarlayan çekimler güvenilir biçimde çalışmalı. Burada en önemli özellik idempotency, yani aynı işin iki kez çalışmasının iki kez para çekmemesi:
def process_upcoming_subscriptions(target_date):
subscriptions = db.query(
"SELECT * FROM subscriptions "
"WHERE next_billing_date <= %s AND status = 'active'",
(target_date,),
)
for sub in subscriptions:
# Abonelik + dönem aynıysa anahtar aynıdır: tekrar deneme çift çekim yapmaz
job_id = f"{sub.subscription_id}:{sub.current_period_end:%Y-%m-%d}"
queue.enqueue("process_billing_event", job_id=job_id,
subscription_id=sub.subscription_id)
Aynı anahtarı ödeme sağlayıcısına da (idempotency key) iletin; böylece ağ hatasından sonra yapılan yeniden deneme de güvenli olur.
Başarısız ödemeler
Her çekim ilk denemede başarılı olmaz: yetersiz bakiye, süresi dolmuş kart, banka tarafındaki geçici hata. İyi bir tahsilat takibi stratejisi şart.
Yeniden deneme takvimi
- 1. gün: ilk çekim denemesi
- 3. gün: müşteriye bildirim, ardından ilk yeniden deneme
- 5. gün: ikinci yeniden deneme
- 7. gün: askıya almadan önceki son deneme
- 14. gün: hâlâ ödenmediyse iptal
Denemeler arasında müşteriye kartını güncellemesi için zaman tanıyın ve birden fazla kanaldan haber verin (e-posta, uygulama içi bildirim, SMS). SMS ve e-postanın ticari ileti sayılıp sayılmadığını ayırın: hizmetle ilgili bilgilendirme ile pazarlama mesajı farklı kurallara tabidir.
Kademeli erişim kısıtlaması
Ödeme başarısız olduğunda erişimi hemen kesmek yerine kısıtlamak, müşteriyle ilişkiyi korur:
def access_level(subscription, days_since_failure):
if subscription.status == "active":
return AccessLevel.FULL
if subscription.status == "payment_failed":
# tahsilat takibi süresince salt okunur erişim
return AccessLevel.READONLY if days_since_failure < 14 else AccessLevel.NONE
return AccessLevel.NONE
Plan değişiklikleri ve kıstelyevm
Müşteriler dönem ortasında plan yükseltir, düşürür ya da aboneliği duraklatır. Bu durumda kalan günlerin hesabı doğru yapılmalı.
Örnek hesap
30 günlük dönemin 15. gününde aylık 100 TL’lik plandan 200 TL’lik plana geçen müşteri:
Dönemde kalan gün: 15
Eski planın kullanılmayan kısmı: 100 × 15/30 = 50 TL alacak
Yeni planın kalan dönemi: 200 × 15/30 = 100 TL borç
Bugün çekilecek net tutar: 100 − 50 = 50 TL
Hesabı her zaman kuruş cinsinden tam sayıyla yapın ve yuvarlama kuralını (aşağı, yukarı, en yakın) tek bir yerde tanımlayın. Kullanılmayan süreyi alacak olarak yansıtmak müşteri güvenini artırır.
Vergi ve uyum
KDV ve e-fatura
- Türkiye’de satışın faturası e-Fatura ya da e-Arşiv olarak düzenlenir; abonelik sistemi her başarılı çekimden sonra faturayı bir entegratör üzerinden kesmeli ve fatura numarasını çekime bağlamalıdır.
- Kurumsal müşteride (B2B) yurt dışı satışlarda geçerli bir KDV numarasıyla ters ibraz (reverse charge) uygulanabilir.
- Bireysel müşteride (B2C) dijital hizmetlerde vergi çoğu ülkede müşterinin bulunduğu yere göre hesaplanır.
Tek ülkede satıyorsanız kuralları kendiniz kodlayabilirsiniz; birden fazla ülkede satıyorsanız bir vergi hesaplama servisi kullanmak daha güvenlidir.
Güçlü müşteri doğrulaması
AB’de PSD2 kapsamındaki güçlü müşteri doğrulaması (SCA), çevrim içi kart ödemelerinin çoğunda iki faktörlü onay ister; düşük tutarlı işlemler ve satıcının başlattığı tekrarlayan çekimler gibi istisnalar vardır. Türkiye’de ilk kart kaydı genellikle 3D Secure ile yapılır, sonraki dönemsel çekimler kayıtlı kartla müşteri olmadan (off-session) yürür:
intent = stripe.PaymentIntent.create(
amount=29900, # 299,00 TL, kuruş cinsinden
currency="try",
customer=customer_id,
payment_method=payment_method_id,
off_session=True, # müşteri o an ekranda değil
confirm=True,
)
İzleme ve mutabakat
Faturalama işin kalbidir; izlemesi de ona göre olmalı.
Takip edilecek metrikler
- Tahsilat başarı oranı: başarılı ve başarısız çekimlerin oranı
- Kayıp oranı (churn): aboneliğini bitiren müşterilerin oranı
- Geri kazanım oranı: tahsilat takibinde kurtarılan ödemeler
- MRR (aylık tekrarlayan gelir): SaaS sağlığının temel göstergesi
- Mutabakat farkları: sağlayıcı ile iç kayıtlar arasındaki tutarsızlıklar
Otomatik mutabakat
Her gün ödeme sağlayıcısının kayıtlarını kendi veritabanınızla karşılaştırın:
def reconcile(yesterday, today):
for charge in stripe.Charge.list(created={"gte": yesterday, "lt": today}).auto_paging_iter():
if not db.get_charge(processor_charge_id=charge.id):
alert(f"Sağlayıcıda olup iç kayıtta olmayan çekim: {charge.id}")
Mutabakat tek yönlü yapılmaz: iç kayıtta olup sağlayıcıda olmayan çekimler de aynı rapora girmeli.
Özet
- Idempotent tasarlayın: her işlem güvenle tekrar denenebilmeli
- Olay yayınlayın: faturalama mantığını bağımlı servislerden ayırın
- Yeniden deneme mantığını sağlam kurun: geçici hatalar her zaman olur
- Kullanılmayan süreyi iade edin: adil kıstelyevm güven kazandırır
- Yakından izleyin: faturalama hatalarına anında alarm
- Kapsamlı test edin: dönem sonu, ay sonu, artık yıl, saat dilimi
- Uyumu baştan planlayın: vergi ve fatura kuralları ülkeye göre değişir
Hangi yolu seçmeli
- Sıfırdan kurmak: tam kontrol, ama ciddi mühendislik yatırımı
- Hazır faturalama platformu: Stripe Billing, Recurly, Chargebee gibi; karmaşıklığı azaltır
- Karma yaklaşım: ödeme sağlayıcısının abonelik modülünü temel alıp kendi mantığınızla genişletmek
Çoğu ekip için hazır bir platformla başlayıp gereken yerde kendi mantığını eklemek doğru dengedir: hızlı çıkarsınız, ayrışmanız gereken yerde esneklik kalır.
Abonelik tasarımı hem sistemin kararlılığında hem de gelirde kendini gösterir. Basit başlayın, karmaşıklığı ancak gerektiğinde ekleyin ve güvenilirliği her zaman özellikten önce tutun.
ByteGurus
Yazar