Masa Aç — POST /plugin-api/tables/create ✓ Canlı

Eklentinin dine-in bir masayı AÇTIĞI yazma ucudur — QR self-order, kiosk ve masadan sipariş akışlarının giriş noktası. Daha önce eklenti yalnızca personelin açtığı bir masayı güncelleyebiliyordu; bu uç o boşluğu kapatır. Masa mağazanın salon planında tanımlı olmalıdır (tableId uydurulamaz), açık masada 409 döner, kuver otomatik uygulanır ve kapatma yetkisi verilmez.

← API Uçları · ortak kurallar (base, auth, hata zarfı) orada · Paket karşılığı: packets/create.

Yeni manifest alanı veya yeni scope gerekmez: mevcut orders:write bu ucu da kapsar.

Genel

Method / yolPOST /plugin-api/tables/create
Authinstall API key — Authorization: Bearer serverId.pluginId.secret
Scopeorders:write — artı orders:read (tableId'yi tables/layout'tan almak için; aşağı bkz.)
Rate limitAyrı write kovası (20/dk) — Limitler
Content-Typeapplication/json
Yan etkitable.created event'i tetiklenir (abone eklentilere)

İstek

POST {RESTOMENUM_BASE}/plugin-api/tables/create
Authorization: Bearer {serverId}.{pluginId}.{secret}     // install API key (token exchange)
Content-Type: application/json
  • Base: Sandbox https://sandbox.plugins.restomenum.app, Production https://plugins.restomenum.app (API Uçları).
  • Auth: Authorization: Bearer <apiKey> — token exchange'teki install API key.
İstek gövdesi
{
  "tableId": "masa-5",                 // ZORUNLU — tables/layout'taki masa id'si
  "personCount": 4,                    // ops (0..999) — kuver hesabında kullanılır
  "cart": [                            // ops (<=200) — boş/yok olabilir: masa siparişsiz açılır
    { "product": "urun-abc123", "quantity": 2, "taxRate": 19,     // taxRate ops (0..100)
      "lineId": "qr-9f2c",
      "metadata": [ { "key": "seat", "value": "2" } ] },        // ops — kimlik + korelasyon (DİZİ)
    { "product": "urun-def456", "quantity": 1, "note": "az şekerli" }
  ],
  "payments": [                        // ops (<=20) — masa AÇILIRKEN tahsilatı da yaz
    { "id": "m-cash", "price": 10.75,  // id = ÖDEME YÖNTEMİ id'si (satır kimliği DEĞİL)
      "lineId": "qr-pay-1",
      "metadata": [ { "key": "terminal", "value": "kiosk-1" } ] }   // ops — DİZİ
  ],
  "idempotencyKey": "self-order-9f2c"  // ops AMA ÖNERİLİR (retry'da çift masa/adisyon engeller)
}

Gövde alanları

AlanTipZorunluKural
tableIdstringevet≤200 karakter; / içeremez; ., .. ve __ayrılmış__ biçimi kabul edilmez. tables/layout yanıtındaki masa id'si olmalı (aşağı bkz.).
personCountintegerhayır0–999 (varsayılan 0). Kuver hesabında kullanılır.
cart[]arrayhayır≤200 kalem. Boş/gönderilmemiş olabilir → masa sipariş olmadan açılır (rezervasyon / müşteri oturtma).
cart[].productstringevetÜrün id'si (products/list) — ≤128 karakter.
cart[].quantitynumberevet0.001–9999.
cart[].options(string | { id } | { title })[]hayır≤50 seçenek beyanı. { id } (katalog kimliği — en kesin), { title } ya da düz string (eski biçim, çalışmaya devam eder). Fiyat beyan edilemez — şema reddeder, katalogtan okunur. Hata → 400 Invalid option: <ürün>: <neden> (<beyan>) (katalogda yok / aynı adlı farklı fiyatlı seçenekler → belirsiz / tekrar beyan).
cart[].discountnumberhayır≥0 — satır indirimi.
cart[].notestringhayır≤500 karakter.
cart[].taxRatenumberhayır0–100 — satırın KDV oranı. Verilirse ürünün oranını ezer ve satırda dondurulur; verilmezse ürünün oranı kullanılır. Geçersiz değer → 400 (Invalid taxRate (0-100 expected): <id>), sessizce ürüne düşmez. Beyanı yapan eklenti satırda taxRateBy ile kaydedilir — bu iç denetim izidir, hiçbir okuma yanıtında dönmez (teyitli). Kaynak: mali fişleme (fiscal.de).
payments[]arrayhayır≤20 satır — masa açılırken tahsilatı da yaz (QR self-order'da müşteri peşin öder). Yoksa masa ödemesiz açılır; sonradan tables/update-payments ile yazılır.
payments[].idstringevetTenant'ın gerçek ödeme yöntemi id'si (payment-methods/list); uydurma id → unknown_payment_method, tenantta hiç yöntem tanımlı değilse no_payment_methods_configured (ikisi de 400). isDiscount:true satırlar bu doğrulamadan muaftır ve o satırlarda title zorunludur.
payments[].pricenumberevet≥0 — satır tutarı. Yalnız price > 0 satırlar saklanır (price ≤ 0 düşer); paid sunucuda bunlardan toplanır. paid > total → 400.
payments[].isDiscountbooleanhayırtrue → satır indirimdir (tahsilat değil); yöntem otoritesinden muaftır.
payments[].titlestringkoşulluYalnız indirim satırında zorunlu (markalı başlık korunur). Normal satırda yok sayılır: title/cash/noreport tenant yöntem kaydından türetilir.
cart[].lineId · payments[].lineIdstringhayırKararlı satır kimliği (⏳ 8–64, alfanümerik başlar, saf rakam reddedilir, A-Z a-z 0-9 . _ : -; kuver/new rezerve). Ayrıntı: Satır kimliği & metadata.
cart[].metadata · payments[].metadataarrayhayırOpak korelasyon alanı — { key, value } dizisi (düz map değil → 400). Yazar başına ≤10 öğe, value ≤256 karakter; istek başına ≤20 KB (sepet ve ödeme ayrı bütçe), belge geneli ≤100 KB. PII yazmayın.
idempotencyKeystringönerilir≤128 karakter. Aynı anahtarla retry → yeni masa açılmaz, ilk sonuç döner (24sa pencere).

Yanıt

// data ayrıca docNoDate alanını da taşır (aşağıdaki nota bak)
{ "success": true, "data": { "tableId": "masa-5", "docNo": 42, "total": 99, "paid": 10.75 } }
  • data.tableId — açılan masanın id'si (gönderdiğinizle aynı); sonraki çağrılarda bunu kullanın.
  • data.docNo — tenant'ın işletme-günü fiş numarası (adisyon/belge no).
  • data.docNoDate — koddan teyitli ek alan; gerçek değeriyle örneğe henüz konmadı.
  • data.paid — gönderdiğiniz payments satırlarından hesaplanan tahsilat (yoksa 0).
  • data.total — kuver ve satır indirimleri dahil, platformun hesapladığı otoriter tutar. Kendi hesabınızla farklıysa platformun değeri geçerlidir.

Masanın tam detayını (adisyon satırları, totaller) tables/get?id= ile çekebilirsiniz.

Hatalar

HTTPNe zamanNot
400tableId floor plan'da yok ya da masa pasif (Table is not defined in the floor plan or is inactive) · cart'ta olmayan ürün (Product not found: <id>) · satırın oranı aralık dışı (Invalid taxRate (0-100 expected): <id>) · unknown_payment_method / no_payment_methods_configured · tahsilat tutarı aşıyor (Paid (12) exceeds total (10.75).) · şema ihlali (geçersiz tableId biçimi, quantity aralık dışı vb.)Doküman hatası; istek düzeltilip tekrar denenebilir.
401Geçersiz/askıya alınmış install key—
403orders:write scope'u yokTenant kurulumda onaylamadı → yeniden consent gerekir.
404Tenant bulunamadı—
409Masa zaten açık · ya da aynı idempotencyKey ile eşzamanlı bir istek işleniyorAçık masaya sipariş eklemek için tables/update-orders.
429write bucket limiti (20 req/dk)Retry-After başlığına uyun — Limitler.
Hata sırası — hatalı istek fiş numarası yakmaz. Ürün bulunamadıysa oran kontrolüne bakılmaz (Product not found: …); aynı istekte birden çok satırın oranı geçersizse hepsi tek mesajda listelenir. Ödeme doğrulaması da docNo tüketiminden önce koşar — reddedilen istek işletme-günü fiş numarasını harcamaz.

cart[] — satır kimliği (lineId) & metadata

İki alan da opsiyoneldir; hiçbiri gönderilmezse davranış bugünküyle aynıdır (mevcut eklentiler etkilenmez). Tam kurallar: Satır Kimliği & metadata.

AlanTipZorunluAçıklama
cart[].lineIdstring–Kararlı satır kimliği — çağıran taşır, platform korur. 8–64 karakter (⏳ alt sınır 1 → 8'e çıktı), alfanümerik başlar, A-Z a-z 0-9 . _ : - içerir — ⚠️ noktalama yalnız İÇERİDE (pay_01H9ABCD geçer; _pay01H9ABCD ve -pay01H9ABCD 400); saf rakamdan oluşan kimlik reddedilir ("1", "42" → 400). kuver ve new rezervedir (büyük/küçük harf farketmez) → 400 Invalid lineId: kuver. Aynı istekteki liste içinde tekrar edemez → 400 Duplicate lineId: <id>; iptal edilmiş (void) bir satırın kimliğini yeniden beyan etmek → 409 lineId belongs to a cancelled line. Gönderilmeyen satıra platform üretir ve üretilen kimlikler beyan edilenlerle çakışmaz. ⚠️ Kimlik korunması koşulludur: her istekte yeniden beyan edilmelidir (aşağı bkz.).
cart[].metadataarray–Opak korelasyon alanı — { key, value } dizisi (düz map değil; map gönderen istek 400 metadata must be an array of {key, value} alır). Platform yorumlamaz: saklar ve okuma yanıtında yazar damgasıyla (by) döner. key alfanümerik başlar, A-Za-z0-9_-, ≤64; value string | number | boolean (iç içe obje / dizi / null → 400). Bütçeler: yazar başına ≤10 öğe / satır ve value ≤256 karakter, istek başına ≤20 KB (sepet ve ödeme ayrı), belge geneli birleşme sonrası ≤100 KB.
⏳ Grandfather — PLANLANAN davranış, bugün ETKİN DEĞİL. Af mantığı platformda yazılı ama şema katmanı isteği daha transaction'a girmeden reddettiği için ona ulaşılmıyor — dört yazma ucunun hepsinde ölçüldü. Bugün dokümanda zaten var olan zayıf bir kimlik de 400 alır. Ayrıntı: Satır kimliği → Grandfather. Aşağıdaki akış tasarlanan davranıştır:

Tam-sepet değiştirmede önceki yanıtta aldığın kimlikleri geri gönderirsin; 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. Bu yüzden:
1️⃣ Biçim geçersizse her zaman 400 — asla affedilmez.
2️⃣ Biçim geçerli ama zayıfsa (<8 karakter ya da saf rakam): kimlik dokümanda zaten varsa kabul edilir (grandfather), yoksa 400 (yeni zayıf beyan).
Kontrol transaction içinde, doküman zaten okunurken yapılır — ek okuma maliyeti yok.
⚠️ packets/create ve tables/create uçlarında grandfather YOKTUR: yeni satış açarlar, karşılaştırılacak mevcut doküman yoktur → her zayıf kimlik yeni beyandır ve 400 alır.
Öneri: anahtarını çifte çevir. Kural bir kalkan; yapısal çözüm (uuid, lineId) çiftini anahtar yapmaktır. uuid (kalıcı satış kimliği) 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.
Altın kural: kimliği HER istekte yeniden beyan et. Bu uç tam değiştirme yapar — platform yalnız gelen listedeki kimlikleri dokümandakilerle eşler. Beyan edilmeyen satır "silinmiş" sayılır ve üzerindeki tüm metadata (başka eklentilerin yazdıkları dahil) onunla birlikte gider. İstek reddedilmez (meşru satır silme de bu yola düşer) ama platform kaybı loglar. Kimliği kendi kaydından türet ve kendi tarafında sakla — doğru/yanlış örneği.
metadata'ya PII yazmayın (ad, telefon, e-posta, adres, kart): sözleşme gereği yasaktır. Platform bu alanda desen taraması yapmaz — yanlış pozitif meşru bir mali kaydı reddedip satışı durdururdu; kural sözleşme düzeyindedir, ihlal eklentinin sorumluluğundadır. Müşteri verisi customers:read + rıza üzerinden alınır.
Görünürlük: alan tenant içi korelasyon verisidir (kiosk yazar, fiskal sağlayıcı okur) → her kanalda orders:read yeterlidir, müşteri-PII rızası gerekmez ve alan push gövdelerinden silinmez. 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.

payments[] — satır kimliği (lineId) & metadata

İki alan da opsiyoneldir; hiçbiri gönderilmezse davranış bugünküyle aynıdır (mevcut eklentiler etkilenmez). Tam kurallar: Satır Kimliği & metadata.

AlanTipZorunluAçıklama
payments[].lineIdstring–Kararlı satır kimliği — çağıran taşır, platform korur. 8–64 karakter (⏳ alt sınır 1 → 8'e çıktı), alfanümerik başlar, A-Z a-z 0-9 . _ : - içerir — ⚠️ noktalama yalnız İÇERİDE (pay_01H9ABCD geçer; _pay01H9ABCD ve -pay01H9ABCD 400); saf rakamdan oluşan kimlik reddedilir ("1", "42" → 400). kuver ve new rezervedir (büyük/küçük harf farketmez) → 400 Invalid lineId: kuver. Aynı istekteki liste içinde tekrar edemez → 400 Duplicate lineId: <id>; iptal edilmiş (void) bir satırın kimliğini yeniden beyan etmek → 409 lineId belongs to a cancelled line. Gönderilmeyen satıra platform üretir ve üretilen kimlikler beyan edilenlerle çakışmaz. Göndermesen bile her ödeme satırı kalıcı bir lineId alır (platform üretir, pay- önekli — önceden ödeme kaydının kimliği hiç yoktu). ⚠️ Kimlik korunması koşulludur: her istekte yeniden beyan edilmelidir (aşağı bkz.).
payments[].metadataarray–Opak korelasyon alanı — { key, value } dizisi (düz map değil; map gönderen istek 400 metadata must be an array of {key, value} alır). Platform yorumlamaz: saklar ve okuma yanıtında yazar damgasıyla (by) döner. key alfanümerik başlar, A-Za-z0-9_-, ≤64; value string | number | boolean (iç içe obje / dizi / null → 400). Bütçeler: yazar başına ≤10 öğe / satır ve value ≤256 karakter, istek başına ≤20 KB (sepet ve ödeme ayrı), belge geneli birleşme sonrası ≤100 KB.
⏳ Grandfather — PLANLANAN davranış, bugün ETKİN DEĞİL. Af mantığı platformda yazılı ama şema katmanı isteği daha transaction'a girmeden reddettiği için ona ulaşılmıyor — dört yazma ucunun hepsinde ölçüldü. Bugün dokümanda zaten var olan zayıf bir kimlik de 400 alır. Ayrıntı: Satır kimliği → Grandfather. Aşağıdaki akış tasarlanan davranıştır:

Tam-sepet değiştirmede önceki yanıtta aldığın kimlikleri geri gönderirsin; 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. Bu yüzden:
1️⃣ Biçim geçersizse her zaman 400 — asla affedilmez.
2️⃣ Biçim geçerli ama zayıfsa (<8 karakter ya da saf rakam): kimlik dokümanda zaten varsa kabul edilir (grandfather), yoksa 400 (yeni zayıf beyan).
Kontrol transaction içinde, doküman zaten okunurken yapılır — ek okuma maliyeti yok.
⚠️ packets/create ve tables/create uçlarında grandfather YOKTUR: yeni satış açarlar, karşılaştırılacak mevcut doküman yoktur → her zayıf kimlik yeni beyandır ve 400 alır.
Öneri: anahtarını çifte çevir. Kural bir kalkan; yapısal çözüm (uuid, lineId) çiftini anahtar yapmaktır. uuid (kalıcı satış kimliği) 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.
payments[].id satır kimliği DEĞİLDİR. O alan ödeme yönteminin kimliğidir (nakit/kart tanımı — bkz. payment-methods/list) 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.
Altın kural: kimliği HER istekte yeniden beyan et. Bu uç tam değiştirme yapar — platform yalnız gelen listedeki kimlikleri dokümandakilerle eşler. Beyan edilmeyen satır "silinmiş" sayılır ve üzerindeki tüm metadata (başka eklentilerin yazdıkları dahil) onunla birlikte gider. İstek reddedilmez (meşru satır silme de bu yola düşer) ama platform kaybı loglar. Kimliği kendi kaydından türet ve kendi tarafında sakla — doğru/yanlış örneği.
metadata'ya PII yazmayın (ad, telefon, e-posta, adres, kart): sözleşme gereği yasaktır. Platform bu alanda desen taraması yapmaz — yanlış pozitif meşru bir mali kaydı reddedip satışı durdururdu; kural sözleşme düzeyindedir, ihlal eklentinin sorumluluğundadır. Müşteri verisi customers:read + rıza üzerinden alınır.
Görünürlük: alan tenant içi korelasyon verisidir (kiosk yazar, fiskal sağlayıcı okur) → her kanalda orders:read yeterlidir, müşteri-PII rızası gerekmez ve alan push gövdelerinden silinmez. 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.

type alanı YOKTUR — masa zaten dine_in

Paket ucunun aksine (packets/create) bu uç satışın tüketim biçimini (type) almaz: masa zaten dine_in'dir, çelişkili beyan kabul edilmez. Okuma yanıtında masa hesabı daima "dine_in" döner (türetilir). Ayrıntı: Satış tipi (type).

tableId uydurulamaz — layout'tan gelir

Masa, tenant'ın salon planında (floor plan) tanımlı ve aktif olmalıdır. Serbest metin masa adı kabul edilmez; tanımsız veya pasif masa 400 döner. Bu, panelde görünmeyen "hayalet masa" oluşmasını engeller.

Akış: GET /plugin-api/tables/layout → bölümler ve masalar → kullanacağınız masanın id alanı → tableId.

tables/create kullanan eklenti orders:read scope'una da ihtiyaç duyar — layout ucu orders:read ile korunuyor. Kurulum ekranında iki scope birlikte istenmeli (manifest requestedScopes: ["orders:read", "orders:write"]).

Masanın görünen adı, bulunduğu salon ve konumu platform tarafından layout'tan türetilir — istekte gönderilmez, gönderilse de yok sayılır.

Fiyat, kuver ve toplam otoritesi platformdadır

Fiyat göndermezsiniz. cart kaleminde fiyat alanı yoktur; fiyat tenant'ın ürün kaydından okunur (client fiyat iddiası kabul edilmez). Yalnız product (id) + quantity + options (beyan) + discount verirsiniz.

Tek istisna oran: satır opsiyonel taxRate (0–100) taşıyabilir — yerinde tüketim / götürü ayrımı yalnız sipariş yüzeyinde doğduğu için. Verilen oran satırda dondurulur.
  • Kuver tenant ayarından otomatik uygulanır ve masaya sabitlenir (personCount dikkate alınır). Eklenti kuver oranı gönderemez. cart boşsa kuver uygulanmaz.
  • total yanıtta döner; eklentinin kendi hesabıyla farklıysa platformun değeri geçerlidir (kendi tutarınızı kullanıcıya kesin tutar diye göstermeyin).

Açık masaya ikinci kez açma yok

Masa zaten açıksa 409 döner ve mevcut adisyona dokunulmaz. Personel ile eşzamanlı açılışta yalnızca bir taraf kazanır — çift adisyon oluşmaz.

Idempotency

idempotencyKey gönderirseniz, aynı anahtarla yapılan tekrar istekler yeni masa açmaz — ilk isteğin sonucu (tableId, docNo, total) aynen döner. Ağ hatası / timeout sonrası güvenle retry edebilirsiniz.

İstek bir iş hatasıyla (400/404/409) sonuçlanırsa anahtar serbest bırakılır: isteği düzeltip aynı anahtarla tekrar deneyebilirsiniz. Anahtar saklama süresi 24 saattir.

En iyi pratik: anahtarı kendi oturum/sepet kimliğinizden türetin (ör. self-order-<sessionId>) — kullanıcı "Gönder"e iki kez bastığında da tek masa açılır.

Timer (süre-bazlı) ürünler — dikkat

Süre-bazlı ürün (dakika ücretlendirmeli) içeren bir masa açabilirsiniz; ancak o masada daha sonra tables/update-orders çağıramazsınız (mevcut kısıt: kalem değişikliği geçen süreyi sıfırlayacağından reddedilir). Masayı sonradan güncellemeyi planlıyorsanız süre-bazlı ürünle açmayın.

table.created webhook'u — kendi eyleminizin echo'su

Bu uç, personelin masa açmasıyla aynı table.created event'ini üretir. table.created'a abone olan eklenti, kendi açtığı masa için de event alır.

Döngü riski. Event'i işleyip yeni masa açan bir mantık yazarsanız döngü oluşur. table.created handler'ınızda kendi oluşturduğunuz masaları elemeniz gerekir — açtığınız tableId/docNo'yu kendi kaydınıza yazın ve event geldiğinde eşleştirip atlayın (envelope id ile dedup ayrıca zorunludur, at-least-once teslim).

Kapatma yetkisi yok

Eklenti masayı kapatamaz, silemez, adisyonu sonlandıramaz. Kapanış (finansal kapanış, adisyon kapatma) yalnızca çekirdek/personel akışındadır. Kapanış anında haberdar olmak için table.closed event'ine abone olun; kapanışı engelleyip onaylamak için table.close gate hook'unu kullanın.

Uçtan uca örnek — QR self-order

Akış
1) GET  /plugin-api/tables/layout           → masaların id'leri
2) GET  /plugin-api/products/list           → ürün id'leri ve fiyatlar
3) POST /plugin-api/tables/create            { tableId, personCount, cart, payments, idempotencyKey }
   → 200 { data: { tableId, docNo, total, paid } } (masa açıldı, adisyon oluştu)
   → 409                                            (masa zaten açık)
4) POST /plugin-api/tables/update-orders     { tableId, cart }   (aynı oturumda yeni sipariş)
5) POST /plugin-api/tables/update-payments   { tableId, payments }
   (kapanış personelde — eklenti kapatmaz)