sequence & sequenceScope ✓ CanlıTeslim sıralı değildir: retry, kuyruk ve elle yeniden teslim yüzünden eski bir snapshot yeniden gelebilir. Zarftaki sequence (satışın kaçıncı durum değişikliği) ve sequenceScope (opak satış hattı kimliği) bayat teslimi tanımanı sağlar. İkisi de opsiyoneldir — gelmezlerse bugünkü davranışın sürer.
← Event Kataloğu · Hesap Yaşam Döngüsü
Aynı çift artık üç gövdede geliyor ve üçünde de aynı sözleşmedir:
| Yol | Konum | Durum |
|---|---|---|
| Webhook zarfı | Zarf kökü — data ile aynı seviyede | ✓ Canlı |
table.close · packet.close hook'u | Hook gövdesi kökü | ⏳ Yakında |
capability:fiscal.de | Payload kökü | ⏳ Yakında |
// kapanış hook'u
{
"type": "hook", "event": "table.close", "stage": "before",
"target": { "type": "table", "id": "masa-12" },
"sequence": 14,
"sequenceScope": "sq_5776130612ab",
…
}fiscal.de: bu capability şu an yalnız sale.type: "packet" kabul eder. "table" bilinçli olarak desteklenmeyen tipler listesindedir ve ayrı bir hata koduyla reddedilir → masa satışları için capability yolu, dolayısıyla oradan gelen sıra numarası da, bugün mevcut değildir. (Masa mali bloğu kapanış gate'i üzerinden receiptExtras ile taşınır.)| Garanti | Anlamı |
|---|---|
sequenceScope yeniden kullanılmaz | Scope yalnız kalıcı bir yazımda üretilir ve değeri rastgeledir. Aynı değerin ikinci kez doğması pratikte imkânsızdır. |
sequence scope içinde monotondur | Sayaç yalnız ileri gider, asla geriye sarmaz. Atlanan numara olabilir (yazım yapmayan olaylar bellekte bir sonraki numarayı kullanır); tekrar eden ya da azalan numara olmaz. |
null da gelmez. Uydurma bir sıra numarası yayınlamaktansa "bilinmiyor" demek doğrudur; alan yoksa o olay için sıralama karşılaştırmasını atla (sequence: 0 varsayma).occurredAt iki teslimi ayırt etmeye yetmez: sunucular arası saat kayması ve aynı milisaniyede yapılan iki yazım mümkündür. Bir hesabın durum değişiklikleri ise satış düzeyinde numaralanır — bayat bir snapshot'ı bu numaradan tanırsın.
{
"id": "evt_9f3a…",
"type": "table.updated",
"occurredAt": 1768818213617,
"sequence": 7, // bu satışın kaçıncı durum değişikliği
"sequenceScope": "sq_9f3ab27c…", // sequence'in geçerli olduğu SATIŞ HATTI
"data": { "tableId": "masa-5", "docNo": 12, "…": "…" }
}| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| sequence | number? | – | Satışın kaçıncı durum değişikliği olduğu. 1'den başlar ve satış düzeyindedir. |
| sequenceScope | string? | – | sq_ önekli opak satış hattı kimliği — numaranın hangi defterde geçerli olduğunu söyler. |
sequence: 0 varsayma — yokluk "sıra bilinmiyor" demektir, "en baştaki olay" değil.(tenantId, sequenceScope) ile anahtarla. Her scope için ayrı bir "son işlenen sequence" tut. Tek slotlu bir uygulama (yalnız son gördüğüm scope'u hatırla) yanlıştır: yeni satış başladıktan sonra gelen geç bir eski-satış olayı "scope farklı → karşılaştırma yok" diye kabul edilir ve bayat durumu yeninin üstüne yazar.sequence, o scope için en son işlediğinden küçükse yok say.sequence sıra bilgisi taşımaz. Tekrar teslimi zarf id'si ile dedup et. Farklı içerikli eşit-numaralı iki olay, aralarında güvenilir bir sıra olmadığı anlamına gelir — son gelen kazanır.*.closed, *.cancelled, *.deleted, *.closed_deleted satışı sonlandırır. Bunlardan birini işledikten sonra aynı scope'ta gelen bir *.updated'ı — numarası eşit ya da büyük olsa bile — satışı yeniden açmak için kullanma. Yalnız *.reopened satışı geri açar.id dedup'u çalışmaz) "bu scope defterde yok → kabul" yolundan geçip satışı diriltir.import {
isSaleSequenceEvent, sequenceKey, sequenceVerdict, advanceSequenceCursor,
} from '@restomenum/plugin-sdk';
if (isSaleSequenceEvent(envelope.type)) {
const key = sequenceKey(envelope.tenantId, envelope); // (tenantId, sequenceScope) — TEK SLOT YASAK
const cursor = key ? await ledger.get(key) : null; // { sequence, terminal? } | null
switch (sequenceVerdict(envelope, cursor)) {
case 'stale': return ok(); // daha küçük numara → bayat, YOK SAY (yine de 2xx dön)
case 'terminal': return ok(); // satış sonlanmış; *.updated onu DİRİLTMEZ
default: break; // 'apply' | 'unsequenced' → işle
}
await handle(envelope);
if (key) await ledger.set(key, advanceSequenceCursor(cursor, envelope)); // ≥ 7 gün sakla
}// SDK kullanmıyorsan birebir aynı karar (kural 2–4):
const key = `${envelope.tenantId}:${envelope.sequenceScope}`; // scope YOKSA karşılaştırma YAPMA
const cursor = await ledger.get(key); // { sequence, terminal }
if (envelope.sequence != null && envelope.sequenceScope && cursor) {
if (envelope.sequence < cursor.sequence) return; // 2) bayat
const TERMINAL = /\.(closed|cancelled|deleted|closed_deleted)$/;
const REOPEN = /\.reopened$/;
if (cursor.terminal && !TERMINAL.test(envelope.type) && !REOPEN.test(envelope.type)) return; // 4)
}
// 3) eşit numara → sıra bilgisi yok: son gelen kazanır (tekrar teslimi zarf id'si ile dedup et)// Defter satırı — anahtar (tenantId, sequenceScope), TTL ≥ 7 gün
{
"tenantId": "tnt_123",
"sequenceScope": "sq_9f3ab27c",
"sequence": 12, // bu hat için İŞLENMİŞ en büyük numara
"terminal": true, // *.closed / *.cancelled / *.deleted / *.closed_deleted işlendi
"updatedAt": 1768820000000
}@restomenum/plugin-sdk ≥ 3.0.0 şart. Daha eski sürümlerde parseEnvelope zarfı sabit bir alan listesinden yeniden kuruyordu: sunucu sequence / sequenceScope (ve actor / origin) gönderse bile alanlar tüketiciye ulaşmıyordu. 3.0.0'dan itibaren eşleyici tanımadığı üst-seviye alanları olduğu gibi taşır → platform zarfa yeni alan eklediğinde yeni bir SDK sürümü beklemen gerekmez. Yükseltemiyorsan alanları ham gövdeden oku (gövde imzalı olduğu için güvenlidir).2xx dön. Bayat/terminal eleme bir hata değildir; 4xx/5xx dönmek retry'a ve teslim sağlığının düşmesine yol açar.sequence bir satış düzeyi sayacıdır, "kaç olay aldım" sayacı değildir. Sayacı ilerleten ama olay yayınlamayan onlarca nokta vardır: teslim/hazır damgası, ödeme terminali yazımı, kampanya uygulaması, entegrasyon ödeme uçları, kapanış öncesi tutar onarımı. Ardışık iki olay arasında numara birden fazla artmış olabilir — tek başına boşluk, olay kaybettiğinin KANITI değildir. Tek kural: aynı scope içinde daha küçük numara = bayat.advanceSequenceCursor). Beklenmedik büyüklükte bir boşluk gördüğünde bunu bir hata sayma ama bir uzlaştırma sinyali say: o satışın güncel hâlini packets/get / tables/get ile çek ve kendi kaydınla karşılaştır. Okuma ucu her zaman gerçeği söyler; webhook yalnız bir bildirimdir. Bu, "sessiz kayıp" sınıfındaki her arızayı — platform kaynaklı olanlar dahil — gün sonu kapanmadan yakalamanı sağlar.*.closed. Bir satış hattı normalde bir kez kapanır. Aynı sequenceScope için ikinci bir table.closed/packet.closed aldıysan ve arada bir geri alma olayı işlemediysen, o geri alma sana ULAŞMAMIŞTIR — bu belirsiz bir sinyal değil,kesin bir çıkarımdır. Boşluk taramasının aksine yanlış alarm üretmez.| Durum | sequenceScope | sequence |
|---|---|---|
| Masa / paket açılışı | yeni hat | 1 |
| Kalem, ödeme, indirim, kuver değişikliği | aynı hat | +1 |
| Masa bölme — yeni masa | yeni hat | 1 |
| Masa bölme — kaynak masa | aynı hat | +1 |
Boş masaya taşıma — kaynak *.deleted | kaynağınki korunur | n+1 |
Boş masaya taşıma — hedef *.updated | kaynağınki korunur | n+2 |
| Dolu masaya taşıma (birleştirme) — hedef | hedefin kendi hattı | +1 |
Kapanış *.closed | aynı hat (terminal) | +1 |
Geri açma *.reopened | aynı hat sürer | +1 |
Devir *.deleted · iptal packet.cancelled · *.closed_deleted | aynı hat (terminal) | +1 |
| Aynı masada yeni satış | yeni hat | 1 |
| Paket birleştirme / başka işletmeye taşıma | hedefte yeni hat | 1 |
Kapanışta hesabın kimliği değişir (table.updated'daki tableId ile table.closed'daki farklıdır), geri açmada da yeni bir kimlik doğar (hesap yaşam döngüsü). Fazlar arasında kararlı kalan tek bağ sequenceScope'tur.
*.deleted, hedef *.updated. İkisi aynı numarayı alsaydı, geç teslim edilen silme olayı hedefteki açık satışı defterinden silerdi. Bu yüzden boş masaya taşımada hat korunur ama hedef iki adım ilerler (n+1 / n+2). Kısmi taşımada iki hesap da yaşar → her biri kendi hattını ilerletir. Sayacı hiç olmayan eski satışlarda silme olayı numara yayınlamaz (uydurma hat kimliği üretilmez) — alan yoksa o olay için karşılaştırmayı atla.table.closed ve packet.closed son güncellemeden kesin olarak büyük numara taşır. Yine de kural 4 ikinci savunma hattın olarak kalmalı: "eşit numarada son gelen kazanır"ı harfiyen uygulayan mali bir tüketici, kapanıştan sonra geç gelen bayat bir güncellemeyle kapanmış satışı geri açıp kestiği fişi storno edebilirdi.packet.created'ı.packet.created'ı — hat bilerek sökülür ki kaynak işletmenin satışıyla karışmasın.Alan yoksa karşılaştırma yapılamaz; bugünkü davranışını sürdür (occurredAt karşılaştırması + id dedup'u).
sequence / sequenceScope teslim edilen gövdelerin alanlarıdır (aşağıdaki üç yol); tables/get · packets/get gibi okuma uçlarının yanıtında yer almaz.id'si ile dedup et, sonra sıra kararını ver.isOwnEcho ile ele.