Tutarlar — amounts & amountExponent ✓ Canlı

Satır tutarları bugüne kadar yalnız sonuç olarak yayınlanıyordu (lineTotalDecimal, ondalık); bileşenleri geri hesaplamak ve yuvarlamayı yapmak size kalıyordu. Artık bileşen kırılımı tamsayı minor unit olarak da geliyor: para birimi belgede bir kez, satırda çıplak tamsayı. Alanlar opsiyoneldir — gelmezlerse ondalık alanlar aynen sürer.

← API Uçları · Olay sırası (sequence)

Kırıcı değişiklik yok. Alanlar eklendi; hiçbir alan kaldırılmadı, hiçbir tip değişmedi. lineTotalDecimal, total, paid, payments[].amount ondalık kalmaya devam ediyor.

Belge kökü — currency + amountExponent

{ "currency": "TRY", "amountExponent": 2 }   // belge KÖKÜNDE, satış düzeyinde
AlanTipZorunluAçıklama
currencystring?–Para birimi — ISO-4217 ("TRY").
amountExponentnumber?–amounts.* tamsayılarının ondalık basamak sayısı (TRY → 2, yani kuruş). Yalnız amounts.* için.

Satır — amounts

Sipariş satırı (vurgulu alanlar yeni)
{
  "lineId": "masa-5-a1f2",
  "title": "Menemen",
  "quantity": 2,
  "options": [{ "id": "o1", "title": "Ekstra peynir", "price": 15, "vatRate": 10 }],
  "lineTotalDecimal": 129,                    // ONDALIK — değişmedi
  "vatRate": 10,
  "amounts": {                         // TAMSAYI minor unit (amountExponent: 2 → kuruş)
    "base": 10000, "options": 3000, "extra": 0, "discount": 100, "timer": 0,
    "gross": 12900, "net": 11727, "vat": 1173
  }
}
AlanTipZorunluAçıklama
basenumber–Adet × birim fiyat (seçenek, ekstra, indirim hariç).
optionsnumber–Seçili seçeneklerin toplamı, adetle çarpılmış.
extranumber–Satır düzeyi ilave.
discountnumber–Satır düzeyi indirim — pozitif değer, çıkarılır.
timernumber–Dakika-bazlı ürünlerde tahakkuk eden ücret (aşağıdaki tuzağa bak).
grossnumber–Satır toplamı — tek kaynak (lineTotalDecimal'ın minor-unit karşılığı).
net · vatnumber–Matrah ve vergi.
perVat[]array?–Yalnız karışık KDV oranlı satırda gelir — { rate, gross, vat, net } kovaları (aşağıda).

Garanti edilen invaryantlar

  • base + options + extra − discount + timer === gross
  • net + vat === gross

Eşitlikler bit-bit tutar: bileşenlerin ayrı yuvarlanmasından doğan artık, mutlak değerce en büyük bileşene emilir. Kendi tarafında yeniden yuvarlama yapmana gerek yok.

⚠️ Kırıcı — satır ondalıkları *Decimal sonekine geçti

Satır düzeyinde aynı ad iki farklı para ölçeğinde yaşıyordu ve hangisinin hangi ölçekte olduğunu söyleyen hiçbir alan yoktu:

Yeniden adlandırma
// ESKİ — aynı nesnede, aynı adlarla:
{ "discount": 12.6,  "extra": 5,   "lineTotal": 187.4,
  "amounts": { "discount": 1260, "extra": 500, "gross": 18740 } }

// YENİ:
{ "discountDecimal": 12.6, "extraDecimal": 5, "lineTotalDecimal": 187.4,
  "amounts": { "discount": 1260, "extra": 500, "gross": 18740 } }
Zarf kökündeki amountExponent: 2 beyanı bu ondalık alanlara uygulanmaz (kökteki total da ondalıktır). Üssü görüp satırdaki discount'ı 100'e bölen bir entegratör 12,60 ₺ yerine 0,126 ₺ yazardı — ve mali tüketici satırı imzaladığı için sapma geri alınamaz hâle gelirdi. Sonek bu tuzağı adın kendisinde görünür kılar.
  • Eski adlar kaldırıldı; amounts minor unit'in tek otoritesidir.
  • Kapsam: orders[], cancels[], lineChanges[].before/after ve customer.order_added satırlarının hepsi — okuma uçları (tables/get, packets/get) dahil.
  • Kapsam dışı: kök total/paid/totalDiscount ve payments[].amount ondalık kalıyor — onların minor unit karşılığı yok, dolayısıyla ad çakışması da yok.
  • ⚠️ Yazma tarafı (cart[]) değişmedi: istekte hâlâ discount gönderirsin.
Sektör: Stripe çift gösterimi adla ayırır (unit_amount / unit_amount_decimal), Square her tutarı Money{amount,currency} nesnesine sarar, Shopify MoneyV2 ondalık string kullanır — hiçbiri aynı adı iki ölçekte kullanmaz. (⏳ Sandbox'ta canlı, production'a dağıtılmadı.)

Tuzak 1 — amountExponent yalnız amounts.* için

Aynı payload'daki lineTotalDecimal, total, paid, extraDecimal, discountDecimal, options[].price, payments[].amount ondalıktır ve değişmemiştir. Üssü onlara uygularsan 100 kat sapma alırsın.

Özet uçlarında (tables/open · packets/open) minor-unit alan olmadığı için amountExponent hiç gönderilmez — yalnız currency gelir. Bu bilinçli bir asimetridir.

Tuzak 2 — timer satırı zamanla BÜYÜR

Dakika-bazlı ürünlerde (oyun salonu, saatlik masa) timer okuma anında hesaplanır: hiçbir durum değişikliği olmadan iki olay arasında gross artar. "Değer düştü → storno" mantığı kuran bir tüketici bu satırları ayrı ele almalıdır. timer tam da bunu ayırt edebilmen için ayrı yayınlanıyor.

perVat — karışık oranlı satır

Satırdaki bir seçenek ürünün oranından farklı bir KDV oranı taşıyorsa (Almanya 2026: yemek %7 / içecek %19 → "burger + kola" menüsü) kova kırılımı eklenir.

"amounts": {
  // …
  "gross": 17000, "vat": 1583, "net": 15417,
  "perVat": [
    { "rate": 19, "gross": 5000,  "vat": 798, "net": 4202 },
    { "rate": 7,  "gross": 12000, "vat": 785, "net": 11215 }
  ]
}
  • Yalnız karışık oranlı satırda gelir; tek oranlı satırda alan yoktur.
  • Σ perVat.gross === amounts.gross ve her kovada net + vat === gross garanti.
  • Yüksek oran önce sıralanır (DSFinV-K okunabilirliği).
  • Kırılım varken üst düzey vat/net kırılımdan türetilir — aksi hâlde aynı satır için iki farklı KDV rakamı olurdu.
  • fiscal.de ile imzalanan fişin matrah kırılımı (amountsPerVatRate) aynı dağıtımdan üretilir → yayınlanan veri ile imzalanan belge çelişmez.

Dağıtım politikası

Her seçeneğin brütü kendi dondurulmuş oranına; ürün tabanı ve timer ürünün oranına; satır düzeyi extraDecimal ve discountDecimal kovalara ciro payıyla orantılı dağıtılır (Stripe discount_amounts[] / Shopify discount_allocations[] deseni).

options[].vatRate

Seçeneğin oranı yazım anında dondurulur — menü sonradan değişse bile geçmiş satış sabit kalır.

vatRate: null ne demek? Dondurulmamış demektir: o seçenek satırın vatRate'ini kullanır ve karışık oran iddia edilmez. Eski satırlarda ve entegrasyon kanallarının açtığı paketlerde null gelir — oran dondurma yalnız panel addOrder ve eklenti sepet uçlarında yapılır.

Nerede karşılaşırsın

Aynı payload şekli hem webhook'ta hem okuma uçlarında kullanılır — tek kaynak.

YüzeycurrencyamountExponentamounts
table.* / packet.* webhook'larıvarvarvar
packets/getvarvarvar
tables/getvarvarvar
packets/open (özet)varyokyok
tables/open (özet)varyokyok
Blocking hook includeData (masa + paket)varvarvar
customer.order_addedvarvarvar

sequence / sequenceScope ise zarf alanlarıdır — yalnız webhook teslimlerinde bulunur (olay sırası).

Alanların hiç gelmediği durum

Tenant'ın para birimi çözülemezse currency / amountExponent / amounts hiç konmaz — bugünkü ondalık alanlar aynen sürer. Uydurma üsle tutar yayınlamak 100 kat sapma demektir, bu yüzden sessizce düşürülür. Satırın verisi bozuksa yalnız o satırda amounts düşer; diğer satırlar etkilenmez.

Dürüst sınırlar

  • Satır toplamı negatifse kovalar da negatif olur — işaret varsayımı yapma.
  • Oran dondurma yalnız panel addOrder ve eklenti sepet uçlarında yapılır; entegrasyon kanallarında yapılmaz.
  • Üssü 3 olan para birimlerinde (OMR, IQD…) KDV hesabı 2 ondalıkta yapıldığı için kova vergisi 10'un katına yuvarlanır.

SDK ile okuma

@restomenum/plugin-sdk
import { minorToDecimal, sumLineAmounts, vatBreakdown } from '@restomenum/plugin-sdk';

const { currency, amountExponent, orders, total } = data;   // "TRY", 2

const totals = sumLineAmounts(orders);        // null → BİR satır bile amounts taşımıyor
if (totals && amountExponent !== undefined) {
  minorToDecimal(totals.gross, amountExponent);   // 129   ← gösterim için ondalığa çevir
  minorToDecimal(totals.vat, amountExponent);     // 11.73
} else {
  // geri düşüş: ondalık alanlar (total / lineTotalDecimal) — 0 VARSAYMA
  console.log(total);
}

// Mali kırılım: karışık oranlı satırda perVat kovaları, tek oranlı satırda satırın vatRate'i.
// null → bir satırın amounts'ı ya da oranı yok: oran TAHMİN EDİLMEZ.
vatBreakdown(orders);   // [{ rate: 19, gross, vat, net }, { rate: 7, … }]  — yüksek oran önce
  • minorToDecimal(minor, exponent) — gösterim için ondalığa çevirir (hesabı tamsayıda yap).
  • sumLineAmounts(lines) — brüt/matrah/vergi toplamı; bir satır bile amounts taşımıyorsa null.
  • vatBreakdown(lines) — KDV kovaları (yüksek oran önce); oran bilinmiyorsa null.
  • parseLineAmounts · lineAmountsBalanced — açık eşleyici + invaryant kontrolü (savunma katmanı).

En iyi pratikler

  • Para hesabını tamsayı üzerinde yap, ondalığa yalnız gösterim için çevir — kayan nokta artığı mali kayıtta kuruş kaybına döner.
  • Alan yokluğuna dayanıklı ol: amounts yoksa ondalık alanlara düş, 0 varsayma. Kod yolunu ikiye ayırma — tek bir "tutar okuyucu" yaz.
  • Karışık oranı destekleyeceksen perVat'ı kullan ve üst düzey vat/net'i ondan türetilmiş kabul et.
  • Invaryantı savunma amaçlı doğrula (lineAmountsBalanced) ama kendi hesabını ona dayandırma — beklenmeyen gövdeyi logla, satışı durdurma.