Mali Fişleme — fiscal.de ⏳ Yakında

Almanya KassenSichV / TSE fiş imzalama yeteneği. Tüketici eklenti 'bu fişi imzala' der, platform tenant'ın bağladığı TSE sağlayıcı eklentiye relay eder; sağlayıcı fişi imzalar ve mali bloğu (QR, Beleg-Nr, Sig-Zähler, TSE zamanları) döndürür. invoice.issue'dan AYRI bir yetenektir ve SENKRONDUR — asenkron durum event'i yoktur.

← Eklentiler-Arası Yetenekler · Mali bloğun fişe taşınması: receiptExtras

Yayına hazırlanıyor. Yetenek, backend dağıtımı tamamlandığında etkinleşir. Manifest şeması değişmez: provides/consumes: [{ capability: "fiscal.de" }] beyan edersiniz, capability:fiscal.de:consume|provide scope'ları otomatik türetilir.

invoice.issue ile karıştırmayın

invoice.issuefiscal.de
Ne yaparE-fatura/e-arşiv keser (GİB)Fişi TSE ile imzalar (KassenSichV)
ÇerçeveTR vergi mevzuatıAlman KassenSichV / DSFinV-K
SonuçFatura numarası/URL, asenkron durumMali blok: QR + Beleg-Nr + Sig-Zähler
AkışAsenkron (invoice.status event'i)Senkron (yanıtta biter)

İkisi ayrı capability'dir; bir eklenti ikisini birden sağlayabilir ama scope'ları ve sözleşmeleri ayrıdır. Detay: Fatura Kesme (invoice.issue).

Scope'lar

ScopeKim alırNe sağlar
capability:fiscal.de:consumeKasa/POS tarafı eklentiPOST /plugin-api/capabilities/fiscal.de/invoke çağırabilir
capability:fiscal.de:provideTSE sağlayıcı eklentiPlatformdan imzalı type:"capability" isteği alır + receiptExtras'ta tse.* yazabilir
PII sınıfı DEĞİLDİR (providerIsPii: false): taşınan veri tutar, KDV oranı ve ödeme tipidir — müşteri referansı bile yoktur. Bu yüzden kurulumda dataConsent gerektirmez.

Payload — fiscal.de@v1

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

{
  "payload": {
    "receiptType": "RECEIPT",
    "amountsPerVatRate": [
      { "vatRate": 19, "amount": 1990 },
      { "vatRate": 7,  "amount": 700 }
    ],
    "amountsPerPaymentType": [
      { "paymentType": "CASH",     "amount": 1690 },
      { "paymentType": "NON_CASH", "amount": 1000 }
    ],
    "reference": "masa-5",
    "idempotencyKey": "close-masa5-1785438065"
  }
}
AlanTipZorunluKural
receiptTypestringhayırRECEIPT (varsayılan). DSFinV-K Belegtyp; yeni tip eklemek geriye-uyumlu.
amountsPerVatRatearrayevet1–50 satır; { vatRate, amount }.
…[].vatRatenumberevet0–100; ondalık izinli (DE: 19 / 7 / 0; tarihsel 10.7 / 5.5).
…[].amountintegerevetCENT (ondalık DEĞİL). Negatif izinli.
amountsPerPaymentTypearrayevet1–50 satır; { paymentType, amount }.
…[].paymentTypestringevetCASH | NON_CASH.
…[].amountintegerevetCENT, negatif izinli.
referencestringhayır≤100 — adisyon referansı (masa/paket id).
idempotencyKeystringevet≤64. Aşağı bkz.
Para CENT (integer). 19.90 değil 1990 — ondalık gönderilirse istek reddedilir.
  • Negatif tutar izinlidir — Storno/iade fişleri de imzalanır, DSFinV-K bunu gerektirir.
  • Toplam denge (KDV toplamı == ödeme toplamı) platform tarafından kontrol EDİLMEZ. Platform minimal şema doğrular; denge kuralı sağlayıcının/TSE'nin işidir (avans/iade senaryolarında yanlış pozitif üretirdi). Dengesiz fiş sağlayıcı tarafından reddedilir.
  • idempotencyKey zorunludur çünkü aynı fişin iki kez imzalanması TSE'de mükerrer işlem = mali/yasal sorundur. Aynı key + farklı içerik409 (keys_reused deseni).
Tüketici — SDK ile (tipli)
import { RestomenumClient } from '@restomenum/plugin-sdk';

// Tüketici (scope: capability:fiscal.de:consume) — tutarlar CENT (integer):
const r = await client.capabilities.invoke('fiscal.de', {
  amountsPerVatRate:     [{ vatRate: 19, amount: 1990 }],   // 19,90 € → 1990
  amountsPerPaymentType: [{ paymentType: 'CASH', amount: 1990 }],
  reference: tableId,
  idempotencyKey: `close-${tableId}-${docNo}`,             // ZORUNLU
});

if (r.status === 'signed')  { /* mali blok hazır */ }
if (r.status === 'ausfall') { /* HATA DEĞİL — fişe "Ausfall" basılır */ }

Sağlayıcı tarafı — isteği karşıla

Platform, tenant'ın bağladığı sağlayıcının actionUrl'üne (yoksa webhookUrl) imzalı type:"capability" POST atar. Ortak model ve binding kuralları: Sağlayıcı olma rehberi.

Sağlayıcı ucu (fiscal.de)
import { verifyAndParseCapability, capabilityResponse } from '@restomenum/plugin-sdk';
import type { FiscalDePayload, FiscalDeStatus } from '@restomenum/plugin-sdk';

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

// 2) requestId DEDUPE (ZORUNLU): aynı fişi İKİ KEZ imzalamak TSE'de mükerrer işlem = mali/yasal sorun.
const prior = await store.find(req.requestId);
if (prior) return res.json(capabilityResponse(prior.status, { providerMessageId: prior.providerMessageId }));

// 3) Dengeyi SEN doğrula — platform KDV/ödeme toplamını kontrol etmez.
const sum = (rows) => rows.reduce((t, r) => t + r.amount, 0);
if (sum(req.payload.amountsPerVatRate) !== sum(req.payload.amountsPerPaymentType))
  return res.json(capabilityResponse<FiscalDeStatus>('failed', { error: { code: 'unbalanced_receipt' } }));

// 4) TSE'ye ulaşılamıyorsa 'ausfall' — 'failed' DÖNME (işlem geçerli, fişe "Ausfall" basılır).
if (!(await tseReachable())) return res.json(capabilityResponse<FiscalDeStatus>('ausfall'));

const providerMessageId = await signAtTse(req.payload);   // CENT tutarlar, negatif olabilir (storno)
await store.save(req.requestId, { status: 'signed', providerMessageId });
res.json(capabilityResponse<FiscalDeStatus>('signed', { providerMessageId }));
requestId dedupe pazarlık konusu değildir (inceleme kriteri): aynı fişin iki kez imzalanması TSE'de mükerrer işlem demektir. Aynı requestId tekrar gelirse işi yeniden yapma, önceki providerMessageId ile aynı yanıtı dön.
Dengeyi sen doğrula. Platform KDV toplamı ile ödeme toplamının eşitliğini kontrol etmez (avans/iade senaryolarında yanlış pozitif üretirdi) — dengesiz fişi reddetmek sağlayıcının işidir. Çalışan referans: examples/sample-plugin/src/routes/capabilityRoute.mjs.

Yanıt durumları — capability SENKRONDUR

DurumAnlamı
acceptedİstek alındı, işleniyor.
signedTSE imzaladı — mali blok hazır.
ausfallTSE arızası/erişilemez. HATA DEĞİLDİR: işlem geçerli şekilde arıza modunda tamamlandı; KassenSichV fişe "Ausfall" basılmasını ister.
failedİmza alınamadı (iş reddi).
Asenkron durum event'i YOKTUR. invoice.issue'daki invoice.status gibi bir event beklemeyin; POST /plugin-api/capabilities/fiscal.de/status ucu bu yetenek için 404 döner. İmza senkron tamamlanır, arıza durumu da senkron yanıtta bildirilir.
ausfall bir hata değildir. TSE'ye ulaşılamadığında işlem geçerli şekilde arıza modunda tamamlanır ve KassenSichV fişe "Ausfall" basılmasını ister. Bunu failed gibi ele alıp işlemi geri sarmayın — fişi arıza işaretiyle bastırın.

Mali blok fişe nasıl gider

fiscal.de imzayı üretir; fişe taşıma kapanış gate'inde receiptExtras ile yapılır. Sağlayıcı eklenti table.close / packet.close gate'inde allow + receiptExtras döner:

{ "decision":"allow",
  "receiptExtras":[
    { "key":"tse.qr",               "type":"qr",   "value":"V0;…" },
    { "key":"tse.txNumber",         "type":"text", "label":"Beleg-Nr",   "value":"366" },
    { "key":"tse.signatureCounter", "type":"text", "label":"Sig-Zähler", "value":"774" },
    { "key":"tse.timeStart",        "type":"text", "label":"TSE-Start",  "value":"1785438065" },
    { "key":"tse.status",           "type":"text", "value":"signed" }
  ] }
tse.* namespace'inin sahibi bu yetenektir: yalnız capability:fiscal.de:provide scope'lu eklenti bu key'leri yazabilir. invoice.issue sağlayıcısı dahil, başka hiçbir eklenti yazamaz — aksi halde fişe sahte mali blok bastırılabilirdi. Yazılan öğeler verified: true işareti alır ve fiş şablonu mali bloğu yalnız bu öğelere basar.

Hata kodları

messageHTTPNe zaman
plugin.fiscal.missingParams400payload yok / obje değil.
plugin.fiscal.invalidReceiptType400receiptType whitelist dışı.
plugin.fiscal.invalidVatRates400KDV satırları boş/bozuk, oran 0–100 dışı, tutar cent-integer değil.
plugin.fiscal.invalidPaymentTypes400Ödeme satırları boş/bozuk, tip CASH/NON_CASH dışı.
plugin.fiscal.invalidReference400reference > 100 karakter.
plugin.fiscal.idempotencyKeyRequired400idempotencyKey yok/boş/uzun (≤64).

Ortak capability hataları (scope reddi, sağlayıcı bağlı değil, sağlayıcı erişilemez, timeout, idempotency yarışı) mevcut sözleşmedeki kodlarla aynıdır — hata ailesi plugin.fiscal.*: Yetenek hata kodları.