fix: exclude closed tenders from upcoming feeds

This commit is contained in:
2026-09-06 14:26:31 +03:00
parent b2a416c908
commit c55257cade
4 changed files with 37 additions and 4 deletions
+7
View File
@@ -1047,3 +1047,10 @@ Taslak türü seçicisine erişilebilir ad eklendi. Üretim derlemesi geçti. Ö
- Tercih GET isteği artık ihale havuzunun yüklenmesine bağlı değildir; içerik yalnız preview=1 ile Önizle işleminde oluşturulur. Genel kullanıcı veri rotasında brief yazımı da sürüm/boolean kontrolünden geçer. Gönderim metni hizmet ve zamanlayıcı etkinliğine bağlı olduğunu açıklar. - Tercih GET isteği artık ihale havuzunun yüklenmesine bağlı değildir; içerik yalnız preview=1 ile Önizle işleminde oluşturulur. Genel kullanıcı veri rotasında brief yazımı da sürüm/boolean kontrolünden geçer. Gönderim metni hizmet ve zamanlayıcı etkinliğine bağlı olduğunu açıklar.
- 236 otomatik test ve derleme geçti. HTTP testi başlangıç tercihi, gövde doğrulama, kapatma/geri okuma, eski sürüm reddi, başka hesabın ayrımı ve güncel sürümle açmayı doğruladı. İlk testteki 400 bu gerçek izinli-anahtar hatasını yakaladı; düzeltme sonrası tüm suite geçti. - 236 otomatik test ve derleme geçti. HTTP testi başlangıç tercihi, gövde doğrulama, kapatma/geri okuma, eski sürüm reddi, başka hesabın ayrımı ve güncel sürümle açmayı doğruladı. İlk testteki 400 bu gerçek izinli-anahtar hatasını yakaladı; düzeltme sonrası tüm suite geçti.
- Yeni panelin tarayıcı hata/kurtarma ve önizleme kabulü, zamanlayıcıdan gerçek test teslimi açık. Bu tur dış özet gönderilmedi; canlıya yayın yapılmadı. - Yeni panelin tarayıcı hata/kurtarma ve önizleme kabulü, zamanlayıcıdan gerçek test teslimi açık. Bu tur dış özet gönderilmedi; canlıya yayın yapılmadı.
### 2026-09-06 — Haftalık özet ve açık beslemelerde kapanmış ihale filtresi
- RSS/MCP açık ihale araması ve haftalık özetin `yaklasanIhaleler` listesi ana listenin `acikIhale` kontrolünü kullanıyor. İptal, sonuçlandı, sözleşme imzalandı veya kazananı açıklanmış kayıtlar gelecek tarih taşısa da açık fırsat olarak gösterilmiyor. `sadeceAcik:false` geçmiş/durum aramasını koruyor.
- Yeni takip anlık görüntüleri kazanan firma alanını da saklıyor; havuzdan çıkan bir sonuç kaydının yalnız bu bilgi kaybolduğu için açık görünmesi önleniyor. Eski görüntülerde hiç saklanmamış kazanan bilgisi bu değişiklikle geriye dönük üretilemiyor; güncel kaynak doğrulaması ihtiyacı sürüyor.
- Regresyon testi güncel kaynakta sonradan iptal, havuz dışındaki üç kapalı durum ve kazanan kaydı, açık saat-belirsiz kayıt, metin/HTML özet ve tüm durumlar aramasını kapsıyor.
- Doğrulama: `npm test` 237/237; `npm run build` başarılı. Mevcut büyük paket uyarısı sürüyor. Canlı gönderim ve dağıtım yapılmadı. Haftalık zamanlayıcının gönderimden önce tamamlandı kaydı yazması ve yeniden deneme/dayanıklılık akışı hâlâ incelenecek; genel plan tamamlanmadı.
+3 -3
View File
@@ -6,6 +6,7 @@
// Claude Desktop (mcp-remote) araçları: search_tenders, get_tender, // Claude Desktop (mcp-remote) araçları: search_tenders, get_tender,
// search_results, search_items, upcoming_deadlines. Bağımlılık yok. // search_results, search_items, upcoming_deadlines. Bağımlılık yok.
import * as store from './user-store.mjs'; import * as store from './user-store.mjs';
import { acikIhale } from '../src/services/ihaleTarih.js';
import { deadlineInfo, tenderDateKey } from '../src/services/tenderCalendar.js'; import { deadlineInfo, tenderDateKey } from '../src/services/tenderCalendar.js';
import { applyTenderStates, tenderStateKey } from '../src/services/tenderState.js'; import { applyTenderStates, tenderStateKey } from '../src/services/tenderState.js';
import { readHistory, resultsByIkn } from './history-store.mjs'; import { readHistory, resultsByIkn } from './history-store.mjs';
@@ -36,8 +37,7 @@ export function trTarih(s) {
export function ihaleFiltrele(tenders, { q, il, sektor, sadeceAcik = true, limit = 50 } = {}, now = new Date()) { export function ihaleFiltrele(tenders, { q, il, sektor, sadeceAcik = true, limit = 50 } = {}, now = new Date()) {
const qq = norm(q); const ill = norm(il); const sek = norm(sektor); const qq = norm(q); const ill = norm(il); const sek = norm(sektor);
return tenders return tenders
.filter((t) => !sadeceAcik || !t.kazananFirma) .filter((t) => !sadeceAcik || acikIhale(t, now))
.filter((t) => !sadeceAcik || deadlineInfo(t, now).expired !== true)
.filter((t) => !ill || norm(t.sehir).includes(ill)) .filter((t) => !ill || norm(t.sehir).includes(ill))
.filter((t) => !sek || norm(t.sektor).includes(sek)) .filter((t) => !sek || norm(t.sektor).includes(sek))
.filter((t) => !qq || norm(t.baslik).includes(qq) || norm(t.kurum).includes(qq) || String(t.ikn ?? '').includes(qq) || .filter((t) => !qq || norm(t.baslik).includes(qq) || norm(t.kurum).includes(qq) || String(t.ikn ?? '').includes(qq) ||
@@ -106,7 +106,7 @@ export function yaklasanIhaleler(email, tenders, gun = 14, now = new Date()) {
const takip = new Set(store.getTrackedTenderKeys(email, states)); const takip = new Set(store.getTrackedTenderKeys(email, states));
const days = Math.min(90, Math.max(1, Math.floor(Number(gun) || 14))); const days = Math.min(90, Math.max(1, Math.floor(Number(gun) || 14)));
return applyTenderStates(tenders, states) return applyTenderStates(tenders, states)
.filter(t => !t.kazananFirma && takip.has(tenderStateKey(t))) .filter(t => takip.has(tenderStateKey(t)) && acikIhale(t, now))
.map(t => ({ t, info: deadlineInfo(t, now) })) .map(t => ({ t, info: deadlineInfo(t, now) }))
.filter(({ info }) => info.days !== null && info.days >= 0 && info.days <= days && info.expired !== true) .filter(({ info }) => info.days !== null && info.days >= 0 && info.days <= days && info.expired !== true)
.sort((a, b) => a.info.days - b.info.days || .sort((a, b) => a.info.days - b.info.days ||
+1 -1
View File
@@ -22,7 +22,7 @@ export function applyTenderStates(tenders, records = {}) {
return result; return result;
} }
export function tenderSnapshot(t) { export function tenderSnapshot(t) {
return Object.fromEntries(['id','ikn','baslik','kurum','tarih','saat','sehir','alimTuru','durum','documentVersion'].map(k => [k,t[k] ?? ''])); return Object.fromEntries(['id','ikn','baslik','kurum','tarih','saat','sehir','alimTuru','durum','kazananFirma','documentVersion'].map(k => [k,t[k] ?? '']));
} }
export function publicTender(tender) { export function publicTender(tender) {
+26
View File
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs'; import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import { tenderSnapshot } from '../src/services/tenderState.js';
const dir = mkdtempSync(join(tmpdir(), 'ihaleist-feed-')); const dir = mkdtempSync(join(tmpdir(), 'ihaleist-feed-'));
process.env.IHALEIST_USER_DATA_DIR = dir; process.env.IHALEIST_USER_DATA_DIR = dir;
@@ -63,3 +64,28 @@ test('weekly notification schedule starts at Monday 08:00 Istanbul', () => {
assert.equal(briefScheduleKey(new Date('2026-09-07T20:59:59Z')), '2026-8-7'); assert.equal(briefScheduleKey(new Date('2026-09-07T20:59:59Z')), '2026-8-7');
assert.equal(briefScheduleKey(new Date('2026-09-07T21:00:00Z')), null); assert.equal(briefScheduleKey(new Date('2026-09-07T21:00:00Z')), null);
}); });
test('closed tenders stay out of open feeds and weekly upcoming items, including saved snapshots', () => {
const owner = 'closed-feed@example.test';
const base = { tarih:'07.09.2026', saat:'10:00' };
const closed = ['İptal', 'Sonuçlandı', 'Sözleşme İmzalandı'].map((durum, i) =>
({ ...base, id:`closed-${i}`, ikn:`2026/closed-${i}`, durum }));
closed.push({ ...base, id:'winner', ikn:'2026/winner', kazananFirma:'Test kazananı' });
const open = { ...base, id:'open', ikn:'2026/open', durum:'Açık' };
const unknownTime = { ...open, id:'unknown-time', ikn:'2026/unknown-time', saat:'' };
const all = [...closed, open, unknownTime];
for (const tender of all) store.saveTenderState(owner, tender.ikn, { takipte:true }, tenderSnapshot(tender), 0);
assert.deepEqual(ihaleFiltrele(all, {}, now).map(t => t.id), ['open', 'unknown-time']);
assert.equal(ihaleFiltrele(all, { sadeceAcik:false }, now).length, all.length);
// Status updates in the current source must override a previously open saved snapshot.
const updated = { ...open, durum:'İptal' };
const upcoming = yaklasanIhaleler(owner, [updated, unknownTime], 14, now);
assert.deepEqual(upcoming.map(t => t.ikn), ['2026/unknown-time']);
assert.equal(upcoming[0].saat_dogrulanmali, true);
for (const output of [briefMetni, briefHtml]) {
const body = output({ hafta:'Test haftası', yaklasan:upcoming, sonuclar:[] });
for (const tender of [...closed, open]) assert.ok(!body.includes(tender.ikn));
assert.match(body, /saat doğrulanmalı/);
}
});