Satır Deltası — lineChanges ✓ Canlı

table.updated ve packet.updated olayları satır düzeyi delta taşır: hangi kalem değişti, ne kadarı iptal edildi, hangi yeni satır hangisinin devamı. Tam durum (orders[]) sözleşmesi değişmedi — bu blok onun üzerine, satır bazında defter tutan tüketiciler için gelir. Alan opsiyoneldir: yalnız satır düzeyinde bir şey değiştiğinde konur.

← Event Kataloğu · Hesap Yaşam Döngüsü

Alan opsiyoneldir — yokluğu hata değildir. lineChanges yalnız satır düzeyinde bir şey değiştiğinde konur; ödeme eklenmesi gibi olaylarda hiç gelmez. data.lineChanges ?? [] ile oku ve gerçeğin kaynağı olarak her zaman orders[] tam durumunu kullan — blok bir optimizasyondur, kaynak değildir.
Yayın kuralı (netleşti): blok changed tipine değil, satır dizilerindeki gerçek deltaya bağlıdır — delta boşsa alan hiç konmaz. Ödeme değişimleri delta üretmez: tahsilat ekleme/iptali satır dizilerini değiştirmediği sürece lineChanges gelmez. Aşağıdaki kapsam listesi bloğun hesaplandığı akışları sayar — her akışta mutlaka geleceğini değil.

Neden ekleniyor

Satış olayları bugün tam durum yayınlıyor: orders[] değişimden sonraki hesabın tamamı. Bu upsert sözleşmesi kayıp veya çift teslimde bile yakınsadığı için doğru tasarım ve değişmiyor.

Ama satır bazında defter tutan bir tüketici (her kalemi ayrı imzalayan mali eklenti gibi) "hangi satır değişti"yi kendi diff'iyle bulmak zorunda kalıyor — ve diff iki yerde yanılıyor:

Yanılma 1 — kimlik değişimi devir gibi görünüyor. Kısmi tahsilat, hesap bölme ve masa taşıma satıra yeni lineId veriyor. Diff bunu "eski satır gitti + yeni satır geldi" olarak okuyor; tüketici imzaladığı kaydı haksız yere storno ediyor ve aynı ciroyu iki kez sayabiliyor.
Yanılma 2 — iptal "satır gitti" demek değil. Adet azaltmada iptal edilen parça cancels[]'a aynı lineId ile yazılıyor ve satır azaltılmış adetle orders[]'da kalmaya devam ediyor. "Satır iptal edildi" diye okuyan tüketici hâlâ duran bir satırı siliyor.

lineChanges ikisini de açık alanlarla ifade ediyor: soy (splitFrom / movedFrom / movedTo) ve adet kapsamlı iptal.

Şekil

data.lineChanges — yalnız satır düzeyinde bir şey değiştiğinde gelir. Ödeme eklenmesi gibi olaylarda alan hiç konmaz.

table.updated — lineChanges bloğu (sözleşme örneği)
{
  "type": "table.updated",
  "sequence": 7, "sequenceScope": "sq_9f3ab27c…",
  "data": {
    "tableId": "masa-5",
    "changed": "order_updated",          // NE oldu — mevcut alan, değişmiyor
    "orders": [ /* tam durum — mevcut, değişmiyor */ ],
    "lineChanges": [                     // YENİ — hangi satırda ne oldu
      { "op": "updated", "lineId": "masa-5-a1f2",
        "before": { "quantity": 2, "amounts": { … } },
        "after":  { "quantity": 1, "amounts": { … } } },

      { "op": "created", "lineId": "masa-5-9z8y",
        "splitFrom": "masa-5-a1f2",      // aynı ciro, yeniden kimliklendirildi
        "after": { "quantity": 1, "amounts": { … } } },

      { "op": "cancelled", "lineId": "masa-5-c3d4",
        "quantity": 1,                   // ← İPTAL EDİLEN adet; satır DURUYOR olabilir
        "before": { "quantity": 3, "amounts": { … } } },

      { "op": "removed", "lineId": "masa-5-e5f6",
        "removeReason": "moved",         // 'moved' → DEVİR (storno YOK) · 'deleted' → STORNO
        "movedTo": { "type": "table", "id": "masa-7" },   // satırın TAMAMI ayrıldı
        "before": { … } },

      // ⏳ KISMİ DEVİR — satır KALIYOR, adedi düşüyor: 'removed' HİÇ GELMEZ
      { "op": "updated", "lineId": "masa-5-f220",
        "movedTo": { "type": "table", "id": "masa-7" },
        "movedQuantity": 4,              // ← devredilen adet; bu adede STORNO YAZMA
        "after": { "quantity": 1 } }
    ]
  }
}
before / after satır şekli. İkisi de orders[] içindeki satırla birebir aynıdır (aynı allowlist): lineId, productId, title, quantity, options, lineTotalDecimal, vatRate, amounts, metadata. Yeni bir satır tipi öğrenmene gerek yok — mevcut ayrıştırıcın çalışır.
Kapsam genişledi — artık tüm yazma akışlarında. Önce yalnız masa CRUD yollarındaydı; oysa bloğun asıl gerekçesi bölme/taşımada haksız storno'ydu, yani en riskli operasyonlarda yoktu. Şimdi: kalem ekleme · düzenleme · iptal · indirim · kuver değişikliği · tahsilat ekleme ve iptali · hesap bölme · masa taşıma · birleştirme (tam ve kısmi) · dört eklenti yazma ucu · cari hesaba devir · ⏳ entegrasyon kanalı kalem silme (integration/removeOrder) · e-fatura/e-adisyon senkronu.
⚠️ Soy bilgisinin ulaştığı tek kanal bu bloktur — splitFrom/movedFrom/movedTo alanları orders[] allowlist'inden geçmez. Devirde zincir kapanır: kaynakta removed + movedTo, hedefte created + movedFrom. (Tam taşımada kaynak dokümanı silindiği için delta yoktur — *.deleted zaten movedTo taşır.)
⚠️ movedTo iki girdide gelir: satırın tamamı ayrıldıysa removed + removeReason:"moved"; ⏳ yalnız bir kısmı devredildiyse updated + movedQuantity — satır kalan adetle satışta durmaya devam eder.
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 düşüyor ama artık cancels[]'a void kaydı yazılıyor (kayda girmiş bir pozisyonun izsiz kaybolması DSFinV-K Tz 4.2.3 ve AO §146a ile bağdaşmıyordu). Zarfta görünen fark: eskiden { op: "removed", removeReason: "deleted" }, şimdi { op: "cancelled", quantity: … }. Kuver satırı türev olduğu için dışlanır; void kaydı plugin:<pluginId> aktör damgası taşır.

op sözlüğü

opNe olduTaşıdığı alanlarMali karşılığı
createdBu satışta yeni satırafter · varsa splitFrom / movedFromYeni kayıt — soy alanı varsa devamdır, yeni satış değil
updatedAynı lineId, alanlar değiştibefore + after · ⏳ kısmi devirde movedTo + movedQuantityFark kaydedilir. movedTo varsa düşüşün movedQuantity kadarı devirdir, iptal değildir → o adede storno yazma
cancelledcancels[]'a taşındı — adet kapsamlıquantity (iptal edilen) + beforeNegatif kayıt, yalnız o adet kadar
removedSatır listeden çıktı — sebebi removeReason söylerremoveReason ("moved" | "deleted") + moved ise movedTo (zorunlu) + beforeremoveReason !== "moved" → STORNO; "moved" → devir, storno yok
⚠️ Kırıcı: moved_out kaldırıldı → removed + removeReason. 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 — başka hiçbir yer bu satırı hesaba katmayacak). Fark yalnız opsiyonel bir alanın varlığındaydı ve adın kendisi tüketiciyi yanlış tarafa itiyordu. removed dalının kuralı tek satırdır:
if (c.op === 'removed' && c.removeReason !== 'moved') → STORNO
removeReason çağıranın beyanı değil, movedTo'dan türetilir → hedefsiz bir "moved" yayınlanması yapısal olarak imkânsız. Belirsizlik daima storno tarafına düşer: fazladan storno mali olarak zararsız, eksik storno Unveränderbarkeit ihlalidir.
⚠️ Ama storno kuralı TEK SATIR DEĞİLDİR — updated dalı da var. Kısmi devir removed üretmez: satır satışta kalır (adedi düşer) ve girdi updated + movedTo + movedQuantity olarak gelir. Yalnız tek satırlık kurala bakan tüketici bu düşüşü "iptal edildi" okur — sahada tam olarak bu yaşandı ("4 adet iptal edildi"). Doğru kural:
Adet düşüşü her zaman iptal değildir. updated girdisinde movedTo varsa düşüşün movedQuantity kadarı devirdir (hedef satışta created + movedFrom ile karşılanır). Storno yalnız movedTo olmayan düşüşlere yazılır; movedTo olup movedQuantity gelmeyen durumda (aynı yazımda iptal de var) düşüşün tamamını iptal sayma — iptal edilen adet ayrı bir cancelled girdisinin quantity'sindedir.
SDK: shouldReverse(change) dört dalı da tek yerde kapatır.
Adlandırma kuralı: sözleşme alanına reason adı verilmez. Platformun redaction katmanı reason anahtarını serbest-metin PII sayıp her derinlikte siler; alan önce reason adıyla yazıldı ve PII rızası olmayan kurulumlarda (yani tipik mali eklentide) undefined geliyordu → kural gereği taşınan her satır storno edilirdi. Bu yüzden kök seviyede deleteReason, satır seviyesinde removeReason kullanılır.

Soy alanları

Satır kimliğinin değiştiği her yerde nereden geldiği söyleniyor. Bu alanlar mali doğruluğun taşıyıcısıdır: onlar olmadan yeniden kimliklenmiş bir satır "sil + ekle" görünür.

AlanTipZorunluAçıklama
splitFromstring?–created ile. Satır aynı satışta bölündü; değer kaynak lineId. Ciro bu satışta kalıyor → storno yazma, kaydı devam ettir.
movedFromobject?–created ile. Satır başka bir satıştan geldi — { type, id, lineId } (kaynak hesap + kaynak satır). Kaynak satışta karşılığı bir removed + movedTo girdisidir.
movedToobject?–removed + removeReason: "moved" ile (zorunlu; aksi hâlde alan hiç konmaz). Satır bu satıştan çıktı — { type, id } (hedef hesap). Ciro iadesi değil, sahip değişimi.

movedTo / movedFrom şekli yeni değil — *.deleted olaylarında zaten kullanılan yapı; aynı ayrıştırıcıyı kullanabilirsin.

Tüketici kuralları

En önemlisi: lineChanges bir optimizasyondur, kaynak değildir. Gerçeğin kaynağı orders[] tam durumudur. Blok gelmezse, eksikse ya da anlamadığın bir op içeriyorsa tam durumdan yakınsamaya devam et — bugünkü davranışın zaten doğrudur.
  1. Delta uygulama, doğrula. Kendi kopyanı orders[] ile güncelle; lineChanges'i "ne oldu"yu anlamak ve defterine kayıt düşmek için kullan.
  2. Soy alanlarını her zaman kontrol et. created görüp splitFrom/movedFrom yoksa gerçekten yeni bir satırdır. Varsa, önceki kaydının devamıdır — storno yazma.
  3. cancelled'ı adetle sınırla. quantity alanı iptal edilen adettir; aynı lineId azaltılmış adetle hâlâ aktif olabilir. Satırı komple silme.
  4. removed'ı körü körüne iade sayma — removeReason'a bak. "moved" ise ciro kaybolmadı, sahip değişti (hedef hesapta movedFrom ile aynı satırı bulacaksın); "deleted" ise satır izsiz silindi → storno et.
  5. Bilinmeyen op'u yok say. Sözlük ileride genişleyebilir; tanımadığın değeri atla ve tam durumdan yakınsa — bloğun tamamını atma.
Tüketici tarafı — tam durum + delta birlikte
import { isKnownLineChangeOp, shouldReverse } from '@restomenum/plugin-sdk';

// 1) Kendi kopyanı DAİMA tam durumdan güncelle — delta kaynak değildir.
store.set(accountKey(data), data);

// 2) lineChanges yalnız "ne oldu"yu anlamak ve deftere kayıt düşmek için.
for (const change of data.lineChanges ?? []) {          // alan yoksa döngü hiç dönmez
  if (!isKnownLineChangeOp(change.op)) continue;        // sözlük büyüyebilir → tanımadığını ATLA
  switch (change.op) {
    case 'created':
      // ⚠️ SOY ALANI VARSA YENİ SATIŞ DEĞİL → storno YAZMA, kaydı devam ettir.
      if (change.splitFrom || change.movedFrom) ledger.continue(change);
      else                                     ledger.open(change);
      break;
    case 'updated':
      // ⚠️ ADET DÜŞÜŞÜ HER ZAMAN İPTAL DEĞİL: movedTo varsa düşüşün movedQuantity kadarı DEVİRDİR.
      if (change.movedTo) ledger.transfer(change.lineId, change.movedTo, change.movedQuantity);
      ledger.adjust(change.before, change.after);
      break;
    case 'cancelled': ledger.reverse(change.lineId, change.quantity); break;  // ⚠️ YALNIZ bu adet
    case 'removed':
      if (change.removeReason === 'moved') ledger.transfer(change.lineId, change.movedTo);  // devir
      else                                 ledger.reverse(change.lineId);                   // izsiz silme
      break;
  }

  // Aynı kararı tek çağrıda isteyen için: dört dalı da kapatan yardımcı.
  // if (shouldReverse(change)) ledger.reverse(change.lineId, change.quantity);
}
Sıralama — dikkat. sequence satış düzeyindedir, satır düzeyinde değil. Tek bir mutasyondan çıkan tüm lineChanges girdileri aynı sequence'a aittir ve aralarında sıra yoktur — hepsi o numaradaki durumun parçasıdır. Girdiler arasında zamansal sıra çıkarma.

SDK tarafı

Tipler 3.0.1+ sürümünde: LineChange · LineChangeOp · KnownLineChangeOp · LineMovedFrom · LINE_CHANGE_OPS · isKnownLineChangeOp.

  • before/after mevcut satır tipini yeniden kullanır (OrderLine) — ayrı bir tip üretilmedi.
  • op genişletilebilir bir tiptir: bilinen değerler otomatik tamamlanır, tanınmayan değer ayrıştırmayı kırmaz.
  • data üzerinde katı şema doğrulaması yapma: payload additive büyüyor, bilinmeyen alanı reddeden bir doğrulayıcı canlı entegrasyonu sessizce kırar. SDK'nın parseEnvelope'u da tanımadığı üst-seviye alanları düşürmez (3.0.0+).

Değişmeyenler

Bu tamamen additive bir değişiklik. Hiçbir alan kaldırılmıyor, hiçbir tipin anlamı değişmiyor.

  • Olay adları ve katalog — yeni olay tipi yok; blok mevcut table.updated / packet.updated içinde geliyor.
  • orders[] tam durumu — şekli, anlamı, upsert sözleşmesi aynı.
  • changed alanı — "ne oldu" özeti aynen duruyor; lineChanges onun yerine geçmiyor, ayrıntısını veriyor.
  • Zarf — id, type, version, tenantId, occurredAt, actor, origin, sequence, sequenceScope.
  • İmza ve teslim — HMAC şeması, retry ve yeniden teslim politikası.
  • Redaction — PII allowlist'i ve rıza kuralı; lineChanges satırları orders[] ile aynı allowlist'ten geçer (personel kimliği, maliyet ve reçete verisi bu blokta da yok).
Bugün ne yapman gerekiyor: hiçbir şey. Blok gelmeye başladığında mevcut entegrasyonun aynen çalışmaya devam eder. Satır bazında defter tutuyorsan yukarıdaki beş kuralı uygulayarak kendi diff'ini bırakabilirsin.