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.
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 zaman | Kapandıktan sonra | Kapanıştan önce |
| Doğa | Async bildirim | Senkron kapı (akışı durdurur) |
| Sizin cevabınız | (yok) | { decision:"allow"|"deny" } |
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
}| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| type:"hook" | literal | ✓ | Webhook event'lerinden ayırt edin (aynı uca düşebilir). |
| event | string | ✓ | Hangi gate (table.close). |
| stage | "before" | ✓ | Akış öncesi gate. |
| tenantId | string | ✓ | Hangi 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. |
| data | object | – | Yalnı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. |
| formData | object | – | Kullanı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. |
| timeoutMs | number | ✓ | Cevap bütçeniz (1–10 sn) — aşarsanız failMode devreye girer. |
| occurredAt | number | ✓ | Unix ms. |
| hookId | string | ✓ | Çift-işlem koruması (idempotency) / log. |
context (total/tableName) yok? Güvenlik: client değerlerine güvenmeyiz. Gereken veriyi otoriter kaynaktan kendiniz çekersiniz (↓) — Stripe/Shopify deseni.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:readtarget.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)
} }// HTTP 200 + JSON
{ "decision": "allow" | "deny",
"message": "Kullanıcıya gösterilecek metin",
"attach": { "invoiceNo": "ABC123" } } // opsiyonel; whitelist: invoiceNo / reference / noteattach güvenli alanlara işlenir (ör. fişin invoiceNo'su).message kullanıcıya (düz metin).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.X-Restomenum-Signature → ham gövde üzerinden webhookSecret ile HMAC-SHA256 (±5 dk). Doğrulanmazsa 401. Algoritma/kod: İmza Doğrulama.
// /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."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.