Files
ihaleist/server/billing.mjs
T
hikmettan 168052c267 feat: geçmiş veri motoru + gerçek AI + KİK kararları + TED + kırmızı bayrak + planlar
Roadmap'in 6 maddesi de gerçek çalışır halde:

1) Geçmiş veri motoru (EKAP v2)
   - server/ekap-v2.mjs: AES-256-CBC imzalı EKAP v2 istemcisi (zero-dep),
     ihale arama + sonuç ilanı parse (kazanan firma, sözleşme bedeli,
     yaklaşık maliyet, teklif sayısı), TR firma adı normalize
   - server/history-store.mjs: SQLite deposu (IKN+kısım anahtarlı, upsert)
   - server/ekap-scan.mjs: tarih aralıklı artımlı tarayıcı (6 paralel)
   - server/scheduler.mjs: sunucu içi periyodik tarama (SCAN_* env ile)
   - /api/tenders artık geçmiş sonuçları da birleştiriyor → kazanan firma,
     rakip ve kırmızı bayrak analizleri gerçek veriyle doluyor

2) Gerçek AI şartname analizi
   - server/ai.mjs: 6 sağlayıcılı fallback zinciri (GLM/DeepSeek/Gemini/
     Grok/GPT/Claude), JSON şemalı çıktı, uydurma yasak promptu
   - pdfjs-dist ile tarayıcıda PDF metin çıkarma (dinamik import)
   - AiSpecAnalyzerModal: sahte sonuç kaldırıldı, gerçek dosya + dürüst hata

3) KİK kurul kararları
   - server/kik.mjs: EKAP v2 JSON API (Sec-1..6 imzalı), uyuşmazlık/
     düzenleyici/mahkeme; karar tam metni (ayrı AES anahtarıyla URL)
   - KikDecisions sekmesi: arama + takipteki idarelerden hızlı öneri

4) Fiyatlandırma + ödeme
   - server/billing.mjs: Stripe Checkout (zero-dep, form-encoded)
   - Pricing sekmesi: 3 plan, aylık/yıllık, yapılandırılmadıysa dürüst uyarı

5) TED (AB ihaleleri)
   - server/ted.mjs: anonim TED v3 API, 3 mod (açık/tüm TR/AB'de kazanan TR)
   - rate-limit korumalı (429 HTML gövdesi ele alınır), çok dilli alan çözümü

6) Kırmızı bayrak & şeffaflık
   - analytics.js: redFlags (tek teklif, düşük kırım, tahmin üstü bedel,
     aşırı düşük teklif, tedarikçi yoğunlaşması) + marketConcentration (HHI)
   - RedFlags sekmesi: seviyeli uyarılar + yöntem şeffaflığı notu
2026-08-28 10:37:50 +03:00

129 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Abonelik / ödeme — Stripe Checkout (zero-dependency, form-encoded REST).
*
* Env:
* STRIPE_SECRET_KEY — sk_live_... / sk_test_...
* STRIPE_PRICE_PRO — Pro planın Stripe Price ID'si (price_...)
* STRIPE_PRICE_KURUMSAL — Kurumsal planın Price ID'si
* APP_URL — başarı/iptal dönüş adresi (örn. https://ihaleist.seymata.com)
*
* Not: Abonelik hakkının kalıcı tanınması için Stripe webhook'u + kullanıcı
* kaydı gerekir (bkz. /api/billing/webhook). Anahtar yoksa uçlar "yapılandırılmadı"
* döner; uygulama ücretsiz katmanda çalışmaya devam eder.
*/
export const PLANS = [
{
id: 'ucretsiz',
ad: 'Ücretsiz',
fiyat: 0,
periyot: 'ay',
ozet: 'İhaleleri keşfedin, takibe alın.',
ozellikler: [
'Canlı ihale + doğrudan temin listesi',
'Gelişmiş arama ve filtreler',
'Favori kurumlar',
'10 ihale takibi',
'Tarayıcı bildirimleri',
],
sinirlar: { takip: 10, ekip: 1 },
},
{
id: 'pro',
ad: 'Pro',
fiyat: 899,
periyot: 'ay',
vurgu: true,
rozet: 'En popüler',
ozet: 'Teklif hazırlayan firmalar için tam analiz.',
ozellikler: [
'Ücretsiz plandaki her şey',
'Sınırsız ihale takibi',
'Kanban süreç yönetimi',
'Kazanan firma & rakip analizi',
'Kalem bazlı fiyat analizi',
'AI şartname analizi',
'PDF teklif cetveli (kaşe/imza)',
'E-posta bildirimleri',
'5 ekip üyesi',
],
sinirlar: { takip: null, ekip: 5 },
priceEnv: 'STRIPE_PRICE_PRO',
},
{
id: 'kurumsal',
ad: 'Kurumsal',
fiyat: 2490,
periyot: 'ay',
ozet: 'Çok kullanıcılı ekipler ve entegrasyon.',
ozellikler: [
'Pro plandaki her şey',
'Sınırsız ekip üyesi',
'Kırmızı bayrak & şeffaflık analizi',
'KİK kurul kararı eşleştirme',
'AB ihaleleri (TED) katmanı',
'API erişimi & webhook',
'Öncelikli destek',
],
sinirlar: { takip: null, ekip: null },
priceEnv: 'STRIPE_PRICE_KURUMSAL',
},
];
export function billingConfigured() {
return Boolean(process.env.STRIPE_SECRET_KEY);
}
export function planCatalog() {
return PLANS.map((p) => ({
...p,
satinAlinabilir: Boolean(p.priceEnv && process.env[p.priceEnv] && billingConfigured()),
}));
}
function formEncode(obj) {
const parts = [];
const walk = (prefix, val) => {
if (val === undefined || val === null) return;
if (Array.isArray(val)) val.forEach((v, i) => walk(`${prefix}[${i}]`, v));
else if (typeof val === 'object') Object.entries(val).forEach(([k, v]) => walk(`${prefix}[${k}]`, v));
else parts.push(`${encodeURIComponent(prefix)}=${encodeURIComponent(String(val))}`);
};
Object.entries(obj).forEach(([k, v]) => walk(k, v));
return parts.join('&');
}
export async function createCheckoutSession({ planId, email }) {
const key = process.env.STRIPE_SECRET_KEY;
if (!key) throw new Error('Ödeme yapılandırılmamış (STRIPE_SECRET_KEY yok)');
const plan = PLANS.find((p) => p.id === planId);
if (!plan || !plan.priceEnv) throw new Error('Geçersiz plan');
const price = process.env[plan.priceEnv];
if (!price) throw new Error(`${plan.ad} planı için ${plan.priceEnv} tanımlı değil`);
const appUrl = process.env.APP_URL || 'https://ihaleist.seymata.com';
const body = formEncode({
mode: 'subscription',
'line_items[0][price]': price,
'line_items[0][quantity]': 1,
success_url: `${appUrl}/?odeme=basarili&plan=${planId}`,
cancel_url: `${appUrl}/?odeme=iptal`,
customer_email: email,
allow_promotion_codes: 'true',
'metadata[planId]': planId,
'metadata[email]': email || '',
});
const res = await fetch('https://api.stripe.com/v1/checkout/sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${key}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.error?.message || `Stripe hatası: HTTP ${res.status}`);
return { url: data.url, id: data.id };
}