feat: validate preparation against owned document files

This commit is contained in:
2026-09-06 08:53:41 +03:00
parent 0d332afdb1
commit 953d3b5c1e
7 changed files with 91 additions and 11 deletions
+9 -1
View File
@@ -79,9 +79,17 @@ Tarayıcı kanıtı: [Yenileme sonrası beş kart](evidence/qa-kanban-after-relo
- İki test seçilmeyen 1.000 TL cihazın 80 TL'lik seçili cetvele karışmamasını, eksik kaynak/eşleştirme ve kaybolan kısım engellerini doğrular. Tarayıcıda tek sentetik satır için boş seçim engeli, kısım eşleştirmesi ve seçili fiyat gösterimi doğrulandı: [kanıt](evidence/qa-lot-selection.txt). - İki test seçilmeyen 1.000 TL cihazın 80 TL'lik seçili cetvele karışmamasını, eksik kaynak/eşleştirme ve kaybolan kısım engellerini doğrular. Tarayıcıda tek sentetik satır için boş seçim engeli, kısım eşleştirmesi ve seçili fiyat gösterimi doğrulandı: [kanıt](evidence/qa-lot-selection.txt).
- Aksaray'ın gerçek 34 kısmının resmi dokümanıyla karşılaştırma ve çok kısımlı tarayıcı kabulü henüz yoktur. Götürü bedel tutarı kullanıcı tarafından seçilen kapsam için ayrıca girilir. - Aksaray'ın gerçek 34 kısmının resmi dokümanıyla karşılaştırma ve çok kısımlı tarayıcı kabulü henüz yoktur. Götürü bedel tutarı kullanıcı tarafından seçilen kapsam için ayrıca girilir.
## Belge deposu ile hazırlık bağlantısı
- Belge gereksinimleri hesaba ait gerçek depo dosyasına bağlanır. Dosya kimliği ve oluşturma/boyut/son kullanma bilgileri kontrol kaydında tutulur; dosya değişikliği yeniden kontrol gerektirir.
- Dosya listesi fiziksel dosyanın mevcut olup olmadığını bildirir; sunucu dosya yolu istemciye verilmez. Kayıp/erişilemeyen dosya veya ihale tarihinde geçersiz belge çıktı koşullarını karşılamaz.
- Hazırlık formunda dosya seçimi, indirme, liste yenileme ve yüklenme/hata durumları bulunur. Çıktı isteğinde dosyalar tekrar sunucudan kontrol edilir. Dosya türüne göre içerik/ÜTS/mevzuat doğrulaması yapılmış sayılmaz.
- Yeni HTTP testi gerçek baytlarla yükleme → listeleme → indirme → üst veri değiştirme → silme zincirini ve başka hesabın indirme isteğine 404'ü doğrular. Bir kural testi kayıp dosya, değişmiş bilgi, süresi dolmuş ve geçersiz tarih senaryolarını kapsar.
- Tarayıcı üzerinden dosya seçme/indirme ve son kontrol sonrası yazdırma davranışı, özellikle yeni pencere engeli, henüz doğrulanmalı. Bu çalışma D01/D07 belge bağlantısını ilerletir; OCR ve beş gerçek dosya kabulünü kapatmaz.
## Doğrulama ## Doğrulama
`npm test`: 33/33 geçti (son yeniden çalıştırma: 6 Eylül 2026). `npm test`: 35/35 geçti (son yeniden çalıştırma: 6 Eylül 2026).
`npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor. `npm run build`: geçti; mevcut büyük paket/dinamik import uyarıları sürüyor.
`node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti. `node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti.
+2 -2
View File
@@ -250,11 +250,11 @@ export function addDocument(email, { ad, kategori, boyut, mime, dosya, sonKullan
} }
export function listDocuments(email, { ikn } = {}) { export function listDocuments(email, { ikn } = {}) {
const alanlar = 'id, ad, kategori, boyut, mime, son_kullanma, ikn, olusturma'; const alanlar = 'id, ad, kategori, boyut, mime, son_kullanma, ikn, olusturma, dosya';
return withDb((db) => (ikn return withDb((db) => (ikn
? db.prepare(`SELECT ${alanlar} FROM belgeler WHERE email=? AND ikn=? ORDER BY olusturma DESC`).all(email, ikn) ? db.prepare(`SELECT ${alanlar} FROM belgeler WHERE email=? AND ikn=? ORDER BY olusturma DESC`).all(email, ikn)
: db.prepare(`SELECT ${alanlar} FROM belgeler WHERE email=? ORDER BY olusturma DESC`).all(email) : db.prepare(`SELECT ${alanlar} FROM belgeler WHERE email=? ORDER BY olusturma DESC`).all(email)
), []); ).map(({dosya,...record}) => ({...record, erisilebilir: fs.existsSync(path.join(BELGE_DIR,dosya))})), []);
} }
export function getDocument(email, id) { export function getDocument(email, id) {
+20 -4
View File
@@ -1,4 +1,6 @@
import React, { useState, useMemo } from 'react'; import { listDocuments, downloadDocument } from '../services/account';
import { tenderDateKey } from '../services/tenderCalendar';
import React, { useState, useMemo, useEffect } from 'react';
import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation'; import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation';
import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource'; import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource';
import { rowLot, selectBidLots } from '../services/bidLots'; import { rowLot, selectBidLots } from '../services/bidLots';
@@ -42,6 +44,15 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp(); const { userProfile, mutateTender, savingCount, authUser, tenders } = useApp();
const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)}; const tender = {...selectedTender, ...tenders.find(t => t.id === selectedTender?.id)};
const storedDraft = tender?.bidDraft || {}; const storedDraft = tender?.bidDraft || {};
const [vault,setVault] = useState({loading:true,error:null,documents:[]});
const [vaultRefresh,setVaultRefresh] = useState(0);
useEffect(() => {
let cancelled=false;
setVault({loading:true,error:null,documents:[]});
listDocuments().then(data=>{if(!cancelled)setVault({loading:false,error:null,documents:data.belgeler || []});})
.catch(error=>{if(!cancelled)setVault({loading:false,error:error.message,documents:[]});});
return ()=>{cancelled=true;};
},[authUser?.email,vaultRefresh]);
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
const [acik, setAcik] = useState(true); const [acik, setAcik] = useState(true);
const [hata, setHata] = useState(null); const [hata, setHata] = useState(null);
@@ -101,7 +112,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0; const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0;
const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || ''); const turCeliskisi = teklifTuru === 'goturu' && /birim fiyat/i.test(tender?.aciklama || '');
const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1])); const sureEksik = !(Number(gecerlilikGun) > 0) || (kaynakGecerlilik && Number(gecerlilikGun) < Number(kaynakGecerlilik[1]));
const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks); const kontrolEksikleri = preparationBlockers(lotSelection.rows, documents, checks, {...vault,deadlineDate:tenderDateKey(tender)});
const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0; const hazir = eksikler.length === 0 && toplam > 0 && !kalemEksik && !turCeliskisi && !sureEksik && !kaynakKontrolGerekli && kontrolEksikleri.length === 0 && lotSelection.errors.length === 0;
const formVerisi = () => ({ const formVerisi = () => ({
@@ -115,10 +126,15 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
ihaleTarihi: tender?.ihaleTarihi || tender?.tarih || '', ihaleTarihi: tender?.ihaleTarihi || tender?.tarih || '',
}); });
const uret = (tip) => { const uret = async (tip) => {
setHata(null); setHata(null);
try { try {
if (!hazir) throw new Error('Kalem, miktar, birim, fiyat, firma ve geçerlilik süresindeki eksikleri giderin.'); if (!hazir) throw new Error('Kalem, miktar, birim, fiyat, firma ve geçerlilik süresindeki eksikleri giderin.');
const currentFiles = await listDocuments();
const currentVault = {loading:false,error:null,documents:currentFiles.belgeler || [],deadlineDate:tenderDateKey(tender)};
setVault(currentVault);
const fileErrors = preparationBlockers(lotSelection.rows, documents, checks, currentVault);
if (fileErrors.length) throw new Error(fileErrors.join(' '));
const f = formVerisi(); const f = formVerisi();
if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam)); if (tip === 'mektup') belgeYazdir('Teklif Mektubu', teklifMektubuHtml(f, toplam));
if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, toplam)); if (tip === 'cetvel') belgeYazdir('Birim Fiyat Teklif Cetveli', teklifCetveliHtml(f, satirlar, toplam));
@@ -332,7 +348,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => {
)} )}
<PreparationChecks rows={lotSelection.rows} documents={documents} manualDocuments={manualDocuments} <PreparationChecks rows={lotSelection.rows} documents={documents} manualDocuments={manualDocuments}
setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks}/> setManualDocuments={value => {setManualDocuments(value);setDirty(true);}} checks={checks} setChecks={setChecks} vault={vault} refreshVault={()=>setVaultRefresh(n=>n+1)} downloadFile={(file)=>downloadDocument(file.id,file.ad).catch(e=>setHata(e.message))}/>
{kontrolEksikleri.length > 0 && <details className="bg-amber-50 border border-amber-300 rounded-xl p-3"> {kontrolEksikleri.length > 0 && <details className="bg-amber-50 border border-amber-300 rounded-xl p-3">
<summary className="text-xs font-bold cursor-pointer">{kontrolEksikleri.length} belge/uygunluk kontrolü tamamlanmalı</summary> <summary className="text-xs font-bold cursor-pointer">{kontrolEksikleri.length} belge/uygunluk kontrolü tamamlanmalı</summary>
<ul className="text-xs list-disc pl-5 mt-2 space-y-1">{kontrolEksikleri.map((message,i)=><li key={i}>{message}</li>)}</ul> <ul className="text-xs list-disc pl-5 mt-2 space-y-1">{kontrolEksikleri.map((message,i)=><li key={i}>{message}</li>)}</ul>
+17 -2
View File
@@ -1,11 +1,11 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
import {HEALTH_CHECKS, documentKey} from '../services/preparationChecks'; import {HEALTH_CHECKS, documentKey, documentRevision} from '../services/preparationChecks';
const fieldClass = 'w-full border border-slate-300 rounded-lg p-2 text-xs bg-white'; const fieldClass = 'w-full border border-slate-300 rounded-lg p-2 text-xs bg-white';
const Status = ({value, onChange, optional = true}) => <select aria-label="Kontrol durumu" className={fieldClass} value={value || 'pending'} onChange={e => onChange(e.target.value)}> const Status = ({value, onChange, optional = true}) => <select aria-label="Kontrol durumu" className={fieldClass} value={value || 'pending'} onChange={e => onChange(e.target.value)}>
<option value="pending">Kontrol gerekli</option><option value="verified">Doğrulandı</option> <option value="pending">Kontrol gerekli</option><option value="verified">Doğrulandı</option>
{optional && <option value="notApplicable">Uygulanmıyor — gerekçe gerekli</option>} {optional && <option value="notApplicable">Uygulanmıyor — gerekçe gerekli</option>}
</select>; </select>;
export function PreparationChecks({rows, documents, manualDocuments, setManualDocuments, checks, setChecks}) { export function PreparationChecks({rows, documents, manualDocuments, setManualDocuments, checks, setChecks, vault, refreshVault, downloadFile}) {
const [name,setName] = useState(''); const [name,setName] = useState('');
const [clause,setClause] = useState(''); const [clause,setClause] = useState('');
const updateDocument = (key, patch) => setChecks(old => ({...old,documents:{...old.documents,[key]:{...old.documents?.[key],...patch}}})); const updateDocument = (key, patch) => setChecks(old => ({...old,documents:{...old.documents,[key]:{...old.documents?.[key],...patch}}}));
@@ -14,6 +14,10 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
<h5 className="font-extrabold text-sm">Belge ve ürün uygunluğu</h5> <h5 className="font-extrabold text-sm">Belge ve ürün uygunluğu</h5>
<p className="text-xs text-slate-600">Her kontrol için belge adı/sürümü ve kaynak maddeyi yazın. ÜTS, numune ve servis koşulları her ihalede zorunlu değildir; uygulanmıyorsa gerekçesini belirtin. Bu kayıtlar kullanıcı kontrolüdür; resmi uygunluk onayı değildir.</p> <p className="text-xs text-slate-600">Her kontrol için belge adı/sürümü ve kaynak maddeyi yazın. ÜTS, numune ve servis koşulları her ihalede zorunlu değildir; uygulanmıyorsa gerekçesini belirtin. Bu kayıtlar kullanıcı kontrolüdür; resmi uygunluk onayı değildir.</p>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs">Dosyaları Doküman Deposu'na yükleyin, ardından burada ilgili belge gereksinimine bağlayın.</p>
<button type="button" onClick={refreshVault} disabled={vault.loading} className="text-xs font-bold text-blue-700">Belge deposunu yenile</button>
{vault.loading && <p role="status" className="text-xs">Belge deposu yükleniyor…</p>}
{vault.error && <p role="alert" className="text-xs text-red-700">Belge deposu yüklenemedi: {vault.error}</p>}
{documents.map(d => { {documents.map(d => {
const key = documentKey(d), c = checks.documents?.[key] || {}; const key = documentKey(d), c = checks.documents?.[key] || {};
return <fieldset key={key} className="border rounded-xl p-3 space-y-2"> return <fieldset key={key} className="border rounded-xl p-3 space-y-2">
@@ -21,6 +25,17 @@ export function PreparationChecks({rows, documents, manualDocuments, setManualDo
<p className="text-xs text-slate-600">{d.note}</p> <p className="text-xs text-slate-600">{d.note}</p>
{manualDocuments.some(item => documentKey(item) === key) && <button type="button" className="text-xs text-red-700" {manualDocuments.some(item => documentKey(item) === key) && <button type="button" className="text-xs text-red-700"
onClick={() => setManualDocuments(manualDocuments.filter(item => documentKey(item) !== key))}>Elle eklenen gereksinimi kaldır</button>} onClick={() => setManualDocuments(manualDocuments.filter(item => documentKey(item) !== key))}>Elle eklenen gereksinimi kaldır</button>}
<label className="block text-xs">Bağlı dosya
<select className={fieldClass} value={c.documentId || ''} disabled={vault.loading || Boolean(vault.error)} onChange={e=>{
const file=vault.documents.find(f=>f.id===e.target.value);
updateDocument(key,{documentId:file?.id || null,documentRevision:file ? documentRevision(file) : null,status:'pending'});
}}>
<option value="">Dosya seçin</option>
{c.documentId && !vault.documents.some(f=>f.id===c.documentId) && <option value={c.documentId}>Önceki dosya bulunamadı</option>}
{vault.documents.map(file=><option key={file.id} value={file.id}>{file.ad} · {file.son_kullanma || 'Süre bilgisi yok'}{file.erisilebilir === false ? ' · erişilemiyor' : ''}</option>)}
</select>
</label>
{vault.documents.some(f=>f.id===c.documentId) && <button type="button" className="text-xs text-blue-700" onClick={()=>downloadFile(vault.documents.find(f=>f.id===c.documentId))}>Bağlı dosyayı indir</button>}
<Status value={c.status} optional={d.mandatory === false} onChange={status => updateDocument(key,{status})}/> <Status value={c.status} optional={d.mandatory === false} onChange={status => updateDocument(key,{status})}/>
<label className="block text-xs">Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde <label className="block text-xs">Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde
<input className={fieldClass} value={c.evidence || ''} onChange={e => updateDocument(key,{evidence:e.target.value})}/> <input className={fieldClass} value={c.evidence || ''} onChange={e => updateDocument(key,{evidence:e.target.value})}/>
+14 -1
View File
@@ -5,12 +5,25 @@ export const HEALTH_CHECKS = [
['delivery', 'Teslimat ve süre'], ['delivery', 'Teslimat ve süre'],
]; ];
export const documentKey = d => JSON.stringify([d.document, d.mandatory !== false, d.note || '']); export const documentKey = d => JSON.stringify([d.document, d.mandatory !== false, d.note || '']);
export function preparationBlockers(rows, documents, checks = {}) { export const documentRevision = file => JSON.stringify([file.id,file.olusturma,file.boyut,file.son_kullanma || null]);
export function documentLinkProblem(check, files, deadlineDate) {
const file = files.find(f=>f.id===check?.documentId);
if (!file || file.erisilebilir === false) return 'Bağlı dosya bulunamadı veya erişilemiyor.';
if (check.documentRevision !== documentRevision(file)) return 'Belge bilgisi değişti; dosyayı yeniden seçip kontrol edin.';
if (file.son_kullanma && ((!/^\d{4}-\d{2}-\d{2}$/.test(file.son_kullanma) || Number.isNaN(Date.parse(file.son_kullanma+'T00:00:00Z')) || new Date(file.son_kullanma+'T00:00:00Z').toISOString().slice(0,10) !== file.son_kullanma) ||
!deadlineDate || file.son_kullanma < deadlineDate)) return 'Belge son teklif tarihinde geçerli değil veya tarih doğrulanamadı.';
return null;
}
export function preparationBlockers(rows, documents, checks = {}, vault) {
const errors = []; const errors = [];
if (!documents.length) errors.push('Belge gereksinimleri belirlenmedi. Resmi dokümana göre belge ekleyin.'); if (!documents.length) errors.push('Belge gereksinimleri belirlenmedi. Resmi dokümana göre belge ekleyin.');
for (const d of documents) { for (const d of documents) {
const c = checks.documents?.[documentKey(d)] || {}; const c = checks.documents?.[documentKey(d)] || {};
if (d.mandatory === false && c.status === 'notApplicable' && c.evidence?.trim()) continue; if (d.mandatory === false && c.status === 'notApplicable' && c.evidence?.trim()) continue;
if (vault) {
const problem = vault.loading ? 'Belge deposu yükleniyor.' : vault.error ? 'Belge deposu doğrulanamadı.' : documentLinkProblem(c,vault.documents || [],vault.deadlineDate);
if(problem) errors.push(`${d.document}: ${problem}`);
}
if (c.status !== 'verified' || !c.evidence?.trim()) errors.push(`${d.document}: belge ve kaynak kontrolü eksik.`); if (c.status !== 'verified' || !c.evidence?.trim()) errors.push(`${d.document}: belge ve kaynak kontrolü eksik.`);
} }
for (const row of rows) { for (const row of rows) {
+16
View File
@@ -39,3 +39,19 @@ test('HTTP documents and API keys expose empty successful state, not endless loa
const keys=await request('/api/me/apikeys');assert.equal(keys.status,200);assert.deepEqual(keys.data.anahtarlar,[]); const keys=await request('/api/me/apikeys');assert.equal(keys.status,200);assert.deepEqual(keys.data.anahtarlar,[]);
const data=await request('/api/me/data');assert.equal(data.status,200);assert.deepEqual(data.data.veri,{}); const data=await request('/api/me/data');assert.equal(data.status,200);assert.deepEqual(data.data.veri,{});
}); });
test('HTTP file upload, owned download, metadata update and deletion operate on actual bytes',async()=>{
const bytes=Buffer.from('QA belge — Türkçe içerik, yalnız izole test.');
const uploaded=await request('/api/me/documents','POST',{ad:'qa.txt',mime:'text/plain',icerikBase64:bytes.toString('base64'),kategori:'Firma',sonKullanma:'2026-12-31'});
assert.equal(uploaded.status,200);
const id=uploaded.data.id;
const list=await request('/api/me/documents');const file=list.data.belgeler.find(f=>f.id===id);
assert.equal(file.erisilebilir,true);assert.equal(file.boyut,bytes.length);assert.equal(file.dosya,undefined);
const downloaded=await fetch(base+'/api/me/documents/download?id='+id,{headers:{Authorization:'Bearer '+token}});
assert.equal(downloaded.status,200);assert.deepEqual(Buffer.from(await downloaded.arrayBuffer()),bytes);
assert.equal((await request('/api/me/documents/download?id='+id,'GET',null,other)).status,404);
assert.equal((await request('/api/me/documents/meta','PATCH',{id,sonKullanma:'2027-01-01'})).status,200);
const changed=await request('/api/me/documents');assert.equal(changed.data.belgeler.find(f=>f.id===id).son_kullanma,'2027-01-01');
assert.equal((await request('/api/me/documents?id='+id,'DELETE')).status,200);
assert.equal((await request('/api/me/documents/download?id='+id)).status,404);
});
+13 -1
View File
@@ -1,6 +1,6 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import {preparationBlockers,documentKey,HEALTH_CHECKS} from '../src/services/preparationChecks.js'; import {preparationBlockers,documentKey,HEALTH_CHECKS,documentRevision,documentLinkProblem} from '../src/services/preparationChecks.js';
const rows=[{priceKey:'kit1',name:'Kit'}]; const rows=[{priceKey:'kit1',name:'Kit'}];
const docs=[{document:'Yetki belgesi',mandatory:true,note:'Şartname 7.1'}]; const docs=[{document:'Yetki belgesi',mandatory:true,note:'Şartname 7.1'}];
const complete=()=>({documents:{[documentKey(docs[0])]:{status:'verified',evidence:'Yetki v2, süre kontrol edildi, 7.1'}},products:{kit1:{product:'QA model',source:'Teknik 3.2, katalog s4',status:'suitable',...Object.fromEntries(HEALTH_CHECKS.map(([key])=>[key,{status:'notApplicable',evidence:'Doküman ilgili maddesi kapsamında uygulanmıyor'}]))}}}); const complete=()=>({documents:{[documentKey(docs[0])]:{status:'verified',evidence:'Yetki v2, süre kontrol edildi, 7.1'}},products:{kit1:{product:'QA model',source:'Teknik 3.2, katalog s4',status:'suitable',...Object.fromEntries(HEALTH_CHECKS.map(([key])=>[key,{status:'notApplicable',evidence:'Doküman ilgili maddesi kapsamında uygulanmıyor'}]))}}});
@@ -17,3 +17,15 @@ test('changed requirement and changed product row require new checks',()=>{
assert.ok(preparationBlockers(rows,[{...docs[0],note:'Zeyilname 2'}],complete()).length>0); assert.ok(preparationBlockers(rows,[{...docs[0],note:'Zeyilname 2'}],complete()).length>0);
assert.ok(preparationBlockers([{...rows[0],priceKey:'kit2'}],docs,complete()).length>0); assert.ok(preparationBlockers([{...rows[0],priceKey:'kit2'}],docs,complete()).length>0);
}); });
test('linked file must exist, match reviewed metadata and remain valid at tender deadline',()=>{
const file={id:'f1',olusturma:'2026-09-01',boyut:100,son_kullanma:'2026-09-10',erisilebilir:true};
const c={documentId:file.id,documentRevision:documentRevision(file)};
assert.equal(documentLinkProblem(c,[file],'2026-09-10'),null);
assert.ok(documentLinkProblem(c,[],'2026-09-10'));
assert.ok(documentLinkProblem(c,[{...file,erisilebilir:false}],'2026-09-10'));
assert.ok(documentLinkProblem(c,[{...file,boyut:101}],'2026-09-10'));
assert.ok(documentLinkProblem(c,[file],'2026-09-11'));
const invalid={...file,son_kullanma:'2026-02-31'};
assert.ok(documentLinkProblem({...c,documentRevision:documentRevision(invalid)},[invalid],'2026-01-01'));
});