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
+24 -2
View File
@@ -20,7 +20,7 @@ import { readDtTenders } from './dt-store.mjs';
import { loginWithGoogle, userFromAuthHeader } from './auth.mjs';
import { emailConfigured, sendEmail, tenderDigestHtml, emailFrom } from './email.mjs';
import { aiConfigured, availableProviders, analyzeSpec } from './ai.mjs';
import { billingConfigured, webhookConfigured, planCatalog, createCheckoutSession, verifyWebhook, planForPrice } from './billing.mjs';
import { billingConfigured, webhookConfigured, planCatalog, createCheckoutSession, verifyWebhook, planForPrice, cancelSubscription, IPTAL_NEDENLERI } from './billing.mjs';
import * as store from './user-store.mjs';
import { planOf, haklar, kullanim, kotaKontrol, ozellikVar } from './entitlements.mjs';
import { searchTed } from './ted.mjs';
@@ -679,6 +679,28 @@ const server = http.createServer(async (req, res) => {
// --- Stripe webhook (imza doğrulamalı) ---
// NOT: Bu uç kimlik doğrulaması İSTEMEZ; güvenlik imza doğrulamasıyla sağlanır.
if (url.pathname === '/api/billing/cancel' && req.method === 'POST') {
const user = userFromAuthHeader(req.headers['authorization']);
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
const kayit = store.getUser(user.email);
const subId = kayit?.stripe_sub_id;
if (!subId) return json(res, 400, { success: false, error: 'Aktif bir aboneliğiniz görünmüyor.' });
const body = await readJson(req);
try {
const out = await cancelSubscription({ subId, neden: body.neden, aciklama: body.aciklama });
// Neden kaydı: ürün kararları için kendi tarafımızda da tutulur.
store.setData(user.email, 'iptal_geri_bildirim', {
neden: body.neden || null,
aciklama: String(body.aciklama || '').slice(0, 500),
tarih: new Date().toISOString(),
});
console.log(`[billing] ${user.email} aboneliğini iptal etti (neden: ${body.neden || 'belirtilmedi'})`);
return json(res, 200, { success: true, ...out });
} catch (e) {
return json(res, 502, { success: false, error: e.message });
}
}
if (url.pathname === '/api/billing/webhook' && req.method === 'POST') {
if (!webhookConfigured()) {
return json(res, 503, { success: false, error: 'STRIPE_WEBHOOK_SECRET tanımlı değil' });
@@ -750,7 +772,7 @@ const server = http.createServer(async (req, res) => {
// --- Abonelik / ödeme ---
if (url.pathname === '/api/billing/plans') {
return json(res, 200, { configured: billingConfigured(), webhookHazir: webhookConfigured(), plans: planCatalog() });
return json(res, 200, { configured: billingConfigured(), webhookHazir: webhookConfigured(), plans: planCatalog(), iptalNedenleri: IPTAL_NEDENLERI });
}
if (url.pathname === '/api/billing/checkout' && req.method === 'POST') {
const user = userFromAuthHeader(req.headers['authorization']);