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)
129 lines
3.6 KiB
JavaScript
129 lines
3.6 KiB
JavaScript
/**
|
||
* Otomatik e-posta bildirimi abonelikleri (SQLite).
|
||
* Kullanıcı kendi filtre kriterlerini kaydeder; zamanlayıcı yeni eşleşen
|
||
* ihaleleri bulup özet e-posta gönderir.
|
||
*/
|
||
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
let DatabaseSync = null;
|
||
try {
|
||
({ DatabaseSync } = await import('node:sqlite'));
|
||
} catch {
|
||
console.warn('[notify-store] node:sqlite yok — otomatik bildirim devre dışı');
|
||
}
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
const DATA_DIR = path.join(__dirname, 'data');
|
||
const DB_PATH = path.join(DATA_DIR, 'notify.sqlite');
|
||
|
||
const SCHEMA = `
|
||
CREATE TABLE IF NOT EXISTS abonelikler (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
email TEXT NOT NULL,
|
||
ad TEXT,
|
||
sehir TEXT,
|
||
sektor TEXT,
|
||
alim_turu TEXT,
|
||
anahtar TEXT,
|
||
kurumlar TEXT, -- JSON: favori kurum listesi
|
||
aktif INTEGER NOT NULL DEFAULT 1,
|
||
sonGonderim TEXT,
|
||
sonGorulenler TEXT, -- JSON: son gönderimde bildirilen ihale id'leri
|
||
olusturma TEXT,
|
||
UNIQUE(email, sehir, sektor, alim_turu, anahtar)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_abone_email ON abonelikler(email);
|
||
`;
|
||
|
||
export function available() {
|
||
return Boolean(DatabaseSync);
|
||
}
|
||
|
||
function open({ create = true } = {}) {
|
||
if (!DatabaseSync) return null;
|
||
if (!fs.existsSync(DB_PATH)) {
|
||
if (!create) return null;
|
||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||
}
|
||
const db = new DatabaseSync(DB_PATH);
|
||
db.exec(SCHEMA);
|
||
return db;
|
||
}
|
||
|
||
const bosaNull = (v) => (v == null || v === '' ? '' : String(v).trim());
|
||
|
||
export function subscribe(email, kriter = {}, ad = null) {
|
||
const db = open();
|
||
if (!db) throw new Error('Bildirim deposu kullanılamıyor');
|
||
try {
|
||
db.prepare(`
|
||
INSERT INTO abonelikler (email, ad, sehir, sektor, alim_turu, anahtar, kurumlar, aktif, olusturma)
|
||
VALUES (?,?,?,?,?,?,?,1,?)
|
||
ON CONFLICT(email, sehir, sektor, alim_turu, anahtar) DO UPDATE SET
|
||
aktif=1, kurumlar=excluded.kurumlar, ad=excluded.ad
|
||
`).run(
|
||
email,
|
||
ad,
|
||
bosaNull(kriter.sehir),
|
||
bosaNull(kriter.sektor),
|
||
bosaNull(kriter.alimTuru),
|
||
bosaNull(kriter.anahtar),
|
||
JSON.stringify(kriter.kurumlar || []),
|
||
new Date().toISOString()
|
||
);
|
||
return list(email);
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
export function list(email) {
|
||
const db = open({ create: false });
|
||
if (!db) return [];
|
||
try {
|
||
const rows = email
|
||
? db.prepare('SELECT * FROM abonelikler WHERE email=? ORDER BY id DESC').all(email)
|
||
: db.prepare('SELECT * FROM abonelikler WHERE aktif=1 ORDER BY id').all();
|
||
return rows.map((r) => ({
|
||
id: r.id,
|
||
email: r.email,
|
||
ad: r.ad,
|
||
sehir: r.sehir || null,
|
||
sektor: r.sektor || null,
|
||
alimTuru: r.alim_turu || null,
|
||
anahtar: r.anahtar || null,
|
||
kurumlar: (() => { try { return JSON.parse(r.kurumlar || '[]'); } catch { return []; } })(),
|
||
aktif: Boolean(r.aktif),
|
||
sonGonderim: r.sonGonderim,
|
||
sonGorulenler: (() => { try { return JSON.parse(r.sonGorulenler || '[]'); } catch { return []; } })(),
|
||
}));
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
export function remove(id, email) {
|
||
const db = open({ create: false });
|
||
if (!db) return false;
|
||
try {
|
||
const r = db.prepare('DELETE FROM abonelikler WHERE id=? AND email=?').run(id, email);
|
||
return r.changes > 0;
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
export function markSent(id, ihaleIdleri) {
|
||
const db = open({ create: false });
|
||
if (!db) return;
|
||
try {
|
||
db.prepare('UPDATE abonelikler SET sonGonderim=?, sonGorulenler=? WHERE id=?')
|
||
.run(new Date().toISOString(), JSON.stringify(ihaleIdleri.slice(0, 400)), id);
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|