Keep unsent notification batches pending and retain seen history

This commit is contained in:
2026-09-06 13:00:12 +03:00
parent d2d66c8e9d
commit 4c4ba0c4c8
5 changed files with 82 additions and 23 deletions
+25 -15
View File
@@ -1,3 +1,4 @@
import {deadlineInfo} from '../src/services/tenderCalendar.js';
/**
* Otomatik ihale bildirimi — abonelik kriterlerine uyan YENİ ihaleleri bulur
* ve özet e-posta gönderir.
@@ -8,7 +9,7 @@
* NOTIFY_MAX=25 → e-postada listelenecek en fazla ihale
*/
import { list, markSent, available } from './notify-store.mjs';
import { list, markSent, markBaseline, available } from './notify-store.mjs';
import { emailConfigured, sendEmail, tenderDigestHtml } from './email.mjs';
import { telegramConfigured, sendTelegram, tgKacir } from './telegram.mjs';
import { getData } from './user-store.mjs';
@@ -38,8 +39,9 @@ export function eslesiyorMu(t, a) {
}
/** Sonuçlanmış/arşiv kayıtları bildirime girmez — yalnızca teklif verilebilir ilanlar. */
function bildirilebilir(t) {
if (t.durum === 'Sonuçlandı') return false;
export function bildirilebilir(t,now=new Date()) {
if (['SONUÇLANDI','İPTAL','İPTAL EDİLDİ','İPTAL EDİLMİŞ'].includes(norm(t.durum))) return false;
if(deadlineInfo(t,now).expired!==false)return false;
if (String(t.id || '').startsWith('hist-')) return false;
return true;
}
@@ -48,17 +50,24 @@ function bildirilebilir(t) {
* Tüm aktif abonelikleri işler.
* @param {Function} tenderGetir () => Promise<{tenders: []}>
*/
export async function bildirimleriIsle(tenderGetir, { zorla = false } = {}) {
let processing=false;
export async function bildirimleriIsle(tenderGetir, { zorla = false } = {}, dependencies={}) {
const {readSubscriptions=list,saveSent=markSent,saveBaseline=markBaseline,configured=emailConfigured,
deliverEmail=sendEmail,telegramReady=telegramConfigured,deliverTelegram=sendTelegram,now=()=>new Date()}=dependencies;
if(processing)return {atlandi:'önceki tur sürüyor'};
processing=true;
try {
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ış' };
if (!configured()) return { atlandi: 'e-posta yapılandırılmamış' };
const abonelikler = list();
const abonelikler = readSubscriptions();
if (!abonelikler.length) return { abonelik: 0 };
const veri = await tenderGetir();
const tumu = (veri.tenders || []).filter(bildirilebilir);
const maxAdet = Number(process.env.NOTIFY_MAX || 25);
const tumu = (veri.tenders || []).filter(t=>bildirilebilir(t,now()));
const requested=Number(process.env.NOTIFY_MAX || 25);
const maxAdet=Number.isInteger(requested)&&requested>0?Math.min(requested,200):25;
let gonderilen = 0;
let hata = 0;
@@ -71,11 +80,11 @@ export async function bildirimleriIsle(tenderGetir, { zorla = false } = {}) {
// İ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));
if (!a.initialized && !zorla) {
saveBaseline(a.id, eslesen.map((t) => t.id));
continue;
}
const gonderilecek = zorla && !yeniler.length ? eslesen.slice(0, maxAdet) : yeniler;
const gonderilecek = (zorla && !yeniler.length ? eslesen : yeniler).slice(0,maxAdet);
if (!gonderilecek.length) continue;
const kriterMetni = [
@@ -93,20 +102,20 @@ export async function bildirimleriIsle(tenderGetir, { zorla = false } = {}) {
Bu bildirimi uygulamadaki Mobil PWA → E-posta Bildirimi bölümünden kapatabilirsiniz.
</p>`;
await sendEmail({
await deliverEmail({
to: a.email,
subject: `İhaleist — ${gonderilecek.length} ihale (${kriterMetni})`,
html,
});
// Telegram bağlıysa aynı özet oraya da (kısa biçim)
try {
const tg = telegramConfigured() ? getData(a.email, 'telegram') : null;
const tg = telegramReady() ? getData(a.email, 'telegram') : null;
if (tg?.chatId) {
const satirlar = gonderilecek.slice(0, 10).map((t) => `• ${tgKacir(t.baslik)}\n ${tgKacir(t.kurum || '')} · ${tgKacir(t.tarih || '')} · ${tgKacir(t.ikn || '')}`);
await sendTelegram(tg.chatId, `<b>İhaleist — ${gonderilecek.length} yeni ihale</b> (${tgKacir(kriterMetni)})\n\n${satirlar.join('\n')}${gonderilecek.length > 10 ? `\n… +${gonderilecek.length - 10} ihale` : ''}`);
await deliverTelegram(tg.chatId, `<b>İhaleist — ${gonderilecek.length} yeni ihale</b> (${tgKacir(kriterMetni)})\n\n${satirlar.join('\n')}${gonderilecek.length > 10 ? `\n… +${gonderilecek.length - 10} ihale` : ''}`);
}
} catch (e) { console.warn(`[notifier] telegram ${a.email}: ${e.message}`); }
markSent(a.id, eslesen.map((t) => t.id));
saveSent(a.id, gonderilecek.map((t) => t.id));
gonderilen++;
console.log(`[notifier] ${a.email} → ${gonderilecek.length} ihale gönderildi (${kriterMetni})`);
} catch (e) {
@@ -115,4 +124,5 @@ export async function bildirimleriIsle(tenderGetir, { zorla = false } = {}) {
}
}
return { abonelik: abonelikler.length, gonderilen, hata };
}finally {processing=false;}
}
+15 -8
View File
@@ -147,6 +147,7 @@ export function list(email) {
konular: (() => { try { return JSON.parse(r.konular || '[]'); } catch { return []; } })(),
aktif: Boolean(r.aktif),
sonGonderim: r.sonGonderim,
initialized: r.sonGorulenler !== null,
sonGorulenler: (() => { try { return JSON.parse(r.sonGorulenler || '[]'); } catch { return []; } })(),
}));
} finally {
@@ -165,13 +166,19 @@ export function remove(id, email) {
}
}
export function markSent(id, ihaleIdleri) {
const db = open({ create: false });
if (!db) return;
function recordSeen(id,ihaleIdleri,sent) {
const db=open({create:false});
if(!db)throw new Error('Bildirim deposu kullanılamıyor');
try {
db.prepare('UPDATE abonelikler SET sonGonderim=?, sonGorulenler=? WHERE id=?')
.run(new Date().toISOString(), JSON.stringify(ihaleIdleri.slice(0, 400)), id);
} finally {
db.close();
}
db.exec('BEGIN IMMEDIATE');
const row=db.prepare('SELECT sonGorulenler FROM abonelikler WHERE id=?').get(id);
if(!row)throw new Error('Abonelik artık yok.');
const seen=[...new Set([...JSON.parse(row.sonGorulenler || '[]'),...ihaleIdleri])];
db.prepare('UPDATE abonelikler SET sonGorulenler=?,sonGonderim=CASE WHEN ? THEN ? ELSE sonGonderim END WHERE id=?')
.run(JSON.stringify(seen),sent?1:0,new Date().toISOString(),id);
db.exec('COMMIT');
}catch(error){try{db.exec('ROLLBACK');}catch{}throw error;}
finally{db.close();}
}
export const markSent=(id,ids)=>recordSeen(id,ids,true);
export const markBaseline=(id,ids)=>recordSeen(id,ids,false);