Hesap Yaşam Döngüsü ✓ Canlı

Bir masa veya paket hesabı açıldıktan sonra başına gelen her şey: kalem ve ödeme değişiklikleri (*.updated), kapanmış bir satışın geri alınması (*.reopened), hesabın başka bir hesaba devredilmesi (*.deleted) ve kapanmış bir satışın kaydının silinmesi (*.closed_deleted). Sekiz olay tipi, ortak actor atfı ve upsert sözleşmesi.

← Event Kataloğu

Sekiz olay üç aileden oluşur ve masa ile paket tarafında aynı sözleşmeyi paylaşır; yalnız kimlik alanları değişir (tableId/tableName/desing ↔ packetId/entegrasyon/orderCode). Veri kapsamı altısında da orders:read.

Bir hesabın geçebileceği durumlar

  *.created ──▶  A Ç I K   H E S A P  ──▶ *.closed
                     │  ▲   │                  │
        *.updated ───┘  │   └──▶ *.deleted     │
   (kalem · ödeme ·     │        (devir:       │
    indirim · kuver)    │         birleştirme, │
                        │         cari hesap)  │
                        └────── *.reopened ────┘
                          (kapanış kaydını storno et)
AlanTipZorunluAçıklama
table.updated · packet.updateddeğişim–Açık hesabın kalemleri veya ödemeleri değişti. Satır düzeyinde ayrı olay yoktur — her kalem/ödeme işlemi bu olayla bildirilir ve gövde hesabın tam hâlini taşır.
table.reopened · packet.reopenedgeri alma–Kapatılmış bir satış personel tarafından geri açıldı. Yeni bir satış değildir — daha önce *.closed ile bildirilen satışın geri alınmasıdır.
table.closed_deleted · packet.closed_deletedstorno–Kapanmış bir satışın kaydı silindi — devir değil, yok etme. Kapanışta kestiğin belgeyi storno et.
table.deleted · packet.deleteddevir–Hesap kapanmadan ortadan kalktı: satırları başka bir hesaba devredildi. Veri kaybolmaz, sahip değişir.

Atıf: actor

Zarf atıf alanını actor ile taşır (kim yaptı). data'nın dışındadır — veri iznin eksik olup data boşalsa bile kaybolmaz. Kimlik bilinmiyorsa alan hiç konmaz. version yine "1"'dir. (Bir dönem belgelenen origin alanı sözleşmeden çıktı — aşağıya bakın.)

Zarf — actor ile
{
  "id": "evt_7b1c4e2a9f",
  "type": "table.updated",
  "version": "1",
  "environment": "production",
  "tenantId": "vfZ9th0bTgbYnyvuG2jpTN7Nnbb2",
  "occurredAt": 1768818213617,
  "sequence": 7,                        // satışın kaçıncı durum değişikliği (opsiyonel)
  "sequenceScope": "sq_9f3ab27c",       // numaranın geçerli olduğu SATIŞ HATTI (opsiyonel)
  "actor":  { "type": "staff", "userId": "Wl0Huwdx2zcq…", "role": "manager" },
  "data": { /* olaya göre kanonik şekil */ }
}
AlanTipZorunluAçıklama
actor.type"staff" | "plugin" | "system"–Personel (panel/POS) · Eklenti (Callback API) — pluginId ile · Platform (otomatik işlem)
actor.userIdstring?–İşlemi yapan personel (opak). ⚠️ PIN ile seçilir, backend'de doğrulanmaz.
actor.role"manager" | "staff"–Yetki sınıfı — userId ile aynı güven seviyesi.
actor.pluginIdstring?–Yazan eklenti (plugin aktöründe). ✅ doğrulanmış.
origin.deviceId—–KALDIRILDI (19 Ağustos 2026) — platform bu alanı göndermiyor. Kısa süre "doğrulanmış cihaz kimliği" olarak belgelenmişti; mali/kasa atfını buna dayandıran kodu registerId beyanına taşı.
Güven seviyeleri eşit değil. Doğrulanmış tek alan actor.pluginId'dir. actor.userId ve actor.role kasada PIN ile seçilir ve backend'de doğrulanmaz — bunları denetim izi olarak yaz, yetkilendirme kararında kullanma.
Mali atıfta origin.deviceId KULLANMA — alan kaldırıldı. Bir süre burada "mali atıfta bunu tercih et" yazıyordu; o yönlendirme geçersizdir ve platform alanı artık göndermiyor. Gelmeyen bir alandan 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.
Doğrusu: kim → actor.userId (denetim izi; doğrulanmaz), hangi kasa/terminal → senin beyanın (fiscal.de → registerId). Personel/cihaz → kasa eşlemesi eklentinin kendi ayarlarındadır, platformdan türetilmez.

Ad, e-posta ve ham yetki listesi zarfta yoktur; userId opaktır. Bir kişiye çözmek GET /users/get gerektirir (users:read + rıza).

Kalıcı satış kimliği — uuid

⏳ Yakında⏳ Sandbox'ta yayında, production'a çıkmadı.

Bir satış yaşam döngüsü boyunca üç farklı doküman kimliği taşır: açık masa masa-300, geri açılmış masa masa-300*, kapanış kaydı ise bir UUID. Tüketiciler bunları ilişkilendiremediği için aynı satışı üç ayrı kayıt sanıyordu. data.uuid bu bağı kurar.

Kapanış kaydının kimliği uuid'dir. Kapanan masa closedTables/{uuid} altına yazılır. Yüzeyler: table.* · packet.* olayları · tables/open · packets/open.
⏳ Kırıcı: table.closed artık masa slug'ı taşıyor. Aynı satışın akışında tableId iki farklı kavramı taşıyordu — table.updated'da konum slug'ı (masa-901), kapanışta closedTables doküman kimliği (d00b9cad…). Kural kendi içinde tutarlıydı ("olayın işaret ettiği dokümanın kimliği") ama alan adı masayı ima ediyor ve tableId ile gruplayan tüketicide kapanış olayı ayrı düşüyordu. Sektörde bu iki rol ayrı alanlardadır (Square Order.id + location_id, Toast check.guid + table.guid). Artık üç olay da aynı tableId'yi taşır:
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. uuid'si olmayan eski satışlarda (kapanış kaydı oto-kimlik almış) bu bağ tamamen kapanır — bilinçli kabul.
ℹ️ Paket kanalı değişmedi: packet.closed aynen eskisi gibi davranır — packets doküman kimliği payload'da başka yerden gelmediği için ayrı bir karar gerektiriyor.
⚠️ Saha ölçümü (17.08.2026, sandbox): table.closed hâlâ kapanış kaydının kimliğini taşıyordu (masa-904 → masa-904* → 23fc6395-…) — düzeltme dev'de yayında ama sandbox'ta gözlenmedi. Hangi davranışta olursan ol uuid doğru anahtardır.
Anahtarlama kuralı: kendi kaydını uuid ile anahtarla. tableId/packetId adrestir, kimlik değil ve tek satış içinde biçim değiştirir. Sahada ölçüldü: 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 kaldı; yani aynı satışın sipariş kayıtlarıyla fişinin bağı koptu (KassenSichV/DSFinV-K açısından ihlal). uuid'ye geçen ölçümde 20 masa (her biri iki fazlı) ve 13 paket senaryosunun tamamı 1:1 tek anahtarda toplandı. SDK'da accountKey(data) bunu uuid önceliğiyle çözer.
AlanTipZorunluAçıklama
Kapanışkorunur–Kapanış kaydının kimliği olur.
Geri açmakorunur–Eskiden yenileniyordu; artık sabit kalır.
Boş masaya taşımakorunur–Masa bir konum, satış bir kayıttır.
Dolu masaya taşıma (birleştirme)hedefinki–Kaynak *.deleted alır ve movedTo ile hedefe bağlanır.
Eski dokümanlarnull–Alan düşmez, null gelir — "uuid yok" ile "uuid gelmedi" ayırt edilebilir.
Satır uuid'leri ayrı konudur. Doküman kimliği kalıcıdır; satır dizilerindeki (orders/payments/cancels) uuid alanları geri açmada yenilenmeye devam eder — satırı lineId ile izle.

Upsert sözleşmesi

data, değişimden sonraki hesabın tam hâlidir — delta değil. orders[] ve payments[] her zaman tam listedir; bir satır listeden düştüyse iptal edilmiş veya taşınmıştır. Satırları kalıcı lineId ile eşle.

Tüketici tarafı — doğru
// DOĞRU — kendi kopyanı tamamen değiştir
store.set(data.tableId ?? data.packetId, data);
Yanlış
// YANLIŞ — satır delta'sı uygulamaya çalışma; bu olay onu taşımaz
store.addLine(data.orders[data.orders.length - 1]);

Bu sözleşme sayesinde kaybolan veya sırası bozulan tek bir olay kalıcı hasar vermez — bir sonraki olay durumu yerine oturtur. Sıra garanti edilmediği için zarftaki sequence + sequenceScope alanlarıyla bayat teslimi ele (alanlar gelmiyorsa occurredAt karşılaştır):

Geç gelen eski olayı ele
// Sıra garanti edilmez → zarftaki sequence + sequenceScope ile ele
import { sequenceKey, sequenceVerdict, advanceSequenceCursor } from '@restomenum/plugin-sdk';

const seqKey = sequenceKey(envelope.tenantId, envelope);  // (tenantId, sequenceScope) — TEK SLOT YASAK
const cursor = seqKey ? await ledger.get(seqKey) : null;

switch (sequenceVerdict(envelope, cursor)) {
  case 'stale':    return;   // aynı hatta daha KÜÇÜK numara → bayat
  case 'terminal': return;   // satış sonlanmış; *.updated onu diriltmez
  default: break;            // 'apply' | 'unsequenced' (alan yok → occurredAt karşılaştır)
}

store.set(key, envelope.data);
if (seqKey) await ledger.set(seqKey, advanceSequenceCursor(cursor, envelope));  // ≥ 7 gün sakla
Defterini (tenantId, sequenceScope) ile anahtarla ve terminal olaydan sonra hemen silme (öneri: 7 gün) — beş kuralın tamamı: Olay sırası. Atlanan teslimde de 2xx dön.

*.updated — değişim olayları

Açık hesabın kalemleri veya ödemeleri değişti. Kalem ekleme, düzenleme, iptal, indirim, kuver değişikliği, ödeme alma ve iptali, masa taşıma ve bölme, ÖKC senkronu — hepsi bu iki olaydan biriyle bildirilir.

data.changed değerleri

Filtrelemen için bir ipucu. null olabilir ve liste ileride büyüyebilir → bilinmeyen bir değer görürsen olayı yine de işle; gövde sözleşmesi değere göre değişmez. switch yazıyorsan default dalı olayı düşürmemeli.

AlanTipZorunluAçıklama
order_addedstring–Hesaba yeni kalem yazıldı
order_updatedstring–Kalem düzenlendi — adet, not, ekstra, seçenek
order_cancelledstring–Kalem iptal edildi, cancels[]’a taşındı
order_discountedstring–Satır indirimi değişti
order_movedstring–Kalemler hesaplar arası taşındı veya hesap bölündü
payment_addedstring–Ödeme alındı — kısmi tahsilat dahil; update-payments mode:"append" (ekleme) de bunu yayar
payment_updatedstring–Ödeme satırları TAM DEĞİŞTİRMEYLE güncellendi — update-payments mode:"replace" (varsayılan); ekleme modu bunu YAYMAZ
payment_cancelledstring–Ödeme satırı iptal edildi
kuver_changedstring–Kuver oranı değişti, kuver satırı ve toplam yeniden kuruldu
fiscal_syncedstring–ÖKC veya e-fatura senkronu kalemleri damgaladı
payment_added ↔ payment_updated — hangisi ne zaman? Ayrım yazma modundandır: update-payments mode:"append" (ekleme — 🧪 yalnız sandbox) payment_added yayar; mode:"replace" (varsayılan, tam değiştirme) payment_updated yayar. Ekleme modunda payment_updated yayılsaydı "tüm satırlar değişti, yeniden diff'le" demiş olurduk. Tekrar gönderimde (yanıtta replayed: true) hiçbir olay yayılmaz — doküman değişmemiştir.
table.updated — ödeme alındı (gerçek gövde)
{
  "id": "evt_7b1c4e2a9f",
  "type": "table.updated",
  "version": "1",
  "actor": { "type": "staff" },
  "environment": "production",
  "tenantId": "<tenantId>",
  "occurredAt": 1768818213617,
  "sequence": 7,
  "sequenceScope": "sq_9f3ab27c",
  "data": {
    "tableId": "masa-5",
    "tableName": "Masa 5",
    "docNo": 142,
    "type": "dine_in",
    "desing": "Salon",
    "location": "Masa 5",
    "personCount": 2,
    "currency": "TRY",
    "amountExponent": 2,
    "orders": [
      { "id": "masa-5-a1f2", "lineId": "masa-5-a1f2", "productId": "p_menemen", "title": "Menemen", "quantity": 2, "options": [], "extraDecimal": 0, "discountDecimal": 0, "note": "", "lineTotalDecimal": 360, "vatRate": 10, "amounts": { "base": 36000, "options": 0, "extra": 0, "discount": 0, "timer": 0, "gross": 36000, "net": 32727, "vat": 3273 }, "metadata": null },
      { "id": "masa-5-b7c3", "lineId": "masa-5-b7c3", "productId": "p_turk_kahve", "title": "Türk Kahvesi", "quantity": 1, "options": [{ "id": "1693060234511", "title": "Az şekerli", "price": 0, "vatRate": 10 }], "extraDecimal": 0, "discountDecimal": 20, "note": "az şekerli", "lineTotalDecimal": 70, "vatRate": 10, "amounts": { "base": 9000, "options": 0, "extra": 0, "discount": 2000, "timer": 0, "gross": 7000, "net": 6364, "vat": 636 }, "metadata": null }
    ],
    "cancels": [
      { "id": "masa-5-c9d1", "lineId": "masa-5-c9d1", "productId": "p_ayran", "title": "Ayran", "quantity": 1, "options": [], "extraDecimal": 0, "discountDecimal": 0, "note": "", "lineTotalDecimal": 45, "vatRate": 1, "amounts": { "base": 4500, "options": 0, "extra": 0, "discount": 0, "timer": 0, "gross": 4500, "net": 4455, "vat": 45 }, "metadata": null, "reason": "Müşteri vazgeçti", "cancelledAt": 1768817913617 }
    ],
    "payments": [
      { "lineId": "masa-5-pay-1", "methodId": "pm_nakit", "title": "Nakit", "amount": 200, "cash": true, "isDiscount": false, "metadata": null }
    ],
    "total": 430,
    "paid": 200,
    "totalDiscount": 0,
    "changed": "payment_added"
  }
}
packet.updated — eklenti kalem güncelledi (gerçek gövde)
{
  "id": "evt_2d8f61a0c3",
  "type": "packet.updated",
  "version": "1",
  "actor": { "type": "plugin", "pluginId": "pl_kiosk" },
  "environment": "production",
  "tenantId": "<tenantId>",
  "occurredAt": 1768818214617,
  "sequence": 4,
  "sequenceScope": "sq_2c14d8e0",
  "data": {
    "packetId": "pkt-9931",
    "docNo": 87,
    "type": "delivery",
    "currency": "TRY",
    "amountExponent": 2,
    "customer": {
      "id": "5551112233",
      "name": "Ali Yılmaz",
      "phone": "5551112233",
      "address": "Bağdat Cad. No:12 D:4",
      "region": "Kadıköy"
    },
    "orders": [
      { "id": "pkt-9931-x1", "lineId": "pkt-9931-x1", "productId": "p_lahmacun", "title": "Lahmacun", "quantity": 1, "options": [], "extraDecimal": 0, "discountDecimal": 0, "note": "", "lineTotalDecimal": 120, "vatRate": 10, "amounts": { "base": 12000, "options": 0, "extra": 0, "discount": 0, "timer": 0, "gross": 12000, "net": 10909, "vat": 1091 }, "metadata": [ { "key": "tse_ref", "value": "TSE-88213", "by": "kiosk" } ] }
    ],
    "cancels": [],
    "payments": [
      { "lineId": "pkt-9931-pay-1", "methodId": "pm_kredi", "title": "Kredi Kartı", "amount": 120, "cash": false, "isDiscount": false, "metadata": null }
    ],
    "entegrasyon": "packet",
    "scheduledDate": null,
    "note": "Zili çalmayın",
    "total": 120,
    "paid": 120,
    "paymentNote": "Kapıda kredi kartı",
    "orderCode": "TY-4471",
    "isScheduled": false,
    "totalDiscount": 0,
    "changed": "order_updated"
  }
}
lineChanges — satır düzeyi delta. Bu iki olayın gövdesi "hangi satır değişti, ne kadarı iptal edildi, hangi yeni satır hangisinin devamı" bloğunu da taşır (opsiyonel: yalnız satır düzeyinde bir şey değiştiğinde konur — ödeme eklenmesinde gelmez). Tam durum (orders[]) sözleşmesi değişmedi; blok bir optimizasyondur, kaynak değildir. Satır bazında defter tutuyorsan (mali eklenti) kendi diff'inin iki bilinen yanılmasını kapatır: yeniden kimliklenen satır ve adet kapsamlı iptal.

Paket tarafında ek alanlar: packetId, entegrasyon, orderCode, isScheduled, scheduledDate, paymentNote. cancels[] iptal edilmiş kalemleri taşır: aktif satır şekli + reason (serbest metin → PII rıza kapısına tabi) + cancelledAt.

⏳ İptal kayıtlarında indirim ve ekstra artık ORANSAL. İptal kaydı discountDecimal: 0 ile yazılıyor, extraDecimal ise hiç bölünmüyordu (satırın tamamının ekstrası 1 adetlik iptal kaydına da geçiyor, kalan satırda da duruyordu) → indirimli kalemde yayınlanan storno tutarı gerçekte tahsil edilenden fazlaydı, ekstralı kalemde ekstra çiftleniyordu. 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. cancels[] okuyup storno yazıyorsan kaydın tutarı artık doğrudur.
⏳ Yeni: cancelPayments[] — iptal edilen tahsilatlar. payments[] ile birebir aynı şekil. Satır void'i (cancels) yayınlanırken ödeme void'inin yayınlanmaması doğrudan bir asimetriydi: iptal edilmiş tahsilat tüketiciye hiç görünmüyor, defterdeki "ödenmiş" tutar POS'takinden sapıyordu. ⚠️ paid aktif ödemelerin toplamıdır — bu liste ona dahil değildir.

*.closed — kapanış anı satır tutarı (lineTotalDecimal)

⏳ Kapanan satışın kaydındaki (closedTables) her kalem artık orders[].lineTotal taşır: kapanış anındaki satır tutarı — mühür gibi, sonradan hesaplanmaz.

İleriye dönük alandır. Daha önce kapanmış kayıtlarda yoktur ve geriye dönük doldurulamaz → eski kapanışları okurken alanın yokluğunu hata sayma, kendi hesabına düş.

*.reopened — geri alma olayları

Kapatılmış bir satış personel tarafından geri açıldı — yanlış kapatma, ek sipariş, ödeme düzeltmesi.

⚠️ "Kapanmış satış geri alındı" TEK bir olay değildir — ÜÇ ayrı yoldan gelir. Yalnız table.reopened'a abone olan bir mali eklenti diğer iki yolu hiç görmez ve kestiği belge ayakta kalır. Üçünü de ele al:
Kullanıcının yaptığıYayılan olayNe anlama gelir
Kapanmış masayı geri açtıtable.reopenedSatış tekrar açık; yeniden kapanınca table.closed ile yeni kayıt gelir.
Kapanmış entegrasyon satışını geri açtıpacket.reopenedtable.reopened YAYILMAZ — aşağıdaki koşullu emit kuralı.
Kapanmış fişi silditable.closed_deleted
packet.closed_deleted
Satış geri gelmez; dayanak ortadan kalkar → ilgili bölüm.
Mali tepkinin ne olacağı kendi kaydını nasıl kurduğuna bağlıdır (storno, ters kayıt, ya da hiçbir şey) — burada tek zorunluluk üç yolu da ele almak, tek bir tepki biçimi dayatılmıyor.

⚠️ Üçünde de olayın hiç gelmeme ihtimaline karşı uzlaştırma yap. Bir geri almanın sana ulaşmaması sessiz olabilir (ne hata, ne uyarı) ve mali sonucu gün sonu kapandıktan sonra düzeltilemez. En kesin dedektör: aynı satış hattında ikinci bir *.closed aldıysan ve arada geri alma işlemediysen, o geri alma sana ulaşmamıştır → kayıp olay tespiti ve uzlaştırma.
Koşullu emit: entegrasyon kaynaklı satış geri açılınca table.reopened YAYILMAZ. Geri açma, satışın entegrasyon alanı doluysa masa kanalından değil paket kanalından bildirilir: packets/{id} yeniden yaratılır ve packet.reopened yayılır. Yani teslim platformu (Getir, Yemeksepeti, Trendyol, Migros) siparişlerinde geri açmayı görmek için packet.reopened'a da abone olmalısın — yalnız table.* dinleyen bir eklenti bu satışların geri açılmasını hiç görmez.
Mali ve muhasebe eklentileri için kritik. Bu olay *.created olarak gelseydi kestiğin fişin üzerine ikinci bir fiş keserdin. Doğru davranış: kapanışta oluşturduğun kaydı storno et, hesabı tekrar açık say; hesap yeniden kapandığında *.closed ile yeni kayıt kes.
Storno akışı
// ⚠️ YENİ SATIŞ DEĞİL — ikinci fiş kesme
switch (envelope.type) {
  case 'table.reopened':
  case 'packet.reopened': {
    const from = envelope.data.reopenedFrom;
    if (!from) break;                       // zincir çözülemedi → HİÇBİR kaydı storno etme
    await ledger.reverse(from.saleId);      // 1) kapanış kaydını storno et
    store.set(accountKey(envelope.data), envelope.data);  // 2) hesabı tekrar açık say
    break;                                  // 3) yeniden kapanınca *.closed → YENİ kayıt
  }
}
AlanTipZorunluAçıklama
reopenedFromobject | null–{ saleId, docNo, closedAt }. saleId, kapanış kaydının kimliğidir → storno edeceğin kaydı bununla bul. ⏳ Masada artık uuid ile eşleşir, kapanış olayının tableId'si ile değil (o alan artık masa slug'ı). Pakette değişmedi: packet.closed'ın packetId'si ile birebir aynı.
reopenedFrom: null—–Zincir çözülemedi. Hesabı yeni bir açık hesap gibi ele al ama hiçbir kapanmış kaydı storno etme — hangisi olduğunu bilmiyorsun.
occurredAtnumber–Geri açılma anıdır, orijinal siparişin oluşturulma zamanı değil.
Kimlik davranışı masada ve pakette FARKLIDIR. Geri açılan masa yeni bir kimlik alır (Masa 5 → Masa 5*) → payload'daki tableId kapanıştakinden farklıdır, bağ yalnız reopenedFrom üzerinden kurulur. Pakette kimlik korunur: data.packetId = reopenedFrom.saleId.
Paket dokümanı aynı kimlikle yeniden yazılsa da platform bunu packet.created olarak göndermez — geri alınan satış yeni sipariş sanılmasın diye tip burada ayrılır.
table.reopened (gerçek gövde)
{
  "id": "evt_a04c7de915",
  "type": "table.reopened",
  "version": "1",
  "actor": { "type": "staff" },
  "environment": "production",
  "tenantId": "<tenantId>",
  "occurredAt": 1768818215617,
  "sequence": 9,
  "sequenceScope": "sq_9f3ab27c",
  "data": {
    "tableId": "masa-5-yildiz",
    "tableName": "Masa 5*",
    "docNo": 142,
    "type": "dine_in",
    "desing": "Salon",
    "location": "Masa 5",
    "personCount": 2,
    "currency": "TRY",
    "amountExponent": 2,
    "orders": [
      { "id": "masa-5-a1f2", "lineId": "masa-5-a1f2", "productId": "p_menemen", "title": "Menemen", "quantity": 2, "options": [], "extraDecimal": 0, "discountDecimal": 0, "note": "", "lineTotalDecimal": 360, "vatRate": 10, "amounts": { "base": 36000, "options": 0, "extra": 0, "discount": 0, "timer": 0, "gross": 36000, "net": 32727, "vat": 3273 }, "metadata": null },
      { "id": "masa-5-b7c3", "lineId": "masa-5-b7c3", "productId": "p_turk_kahve", "title": "Türk Kahvesi", "quantity": 1, "options": [{ "id": "1693060234511", "title": "Az şekerli", "price": 0, "vatRate": 10 }], "extraDecimal": 0, "discountDecimal": 20, "note": "az şekerli", "lineTotalDecimal": 70, "vatRate": 10, "amounts": { "base": 9000, "options": 0, "extra": 0, "discount": 2000, "timer": 0, "gross": 7000, "net": 6364, "vat": 636 }, "metadata": null }
    ],
    "cancels": [
      { "id": "masa-5-c9d1", "lineId": "masa-5-c9d1", "productId": "p_ayran", "title": "Ayran", "quantity": 1, "options": [], "extraDecimal": 0, "discountDecimal": 0, "note": "", "lineTotalDecimal": 45, "vatRate": 1, "amounts": { "base": 4500, "options": 0, "extra": 0, "discount": 0, "timer": 0, "gross": 4500, "net": 4455, "vat": 45 }, "metadata": null, "reason": "Müşteri vazgeçti", "cancelledAt": 1768817913617 }
    ],
    "payments": [
      { "lineId": "masa-5-pay-1", "methodId": "pm_nakit", "title": "Nakit", "amount": 200, "cash": true, "isDiscount": false, "metadata": null }
    ],
    "total": 430,
    "paid": 200,
    "totalDiscount": 0,
    "reopenedFrom": {
      "saleId": "cl_8f2ad41b",
      "docNo": 142,
      "closedAt": 1768814613617
    }
  }
}
packet.reopened — yalnız farklı alanlar (kısaltıldı)
{
  "id": "evt_c51b9027ea",
  "type": "packet.reopened",
  "version": "1",
  "actor": { "type": "staff" },
  "environment": "production",
  "tenantId": "<tenantId>",
  "occurredAt": 1768818216617,
  "sequence": 6,
  "sequenceScope": "sq_2c14d8e0",
  "data": {
    "packetId": "pkt-9931",
    "docNo": 87,
    "type": "delivery",
    "currency": "TRY",
    "amountExponent": 2,
    "orderCode": "TY-4471",
    "entegrasyon": "packet",
    "total": 120,
    "paid": 120,
    "totalDiscount": 0,
    "reopenedFrom": {
      "saleId": "pkt-9931",
      "docNo": 87,
      "closedAt": 1768816413617
    }
  }
}

*.deleted — devir olayları

Bir hesap kapanmadan ortadan kalkabilir: satırları başka bir hesaba devredilir. Veri kaybolmaz, sahip değişir.

Neden ayrı bir tip? *.updated göndermek "hâlâ açık ve boş" demek olurdu; *.closed göndermek "satış kapandı" demek — ikincisi taşınan hesabı satış sayıp ciroyu çiftler. Devir bu yüzden kendi tipini alır.
AlanTipZorunluAçıklama
data (gövde)object–*.updated ile aynı kanonik şekil — kaldırılmadan önceki son hâl — artı deleteReason ve movedTo.
movedToobject | null–{ type: "table" | "packet" | "customer", id }. null ise hedef çözülemedi: kaydı kapat ama satırları bir yere bağlama.
Kısmi devir—–Satırların bir kısmı kalıyorsa hesap durur: *.deleted değil, *.updated (changed: "order_moved") gelir.

data.deleteReason değerleri

AlanTipZorunluAçıklama
mergedmovedTo.type: table | movedTo.type: packet–İki hesap birleştirildi — masa veya paket birleştirme
moved_to_tablemovedTo.type: table–Satırlar boş veya yeni bir masaya taşındı
moved_to_accountmovedTo.type: customer–Satırlar müşterinin cari hesabına aktarıldı
Alan adı reason değil: gizlilik kırpması reason anahtarını serbest metin sayıp her derinlikte siler — o kural cancels[].reason için doğrudur. Aynı adı taşısaydı bu whitelist değeri, PII rızası olmayan tüketicilerde sessizce düşerdi.
table.deleted — masa birleştirme (gerçek gövde)
{
  "id": "evt_3e6a82f7bd",
  "type": "table.deleted",
  "version": "1",
  "actor": { "type": "staff" },
  "environment": "production",
  "tenantId": "<tenantId>",
  "occurredAt": 1768818217617,
  "sequence": 11,
  "sequenceScope": "sq_9f3ab27c",
  "data": {
    "tableId": "masa-5",
    "tableName": "Masa 5",
    "docNo": 142,
    "type": "dine_in",
    "desing": "Salon",
    "location": "Masa 5",
    "personCount": 2,
    "currency": "TRY",
    "amountExponent": 2,
    "orders": [ "… kaldırılmadan önceki tam liste …" ],
    "cancels": [ "…" ],
    "payments": [ "…" ],
    "total": 430,
    "paid": 200,
    "totalDiscount": 0,
    "deleteReason": "merged",
    "movedTo": {
      "type": "table",
      "id": "masa-8"
    }
  }
}
packet.deleted — cari hesaba aktarma (gerçek gövde)
{
  "id": "evt_9f0d3b6c41",
  "type": "packet.deleted",
  "version": "1",
  "actor": { "type": "staff" },
  "environment": "production",
  "tenantId": "<tenantId>",
  "occurredAt": 1768818218617,
  "sequence": 8,
  "sequenceScope": "sq_2c14d8e0",
  "data": {
    "packetId": "pkt-9931",
    "docNo": 87,
    "type": "delivery",
    "currency": "TRY",
    "amountExponent": 2,
    "customer": {
      "id": "5551112233",
      "name": "Ali Yılmaz",
      "phone": "5551112233",
      "address": "Bağdat Cad. No:12 D:4",
      "region": "Kadıköy"
    },
    "orders": [
      { "id": "pkt-9931-x1", "lineId": "pkt-9931-x1", "productId": "p_lahmacun", "title": "Lahmacun", "quantity": 1, "options": [], "extraDecimal": 0, "discountDecimal": 0, "note": "", "lineTotalDecimal": 120, "vatRate": 10, "amounts": { "base": 12000, "options": 0, "extra": 0, "discount": 0, "timer": 0, "gross": 12000, "net": 10909, "vat": 1091 }, "metadata": [ { "key": "tse_ref", "value": "TSE-88213", "by": "kiosk" } ] }
    ],
    "cancels": [],
    "payments": [
      { "lineId": "pkt-9931-pay-1", "methodId": "pm_kredi", "title": "Kredi Kartı", "amount": 120, "cash": false, "isDiscount": false, "metadata": null }
    ],
    "entegrasyon": "packet",
    "scheduledDate": null,
    "note": "Zili çalmayın",
    "total": 120,
    "paid": 120,
    "paymentNote": "Kapıda kredi kartı",
    "orderCode": "TY-4471",
    "isScheduled": false,
    "totalDiscount": 0,
    "deleteReason": "moved_to_account",
    "movedTo": {
      "type": "customer",
      "id": "5551112233"
    }
  }
}

*.closed_deleted — kapanmış satışın kaydı silindi

*.deleted ile karıştırma — bunlar 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 — kapanışta kestiğin belgenin dayanağı ortadan kalkar.

Mali ve muhasebe eklentileri için bu bir storno sinyalidir: *.closed ile bir kayıt oluşturduysan (fiş, fatura, ciro satırı) bu olayda onu storno et. *.reopened'dan farkı: orada satış tekrar açılır ve yeniden kapanınca yeni bir kayıt gelir; burada satış geri gelmez.

AlanTipZorunluAçıklama
saleIdstring–Silinen satışın kimliği — kapanış kaydının kimliği. Storno edeceğin kaydı bununla bul. ⏳ Masada uuid ile eşleşir (kapanış olayının tableId'si artık masa slug'ıdır); pakette packet.closed'ın packetId'si ile birebir aynı.
channelstring–Kanonik kanal — "TABLE" / "PACKET".
statusstring | null–Satışın silinme anındaki durumu (gözlenen değer: "COMPLETED").
closedAtnumber–Satışın KAPANDIĞI an (epoch ms).
deletedAtnumber–Kaydın SİLİNDİĞİ an (epoch ms).
orders[] · payments[]array–Silinmeden önceki tam kayıt. ⚠️ Kayıt silindiği için bunu tables/get / packets/get ile artık çekemezsin — arşivin bu gövdedir, sakla.

⚠️ Gövde hesap yaşam döngüsünün diğer beş olayından farklıdır: tableId/packetId yoktur, kimlik saleId'dedir; cancels[], total/paid gibi hesap alanları da bu gövdede yer almaz.

table.closed_deleted
{
  "id": "evt_<uuid>",
  "type": "table.closed_deleted",
  "version": "1",
  "actor": { "type": "staff" },
  "environment": "production",
  "tenantId": "<tenantId>",
  "occurredAt": 1768820000000,
  "sequence": 14,
  "sequenceScope": "sq_c47f10d9",
  "data": {
    "saleId": "9f3c1a7e-…",
    "channel": "TABLE",
    "status": "COMPLETED",
    "closedAt": 1768810000000,
    "deletedAt": 1768820000000,
    "currency": "TRY",
    "amountExponent": 2,
    "orders": [ "… silinmeden önceki tam kayıt …" ],
    "payments": [ "…" ]
  }
}
packet.closed_deleted — aynı sözleşme, kanal PACKET
{
  "id": "evt_<uuid>",
  "type": "packet.closed_deleted",
  "version": "1",
  "actor": { "type": "staff" },
  "environment": "production",
  "tenantId": "<tenantId>",
  "occurredAt": 1768820000000,
  "sequence": 12,
  "sequenceScope": "sq_e08b52a3",
  "data": {
    "saleId": "9f3c1a7e-…",
    "channel": "PACKET",
    "status": "COMPLETED",
    "closedAt": 1768810000000,
    "deletedAt": 1768820000000,
    "currency": "TRY",
    "amountExponent": 2,
    "orders": [ "… silinmeden önceki tam kayıt …" ],
    "payments": [ "…" ]
  }
}

Döngü koruması (echo) — SORUMLULUK SENDE

Kendi yazdığın olay SANA DA gelir. Callback API ile bir hesabı güncellediğinde o değişikliğin olayı sana da teslim edilir — platform kaynağa göre eleme yapmaz (Stripe, Shopify, Slack ve GitHub ile aynı davranış). Yaz → olay al → tekrar yaz sonsuz döngüsüne girmemek senin sorumluluğunda: handler'ının en başında kendi yazımını ele.
Tüketici tarafı — handler'ın İLK satırı
import { isOwnEcho } from '@restomenum/plugin-sdk';

// Handler'ın İLK satırı — bu olmadan kendi yazdığın olay seni tekrar tetikler.
if (isOwnEcho(envelope, MY_PLUGIN_ID)) return;

// SDK kullanmıyorsan birebir aynısı:
// if (event.actor?.type === "plugin" && event.actor.pluginId === MY_PLUGIN_ID) return;
id dedup'u bunun yerine geçmez. Dedup aynı olayın tekrar teslimini eler; echo ise farklı id'li yeni bir olaydır — dedup onu durdurmaz.
actor hiç gelmemişse (kaynak bilinmiyor) olayı kendine mal etme; isOwnEcho bu durumda false döner.

Kendi yazımının sunucuda nasıl sonuçlandığını uç yanıtından (total, paid) veya tables/get ile packets/get'ten de öğrenebilirsin — fiyat otoriter alınır, kuver yeniden enjekte edilir, korunan satırların durumu taşınır.

Kapsam ve rıza: aynı olay, üç farklı gövde

Aşağıdakiler aynı packet.updated olayının üç kuruluma nasıl gittiğidir. Zarf her üçünde de aynı; değişen yalnız data.

1. orders:read yok

Olayın varlığını bilirsin, içeriğini almazsın.

data tamamen boş
{
  "id": "evt_2d8f61a0c3",
  "type": "packet.updated",
  "version": "1",
  "actor": { "type": "plugin", "pluginId": "pl_kiosk" },
  "environment": "production",
  "tenantId": "<tenantId>",
  "occurredAt": 1768818214617,
  "data": {}
}

2. orders:read var, PII rızası yok

Müşteri nesnesi id ve region'a daralır; serbest metin alanları (note, paymentNote) düşer. Satır metadata'sı kalır — o müşteri verisi değil, eklentiler arası korelasyon verisidir.

data kırpılmış
{
  "data": {
    "packetId": "pkt-9931",
    "docNo": 87,
    "type": "delivery",
    "currency": "TRY",
    "amountExponent": 2,
    "customer": {
      "id": "5551112233",
      "region": "Kadıköy"
    },
    "orders": [
      {
        "id": "pkt-9931-x1",
        "lineId": "pkt-9931-x1",
        "productId": "p_lahmacun",
        "title": "Lahmacun",
        "quantity": 1,
        "options": [],
        "extraDecimal": 0,
        "discountDecimal": 0,
        "lineTotalDecimal": 120,
        "vatRate": 10,
        "amounts": { "base": 12000, "options": 0, "extra": 0, "discount": 0, "timer": 0, "gross": 12000, "net": 10909, "vat": 1091 },
        "metadata": [ { "key": "tse_ref", "value": "TSE-88213", "by": "kiosk" } ]
      }
    ],
    "cancels": [],
    "payments": [
      { "lineId": "pkt-9931-pay-1", "methodId": "pm_kredi", "title": "Kredi Kartı", "amount": 120, "cash": false, "isDiscount": false, "metadata": null }
    ],
    "entegrasyon": "packet",
    "scheduledDate": null,
    "total": 120,
    "paid": 120,
    "orderCode": "TY-4471",
    "isScheduled": false,
    "totalDiscount": 0,
    "changed": "order_updated"
  }
}

3. orders:read + customers:read + rıza

Tam gövde — yukarıdaki packet.updated örneğinin aynısı.

Bu olayların atılmadığı durumlar

Daha özel bir olay zaten ateşliyorsa çift bildirim yapılmaz.

AlanTipZorunluAçıklama
Dine-in masa açılışıevent–table.created
Paket oluşturma veya birleştirmeevent–packet.created
Hesap kapanışıevent–table.closed · packet.closed
Paket tam iptalievent–packet.cancelled

Ayrıca kanonik gövdeye yansımayan yazımlar olay üretmez. Örneğin bir satırın "hazır" veya "teslim edildi" mutfak damgası gövdenin alan listesinde olmadığı için, o işlem senin açından gözlemlenebilir bir değişim değildir ve bilerek gönderilmez.

Abonelik

manifest
{
  "events": [
    "table.updated",  "table.reopened",  "table.deleted",  "table.closed_deleted",
    "packet.updated", "packet.reopened", "packet.deleted", "packet.closed_deleted"
  ],
  "requestedScopes": [
    "events:subscribe",
    "orders:read",
    "customers:read"
  ]
}

events:subscribe olmadan abonelik yok sayılır. Veri kapsamı altı olayda da orders:read; müşteri alanları ve serbest metin için ek olarak customers:read ve tenant'ın açık PII rızası gerekir.

En iyi pratikler. İmzayı ham gövde üzerinden doğrula ve geçersizse 401 dön (imza şeması); zarf id'si ile dedup et (at-least-once → aynı id tekrar gelebilir); gövdedeki tenantId'nin sana ait bir kuruluma karşılık geldiğini doğrula; 2xx dön ve ağır işi kuyruğa al (yavaş yanıt retry/dead-letter'a düşer — teslim sağlığı). Tanımadığın bir event tipine 200 dön.