Files
ihaleist/server/notifier.mjs
T
hikmettan c64889782f feat: yönetici için 'şimdi gönder' (POST /api/notify/run?zorla=1)
Bildirim turunu temel-alma adımını atlayarak çalıştırır; abonelik kriterine uyan
ihaleler hemen e-posta olarak gönderilir. Test ve talep üzerine özet için.
2026-08-29 03:22:03 +03:00

103 lines
3.9 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.
/**
* Otomatik ihale bildirimi — abonelik kriterlerine uyan YENİ ihaleleri bulur
* ve özet e-posta gönderir.
*
* Env:
* NOTIFY_ENABLED=0 → kapat
* NOTIFY_INTERVAL_SAAT=6 → kontrol sıklığı
* NOTIFY_MAX=25 → e-postada listelenecek en fazla ihale
*/
import { list, markSent, available } from './notify-store.mjs';
import { emailConfigured, sendEmail, tenderDigestHtml } from './email.mjs';
const norm = (v) => String(v || '').trim().toLocaleUpperCase('tr-TR');
/** Bir ihale, aboneliğin kriterlerine uyuyor mu? */
export function eslesiyorMu(t, a) {
if (a.sehir && norm(t.sehir) !== norm(a.sehir)) return false;
if (a.sektor && norm(t.sektor) !== norm(a.sektor)) return false;
if (a.alimTuru && !norm(t.alimTuru).includes(norm(a.alimTuru))) return false;
if (a.anahtar) {
const q = norm(a.anahtar);
const alan = norm(`${t.baslik || ''} ${t.kurum || ''} ${(t.items || []).map((i) => i.ad).join(' ')}`);
if (!alan.includes(q)) return false;
}
if (a.kurumlar?.length) {
const kurum = norm(t.kurum);
if (!a.kurumlar.some((k) => norm(k) === kurum)) return false;
}
return true;
}
/** Sonuçlanmış/arşiv kayıtları bildirime girmez — yalnızca teklif verilebilir ilanlar. */
function bildirilebilir(t) {
if (t.durum === 'Sonuçlandı') return false;
if (String(t.id || '').startsWith('hist-')) return false;
return true;
}
/**
* Tüm aktif abonelikleri işler.
* @param {Function} tenderGetir () => Promise<{tenders: []}>
*/
export async function bildirimleriIsle(tenderGetir, { zorla = false } = {}) {
if (process.env.NOTIFY_ENABLED === '0') return { atlandi: 'kapalı' };
if (!available()) return { atlandi: 'depo yok' };
if (!emailConfigured()) return { atlandi: 'e-posta yapılandırılmamış' };
const abonelikler = list();
if (!abonelikler.length) return { abonelik: 0 };
const veri = await tenderGetir();
const tumu = (veri.tenders || []).filter(bildirilebilir);
const maxAdet = Number(process.env.NOTIFY_MAX || 25);
let gonderilen = 0;
let hata = 0;
for (const a of abonelikler) {
try {
const gorulen = new Set(a.sonGorulenler || []);
const eslesen = tumu.filter((t) => eslesiyorMu(t, a));
const yeniler = eslesen.filter((t) => !gorulen.has(t.id));
// İlk kurulumda geçmişi spam'lemeyelim: sadece işaretle, gönderme.
// zorla=true ise (yönetici "şimdi gönder" dediğinde) bu adım atlanır.
if (!a.sonGonderim && !zorla) {
markSent(a.id, eslesen.map((t) => t.id));
continue;
}
const gonderilecek = zorla && !yeniler.length ? eslesen.slice(0, maxAdet) : yeniler;
if (!gonderilecek.length) continue;
const kriterMetni = [
a.sehir && `İl: ${a.sehir}`,
a.sektor && `Sektör: ${a.sektor}`,
a.alimTuru && `Tür: ${a.alimTuru}`,
a.anahtar && `Anahtar: ${a.anahtar}`,
a.kurumlar?.length && `${a.kurumlar.length} favori kurum`,
].filter(Boolean).join(' · ') || 'Tüm ihaleler';
const html = tenderDigestHtml({ name: a.ad || a.email, tenders: gonderilecek.slice(0, maxAdet) })
+ `<p style="font-family:Arial,sans-serif;font-size:11px;color:#8b93a4;max-width:640px;margin:12px auto 0">
Kriteriniz: ${kriterMetni} — toplam ${gonderilecek.length} ihale.
Bu bildirimi uygulamadaki Mobil PWA → E-posta Bildirimi bölümünden kapatabilirsiniz.
</p>`;
await sendEmail({
to: a.email,
subject: `İhaleist — ${gonderilecek.length} ihale (${kriterMetni})`,
html,
});
markSent(a.id, eslesen.map((t) => t.id));
gonderilen++;
console.log(`[notifier] ${a.email} → ${gonderilecek.length} ihale gönderildi (${kriterMetni})`);
} catch (e) {
hata++;
console.warn(`[notifier] abonelik ${a.id} gönderilemedi: ${e.message}`);
}
}
return { abonelik: abonelikler.length, gonderilen, hata };
}