fix: carry source-backed bid validity into preparation drafts
This commit is contained in:
@@ -248,3 +248,11 @@ SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kont
|
||||
- Detay isteğine 30 saniye zaman aşımı ve iptal sinyali eklendi; pencere kapatıldığında/ihale değiştiğinde istek durduruluyor ve geç yanıt önceki kayda uygulanmıyor.
|
||||
- Kaynak detayının kullanıcıya özel görev/not/taslak alanlarını ezmemesi için yanıt publicTender süzgecinden geçiriliyor.
|
||||
- Kanıt: boş başarılı yanıt, sonraki başarılı deneme, HTTP 503, zaman aşımı ve iptal regresyonuyla 66/66 test ve build geçti. Gerçek kaynakta hata→yeniden deneme tarayıcı kabulü ve tüm kaynak türlerinin detay akışları ayrıca açık.
|
||||
|
||||
### D06 — Teklif geçerlilik süresinin kaynaklı aktarımı
|
||||
|
||||
- Analiz sonucunda procurementTerms.bidValidity alanı sunucuda tüketilen kaynak metinden üretiliyor: gün sayısı, bulunan/bulunamayan/çelişkili durumu, birebir alıntı ve karakter aralığı. Teslimat/garanti günleri teklif geçerliliği olarak seçilmiyor.
|
||||
- Form bu yapılandırılmış değeri ve ilan maddelerini birlikte kullanıyor; sonuç taslakla saklanıyor ve kaynak sürümü kontrolüne katılıyor. Bulunan kaynak alıntıları kullanıcıya gösteriliyor. Birden fazla farklı süre varsa ilkini sessizce seçmek yerine çıktı engelleniyor.
|
||||
- Süre alanında pozitif tam gün isteniyor. Mevcut kullanıcı düzenlemesi yeni analizle sessizce ezilmiyor; kaynak koşulundan kısa süre hazır kabul edilmiyor.
|
||||
- Kanıt: büyük harfli Türkçe madde, parantezli yazıyla sayı, 120 günün analiz/JSON saklama/form kaynağına aktarımı, tekrar aynı süre ve 120/60 çelişkisi testleriyle 68/68 test ve build geçti.
|
||||
- Sınır: Açıkça etiketlenmiş sayısal Türkçe maddeler destekleniyor; salt yazıyla ifade, OCR bozulması ve farklı madde biçimleri manuel kaynak kontrolü gerektirebilir. Bu, tüm ihale koşullarının ortak veri modeline taşındığı veya hukuki çelişki denetiminin tamamlandığı anlamına gelmez.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { verifyAnalysisCitations } from './analysis-citations.mjs';
|
||||
import { validateAnalysisShape } from './analysis-schema.mjs';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {extractBidValidity} from '../src/services/bidTerms.js';
|
||||
export function qualifyAnalysis(result,{text='',limit=60000,fileName='',expectedItems=0,repaired=false}={}) {
|
||||
if(!result || typeof result !== 'object' || Array.isArray(result)) throw new Error('Analiz nesne biçiminde değil.');
|
||||
const clean={...result};delete clean._outputRepaired;
|
||||
@@ -15,8 +16,10 @@ export function qualifyAnalysis(result,{text='',limit=60000,fileName='',expected
|
||||
if(repaired)issues.push('Analiz yanıtı kesildi; yalnız kurtarılabilen alanlar gösteriliyor.');
|
||||
if(expectedItems>0 && (clean.itemsAndSpecs?.length || 0)<expectedItems)issues.push(`Kaynakta ${expectedItems} kalem var; analiz listesi daha kısa.`);
|
||||
const citations=verifyAnalysisCitations(clean,source.slice(0,limit));
|
||||
const bidValidity=extractBidValidity(source.slice(0,limit));
|
||||
if(bidValidity.status==='conflict')issues.push('Kaynakta farklı teklif geçerlilik süreleri var; ilgili maddeler karşılaştırılmalı.');
|
||||
const version=createHash('sha256').update(source).digest('hex');
|
||||
return {...clean,sourceEvidence:citations.evidence,documentVersion:version,sourceReview:{status:issues.length?'partial':'review_required',
|
||||
return {...clean,procurementTerms:{bidValidity},sourceEvidence:citations.evidence,documentVersion:version,sourceReview:{status:issues.length?'partial':'review_required',
|
||||
fileName,sourceCharacters:source.length,analyzedCharacters:Math.min(source.length,limit),
|
||||
outputRepaired:repaired,sections,issues,unverifiedFindings:citations.missing}};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { bidValidityForForm } from '../services/bidTerms';
|
||||
import { createBidWorkbook, downloadBidWorkbook } from '../services/bidWorkbook';
|
||||
import { getToken } from '../services/auth';
|
||||
import { listDocuments, downloadDocument } from '../services/account';
|
||||
@@ -71,12 +72,13 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
});
|
||||
|
||||
const [teklifTuru, setTeklifTuru] = useState(storedDraft.teklifTuru || 'birim');
|
||||
const kaynakGecerlilik = /(?:geçerlilik[^.]{0,100}?|ihale tarihinden itibaren\s*)(\d+)\s*(?:takvim\s*)?gün/i.exec(tender?.aciklama || '');
|
||||
const [gecerlilikGun, setGecerlilikGun] = useState(storedDraft.gecerlilikGun || kaynakGecerlilik?.[1] || '');
|
||||
const effectiveResults = results || storedDraft.analysisSource || {};
|
||||
const kaynakGecerlilik = bidValidityForForm(tender?.aciklama, effectiveResults.procurementTerms?.bidValidity);
|
||||
const [gecerlilikGun, setGecerlilikGun] = useState(storedDraft.gecerlilikGun || kaynakGecerlilik.days || '');
|
||||
|
||||
const [kisim, setKisim] = useState(storedDraft.kisim || '');
|
||||
const [goturuBedel, setGoturuBedel] = useState(storedDraft.goturuBedel || '');
|
||||
|
||||
const effectiveResults = results || storedDraft.analysisSource || {};
|
||||
const [itemSource, setItemSource] = useState(storedDraft.itemSource || {});
|
||||
const sourceOptions = bidSourceOptions(tender, effectiveResults, itemSource);
|
||||
const [manualDocuments, setManualDocuments] = useState(storedDraft.manualDocuments || []);
|
||||
@@ -112,7 +114,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
useEffect(()=>{mounted.current=true;return ()=>{mounted.current=false;printWindow.current?.close();};},[]);
|
||||
const saveDraft = async () => {
|
||||
const submitted = latestDraft.current;
|
||||
const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null,sourceReview:effectiveResults.sourceReview || null}}});
|
||||
const saved = await mutateTender(tender.id, {bidDraft:{firma,teklifTuru,gecerlilikGun,kisim,goturuBedel,priceSchema:1,pricesBySource:fiyatlar,sourceSnapshot,reviewedSource,checks,manualDocuments,partialBid,itemSource,analysisSource:{itemsAndSpecs:effectiveResults.itemsAndSpecs || [],documentChecklist:effectiveResults.documentChecklist || [],documentVersion:effectiveResults.documentVersion || null,procurementTerms:effectiveResults.procurementTerms || null,sourceReview:effectiveResults.sourceReview || null}}});
|
||||
if (!mounted.current) return;
|
||||
if (saved) { setDirty(latestDraft.current !== submitted); setHata(null); }
|
||||
else setHata('Taslak kaydedilemedi. Tekrar deneyin.');
|
||||
@@ -123,7 +125,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
const eksikler = ALANLAR.filter((a) => a.zorunlu && !String(firma[a.k] || '').trim());
|
||||
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
|
||||
const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || '');
|
||||
const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1]));
|
||||
const sureEksik = !Number.isInteger(Number(gecerlilikGun)) || !(Number(gecerlilikGun) > 0) || kaynakGecerlilik.status === 'conflict' || (kaynakGecerlilik.days && Number(gecerlilikGun) < kaynakGecerlilik.days);
|
||||
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender)});
|
||||
const analizEksik = effectiveResults.sourceReview?.status === 'partial';
|
||||
const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0 && !analizEksik && !sourceOptions.needsSelection;
|
||||
@@ -232,6 +234,11 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
|
||||
</label>
|
||||
{sourceOptions.needsSelection && <p role="alert" className="text-xs text-red-700">Kaynak seçilmeden çıktı hazırlanamaz. Kaynak listeleri değiştiğinde seçim yeniden yapılmalıdır.</p>}
|
||||
</fieldset>}
|
||||
{kaynakGecerlilik.evidence.length > 0 && <div className="text-xs border rounded-lg p-3 space-y-1">
|
||||
<p className="font-bold">Teklif geçerlilik süresinin kaynağı</p>
|
||||
{kaynakGecerlilik.status === 'conflict' && <p role="alert" className="text-red-700">Kaynaklarda farklı süreler var. Güncel resmi dokümanı belirleyip analizi yenileyin; çelişki çözülmeden çıktı alınamaz.</p>}
|
||||
{[...new Set(kaynakGecerlilik.evidence.map(e=>e.quote))].map(quote=><blockquote key={quote}>{quote}</blockquote>)}
|
||||
</div>}
|
||||
{analizEksik && <p role="alert" className="text-xs text-red-700">Analiz kısmi: kaynak veya kalem listesi eksik. Tam dokümanı ve resmi cetveli kontrol ederek analizi tamamlayın.</p>}
|
||||
{/* Sizden beklenen bilgiler */}
|
||||
{eksikler.length > 0 && (
|
||||
|
||||
@@ -32,6 +32,7 @@ export function bidSourceSnapshot(tender, results, rows = sourceRows(tender, res
|
||||
announcement: tender?.aciklama || '',
|
||||
deadline: [tender?.tarih || '', tender?.saat || ''],
|
||||
documentVersion: results?.documentVersion ?? tender?.documentVersion ?? null,
|
||||
procurementTerms: results?.procurementTerms || null,
|
||||
analyzedItems: results?.itemsAndSpecs || [],
|
||||
rows: rows.map(r => r.priceKey).sort(),
|
||||
documents: results?.documentChecklist || [],
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Extract only explicitly labelled bid validity clauses; delivery/warranty days are unrelated.
|
||||
export function extractBidValidity(text) {
|
||||
const source=String(text || '');
|
||||
const pattern=/teklif(?:lerin|in)?\s+geçerlilik\s+süresi[^\n.\d]{0,100}(\d{1,4})(?:\s*\([^)]{0,80}\))?\s*(?:takvim\s*)?gün/giu;
|
||||
const evidence=[];
|
||||
for(const match of source.toLocaleLowerCase('tr-TR').matchAll(pattern)) {
|
||||
const days=Number(match[1]);
|
||||
if(days>0) evidence.push({days,quote:source.slice(match.index,match.index+match[0].length),start:match.index,end:match.index+match[0].length});
|
||||
}
|
||||
const values=[...new Set(evidence.map(e=>e.days))];
|
||||
return {status:values.length>1?'conflict':values.length?'found':'not_found',days:values.length===1?values[0]:null,evidence};
|
||||
}
|
||||
|
||||
export function bidValidityForForm(announcement, analyzed) {
|
||||
const local=extractBidValidity(announcement);
|
||||
const evidence=[...local.evidence,...(Array.isArray(analyzed?.evidence)?analyzed.evidence:[])];
|
||||
const values=[...new Set(evidence.map(e=>e.days).filter(d=>Number.isInteger(d)&&d>0))];
|
||||
return {status:values.length>1?'conflict':values.length?'found':'not_found',days:values.length===1?values[0]:null,evidence};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {extractBidValidity,bidValidityForForm} from '../src/services/bidTerms.js';
|
||||
import {qualifyAnalysis} from '../server/analysis-quality.mjs';
|
||||
test('bid validity is structured from source with exact clause evidence',()=>{
|
||||
const source='Teslimat 15 gün. TEKLİFLERİN GEÇERLİLİK SÜRESİ ihale tarihinden itibaren 120 (yüzyirmi) takvim günüdür.';
|
||||
const terms=extractBidValidity(source);
|
||||
assert.equal(terms.days,120);
|
||||
assert.equal(source.slice(terms.evidence[0].start,terms.evidence[0].end),terms.evidence[0].quote);
|
||||
assert.equal(extractBidValidity('Garanti süresi 60 gün. Belge geçerlilik süresi 90 gün.').status,'not_found');
|
||||
const result=qualifyAnalysis({},{text:source});
|
||||
assert.equal(result.procurementTerms.bidValidity.days,120);
|
||||
assert.equal(bidValidityForForm('',JSON.parse(JSON.stringify(result.procurementTerms.bidValidity))).days,120);
|
||||
});
|
||||
test('conflicting clauses remain unresolved instead of choosing the first duration',()=>{
|
||||
const a='Teklif geçerlilik süresi 120 takvim günü.';
|
||||
const b='Tekliflerin geçerlilik süresi 60 gündür.';
|
||||
assert.equal(extractBidValidity(a+' '+b).status,'conflict');
|
||||
assert.equal(bidValidityForForm(a,extractBidValidity(b)).status,'conflict');
|
||||
assert.equal(qualifyAnalysis({},{text:a+' '+b}).sourceReview.status,'partial');
|
||||
assert.equal(extractBidValidity(a+' '+a).days,120);
|
||||
});
|
||||
Reference in New Issue
Block a user