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:
+24
-2
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user