Files
ihaleist/server/tender-quality.mjs

98 lines
4.8 KiB
JavaScript
Raw Permalink 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.
const norm = v => String(v || '').trim().replace(/\s+/g,' ').toLocaleLowerCase('tr-TR');
export function tenderIdentity(t) {
const ikn = String(t.ikn || '').replace(/\s/g,'');
// Awards from different lots must never be collapsed into one contract.
if (norm(t.durum) === 'sonuçlandı') return `award:${ikn}:${t.kisim ?? t.id}:${norm(t.kazananFirma)}`;
if (/^\d{2}DT\d+$/i.test(ikn)) return `dt:${ikn.toUpperCase()}:${norm(t.kurum)}`;
// Corrections/cancellations remain separate events until their source linkage is verified.
if (/düzeltme|zeyilname|iptal/i.test(t.baslik || '')) return `event:${t.id}`;
return /^\d{4}\/\d+$/.test(ikn) ? `tender:${ikn}` : `source:${t.id}`;
}
export function uniqueTenders(records) {
const found = new Map();
for (const t of records) {
// Kaynaklardan gelen bozuk/null satırlar tüm havuz birleştirmesini düşürmemeli.
if (!t || typeof t !== 'object' || Array.isArray(t) || typeof t.id !== 'string' || !t.id.trim()) continue;
const key = tenderIdentity(t), old = found.get(key);
if (!old) { found.set(key,{...t,sourceIds:[t.id]}); continue; }
const sourceIds = [...new Set([...old.sourceIds,t.id])];
const merged = {...old};
for (const [field,value] of Object.entries(t)) {
if (merged[field] == null || merged[field] === '' || (Array.isArray(merged[field]) && !merged[field].length)) merged[field] = value;
}
if ((t.aciklama || '').length > (old.aciklama || '').length) merged.aciklama = t.aciklama;
found.set(key,{...merged,id:old.id,sourceIds});
}
return [...found.values()];
}
export function announcementFields(text) {
const content = String(text || '');
const result = {aciklama:content};
// EKAP table cells are separated by pipes in extracted text. Keep the
// original announcement intact; normalize separators only for field parsing.
const fields = content.replace(/\s*\|\s*/g, ' ');
// EKAP/ilan kaynakları aynı son teklif anını farklı başlıklarla verir.
// Yalnız tarih + saat birlikte bulunduğunda alan üret; teslim süresi gibi
// yalnızca tarih içeren maddeleri son teklif tarihi sanma.
const deadlinePatterns = [
/Tarih\s*ve\s*Saati\s*:\s*(\d{2}\.\d{2}\.\d{4})\s*[-–]?\s*(\d{2}:\d{2})/i,
/(?:İhale\s+|Son\s+Teklif\s+)?Tarih(?:i)?\s*(?:\/\s*Saat(?:i)?|ve\s+Saat(?:i)?|\s+Saat(?:i)?)\s*:\s*(\d{2}\.\d{2}\.\d{4})\s*[-–]?\s*(\d{2}:\d{2})/i,
/Son\s+Teklif\s+Tarihi\s*:\s*(\d{2}\.\d{2}\.\d{4})\s*[-–]?\s*(\d{2}:\d{2})/i,
];
const deadline = deadlinePatterns.map(pattern => pattern.exec(fields)).find(Boolean);
if (deadline) { result.tarih = deadline[1]; result.saat = deadline[2]; }
const place = /3\.3\.?\s*[^:]{0,120}:\s*([\s\S]*?)(?=3\.4\.?)/i.exec(fields);
const duration = /3\.4\.?\s*[^:]{0,100}:\s*([\s\S]*?)(?=3\.5\.?|4\s*[-–])/i.exec(fields);
if (place) result.teslimYeri = place[1].trim();
if (duration) result.teslimatSuresi = duration[1].trim();
if (/hizmet alımı\s+4734/i.test(content)) result.alimTuru = 'Kiralama ve Hizmet Alımı';
return result;
}
/**
* EKAP ihale detayındaki tarih-saat alanını liste ve takvim alanlarına ayırır.
* Ayrıştırılamayan/eksik değerler için varsayılan tarih veya saat üretmez.
*/
export function officialDateFields(value) {
const raw = String(value || '').trim();
if (!raw) return {};
const dotted = raw.match(/(\d{2})\.(\d{2})\.(\d{4})\s*(?:[-–T]|\s)\s*(\d{1,2}):(\d{2})/);
if (dotted) return {
tarih: `${dotted[1]}.${dotted[2]}.${dotted[3]}`,
saat: `${dotted[4].padStart(2, '0')}:${dotted[5]}`,
sourceDateTime: raw,
};
const iso = raw.match(/(\d{4})-(\d{2})-(\d{2})[T\s]+(\d{1,2}):(\d{2})/);
if (iso) return {
tarih: `${iso[3]}.${iso[2]}.${iso[1]}`,
saat: `${iso[4].padStart(2, '0')}:${iso[5]}`,
sourceDateTime: raw,
};
return { sourceDateTime: raw };
}
/**
* İlan metninden çıkarılan alanları, resmî EKAP ihale detayındaki alanlarla
* birleştirir. Resmî detay tarihi ilan HTML'inden eski/çelişkili bir tarih
* gelirse üstün tutulur; fark kullanıcıya açık bir uyarı olarak taşınır.
*/
export function mergeAnnouncementFields(text, authoritative = {}) {
const parsed = announcementFields(text);
const sourceTarih = String(authoritative.tarih || '').trim();
const sourceSaat = String(authoritative.saat || '').trim();
const dateConflict = Boolean(
(sourceTarih && parsed.tarih && sourceTarih !== parsed.tarih) ||
(sourceSaat && parsed.saat && sourceSaat !== parsed.saat),
);
const merged = {
...parsed,
...(sourceTarih ? { tarih: sourceTarih } : {}),
...(sourceSaat ? { saat: sourceSaat } : {}),
...(authoritative.sourceDateTime ? { sourceDateTime: authoritative.sourceDateTime } : {}),
};
if (dateConflict) {
merged.sourceDateWarning = 'Resmî ihale detay tarihi ile ilan metnindeki tarih farklı; güncel detay tarihi esas alındı.';
}
return merged;
}