Action Ucu (actionUrl)

UI butonlarının (packet.detail.actions) tıklanmasında çalışan SENKRON uç. Webhook'tan ayrıdır: kullanıcı butona basar, Restomenum bu uca imzalı POST atar, sen kısa sürede { success, message, level } dönersin — sonuç kullanıcıya toast olarak gösterilir.

1 · Webhook ≠ Action

Webhook (webhookUrl)Action (actionUrl)
TetikEvent (packet.created…)Kullanıcı butona basınca
ÇağrıAsync, fire-and-forgetSenkron, kullanıcı bekler
Yanıt200 ack yeter{ success, message, level, display } JSON — sonucu kullanıcı görür
HataRetry edilirRetry YOK
Timeout8 sn (aşarsa kullanıcıya hata)
actionUrl opsiyoneldir: tanımlamazsan senkron istekler de webhookUrl'e düşer (tek uç). İstersen tek URL'de toplayıp isteği type alanıyla ayırırsın; istersen iki ayrı URL (her biri yalnız kendi tiplerini alır). Bu uca buton (type:"action") ve before-hook gate (type:"hook") gelir — bkz. aşağıdaki İki istek tipi. (Slack Events vs Interactivity deseni.)

2 · Kurulum (manifest)

"actionUrl": "https://<eklenti>/api/action",        // webhookUrl'in yanında
"manifest": {
  "requestedScopes": ["…", "ui:button"],             // buton için zorunlu
  "buttons": [
    { "id": "send-to-courier", "slot": "packet.detail.actions",
      "label": { "tr": "Kuryeye Gönder" }, "icon": "truck",
      "action": { "type": "hook", "hook": "packet.sendToCourier" },
      "confirm": { "tr": "Emin misin?" } }
  ]
}
// actionUrl verilmezse → webhookUrl'e type:"action" ile fallback (ama ayrı uç önerilir).
actionUrl https olmalı ve same-apex kuralına dahildir: webhookUrl + connectUrl + actionUrl + sayfa origin'leri aynı kayıtlı domain altında olmalı. Buton şeması için bkz. Aksiyon Butonları.

3 · Eklentinin ALDIĞI istek

POST {actionUrl}
Content-Type: application/json
X-Restomenum-Signature: t=<unixSec>,v1=<HMAC_SHA256(webhookSecret,"<t>.<rawBody>")>
X-Restomenum-Event: action
Gövde — tek tip, slot'tan bağımsız
{
  "type": "action",
  "hook": "packet.sendToCourier",                        // butonun action.hook'u
  "tenantId": "kcK88…",
  "slot": "packet.detail.actions",                       // buton hangi slot'tan
  "target": { "type": "packet", "id": "1780633662954" }, // bağlam — UNIFORM (slot'tan bağımsız)
  "actor": { "userId": "uid_9", "role": "manager" },     // işlemi yapan kullanıcı (role ∈ manager|staff)
  "formData": { "<key>": "<değer>" },                    // YALNIZ type:form butonlarında dolu
  "occurredAt": 1780633662954,
  "id": "act_3f2a…"                                      // çağrı id'si
}

Payload her zaman aynı şekildedir: hook ile hangi buton, target.type/target.id ile hangi nesne olduğunu anlarsın. Yeni slot eklendikçe aşağıdaki tablo büyür, payload şekli değişmez.

Slottarget.typetarget.id
packet.detail.actionspacketpaket id
(gelecek) order.detail.actionsordersipariş id
(gelecek) customer.detail.actionscustomermüşteri id
type:form butonları: buton bir Declarative Form'u referans alır (action.form.formId); restoyeni formu native render eder, kullanıcı doldurur ve değerler formData olarak bu isteğe eklenir (ui:form scope gerekir). Diğer türlerde formData gelmez. Buton şeması: Aksiyon Butonları.
İmzayı webhook ile AYNI şekilde doğrula (ham gövde, webhookSecret, ±5 dk tolerans). Doğrulanmazsa 401 dön. Ayrıca target.id'nin (örn. paket) gerçekten istek sahibi tenant'a (tenantId) ait olduğunu teyit et. formData'yı da doğrula (required/maxLength/select) — istemciye güvenme.

4 · Eklentinin DÖNDÜĞÜ yanıt (sözleşme)

HTTP 200 + JSON:

HTTP 200
{
  "success": true,                  // işlem başarılı mı (ZORUNLU)
  "message": "Kuryeye gönderildi",  // kullanıcıya gösterilecek METİN (string)
  "level": "success",               // RENK: info | success | warning | error (ops.)
  "display": "toast",               // SUNUM: toast | popup (ops. — eksikse toast)
  "data": { }                       // opsiyonel, geçirilir
}
AlanTipZorunluAçıklama
successbooleanİşlem başarılı mı.
messagestringKullanıcıya gösterilecek metin (düz metin, HTML değil).
levelenumRENK: info · success · warning · error. Geçersiz değer kabul edilmez → success'e göre düşürülür.
displayenumSUNUM: toast · popup. Eksikse toast. Önemli/onay gerektiren sonuçlar için popup, hızlı bilgilendirmeler için toast.
dataobjectOpsiyonel ek veri; geçirilir.
  • level verilmezse/geçersizse Restomenum türetir: success:true → success, success:false → error. (UI render'ı kandırılamasın diye geçersiz değer success'e göre düşürülür.)
  • hook'a göre dallan: birden çok butonun varsa hook ile ayırt et. Tanımadığın hook'a { success:false, level:"error", message:"…" } dön.
  • Renk vs sunum: level rengi, display sunumu seçer — ikisini de eklenti belirler. toast = hızlı bilgilendirme, popup = önemli/onay gerektiren sonuç. (Kurye örneği: başarı → toast, hata/uyarı → popup.)
Hata durumları: İşi yapamadın → { success:false, level:"error"|"warning", message:"…" } + HTTP 200. HTTP 200 dışı / 8 sn timeout → Restomenum kullanıcıya genel hata gösterir.

İki istek tipi — actionUrl'e ne gelir?

Bu uca iki senkron istek tipi gelir; type (+ X-Restomenum-Event) ile ayır:

  • type:"action"buton tıklaması (yukarıdaki gövde) → { success, message, level, display } dön.
  • type:"hook"before-hook gate (table.close), işlem öncesi{ decision:"allow"|"deny", message? } dön.
type:'hook' — gate isteği (aynı uca gelir)
{
  "type": "hook", "stage": "before", "event": "table.close",
  "tenantId": "<tenantId>",
  "target": { "type": "table", "id": "masa-1" },
  "data": { "...": "includeData:true ise kanonik veri" },   // ops.
  "formData": { "...": "gate formu varsa" },                 // ops.
  "actor": { "userId": "...", "role": "manager" },
  "timeoutMs": 10000, "hookId": "hk_<uuid>"
}
// Yanıt (senkron): { "decision": "allow" | "deny", "message"?, "attach"? }   // deny → işlem ENGELLENİR

Tek uç iskeleti — type'a göre dallan

app.post('/restomenum', express.raw({ type: '*/*' }), (req, res) => {
  // 1) HER ZAMAN önce imzayı doğrula (webhookSecret ile HMAC, ham gövde) — geçersizse 401
  if (!verifySignature(req)) return res.status(401).end();
  const env = JSON.parse(req.body.toString('utf8'));

  switch (env.type) {
    // ── Senkron etkileşim (actionUrl) ──
    case 'action':                                  // buton tıklaması
      return res.json(handleAction(env));           // { success, message, level, display }
    case 'hook':                                    // before-gate (table.close)
      return res.json(handleGate(env));             // { decision:'allow'|'deny', message }

    // ── Async event (webhookUrl) — tek uç kullanıyorsan ──
    default:                                         // table.closed / product.* / ingredient.* …
      ackInBackground(env);                          // işi arka planda yap
      return res.json({ ok: true });                // 2xx ack (bilinmeyen type → yine güvenli 2xx)
  }
});

Yanıt sözleşmesi (özet):

İstek typeBeklenen yanıtYanıt verilmezse
action{ success, message?, level?, display? }hata toast'ı / success:false sayılır
hook (blocking){ decision:"allow"|"deny", message?, attach? }failMode'a göre (closed=deny / open=allow)
event (async)herhangi bir 2xxretry → dead-letter
Bilinmeyen type/event → güvenli şekilde 2xx ack dön (ileride yeni tip eklenebilir; eklentin kırılmasın).

5 · Akış

Buton tıklama (restoyeni)
  → Restomenum imzalı POST {actionUrl}
  → eklenti: imza doğrula → hook'a göre iş → { success, message, level }
  → Restomenum yanıtı restoyeni'ye geçirir
  → restoyeni: display'e göre toast/popup + level rengi (yeşil/kırmızı/sarı/mavi), message'ı gösterir

6 · Slot detayları

Her slot kendi target.type'ına eşlenir ve kendi detay sayfasına sahiptir (yukarıdaki tablodaki linkler). Şu an canlı:

Yeni slot'lar zamanla eklenir; payload şekli (slot + target) değişmez — her biri kendi detay sayfasıyla buraya bağlanır.

7 · Güvenlik notları

  • İmza doğrulaması zorunlu (her tip için) — webhook ile aynı webhookSecret, ±5dk replay penceresi, sabit-zamanlı karşılaştırma. İmzasız body'ye asla güvenme.
  • Senkron uçlar hızlı olmalı (action ~8sn, hook timeoutMs); ağır işi async webhook'a/arka plana kaydır.
  • actionUrl https olmalı (SSRF guard; private IP/redirect engellenir) + same-apex.
  • Yetkiyi sunucuda ver: actor (manager/staff) + kendi mantığın — buton görünürlüğü yetki değildir. Bkz. Aksiyon Butonları + Rol Yetkisi.
  • Bilinmeyen type/event → güvenli 2xx ack (ileri uyumluluk).
  • message serbest metindir ama restoyeni text olarak basar (HTML değil) — yine de yanıltıcı içerik koyma.
  • Action non-blocking: sipariş akışını durdurmaz, yalnız kullanıcıya sonuç bildirir. (Akış-durduran davranış için bkz. Hook'lar.)