From 32ea7496691f62e13c4afc333d659f5024e7a540 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 09:34:52 +0300 Subject: [PATCH] Persist document processing outcomes separately from upload success --- docs/quality/uygulama-durumu.md | 8 ++++++++ server/index.js | 22 ++++++++++++++++++++-- server/user-store.mjs | 12 ++++++++++-- src/components/TenderDocuments.jsx | 9 ++++++--- src/components/tools/DocumentVault.jsx | 10 ++++++++-- src/services/documentProcessing.js | 10 ++++++++++ tests/http-state.test.mjs | 9 +++++++++ tests/user-store.test.mjs | 2 +- 8 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 src/services/documentProcessing.js diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 33c3e7a..418ea8c 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -287,3 +287,11 @@ SQLite snapshot, referanslı belge kopyası, SHA-256 manifesti, bütünlük kont - /api/kalem/cikar artık belge kimliği, sahiplik, ihale türü ve aynı İKN bağı şartlarını kontrol ediyor. Başka hesaba ait dosya, farklı ihale, firma belgesi veya belgesiz metin ortak havuza yazılamıyor. PDF istemcileri yüklenen belge kimliğini iletiyor. - Kanıt: gerçek HTTP akışında firma ve ihale dosyası yükleme; kategori adını değiştirerek yükseltme girişimi; farklı hesap/İKN ve belgesiz çağrı reddi; doğru dokümanla boş cetvel ayrıştırma başarı yolu test edildi. Eski şema migration testinde varsayılan firma türü doğrulandı. 70/70 test ve build geçti. - Yayın notu: frontend ve backend birlikte yayımlanmalı; eski istemcinin belgesiz kalem POST’u artık reddedilir. Önceden yüklenmiş resmi dokümanlar otomatik ortak yayın yetkisi kazanmaz; kontrollü yeniden sınıflandırma akışı henüz yok. Geçmişte havuza yazılmış kayıtların köken denetimi ve gerçek PDF/OCR bütünlüğü ayrıca açık. + +### Belge yükleme ve işleme sonuçlarının ayrılması + +D01 kapsamında `belgeler.isleme` sürümlü migration ile eklendi. Dosyanın kaydedilmesi artık kalem çıkarımının başarılı olduğu anlamına gelmiyor: bekleyen metin, kalemsiz sonuç, desteklenmeyen biçim, eksik İKN, firma belgesi ve işleme hatası ayrı saklanıp iki belge ekranında gösteriliyor. Yetkisi doğrulanmış çıkarım isteğinin hatası kalıcı kaydediliyor; sonraki başarılı yeniden deneme durumu değiştirebiliyor. Toplu yüklemede liste yenileme hatası kaydedilmiş dosyayı başarısız yükleme olarak etiketlemiyor. + +Doğrulama: 70 otomatik test ve üretim derlemesi geçti. Gerçek HTTP testinde özel firma belgesi, bekleyen ihale metni, yetkisiz istek, bozuk metin → hata → yeniden deneme → kalemsiz sonuç ve ayrı GET üzerinden kalıcı durum kontrol edildi. Mevcut bundle boyutu/import uyarıları sürüyor. + +Sınırlar: Bu paket tam belge yaşam döngüsü kabulü değildir. Tarayıcıda PDF okuma hatasının sunucuya kalıcı aktarımı, ekrandan mevcut belgeyi yeniden işleme, dosya/DB çökme atomikliği ve gerçek resmi dosyalarla uçtan uca kabul halen tamamlanmadı. Canlıya dağıtım yapılmadı. diff --git a/server/index.js b/server/index.js index b4ce471..0df4c9e 100644 --- a/server/index.js +++ b/server/index.js @@ -743,21 +743,30 @@ const server = http.createServer(async (req, res) => { if (url.pathname === '/api/kalem/cikar' && req.method === 'POST') { const user = userFromAuthHeader(req.headers['authorization']); if (!user) return json(res, 401, { success: false, error: 'oturum yok' }); + let validatedDocument = null; try { const body = await readBody(req, 4 * 1024 * 1024); if (!body.ikn) return json(res, 400, { success: false, error: 'ikn gerekli' }); const sourceDocument = body.documentId && store.getDocument(user.email,String(body.documentId)); if (!sourceDocument || sourceDocument.tur !== 'ihale' || sourceDocument.ikn !== String(body.ikn)) return json(res,403,{success:false,error:'Kalem aktarımı için bu ihaleye bağlı, size ait ihale dokümanı gerekli. Firma belgeleri ortak havuza aktarılamaz.'}); + validatedDocument = sourceDocument; + if (typeof body.metin !== 'string') throw new Error('Doküman metni gerekli.'); const { kalemler, yontem } = kalemleriCikar(body.metin || ''); if (!kalemler.length) { + store.setDocumentProcessing(user.email,sourceDocument.id,{status:'no_items',count:0}); return json(res, 200, { success: true, kalemler: [], yontem, not: 'Metinde teklif cetveli tablosu bulunamadı.' }); } kalemStore.kalemYaz(String(body.ikn), kalemler, { dosya: body.dosya ? String(body.dosya).slice(0, 200) : null, yontem, ekleyen: user.email, }); + store.setDocumentProcessing(user.email,sourceDocument.id,{status:'processed',count:kalemler.length}); return json(res, 200, { success: true, kalemler, yontem, sayi: kalemler.length }); } catch (e) { + if (validatedDocument) { + try { store.setDocumentProcessing(user.email,validatedDocument.id,{status:'error',error:'Kalem çıkarımı tamamlanamadı. Doküman metnini kontrol edip yeniden deneyin.'}); } + catch (statusError) { console.error('[document-processing] Durum kaydedilemedi:', statusError.message); } + } return json(res, 400, { success: false, error: e.message }); } } @@ -970,8 +979,9 @@ const server = http.createServer(async (req, res) => { // PDF tarayıcıda okunduğu için istemci ikn'yi kendisi gönderir. let ikn = body.ikn ? String(body.ikn).slice(0, 40) : null; let ofisMetni = null; + let readError = null; if (sunucuOkuyabilir(body.ad)) { - try { ofisMetni = arsivMetni(String(body.ad), buf); } catch { /* metin çıkmazsa devam */ } + try { ofisMetni = arsivMetni(String(body.ad), buf); } catch { readError = 'Dosya metni okunamadı. Dosya biçimini ve bütünlüğünü kontrol edin.'; } if (documentType === 'ihale' && !ikn && body.otoIkn && ofisMetni) ikn = iknCikar(ofisMetni); } const id = store.saveDocumentFile(user.email, { @@ -986,19 +996,27 @@ const server = http.createServer(async (req, res) => { // tıklama beklemez. PDF'ler tarayıcıda çözüldüğü için istemci // metni /api/kalem/cikar'a gönderir. let kalemBilgi = null; + let processing = documentType !== 'ihale' ? {status:'not_applicable'} + : readError ? {status:'error',error:readError} + : /\.(pdf|txt|md)$/i.test(body.ad) ? {status:'awaiting_text'} + : !sunucuOkuyabilir(body.ad) ? {status:'unsupported'} + : !ikn ? {status:'needs_tender'} : {status:'no_items',count:0}; if (documentType === 'ihale' && ikn && ofisMetni) { try { const { kalemler, yontem } = kalemleriCikar(ofisMetni); if (kalemler.length) { kalemStore.kalemYaz(ikn, kalemler, { dosya: String(body.ad), yontem, ekleyen: user.email }); kalemBilgi = { sayi: kalemler.length, yontem }; + processing = {status:'processed',count:kalemler.length}; console.log(`[kalem] ${ikn} → ${kalemler.length} kalem (${yontem}, ${body.ad})`); } } catch (e) { + processing = {status:'error',error:'Kalem çıkarımı tamamlanamadı; yeniden deneyin.'}; console.warn('[kalem] çıkarılamadı:', e.message); } } - return json(res, 200, { success: true, id, ikn, kalem: kalemBilgi, belgeler: store.listDocuments(user.email), kullanim: store.documentUsage(user.email) }); + store.setDocumentProcessing(user.email,id,processing); + return json(res, 200, { success: true, id, ikn, processing, kalem: kalemBilgi, belgeler: store.listDocuments(user.email), kullanim: store.documentUsage(user.email) }); } catch (e) { return json(res, e.kod === 'KOTA_ASILDI' ? 402 : 400, { success: false, error: e.message, kod: e.kod || null }); } diff --git a/server/user-store.mjs b/server/user-store.mjs index 0252eae..00645d3 100644 --- a/server/user-store.mjs +++ b/server/user-store.mjs @@ -98,12 +98,15 @@ export function openDb() { if (!columns.has(column)) db.exec(`ALTER TABLE belgeler ADD COLUMN ${column} TEXT`); } if (!columns.has('tur')) db.exec("ALTER TABLE belgeler ADD COLUMN tur TEXT NOT NULL DEFAULT 'firma'"); + if (!columns.has('isleme')) db.exec('ALTER TABLE belgeler ADD COLUMN isleme TEXT'); db.exec('CREATE INDEX IF NOT EXISTS idx_belge_ikn ON belgeler(email, ikn)'); db.exec(`CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT PRIMARY KEY, applied_at TEXT NOT NULL)`); db.prepare('INSERT OR IGNORE INTO schema_migrations VALUES (?,?)') .run('20260906_document_columns', new Date().toISOString()); db.prepare('INSERT OR IGNORE INTO schema_migrations VALUES (?,?)') .run('20260906_document_type', new Date().toISOString()); + db.prepare('INSERT OR IGNORE INTO schema_migrations VALUES (?,?)') + .run('20260906_document_processing', new Date().toISOString()); db.exec('COMMIT'); } catch (error) { try { db.exec('ROLLBACK'); } catch { /* original error is authoritative */ } @@ -269,11 +272,16 @@ export function saveDocumentFile(email, metadata, bytes) { } export function listDocuments(email, { ikn } = {}) { - const alanlar = 'id, ad, kategori, boyut, mime, son_kullanma, ikn, olusturma, dosya, tur'; + const alanlar = 'id, ad, kategori, boyut, mime, son_kullanma, ikn, olusturma, dosya, tur, isleme'; 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=? ORDER BY olusturma DESC`).all(email) - ).map(({dosya,...record}) => ({...record, erisilebilir: fs.existsSync(path.join(BELGE_DIR,dosya))})), []); + ).map(({dosya,isleme,...record}) => ({...record, isleme:isleme ? JSON.parse(isleme) : null, erisilebilir: fs.existsSync(path.join(BELGE_DIR,dosya))})), []); +} + +export function setDocumentProcessing(email,id,processing) { + return withDb(db=>db.prepare('UPDATE belgeler SET isleme=? WHERE id=? AND email=?') + .run(JSON.stringify({...processing,updatedAt:now()}),id,email).changes>0); } export function getDocument(email, id) { diff --git a/src/components/TenderDocuments.jsx b/src/components/TenderDocuments.jsx index 19d5791..408ca2f 100644 --- a/src/components/TenderDocuments.jsx +++ b/src/components/TenderDocuments.jsx @@ -1,3 +1,4 @@ +import { documentProcessingLabel } from '../services/documentProcessing'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useApp } from '../context/AppContext'; import { getToken } from '../services/auth'; @@ -49,16 +50,17 @@ export const TenderDocuments = ({ tender }) => { // otomatik çıkarır. PDF tarayıcıda çözüldüğü için metni biz göndeririz. const cevap = await uploadDocument(f, 'İhale Dokümanı', ikn, {documentType:'ihale'}); let kalem = cevap?.kalem?.sayi || null; + let kalemHata = cevap.processing?.status === 'error' ? cevap.processing.error : null; if (!kalem && ikn && /\.(pdf|txt|md)$/i.test(f.name)) { try { const { text } = await extractText(f); const { kalemCikar } = await import('../services/kalem'); const d = await kalemCikar({ ikn, metin: text, dosya: f.name, documentId:cevap.id }); kalem = d.sayi || null; - } catch { /* kalem çıkmazsa yükleme yine de başarılı sayılır */ } + } catch (error) { kalemHata = error.message; } } belgeleriYukle(); - setBilgi({ yuklendi: f.name, kalem }); + setBilgi({ yuklendi: f.name, kalem, kalemHata }); } catch (err) { setError(err.kod === 'KOTA_ASILDI' ? err.message : `Yüklenemedi: ${err.message}`); } finally { setYukleniyor(false); } @@ -283,6 +285,7 @@ export const TenderDocuments = ({ tender }) => { {bilgi.yuklendi && ( <> {bilgi.yuklendi} yüklendi ve bu ihaleye bağlandı. + {bilgi.kalemHata && Kalem aktarımı tamamlanamadı: {bilgi.kalemHata}. Dosya kaydı korunmuştur.} {bilgi.kalem ? <> Teklif cetvelinden {bilgi.kalem} kalem otomatik çıkarıldı; bu ihale artık kalemleriyle listeleniyor. : <> Bu dosyadan teklif cetveli kalemi çıkarılamadı.} @@ -339,7 +342,7 @@ export const TenderDocuments = ({ tender }) => { {belgeler.map((b) => (
- {b.ad} + {b.ad}{documentProcessingLabel(b.isleme)} {(b.boyut / 1024).toLocaleString('tr-TR', { maximumFractionDigits: 0 })} KB diff --git a/src/components/tools/DocumentVault.jsx b/src/components/tools/DocumentVault.jsx index c4431d6..f9a78fd 100644 --- a/src/components/tools/DocumentVault.jsx +++ b/src/components/tools/DocumentVault.jsx @@ -1,3 +1,4 @@ +import { documentProcessingLabel } from '../../services/documentProcessing'; import React, { useState, useEffect, useRef } from 'react'; import { useApp } from '../../context/AppContext'; import { listDocuments, uploadDocument, deleteDocument, downloadDocument, formatBayt, updateDocumentMeta } from '../../services/account'; @@ -67,14 +68,17 @@ export const DocumentVault = () => { const cevap = await uploadDocument(f, 'İhale Dokümanı', ikn, { otoIkn: true, documentType:'ihale' }); ikn = cevap?.ikn || ikn; let kalem = cevap?.kalem?.sayi || null; + let kalemHata = cevap.processing?.status === 'error' ? cevap.processing.error : null; if (ikn && !kalem && metin) { try { const { kalemCikar } = await import('../../services/kalem'); kalem = (await kalemCikar({ ikn, metin, dosya: f.name, documentId:cevap.id })).sayi || null; - } catch { /* kalem çıkmazsa yükleme yine başarılı */ } + } catch (error) { kalemHata = error.message; } } - guncelle(i, { durum: 'tamam', ikn, kalem }); + guncelle(i, { durum: 'tamam', ikn, kalem, kalemHata }); setDurum(cevap); + try { setDurum(await listDocuments()); } + catch { setHata('Dosya kayıtlı; güncel belge listesi alınamadı. Listeyi yeniden yükleyin.'); } } catch (err) { guncelle(i, { durum: 'hata', hata: err.message }); } @@ -218,6 +222,7 @@ export const DocumentVault = () => { {t.ikn ? {t.ikn} : İKN bulunamadı — ihaleye bağlanmadı} + {t.kalemHata && Dosya kayıtlı; kalem aktarımı başarısız: {t.kalemHata}} {t.kalem ? {t.kalem} kalem : null} )} @@ -254,6 +259,7 @@ export const DocumentVault = () => {

{new Date(d.olusturma).toLocaleString('tr-TR')} + {documentProcessingLabel(d.isleme)}