Eklentiler-Arası Yetenekler (Capability) ⏳ Yakında

Bir eklenti, başka bir eklentinin yeteneğini onun kimliğini bilmeden kullanır: tüketici "mesaj gönder" / "fatura kes" ister, platform tenant'ın bağladığı sağlayıcıya yönlendirir. Bu sayfa ortak modeli (tüketici + sağlayıcı olma rehberi) anlatır; her yeteneğin kendi sözleşmesi için alt sayfalara bak.

Model — tüketici, sağlayıcı, binding

İki rol var: tüketici (yeteneği çağırır — capability:<id>:consume) ve sağlayıcı (yeteneği sağlar — capability:<id>:provide). Eklentiler birbirini asla doğrudan çağırmaz; tek aracı platform runtime'ıdır. Tenant her yetenek için bir sağlayıcı bağlar (tek aday varsa otomatik; panelden değiştirilebilir) — tüketici hangi sağlayıcının bağlı olduğunu bilmez.

Tüketici eklenti          Restomenum (platform)              Sağlayıcı eklenti
(rezervasyon, POS…)                                            (WhatsApp / e-fatura…)
     │                              │                                │
     │ POST /plugin-api/            │                                │
     │  capabilities/{cap}/invoke   │                                │
     │ Bearer <apiKey>              │                                │
     │─────────────────────────────►│ scope + ham-PII reddi          │
     │                              │ + kota + idempotency ledger    │
     │                              │ tenant'ın BAĞLADIĞI sağlayıcı  │
     │                              │ (binding / tek-adayda auto-bind)│
     │                              │                                │
     │                              │ imzalı POST type:"capability"  │
     │                              │───────────────────────────────►│ imza doğrula (webhook ile aynı)
     │                              │                                │ requestId DEDUPE (zorunlu)
     │                              │  { status, providerMessageId } │ opak referansı çöz + işi yap
     │                              │◄───────────────────────────────│ (customers/users:read + consent)
     │  { requestId, status,        │                                │
     │    providerMessageId }       │                                │
     │◄─────────────────────────────│                                │
     │                              │  (async) POST .../status       │ nihai sonuç (DLR / GİB)
     │  event: <cap>.status ◄───────│◄───────────────────────────────│
Manifest beyanı = izin. Tüketici consumes: [{ capability }], sağlayıcı provides: [{ capability }] beyan eder; türetilmiş capability:<id>:* scope'u otomatik eklenir (elle scope seçilmez). Portal manifest editöründe "Eklentiler-Arası Yetenekler" kartından işaretle. Sağlayıcı PII sınıfıysa (messaging) kurulumda tenant açık consent verir.

Kayıtlı yetenekler

Platform-tanımlı yetenekler (her biri kendi payload/durum sözleşmesine sahip):

YetenekİşPIISenkron durumAsenkron durum event'i
messaging.sendMüşteriye mesaj (WhatsApp/SMS)Sağlayıcı PIIaccepted · sent · failedmessaging.message.status: sent·delivered·read·failed
notify.staffPersonele bildirimaccepted · sent · failedyok (fire-and-forget)
invoice.issueE-fatura/e-arşiv kesaccepted · issued · failedinvoice.status: issued·paid·void·failed
fiscal.deFişi TSE ile imzala (KassenSichV)accepted · signed · ausfall · failedyok (SENKRON; /status → 404)
fiscal.deinvoice.issue — karıştırmayın (⏳ yakında). invoice.issue e-fatura/e-arşiv belgesi keser (GİB, asenkron invoice.status event'i); fiscal.de fişi TSE ile imzalar (KassenSichV, senkron — yanıtta biter, durum event'i yok, /status ucu 404). Ayrıca fiscal.de sağlayıcısı fişe mali bloğu receiptExtras ile yazar — tse.* namespace'inin sahibi yalnız capability:fiscal.de:provide'dır (invoice.issue sağlayıcısı dahil kimse yazamaz).

Tüketici — bir yeteneği çağır

Jenerik uç: POST /plugin-api/capabilities/{capabilityId}/invoke. SDK ile client.capabilities.invoke('<id>', payload) — payload tipi id'den türer, idempotencyKey her istekte zorunlu.

İstek
POST {RESTOMENUM_BASE}/plugin-api/capabilities/{capabilityId}/invoke
Authorization: Bearer <apiKey>
Content-Type: application/json

{ "payload": {
    "to": { "customerId": "c_…" },      // capability-özel; ham PII (telefon/VKN) YASAK
    "idempotencyKey": "iş-anlamlı-key",  // ZORUNLU (≤64) — çift işlem koruması
    "…": "…"                              // messaging: text/template · invoice: amount/items · notify: title/body
} }
Yanıt
{ "success": true, "data": {
    "requestId": "req_…",                 // idempotencyKey'den deterministik
    "status": "accepted",                 // capability-özel senkron durum (aşağıdaki tablo)
    "providerMessageId": "…",             // sağlayıcının kimliği (varsa)
    "idempotentReplay": false             // true → önceki başarılı isteğin tekrarı (sağlayıcı YENİDEN çağrılmadı)
} }
Zarif bozulma: plugin.<pfx>.noProvider (424) = tenant henüz sağlayıcı bağlamamış. Bu bir hata değil, önkoşul eksikliğidir — özelliği gizle, retry etme. accepted teslim DEĞİLDİR; nihai sonuç asenkron <cap>.status event'iyle gelir.

Sağlayıcı olma rehberi

Sağlayıcı eklenti, provides ettiği yetenek için platformdan imzalı capability isteği alır (actionUrl'üne, yoksa webhookUrl'üne). İmza şeması webhook ile birebir aynıdır.

Platform → sağlayıcı (imzalı istek)
POST <sağlayıcının actionUrl'ü (yoksa webhookUrl)>
X-Restomenum-Signature: t=<unixSec>,v1=HMAC_SHA256(webhookSecret,"<t>.<rawBody>")
X-Restomenum-Event: capability
X-Restomenum-Capability: invoice.issue
X-Restomenum-Request: req_…

{ "type": "capability", "capability": "invoice.issue", "v": 1,
  "environment": "sandbox",               // "sandbox" | "production" — imzalı gövdede
  "tenantId": "…", "requestId": "req_…",
  "consumer": { "pluginId": "…" },        // isteği yapan tüketici (kota/metering)
  "occurredAt": 0,
  "payload": { … } }                       // platform allowlist'inden geçmiş (ham PII yok)

Adımlar (SDK ile):

Sağlayıcı uç (verifyAndParseCapability + dedupe + capabilityResponse)
import { verifyAndParseCapability, capabilityResponse } from '@restomenum/plugin-sdk';
import type { InvoiceIssuePayload, InvoiceIssueStatus } from '@restomenum/plugin-sdk';

// 1) İMZA + ŞEKİL doğrula (webhook ile AYNI HMAC şeması). null → 401 dön.
const req = await verifyAndParseCapability<InvoiceIssuePayload>(rawBody, sigHeader, {
  getSecret: (tenantId) => installStore.find(tenantId)?.webhookSecret,
});
if (!req) return res.status(401).json({ error: 'invalid_signature' });

// 2) requestId DEDUPE (ZORUNLU sözleşme — review kriteri): aynı requestId tekrar gelirse işi YENİDEN
//    YAPMA (mesaj/fatura), önceki providerMessageId ile AYNI yanıtı dön.
const prior = await store.find(req.requestId);
if (prior) return res.json(capabilityResponse(prior.status, { providerMessageId: prior.providerMessageId }));

// 3) İşi yap: to.customerId OPAK → gerekirse client.customers.get(customerId) (customers:read + PII consent).
const providerMessageId = await issueInvoiceAtGib(req.payload);
await store.save(req.requestId, { status: 'issued', providerMessageId });

// 4) Senkron yanıt — status capability'nin responseStatuses whitelist'inden olmalı (invoice: accepted|issued|failed).
res.json(capabilityResponse<InvoiceIssueStatus>('issued', { providerMessageId }));
requestId DEDUPE ZORUNLUDUR (review kriteri). Aynı requestId tekrar gelirse işi (mesaj/fatura) YENİDEN YAPMA — önceki providerMessageId ile aynı yanıtı dön. Timeout sonrası platform aynı istekle retry edebilir; dedupe yoksa çift SMS / çift fatura olur.

Nihai sonucu (DLR / GİB durumu) dakikalar sonra asenkron raporla:

Async durum raporu (reportStatus)
// invoice.issue / messaging.send ASYNC sonucu (dakikalar sonra): nihai durumu raporla →
// platform YALNIZ istek sahibi tüketiciye hedefli <cap>.status event'i teslim eder (broadcast yok).
await client.capabilities.reportStatus('invoice.issue', {
  requestId: req.requestId,        // capability isteğinde aldığın req_… (kendi fatura no'n DEĞİL)
  status: 'paid',                  // issued→paid ileri; void/failed terminal (MONOTON)
  providerMessageId,
});
// notify.staff fire-and-forget'tir (asenkron durum YOK).
Sağlayıcının senkron status'u yeteneğin responseStatuses whitelist'inden olmalı (messaging/notify → accepted|sent|failed, invoice → accepted|issued|failed). Serbest metin alanları (text, title, fatura description) PII taşıyabilir → yalnız amaçla işle, loglama/sızdırma yapma.

Hata kodları

Tam kod plugin.<errorPrefix>.<suffix>; <pfx> = capability id'nin İLK segmenti: messaging.send → plugin.messaging, notify.staff → plugin.notify, invoice.issue → plugin.invoice (ör. notify.staff için plugin.notify.noProvider, plugin.notify.staff.* DEĞİL). SDK'da CAPABILITY_ERRORS + capabilityErrorCode('<id>', suffix) tip-güvenli guard sağlar.

KodHTTPAnlam
plugin.scope.denied403Tüketicide capability:<id>:consume scope'u yok.
plugin.<pfx>.noProvider424Tenant sağlayıcı bağlamamış (ya da ≥2 aday, seçim yok). Önkoşul eksik → özelliği gizle, RETRY ETME.
plugin.<pfx>.providerUnavailable503Sağlayıcı inaktif/askıda/breaker-open/ulaşılamıyor. Aynı key ile sonra dene.
plugin.<pfx>.timeout504Sağlayıcı ≈10s içinde yanıtlamadı. AYNI idempotencyKey ile retry (belirsiz sonuç).
plugin.<pfx>.duplicateInProgress409Aynı idempotencyKey eşzamanlı işleniyor.
plugin.<pfx>.idempotencyKeyReused409Aynı key FARKLI içerikle kullanıldı (Stripe keys_reused paritesi).
plugin.<pfx>.providerChanged409Belirsiz sonuçtan (timeout) sonra tenant sağlayıcıyı DEĞİŞTİRDİ → çift-işlem (çift fatura/SMS) koruması, bu key ile retry engellendi. Manuel uzlaştır.
plugin.<pfx>.selfTarget409Tüketici == bağlı sağlayıcı (döngü tabanı).
plugin.<pfx>.suspended403Tüketici eklenti kill-switch ile askıya alınmış.
plugin.<pfx>.consumerBlocked403Tenant bu tüketiciyi panelden engellemiş.
plugin.<pfx>.rawPiiForbidden400to içinde customerId/userId dışında alan (ham PII).
plugin.<pfx>.idempotencyKeyRequired400idempotencyKey eksik/geçersiz (zorunlu, ≤64).
plugin.<pfx>.invalidPayload400Payload doğrulaması başarısız (nested metadata, geçersiz tutar/kanal/hedef).
plugin.capability.notFound404Bilinmeyen capability (jenerik /capabilities/{cap}/* ucunda).

Kota: capability çağrıları ortak cap:<id> kovasını kullanır (varsayılan 60/dk/install; dev-store projesinde 5/dk) — bkz. Limitler.

Yetenek sayfaları