Satır Kimliği (lineId) & metadata ✓ Canlı

Sipariş ve ödeme satırlarına kararlı bir kimlik (lineId) ve çağırana ait opak bir korelasyon alanı (metadata) eklendi. İkisi de opsiyoneldir; hiçbiri gönderilmezse davranış bugünküyle aynıdır — mevcut eklentiler etkilenmez. Aynı kurallar altı yazma ucunun hepsinde geçerlidir.

← API Uçları · Satışın tüketim biçimi: type (dine_in / takeaway / delivery).

Neden — tam-sepet değiştirmede kimlik korunur

packets/update-orders ve tables/update-orders sepetin tamamını değiştirir; alan gönderilmediğinde her yazımda satır kimlikleri yeniden üretilirdi. Almanya'da her satır kayıt anında TSE ile imzalandığı için "bu satır fişlendi mi?" sorusunun tek yanıtı satır kimliğidir — kimlik değişince zincir kopar. Artık kimliği çağıran taşır, platform korur. Sektör deseni aynı: Stripe line_item.id, Shopify OrderEdit satır kimliği — kimliği istemci taşır, sunucu korur.

lineId ve metadata AYNI yüzeylerde geçerlidir. Sepet kalemi ve ödeme satırı tek bir şemadan gelir, dolayısıyla iki alan da altı ucun hepsinde kabul edilir — sepet taşıyan uçta cart[].metadata, ödeme taşıyan uçta payments[].metadata. packets/create ve tables/create ikisini birden alır (hem sepet hem ödeme aynı gövdede).
Uçcart[]payments[]
POST /packets/createlineId · metadatalineId · metadata
POST /packets/update-orderslineId · metadata—
POST /packets/update-payments—lineId · metadata
POST /tables/createlineId · metadatalineId · metadata
POST /tables/update-orderslineId · metadata—
POST /tables/update-payments—lineId · metadata

Altın kural — kimliği HER istekte yeniden beyan et

Kimlik korunması koşulludur. Tam değiştirmede platform, gelen sepetteki kimlikleri dokümandaki kimliklerle eşler. Beyan edilmeyen bir satır "silinmiş" sayılır — üzerindeki tüm metadata (seninki ve başka eklentilerinki) onunla birlikte gider.

✅ Doğru — kimlik yeniden beyan edildi
{ "product": "POS-11",
  "quantity": 1,
  "lineId": "kiosk-a1" }

Satır kimliği korunur. Başka eklentilerin (ör. fiskal sağlayıcının) bu satıra yazdığı metadata öğeleri de korunur.

❌ Yanlış — kimlik gönderilmedi
{ "product": "POS-11",
  "quantity": 1 }

Platform yeni kimlik üretir. Eski satır ve üzerindeki tüm metadata düşer; mali zincir kopar. İstek reddedilmez (meşru satır silme de bu yola düşer) ama platform kaybı loglar.

Kiosk 12 ₺'lik sepeti yazar ve fişler → garson POS'tan 35 ₺'lik ürün ekler → kiosk sepeti kimliksiz yeniden gönderir → tüm kimlikler değişir → sistem daha önce imzalanan satırları tanımaz. Müşteri 47 ₺ öder, fişte 12 ₺ kalır ve ilk satırlar ikinci kez imzalanır. Kimliği kendi kaydından türet (ör. kiosk-<sessionId>-<lineNo>) ve kendi tarafında sakla.

lineId — kurallar

  • Biçim: 8–64 karakter, alfanümerik başlar, A-Z a-z 0-9 . _ : - içerir. ⏳ Saf rakamdan oluşan kimlik reddedilir ("1", "42") — aşağıdaki kırıcı değişikliğe bak.
  • Rezerve: kuver ve new kullanılamaz (büyük/küçük harf farketmez) — platform bu iki kimliği kendi üretir (kuver satırı, "yeni satır" sentinel'i).
  • Benzersizlik: aynı istekteki liste içinde tekrar edemez → 400. İptal edilmiş (void) bir satırın kimliğini yeniden beyan etmek ise 409 lineId belongs to a cancelled line döner (aşağı bkz.).
  • Korunur (koşullu): gönderilen kimlik satırın kimliği olur — ancak tam değiştirmede yalnız yeniden beyan edilirse (altın kural). Gönderilmeyene platform üretir ve üretilen kimlikler beyan edilenlerle çakışmaz.
  • Karışık sepet: aynı sepette kimlikli ve kimliksiz kalemler olabilir; kimliksizler yeni kimlik alır, kimlikliler değişmez.
  • Ödemeler: ödeme satırı her zaman kalıcı bir lineId taşır; göndermezsen platform üretir (pay- önekli — önceden ödeme kaydının kimliği hiç yoktu).
  • Geçersiz değer sessizce düşmez: biçim/rezerve ihlalinde istek 400 alır — üretilmiş bir kimliğe düşülmez. Alanı olmayan satırlarda (panelden girilen ödemeler, bu değişiklikten önce yazılmış kayıtlar) okuma ucu lineId: null döner; platform kimlik uydurmaz.
İstek
POST {RESTOMENUM_BASE}/plugin-api/packets/update-orders
{
  "packetId": "pkt_123",
  "cart": [
    { "product": "POS-11", "quantity": 1,
      "lineId": "kiosk-9f2c" },     // fişi kesilmiş satır — kimliği KORUNUR
    { "product": "POS-12", "quantity": 1 }  // yeni → kimlik üretilir
  ]
}
Hatalar
// 400
"Invalid lineId: kuver"
"Duplicate lineId: kiosk-9f2c"
"Invalid metadata: POS-11: <sebep>"
"metadata must be an array of {key, value}"

// 409 — iptal edilmiş (void) satırın kimliği yeniden beyan edildi
"lineId belongs to a cancelled line: kiosk-9f2c"
Okuma yanıtı
GET {RESTOMENUM_BASE}/plugin-api/packets/get
{ "orders": [
    { "id": "kiosk-9f2c",
      "lineId": "kiosk-9f2c", … } ] }
payments[].id satır kimliği DEĞİLDİR. O alan ödeme yönteminin kimliğidir (nakit/kart tanımı) ve yerli akışlarda satır uuid'si de taşıyabilir — polimorfiktir, satır silme her yerde onu hedefler. Bu yüzden kalıcı kimlik ayrı lineId alanına yazıldı; id'nin anlamı değişmedi. Okuma yanıtında ödeme satırı lineId (kimlik) ve methodId (yöntem) alanlarını ayrı ayrı döner.

Grandfather — kural geriye dönük satırları kilitlemez ⏳ Yakında

⏳ Bu bölüm PLANLANAN davranışı anlatır — bugün ETKİN DEĞİL. Af mantığı platformda yazılı ve gerekçeli duruyor, ama şema katmanı isteği daha transaction'a girmeden reddettiği için ona hiç ulaşılmıyor. Ölçüldü — dört yazma ucunun hepsinde: packets/update-orders · tables/update-orders · packets/update-payments · tables/update-payments.

Bugün gerçekte ne oluyor: dokümanda zaten var olan zayıf bir kimliği geri gönderdiğinde kabul edilmiyor — 400 alıyorsun, ve mesaj şemanın ham metni oluyor ("lineId" length must be at least 8 characters long, saf rakamda "lineId" contains an invalid value) — yani hata "bu kimlik dokümanda var ama artık kabul edilmiyor" diye açıklamıyor da.

Pratik sonuç: kural konmadan önce yazılmış zayıf kimlik taşıyan bir dokümana tam-sepet yazamazsın — af tam da bunu önlemek için tasarlanmıştı. Kimliğini güçlü bir kalıba çevirmek (sabit önek + uuid, ör. pay-01H9XYZ…) tek çıkış yolu. Düzeltilirse değişiklik günlüğünden duyurulur.

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. Alt sınır bu yüzden 8'e çıktı ve saf rakam yasaklandı — "00000001" sekiz karakterdir ama iki bağımsız sayaç kaçınılmaz olarak aynı değerleri üretir, uzunluk tek başına yetmiyor.

Tam-sepet değiştirmede önceki yanıtta aldığın kimlikleri geri gönderiyorsun. Kural konmadan önce yazılmış zayıf bir kimlik dokümanda duruyorsa onu reddetmek ilgisiz satırlar dahil tüm isteğini düşürür ve dokümanı kalıcı kilitlerdi.Planlanan karar akışı (⏳ 2. ve 3. adıma bugün hiç ulaşılmıyor — şema ilk kapıda eliyor):

  1. Biçim geçerli mi? Değilse 400 — asla affedilmez.
  2. Ayırt edici mi? (≥8 karakter ve saf rakam değil) → kabul.
  3. Değilse: dokümanda zaten var mı? Varsa kabul — grandfather; yoksa 400 (yeni zayıf beyan).
Ek okuma maliyeti yok. Kontrol transaction içinde, doküman zaten okunurken yapılıyor.
create uçlarında grandfather YOK. packets/create ve tables/create yeni satış açar — karşılaştırılacak mevcut doküman yoktur, her zayıf kimlik yeni beyandır ve 400 alır.
Platformun kendi ürettiği kimlikler de düzeltildi. Kural konduktan sonra üretim verisi tarandı: 1196 satırın 409'u (%34) alt sınırın altındaydı ve hiçbiri eklenti yazımı değildi — hepsi platform üretimiydi. Kimlik <doküman kimliği>-<4 hex> kuruluyor ve kısa masa adlarında (b1, a1) toplam 7 karaktere düşüyordu: b1-1829. Üretim artık prefix'i telafi ediyor (b1-1d049). Senin geri gönderdiğin platform kimlikleri kuraldan geçer.
Öneri: anahtarını çifte çevir. Kural bir kalkan; yapısal çözüm (uuid, lineId) çiftini anahtar yapmaktır — uuid artık her olayda geliyor ve satış bazında kesin ayrık, çift bu hata sınıfını kuralın da ötesinde kapatır.

metadata — şekil ve sahiplik

Satırı dış sistemdeki karşılığına bağlayan opak alan (TSE imza referansı, kiosk oturumu, dış satır numarası). Bugüne kadar bu bilgi için taşıyıcı alan yoktu ve entegrasyonlar note alanını kirletiyordu. Platform yorumlamaz — ne fiyat/vergi hesabına, ne raporlamaya, ne de bir karara girer.

Şekil bir DİZİDİR, düz map değil. Sözleşmenin ilk taslağında { "tseRef": "TSE-9911" } düz map olarak anlatılmıştı; nihai şekil { key, value, by } dizisidir ve geriye uyum yoktur — map gönderen istek 400 metadata must be an array of {key, value} alır.
İstek
// İSTEK — { key, value } dizisi (by GÖNDERİLMEZ)
{
  "cart": [{
    "product": "POS-11", "quantity": 1,
    "lineId": "kiosk-a1",
    "metadata": [ { "key": "tseRef", "value": "TSE-9911" } ]
  }],
  "payments": [{
    "id": "m-cash", "price": 47,
    "lineId": "kiosk-pay-1",
    "metadata": [ { "key": "tseTx", "value": "77" } ]
  }]
}
Yanıt
// YANIT — platform her öğeye yazarı (by) damgalar
{
  "orders": [
    { "id": "kiosk-a1", "lineId": "kiosk-a1", "productId": "POS-11",
      "metadata": [ { "key": "tseRef", "value": "TSE-9911", "by": "hellokiosk" },
                    { "key": "loyalty", "value": "L-2", "by": "sadakat" } ] }
  ],
  "payments": [
    { "lineId": "kiosk-pay-1", "methodId": "m-cash", "amount": 47,
      "metadata": [ { "key": "tseTx", "value": "77", "by": "fiskaly" } ] }
  ]
}

by damgasını platform basar: istekte gönderirsen yok sayılır. Bu damga sayesinde iki eklenti aynı anahtarı çakışmadan kullanabilir ve kimsenin verisi bir başkası tarafından ezilemez. Kendi öğelerini by ile ayıkla — aynı satırda başkalarının öğeleri de bulunur.

KuralDavranış
OpsiyonelVerilmezse alan hiç yazılmaz; okuma ucunda null döner
OpakPlatform yorumlamaz — hesaba, raporlamaya ya da bir karara girmez
Değer tipistring, sonlu number veya boolean. İç içe obje / dizi / null → 400
Sahiplikby platformca damgalanır; çağıranın gönderdiği değer yok sayılır
Yazma yetkisiÇağıran yalnız kendi öğelerini değiştirir/siler; başka eklentinin öğesine dokunamaz
Tam değiştirmeUçlar full-replace olsa bile, aynı lineId yeniden beyan edildiği sürece diğer yazarların öğeleri korunur (altın kural)
Aynı anahtarAynı yazar bir anahtarı iki kez yazamaz (400); farklı yazarlar aynı anahtarı kullanabilir — by ayırır
Anahtar biçimiAlfanümerik başlar, A-Za-z0-9_-, en çok 64 karakter
Round-tripYazıldığı gibi geri döner (packets/get, tables/get, webhook)
Sınırlar — üç ayrı bütçe
// value: string | sonlu number | boolean
// key:   alfanümerik başlar, A-Za-z0-9_- , ≤64

yazar başına   ≤ 10 öğe / satır, value ≤ 256 karakter
istek başına   ≤ 20 KB   (cart ve payments AYRI bütçe)
belge geneli   ≤ 100 KB  (birleşme SONRASI tüm belge:
               orders + payments + iptaller; seninkiler
               + korunan yabancı öğeler)

// reddedilir → 400
{ "tseRef": "TSE-9911" }        // düz map (geriye uyum YOK)
[ { "key": "a", "value": { "b": 1 } } ]   // iç içe obje
[ { "key": "a", "value": [1, 2] } ]       // dizi
[ { "key": "a", "value": null } ]         // null

Belge geneli bütçe, birçok eklentinin aynı adisyona yazdığı durumda Firestore belge sınırının zorlanmasını engeller. Aşım 400 döner ve mesaj aşımın kimden geldiğini söyler — kendi payload'ını küçültmenin çözüp çözmeyeceğini buradan anlarsın:

400 — belge geneli bütçe aşımı
{
  "success": false,
  "status": 400,
  "message": "Total metadata size after merge (108006) exceeds 102400 bytes
              (yours: 18060, other plugins: 90300)"
}
Görünürlük: orders:read yeterlidir, PII rızası gerekmez. metadata tenant içi korelasyon verisidir — kiosk yazar, fiskal sağlayıcı okur. Bu yüzden görünürlük koşulu her kanalda aynıdır (okuma uçları, webhook fan-out'u, hook çağrısı): orders:read yeterlidir ve alan push gövdelerinden silinmez. Aynı ilke Google Drive'ın dosya properties alanında da geçerlidir. Buna karşılık note, paymentNote ve iptal reason bu kapsamda değildir: onlar gerçek kullanıcı girdisi taşır ve rıza kapısına tabi olmaya devam eder.
PII yasağı — sözleşme kuralı, otomatik denetim değil. metadata alanına ad, telefon, e-posta, adres veya kart bilgisi yazılamaz. Platform bu alanda desen taraması (e-posta/telefon regex'i) yapmaz: yanlış pozitif meşru bir mali kaydı reddedip satışı durdururdu. Kural sözleşme düzeyindedir, ihlal eklentinin sorumluluğundadır. Müşteri verisine erişim customers:read + rıza üzerinden yapılır.

POS/panel etkisi ve bilinen sınırlar

  • POS/panel düzenlemesi kimliği korur: personel panelinden mevcut bir masaya ürün eklemek tam değiştirme değildir — yeni satırlar mevcutların üzerine eklenir ve mevcut kimlikler korunur. Satır iptali/indirim gibi panel işlemleri de kimliği taşır. Kuver satırı platform üretimidir ve kimliği sabittir: kuver.
    ⏳ Başlığı (title) artık her akışta doludur ve şu sırayla çözülür: tenant ayarı (kuverText) → satışta hâlihazırda duran kuver satırının başlığı → "kuver". Önce bazı uçlar tenant metnini damgalıyor, alan istekte yoksa title undefined olup dokümandan düşüyordu — aynı tenant'ın bir fişinde işletmenin verdiği ad, diğerinde ham kimlik görünüyordu (DSFinV-K ARTIKELTEXT tutarsızlığı). ⚠️ Kuver satırı yalnız masa satışında oluşur; paket kanalında kuver satırı yoktur. (Sandbox'ta yayında, production'a çıkmadı.)
  • Masa taşıma/birleştirme kimlikleri KORUR — yalnız hedef masada aynı kimlik zaten varsa taşınan satır yeni kimlik alır (çakışma çözümü). Bir satırın yalnız bir kısmı taşınırsa kaynakta kalan parça kimliğini korur, taşınan parça yeni kimlik alır → bu senaryoda eşlemeyi metadata üzerinden sürdür (o satırla birlikte taşınır).
    ℹ️ Yeniden kimliklenen satırın nereden geldiği lineChanges soy alanlarıyla (splitFrom / movedFrom) açıkça bildirilir — "sil + ekle" gibi görünen bu durumda storno yazmaman için. Hedefte aynı kimlik zaten varsa satır yeni kimlik alır ve movedFrom.lineId eski kimliği taşır. Ödeme→satır bağı da birlikte taşınır. Üretilen olaylar: tam taşımada hedef table.updated + kaynak table.deleted (deleteReason: moved_to_table | merged); kısmi taşımada iki table.updated.
  • ⏳ Tam adet tahsilatta satır kimliği artık KORUNUYOR: bir satırın tamamı tahsil edildiğinde (payOrders) satıra yeni lineId veriliyordu; mali tüketici bunu "eski satır silindi + yeni satır doğdu" görüp imzaladığı kaydı haksız yere storno ediyordu. Kimlik artık ömür boyu sabittir; ödeme↔satır ilişkisi ayrı bir tahsis kaydıyla kurulur (Square line_items[].uid, Toast selections[].guid, Stripe invoice_line_item.id deseni). ⚠️ Kısmi tahsilatta satır hâlâ bölünüyor ve parçalar yeni kimlik alıyor (bilinçli olarak ertelendi) — o senaryoda soyu lineChanges (splitFrom) üzerinden izle.
  • Sunucu tarafı heuristik eşleme yok: "değişmeyen kalemleri ürün+fiyat+opsiyon+not ile eşleyip eski kimliği geri ver" bilinçli olarak yapılmadı — aynı üründen iki satır varken yanlış satırı "fişlendi" sayabilir ve hata sessiz kalır. Kimlik sözleşmesi bu riski çağırana bırakmadan çözer.
  • Benzersizlik istek içidir — ancak iptal edilmiş bir satırın kimliğini yeniden beyan etmek reddedilir (409 lineId belongs to a cancelled line): aynı adisyonda aktif ve void satır aynı kimliği taşıyamaz, aksi halde "fişlendi mi, storno mu?" belirsizleşir.
  • Kimlik korunursa satır durumu da korunur: created zamanı (BQ/mali çıpa) her zaman taşınır; mutfak/depo alanları (ready, storages) yalnız ürün aynıysa taşınır — aynı lineId altında ürün değiştirmek bir değiştirme sayılır.
  • lineId içerik değişmezliği garanti etmez: orders:write yetkili bir çağıran mevcut bir kimliği farklı ürün/fiyatla yeniden beyan edebilir (full-replace yetkisinin doğal sonucu). İmza doğrulaması mali sistemin işidir.

En iyi pratik

  • Kimliği kendi kaydından türet (ör. kiosk-<sessionId>-<lineNo>) ve kendi tarafında sakla — tam-sepet değiştirmede aynı kimliği geri gönderirsen fiş/TSE zinciri kopmaz.
  • Eski satırlarda null bekle: bu sürümden önce yazılmış satırlarda lineId/metadata null döner — payments[].id'ye fallback yapma (o alan yöntem id'si de olabilir → iki farklı satırı aynı kimlikle gösterirsin).
  • Bütçeyi taşırma: metadata yazar başına 10 öğe / 256 karakter, istek başına 20 KB ya da belge geneli 100 KB sınırını aşarsa istek tümüyle 400 alır — büyük veriyi kendi tarafında tut, buraya yalnız anahtar yaz. Belge bütçesi paylaşılır: mesajdaki other plugins kırılımına bak, aşım senden gelmiyorsa payload'ını küçültmek çözmez.
  • Kendi öğelerini by ile ayıkla — okuma yanıtındaki dizi aynı satıra yazan tüm eklentilerin öğelerini taşır. İlk eşleşen anahtarı almak başka bir eklentinin verisini okumana yol açar.
  • PII'yi metadata'ya koyma — sözleşme yasağıdır ve platform taramaz, yani ihlal sessizce geçer; müşteri verisi için customers/get kullan.
  • Doğrulama yazımdan ÖNCE koşar: kısmi yazım olmaz ve hatalı istek fiş numarası (docNo) tüketmez. Sepet uçlarında hata mesajı ihlalin hangi ürün satırında, ödeme uçlarında hangi indekste olduğunu söyler.