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.
tableId/tableName/desing ↔ packetId/entegrasyon/orderCode). Veri kapsamı altısında da orders:read. *.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)| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| table.updated · packet.updated | değ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.reopened | geri 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_deleted | storno | – | Kapanmış bir satışın kaydı silindi — devir değil, yok etme. Kapanışta kestiğin belgeyi storno et. |
| table.deleted · packet.deleted | devir | – | Hesap kapanmadan ortadan kalktı: satırları başka bir hesaba devredildi. Veri kaybolmaz, sahip değişir. |
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.)
{
"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 */ }
}| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| actor.type | "staff" | "plugin" | "system" | – | Personel (panel/POS) · Eklenti (Callback API) — pluginId ile · Platform (otomatik işlem) |
| actor.userId | string? | – | İş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.pluginId | string? | – | 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şı. |
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.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.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).
⏳ 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.
uuid'dir. Kapanan masa closedTables/{uuid} altına yazılır. Yüzeyler: table.* · packet.* olayları · tables/open · packets/open.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*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.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.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.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.| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| Kapanış | korunur | – | Kapanış kaydının kimliği olur. |
| Geri açma | korunur | – | Eskiden yenileniyordu; artık sabit kalır. |
| Boş masaya taşıma | korunur | – | 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ümanlar | null | – | Alan düşmez, null gelir — "uuid yok" ile "uuid gelmedi" ayırt edilebilir. |
orders/payments/cancels) uuid alanları geri açmada yenilenmeye devam eder — satırı lineId ile izle.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.
// DOĞRU — kendi kopyanı tamamen değiştir store.set(data.tableId ?? data.packetId, data);
// 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):
// 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(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.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ğerleriFiltrelemen 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.
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| order_added | string | – | Hesaba yeni kalem yazıldı |
| order_updated | string | – | Kalem düzenlendi — adet, not, ekstra, seçenek |
| order_cancelled | string | – | Kalem iptal edildi, cancels[]’a taşındı |
| order_discounted | string | – | Satır indirimi değişti |
| order_moved | string | – | Kalemler hesaplar arası taşındı veya hesap bölündü |
| payment_added | string | – | Ödeme alındı — kısmi tahsilat dahil; update-payments mode:"append" (ekleme) de bunu yayar |
| payment_updated | string | – | Ödeme satırları TAM DEĞİŞTİRMEYLE güncellendi — update-payments mode:"replace" (varsayılan); ekleme modu bunu YAYMAZ |
| payment_cancelled | string | – | Ödeme satırı iptal edildi |
| kuver_changed | string | – | Kuver oranı değişti, kuver satırı ve toplam yeniden kuruldu |
| fiscal_synced | string | – | Ö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.{
"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"
}
}{
"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.
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.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.⏳ 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.
Kapatılmış bir satış personel tarafından geri açıldı — yanlış kapatma, ek sipariş, ödeme düzeltmesi.
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 olay | Ne anlama gelir |
|---|---|---|
| Kapanmış masayı geri açtı | table.reopened | Satış tekrar açık; yeniden kapanınca table.closed ile yeni kayıt gelir. |
| Kapanmış entegrasyon satışını geri açtı | packet.reopened | table.reopened YAYILMAZ — aşağıdaki koşullu emit kuralı. |
| Kapanmış fişi sildi | table.closed_deleted packet.closed_deleted | Satış geri gelmez; dayanak ortadan kalkar → ilgili bölüm. |
*.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.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.*.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.// ⚠️ 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
}
}| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| reopenedFrom | object | 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. |
| occurredAt | number | – | Geri açılma anıdır, orijinal siparişin oluşturulma zamanı değil. |
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.packet.created olarak göndermez — geri alınan satış yeni sipariş sanılmasın diye tip burada ayrılır.{
"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
}
}
}{
"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
}
}
}Bir hesap kapanmadan ortadan kalkabilir: satırları başka bir hesaba devredilir. Veri kaybolmaz, sahip değişir.
*.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.| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| data (gövde) | object | – | *.updated ile aynı kanonik şekil — kaldırılmadan önceki son hâl — artı deleteReason ve movedTo. |
| movedTo | object | 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| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| merged | movedTo.type: table | movedTo.type: packet | – | İki hesap birleştirildi — masa veya paket birleştirme |
| moved_to_table | movedTo.type: table | – | Satırlar boş veya yeni bir masaya taşındı |
| moved_to_account | movedTo.type: customer | – | Satırlar müşterinin cari hesabına aktarıldı |
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.{
"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"
}
}
}{
"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"
}
}
}*.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.
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| saleId | string | – | 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ı. |
| channel | string | – | Kanonik kanal — "TABLE" / "PACKET". |
| status | string | null | – | Satışın silinme anındaki durumu (gözlenen değer: "COMPLETED"). |
| closedAt | number | – | Satışın KAPANDIĞI an (epoch ms). |
| deletedAt | number | – | 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.
{
"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": [ "…" ]
}
}{
"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": [ "…" ]
}
}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.
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.
orders:read yokOlayın varlığını bilirsin, içeriğini almazsın.
{
"id": "evt_2d8f61a0c3",
"type": "packet.updated",
"version": "1",
"actor": { "type": "plugin", "pluginId": "pl_kiosk" },
"environment": "production",
"tenantId": "<tenantId>",
"occurredAt": 1768818214617,
"data": {}
}orders:read var, PII rızası yokMüş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": {
"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"
}
}orders:read + customers:read + rızaTam gövde — yukarıdaki packet.updated örneğinin aynısı.
Daha özel bir olay zaten ateşliyorsa çift bildirim yapılmaz.
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| Dine-in masa açılışı | event | – | table.created |
| Paket oluşturma veya birleştirme | event | – | packet.created |
| Hesap kapanışı | event | – | table.closed · packet.closed |
| Paket tam iptali | event | – | 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.
{
"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.
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.