fix: preserve complete weekly brief content and date bounds

This commit is contained in:
2026-09-06 14:29:59 +03:00
parent 23512b7a87
commit d3a52caf2b
8 changed files with 143 additions and 24 deletions
+35
View File
@@ -0,0 +1,35 @@
/** Assemble every tracked result in the previous seven Istanbul calendar dates. */
export function createBriefBuilder({ trackedKeys, upcoming, results, parseDate }) {
return function buildBrief(email, tenders, now = new Date()) {
const yaklasan = upcoming(email, tenders, 7, now);
const today = new Date(now.getTime() + 3 * 3600000).toISOString().slice(0,10);
const end = Date.parse(today + 'T00:00:00+03:00');
const start = end - 7 * 86400000;
const dated = [];
for (const ikn of new Set(trackedKeys(email))) {
for (const result of results(ikn) || []) {
const date = parseDate(result.sozlesmeTarihi) || parseDate(result.tarih);
if (!date || date.getTime() < start || date.getTime() >= end) continue;
dated.push({ timestamp:date.getTime(), value:{ ikn, baslik:result.baslik, kazanan:result.kazananFirma,
bedel:result.sozlesmeBedeli, birim:result.paraBirimi, teklif:result.teklifSayisi } });
}
}
dated.sort((a,b) => b.timestamp-a.timestamp || String(a.value.ikn).localeCompare(String(b.value.ikn),'tr'));
const sonuclar = dated.map(row=>row.value);
const hafta = now.toLocaleDateString('tr-TR', { timeZone:'Europe/Istanbul', day:'2-digit', month:'long', year:'numeric' });
return { hafta, yaklasan, sonuclar, bos:!yaklasan.length && !sonuclar.length };
};
}
/** Keep UTF-16 surrogate pairs intact while bounding each Telegram message. */
export function splitBriefText(text, limit=3900) {
if (!Number.isInteger(limit) || limit < 2 || limit > 3900) throw new Error('Geçersiz mesaj uzunluğu.');
const parts=[];
let part='';
for (const char of String(text)) {
if (part.length + char.length > limit) { parts.push(part); part=''; }
part += char;
}
if (part) parts.push(part);
return parts;
}
+11 -4
View File
@@ -1,3 +1,5 @@
import { splitBriefText } from './brief-content.mjs';
/** Delivery accounting shared by scheduled and administrator-triggered briefs. */
export function createBriefSender({ store, buildBrief, text, html, telegramConfigured, sendTelegram, emailConfigured, sendEmail, warn = console.warn }) {
return async function sendBrief(tenderGetir) {
@@ -14,16 +16,20 @@ export function createBriefSender({ store, buildBrief, text, html, telegramConfi
if (brief.bos) { r.bos++; continue; }
const channels = [];
const telegram = store.getData(user.email, 'telegram');
if (telegramConfigured() && telegram?.chatId) channels.push({
name:'telegram', send:() => sendTelegram(telegram.chatId, text(brief).slice(0,3900)),
valid:receipt => receipt === true,
});
if (telegramConfigured() && telegram?.chatId) {
for (const part of splitBriefText(text(brief))) channels.push({
name:'telegram', send:() => sendTelegram(telegram.chatId, part, { parseMode:null }),
valid:receipt => receipt === true,
});
}
if (emailConfigured()) channels.push({
name:'email', send:() => sendEmail({ to:user.email, subject:`İhaleist — Haftalık Brief (${brief.hafta})`, html:html(brief), text:text(brief) }),
valid:receipt => typeof receipt?.id === 'string' && Boolean(receipt.id.trim()),
});
if (!channels.length) r.kanalsiz++;
const failedChannels = new Set();
for (const channel of channels) {
if (failedChannels.has(channel.name)) continue;
// An earlier channel can await the network while the user disables briefs.
if (store.getData(user.email, 'brief')?.kapali) { r.kapali++; break; }
const counts = r.kanallar[channel.name];
@@ -35,6 +41,7 @@ export function createBriefSender({ store, buildBrief, text, html, telegramConfi
accepted = true;
} catch (error) {
counts.hata++;
failedChannels.add(channel.name);
failed = true;
warn('[brief]', user.email, channel.name, error.message);
}
+7 -18
View File
@@ -1,8 +1,9 @@
// Haftalık brief — Pazartesi 08:00: takipteki ihalelerden bu hafta yapılacaklar,
// geçen hafta sonuçlanan takipliler, takiplilere gelen zeyilname sayısı.
// önceki yedi takvim günündeki takipli sonuç kayıtları.
// Telegram (bağlıysa) + e-posta. Son gönderim data/brief-son.json'da tutulur;
// saatlik kontrol, haftada bir gönderim (node-cron yok, setInterval).
import fs from 'node:fs';
import { createBriefBuilder } from './brief-content.mjs';
import { createBriefSender } from './brief-delivery.mjs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -25,21 +26,9 @@ function sonGonderimYaz(d) {
const paraTR = (v, birim) => (v == null ? '' : `${Number(v).toLocaleString('tr-TR')} ${birim || 'TL'}`);
/** Bir kullanıcı için brief içeriği. */
export function briefOlustur(email, tenders) {
const takip = store.getTrackedTenderKeys(email);
const yaklasan = yaklasanIhaleler(email, tenders, 7);
const birHafta = Date.now() - 7 * 86400000;
const sonuclar = [];
for (const ikn of takip.slice(0, 200)) {
const r = resultsByIkn(ikn) || [];
for (const s of r) {
const d = trTarih(s.sozlesmeTarihi) || trTarih(s.tarih);
if (d && d.getTime() >= birHafta) sonuclar.push({ ikn, baslik: s.baslik, kazanan: s.kazananFirma, bedel: s.sozlesmeBedeli, birim: s.paraBirimi, teklif: s.teklifSayisi });
}
}
const hafta = new Date().toLocaleDateString('tr-TR', { day: '2-digit', month: 'long', year: 'numeric' });
return { hafta, yaklasan, sonuclar: sonuclar.slice(0, 15), bos: !yaklasan.length && !sonuclar.length };
}
export const briefOlustur = createBriefBuilder({
trackedKeys:store.getTrackedTenderKeys, upcoming:yaklasanIhaleler, results:resultsByIkn, parseDate:trTarih,
});
const deadlineText = t => t.son_teklif_zamani
? new Date(t.son_teklif_zamani).toLocaleTimeString('tr-TR', { timeZone: 'Europe/Istanbul', hour: '2-digit', minute: '2-digit' }) + ' (Türkiye saati)'
@@ -52,7 +41,7 @@ export function briefMetni(b) {
for (const t of b.yaklasan) s.push(`• ${t.kalan_gun === 0 ? 'BUGÜN' : t.kalan_gun + ' gün'} — ${t.baslik} (${t.kurum}, ${t.tarih} · ${deadlineText(t)})`);
}
if (b.sonuclar.length) {
s.push('', `🏁 Geçen hafta sonuçlanan takipliler (${b.sonuclar.length}):`);
s.push('', `🏁 Önceki 7 takvim günündeki takipli sonuç kayıtları (${b.sonuclar.length}):`);
for (const r of b.sonuclar) s.push(`• ${r.baslik} → ${r.kazanan || 'kazanan açıklanmadı'}${r.bedel != null ? ' · ' + paraTR(r.bedel, r.birim) : ''}${r.teklif != null ? ' · ' + r.teklif + ' teklif' : ''}`);
}
s.push('', 'ihaleist.com');
@@ -66,7 +55,7 @@ export function briefHtml(b) {
return `<div style="font-family:Inter,Arial,sans-serif;max-width:640px;margin:auto;padding:20px;color:#0f172a">
<h2 style="margin:0 0 4px;font-size:20px">📋 Haftalık İhale Brief</h2><div style="color:#64748b;font-size:13px">${esc(b.hafta)}</div>
${bolum('⏰ Bu hafta yapılacak takipli ihaleler', b.yaklasan.map((t) => `<b>${t.kalan_gun === 0 ? 'BUGÜN' : t.kalan_gun + ' gün'}</b> — ${esc(t.baslik)} (${esc(t.kurum)}, ${esc(t.tarih)} · ${esc(deadlineText(t))})`))}
${bolum('🏁 Geçen hafta sonuçlanan takipliler', b.sonuclar.map((r) => `${esc(r.baslik)} → ${esc(r.kazanan || 'kazanan açıklanmadı')}${r.bedel != null ? ' · ' + esc(paraTR(r.bedel, r.birim)) : ''}`))}
${bolum('🏁 Önceki 7 takvim günündeki takipli sonuç kayıtları', b.sonuclar.map((r) => `${esc(r.baslik)} → ${esc(r.kazanan || 'kazanan açıklanmadı')}${r.bedel != null ? ' · ' + esc(paraTR(r.bedel, r.birim)) : ''}`))}
<p style="margin-top:20px;font-size:12px;color:#94a3b8">Bu brief her Pazartesi gönderilir; Bildirimler sekmesinden kapatabilirsiniz.</p></div>`;
}
+2 -2
View File
@@ -26,12 +26,12 @@ export async function botAdi() {
return null;
}
export async function sendTelegram(chatId, text) {
export async function sendTelegram(chatId, text, { parseMode = 'HTML' } = {}) {
if (!telegramConfigured() || !chatId) return false;
const r = await fetch(`${API()}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: chatId, text: String(text).slice(0, 4000), parse_mode: 'HTML', disable_web_page_preview: true }),
body: JSON.stringify({ chat_id: chatId, text: String(text).slice(0, 4000), ...(parseMode === null ? {} : { parse_mode:parseMode }), disable_web_page_preview: true }),
signal: AbortSignal.timeout(15000),
});
const d = await r.json().catch(() => ({}));