İçeriğe geç
Mühendislik

Türkiye’de ERP Yazarken Parayı Asla Float ile Toplamayın

JavaScript’te 0.1 + 0.2 neden 0.3 etmez ve bu bir e-fatura reddine nasıl dönüşür? ERP geliştirirken yaşadığımız üç para hatası ve çözümleri.

Emre Aydın· Kurucu, PAFTA13 Ağustos 20265 dk okuma

ERP yazmanın en utandırıcı bug’ı finansal hatadır. Kullanıcıya “500 TL faturanız gönderildi” dersiniz, GİB “tutarlar uyuşmuyor” diyerek reddeder. Bizim de başımıza geldi. Aşağıda PAFTA’yı geliştirirken gerçekten yaşanan üç para hatası ve çözümleri var.

Bug 1: Fatura toplamında ±1 kuruş

JavaScript’te float aritmetiği IEEE 754 standardını kullanır; ondalıklı sayılar ikili sistemde tam olarak ifade edilemez. Sonuç, tarayıcı konsolunda bile görülebilir:

javascript
0.1 + 0.2           // 0.30000000000000004
(0.1 + 0.2) === 0.3 // false

Fatura kalemi toplama kodu tipik olarak şöyle yazılır — ve tam burada sapar:

javascript
// YANLIŞ: her toplama adımı float hatası biriktirir
const toplam = kalemler.reduce((acc, k) => acc + k.tutar, 0);

Üç kalemin tutarı 100,10 + 200,05 + 99,85 olsun. Beklenen 400,00; gerçekte 399,99999… veya 400,00000001 çıkabilir. GİB’in UBL-TR e-fatura şeması kalem tutarlarını ve genel toplamı ayrı ayrı doğrular — 1 kuruşluk sapma reddedilmeye yeter.

Çözümümüz, para aritmetiğini tek bir merkezi sınıftan geçirmek oldu. Her işlem sonucunu sabit ondalık hassasiyete indiren özel bir yardımcı kullanıyoruz:

typescript
// src/utils/financialMath.ts (kısaltılmış)
export class FinancialMath {
  private static safe(value: number): number {
    if (!Number.isFinite(value)) return 0;
    return Math.round(value * 100) / 100;
  }

  static add(a: number, b: number): number {
    return this.safe(a + b);
  }

  static multiply(a: number, b: number): number {
    return this.safe(a * b);
  }
}

“Sonsuz olmayan sayı” kontrolünün orada durması tesadüf değil: bölme veya bozuk girdi NaN/Infinity ürettiğinde bunun sessizce fatura toplamına sızması, yanlış rakamdan daha kötüdür.

Bir uyarı: yuvarlamayı her ara adımda değil, mümkün olduğunca son adımda yapın. Yuvarlanmış bir KDV’yi tekrar başka bir çarpımın girdisi yapmak “yuvarlama üstüne yuvarlama” hatasını biriktirir. Formülü son toplamı tek seferde çözecek şekilde kurun.

Negatif tutarlarda ikinci bir tuzak var: JavaScript’in Math.round’u yarım değeri artı sonsuza doğru yuvarlar, PostgreSQL’in ROUND’u ise sıfırdan uzağa. İade ve tevkifat gibi negatif senaryolarda bu fark gerçek bir kuruş sapması üretir — veritabanı ile bire bir eşleşmesi gereken yerler için ayrı bir yuvarlama primitifi tutuyoruz.

Bug 2: Çift KDV — mobil ile web arasındaki sessiz convention farkı

PAFTA’nın mobil uygulaması teklif oluştururken web ile aynı veriyi üretmek zorunda. İkisi bir süre paralel geliştirildi. Sonra mobilden gelen tekliflerin toplamının şiştiğini fark ettik.

Sebep bir convention farkıydı: web’de satır tutarları net (KDV hariç) hesaplanır, KDV üst toplamda eklenir. Mobil tarafta her satıra ayrı KDV hesaplanmış, ardından form genelinde bir “genel KDV” alanı da eklenmişti. KDV iki kez giriyordu.

dart
// YANLIŞ (sadeleştirilmiş eski mobil kod)
final satirKdv = satirTutar * kdvOrani;   // satır bazında KDV
final genelKdv = araToplam * kdvOrani;    // bir de genel KDV
final toplam   = araToplam + satirKdv + genelKdv;  // çift sayım

Kalıcı çözüm “mobili düzeltmek” değildi; aynı hesabı iki kez yazmayı bırakmaktı. Web’deki toplam hesaplama fonksiyonunu mobile port ettik, “genel KDV” alanını kaldırdık ve iki platformun aynı girdi için aynı çıktıyı ürettiğini doğrulayan testleri önce yazdık.

Bug 3: NET mi brüt mü — convention’ı yazılı hale getirin

Teklif tablosundaki ara toplam kolonunun neyi tuttuğu başlangıçta belirsizdi. Bazı hesaplar onu iskonto öncesi, bazıları iskonto sonrası varsaydı. İkisi de “çalışıyor” göründü, ta ki iskontolu bir teklifte rakamlar ayrışana kadar.

Buradaki asıl ders, doğru cevabın ne olduğu değil: bir finansal kolonun tabanı (net mi brüt mü, iskonto öncesi mi sonrası mı) tip tanımının yanında yazılı değilse, o kolonu okuyan her yeni kod kendi varsayımını üretir. Belgelemek de tek başına yetmiyor — bilinen girdi ve beklenen çıktıyla çalışan bir “golden master” testi, varsayım kaydığında CI’ı kırıyor.

Özet

HataBelirtiÇözüm
Float aritmetiği±1 kuruş sapma, e-fatura reddiMerkezi para yardımcısı (sabit ondalık + sonlu sayı kontrolü)
Çift KDVMobil teklif toplamı şişiyorHesabı tek yerde tut, platforma port et, test-first
Net/brüt belirsizliğiİskonto yanlış tabana uygulanıyorTip tanımında convention notu + golden master testi

Ders: parayı tek bir merkezi yardımcı üzerinden geçirin, net/brüt convention’ını kolonun yanına yazın ve platformlar arası paylaşılan finansal mantığı test-first port edin. Çünkü bu sınıf bir hatayı fark ettiğinizde, kullanıcı çoktan reddedilmiş bir fatura almış olabilir.

PAFTA, teklif–sipariş–fatura zincirini tek akışta yöneten bulut tabanlı bir KOBİ ERP’sidir.

PAFTA’yı ücretsiz dene
Emre AydınKurucu, PAFTA

PAFTA’nın kurucusu. KOBİ’ler için ERP, e-fatura ve ön muhasebe süreçlerini sadeleştirme üzerine yazıyor.

LinkedIn

Sıkça Sorulan Sorular