Paket hesabı kapatılmadan önce sorulan before-action gate. table.close gibi TENANT-GENEL çalışır: SAHİPLİK ŞARTI YOKTUR — bu hook’u kaydeden her aktif eklenti, restoranın TÜM paket kapanışlarında (kendi oluşturmadığı paketler dahil) sorgulanır ve herhangi bir eklentinin deny’ı kapanışı engeller. allow/deny dönersiniz; deny → hesap kapanmaz, message personele gösterilir. transition YOKTUR. (Sahiplik kuralı yalnız packet.status.update için geçerlidir.)
| Hook key | packet.close |
| Scope | hooks:packet.close (consent-gated güçlü yetki) |
| Sahiplik | YOK — tenant-genel (table.close gibi); tüm paket kapanışlarında sorulur |
| Manifest | hooks: [{ event, ui, timeoutMs, failMode, includeData }] |
| Hedef | manifest actionUrl (yoksa webhookUrl) |
| Method | POST (senkron) |
| Rate limit | hook kovası (varsayılan 60/dk per-install) |
{
"event": "packet.close",
"ui": { "kind": "none" }, // gate genelde UI'sız karar; form/iframe da desteklenir
"timeoutMs": 5000, // cevap bütçesi [1s,10s]; aşımda failMode uygulanır
"failMode": "open", // VARSAYILAN: open (timeout/hata → kapanışa izin ver + uyar)
"includeData": true // gövdeye paketin kanonik verisi gömülür (packets/get şekli)
}Paket kanalı bugüne dek yalnız SUNUCU-İÇİ SENKRON gate kullanıyordu: kararın istendiği an istemci turunun DIŞINDA olduğu için kullanıcıya bir şey GÖSTEREMEZ (form açamaz, receiptExtras döndüremezdiniz). Masa kapanışındaki token modeli artık pakette de geçerli: panel önce hook’u çağırır (UI/form gösterebilir, receiptExtras dönebilirsiniz), aldığı gateToken’ı kapanış isteğine data.gateTokens[] içinde taşır; backend token’ı doğrular ve senkron çağrıyı ATLAR. Token: serverId + pluginId + event + refId + exp ile HMAC imzalı, ömrü 120 sn; YALNIZ gerçek allow token alır (degraded/failMode kaynaklı izin ALMAZ). Başka pakete/eklentiye/event’e/tenant’a ait token REDDEDİLİR; bozuk/süresi geçmiş token sessizce izne DÜŞMEZ. ⏳ Sandbox’ta yayında, production’a çıkmadı (panel henüz token göndermiyor). SENKRON YOL KALDIRILMADI: integration/closePacket ucunu api_admin token’lı dış POS ortakları da çağırıyor ve hook turunu yapmaz → token göndermeyen çağıran için senkron gate aynen çalışır.
Token yolunda sıra tersine döner (önce imza, sonra kapanış). Bu yüzden hook turu artık hesabın KAPANABİLİR olup olmadığını önceden kontrol eder: kapanamayacak bir hesap için table.close / packet.close hook’u hiç çalıştırılmaz ve plugin.hook.preconditionFailed döner — imzalanmış ama karşılığı olmayan fiş üretimini baştan engeller. Bu kapı stale okumaya dayanır (erken eleme); nihai karar kapanış anındaki taze okumadır → aradaki dar pencere table.close_aborted olayıyla telafi edilir (masa tarafında; paket karşılığı henüz yok).
Varsayılan open: endpoint’iniz cevap veremezse hesap kapatma engellenmez (degraded uyarısıyla devam). "closed" seçebilirsiniz ama ciddi sorumluluk: sunucunuz düştüğünde restoranın o paketlerdeki hesap kapatması bloklanır.
enforce bu hook’ta DESTEKLENMEZ (yok sayılır): gate çekirdek işlemin (hesap kapatma) içinde çalışır, ayrı sert doğrulayıcı yok. deny sunucuda kapanışı engeller; bu yeterli garantidir.
{
"type": "hook",
"event": "packet.close",
"stage": "before",
"tenantId": "<tenantId>",
"target": { "type": "packet", "id": "<packetId>" },
"data": {
"packetId": "<packetId>", "docNo": 12,
"customer": { "id": "c1", "name": "...", "phone": "..." },
"orders": [ { "id": "...", "title": "...", "quantity": 1, "options": [], "lineTotal": 130 } ],
"entegrasyon": "packet", "total": 130, "paid": 0, "totalDiscount": 0
},
"formData": { "<sizin-field-keyiniz>": "<personel-degeri>" }, // DİNAMİK: formunuzdaki key'ler; yalnız ui.kind form/iframe ise
"actor": { "userId": "<uid>", "role": "staff" },
"timeoutMs": 5000,
"occurredAt": 1718000000000,
"hookId": "hk_<uuid>"
}| Alan | Açıklama |
|---|---|
| target.id | Paket id’si. Veriyi packets/get?packetId= ile de çekebilirsiniz. (transition YOKTUR — bu hook kapanış öncesidir.) |
| data | Yalnız includeData:true ise. packets/get ile birebir kanonik şekil; customer yalnız customers:read + PII consent ile, data yalnız orders:read ile. |
| data.currency · data.amountExponent · data.orders[].amounts | Tutar alanları — gate gövdesi TAM gövdedir (özet DEĞİL) → okuma uçlarıyla aynı modelden gelir. currency ISO-4217; amountExponent YALNIZ amounts.* için (TRY → 2). amounts = TAMSAYI minor unit kırılımı { base, options, extra, discount, timer, gross, net, vat, perVat? }; garanti base+options+extra−discount+timer = gross ve net+vat = gross. ⚠️ total/paid/lineTotal ONDALIKTIR — üssü onlara uygulama (100 kat sapma). Para birimi çözülemezse alanlar hiç gelmez → ondalığa düş. Ayrıntı: /docs/api/amounts. |
| formData | Yalnız ui.kind form/iframe ise gelir. İçeriği DİNAMİKTİR: anahtarlar SİZİN formunuzda tanımladığınız field key’ler, değerler personelin girdiği değerlerdir (her eklentiye özel; sabit şema yok). ui.kind none ise hiç gelmez. |
| actor | Kapatmayı başlatan kullanıcı {userId, role} (manager/staff) — imzalı gövdede, güvenilir. |
| hookId | Bu çağrının id’si (loglama/dedup). |
{ "decision": "allow", "message": "Hesap kapatılabilir." }{ "decision": "deny", "message": "Önce kurye teslimini onaylayın." }{ "decision": "allow",
"receiptExtras": [
{ "key": "tse.qr", "type": "qr", "value": "V0;…" },
{ "key": "tse.txNumber", "type": "text", "label": "Beleg-Nr", "value": "366" }
] }| message | Anlam |
|---|---|
| plugin.hook.targetNotFound | Paket bulunamadı. |
| plugin.hook.notRegistered | Manifest’inizde bu hook yok. |
| plugin.scope.denied | hooks:packet.close onaylı değil. |
| plugin.hook.inactive | Kurulum pasif (kill-switch / billing / connect). |