feat: otomatik e-posta bildirimi
Kullanıcı kriter kaydediyor; sunucu periyodik olarak eşleşen YENİ ihaleleri bulup özet e-posta gönderiyor. - server/notify-store.mjs: abonelik deposu (SQLite, kalıcı volume'de) kriterler: il · sektör · alım türü · anahtar kelime · favori kurumlar - server/notifier.mjs: eşleştirme + gönderim motoru · aynı ihale iki kez bildirilmez (sonGorulenler takibi) · ilk abonelikte geçmiş bildirilmez, yalnızca temel alınır (spam önleme) · sonuçlanmış/arşiv kayıtları bildirime girmez - /api/notify/subscriptions (GET/POST/DELETE) + /api/notify/run (admin, anlık test) - scheduler: NOTIFY_INTERVAL_SAAT (varsayılan 6) ile periyodik tur - NotifySubscriptions paneli: kriter ekle/sil, 'ekrandaki filtreleri kullan' - tendersPayload ortak yardımcısına çıkarıldı (API ve bildirim aynı veriyi kullanır)
This commit is contained in:
+67
-32
@@ -25,6 +25,8 @@ import { readHistory, historyStats } from './history-store.mjs';
|
||||
import { startScheduler, schedulerStatus } from './scheduler.mjs';
|
||||
import { searchKik, kararMetni } from './kik.mjs';
|
||||
import { documentUrl, ilanMetni, documentText, browserAvailable } from './documents.mjs';
|
||||
import { subscribe, list as listSubs, remove as removeSub, available as notifyAvailable } from './notify-store.mjs';
|
||||
import { bildirimleriIsle } from './notifier.mjs';
|
||||
|
||||
// ilan.gov.tr sunucusu ara sertifikayı (GeoTrust TLS RSA CA G1) göndermiyor;
|
||||
// zinciri tamamlamak için PEM'i yerelden ekliyoruz.
|
||||
@@ -395,6 +397,33 @@ function readBody(req, limit = 100 * 1024) {
|
||||
});
|
||||
}
|
||||
|
||||
// /api/tenders gövdesini üretir; bildirim işi de aynı veriyi kullanır.
|
||||
async function tendersPayload(force = false) {
|
||||
const c = await getTenders(force);
|
||||
let dt = { tenders: [], totalCount: 0, available: false };
|
||||
try { dt = readDtTenders(DT_LIMIT); }
|
||||
catch (e) { console.warn('[ihaleist-api] DT okuma hatası:', e.message); }
|
||||
let hist = { tenders: [], totalCount: 0, available: false };
|
||||
try { hist = readHistory(HISTORY_LIMIT); }
|
||||
catch (e) { console.warn('[ihaleist-api] geçmiş okuma hatası:', e.message); }
|
||||
|
||||
const sources = ['ilan.gov.tr (Basın İlan Kurumu) — resmi ihale ilanları'];
|
||||
if (dt.available) sources.push('EKAP Doğrudan Temin (22/d) worker');
|
||||
if (hist.available && hist.totalCount) sources.push('EKAP Sonuç İlanları (geçmiş sonuçlar)');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
source: sources.join(' + '),
|
||||
fetchedAt: new Date(c.fetchedAt).toLocaleString('tr-TR'),
|
||||
totalCount: c.totalCount + dt.totalCount + hist.totalCount,
|
||||
ilanCount: c.totalCount,
|
||||
todayCount: c.todayCount,
|
||||
dtCount: dt.totalCount,
|
||||
historyCount: hist.totalCount,
|
||||
tenders: [...dt.tenders, ...c.tenders, ...hist.tenders],
|
||||
};
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
try {
|
||||
@@ -561,6 +590,40 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Otomatik bildirim abonelikleri ---
|
||||
if (url.pathname === '/api/notify/subscriptions') {
|
||||
const user = userFromAuthHeader(req.headers['authorization']);
|
||||
if (!user) return json(res, 401, { success: false, error: 'oturum yok' });
|
||||
if (!notifyAvailable()) return json(res, 503, { success: false, error: 'Bildirim deposu kullanılamıyor' });
|
||||
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
const body = await readBody(req);
|
||||
const abone = subscribe(user.email, body.kriter || {}, user.name);
|
||||
return json(res, 200, { success: true, abonelikler: abone });
|
||||
} catch (e) {
|
||||
return json(res, 400, { success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
if (req.method === 'DELETE') {
|
||||
const id = Number(url.searchParams.get('id'));
|
||||
const ok = removeSub(id, user.email);
|
||||
return json(res, ok ? 200 : 404, { success: ok, abonelikler: listSubs(user.email) });
|
||||
}
|
||||
return json(res, 200, { success: true, abonelikler: listSubs(user.email) });
|
||||
}
|
||||
// Aboneliklerin hemen işlenmesi (admin/test)
|
||||
if (url.pathname === '/api/notify/run' && req.method === 'POST') {
|
||||
const user = userFromAuthHeader(req.headers['authorization']);
|
||||
if (!user?.isAdmin) return json(res, 403, { success: false, error: 'yönetici yetkisi gerekli' });
|
||||
try {
|
||||
const sonuc = await bildirimleriIsle(() => tendersPayload());
|
||||
return json(res, 200, { success: true, ...sonuc });
|
||||
} catch (e) {
|
||||
return json(res, 502, { success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
// --- E-posta bildirimi (Resend) ---
|
||||
if (url.pathname === '/api/notify/email/config') {
|
||||
return json(res, 200, { configured: emailConfigured() });
|
||||
@@ -585,37 +648,8 @@ const server = http.createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/tenders') {
|
||||
const force = url.searchParams.get('refresh') === '1';
|
||||
const c = await getTenders(force);
|
||||
// Doğrudan temin (EKAP worker) verisini varsa birleştir
|
||||
let dt = { tenders: [], totalCount: 0, available: false };
|
||||
try {
|
||||
dt = readDtTenders(DT_LIMIT);
|
||||
} catch (e) {
|
||||
console.warn('[ihaleist-api] DT okuma hatası:', e.message);
|
||||
}
|
||||
// Geçmiş sonuç ilanları (kazanan firma + bedel + teklif sayısı)
|
||||
let hist = { tenders: [], totalCount: 0, available: false };
|
||||
try {
|
||||
hist = readHistory(HISTORY_LIMIT);
|
||||
} catch (e) {
|
||||
console.warn('[ihaleist-api] geçmiş okuma hatası:', e.message);
|
||||
}
|
||||
const sources = ['ilan.gov.tr (Basın İlan Kurumu) — resmi ihale ilanları'];
|
||||
if (dt.available) sources.push('EKAP Doğrudan Temin (22/d) worker');
|
||||
if (hist.available && hist.totalCount) sources.push('EKAP Sonuç İlanları (geçmiş sonuçlar)');
|
||||
return json(res, 200, {
|
||||
success: true,
|
||||
source: sources.join(' + '),
|
||||
fetchedAt: new Date(c.fetchedAt).toLocaleString('tr-TR'),
|
||||
// Havuz = canlı ilan + doğrudan temin + sonuç arşivi (yüklü sayısıyla tutarlı olsun)
|
||||
totalCount: c.totalCount + dt.totalCount + hist.totalCount,
|
||||
ilanCount: c.totalCount,
|
||||
todayCount: c.todayCount,
|
||||
dtCount: dt.totalCount,
|
||||
historyCount: hist.totalCount,
|
||||
tenders: [...dt.tenders, ...c.tenders, ...hist.tenders],
|
||||
});
|
||||
const out = await tendersPayload(url.searchParams.get('refresh') === '1');
|
||||
return json(res, 200, out);
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/dt') {
|
||||
@@ -666,5 +700,6 @@ const server = http.createServer(async (req, res) => {
|
||||
server.listen(PORT, () => {
|
||||
console.log(`[ihaleist-api] http://localhost:${PORT} — canlı veri: ilan.gov.tr`);
|
||||
getTenders().catch((e) => console.error('[ihaleist-api] ilk yükleme hatası:', e.message));
|
||||
startScheduler();
|
||||
// Bildirim işi tender verisine ihtiyaç duyar; enjekte ederek başlat
|
||||
startScheduler({ tenderGetir: () => tendersPayload() });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user