Değişiklik Günlüğü

Geliştiriciyi etkileyen değişiklikler (yeni uçlar, hook'lar, scope'lar, yetenekler). Yalnızca canlıya alınmış/doğrulanmış özellikler listelenir; planlananlar 'Yakında' altında.

RSS: kırıcı olabilecek değişiklikleri otomatik takip et — /changelog.xml (RSS 2.0; bot/izleme aracına ekle). AI ajanları için /llms.txt.

2026-09-04 — Geri alma ÜÇ yoldan gelir + sessiz abonelik düşüşü + sıra boşluğu uzlaştırması

  • ⚠️ Yeni alan yok — eksik anlatılmış davranışlar var. Bir saha vakasından çıktı ve büyük olasılıkla tek kurban o eklenti değildi. Mali/muhasebe eklentisi yazıyorsan dördünü de oku.
  • "Kapanmış satış geri alındı" TEK bir olay değildir — ÜÇ yoldan gelir. table.reopened (kapanmış masa geri açıldı) · packet.reopened (kapanmış entegrasyon satışı geri açıldı) · table.closed_deleted/packet.closed_deleted (kapanmış fiş silindi — satış geri gelmez, kestiğin belgenin dayanağı ortadan kalkar). Yalnız table.reopened dinleyen bir eklenti diğer ikisini hiç görmez ve kestiği belge ayakta kalır → karşılaştırma tablosu.
  • Mali tepki dayatılmıyor. Storno, ters kayıt ya da hiçbir şey — kendi kaydını nasıl kurduğuna ve mevzuat yorumuna bağlı. Zorunlu olan tek şey üç yolu da ele almak.
  • Koşullu emit: satışın entegrasyon alanı doluysa geri açma masa kanalından bildirilmez — table.reopened yayılmaz; packets/{id} yeniden yaratılır ve onCreate trigger'ı packet.reopened yayar. Teslim platformu siparişlerinde geri açmayı görmek için packet.reopened aboneliği şarttır.
  • Abone olmadığın tip SESSİZCE düşer. Sana hiç yayınlanmaz; ne teslim log'una satır düşer, ne hata/uyarı alırsın. Teslim edilmeyen bir olay ile hiç abone olmadığın bir olay dışarıdan AYNI görünür. Ayrım tip bazındadır: table.closed akarken table.reopened hiç gelmiyor olabilir → "webhook çalışıyor" gözlemi o tipe abone olduğunu kanıtlamaz. "Event gelmiyor" dediğinde ilk bakacağın yer manifest'inin events[] listesi.
  • ⚠️ Sözleşme düzeltmesi — sequence: eskiden "numara atlaması olay kaybı değildir" diyorduk. Teknik olarak doğruydu ve tam bu yüzden tehlikeliydi: okuyucuyu boşluğu masum saymaya götürüyordu. Doğrusu: tek başına boşluk kaybın KANITI değildir, ama kaybı DIŞLAMAZ da. Bir olayın sana ulaşmaması platform tarafında da sessiz olabilir.
  • Sıra boşluğu = en ucuz kayıp dedektörü. Beklenmedik büyüklükte bir boşluk gördüğünde hata sayma, uzlaştırma sinyali say: satışın güncel hâlini packets/get / tables/get ile çek, kendi kaydınla karşılaştır. Okuma ucu her zaman gerçeği söyler; webhook yalnız bir bildirimdir → Olay sırası.
  • ⚠️ Ama boşluğu tek başına alarma bağlama. Sayaç her satış-dokümanı yazımında ilerliyor → meşru boşluk sık oluşur. Boşluk başına alarm kuran bir eklentide uyarı gürültüye döner ve gerçek kayıp da onunla birlikte görmezden gelinir. Boşluk bir tarama tetikleyicisidir, kanıt değil.
  • ✅ Kesin dedektör: aynı satış hattında İKİNCİ bir *.closed. Bir satış hattı normalde bir kez kapanır → aynı sequenceScope için ikinci bir kapanış aldıysan ve arada geri alma işlemediysen, o geri alma sana ulaşmamıştır. Belirsiz değil, kesin çıkarım; yanlış alarm üretmez ve defterinde zaten var olan veriyle uygulanır. Sahada beş kayıp geri almanın beşini de ayırt eden sinyal buydu.
  • Saha vakası (neden önemli): bir mali eklenti dokuz gün boyunca 19 geri açmanın 19'unu aldı, ertesi gün 5'inin 5'ini hiç almadı. Ne hata, ne uyarı, ne teslim log'u kaydı vardı. Fark eden tek şey kendi defterlerindeki tutarsızlıktı — ve gün sonu raporu kapandığı için düzeltilemedi.

2026-09-04 — Ödeme EKLEME modu + iki sözleşme düzeltmesi (yazma zarfı, packets/create alan adı)

  • 🧪 YALNIZ SANDBOX — production'da YOK ve prod'a alınması PLANLANMIYOR (ürün kararı). Bu geçici bir bekleyiş değil, kalıcı durum. Prod'da mode göndermek isteği komple 400'e düşürür ("mode" is not allowed) — eski davranışa düşmez. Production entegrasyonunda bu alanı gönderme; durum değişirse burada duyurulur. Sandbox'ta gerçek HTTP çağrılarıyla uçtan uca doğrulandı — paket ucu 18/18, masa ucu sandbox.plugins.restomenum.app hostname'i üzerinden 9/9; doküman sayfalarındaki yanıt gövdeleri o testlerden yakalanmış gerçek gövdelerdir.
  • Ekleme modu — mode: "replace" | "append" (packets · tables). Varsayılan "replace" → mode göndermeyen mevcut çağıranın davranışı bit-bit aynı; kırıcı değişiklik yok, sürüm artışı gerekmiyor.
  • Neden var: POS terminali eklentileri ödeme satırını bu uçtan yazıyor ve full-replace onlar için bir tuzak: "kendi satırımı gönderdim" diyen en doğal geliştirici davranışı, aynı hesaptaki nakit tahsilatı, ikinci kartı ve indirim satırlarını siliyor ve paid düşüyordu. Ekleme modunda satırlar mevcutların üzerine eklenir, mevcutlara dokunulmaz.
  • lineId ekleme modunda ZORUNLU — keyfi değil, tekrar gönderim kalkanı. Kimlik göndermeyen satıra platform her çağrıda farklı bir kimlik üretir → ağ hatasındaki retry ikinci satır olarak eklenirdi = çift tahsilat. Kimlik güçlü olmalı (≥8 karakter, saf rakam değil); grandfather affı burada işlemez, çünkü af yalnız dokümanda zaten var olan kimliği bağışlar. Kalıp: pay-<uuid>.
  • Yanıtta yeni alan: replayed. true → aynı lineId ile tekrar gönderim algılandı, yazma yapılmadı, doküman değişmedi ve hiçbir event yayılmadı. Retry sonrası "ödemem zaten kayıtlı" demektir.
  • Yeni hata kodları (400): append_requires_line_id · append_requires_payment_line · append_partial_duplicate · append_replay_amount_mismatch · append_replay_method_mismatch · too_many_payment_lines. append_partial_duplicate bilerek serttir: satırların bir kısmı yazılı bir kısmı yeniyken sessizce yalnız yenileri eklemek, çağıranın hangi satırının geçtiğini bilmediği bir durumda çift tahsilat üretirdi → istek toptan reddedilir, tüm isteği aynı lineId kümesiyle tekrar gönder.
  • İki tavan: istek başına ≤20 satır (şema, her iki modda — replace bu yüzden dokümanı fiilen 20'ye indirir) + ekleme modunda birleşim ≤100. ⚠️ Çekirdek/personel akışları bu uçlardan geçmez ve dokümanı 20'nin üstüne çıkarabilir → böyle bir hesaba replace çağırmak fazlasını siler.
  • Webhook sınıflandırması değişti. Ekleme modu *.updated olayını changed:"payment_added" ile yayar, "payment_updated" ile değil — ikincisi "ödeme satırları tam değiştirmeylegüncellendi" demektir ve ekleme modunda onu yaymak aboneye "tüm satırlar değişti, yeniden diff'le" dedirtirdi. replace modu eskisi gibi payment_updated yayar.
  • ⚠️ DÜZELTME — yazma zarfı: "düz { ok:true } gövde" iddiası YANLIŞTI. Doküman ve SDK bir dönem "yazma uçları okuma zarfını kullanmaz, data yoktur" diyordu. Doğrusu: yazma uçları da { success:true, data:{…} } döner; ok platformun modül-içi dönüş alanıdır ve HTTP sınırına hiç çıkmaz. Yedi paket/masa yazma ucunun data şekli artık Genel Bakış'ta teyitli olarak listeli.
  • ⚠️ DÜZELTME — packets/create alan adı packetId, uuid DEĞİL. Doküman ve SDK tersini söylüyordu. Platformun iç modelinde kimlik uuid diye taşınır ama dış sözleşmeye packetId adıyla çıkar → result.uuid okuyan kod her zaman undefined alır ve create → update-payments zincirinde kimliği bulamaz. SDK PacketCreateResult tipi düzeltildi.
  • docNoDate ≠ businessDate. Biri numaranın hangi seriden/günden kesildiği, diğeri satışın ait olduğu iş günü (ikisi de YYYY-MM-DD, null olabilir). Çoğu gün aynı görünürler ama gün sınırında ve geri-açmada ayrışırlar — karıştıran entegrasyon raporlamayı yanlış güne yazar. ⚠️ businessDate create yanıtında dolu gelir ama pakete yazılmaz → packets/get ile geri okunamaz; ihtiyacın varsa yanıttan kendi kaydına sakla.
  • Hata gövdesinde status. Hata zarfı { success:false, status, message } — status HTTP kodunun gövde içindeki kopyasıdır (kuyruklanan/loglanan gövdelerde kod kaybolmasın diye).

2026-08-23 — Teslimat ödemeleri artık NAKİT görünüyor — cashDeclared + hook locale

  • ⚠️ Önce bunu oku — alan eklemesi değil, DEĞER değişimi. Teslimat entegrasyonları (Getir/Yemeksepeti/Trendyol/Migros) artık ürettikleri sözde ödeme yöntemlerine nakitlik beyanı yazıyor — sağlayıcının yapısal enum alanından, başlıktan değil. Somut sonuç: "Yemeksepeti-Kapıda Nakit" dün cash:false görünüyordu, bugün cash:true.
  • Mali eklentiler için kozmetik değil. Platform CASH/NON_CASH ayrımını ödeme satırları + cash bayrağından türetiyor → imzalanan fişteki ZAHLART_TYP değişiyor: dün NON_CASH damgalanan teslimat ödemeleri bugün CASH damgalanacak. Almanya'da bu yasal bir beyan alanıdır; geçmiş kayıtlarla karşılaştırma yapıyorsanız kırılma noktasını bu tarihe koyun.
  • Yeni alan: cashDeclared — hem payment-methods/list kataloğunda hem ödeme satırında. cash bir beyan mı, platformun güvenli varsayımı mı? false → yöntem kaydında nakitlik alanı hiç doğmamış → varsayıma dayalı sınıflandırmayı damgalama, kiracıdan beyan iste.
  • Neden satırda da var: join çalışmıyor. Ölçümde ödeme satırlarının %39'u kiracı kataloğunda karşılığı olmayan bir methodId taşıyor (teslimat entegrasyonlarının sözde-yöntemleri) → o satırlarda join edecek hedef yoktur, nakitliği satırdan oku.
  • Yeni alan: locale — blocking hook gövdesinin üst seviyesinde (data'nın içinde değil). decision:"deny" döndüğünde message'ınız kasiyerin ekranında görünüyor; o metni bu dile göre yerelleştirin. ⚠️ Opsiyonel, fallback zorunlu — tanınmayan dilde alan hiç gelmez. Ayrıntı: hook sözleşmesi.
  • Doküman düzeltmesi: payment_method.* webhook'unun data'sı katalogla “birebir aynı şekil” değil — description serbest metin olduğu için PII taramasına takılıyor ve müşteri-PII rızası olmayan kurulumlarda gelmiyor.
  • ⏳ Üçü de dev'de yayında, production'a çıkmadı.

2026-08-19 — origin zarftan KALDIRILDI — mali atıf yönlendirmesi tersine döndü; fiscal.de satır listesi

  • ⚠️ Bu kayıt "echo kuralı tersine döndü…" kaydının (3) maddesini GERİ ALIR. İki gün önce origin.deviceId'yi "doğrulanmış cihaz kimliği — mali atıfta bunu tercih et" diye duyurmuştuk. O yönlendirme geçersizdir ve alan zarftan kaldırıldı: platform origin'i göndermiyor.
  • Neden kritik: gelmeyen bir alandan mali atıf kurmak sessiz undefined değil, yanlış kasa kimliği demektir — Almanya'da Z_KASSE_ID DSFinV-K kaydında yasal bir beyandır. Alan yayındayken de zayıf temeldi: deviceId bir hesap kimliğiydi (terminalId değil) ve aynı ortak hesap birden çok tablette aynı değeri taşıyabiliyordu.
  • Doğrusu: kim → actor.userId (⚠️ doğrulanmaz, denetim izi olarak yaz); hangi kasa/terminal → senin beyanın (fiscal.de → registerId). Personel/cihaz → kasa eşlemesi eklentinin kendi ayarlarındadır. SDK 3.2.0'da Origin ve WebhookEnvelope.origin @deprecated; eşleyici ve passthrough yerinde bırakıldı — alan bir şekilde gelirse yine de düşmez.
  • ⏳ fiscal.de payload'ına satır listesi: lines = satışın sipariş satırları, şekli kapanış gövdesindeki orders[] ile birebir aynı (DSFinV-K satır ayrıntısı). ⚠️ Ya tam ya hiç — kırpılmaz: satış 200 satırı aşarsa liste hiç konmaz, yerine linesOmitted: "too_many_lines" gelir. Alanın yokluğu "satır yok" değildir → boş liste sanıp eksik fiş imzalama; ayrıntı gerekiyorsa tables/get ile çek.
  • docNo satış ömrü boyunca sabit değil: belge numarası her kesilen belgede sayaçtan alınır → geri açılan satış yeniden kapanınca yeni numara gelir. Satış kimliği (uuid) ömür boyu sabittir. Satışı anahtarlamak için uuid kullan; mali tekleme anahtarında docNo + iş günü birlikte kullanılmaya devam eder.
  • reopenedFrom kapanış gövdesinde de tipli: gate gövdesine reopenedFrom ({ saleId, docNo?, closedAt? } — *.reopened ile aynı şekil) eklendi. ⚠️ Garanti edilmez, savunmacı oku: varsa yerine geçen fişe atıf bas ("… nolu belge yerine"), yoksa hiçbir şey değiştirme — yokluğunu "geri açılma yok" sayma. Kesin zincir *.reopened olayındadır.

2026-08-19 — Dört katalog deltası — kısmi devir artık storno DEĞİL, entegrasyon silme olay yayınlıyor

  • ⏳ Dördü de sandbox'ta canlı, production'a dağıtılmadı — ve dördü de gerçekten yayınlanmış zarflarla doğrulandı. Kırıcı yok, yeni event/scope yok.
  • D1 · Entegrasyon kanalı kalem silme artık olay yayınlıyor: integration/removeOrder dokümanı yazıyordu ama hiçbir olay çıkarmıyordu; artık panelin iptal ucuyla aynı desende packet.updated yayınlıyor. Silinen satır cancels[]'a taşındığı için op: "cancelled"'dır, removed değil — kapsam listesine eklendi.
  • D2 · Kısmi devrin iki alanı katalogda yoktu: kısmi taşımada kaynak satır satışta kalır (adedi düşer) ve zarf op:"updated" girdisine movedTo + movedQuantity koyar. ⚠️ movedQuantity yalnız düşüş tek anlamlıyken konur: aynı yazımda satıra iptal kaydı da düştüyse adet ayrıştırılamaz → movedTo gelir ama movedQuantity konmaz.
  • D3 · Kuver satırının başlığı artık garanti: title her akışta dolu; sıra tenant ayarı (kuverText) → satıştaki mevcut kuver başlığı → "kuver". Önce alan istekte yoksa undefined olup dokümandan düşüyordu — aynı tenant'ın bir fişinde işletme adı, diğerinde ham kimlik görünüyordu (DSFinV-K ARTIKELTEXT tutarsızlığı). Kuver yalnız masa satışında oluşur.
  • D4 · Storno kuralı tek satır değil: op === 'removed' && removeReason !== 'moved' kısmi devri kaçırır (orada removed hiç gelmez) — sahada tam olarak bu yaşandı ("4 adet iptal edildi"). Doğrusu: adet düşüşü her zaman iptal değildir; updated + movedTo varsa düşüşün movedQuantity kadarı devirdir. SDK'da LineChangeUpdated iki alanı da tanıyor ve yeni shouldReverse(change) yardımcısı dört dalı tek yerde kapatıyor.

2026-08-18 — sequence/sequenceScope üç yolda — kapanış hook'u ve fiscal.de payload'ı da taşıyor

  • Aynı çift artık üç gövdede: webhook zarfı (canlı) · table.close/packet.close gate gövdesi (⏳) · capability:fiscal.de payload'ı (⏳). Üçü de aynı sözleşme; kırıcı değil, ek okuma maliyeti yok.
  • İki garanti yazıya geçti: sequenceScope yeniden kullanılmaz (yalnız kalıcı yazımda üretilir, değeri rastgele) · sequence scope içinde monotondur (yalnız ileri; atlanan numara olabilir, tekrar eden/azalan olmaz). ⚠️ Karşılaştırma daima aynı scope içinde — farklı scope'lar arasında sıra ilişkisi tanımsızdır; hat yeniden başlarsa yeni scope üretilir ve sayaç 1'den başlar.
  • İkisi de opsiyonel: sayaç çözülemezse alanlar hiç konmaz (null da gelmez) → o olay için karşılaştırmayı atla; sequence: 0 varsayma.
  • Hook ile zarfın numarası farklı olabilir: kapanış hook'u kapanıştan önce çalışır, kapanışın kendisi sayacı ilerletir → zarftaki numara daha büyük olabilir (ikisi aynı scope'ta).
  • Kapsam sınırı — fiscal.de: bugün yalnız sale.type: "packet" kabul ediliyor; "table" ayrı bir hata koduyla reddediliyor → masa satışlarında capability yolu yok, mali blok kapanış gate'i üzerinden taşınır.
  • lineChanges yayın kuralı netleşti: blok changed tipine değil gerçek satır deltasına bağlı; delta boşsa alan hiç konmaz ve ödeme değişimleri delta üretmez.

2026-08-17 — packet.close_aborted eklendi — kapanış iptali artık iki kanalda da var

  • Canlı — masa ve paket kanalında yayında. packet.close_aborted yayınlanmaya başladı — table.close_aborted ile birebir aynı sözleşme, ikisi de artık canlı.
  • Aynı olan her şey: gerekçe (gate onayından sonra düşen kapanış), abortReason sözlüğü (underpaid · overpaid · payment_without_order · total_repaired — dördü de kestiğin fişi storno et), "yalnız gate gerçekten çalıştıysa yayınlanır" koşulu ve sıra kuralı: iptal kapanışın tersi değildir, hat sürer (sequence artmaz) → defterini kapatma.
  • Tek fark: gövde paket şeklidir (packet.updated ile aynı kanonik şekil + abortReason).
  • Önceki bültendeki "paket karşılığı henüz yok" sınırı kalktı → ikisine birden abone ol; tek handler ikisini de karşılar (örnek eklentide ortak karar mantığı lib/closeAborted.mjs'te). SDK tipi: PacketCloseAbortedPayload.

2026-08-17 — ⚠️ Bülten güncellendi: 11 değişiklik, 3 kırıcı — *Decimal adları · packet.status_changed · retry penceresi

  • Bülten güncellendi: 8 değişiklik / 2 kırıcı → 11 değişiklik / 3 kırıcı. Üçü de ⏳ sandbox'ta canlı, production'a dağıtılmadı.
  • ⚠️ Yeni kırıcı — satır ondalıkları *Decimal sonekine geçti: discount → discountDecimal, extra → extraDecimal, lineTotal → lineTotalDecimal. Satır düzeyinde aynı ad iki farklı para ölçeğinde yaşıyordu ve kökteki amountExponent: 2 bu ondalıklara uygulanmıyor → üssü görüp satırdaki discount'ı 100'e bölen entegratör 12,60 ₺ yerine 0,126 ₺ yazardı; mali tüketici satırı imzaladığı için sapma geri alınamazdı. Eski adlar kaldırıldı; amounts minor unit'in tek otoritesi. Kök total/paid/totalDiscount, payments[].amount ve yazma tarafı cart[] değişmedi.
  • Yeni olay packet.status_changed: teslimat statüsü geçişi artık ayrı tip — gövde packet.updated ile aynı + status / previousStatus (bilinmiyorsa null, uydurulmaz). ⚠️ Zarfta actor bulunmaz (Firestore trigger yayınlar) → isOwnEcho daima false. Statü değişmeyen yazımlarda olay çıkmaz.
  • Retry penceresi 31 sn → ~11,4 saat: 6 deneme · 1 sn backoff (1+2+4+8+16 = 31 sn) bir deploy'u ya da kısa kesintiyi kaldıramıyor, olayı kalıcı olarak düşürüyordu. Artık 20 deneme · 10 sn → 3600 sn (+ 24 saatlik sert tavan). ⚠️ Idempotency artık daha kritik: aynı olay 20 kez gelebilir ve aralarında saatler geçebilir → zarf id'si ile dedup zorunlu. (Şu an yalnız sandbox; production kuyruğu eski ayarda.)
  • Doğrulama durumu: bölme, birleştirme, eklenti tam-değiştirme, tahsilat iptali, satış kimliği ve statü geçişi gerçek teslim edilmiş zarflarda ölçüldü; cari hesaba devir ölçülmedi (kod bağlı ve kaynak taramasıyla çivili, ancak gerçek tetikle denenmedi).

2026-08-17 — SDK 3.1.0 — satışın kimliği uuid: accountKey() artık uuid döndürüyor

  • Saha raporu: tableId ile anahtarlayan bir mali eklentide aynı masanın kayıtları üç ayrı anahtara dağıldı — siparişler table:masa-904'te, fiş table:<uuid>'de. Aynı satışın sipariş kayıtlarıyla fişinin bağı koptu (KassenSichV/DSFinV-K açısından ihlal).
  • Sebep: tableId/packetId bir adrestir, kimlik değil — tek satış içinde biçim değiştirir: masa-904 → geri açmada masa-904* → kapanışta kapanış kaydının kimliği. uuid ise açılış · düzenleme · kapanış · geri açma boyunca sabittir (ölçüm: 20 masa — her biri iki fazlı — + 13 paket, hepsi 1:1 tek anahtarda).
  • ⚠️ Davranış değişikliği — accountKey(data) artık uuid döndürüyor (uuid → saleId → eski dokümanlarda tableId/packetId). Caret bağımlılıkla (^3.0.0) bu sürüm otomatik gelir → store anahtarların değişir: kayıtlarını uuid'ye taşı ya da eski anahtarı kendi tarafında data.tableId ?? data.packetId ile üret.
  • Tip eklemeleri: Table/Packet olay yükü tiplerinde uuid artık ilan ediliyor (alan zaten gönderiliyordu, sözleşmede görünmüyordu) · cancelPayments[] eklendi · tableId yorumu "adres/slug — kimlik değil" olarak düzeltildi.
  • Kapandı: 3.0.1'deki zarf geçirgenliği düzeltmesi rapor sahibince kodda teyit edildi — actor, origin, sequence, sequenceScope tüketiciye ulaşıyor.
  • ⚠️ Açık çelişki: 17.08.2026 sandbox ölçümünde table.closed hâlâ kapanış kaydının kimliğini taşıyordu — "artık masa slug'ı" düzeltmesi dev'de yayında ama sandbox'ta gözlenmedi. Hangi davranışta olursan ol uuid doğru anahtardır.

2026-08-17 — ⚠️ Üç kırıcı değişiklik — lineId ayırt ediciliği · table.closed kimliği · sayı alanları

  • ⏳ Üçü de dev/sandbox'ta yayında, production'a çıkmadı ve gerçek tetikle doğrulanmadı. İkisi kırıcı.
  • ⚠️ Kırıcı 1 — lineId artık ayırt edici olmak zorunda: canlıda bir eklenti satır kimliği olarak "1" yazdı. Sözleşme benzersizliği yalnız doküman içinde garanti eder — ama mali tüketici için lineId, "bu satır fişlendi mi?" sorusunun tek yanıtıdır: iki satış aynı kimliği taşıyınca ikisi tek satır sayılır ve ikincisi fişsiz kalır. Uzunluk 8–64 (yeni alt sınır), saf rakam reddedilir ("1", "00000001" → 400); biçim ve rezerve kimlikler (kuver, new) değişmedi. "00000001" sekiz karakterdir ama iki bağımsız sayaç kaçınılmaz olarak aynı değerleri üretir — uzunluk tek başına yetmiyor.
  • Grandfather — mevcut satırların kilitlenmez: biçim geçersizse her zaman 400; biçim geçerli ama zayıfsa (<8 ya da saf rakam) kimlik dokümanda zaten varsa kabul, yoksa 400. Kontrol transaction içinde — ek okuma maliyeti yok. ⚠️ packets/create / tables/create uçlarında grandfather yok. Platformun kendi üretimi de kırılmıştı: 1196 satırın 409'u (%34) alt sınırın altındaydı (b1-1829) — üretim artık prefix'i telafi ediyor (b1-1d049). Öneri: anahtarını (uuid, lineId) çiftine çevir.
  • ⚠️ Kırıcı 2 — table.closed artık masa slug'ı döndürüyor: aynı satışın akışında tableId iki farklı kavram taşıyordu (table.updated'da masa-901, kapanışta closedTables doküman kimliği). Artık üçü de aynı: table.updated masa-901 · table.closed masa-901 · table.reopened masa-901*. Kapanış kaydına tableId ile ulaşıyorsan kırılırsın → uuid kullan (closedTables/{uuid}); reopenedFrom.saleId ve closed_deleted saleId de artık kapanış olayının uuid'siyle eşleşir. uuid'si olmayan eski satışlarda bu bağ tamamen kapanır (bilinçli kabul). ℹ️ Paket kanalı değişmedi — packet.closed aynen eskisi gibi.
  • Davranış değişikliği — satır sayı alanları her zaman sayı: quantity, extra, discount dokümandan ham geçiyordu ve alan eksikse payload'dan sessizce düşüyordu. Artık: "10"/"10,50" ayrıştırılır → 10/10.5 (virgül ondalık ayırıcı); boş/çözülemeyen ve eksik alan → extra/discount 0, quantity 1; açık 0 korunur. quantity 1'e düşer çünkü satırın lineTotalDecimal'ı eksik adedi 1 kabul ederek hesaplanır (0 deseydik gövde kendini yalanlardı). Yuvarlama yok — quantity para değildir. Number("") → NaN dalı artık hiç doğmuyor; 4 üretim tenant'ı · 1196 satır taramasında tamamı zaten sayıydı → veri düzeltmesi değil, sözleşme garantisi.

2026-08-17 — ⚠️ 8 sözleşme değişikliği — 2 kırıcı (moved_out kaldırıldı · PII scope ayrımı)

  • ⏳ Sekizi de sandbox'ta yayında, production'a dağıtılmadı. İkisi kırıcıdır — kod değişikliği gerektirir.
  • ⚠️ Kırıcı 1 — moved_out kaldırıldı: yerine removed + removeReason ('moved' | 'deleted'). Eski ad iki farklı olguyu aynı kelimeyle söylüyordu: hedefi belli devir (storno etme — hedef zinciri sürdürüyor) ile izsiz silme (storno et). Tüketici kuralı tek satır: if (op === 'removed' && removeReason !== 'moved') storno(). removeReason çağıranın beyanı değil, movedTo'dan türetilir → hedefsiz bir 'moved' yayınlanması yapısal olarak imkânsızdır.
  • ⚠️ Kırıcı 2 — PII scope'ları veri sınıfına ayrıldı: müşteri ve personel scope'ları aynı torbadaydı, redaction tek bir bayrağa bakıyordu → yalnız users:read onaylatan eklenti, customers:read hiç istemeden müşterinin adını, telefonunu, adresini ve konumunu alıyordu. Artık CUSTOMER_PII (customers:read, customers:write, messaging:provide, capability:messaging.send:provide) ve USER_PII (users:read) ayrı kapılar. Müşteri verisi okuyorsan manifest'ine customers:read ekle ve yeniden onay al; yoksa customer alanında yalnız { id, region } görürsün.
  • Adlandırma kuralı — sözleşme alanına reason adı verilmez: redaction katmanı reason anahtarını serbest-metin PII sayıp her derinlikte siler; alan önce bu adla yazılmıştı ve PII rızası olmayan tipik mali eklentide undefined geliyordu → kural gereği taşınan her satır storno edilirdi. Kök seviyede deleteReason, satır seviyesinde removeReason.
  • lineChanges kapsamı genişledi: önce yalnız masa CRUD yollarındaydı — yani bloğun asıl gerekçesi olan bölme/taşımada yoktu. Şimdi kalem ekleme/düzenleme/iptal/indirim/kuver · tahsilat ekleme ve iptali · hesap bölme · masa taşıma · birleştirme (tam ve kısmi) · dört eklenti yazma ucu · cari hesaba devir · e-fatura/e-adisyon senkronunda yayınlanıyor.
  • Tam değiştirmede düşen satır artık iz bırakıyor: update-orders PUT semantiğinde sepette olmayan satır orders[]'dan sessizce düşüyor, cancels[]'a hiçbir kayıt yazılmıyordu. Artık void kaydı yazılır ve delta cancelled olarak gelir (kuver satırı türev olduğu için dışlanır; void kaydı plugin:<pluginId> aktör damgası taşır).
  • İptal kayıtlarında indirim ve ekstra oransal: iptal kaydı discount: 0 ile yazılıyor, extra hiç bölünmüyordu → indirimli kalemde storno tutarı fazla, ekstralı kalemde ekstra çift sayılıyordu. 3 adetlik satır (indirim 10 · ekstra 30) 1 adet iptalinde: eski iptal {discount:0, extra:30} + kalan {discount:10, extra:30}; yeni iptal {discount:3.33, extra:10} + kalan {discount:6.67, extra:20} → toplam korunuyor.
  • Tam adet tahsilatta satır kimliği korunuyor: payOrders satırın tamamı tahsil edilince yeni lineId veriyordu; mali tüketici bunu "sil + ekle" görüp imzaladığı kaydı haksız yere storno ediyordu. Kimlik artık sabit. ⚠️ Kısmi tahsilatta satır hâlâ bölünür ve parçalar yeni kimlik alır (bilinçli olarak ertelendi) — soyu lineChanges'ten izle.
  • Yeni alan cancelPayments[]: iptal edilen tahsilatlar, payments[] ile birebir aynı şekil. Satır void'i yayınlanırken ödeme void'inin yayınlanmaması doğrudan bir asimetriydi. ⚠️ paid aktif ödemelerin toplamıdır — bu liste ona dahil değildir.
  • Bugün yapman gereken: moved_out geçen kodunu removed + removeReason'a çevir; müşteri verisi okuyorsan customers:read ekleyip yeniden onay al. Tipler SDK 3.0.1+'da.

2026-08-17 — ⏳ Sandbox: kapanış iptali, paket gate token'ı, kalıcı satış kimliği (uuid)

  • ⏳ Dördü de sandbox'ta yayında, production'a çıkmadı. Test mağazanda karşılaşabilirsin; canlı mağazada henüz akmıyor.
  • Yeni event table.close_aborted: gate onayından sonra kapanış düşerse gelir (kalem/ödeme eklendi → underpaid/overpaid/payment_without_order/total_repaired). Kestiğin fişi storno et. ⚠️ İptal kapanışın tersi değildir: satış hattı sürer (sequence artmaz) → defterini kapatma. (⚠️ Güncellendi: packet.close_aborted artık yayınlanıyor — birebir aynı sözleşme.)
  • packet.close token yolu: paket kapanışında da UI/form gösterip receiptExtras döndürebilirsin — panel gateToken'ı data.gateTokens[] ile taşır (HMAC imzalı, 120 sn, yalnız gerçek allow). Senkron yol kaldırılmadı. Ayrıca ön koşul kapısı: kapanamayacak hesapta gate hiç çalışmaz → plugin.hook.preconditionFailed.
  • uuid — kalıcı satış kimliği: bir satışın üç doküman kimliği vardı (masa-300 → masa-300* → kapanış UUID'si). uuid ömür boyu sabittir ve kapanış kaydının kimliğidir (⚠️ güncellendi: table.closed'ın tableId'si artık masa slug'ı — bkz.); olaylarda + tables/open · packets/open'da gelir.
  • Sıra sayacı taşımada: boş masaya taşımada kaynak n+1, hedef n+2 (aynı hat); birleştirmede hedef kendi hattını ilerletir; kısmi taşımada iki hesap da kendi hattını sürdürür.
  • Açık soru cevapları: extra ölçümde tamamen sayı çıktı. (⚠️ Güncellendi: extra/discount/quantity artık normalize ediliyor ve sözleşme tip garantisi veriyor — bkz. Üç kırıcı değişiklik.) Taşımada satır kimliği korunur (çakışmada movedFrom.lineId eskiyi taşır). İptal sebebi metni (reasonId → katalog) hâlâ açık madde.

2026-08-17 — Satır deltası (lineChanges) — *.updated olaylarında satır düzeyi delta

  • Ne geldi: table.updated / packet.updated gövdesine data.lineChanges — hangi kalem değişti, ne kadarı iptal edildi, hangi yeni satır hangisinin devamı. Alan opsiyoneldir: yalnız satır düzeyinde bir şey değiştiğinde gelir (ödeme eklenmesinde gelmez) → ?? [] ile oku. Tam sözleşme: Satır deltası (lineChanges); tipler SDK 3.0.1'de.
  • Neden: tüketicinin kendi diff'i iki yerde yanılıyor — yeniden kimliklenen satır (kısmi tahsilat, bölme, taşıma) "sil + ekle" görünüp haksız storno ürettiriyor; adet azaltmada iptal ise satır hâlâ aktifken "satır gitti" gibi okunuyor.
  • op sözlüğü: created · updated · cancelled · moved_out (⚠️ sonradan kaldırıldı → removed + removeReason; bkz. sözleşme bülteni). deleted bilerek yok: bir satır ya iptal edilir ya devredilir, mali işlemleri zıttır.
  • ⚠️ İki kritik kural: blok bir optimizasyondur, kaynak değildir (gerçek orders[] tam durumu; blok yoksa/eksikse tam durumdan yakınsa) ve cancelled adet kapsamlıdır (quantity iptal edilen adettir; satır azaltılmış adetle hâlâ aktif olabilir).
  • Soy alanları: splitFrom · movedFrom · movedTo — yeniden kimliklenen satırın nereden geldiğini söyler; varsa storno yazma. Şekil *.deleted'takiyle aynı.
  • Tamamen additive: yeni olay tipi yok; orders[], changed, zarf, imza ve redaction aynen duruyor. Bugün yapman gereken: hiçbir şey.

2026-08-16 — Satış olaylarına sequence + amounts eklendi (kırıcı değişiklik yok)

  • Kırıcı değişiklik yok. İki blok eklendi; hiçbir alan kaldırılmadı, hiçbir tip değişmedi. Alanları okumayan mevcut eklentiler etkilenmez.
  • Sıra: zarfta sequence (satışın kaçıncı durum değişikliği) + sequenceScope (sq_ önekli opak satış hattı). Teslim sıralı değildir; occurredAt bayat snapshot'ı ayırt etmeye yetmez. Beş kural + defter tasarımı: Olay sırası (sequence).
  • ⚠️ Defteri (tenantId, sequenceScope) ile anahtarla ve terminal olaydan sonra hemen silme (öneri: 7 gün). Tek slotlu tasarım ve erken silme, satışın dirilmesine yol açar. Numara atlayabilir — olay kaybı değildir; alan yoksa sequence: 0 varsayma.
  • Tutarlar: belge kökünde currency + amountExponent, satırda amounts (tamsayı minor unit bileşen kırılımı) ve karışık KDV oranlı satırda perVat[]; options[].vatRate dondurulmuş orandır (null = dondurulmamış). Garantiler ve tuzaklar: Tutarlar (amounts).
  • ⚠️ amountExponent yalnız amounts.* içindir. lineTotalDecimal, total, paid, payments[].amount, options[].price ondalıktır — üssü onlara uygularsan 100 kat sapma. Özet uçlarında (tables/open · packets/open) üs hiç gönderilmez.
  • Yüzeyler: table.*/packet.* webhook'ları · packets/get · tables/get · blocking hook includeData · customer.order_added. sequence/sequenceScope yalnız webhook zarfında.
  • SDK 3.0.0: sequenceVerdict · advanceSequenceCursor · sequenceKey · minorToDecimal · sumLineAmounts · vatBreakdown (+ LineAmounts/VatBucket tipleri) — SDK.

2026-08-16 — Uç nokta adresleri eklenti seviyesine taşındı (sürüm mührü korunur)

  • Adres artık eklentinin özelliği. webhook / connect /action yolları ve UI sayfalarının ortak origin’i bir kez tanımlanır — her sürümde yeniden girilmez. Sürüm editöründeki “Uç noktalar” kartı bu tek konfigürasyonu düzenler.
  • Sektör standardı: Shopify (application_url + redirect_urls), Slack (Request URL), GitHub Apps (Webhook/Callback URL), Stripe (endpoint URL) — hepsi uygulama seviyesinde tutar ve origin/path diye bölmez.
  • ⚠️ Sürüm mührü korunur. Bir sürümü kaydettiğinde adres o sürüme mühürlenir; kurulumlar mühürlenmiş adrese teslim alır. Eklenti seviyesindeki adresi değiştirmek yayındaki sürümü etkilemez — yeni adres yeni bir sürüm yayınlanınca geçerli olur.
  • Neden böyle: onaylanmış bir sürümün teslim adresi yeniden incelemeden değiştirilemez. Aksi halde onaydan sonra adresi başka bir alan adına çevirmek mümkün olurdu ve onay anındaki canlı frame-ancestors denetimi anlamsızlaşırdı.
  • Domain taşırken eski adresi hemen kapatma. Kurulu tenant’lar yeni sürüme yükselene kadar eskisine teslim almaya devam eder. Ayrıntı: Ortamlar → Uç nokta adresleri.
  • MCP: yeni set_origins aracı; set_dev_origin geriye dönük ad olarak kaldı; create_version’da webhook_url/connect_url artık opsiyonel (verilmezse eklenti konfigürasyonundan türetilir).

2026-08-16 — Sipariş satırı options[] — düz ad dizisi yerine { id, title, price }

  • ⚠️ Kırıcı — okuma şekli değişti. Sipariş satırının options[] alanı artık düz ad dizisi değil, tipli nesne dizisidir: { id, title, price }. Yalnız ada ihtiyacın varsa options.map(o => o.title) yaz.
  • price katalogtan okunur ve satır toplamına platformca dahil edilir — lineTotalDecimal üzerine ayrıca ekleme.
  • Tüm okuma yüzeyleri aynı şekli döner: packets/get · tables/get · table.updated · packet.updated · *.created · *.closed · *.deleted · *.reopened · *.closed_deleted · packet.cancelled · customer.order_added · hook includeData gövdesi · callback servisi · ve iptal listesi cancels[].
  • Yazma geriye dönük uyumlu. Seçeneği { id } (katalog kimliği — en kesin), { title } ya da düz string (eski biçim) ile beyan edersin; mevcut çağrılar çalışmaya devam eder.
  • Fiyat beyan edilemez — şema seviyesinde reddedilir, katalogtan çözülür (fiyat otoritesi platformdadır).
  • Hata → 400 Invalid option: <ürün>: <neden> (<beyan>): seçenek katalogda yok · başlıkla beyan belirsiz (aynı adlı farklı fiyatlı seçenekler → { id } ile beyan et) · aynı seçenek tekrar beyan edilmiş.
  • SDK: OrderOption (okuma) ve CartOptionInput (yazma) tipleri eklendi; CartLine.options ve OrderLine.options bunlara bağlandı.

2026-08-15 — table.closed_deleted / packet.closed_deleted — kapanmış satışın kaydı silindi

  • İki yeni event — katalog 33 → 35: table.closed_deleted ve packet.closed_deleted. Kapanmış bir satışın kaydı silindiğinde düşer. Kapsam: orders:read. Tam sözleşme: Hesap Yaşam Döngüsü.
  • ⚠️ *.deleted ile karıştırmayın — farklı olaylar. *.deleted açık bir hesabın başka hesaba devridir (veri yaşar, sahip değişir); *.closed_deleted ise kapanmış bir satışın yok edilmesidir.
  • Mali/muhasebe için storno sinyali. *.closed ile kayıt oluşturduysanız (fiş, fatura, ciro satırı) bu olayda onu storno edin. *.reopened'dan farkı: orada satış tekrar açılır ve yeniden kapanınca yeni kayıt gelir; burada satış geri gelmez.
  • Gövde diğer hesap olaylarından farklı: { saleId, channel, status, closedAt, deletedAt, orders[], payments[] } — tableId/packetId yoktur, kimlik saleId'dedir. saleId, kapanış olayının gövdesindeki tableId/packetId ile birebir aynıdır → storno hedefini bununla bulun.
  • ⚠️ orders[] / payments[] tek arşiviniz. Silinmeden önceki tam kayıttır; kayıt silindiği için tables/get / packets/get ile artık çekemezsiniz — gövdeyi saklayın.
  • SDK 3.0.0: ClosedSaleDeletedPayload tipi; örnek eklentide iki yeni handler (44/44 event testi).

2026-08-15 — Platform sözleşmesi — echo kuralı TERSİNE döndü, actor genişledi, origin eklendi

  • ⚠️ Echo kuralı TAM TERSİNE döndü — en kritik düzeltme. Aynı gün yayımlanan "Hesap yaşam döngüsü" kaydı "kendi yazdığın olay sana teslim edilmez, koruma platformdadır" diyordu. Doğrusu: kendi yazdığın olay SANA DA gelir; platform kaynağa göre eleme yapmaz (Stripe, Shopify, Slack, GitHub ile aynı davranış) ve döngü koruması senin sorumluluğunda. Handler'ının ilk satırında ele — isOwnEcho(envelope, MY_PLUGIN_ID). Atlarsan yaz → olay al → tekrar yaz sonsuz döngüsüne girersin; id dedup'u bunu durdurmaz. Ayrıntı.
  • actor genişledi — ve güven seviyeleri ayrıştı. Önceki kayıt "kişi kimliği taşımaz" diyordu; doğrusu actor.userId (opak personel) ve actor.role (manager|staff) taşınır. ⚠️ Ama doğrulanmazlar: personel kasada PIN ile seçilir, backend bunu doğrulamaz → denetim izi olarak yaz, yetki kararında kullanma. Doğrulanmış tek actor alanı pluginId'dir.
  • Yeni zarf alanı: origin.deviceId. ⚠️ BU MADDE GERİ ALINDI — alan 19 Ağustos'ta kaldırıldı, platform göndermiyor; "mali atıfta bunu tercih et" yönlendirmesi GEÇERSİZDİR (bkz. en üstteki origin zarftan kaldırıldı kaydı — mali/kasa atfı için actor.userId + kendi registerId beyanın). Kayıt tarihçe için duruyor: alan o gün "oturum açmış cihaz hesabının doğrulanmış token kimliği" diye duyurulmuştu.
  • ⚠️ BU MADDE SONRADAN DÜZELTİLDİ — bkz. "Ödeme EKLEME modu" kaydı: yazma zarfı ayrı DEĞİLDİR, yazma uçları da { success:true, data:{…} } döner ve packets/create alanı packetId'dir (uuid değil). Aşağıdaki metin tarihsel kayıt olarak duruyor. Yazma yanıt zarfı ayrı: { ok: true, … }. Okuma uçları { success, data } döner, yazma uçları düz gövde — data yoktur. ⚠️ packets/create → { ok, uuid }: alan adı uuid, packetId değil. Dönen tutar otoriterdir (gönderdiğin fiyat yok sayılır, kuver yeniden enjekte edilir); paid > total yazma reddedilir.
  • Yeni nav yuvaları: sidebar.tools ve settings.menu (önceden yalnız sidebar.main).
  • cancels[] okuma yanıtlarında da var — tables/get ve packets/get iptal edilmiş kalemleri döner (reason serbest metin → PII rıza kapısına tabi). open özetleri de genişledi (type; pakette orderCode, entegrasyon, isScheduled, scheduledDate).
  • Hata → HTTP statü, mesajın son ekinden türetilir — hata referansı: *.notFound 404 · *.missingParams 400 · plugin.scope.denied 403 · *.duplicateInProgress 409 · *.noProvider 424 · *.providerUnavailable 503 · *.timeout 504 · internal 500.
  • Hiçbir uç hesabı kapatamaz — paid == total olsa bile. Kapanış, iptal ve silme yalnız çekirdek/personel akışındadır; eklenti oluşturur ve alan günceller.
  • SDK 3.0.0: isOwnEcho, Origin, ActorRole, Actor.userId/role, Table.cancels / Packet.cancels ve yazma yanıtı düzeltmesi (istemci yazma çağrılarında undefined dönüyordu).

2026-08-15 — Hesap yaşam döngüsü — 6 yeni event + zarfta actor alanı (kısmen DÜZELTİLDİ)

  • Altı yeni event — katalog 27 → 33. Bir masa/paket hesabı açıldıktan sonra başına gelen her şey artık akıyor: table.updated · table.reopened · table.deleted · packet.updated · packet.reopened · packet.deleted. Veri kapsamı altısında da orders:read; müşteri alanları ve serbest metin için ek olarak customers:read + rıza. Tam referans: Hesap Yaşam Döngüsü.
  • Zarfta yeni alan: actor. Değişikliğe kimin sebep olduğunu söyler ({ type: "staff" | "plugin" (+pluginId) | "system" }) ve data'nın dışında, üst seviyede durur — kapsam eksikken data boşaltılır ama aktör bilgisi hassas değildir ve kaybolmaz. Opsiyoneldir: alan yoksa kaynak bilinmiyordur → "personel" varsayma. (⚠️ DÜZELTİLDİ — "kişi kimliği taşımaz" iddiası YANLIŞTI: actor userId + role de taşır — ama bunlar doğrulanmaz; ayrıca zarfta origin.deviceId vardır — ⚠️ o da geri alındı, alan kaldırıldı. Bkz. üstteki "Platform sözleşmesi" ve "origin zarftan kaldırıldı" kayıtları.) Zarf sürümü yine "1".
  • *.updated — satır düzeyinde ayrı olay YOKTUR. Kalem ekleme/düzenleme/iptal, satır indirimi, kuver, ödeme alma ve iptali, masa taşıma/bölme ve ÖKC senkronu hepsi bu tek olayla bildirilir. data.changed hangi işlemin tetiklediğini söyler ama yalnız filtre ipucudur: null olabilir, liste büyüyebilir ve gövde sözleşmesi değere göre değişmez → bilinmeyen değerde olayı yine de işle.
  • *.reopened — bu YENİ BİR SATIŞ DEĞİLDİR. Olay *.created olarak gelseydi kesilen fişin üzerine ikinci bir fiş kesilirdi. Doğru davranış: kapanış kaydını storno et, hesabı tekrar açık say, yeniden kapanınca *.closed ile yeni kayıt kes. Zincir data.reopenedFrom.saleId ile kurulur (kapanış gövdesindeki tableId/packetId ile birebir aynı); null ise hiçbir kaydı storno etme. ⚠️ Geri açılan masa yeni bir kimlik alır (Masa 5 → Masa 5*) — pakette kimlik korunur.
  • *.deleted — devir, kapanış değil. Hesap kapanmadan satırları başka bir hesaba geçti: veri kaybolmaz, sahip değişir. *.closed göndermek taşınan hesabı satış sayıp ciroyu çiftlerdi. Gövde *.updated ile aynı şekil + deleteReason (merged · moved_to_table · moved_to_account) + movedTo { type, id }; movedTo: null ise kaydı kapat ama satırları bağlama. Kısmi devirde bu olay atılmaz → *.updated (changed: "order_moved").
  • Upsert: data hesabın TAM hâlidir, delta değil. orders[]/payments[] her zaman tam listedir → kendi kopyanı tamamen değiştir. Sıra garanti edilmez: occurredAt karşılaştır, geç gelen eski olayı yok say — kaybolan ya da sırası bozulan tek bir olay kalıcı hasar vermez.
  • ⚠️ DÜZELTİLDİ — TERSİ DOĞRU. Bu madde "döngü koruması platformda, kendi yazdığın olay sana teslim edilmez" diyordu; doğrusu: kendi yazdığın olay sana da gelir, platform kaynağa göre eleme yapmaz ve koruma sende (isOwnEcho). Bkz. üstteki "Platform sözleşmesi — echo kuralı TERSİNE döndü" kaydı.
  • SDK 3.0.0 tipleri taşır: Actor · ChangedReason · DeleteReason · MovedTo · ReopenedFrom · CancelledLine + altı gövde tipi ve accountKey / isAccountLifecycleEvent yardımcıları. parseEnvelope artık actor'ü açık bir eşleyiciyle doğrular (tanınmayan type → alan düşer).

2026-08-15 — Satır kimliği sözleşmesi — nihai: metadata artık {key,value,by} DİZİSİ

  • metadata artık { key, value, by } DİZİSİ. Taslakta düz map ({ "tseRef": "TSE-9911" }) olarak anlatılmıştı; nihai şekil dizidir ve geriye uyum yoktur — map gönderen istek 400 metadata must be an array of {key, value} alır. Yanıtta her öğe platformun bastığı bir by yazar damgası taşır (istekte gönderilirse yok sayılır): iki eklenti aynı anahtarı çakışmadan kullanabilir, çağıran yalnız kendi öğelerini değiştirir/siler. Aynı yazar bir anahtarı iki kez yazamaz (400). Anahtar: alfanümerik başlar, A-Za-z0-9_-, ≤64. Ayrıntı: Satır kimliği & metadata.
  • Altın kural: kimliği HER istekte yeniden beyan et. Kimlik korunması koşulludur — tam sepet değiştirmede platform yalnız gelen listedeki kimlikleri eşler. Beyan edilmeyen satır "silinmiş" sayılır ve üzerindeki tüm metadata (başka eklentilerinki dahil) onunla gider. İstek reddedilmez (meşru satır silme de bu yola düşer) ama platform kaybı loglar.
  • Görünürlük düzeltmesi — rıza kapısı KALKTI. metadata tenant içi korelasyon verisidir (kiosk yazar, fiskal sağlayıcı okur) → her kanalda orders:read yeterlidir, müşteri-PII rızası gerekmez ve alan push gövdelerinden silinmez. Taslakta "customers:read + rıza yoksa silinir" deniyordu. note, paymentNote ve iptal reason bu kapsamda değildir — rıza kapısında kalır. PII yasağı sözleşme düzeyindedir: platform desen taraması yapmaz (yanlış pozitif meşru bir mali kaydı reddedip satışı durdururdu).
  • Üç ayrı bütçe. Yazar başına ≤10 öğe / satır ve value ≤256 karakter · istek başına ≤20 KB (sepet ve ödeme ayrı) · belge geneli birleşme sonrası ≤100 KB (orders + payments + iptaller; seninkiler + korunan yabancı öğeler). Belge aşımının mesajı kırılımlıdır — (yours: X, other plugins: Y); aşım karşı taraftaysa kendi payload'ını küçültmek çözmez.
  • Masa taşıma düzeltmesi — kimlikler KORUNUR. Taslakta "yeniden üretilir, kapsam dışı" deniyordu. Nihai davranış: taşıma/birleştirmede kimlikler korunur; yalnız hedef masada aynı kimlik zaten varsa taşınan satır yeni kimlik alır (çakışma çözümü) ve bir satırın yalnız bir kısmı taşınırsa taşınan parça yeni kimlik alır → eşlemeyi metadata üzerinden sürdür (o satırla birlikte taşınır).
  • POS/panel ve üretilen kimlikler. Panelden ürün eklemek tam değiştirme değildir (append) → kimlikler değişmez; kuver satırının kimliği sabittir: kuver. Ödeme satırı her zaman kalıcı bir lineId taşır; göndermezsen platform üretir (pay- önekli). Alanı olmayan satırlarda (panel ödemeleri, eski kayıtlar) okuma ucu lineId: null döner — platform kimlik uydurmaz.
  • Doğrulama yazımdan ÖNCE koşar: kısmi yazım olmaz ve hatalı istek fiş numarası (docNo) tüketmez. Sepet uçlarında mesaj ihlalin hangi ürün satırında, ödeme uçlarında hangi indekste olduğunu söyler → Hata Kodları.

2026-08-15 — fiscal.de — masa kanalı açıldı, kasa kimliği (registerId) eklendi

  • Masa kanalı açıldı — sale.type: "table". fiscal.de invoke artık packet ve table kabul ediyor (v2'de table → 400 saleTypeUnsupported dönüyordu). Kod kalkmadı: tanınmayan tipler ve ileride eklenecek kanallar için ayrıldı.
  • ⚠️ Masa anahtarı oturum-tekil DEĞİLDİR. tableId kat planındaki kalıcı masa kimliğidir — masa kapanınca kayıt silinir, aynı masa aynı id ile yeniden açılır; oturumları docNo ayırır. Sağlayıcı tekleme anahtarını yalnız table:<id> olarak kurarsa dünkü oturumun Beleg'i bugünkü satışa replay edilir ve o satış hiç TSE imzası almaz. Masa kanalında anahtara oturum ayracı (docNo + iş günü) ekleyin. packet kanalında bu sorun yoktur (paket id'si işlem-tekil).
  • Kasa/terminal kimliği — registerId (ops., ≤64, opak). Platform yorumlamaz, sağlayıcıya olduğu gibi iletir; sağlayıcı bunu kendi kasa kaydına (DSFinV-K Z_KASSE_ID) eşler. Tanınmayan değer imzayı engellemez — varsayılan kasaya düşülür. Gönderilmezse davranış bugünküyle aynıdır.
  • SDK 2.1.0 (npm'de yayında) alanları taşır: FiscalDeInput.registerId · FiscalDePayload.registerId · FiscalSaleType = "packet" | "table". Eklemeli — mevcut çağrılar aynen çalışır.

2026-08-15 — Sipariş & ödeme alanları — lineId · metadata · type (taslak)

  • ⚠️ Bu kayıt TASLAK sözleşmeyi anlatır. Nihai şekil için yukarıdaki Satır kimliği sözleşmesi — nihai kaydına bakın: metadata düz map değil { key, value, by } dizisidir, rıza kapısı kalktı (orders:read yeterli) ve masa taşımada kimlikler korunur. Aşağıdaki maddeler tarihsel kayıttır.
  • Kararlı satır kimliği — lineId (ops.). packets/update-orders ve tables/update-orders sepetin tamamını değiştirdiği için satır kimlikleri her yazımda yeniden üretiliyordu; Almanya'da her satır kayıt anında TSE ile imzalandığından kimlik değişince fiş zinciri kopuyordu. Artık kimliği çağıran taşır, platform korur (Stripe line_item.id deseni). Biçim: 1–64 karakter, alfanümerik başlar, A-Z a-z 0-9 . _ : -; kuver ve new rezerve. Aynı istekte tekrar → 400; iptal edilmiş (void) bir satırın kimliğini yeniden beyan → 409 lineId belongs to a cancelled line. Gönderilmeyene platform üretir ve üretilen kimlikler beyan edilenlerle çakışmaz. Ayrıntı: Satır kimliği & metadata.
  • Ödeme satırının artık kimliği var. Gönderilmese bile her ödeme satırı kalıcı bir lineId alır (önceden ödeme kaydının kimliği hiç yoktu). ⚠️ payments[].id satır kimliği değildir — o alan ödeme yönteminin id'sidir ve polimorfiktir; okuma yanıtında lineId (kimlik) ve methodId (yöntem) ayrı döner, id'ye fallback yapılmaz.
  • Opak korelasyon alanı — metadata (ops.). Düz key/value; platform yorumlamaz, okuma yanıtında aynen döner. Değerler string | number | boolean; ≤10 anahtar, string değer ≤256 karakter, istek başına toplam ≤20 KB (sepet ve ödeme ayrı bütçe); iç içe obje / dizi / null → 400. PII yasak: push kanallarının ikisinde de (webhook fan-out'u ve hook çağrısı) alan note ile aynı sınıftadır — customers:read + rıza olmayan eklentiye giden gövdede silinir; okuma uçlarında orders:read ile döner. (DÜZELTİLDİ — nihai sözleşme: metadata düz map değil { key, value, by } dizisidir; rıza kapısı kalktı, her kanalda orders:read yeterlidir ve alan push gövdelerinden silinmez; bütçeler yazar başına 10 öğe / istek başına 20 KB / belge geneli 100 KB.)
  • İki alan da ALTI ucun hepsinde geçerli (sepet kalemi ve ödeme satırı tek şemadan gelir): packets/create · packets/update-orders · packets/update-payments · tables/create · tables/update-orders · tables/update-payments.
  • Satışın tüketim biçimi — type (ops.). dine_in | takeaway | delivery; bu bilgi yalnız satışın alındığı yüzeyde doğar, platform türetemez (mali oran DE %19/%7 ve raporlama buna bakar). packets/create opsiyonel alır (whitelist dışı → 400), packets/update ile güncellenebilir (değişim eski→yeni loglanır), tables/create almaz (masa zaten dine_in). Okumada değer veya null ("bilinmiyor" — sessizce bir biçime düşürülmez); masa hesaplarında daima dine_in. deliveryType/restaurantDelivery (kim teslim ediyor) ve fiscal.de sale.type (hangi koleksiyon) ile karıştırmayın → Satış tipi (type).
  • Bilinen sınırlar. POS/panel düzenlemesi kimliği korur; masa taşıma/birleştirme kimlikleri yeniden üretir (moveTableToTable kapsam dışı); sunucu tarafı heuristik eşleme yok; kimlik korunursa created zamanı her zaman, ready/storages yalnız ürün aynıysa taşınır; lineId içerik değişmezliği garanti etmez (orders:write yetkili çağıran aynı kimliği farklı ürün/fiyatla beyan edebilir). (DÜZELTİLDİ — nihai sözleşmede masa taşıma/birleştirme kimlikleri korur; yalnız hedef masada aynı kimlik varsa ya da satırın bir kısmı taşınırsa yeni kimlik verilir.)
  • Geri uyum: üç alan da opsiyoneldir — hiçbiri gönderilmezse davranış bugünküyle aynıdır, mevcut eklentiler etkilenmez.

2026-08-15 — fiscal.de v2 — sepet satırında KDV oranı, tutar türetme kuralları, gate’te binding

  • Sepet satırında KDV oranı — taxRate (ops., 0–100). Almanya'da oran ürüne değil siparişe bağlı olabilir (yerinde tüketim %19 / götürü %7); bu bilgi yalnız sipariş yüzeyinde doğar, platform türetemez. Verilirse ürünün oranını ezer ve satırda dondurulur — ürünün oranı sonradan değişse bile geçmiş satışın mali kaydı değişmez. Geçersiz değer 400 döner (sessizce ürüne düşmez); beyanı yapan eklenti satırda taxRateBy ile kaydedilir. Aynı şema dört uçta: packets/create · packets/update-orders · tables/create · tables/update-orders. Fiyat otoritesi değişmedi (kalem fiyat göndermez).
  • Tutar türetmesi yazıya döküldü. İndirim satırları amountsPerPaymentType'a girmez; KDV tarafında oranlara ciro payıyla orantılı dağıtılır (en-büyük-kalan yöntemi, kuruş kaybı yok). Oranlar kova kova toplanır, yüksek oran önce. Platform Σ amountsPerVatRate == Σ amountsPerPaymentType dengesini garanti eder: en fazla 2 kuruşluk artık en büyük KDV kovasına emilir, üzeri imza atılmadan reddedilir. Sağlayıcıdaki denge kontrolü artık savunma katmanıdır.
  • Fail-closed — sessiz varsayılan yok. Satırın KDV oranı çözülemiyorsa, ödeme satırının yöntem tanımı yoksa, satış satırsız/tahsilatsızsa, indirim ciroyu aşıyorsa ya da iki taraf dengelenmiyorsa istek reddedilir ve imza atılmaz.
  • Yeni hata kodları: saleTypeUnsupported (400) · vatUnresolved (400, details.lines[]) · paymentUnresolved (400, details.payments[]) · saleEmpty / saleUnpaid (400) · amountsInvalid (400). Kaldırılanlar: invalidVatRates · invalidPaymentTypes · invalidReference · invalidReferenceType.
  • sale sıkılaştı: type bu turda yalnız packet (table → 400 saleTypeUnsupported; masa id'si kalıcı olduğu için anahtar oturum-tekil değil). id ≤100 ve doküman id'si olarak kullanılabilir olmalı (/, ., .., __x__ yasak). idempotencyKey parmak izi = receiptType + sale. (DÜZELTİLDİ — masa kanalı sonradan açıldı: type artık packet ve table kabul eder; oturum ayracı sorumluluğu sağlayıcıdadır.)
  • accepted terminal DEĞİL: sonuç taşımaz ve aynı key ile yeni çağrı sağlayıcıya tekrar gider. Replay: aynı key ile ikinci çağrı sağlayıcıya gitmez, saklanan yanıt idempotentReplay: true ile döner — satış bu arada silinmiş olsa bile (varlık kontrolü yalnız yeni isteklerde koşar). Platform retry yapmaz.
  • Gate yolunda scope yetmez — binding de aranır. Kapanış gate'i tenant-genel sorulduğu için, tse.* yazan eklentinin o tenant'ta fiscal.de'nin bağlı sağlayıcısı olduğu da doğrulanır; değilse öğe sessizce düşer (tek aday varsa davranış değişmez). Yazılmış bir key hiçbir yoldan üzerine yazılamaz.
  • Kapsam dışı: Storno/iade bu kanaldan imzalanamaz (tutarlar platformdan türetiliyor, satış satırları negatif olamaz).

2026-08-14 — fiscal.de sözleşmesi — mali blok yanıtta, zorunlu sale referansı, tutarları platform türetiyor

  • Mali blok artık invoke yanıtında: fiscal.de çağrısının sonucu receiptExtras taşır (tse.qr, tse.txNumber = Beleg-Nr, tse.signatureCounter, TSE zamanları). Neden: fişini KENDİ basan eklenti (self-servis kiosk) POS kapanış gate'ini tetikleyemez, packet.closed ise kapanıştan SONRA gelir — müşteri fişini beklerken QR elde edilemiyordu. KassenSichV §6 mali alanların müşteri fişinde bulunmasını ister.
  • Duruma göre içerik: signed → blok dolu · ausfall → tek öğe (tse.ausfall, arıza fişte görünür kılınır) · failed → blok taşınmaz (imza atılmamıştır).
  • tse.qr BYTE-KESİN taşınır: kırpma / escape / normalize yok. Kırpılmış QR geçerli görünür ama doğrulanamaz — hiç QR olmamasından kötüdür. Sınırlar, tse.* namespace sahipliği ve verified kuralı kapanış gate'iyle birebir aynı; iki yol da aynı bloğu üretir.
  • Tüketici artık TUTAR GÖNDERMİYOR. KDV kırılımı (satırlar × ürünün tax alanı) ve ödeme kırılımı (payments[] + yöntemin cash bayrağı → CASH/NON_CASH) platform tarafından satış kaydından türetilir ve sağlayıcıya öyle iletilir; gönderilen tutarlar yok sayılır. Neden: mali kayıt POS kaydıyla birebir tutmak zorundadır (AO §146a / DSFinV-K) — tüketici beyanı ile POS kaydı ayrışırsa yanlış Zahlart ve yanlış ciro beyanı doğar. Sağlayıcı tarafında değişiklik yok: alınan payload aynı alanları taşır, yalnız kaynağı platformdur.
  • Zorunlu, tipli satış referansı sale: { type: "packet", id }. Eski serbest metin reference alanı kullanımdan kalktı (@deprecated) — opsiyonel ve tipsiz olduğu için tekleme anahtarı olarak güvenli değildi. Platform sale'in çağıranın işletmesinde var olduğunu doğrular (yoksa 404, 403 değil). Sağlayıcı tekleme anahtarını bundan kurar (fiscalSaleKey → packet:pkt_123) — aynı satışın POS kapanışı ikinci Kassenbeleg kesmez.
  • SDK 2.0.0 (kırıcı): FiscalDeInput artık { sale, receiptType?, idempotencyKey }. Yeni: FiscalSaleRef, fiscalSaleKey(), FiscalDeResult.receiptExtras, fiscalResponse(). ⚠️ Jenerik capabilityResponse() yalnız {status, providerMessageId, error} kurar → onunla yanıt üreten sağlayıcı mali bloğu kendi elinde düşürür; replay yanıtlarında da bloğu taşıyın. KDV oranı DSFinV-K listesinden olmalı (19/7/10.7/5.5/0 — oran tahmin edilmez); providerMessageId = Beleg-Nr.

2026-08-03 — Yayıncı kullanıcı adı (@handle)

  • Yayıncı kimliği artık kullanıcı adı: eklentiler marketplace'te geliştiricinin @kullanıcıadı handle'ıyla listelenir. Kullanıcı adı global benzersizdir — görünen ad (serbest metin) aynı kalabilir, yayıncı kimliği karışmaz.
  • Bir kez seçilir, sonra kilitlenir: mevcut hesaplara görünen addan otomatik bir handle türetildi; portalda Hesap sayfasından bir kez değiştirebilirsin. Kaydettikten sonra kilitlenir (değişiklik için destek talebi) — yayınlanmış bir yayıncı kimliği kendiliğinden el değiştirmesin diye.
  • Kayıt formunda seçilebilir: yeni hesap açarken kullanıcı adı alanı doldurulursa doğrudan o handle kurulur (boşsa görünen addan türetilir). restomenum, admin, support gibi platform çağrışımlı adlar rezervedir.
  • Go-live kontrol listesine eklendi: ilk yayından önce handle'ını istediğin hâle getir.

2026-08-01 — Sürümle mağaza bilgisi değişikliği + packet.cancelled

  • Mağaza bilgisi değişikliği sürümle gelir: eklenti adı / açıklaması / ikonu artık yeni sürümle birlikte değiştirilebilir. Manifest editöründeki Mağaza Bilgileri kartını doldur (boş alan = değişiklik yok) — öneri sürümle incelemeye girer, sürüm onaylanıp yayınlanınca marketplace'e uygulanır (anında değişmez).
  • MCP aracı set_listing: AI geliştirme akışında da aynı öneri verilebilir (display_name / description / icon_url; "" = öneriyi temizle). get_manifest mevcut öneriyi listing alanında gösterir.
  • Yeni event packet.cancelled: paket sipariş tam iptali. Önerilen scope: orders:read.

2026-06-14 — Event kataloğu + REST hata kodları + müşteri uçları

  • Yeni event table.created (canlı): dine-in masa açıldı (müşterisiz açılış; müşteri atanmış açılış packet.created sayılır). Masa taşıma/dönüşümde tetiklenmez. Scope: orders:read.
  • Yeni event'ler customer.created / customer.updated (canlı): müşteri (CRM) oluşturma/güncelleme — PII, customers:read + consent ile dolu gelir.
  • Yeni okuma uçları customers/list + customers/get (canlı): müşteri kataloğu (CRM/sadakat) — {id, region, name, phone, address, total} allowlist; PII consent gate; list cursor pagination (after/nextCursor, max 500). Scope customers:read.
  • Callback API REST hata kodları: /plugin-api/* artık doğru HTTP status döner — not-found 404, doğrulama 400, scope/sahiplik 403, çakışma 409 (eskiden hepsi 200). Gövde {success,message} uyumlu kaldı (Hata Kodları).
  • OAuth token ucu (RFC 6749): hata yanıtı standart — HTTP 4xx + {error, error_description} (invalid_client→401, invalid_grant→400…). Başarı değişmedi (Token Exchange).

2026-06-13 — Masa yazma uçları, apiKey rotasyonu & rate-limit başlıkları

  • Masa (dine-in) yazma uçları CANLI: tables/update-orders + tables/update-payments — packets karşılıkları (full-replace). Kuver otomatik eklenir, timer ürünlü masalar desteklenmez, ödeme doğrulaması packets ile birebir. tableId'yi tables/open'dan al.
  • apiKey rotasyonu: kurulum başına Callback API key'i portaldan yenilenebilir — eski key anında geçersiz, yenisi bir kez gösterilir (Token Exchange → apiKey rotasyonu).
  • Rate-limit yanıt başlıkları: tüm /plugin-api/* yanıtlarında X-RateLimit-Limit/Remaining/Reset; 429'da Retry-After (sn) — backoff yerine buna uy (Limitler).
  • Eklenti kaldırma (sunset): geliştirici eklentisini portaldan kaldırabilir — tüm kurulumlar sonlandırılır (abonelik iptali + app.uninstalled GDPR sinyali); kayıt tombstone olarak kalır, geri alma taslağa döndürür.
  • Fiyat değişikliği politikası: abonelik fiyatı düşerse mevcut aboneler bir sonraki dönemden itibaren yeni fiyata geçirilir (dönem ortası iade/kredi yok); artarsa/aynıysa mevcut aboneler eski fiyatta kalır (grandfather — sessiz zam yok). Kaldırılan interval'deki aboneler kalıcı grandfather. Migrate edilen abonede tutar, bundan sonraki dönem fiyatıdır.

2026-06-12 — Sipariş platformu yetenekleri

  • Yeni yazma ucu: POST /plugin-api/packets/create — paket/sipariş oluştur (orders:write canlı). Fiyat sunucuda hesaplanır; idempotencyKey ile retry-güvenli.
  • Yeni gate hook'ları: packet.status.update (statü geçişi öncesi — sahiplik damgalı) ve packet.close (hesap kapanışı öncesi — tenant-genel). Scope'lar: hooks:packet.status, hooks:packet.close.
  • Gate iframe akışı: App Bridge resolve/close + önerilen Desen B (iframe → kendi backend'in, session token ile).
  • Gate decision:"pending" + async resolve: packet.status.update gate'inde senkron pencereye (≤10 sn) sığmayan kararlar için geçişi askıya al → gate-resolve ucuyla sonra allow/deny ver.
  • packets/create.callbackUrl: sahiplik damgası (packet.status.update gate'ini açar) + ileride statü bildirimi hedefi.
  • Ödeme doğrulaması (kırıcı olabilir): packets/create + update-payments normal ödeme satırlarının id'sini tenant'ın gerçek yöntemlerine karşı doğrular (unknown_payment_method); önce payment-methods/list. İndirim satırları muaf.

2026-06-11 — Event & uç genişlemesi

  • Lifecycle webhook'ları canlı: app.installed, subscription.activated/past_due/canceled, app.uninstalled — abone olmadan her bağlı kuruluma gelir.
  • Yeni okuma uçları: payment-methods/list, ingredients/list, categories/get.
  • Event payload'ları doğrulandı: tüm subscribable event'ler (product/category/user/payment_method/ingredient + packet/table) canlı; zarf version string "1", entegrasyon string kanal kodu.

2026-06-10 — Teslim sağlığı & GDPR

  • Teslim sağlığı (circuit breaker + cap): sürekli başarısız endpoint'te teslimler geçici atlanır (skipped), 72sa sonra kalıcı durur; hacim cap'i aşımında dropped. Sinyaller: delivery.degraded/restored/disabled/throttled lifecycle webhook'ları (canlı).
  • customer.redact (GDPR/KVKK): tenant müşteri silince zorunlu PII-silme webhook'u (PII scope'lu kurulumlara; critical, breaker/cap muaf).
  • Billing: subscription.canceled.reason="plugin_now_free" (ücretsize geçişte erişim ücretsiz sürer — deprovision etme); ücretsize geçişte mevcut kurulumlar ücretsiz kalır (grandfather).

Yakında

  • packet.status_changed event'i: statü değişince sahip eklentiye async (imzalı) webhook bildirim — packets/create'teki callbackUrl'e (yoksa webhookUrl'e) düşecek. Şu an callbackUrl yalnız saklanır.
  • ui:form ve ui:widget scope'ları: manifest'te seçilebilir ama canlı katalogda yok — bildirimsel form/widget motoru yayına girene kadar akmazlar. Diğer tüm scope, event ve yetenekler canlıdır.
Test yöntemi & doğrulama için: Test Mağazaları · Hızlı Başlangıç.