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",
"environment": "sandbox", // "sandbox" | "production" — imzalı gövdede (header kopyası imzasız)
"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ış-anı snapshot'ı
"tableId": "masa-1", "tableName": "Masa 1", "docNo": 4, "desing": "Bahçe",
"location": "Masa 1", "personCount": 0,
"orders": [ { "id": "masa-1-5be4", "title": "Frozen", "quantity": 1, "lineTotal": 14 } ],
"payments": [ { "methodId": "29-cash", "title": "nakit", "amount": 14, "isDiscount": false } ],
"total": 14, "paid": 14, "totalDiscount": 0
},
"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. |
| environment | "sandbox" | "production" | ✓ | Teslimin ortamı (test/dev mağazası = "sandbox") — imzalı gövdede. Header kopyası imzasızdır; gövdedekini kullan. |
| 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",
"receiptExtras": [ // ⏳ yakında — YALNIZ kapanış gate'lerinde (table.close/packet.close)
{ "key": "tse.qr", "type": "qr", "value": "V0;…" },
{ "key": "tse.txNumber", "type": "text", "label": "Beleg-Nr", "value": "366" }
] }message kullanıcıya (düz metin).receiptExtras (⏳ yakında) → yalnız kapanış gate'lerinde (table.close / packet.close) ve yalnız allow'da: öğeler kapanış yazılmadan önce adisyona işlenir ve fişe basılabilir hale gelir (mali blok / belge no / sadakat satırı) — ikinci bir API çağrısı gerekmez. Şema, sahiplik (tse.*) ve sınırlar: Fişe Ek Alanlar.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", environment, tenantId,
// target:{ type:"table", id:"Masa 5" }, // environment = "sandbox" | "production" (imzalı gövdede)
// 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?, receiptExtras? } // receiptExtras ⏳ yakında (yalnız kapanış gate'lerinde)
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);
// Belgeyi fişe bağlamak için İKİNCİ bir API çağrısı YOK — karar ile veri aynı yanıtta (⏳ yakında):
return res.json({ decision: 'allow', message: 'Onaylandı.',
receiptExtras: [{ key: 'tse.txNumber', type: 'text', label: 'Beleg-Nr', value: String(invoiceNo) }] });
});
// allow → işlem devam eder. Adisyon belgesine YAZILAN tek alan receiptExtras'tır (kapanış gate'leri;
// tse.* için capability:fiscal.de:provide şart). message/level protokol alanıdır — belgeye yazılmaz.
// 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?, receiptExtras? } 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ü).
receiptExtras'tır. decision, message, level ve display protokol alanlarıdır: akışı yönetir / kasiyere bildirim gösterir, adisyon dokümanına yazılmaz. Fişe veri işlemek için kapanış gate'lerinde receiptExtras; tutar/kalem için update-orders · update-payments. Rastgele alan enjekte edemezsiniz.