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
This commit is contained in:
2026-08-28 10:37:50 +03:00
parent 3e129c2988
commit 168052c267
26 changed files with 3121 additions and 219 deletions
+128
View File
@@ -0,0 +1,128 @@
/**
* 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 };
}