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
+92
View File
@@ -164,6 +164,98 @@ export function monthlyStats(tenders) {
.map((x) => ({ month: AY_KISA[x.m], kazanim: x.kazanim, katilim: x.katilim }));
}
// --- Kırmızı bayrak / şeffaflık analizi ---
// Kaynak veride bulunan alanlardan hesaplanır; alan yoksa o bayrak üretilmez.
// (Uyarı niteliğindedir — usulsüzlük iddiası değil, incelenmeye değer sinyaldir.)
export function redFlags(tenders) {
const flags = [];
// Firma-kurum yoğunlaşması için sayaçlar
const firmKurum = new Map(); // "firma||kurum" -> sayı
const kurumToplam = new Map(); // kurum -> sonuçlanan ihale sayısı
for (const t of tenders) {
const bedel = parseTL(t.sozlesmeBedeli);
const yaklasik = parseTL(t.yaklasikMaliyet);
const teklif = t.teklifSayisi != null ? Number(t.teklifSayisi) : null;
// 1) Tek teklifli ihale
if (teklif === 1) {
flags.push({
id: `tek-${t.id}`, seviye: 'yuksek', tur: 'Tek teklif',
tender: t,
aciklama: 'İhaleye yalnızca bir geçerli teklif verilmiş — rekabet oluşmamış.',
});
}
// 2) Yaklaşık maliyete çok yakın sözleşme (düşük kırım)
if (bedel != null && yaklasik != null && yaklasik > 0) {
const kirim = 1 - bedel / yaklasik;
if (kirim >= 0 && kirim < 0.03) {
flags.push({
id: `kirim-${t.id}`, seviye: 'orta', tur: 'Düşük kırım',
tender: t, deger: `%${(kirim * 100).toFixed(1)} kırım`,
aciklama: 'Sözleşme bedeli yaklaşık maliyete neredeyse eşit — fiyat rekabeti sınırlı.',
});
}
// 3) Yaklaşık maliyetin üzerinde sözleşme
if (bedel > yaklasik) {
flags.push({
id: `ustu-${t.id}`, seviye: 'yuksek', tur: 'Tahmin üstü bedel',
tender: t, deger: `%${(((bedel - yaklasik) / yaklasik) * 100).toFixed(1)} üzerinde`,
aciklama: 'Sözleşme bedeli idarenin yaklaşık maliyetini aşıyor.',
});
}
// 4) Aşırı düşük teklif riski
if (kirim > 0.4) {
flags.push({
id: `asiri-${t.id}`, seviye: 'orta', tur: 'Aşırı düşük teklif',
tender: t, deger: `%${(kirim * 100).toFixed(1)} kırım`,
aciklama: 'Yaklaşık maliyete göre çok yüksek kırım — sürdürülebilirlik/ifa riski.',
});
}
}
if (t.kazananFirma && t.kurum) {
const key = `${t.kazananFirma.trim()}||${t.kurum.trim()}`;
firmKurum.set(key, (firmKurum.get(key) || 0) + 1);
kurumToplam.set(t.kurum.trim(), (kurumToplam.get(t.kurum.trim()) || 0) + 1);
}
}
// 5) Kurum-firma yoğunlaşması: aynı firma bir kurumun ihalelerinin çoğunu almış
for (const [key, adet] of firmKurum) {
const [firma, kurum] = key.split('||');
const toplam = kurumToplam.get(kurum) || 0;
if (toplam >= 3 && adet / toplam >= 0.6) {
flags.push({
id: `yogun-${key}`, seviye: 'yuksek', tur: 'Tedarikçi yoğunlaşması',
firma, kurum, deger: `${adet}/${toplam} ihale`,
aciklama: `${firma}, bu idarenin sonuçlanan ihalelerinin %${Math.round((adet / toplam) * 100)}'ini almış.`,
});
}
}
const siraDeger = { yuksek: 0, orta: 1, dusuk: 2 };
flags.sort((a, b) => siraDeger[a.seviye] - siraDeger[b.seviye]);
return {
flags,
yuksek: flags.filter((f) => f.seviye === 'yuksek').length,
orta: flags.filter((f) => f.seviye === 'orta').length,
// Analiz edilebilen (sonuç verisi olan) ihale sayısı — şeffaflık için
analiz: tenders.filter((t) => t.kazananFirma || t.sozlesmeBedeli != null || t.teklifSayisi != null).length,
};
}
// --- Pazar yoğunlaşması (HHI) ---
export function marketConcentration(tenders) {
const { firms, totalMarket } = firmStats(tenders);
if (!firms.length || !totalMarket) return { hhi: null, seviye: null, firms: [] };
const hhi = firms.reduce((s, f) => s + Math.pow((f.total / totalMarket) * 100, 2), 0);
const seviye = hhi < 1500 ? 'Rekabetçi' : hhi < 2500 ? 'Orta yoğunlaşma' : 'Yüksek yoğunlaşma';
return { hhi: Math.round(hhi), seviye, firms };
}
// --- Alım türü dağılımı ---
export function alimTuruStats(tenders) {
const map = new Map();