fix: preserve bid drafts and align tender source data
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import {test} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {quantityParts,validateBidRows,bidRowTotal} from '../src/services/bidValidation.js';
|
||||
import {yaziylaTL,teklifCetveliHtml} from '../src/services/bidDocs.js';
|
||||
test('Turkish quantities, decimal quantities and units are preserved',()=>{
|
||||
assert.deepEqual(quantityParts('2.138.330 puan'),{miktar:2138330,birim:'puan'});
|
||||
assert.deepEqual(quantityParts('1.234,50 litre'),{miktar:1234.5,birim:'litre'});
|
||||
assert.deepEqual(quantityParts('1.5 litre'),{miktar:1.5,birim:'litre'});
|
||||
assert.equal(quantityParts('kalem 15').miktar,null);
|
||||
assert.equal(validateBidRows([{name:'Kit',quantity:'20',birimFiyat:10}]).length,1);
|
||||
});
|
||||
test('empty and partially priced bids cannot generate a valid sheet',()=>{
|
||||
assert.ok(validateBidRows([]).length);
|
||||
assert.throws(()=>teklifCetveliHtml({},[],0));
|
||||
assert.ok(validateBidRows([{name:'Kit',quantity:'20 adet',birimFiyat:''}]).length);
|
||||
assert.equal(bidRowTotal({quantity:'3 adet',birimFiyat:'0.335'}),1.01);
|
||||
assert.equal(yaziylaTL(1.999),'iki Türk Lirası');
|
||||
});
|
||||
test('100 rows retain Turkish names and escaped content in printable sheet',()=>{
|
||||
const rows=Array.from({length:100},(_,i)=>({name:`Ölçüm <kit> ${i}`,quantity:'2 adet',birimFiyat:10}));
|
||||
const out=teklifCetveliHtml({firmaUnvan:'İğdır',ikn:'2026/1'},rows,2000);
|
||||
assert.equal((out.match(/Ölçüm <kit>/g)||[]).length,100);
|
||||
assert.ok(out.includes('İğdır'));assert.ok(out.includes('2.000,00'));
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// Isolated, localhost-only UI fixture. It is never imported by production.
|
||||
import {createServer} from 'vite';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {mkdtempSync,rmSync} from 'node:fs';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join} from 'node:path';
|
||||
import {randomBytes} from 'node:crypto';
|
||||
import {createServer as netServer} from 'node:net';
|
||||
const dir=mkdtempSync(join(tmpdir(),'ihaleist-browser-'));
|
||||
process.env.AUTH_SECRET=randomBytes(32).toString('hex');
|
||||
const {signToken}=await import('../server/auth.mjs');
|
||||
const user={email:'browser-qa@example.test',name:'Laboratuvar QA',isAdmin:false};
|
||||
const token=signToken(user);
|
||||
const probe=netServer();await new Promise(r=>probe.listen(0,'127.0.0.1',r));const port=probe.address().port;await new Promise(r=>probe.close(r));
|
||||
const api=spawn(process.execPath,['server/index.js'],{cwd:new URL('..',import.meta.url),env:{PATH:process.env.PATH,NODE_ENV:'test',AUTH_SECRET:process.env.AUTH_SECRET,IHALEIST_USER_DATA_DIR:dir,PORT:String(port),UYUM_GOMME:'0'},stdio:'ignore'});
|
||||
const entries=[['1582991','Giresun','10.09.2026','10:00'],['1534665','Kayseri','22.09.2026','11:00'],['1499061','Akdeniz','22.09.2026','10:30'],['1573235','Aksaray','09.09.2026','10:00'],['1523851','Tepecik','11.09.2026','10:00']];
|
||||
const tenders=entries.map(([id,city,date,time])=>({id:'qa-'+id,ikn:'2026/'+id,baslik:'QA — '+city+' laboratuvar',kurum:city+' TEST KURUMU',sehir:city,tarih:date,saat:time,alimTuru:'Mal Alımı',sektor:'Laboratuvar & Ölçüm',sektorler:['Laboratuvar & Ölçüm'],durum:'Açık',takipte:false,kanbanStatus:'incelenecek',items:[{id:'1',ad:'Sentetik test kalemi',miktar:2,birim:'adet'}],aciklama:'Bu yalnızca yerel arayüz test verisidir. Teklif geçerlilik süresi 120 takvim günü. Birim fiyat teklif. '.repeat(3)}));
|
||||
const ui=await createServer({server:{host:'127.0.0.1',port:0,proxy:{'/api':{target:'http://127.0.0.1:'+port}}},plugins:[{name:'qa-fixtures',configureServer(server){server.middlewares.use((req,res,next)=>{
|
||||
if(req.url==='/qa-signin') {res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<html lang="tr"><h1>Yerel laboratuvar testi</h1><p>Sentetik ihale verileri ve geçici kullanıcı deposu kullanılır.</p><button id="login">Test hesabıyla gir</button><script type="module">import {setSession} from '/src/services/auth.js';document.querySelector('#login').onclick=()=>{setSession(${JSON.stringify(token)},${JSON.stringify(user)});location.href='/';};</script></html>`);return;}
|
||||
if(req.url?.startsWith('/api/tenders')){res.setHeader('Content-Type','application/json');res.end(JSON.stringify({success:true,tenders,totalCount:5,todayCount:0,historyCount:0,source:'Yerel sentetik QA',fetchedAt:new Date().toISOString()}));return;}
|
||||
next();
|
||||
});}}]});
|
||||
await ui.listen();console.log('QA_URL='+ui.resolvedUrls.local[0]+'qa-signin');
|
||||
async function stop(){await ui.close();api.kill();rmSync(dir,{recursive:true,force:true});process.exit();}
|
||||
process.on('SIGINT',stop);process.on('SIGTERM',stop);
|
||||
@@ -0,0 +1,7 @@
|
||||
import {test} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {documentContent} from '../server/document-content.mjs';
|
||||
test('EKAP metin, upload text and plain string share the same reader',()=>{
|
||||
for(const input of [{metin:'Şartname'},{text:'Şartname'},'Şartname']) assert.equal(documentContent(input),'Şartname');
|
||||
assert.equal(documentContent(null),'');assert.equal(documentContent({metin:'',text:'İlan'}),'İlan');
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import {test} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {parseRates} from '../server/exchange-rates.mjs';
|
||||
test('rates carry bulletin date and use unit-adjusted selling price',()=>{
|
||||
const xml='<Tarih_Date Tarih="04.09.2026"><Currency CurrencyCode="USD"><Unit>1</Unit><ForexSelling>48.3195</ForexSelling></Currency><Currency CurrencyCode="EUR"><Unit>100</Unit><ForexSelling>5500</ForexSelling></Currency></Tarih_Date>';
|
||||
const d=parseRates(xml);assert.equal(d.rates.USD,48.3195);assert.equal(d.rates.EUR,55);assert.equal(d.bulletinDate,'04.09.2026');
|
||||
assert.throws(()=>parseRates('<html>error</html>'));
|
||||
assert.throws(()=>parseRates(xml.replace('<Unit>100','<Unit>0')));
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import {test,before,after} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {spawn} from 'node:child_process';
|
||||
import {mkdtempSync,rmSync} from 'node:fs';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join} from 'node:path';
|
||||
import {randomBytes} from 'node:crypto';
|
||||
import {createServer} from 'node:net';
|
||||
const dir=mkdtempSync(join(tmpdir(),'ihaleist-http-'));
|
||||
process.env.AUTH_SECRET=randomBytes(32).toString('hex');
|
||||
const {signToken}=await import('../server/auth.mjs');
|
||||
const token=signToken({email:'qa@example.test',name:'QA'});
|
||||
const other=signToken({email:'other@example.test',name:'Other'});
|
||||
let child,base;
|
||||
const request=async(path,method='GET',body,auth=token)=>{
|
||||
const r=await fetch(base+path,{method,headers:{'Content-Type':'application/json',...(auth?{Authorization:'Bearer '+auth}:{})},body:body?JSON.stringify(body):undefined});
|
||||
return {status:r.status,data:await r.json()};
|
||||
};
|
||||
before(async()=>{
|
||||
const socket=createServer();await new Promise(r=>socket.listen(0,'127.0.0.1',r));const port=socket.address().port;await new Promise(r=>socket.close(r));
|
||||
base='http://127.0.0.1:'+port;
|
||||
child=spawn(process.execPath,['server/index.js'],{cwd:new URL('..',import.meta.url),env:{...process.env,NODE_ENV:'test',PORT:String(port),IHALEIST_USER_DATA_DIR:dir,UYUM_GOMME:'0'},stdio:'ignore'});
|
||||
for(let i=0;i<100;i++){try{await fetch(base+'/api/auth/google/config');return;}catch{await new Promise(r=>setTimeout(r,50));}}
|
||||
throw new Error('QA API did not start');
|
||||
});
|
||||
after(async()=>{if(child){child.kill();await new Promise(r=>child.once('exit',r));}rmSync(dir,{recursive:true,force:true});});
|
||||
test('HTTP preparation requires auth, persists draft and rejects stale revisions',async()=>{
|
||||
assert.equal((await request('/api/me/tender-state','GET',null,null)).status,401);
|
||||
const draft={firma:{firmaUnvan:'QA Laboratuvar'},fiyatlar:{0:'12.50'},gecerlilikGun:'120'};
|
||||
const data={key:'2026/1582991',patch:{takipte:true,gorev:{kontrolListesi:[]},bidDraft:draft},snapshot:{id:'ilan-qa',ikn:'2026/1582991'},version:0};
|
||||
assert.equal((await request('/api/me/tender-state','PUT',data)).status,200);
|
||||
const read=await request('/api/me/tender-state');assert.deepEqual(read.data.records[data.key].data.bidDraft,draft);
|
||||
assert.equal((await request('/api/me/tender-state','PUT',data)).status,409);
|
||||
assert.deepEqual((await request('/api/me/tender-state','GET',null,other)).data.records,{});
|
||||
});
|
||||
test('HTTP documents and API keys expose empty successful state, not endless load errors',async()=>{
|
||||
const docs=await request('/api/me/documents');assert.equal(docs.status,200);assert.deepEqual(docs.data.belgeler,[]);
|
||||
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,{});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import {test} from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {uniqueTenders,announcementFields} from '../server/tender-quality.mjs';
|
||||
test('duplicate DT records merge without merging another institution or losing source identities',()=>{
|
||||
const t={id:'dt-1',ikn:'26DT1693370',kurum:'Hastane'};
|
||||
const out=uniqueTenders([t,{...t,id:'dt-2',aciklama:'Tam metin'},{...t,id:'dt-3',kurum:'Başka hastane'}]);
|
||||
assert.equal(out.length,2);assert.deepEqual(out[0].sourceIds,['dt-1','dt-2']);assert.equal(out[0].aciklama,'Tam metin');
|
||||
});
|
||||
test('distinct contract lots and corrections remain separate',()=>{
|
||||
const t={ikn:'2026/123',durum:'Sonuçlandı',kisim:'1',id:'a',kazananFirma:'Firma'};
|
||||
assert.equal(uniqueTenders([t,t,{...t,id:'b',kisim:'2'}]).length,2);
|
||||
assert.equal(uniqueTenders([{ikn:'2026/123',id:'1'},{ikn:'2026/123',id:'2',baslik:'Düzeltme ilanı'}]).length,2);
|
||||
});
|
||||
test('announcement preserves full text and extracts source-backed fields',()=>{
|
||||
const text='x'.repeat(4500)+' Tarih ve Saati : 10.09.2026 - 10:00 3.3. Teslim edileceği yer : Patoloji laboratuvarı 3.4. Süresi/teslim tarihi : 30 gün 3.5. İşe başlama tarihi : sözleşme';
|
||||
const d=announcementFields(text);assert.equal(d.aciklama,text);assert.equal(d.saat,'10:00');assert.equal(d.teslimYeri,'Patoloji laboratuvarı');assert.equal(d.teslimatSuresi,'30 gün');
|
||||
assert.deepEqual(announcementFields('Eksik ilan'),{aciklama:'Eksik ilan'});
|
||||
});
|
||||
Reference in New Issue
Block a user