From 311344306b15186b05c57538c75097460cad00f7 Mon Sep 17 00:00:00 2001 From: ht Date: Sun, 6 Sep 2026 08:50:18 +0300 Subject: [PATCH] fix: preserve Turkish decimal prices and verify preparation persistence --- .../evidence/qa-checks-after-reload.txt | 150 ++++++++++++++++++ .../evidence/qa-draft-after-reload.txt | 122 ++++++++++++++ .../evidence/qa-turkish-price-fixed.txt | 72 +++++++++ docs/quality/uygulama-durumu.md | 10 +- src/components/BidPackageBuilder.jsx | 9 +- src/services/bidDocs.js | 4 +- src/services/bidValidation.js | 16 +- tests/bid-validation.test.mjs | 14 +- 8 files changed, 387 insertions(+), 10 deletions(-) create mode 100644 docs/quality/evidence/qa-checks-after-reload.txt create mode 100644 docs/quality/evidence/qa-draft-after-reload.txt create mode 100644 docs/quality/evidence/qa-turkish-price-fixed.txt diff --git a/docs/quality/evidence/qa-checks-after-reload.txt b/docs/quality/evidence/qa-checks-after-reload.txt new file mode 100644 index 0000000..1156c08 --- /dev/null +++ b/docs/quality/evidence/qa-checks-after-reload.txt @@ -0,0 +1,150 @@ + + - status: Sunucuda kayıtlı taslak + - alert: Kalemler ve teklif koşulları resmi dokümanla henüz karşılaştırılmadı. Kaynağı kontrol etmeden teklif çıktısı oluşturulamaz. + - checkbox "Bu sürümün kalemlerini, miktarlarını, teklif türünü ve geçerlilik süresini resmi dokümanla karşılaştırdım." + - text: Bu sürümün kalemlerini, miktarlarını, teklif türünü ve geçerlilik süresini resmi dokümanla karşılaştırdım. + - heading "İstekli (firma) bilgileri" [level=5] + - generic: Firma ticaret unvanı * + - textbox "Firma ticaret unvanı *": + - /placeholder: "" + - text: QA Laboratuvar Test Ltd + - generic: Tebligata esas adres * + - textbox "Tebligata esas adres *": + - /placeholder: "" + - text: Yerel test adresi + - generic: Vergi dairesi * + - textbox "Vergi dairesi *": + - /placeholder: "" + - text: Test + - generic: Vergi numarası * + - textbox "Vergi numarası *": + - /placeholder: "" + - text: "0000000000" + - generic: Ticaret sicil / MERSİS no + - textbox "Ticaret sicil / MERSİS no": + - /placeholder: "" + - generic: Yetkili ad soyad * + - textbox "Yetkili ad soyad *": + - /placeholder: "" + - text: Test Yetkilisi + - generic: Yetkilinin görevi * + - textbox "Yetkilinin görevi *": + - /placeholder: Örn. Genel Müdür + - text: Test Sorumlusu + - generic: Telefon * + - textbox "Telefon *": + - /placeholder: "" + - text: "0000000000" + - generic: E-posta * + - textbox "E-posta *": + - /placeholder: "" + - text: qa@example.test + - generic: Teklif türü + - combobox "Teklif türü": + - option "Birim fiyat" [selected] + - option "Anahtar teslim götürü bedel" + - generic: Teklif geçerlilik (gün) + - textbox "Teklif geçerlilik (gün)": "120" + - generic: Kısım (varsa) + - textbox "Kısım (varsa)": + - /placeholder: Örn. 2. kısım + - heading "Kalem fiyatlandırma" [level=5] + - paragraph: Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın. + - table: + - rowgroup: + - row "Kalem Miktar Birim fiyat Tutar": + - columnheader "Kalem" + - columnheader "Miktar" + - columnheader "Birim fiyat" + - columnheader "Tutar" + - rowgroup: + - row "Sentetik test kalemi 2 adet 125,50 251,00": + - cell "Sentetik test kalemi" + - cell "2 adet" + - cell "125,50": + - textbox "0": 125,50 + - cell "251,00" + - region "Belge ve ürün uygunluğu": + - heading "Belge ve ürün uygunluğu" [level=5] + - paragraph: 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. + - group "Sentetik yetki belgesi (zorunlu)": + - generic: Sentetik yetki belgesi (zorunlu) + - paragraph: Yerel QA dokümanı madde 7 + - button "Elle eklenen gereksinimi kaldır" + - combobox "Kontrol durumu": + - option "Kontrol gerekli" + - option "Doğrulandı" [selected] + - text: Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde + - textbox "Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde": QA dosyası v1 — yalnız test, madde 7 + - paragraph: Resmi dokümandan eksik belge gereksinimi ekle + - text: Belge adı + - textbox "Belge adı" + - text: Kaynak doküman ve madde + - textbox "Kaynak doküman ve madde" + - button "Belge gereksinimi ekle" [disabled] + - generic "Sentetik test kalemi — ürün/hizmet ve yükümlülük kontrolü" [active] + - text: Teklif edilen marka/model veya hizmet kapsamı + - textbox "Teklif edilen marka/model veya hizmet kapsamı": QA Model 1 + - text: Teknik doküman/madde ve uygunluk dayanağı + - textbox "Teknik doküman/madde ve uygunluk dayanağı": Sentetik QA teknik madde 3 + - text: Ürün/hizmet uygunluğu + - combobox "Ürün/hizmet uygunluğu": + - option "Kontrol gerekli" + - option "Uygun" [selected] + - option "Uygun değil" + - group "ÜTS / ürün kaydı": + - generic: ÜTS / ürün kaydı + - combobox "Kontrol durumu": + - option "Kontrol gerekli" + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" [selected] + - text: Kaynak, karşılanan koşul veya uygulanmama gerekçesi + - textbox "Kaynak, karşılanan koşul veya uygulanmama gerekçesi": Yalnız yerel sentetik test; gerçek şartname kontrolü değildir. + - group "Numune / demonstrasyon": + - generic: Numune / demonstrasyon + - combobox "Kontrol durumu": + - option "Kontrol gerekli" + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" [selected] + - text: Kaynak, karşılanan koşul veya uygulanmama gerekçesi + - textbox "Kaynak, karşılanan koşul veya uygulanmama gerekçesi": Yalnız yerel sentetik test; gerçek şartname kontrolü değildir. + - group "Kurulum / bakım / servis": + - generic: Kurulum / bakım / servis + - combobox "Kontrol durumu": + - option "Kontrol gerekli" + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" [selected] + - text: Kaynak, karşılanan koşul veya uygulanmama gerekçesi + - textbox "Kaynak, karşılanan koşul veya uygulanmama gerekçesi": Yalnız yerel sentetik test; gerçek şartname kontrolü değildir. + - group "Teslimat ve süre": + - generic: Teslimat ve süre + - combobox "Kontrol durumu": + - option "Kontrol gerekli" + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" [selected] + - text: Kaynak, karşılanan koşul veya uygulanmama gerekçesi + - textbox "Kaynak, karşılanan koşul veya uygulanmama gerekçesi": Yalnız yerel sentetik test; gerçek şartname kontrolü değildir. + - text: Sorumlu + - textbox "Sorumlu": QA Yetkilisi + - generic: Teklif toplamı (KDV hariç) + - generic: 251,00 TL + - paragraph: "Yazı ile: ikiyüzellibir Türk Lirası" + - heading "Hazırlanacak belgeler" [level=5] + - button "Teklif Mektubu Rakam ve yazı ile bedel Yazdır / PDF" [disabled]: + - generic: Teklif Mektubu + - generic: Rakam ve yazı ile bedel + - text: Yazdır / PDF + - button "Birim Fiyat Teklif Cetveli 1 kalem Yazdır / PDF" [disabled]: + - generic: Birim Fiyat Teklif Cetveli + - generic: 1 kalem + - text: Yazdır / PDF + - button "Evrak Kontrol Listesi 1 belge Yazdır / PDF" [disabled]: + - generic: Evrak Kontrol Listesi + - generic: 1 belge + - text: Yazdır / PDF + - paragraph: Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın. + - paragraph: Belgeler taslaktır ve idarenin standart formlarının yerine geçmez. İmzalamadan önce ihale dokümanındaki form metinleriyle karşılaştırın. +- button +- button "Kullanım kılavuzu (? tuşu)": +- region "Notifications (F8)": + - list \ No newline at end of file diff --git a/docs/quality/evidence/qa-draft-after-reload.txt b/docs/quality/evidence/qa-draft-after-reload.txt new file mode 100644 index 0000000..8820b50 --- /dev/null +++ b/docs/quality/evidence/qa-draft-after-reload.txt @@ -0,0 +1,122 @@ + + - status: Sunucuda kayıtlı taslak + - alert: Kalemler ve teklif koşulları resmi dokümanla henüz karşılaştırılmadı. Kaynağı kontrol etmeden teklif çıktısı oluşturulamaz. + - checkbox "Bu sürümün kalemlerini, miktarlarını, teklif türünü ve geçerlilik süresini resmi dokümanla karşılaştırdım." + - text: Bu sürümün kalemlerini, miktarlarını, teklif türünü ve geçerlilik süresini resmi dokümanla karşılaştırdım. + - heading "İstekli (firma) bilgileri" [level=5] + - generic: Firma ticaret unvanı * + - textbox "Firma ticaret unvanı *": + - /placeholder: "" + - text: QA Laboratuvar Test Ltd + - generic: Tebligata esas adres * + - textbox "Tebligata esas adres *": + - /placeholder: "" + - text: Yerel test adresi + - generic: Vergi dairesi * + - textbox "Vergi dairesi *": + - /placeholder: "" + - text: Test + - generic: Vergi numarası * + - textbox "Vergi numarası *": + - /placeholder: "" + - text: "0000000000" + - generic: Ticaret sicil / MERSİS no + - textbox "Ticaret sicil / MERSİS no": + - /placeholder: "" + - generic: Yetkili ad soyad * + - textbox "Yetkili ad soyad *": + - /placeholder: "" + - text: Test Yetkilisi + - generic: Yetkilinin görevi * + - textbox "Yetkilinin görevi *": + - /placeholder: Örn. Genel Müdür + - text: Test Sorumlusu + - generic: Telefon * + - textbox "Telefon *": + - /placeholder: "" + - text: "0000000000" + - generic: E-posta * + - textbox "E-posta *": + - /placeholder: "" + - text: qa@example.test + - generic: Teklif türü + - combobox "Teklif türü": + - option "Birim fiyat" [selected] + - option "Anahtar teslim götürü bedel" + - generic: Teklif geçerlilik (gün) + - textbox "Teklif geçerlilik (gün)": "120" + - generic: Kısım (varsa) + - textbox "Kısım (varsa)": + - /placeholder: Örn. 2. kısım + - heading "Kalem fiyatlandırma" [level=5] + - table: + - rowgroup: + - row "Kalem Miktar Birim fiyat Tutar": + - columnheader "Kalem" + - columnheader "Miktar" + - columnheader "Birim fiyat" + - columnheader "Tutar" + - rowgroup: + - row "Sentetik test kalemi 2 adet 125.50 251,00": + - cell "Sentetik test kalemi" + - cell "2 adet" + - cell "125.50": + - textbox "0": "125.50" + - cell "251,00" + - region "Belge ve ürün uygunluğu": + - heading "Belge ve ürün uygunluğu" [level=5] + - paragraph: 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. + - group "Sentetik yetki belgesi (zorunlu)": + - generic: Sentetik yetki belgesi (zorunlu) + - paragraph: Yerel QA dokümanı madde 7 + - button "Elle eklenen gereksinimi kaldır" + - combobox "Kontrol durumu": + - option "Kontrol gerekli" [selected] + - option "Doğrulandı" + - text: Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde + - textbox "Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde" + - paragraph: Resmi dokümandan eksik belge gereksinimi ekle + - text: Belge adı + - textbox "Belge adı" + - text: Kaynak doküman ve madde + - textbox "Kaynak doküman ve madde" + - button "Belge gereksinimi ekle" [disabled] + - generic "Sentetik test kalemi — ürün/hizmet ve yükümlülük kontrolü" + - option "Kontrol gerekli" [selected] + - option "Uygun" + - option "Uygun değil" + - option "Kontrol gerekli" [selected] + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" + - option "Kontrol gerekli" [selected] + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" + - option "Kontrol gerekli" [selected] + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" + - option "Kontrol gerekli" [selected] + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" + - generic "6 belge/uygunluk kontrolü tamamlanmalı" + - generic: Teklif toplamı (KDV hariç) + - generic: 251,00 TL + - paragraph: "Yazı ile: ikiyüzellibir Türk Lirası" + - heading "Hazırlanacak belgeler" [level=5] + - button "Teklif Mektubu Rakam ve yazı ile bedel Yazdır / PDF" [disabled]: + - generic: Teklif Mektubu + - generic: Rakam ve yazı ile bedel + - text: Yazdır / PDF + - button "Birim Fiyat Teklif Cetveli 1 kalem Yazdır / PDF" [disabled]: + - generic: Birim Fiyat Teklif Cetveli + - generic: 1 kalem + - text: Yazdır / PDF + - button "Evrak Kontrol Listesi 1 belge Yazdır / PDF" [disabled]: + - generic: Evrak Kontrol Listesi + - generic: 1 belge + - text: Yazdır / PDF + - paragraph: Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın. + - paragraph: Belgeler taslaktır ve idarenin standart formlarının yerine geçmez. İmzalamadan önce ihale dokümanındaki form metinleriyle karşılaştırın. +- button +- button "Kullanım kılavuzu (? tuşu)": +- region "Notifications (F8)": + - list \ No newline at end of file diff --git a/docs/quality/evidence/qa-turkish-price-fixed.txt b/docs/quality/evidence/qa-turkish-price-fixed.txt new file mode 100644 index 0000000..a81475c --- /dev/null +++ b/docs/quality/evidence/qa-turkish-price-fixed.txt @@ -0,0 +1,72 @@ + [level=5] + - paragraph: Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın. + - table: + - rowgroup: + - row "Kalem Miktar Birim fiyat Tutar": + - columnheader "Kalem" + - columnheader "Miktar" + - columnheader "Birim fiyat" + - columnheader "Tutar" + - rowgroup: + - row "Sentetik test kalemi 2 adet 125,50 251,00": + - cell "Sentetik test kalemi" + - cell "2 adet" + - cell "125,50": + - textbox "0": 125,50 + - cell "251,00" + - region "Belge ve ürün uygunluğu": + - heading "Belge ve ürün uygunluğu" [level=5] + - paragraph: 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. + - group "Sentetik yetki belgesi (zorunlu)": + - generic: Sentetik yetki belgesi (zorunlu) + - paragraph: Yerel QA dokümanı madde 7 + - button "Elle eklenen gereksinimi kaldır" + - combobox "Kontrol durumu": + - option "Kontrol gerekli" + - option "Doğrulandı" [selected] + - text: Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde + - textbox "Belge adı/sürümü, geçerlilik kontrolü ve kaynak madde": QA dosyası v1 — yalnız test, madde 7 + - paragraph: Resmi dokümandan eksik belge gereksinimi ekle + - text: Belge adı + - textbox "Belge adı" + - text: Kaynak doküman ve madde + - textbox "Kaynak doküman ve madde" + - button "Belge gereksinimi ekle" [disabled] + - generic "Sentetik test kalemi — ürün/hizmet ve yükümlülük kontrolü" + - option "Kontrol gerekli" + - option "Uygun" [selected] + - option "Uygun değil" + - option "Kontrol gerekli" + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" [selected] + - option "Kontrol gerekli" + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" [selected] + - option "Kontrol gerekli" + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" [selected] + - option "Kontrol gerekli" + - option "Doğrulandı" + - option "Uygulanmıyor — gerekçe gerekli" [selected] + - generic: Teklif toplamı (KDV hariç) + - generic: 251,00 TL + - paragraph: "Yazı ile: ikiyüzellibir Türk Lirası" + - heading "Hazırlanacak belgeler" [level=5] + - button "Teklif Mektubu Rakam ve yazı ile bedel Yazdır / PDF" [disabled]: + - generic: Teklif Mektubu + - generic: Rakam ve yazı ile bedel + - text: Yazdır / PDF + - button "Birim Fiyat Teklif Cetveli 1 kalem Yazdır / PDF" [disabled]: + - generic: Birim Fiyat Teklif Cetveli + - generic: 1 kalem + - text: Yazdır / PDF + - button "Evrak Kontrol Listesi 1 belge Yazdır / PDF" [disabled]: + - generic: Evrak Kontrol Listesi + - generic: 1 belge + - text: Yazdır / PDF + - paragraph: Tutar, kaynak, belge ve ürün uygunluğu kontrollerini tamamlayın. + - paragraph: Belgeler taslaktır ve idarenin standart formlarının yerine geçmez. İmzalamadan önce ihale dokümanındaki form metinleriyle karşılaştırın. +- button +- button "Kullanım kılavuzu (? tuşu)": +- region "Notifications (F8)": + - list \ No newline at end of file diff --git a/docs/quality/uygulama-durumu.md b/docs/quality/uygulama-durumu.md index 8a1ce84..690998f 100644 --- a/docs/quality/uygulama-durumu.md +++ b/docs/quality/uygulama-durumu.md @@ -63,9 +63,17 @@ Tarayıcı kanıtı: [Yenileme sonrası beş kart](evidence/qa-kanban-after-relo - Açık kalan Kanban, sağ panel ve takvim tarih etiketleri 30 saniyede bir güncellenir. Ürün aramasındaki geçmiş tarih filtresi aynı kaynak saatini kullanır; filtre etiketi bilinmeyen saati başvuruya açık garantisi gibi sunmaz. - Üç tarih testi gün farkını, Türkiye'de yıl/gece geçişini, tam kapanış anını, eksik saat ve artık günleri doğrular. Google/Outlook gerçek içe aktarımı, zeyilname güncellemesi ve bütün sunucu bildirim zamanlayıcıları henüz bu kapsamda doğrulanmadı. +## Tarayıcı doğrulaması ve virgüllü fiyat hatası + +- Geçici normal hesapla Giresun adlı sentetik kayıtta firma alanları, 120 gün, fiyat ve manuel belge gereksinimi dolduruldu; kaydetme/yenileme sonrası geri geldi. Eksik kontroller varken çıktı düğmeleri kapalı kaldı. +- Ürün modeli, teknik kaynak, belge doğrulaması, sorumlu ve dört sağlık yükümlülüğü kontrolü kaydedilip yenilemeden sonra ekrandan doğrulandı. Resmi kaynak onayı verilmedi; gerçek başvuru veya resmi uygunluk iddiası yok. +- Tarayıcı testi yeni P1 kusuru ortaya çıkardı: `125,50` girdisi virgül silinerek `12550` oluyordu. Girdi artık değiştirilmeden ortak fiyat ayrıştırıcısına gider; nokta/virgül ondalığı desteklenir, geçersiz metin reddedilir. PDF HTML'sinde de aynı ayrıştırıcı kullanılır, birim fiyat hassasiyeti korunur. +- Düzeltme sonrasında tarayıcıda `125,50 × 2 = 251,00 TL` görüldü ve yeniden yüklemede korundu. Bir otomatik test ekran hesabı/PDF HTML tutarlılığı, geçersiz sayı ve aşırı tutarı kapsar. +- Kanıtlar: [taslak yenilemesi](evidence/qa-draft-after-reload.txt), [virgüllü fiyat](evidence/qa-turkish-price-fixed.txt), [kontrol yenilemesi](evidence/qa-checks-after-reload.txt). + ## Doğrulama -`npm test`: 30/30 geçti (son yeniden çalıştırma: 6 Eylül 2026). +`npm test`: 31/31 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. `node --check server/index.js`, `node --check server/user-store.mjs`, `git diff --check`: geçti. diff --git a/src/components/BidPackageBuilder.jsx b/src/components/BidPackageBuilder.jsx index a0f8a50..dd0127d 100644 --- a/src/components/BidPackageBuilder.jsx +++ b/src/components/BidPackageBuilder.jsx @@ -1,5 +1,5 @@ import React, { useState, useMemo } from 'react'; -import { bidRowTotal, validateBidRows, roundMoney } from '../services/bidValidation'; +import { bidRowTotal, validateBidRows, roundMoney, parseBidPrice } from '../services/bidValidation'; import { sourceRows, bidSourceSnapshot, restoreBidPrices } from '../services/bidSource'; import { PreparationChecks } from './PreparationChecks'; import { preparationBlockers, documentKey } from '../services/preparationChecks'; @@ -91,7 +91,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { else setHata('Taslak kaydedilemedi. Tekrar deneyin.'); }; - const toplam = teklifTuru === 'goturu' ? (Number(goturuBedel) || 0) : kalemToplami; + const toplam = teklifTuru === 'goturu' ? (parseBidPrice(goturuBedel) || 0) : kalemToplami; const eksikler = ALANLAR.filter((a) => a.zorunlu && !String(firma[a.k] || '').trim()); const kalemEksik = teklifTuru === 'birim' && validateBidRows(satirlar).length > 0; @@ -246,7 +246,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { setGoturuBedel(e.target.value.replace(/[^\d.]/g, ''))} + onChange={(e) => setGoturuBedel(e.target.value)} placeholder="0" className="w-full sm:w-64 text-sm font-bold px-3 py-2 rounded-lg border border-slate-300" /> @@ -254,6 +254,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { ) : kalemler.length > 0 ? (
Kalem fiyatlandırma
+

Ondalık fiyatı 125,50 veya 125.50 biçiminde yazın; binlik ayırıcı kullanmayın.

@@ -278,7 +279,7 @@ export const BidPackageBuilder = ({ results, tender: selectedTender }) => { - setFiyatlar((o) => ({ ...o, [r.priceKey]: e.target.value.replace(/[^\d.]/g, '') }))} + setFiyatlar((o) => ({ ...o, [r.priceKey]: e.target.value }))} placeholder={r.estimatedUnitPrice != null ? String(r.estimatedUnitPrice) : '0'} className="w-28 text-xs text-right px-2 py-1 rounded border border-slate-300" /> diff --git a/src/services/bidDocs.js b/src/services/bidDocs.js index ce28469..bd1d942 100644 --- a/src/services/bidDocs.js +++ b/src/services/bidDocs.js @@ -1,4 +1,4 @@ -import { quantityParts, bidRowTotal, validateBidRows } from './bidValidation.js'; +import { quantityParts, parseBidPrice, bidRowTotal, validateBidRows } from './bidValidation.js'; /** * İhale dosyası belgeleri — teklif mektubu, birim fiyat teklif cetveli ve * evrak kontrol listesi. @@ -162,7 +162,7 @@ export function teklifCetveliHtml(f, satirlar, toplam) { if (errors.length) throw new Error(errors.join(" ")); const govde = satirlar.map((r, i) => { const { miktar, birim } = miktarAyristir(r.quantity); - const bf = Number(r.birimFiyat) || 0; + const bf = parseBidPrice(r.birimFiyat); const tutar = bidRowTotal(r); return ` diff --git a/src/services/bidValidation.js b/src/services/bidValidation.js index 86b5484..d71e205 100644 --- a/src/services/bidValidation.js +++ b/src/services/bidValidation.js @@ -1,3 +1,14 @@ +// Preserve typed input; never delete a decimal separator and change its value. +export function parseBidPrice(value) { + if (typeof value === 'number') return Number.isFinite(value) && value >= 0 ? value : null; + const text = String(value ?? '').trim(); + const plain = /^\d+(?:[.,]\d{1,8})?$/; + const groupedTurkish = /^\d{1,3}(?:\.\d{3})+,\d{1,8}$/; + if (!plain.test(text) && !groupedTurkish.test(text)) return null; + const normalized = text.includes(',') ? text.replace(/\./g,'').replace(',','.') : text; + const result = Number(normalized); + return Number.isFinite(result) ? result : null; +} export function quantityParts(value) { if (typeof value === 'number') return {miktar:Number.isFinite(value) ? value : null,birim:''}; const match = /^\s*([+-]?[\d]+(?:[.,]\d+)*)\s+(.+?)\s*$/.exec(String(value || '')); @@ -11,8 +22,9 @@ export function quantityParts(value) { export const roundMoney = n => Math.round((n + Number.EPSILON) * 100) / 100; export function bidRowTotal(row) { const {miktar} = quantityParts(row.quantity); - const price = Number(row.birimFiyat); - return miktar > 0 && Number.isFinite(price) && price > 0 ? roundMoney(miktar * price) : null; + const price = parseBidPrice(row.birimFiyat); + const total = miktar * price; + return miktar > 0 && price > 0 && Number.isFinite(total) && total <= Number.MAX_SAFE_INTEGER / 100 ? roundMoney(total) : null; } export function validateBidRows(rows) { if (!rows.length) return ['Resmi teklif cetvelindeki kalemler eksik.']; diff --git a/tests/bid-validation.test.mjs b/tests/bid-validation.test.mjs index 20b6f4d..f3e25e2 100644 --- a/tests/bid-validation.test.mjs +++ b/tests/bid-validation.test.mjs @@ -1,6 +1,6 @@ import {test} from 'node:test'; import assert from 'node:assert/strict'; -import {quantityParts,validateBidRows,bidRowTotal} from '../src/services/bidValidation.js'; +import {quantityParts,validateBidRows,bidRowTotal,parseBidPrice} 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'}); @@ -22,3 +22,15 @@ test('100 rows retain Turkish names and escaped content in printable sheet',()=> assert.equal((out.match(/Ölçüm <kit>/g)||[]).length,100); assert.ok(out.includes('İğdır'));assert.ok(out.includes('2.000,00')); }); + +test('Turkish decimal price is identical in row total and printable sheet',()=>{ + const row={name:'Kit',quantity:'2 adet',birimFiyat:'125,50'}; + assert.equal(parseBidPrice(row.birimFiyat),125.5); + assert.equal(parseBidPrice('1.250,50'),1250.5); + assert.equal(bidRowTotal(row),251); + const html=teklifCetveliHtml({},[row],251); + assert.ok(html.includes('125,50'));assert.ok(html.includes('251,00')); + assert.ok(!html.includes('25.100')); + for(const value of ['', '125,,50','1e3','125 TL','1.2.3','-20','125,']) assert.equal(parseBidPrice(value),null); + assert.equal(bidRowTotal({quantity:'1000000000 adet',birimFiyat:'999999999999999'}),null); +});
${i + 1}