feat: abonelik iptal ekranı (ayrılma nedeni + açıklama) & gerçek abonelik rozeti (yönetici erişimi aboneliği gizlemesin)

This commit is contained in:
2026-08-29 21:47:58 +03:00
parent 5d502c8ae4
commit 65997055fe
8 changed files with 261 additions and 7 deletions
+42
View File
@@ -187,3 +187,45 @@ export async function createCheckoutSession({ planId, email, periyot = 'yillik'
if (!res.ok) throw new Error(data?.error?.message || `Stripe hatası: HTTP ${res.status}`);
return { url: data.url, id: data.id };
}
/** Ayrılma nedeni seçenekleri — arayüz ve Stripe aynı listeyi kullanır. */
export const IPTAL_NEDENLERI = [
{ id: 'too_expensive', label: 'Fiyat bütçemize uygun değil' },
{ id: 'missing_features', label: 'İhtiyacım olan özellik yok' },
{ id: 'switched_service', label: 'Başka bir hizmete geçtim' },
{ id: 'unused', label: 'Yeterince kullanmıyorum' },
{ id: 'too_complex', label: 'Kullanımı karmaşık geldi' },
{ id: 'low_quality', label: 'Veri/sonuç kalitesi beklentimi karşılamadı' },
{ id: 'customer_service', label: 'Destekten memnun kalmadım' },
{ id: 'other', label: 'Diğer' },
];
/**
* Aboneliği dönem sonunda iptal eder (erişim ödenen sürenin sonuna kadar sürer)
* ve ayrılma nedenini Stripe'a işler.
*/
export async function cancelSubscription({ subId, neden, aciklama }) {
const key = process.env.STRIPE_SECRET_KEY;
if (!key) throw new Error('Ödeme yapılandırılmamış (STRIPE_SECRET_KEY yok)');
if (!subId) throw new Error('İptal edilecek abonelik bulunamadı');
const govde = { cancel_at_period_end: 'true' };
if (IPTAL_NEDENLERI.some((n) => n.id === neden)) {
govde['cancellation_details[feedback]'] = neden;
}
const not = String(aciklama || '').trim().slice(0, 500);
if (not) govde['cancellation_details[comment]'] = not;
const res = await fetch(`https://api.stripe.com/v1/subscriptions/${encodeURIComponent(subId)}`, {
method: 'POST',
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/x-www-form-urlencoded' },
body: formEncode(govde),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.error?.message || `Stripe hatası: HTTP ${res.status}`);
return {
id: data.id,
donemSonunda: Boolean(data.cancel_at_period_end),
bitis: data.cancel_at ? new Date(data.cancel_at * 1000).toISOString() : null,
};
}