import fs from 'node:fs'; import path from 'node:path'; import {createHash} from 'node:crypto'; import {openDb,BELGE_DIR} from './user-store.mjs'; const hash=value=>createHash('sha256').update(value).digest('hex'); const fail=(status,message)=>Object.assign(new Error(message),{status}); function source(db,email,id) { const record=db.prepare('SELECT * FROM belgeler WHERE id=? AND email=?').get(id,email); if(!record || record.tur !== 'ihale') throw fail(404,'İhale dokümanı bulunamadı.'); let bytes; try {bytes=fs.readFileSync(path.join(BELGE_DIR,record.dosya));} catch {throw fail(409,'Kaynak dosya okunamıyor; inceleme kaydedilemez.');} return hash(bytes); } function latest(db,email,id) { return db.prepare('SELECT revision,file_hash,text_hash,corrected_text,review_note,reviewed_at FROM document_text_reviews WHERE document_id=? AND email=? ORDER BY revision DESC LIMIT 1').get(id,email) || null; } export function documentReview(email,id) { const db=openDb(); try { const fileHash=source(db,email,id), review=latest(db,email,id); return {documentId:id,fileHash,review,stale:Boolean(review && review.file_hash!==fileHash)}; } finally {db.close();} } export function saveDocumentReview(email,id,input) { if(typeof input.correctedText !== 'string' || input.correctedText.trim().length<40 || Buffer.byteLength(input.correctedText)>1024*1024) throw fail(400,'Kontrol edilen metin 40 karakter ile 1 MB arasında olmalı.'); if(typeof input.note !== 'string' || input.note.trim().length<20 || input.note.length>2000) throw fail(400,'Karşılaştırılan sayfaları ve yapılan düzeltmeleri en az 20 karakterle açıklayın.'); if(input.allPagesCompared !== true) throw fail(400,'Tüm kaynak sayfalarıyla karşılaştırma gerekli.'); if(!Number.isInteger(input.expectedRevision) || input.expectedRevision<0) throw fail(400,'Geçerli inceleme sürümü gerekli.'); const db=openDb(); try { db.exec('BEGIN IMMEDIATE'); const fileHash=source(db,email,id), previous=latest(db,email,id); if(input.fileHash!==fileHash || input.expectedRevision!==(previous?.revision || 0)) throw fail(409,'Dosya veya inceleme sürümü değişti. Güncel kaydı yeniden açın.'); const text=input.correctedText.trim(); db.prepare('INSERT INTO document_text_reviews VALUES (?,?,?,?,?,?,?,?)').run(id,email,input.expectedRevision+1,fileHash,hash(text),text,input.note.trim(),new Date().toISOString()); const review=latest(db,email,id); db.exec('COMMIT');return {documentId:id,fileHash,review,stale:false}; } catch(error) {try {db.exec('ROLLBACK');} catch {} throw error;} finally {db.close();} } /** Resolve a requested review to server-owned text; caller text/approval is never used. */ export function reviewedAnalysisSource(email,reference,tenderKey) { if(!reference || typeof reference.documentId !== 'string' || !Number.isInteger(reference.revision)) throw fail(400,'Geçerli belge ve inceleme sürümü gerekli.'); const db=openDb(); try { const fileHash=source(db,email,reference.documentId); const doc=db.prepare('SELECT ad,ikn FROM belgeler WHERE id=? AND email=?').get(reference.documentId,email); if(!doc.ikn || doc.ikn!==tenderKey) throw fail(409,'İnceleme bu ihaleye bağlı değil.'); const review=latest(db,email,reference.documentId); if(!review || review.revision!==reference.revision || review.file_hash!==fileHash) throw fail(409,'İnceleme veya kaynak dosya değişti; güncel incelemeyi açın.'); return {text:review.corrected_text,fileName:doc.ad, humanReview:{documentId:reference.documentId,revision:review.revision,fileHash,textHash:review.text_hash, reviewedBy:email,reviewedAt:review.reviewed_at,note:review.review_note}}; } finally {db.close();} }