4 · Action Hook (before-hook sözleşmesi)

Before-hook, bir çekirdek işlem (ör. masa kapatma) gerçekleşmeden ÖNCE çalışan senkron bir kapıdır. Restomenum actionUrl'inize imzalı POST atar; siz allow/deny kararı dönersiniz ve işlem buna göre devam eder veya iptal olur.

Kavram: gate, bildirim değil

Normal event'ler (webhook) işlem olduktan sonra gelir (async, yalnız haber alırsınız). Before-hook ise işlem olmadan önce çalışan senkron bir kapıdır — allow/deny kararınız işlemin devam edip etmeyeceğini belirler. Örn: "masa kapanmadan önce e-faturayı kes; kesmeden kapanma."

after-event (table.closed)before-hook (table.close)
Ne zamanKapandıktan sonraKapanıştan önce
DoğaAsync bildirimSenkron kapı (akışı durdurur)
Sizin cevabınız(yok){ decision:"allow"|"deny" }

Çalışma mantığı (adım adım)

  1. Kullanıcı panelde "Masa Kapat"a basar.
  2. Restomenum, manifest'te tanımladığınız table.close hook'unu bulur.
  3. Bildirdiğiniz FORMU kullanıcıya gösterir (siz HTML yazmazsınız — şemayı bildirirsiniz, native render).
  4. Kullanıcı formu doldurur.
  5. Restomenum → actionUrl'inize İMZALI SENKRON POST atar (aşağıdaki istek).
  6. SİZ: imzayı doğrula → (gerekirse target.id ile veriyi çek) → karar ver → timeoutMs içinde { decision } dön.
  7. allow → masa kapanır (+ attach fişe işlenir) · deny → kapanma İPTAL, mesajınız kullanıcıya.
Formu siz render etmezsiniz; manifest'te şemayı bildirirsiniz, Restomenum native gösterir. Token/DOM erişiminiz yoktur — yalnız kararınızı dönersiniz.

1) Aldığınız istek (CANLI)

POST {actionUrl}
Content-Type: application/json
X-Restomenum-Signature: t=<unixSec>,v1=<HMAC_SHA256(webhookSecret,"<t>.<rawBody>")>
X-Restomenum-Event: hook

{
  "type": "hook",                 // event/action'dan ayırt edin
  "event": "table.close",
  "stage": "before",
  "tenantId": "kcK88DtUafc…",     // hangi restoran (tenant)
  "target": { "type": "table", "id": "bah%C3%A7e1" },  // bağlam REFERANSI (doküman id)
  "data": { /* … */ },            // YALNIZ manifest includeData:true ise — kapanış-öncesi kanonik veri (tables/get şekli)
  "formData": { "courierCalled": true },               // kullanıcının formda girdiği değerler
  "actor": { "userId": "<uid>", "role": "manager" },   // işlemi yapan kullanıcı; role ∈ manager|staff (imzalı → güvenilir)
  "timeoutMs": 10000,             // cevap bütçeniz (1–10 sn); aşılırsa failMode
  "occurredAt": 1780713277601,
  "hookId": "hk_0380ad69-…"       // idempotency / izleme
}
AlanTipZorunluAçıklama
type:"hook"literalWebhook event'lerinden ayırt edin (aynı uca düşebilir).
eventstringHangi gate (table.close).
stage"before"Akış öncesi gate.
tenantIdstringHangi restoran (tenant).
target{ type, id }Değer-torbası değil — yalnız referans. Veriye ihtiyacınız varsa id ile Data API'den çekin (↓) — ya da includeData:true ile data gövdede gelir.
dataobjectYalnız includeData:true ise. Kapanış-öncesi server-türevli kanonik veri (masa → tables/get şekli). customer yalnız customers:read+consent; data yalnız orders:read. Kaynak kapanışta silineceği için karar-anı veriyi tek seferde alır.
formDataobjectKullanıcının doldurduğu form çıktısı.
actor{ userId, role }İşlemi yapan kullanıcı; role ∈ manager|staff. Per-user yetki için (imzalı → güvenilir). Ad/PII için users/get.
timeoutMsnumberCevap bütçeniz (1–10 sn) — aşarsanız failMode devreye girer.
occurredAtnumberUnix ms.
hookIdstringÇift-işlem koruması (idempotency) / log.
Neden context (total/tableName) yok? Güvenlik: client değerlerine güvenmeyiz. Gereken veriyi otoriter kaynaktan kendiniz çekersiniz (↓) — Stripe/Shopify deseni.

2) Veriyi çekin (target → Data API)

Kısayol — includeData:true: manifest'te açarsan kapanış-öncesi kanonik veri data alanında gövdeyle gelir → bu adımı (ayrı fetch) atlarsın. Gate allow dedikten sonra masa kapanır ve tables/{id} silinir; o yüzden karar anında veri lazımsa includeData en güvenli yoldur (kaybolan-kaynak yarışı yok).

includeData kullanmıyorsan: target sadece referans verir; masanın içeriğini (ürünler/tutar) çekmek için Masa Detayı (tables/get):

GET {RESTOMENUM_BASE}/plugin-api/tables/get?id=<encodeURIComponent(target.id)>
Authorization: Bearer <apiKey>        // kurulumdaki install API key  ·  scope: orders:read
target.id URL-encoded olabilir (bah%C3%A7e1) → query'de encodeURIComponent kullanın.

Yanıt (kanonik order — packets/get ile aynı şekil):

{ "success": true, "data": {
  "tableId": "bah%C3%A7e1", "tableName": "Bahçe1", "docNo": …, "desing": "Bahçe",
  "total": 285, "paid": 285, "totalDiscount": 0,
  "orders": [ { "title": "HYPATİA KAHVALTI", "quantity": 1, "options": [], "lineTotal": 160 }, … ],
  "customer": { … }   // varsa (customers:read + consent ile)
} }

3) Döndüğünüz yanıt (zorunlu)

// HTTP 200 + JSON
{ "decision": "allow" | "deny",
  "message": "Kullanıcıya gösterilecek metin",
  "attach":  { "invoiceNo": "ABC123" } }   // opsiyonel; whitelist: invoiceNo / reference / note
  • allow → işlem devam; attach güvenli alanlara işlenir (ör. fişin invoiceNo'su).
  • deny → işlem iptal; message kullanıcıya (düz metin).

4) timeoutMs · failMode · enforce

  • timeoutMs içinde yanıt verin. Aşarsanız → failMode (manifest): closed=deny (durdur) / open=allow (geç).
  • enforce:true (manifest) → sert garanti: gate çalışmadan işlem gerçekleşmez (backend doğrular, atlanamaz). Erişilemezseniz işlem bloklanabilir — yalnız zorunlu gate'lerde kullanın. Token'ı Restomenum üretir; sizi ilgilendirmez. Detay: Hook'lar → Sert Garanti.

5) İmza doğrulama (webhook ile aynı)

X-Restomenum-Signatureham gövde üzerinden webhookSecret ile HMAC-SHA256 (±5 dk). Doğrulanmazsa 401. Algoritma/kod: İmza Doğrulama.

Tam örnek

// /hooks/table-close — Action Hook (akışı DURDURAN). Restomenum HMAC imzalı senkron POST eder.
// Restomenum → sana (✅ teyitli — canlı örnek):
//   { type:"hook", event:"table.close", stage:"before", tenantId,
//     target:{ type:"table", id:"Masa 5" },
//     actor:{ userId, role },        // işlemi yapan kullanıcı; role ∈ manager|staff (imzalı → güvenilir)
//     data,                          // YALNIZ manifest includeData:true ise — kapanış-öncesi kanonik veri
//                                    //   (tables/get ile aynı şekil; customer → customers:read+consent, data → orders:read)
//     formData, hookId:"hk_…", occurredAt }
// Sen → Restomenum:  { decision, message, attach }
import express from 'express';
const app = express();

app.post('/action', express.raw({ type: '*/*' }), async (req, res) => {
  const rawBody = req.body.toString('utf8');
  const sigHeader = req.get('X-Restomenum-Signature');
  const body = JSON.parse(rawBody);
  if (!(body.type === 'hook' && body.event === 'table.close')) return res.json({});

  // 1) imzayı doğrula (webhook ile AYNI: HMAC_SHA256(webhookSecret, "<t>.<rawBody>"))
  if (!verifySignature(webhookSecret, rawBody, sigHeader)) return res.sendStatus(401);

  // 2) per-user yetki: actor.role'e göre karar ver (imzalı gövde → güvenilir)
  if (body.actor?.role !== 'manager')
    return res.json({ decision: 'deny', message: 'Bu işlemi yalnız yönetici yapabilir.' });

  // 3) kapanış-öncesi veri: includeData:true ise body.data hazır; değilse target.id ile çek
  //    (kapanıştan SONRA kaynak kaybolur → karar anında includeData ile tek seferde al)
  const table = body.data ?? (await fetch(`${BASE}/plugin-api/tables/get?id=${encodeURIComponent(body.target.id)}`,
                            { headers: { authorization: 'Bearer ' + apiKey } }).then((r) => r.json())).data;

  // 4) iş + karar (formData kullanıcı girdisi, table otoriter veri)
  if (!body.formData?.courierCalled)
    return res.json({ decision: 'deny', message: 'Önce kuryeyi çağırın.' });
  const invoiceNo = await issueInvoice(table);
  return res.json({ decision: 'allow', message: 'Onaylandı.', attach: { invoiceNo } });
});

// allow → işlem devam eder (attach yalnız whitelist alanlara: invoiceNo/reference/note).
// deny  → işlem iptal, message kullanıcıya gösterilir (düz metin).
// timeout/hata → manifest.failMode:  closed (varsayılan) = deny · open = allow.
//
// SENİN SORUMLULUĞUN yalnız bu { decision, message, attach } cevabını dönmek.
// Consent ekranı, onay-fişi ve enforcement RESTOMENUM tarafındadır — sen yapmazsın.

Manifest

"hooks": [{
  "action": "table.close", "blocking": true,
  "failMode": "closed", "enforce": true, "timeoutMs": 5000,
  "includeData": true,            // gate gövdesine kapanış-öncesi kanonik data göm (ayrı fetch gerekmez)
  "ui": { "kind": "form", "form": {
    "fields": [ { "key": "courierCalled", "type": "checkbox", "label": { "tr": "Kurye çağrıldı" } } ],
    "submitLabel": { "tr": "Onayla" }
  } }
}]

Editörde ui.formId ile bir form seçersiniz; portal yayında formu inline gömer (yukarıdaki ui.form). Manifest şeması: Hook'lar (Akış Kontrolü).

attach yalnız izinli (whitelist) alanlarla çekirdek payload'una işlenir; rastgele alan enjekte edemezsiniz.