Fatura Kesme (invoice.issue) ⏳ Yakında

Eklentiniz, tenant'ın bağladığı fatura sağlayıcısı eklenti (Paraşüt/GİB entegratörü) üzerinden e-fatura/e-arşiv kestirebilir — sağlayıcının kimliğini bilmeden. messaging.send ile aynı eklentiler-arası capability altyapısını kullanır; tutarlar KURUŞ (integer) taşınır ve sonuç ASENKRON invoice.status event'iyle (issued/paid/void/failed) döner.

1 · Model

Tüketici fatura ister, sağlayıcıyı tenant seçer (Android chooser modeli). Platform-tarafı fatura altyapısı yoktur — sağlayıcı GİB/entegratöre keser. Alıcı { customerId } opak referanstır (PII değil); sağlayıcı fatura başlığını (VKN/unvan/adres) kendi customers:read'iyle çözer. Scope'lar: capability:invoice.issue:consume (tüketici) · capability:invoice.issue:provide (sağlayıcı).

Ortak model + sağlayıcı olma rehberi (dedupe, imza, async durum): Yetenekler — Genel Bakış. invoice.issue, messaging.send ve notify.staff ile aynı platform altyapısını kullanır: sağlayıcı bağlama, idempotency, imzalı relay, breaker, kota, kill-switch, per-tüketici blok. Manifest'te provides:[{capability:"invoice.issue"}] (sağlayıcı) veya consumes:[{capability:"invoice.issue"}] (tüketici) beyan edin; türetilmiş scope otomatik eklenir.
Para her zaman KURUŞ (integer). amount.total ve kalem unitPrice tam sayı kuruştur — 123.45 gibi ondalık gönderme; 12345 yaz (= ₺123,45). Float para = yuvarlama/mali hata; platform ondalığı invalidAmount ile reddeder.

2 · Tüketici — fatura kestir

client.capabilities.invoke('invoice.issue', …)
import { RestomenumClient } from '@restomenum/plugin-sdk';   // v1.9.0+

// TÜKETİCİ (scope: capability:invoice.issue:consume) — jenerik capability yolu.
// Sonuç tipi (InvoiceIssueResult) capability id'den OTOMATİK türer; elle <> tip argümanı GEREKMEZ.
const r = await client.capabilities.invoke('invoice.issue', {
  to: { customerId: 'cust_123' },        // OPSİYONEL opak referans; sağlayıcı customers:read ile çözer
  amount: { total: 12345, currency: 'TRY' },  // total KURUŞ (integer) → 12345 = ₺123,45. ondalık YOK
  items: [                               // opsiyonel kalemler (unitPrice de KURUŞ)
    { description: 'Adana Kebap', quantity: 2, unitPrice: 4500 },
    { description: 'Ayran', quantity: 3, unitPrice: 1115 },
  ],
  reference: 'packet_789',               // opsiyonel dış referans (paket/sipariş no)
  idempotencyKey: `invoice-${packetId}`,  // ZORUNLU — çift fatura koruması (mali/yasal risk)
});
// r = { requestId, status: 'accepted'|'issued'|'failed', providerMessageId?, idempotentReplay? }
// 'accepted' = kesim kuyruğa alındı (sonuç invoice.status event'iyle gelir); 'issued' = anında kesildi.
// Sağlayıcı bağlı değilse ApiError('plugin.invoice.noProvider', 424) → özelliği gizle.
to yalnız opak { customerId } alır — ham müşteri PII (ad/VKN/telefon/adres) yasaktır (rawPiiForbidden). Ancak items[].description ve reference serbest metindir: içine kişisel veri gömmeyin (sağlayıcıya aynen gider). Fatura başlığı için gereken müşteri bilgisini sağlayıcı kendi customers:read'iyle customerId'den çözer.

3 · Sağlayıcı — fatura isteklerini işle

verifyAndParseCapability<InvoiceIssuePayload>
import { verifyAndParseCapability, capabilityResponse } from '@restomenum/plugin-sdk';
import type { InvoiceIssuePayload } from '@restomenum/plugin-sdk';

// SAĞLAYICI (scope: capability:invoice.issue:provide) — actionUrl'e gelen imzalı type:"capability" POST:
const req = await verifyAndParseCapability<InvoiceIssuePayload>(rawBody, headers['x-restomenum-signature'], {
  getSecret: (tenantId) => installStore.find(tenantId)?.webhookSecret,   // webhook ile AYNI imza şeması
});
if (!req) return res.status(401).json({ error: 'invalid_signature' });
// ZORUNLU dedupe: aynı req.requestId tekrar gelirse YENİDEN KESME (önceki yanıtı dön) — mükerrer fatura yasal sorun.

const { to, amount, items, reference } = req.payload;
// amount.total KURUŞ (integer). to.customerId → KENDİ customers:read'inle VKN/unvan/adres çöz (fatura başlığı).
// … GİB/e-fatura entegratörüne (Paraşüt/Uyumsoft…) kes …
res.json(capabilityResponse('accepted', { providerMessageId: invoiceNo })); // 'accepted' (async) veya 'issued' (anında)
Mükerrer kesim yasal/mali risktirrequestId ile ZORUNLU dedupeyapın; aynı istek tekrar gelirse yeniden kesmeyin, önceki fatura numarasını dönün. Tutarları KURUŞ integer olarak işleyin; cross-tenant kontrolü yapın (tenantId imzayla doğrulanır).

4 · Asenkron durum (invoice.status)

messaging DLR'ı gibi, fatura sonucu asenkron gelir (GİB onayı dakikalar sürebilir). Sağlayıcı reportStatus ile bildirir; platform yalnız istek sahibi tüketiciye hedefli invoice.status event'i teslim eder (broadcast yok).

capabilities.reportStatus + invoice.status event
import type { InvoiceStatusReport, InvoiceStatusEventData } from '@restomenum/plugin-sdk';

// SAĞLAYICI — GİB sonucu geldiğinde (dakikalar sürebilir) asenkron durum raporla:
await client.capabilities.reportStatus('invoice.issue', {
  requestId,                       // invoke yanıtındaki requestId
  status: 'issued',                // 'issued' | 'paid' | 'void' | 'failed'
  providerMessageId: invoiceNo,    // fatura no/UUID
} satisfies InvoiceStatusReport);

// TÜKETİCİ — invoice.status event'ine abone ol (yalnız İSTEK SAHİBİNE hedefli teslim, broadcast YOK):
// webhook handler'ında env.type === 'invoice.status':
const data = env.data as InvoiceStatusEventData;   // { requestId, status, providerMessageId?, error? }
// data.status: issued→paid ileri gider; void/failed terminal. requestId ile kendi kaydınla eşle.
Durum makinesi: issuedpaid ileri gider; void (iptal) / failed (kesim hatası) terminaldir. Platform sırasız/gerileyen raporu yok sayar (monoton guard). Event at-least-once'tır — aynı (requestId, status) tekrarında zarf id'si aynıdır; tüketici id ile dedupe etsin.

5 · Hata kodları

KodHTTPAnlamı
plugin.invoice.noProvider424Tenant fatura sağlayıcısı bağlamamış — önkoşul eksik. Özelliği gizle; retry etme.
plugin.invoice.providerUnavailable503Sağlayıcı inaktif/askıda/breaker-open/ulaşılamaz.
plugin.invoice.timeout504Sağlayıcı 10 sn içinde yanıtlamadı. AYNI idempotencyKey ile retry.
plugin.invoice.invalidAmount400amount.total pozitif integer (KURUŞ) olmalı — ondalık/float yasak.
plugin.invoice.invalidCurrency400currency yalnız TRY|USD|EUR.
plugin.invoice.invalidItems400items[]: description(≤200), quantity(pozitif integer), unitPrice(KURUŞ, ≥0); en çok 100 kalem.
plugin.invoice.rawPiiForbidden400to yalnız {customerId} alır; ham müşteri PII (ad/VKN/adres) yasak.
plugin.invoice.idempotencyKeyRequired400idempotencyKey eksik/geçersiz (zorunlu, ≤64).
plugin.invoice.consumerBlocked403Tenant, eklentinizin fatura kesmesini engellemiş.
plugin.invoice.duplicateInProgress409Aynı idempotencyKey eşzamanlı işleniyor.
plugin.invoice.idempotencyKeyReused409Aynı key FARKLI tutar/içerikle kullanıldı.
plugin.invoice.providerChanged409Belirsiz sonuçtan (timeout/unreachable) sonra tenant fatura sağlayıcısını DEĞİŞTİRDİ — orijinal sağlayıcı faturayı kesmiş olabilir; çift faturaya karşı bu key ile retry engellendi. Sonucu manuel uzlaştırın (yeni idempotencyKey ile yalnız kesilmediğinden EMİN olunca).
plugin.invoice.suspended403Eklentiniz kill-switch ile askıya alınmış — fatura kesemez.
plugin.invoice.selfTarget409Tüketici == bağlı sağlayıcı (kendine yönlendirme; binding çakışması).
plugin.capability.notFound404Bilinmeyen capability (jenerik /capabilities/{cap}/* ucunda).

status:'failed' (senkron) sağlayıcının kesim reddidir (HTTP 200; iş sonucu). Transport hataları (timeout/unreachable) success:false döner — AYNI idempotencyKey ile retry edin. Çift kesim koruması sağlayıcı-tarafı requestId dedupe'undadır (aynı requestId aynı sağlayıcıya gider → sağlayıcı tekrar kesmez); ledger yalnız tekrar-relay'i yönetir, sağlayıcının işini geri almaz. Belirsiz sonuçtan sonra tenant sağlayıcıyı değiştirirse retry plugin.invoice.providerChanged(409) alır (çift fatura engeli) — bu durumda manuel uzlaştırma gerekir.

6 · Fişe basma — receiptExtras

Fatura/mali belge kestiniz; peki fişe basılacak veriyi (mali blok, belge no) platforma nasıl iletirsiniz? Kapanış gate'inin yanıtıyla. table.close / packet.close gate'inde allow ile birlikte receiptExtras dizisi dönersiniz — öğeler kapanış yazılmadan önce adisyona işlenir. İkinci bir API çağrısı yoktur; karar ile veri aynı yanıtta gider.

tse.* namespace'i bu scope'a bağlıdır. Mali (KassenSichV/TSE) blok key'lerini yalnız capability:invoice.issue:provide yetkisi olan eklenti yazabilir; yetkisiz eklentinin tse.* key'i sessizce düşürülür ve hiçbir öğe verified işaretini almaz — sahte mali blok bastırılamaz. Şema, sınırlar (QR için 2000 karakter) ve yazım kuralları: Fişe Ek Alanlar (receiptExtras).