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.
Webhook (webhookUrl) | Action (actionUrl) | |
|---|---|---|
| Tetik | Event (packet.created…) | Kullanıcı butona basınca |
| Çağrı | Async, fire-and-forget | Senkron, kullanıcı bekler |
| Yanıt | 200 ack yeter | { success, message, level, display } JSON — sonucu kullanıcı görür |
| Hata | Retry edilir | Retry YOK |
| Timeout | — | 8 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.)"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ı.POST {actionUrl}
Content-Type: application/json
X-Restomenum-Signature: t=<unixSec>,v1=<HMAC_SHA256(webhookSecret,"<t>.<rawBody>")>
X-Restomenum-Event: action{
"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.
| Slot | target.type | target.id |
|---|---|---|
| packet.detail.actions | packet | paket id |
| (gelecek) order.detail.actions | order | sipariş id |
| (gelecek) customer.detail.actions | customer | müşteri id |
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ı.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.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
}| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| success | boolean | ✓ | İşlem başarılı mı. |
| message | string | – | Kullanıcıya gösterilecek metin (düz metin, HTML değil). |
| level | enum | – | RENK: info · success · warning · error. Geçersiz değer kabul edilmez → success'e göre düşürülür. |
| display | enum | – | SUNUM: toast · popup. Eksikse toast. Önemli/onay gerektiren sonuçlar için popup, hızlı bilgilendirmeler için toast. |
| data | object | – | Opsiyonel 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 ile ayırt et. Tanımadığın hook'a { success:false, level:"error", message:"…" } dön.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.){ success:false, level:"error"|"warning", message:"…" } + HTTP 200. HTTP 200 dışı / 8 sn timeout → Restomenum kullanıcıya genel hata gösterir.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", "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İRapp.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 type | Beklenen yanıt | Yanı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 2xx | retry → dead-letter |
type/event → güvenli şekilde 2xx ack dön (ileride yeni tip eklenebilir; eklentin kırılmasın).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österirHer slot kendi target.type'ına eşlenir ve kendi detay sayfasına sahiptir (yukarıdaki tablodaki linkler). Şu an canlı:
slot + target) değişmez — her biri kendi detay sayfasıyla buraya bağlanır.webhookSecret, ±5dk replay penceresi, sabit-zamanlı karşılaştırma. İmzasız body'ye asla güvenme.timeoutMs); ağır işi async webhook'a/arka plana kaydır.actionUrl https olmalı (SSRF guard; private IP/redirect engellenir) + same-apex.actor (manager/staff) + kendi mantığın — buton görünürlüğü yetki değildir. Bkz. Aksiyon Butonları + Rol Yetkisi.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.