Mali Fişleme — fiscal.de ✓ Canlı

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

Yeni manifest alanı yok. provides/consumes: [{ capability: "fiscal.de" }] beyan edersiniz; capability:fiscal.de:consume|provide scope'ları otomatik türetilir.
Bu tur KIRICIDIR: tüketici artık tutar göndermez (amountsPerVatRate / amountsPerPaymentType kaldırıldı), sale zorunlu oldu ve sepet satırına opsiyonel taxRate geldi.

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.

Tüketici isteği — tutar göndermezsiniz

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

// tutar alanı YOK — platform türetir
{
  "payload": {
    "sale": { "type": "packet", "id": "pkt_123" },   // ZORUNLU — imzalanacak satış
    "receiptType": "RECEIPT",                         // ops., varsayılan RECEIPT
    "registerId": "kiosk-1",                          // ops., ≤64, OPAK kasa/terminal kimliği
    "idempotencyKey": "close-pkt_123-37"              // ZORUNLU, satış başına SABİT
  }
}
AlanTipZorunluKural
saleobjectevetİmzalanan satış. Sağlayıcı tekleme anahtarını bundan kurar (packet:pkt_123); platform satışın tenantınızda olduğunu doğrular (yoksa 404).
sale.typestringevet⏳ Bugün yalnız packet kabul edilir. table bilinçli olarak desteklenmeyen tipler listesindedir ve ayrı bir hata koduyla reddedilir → masa satışları için capability yolu bugün mevcut değildir; masa mali bloğu kapanış gate'i üzerinden taşınır. (Tip yeniden açıldığında geçerli olacak tekleme tuzağı için aşağıdaki uyarı.)
sale.idstringevet≤100; doküman id'si olarak kullanılabilir olmalı — /, ., .., __x__ yasak.
receiptTypestringhayırRECEIPT (varsayılan). DSFinV-K Belegtyp; yeni tip eklemek geriye-uyumlu.
registerIdstringhayır≤64, opak kasa/terminal kimliği (kiosk-1). Sağlayıcının kasa (DSFinV-K Z_KASSE_ID) eşlemesinde anahtardır; platform yorumlamaz, olduğu gibi iletir. Tanınmayan değer imzayı engellemez — varsayılan kasaya düşülür.
idempotencyKeystringevet≤64. Parmak izi receiptType + sale: aynı key + farklı satış → 409. Aşağı bkz.
amountsPerVatRate
amountsPerPaymentType
—kaldırıldıGönderilirse sessizce yok sayılır (geçiş penceresi); sonraki sürümde 400.
reference
referenceType
—deprecatedsale yerini aldı; sessizce yok sayılır.
sale.type: "table" — tekleme tuzağı. tableId kat planındaki kalıcı masa kimliğidir: masa kapanınca kayıt silinir, aynı masa aynı id ile yeniden açılır — oturumları docNo ayırır. Sağlayıcı tekleme anahtarını yalnız table:masa-1 olarak kurarsa, öğlen imzalanan Beleg akşamki yeni satışta replay edilir ve o satış hiç TSE imzası almaz. Sağlayıcıysanız masa kanalında anahtara oturum ayracı ekleyin (docNo + iş günü). packet kanalında bu sorun yoktur — paket id'si işlem-tekildir.
Tutar GÖNDERMEZSİNİZ. KDV kırılımı ve ödeme kırılımı platform tarafından satış kaydından türetilir ve sağlayıcıya öyle iletilir. Gönderirseniz yok sayılır. Neden: mali kayıt POS kaydıyla birebir tutmak zorundadır (AO §146a / DSFinV-K) — tüketici beyanı ile POS kaydı ayrışırsa yanlış Zahlart ve yanlış ciro beyanı doğar.
idempotencyKey satış başına SABİTTİR — deneme sayacı EKLEMEYİN. Aynı fişin iki kez imzalanması TSE'de mükerrer işlem = mali/yasal sorundur. Parmak izi receiptType + sale: aynı key + farklı satış → 409 idempotencyKeyReused; aynı key eşzamanlı işleniyorsa 409 duplicateInProgress.
Tekrar deneme (replay). Aynı idempotencyKey ile ikinci çağrıda sağlayıcıya yeni istek gitmez; saklanan yanıt bloğuyla birlikte "idempotentReplay": true ile döner. Satış bu arada kapanıp silinmiş olsa bile replay çalışır — varlık kontrolü yalnız yeni isteklerde koşar.
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', {
  sale: { type: 'packet', id: packetId },        // ZORUNLU — hangi satış
  idempotencyKey: `close-${packetId}-${docNo}`,  // ZORUNLU, satış başına SABİT
});
// TUTAR GÖNDERMEZSİN: KDV + ödeme kırılımını platform satış kaydından türetir.

// Mali blok YANITTA gelir → fişi kendi basan eklenti (kiosk) kapanış gate'ini beklemez.
if (r.status === 'signed')  printReceipt(r.receiptExtras);   // tse.qr BYTE-KESİN basılır
if (r.status === 'ausfall') printReceipt(r.receiptExtras);   // HATA DEĞİL — tek öğe: tse.ausfall
Yanıt — mali blok YANITTA gelir
{ "success": true, "data": {
    "requestId": "req_9f1c…",
    "status": "signed",                     // accepted | signed | ausfall | failed
    "providerMessageId": "412",             // = Beleg-Nr
    "receiptExtras": [                      // MALİ BLOK — fişe olduğu gibi basılır
      { "key": "tse.qr",       "type": "qr", "value": "V0;…", "verified": true },
      { "key": "tse.txNumber", "value": "412", "label": "Beleg-Nr", "verified": true }
    ]
} }

// Aynı idempotencyKey ile 2. çağrı: sağlayıcıya YENİ istek GİTMEZ, saklanan yanıt döner
{ "success": true, "data": { "requestId": "req_9f1c…", "status": "signed",
    "providerMessageId": "412", "idempotentReplay": true, "receiptExtras": [ … ] } }

Tutarları platform nasıl türetir

Mali kayıt POS kaydıyla birebir tutmak zorunda olduğu için (AO §146a / DSFinV-K) kırılım tüketici beyanından değil satış dokümanından hesaplanır. Sağlayıcıya giden alan adları değişmedi — yalnız kaynağı platformdur.

AlanKaynakKural
sequence · sequenceScopeSatışın sıra sayacı (⏳ dev'de yayında)Zarftakiyle AYNI sözleşme: scope içinde monoton, atlanan numara olabilir. OPSİYONEL — çözülemezse alan HİÇ KONMAZ (null da gelmez) → o istek için karşılaştırmayı atla.
amountsPerPaymentTypeSatışın ödeme satırlarıYöntem tanımındaki cash bayrağı → CASH / NON_CASH; işaretsiz yöntem NON_CASH. İndirim satırları tahsilat sayılmaz.
amountsPerVatRateSipariş satırları × satırın KDV oranıSatır-içi indirim düşülmüş tutar; oranlar kova kova toplanır, YÜKSEK ORAN ÖNCE.
lines · linesOmittedSatışın sipariş satırları (⏳ dev'de yayında)Şekil kapanış gövdesindeki orders[] ile BİREBİR aynıdır (DSFinV-K satır ayrıntısı için). YA TAM YA HİÇ — KIRPILMAZ: 200 satırı aşan satışta liste HİÇ KONMAZ, yerine linesOmitted:"too_many_lines" gelir. Alanın YOKLUĞU "satır yok" DEĞİLDİR → ayrıntı gerekiyorsa tables/get · packets/get ile çek. İkisi birlikte gelmez.

Satırın KDV oranı, sepete yazılırken taxRate verildiyse o satırda dondurulur; verilmediyse ürünün oranı kullanılır (aşağıdaki "Sepet satırında KDV oranı" bölümü).

İndirim politikası. İndirim satırları amountsPerPaymentType'a girmez; KDV tarafında oranlara ciro payıyla orantılı dağıtılır (en-büyük-kalan yöntemi, kuruş kaybı yok).
Yayınlanan veriyle aynı dağıtım. Okuma uçlarında ve olaylarda gelen amounts.perVat kovaları aynı dağıtımdan üretilir → yayınlanan veri ile imzalanan belge çelişmez. Karışık oranlı satırda (seçeneğin oranı üründen farklıysa) kırılım satırda da görünür; kova sırası her iki yüzeyde de yüksek oran önce.
Denge invariantı. Platform Σ amountsPerVatRate == Σ amountsPerPaymentType olmasını garanti eder. En fazla 2 kuruşluk yuvarlama artığı en büyük KDV kovasına emilir; üzeri veri tutarsızlığı sayılır (amountsInvalid) ve imza atılmaz.
Fail-closed — sessiz varsayılan yok. Bir satırın KDV oranı çözülemiyorsa, bir ödeme satırının yöntem tanımı yoksa, satış satırsız ya da tahsilatsızsa, indirim ciroyu aşıyorsa veya iki taraf dengelenmiyorsa istek reddedilir ve imza atılmaz. Yanlış oranlı bir imzalı fiş geri alınamaz; bu yüzden tahmin edilmez.

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ının ALDIĞI istek (kırılımlar platformca dolduruldu)
POST <sağlayıcının actionUrl / webhookUrl'i>
x-restomenum-signature:   t=…,v1=…
x-restomenum-event:       capability
x-restomenum-capability:  fiscal.de
x-restomenum-request:     req_9f1c…
x-restomenum-environment: production

{
  "type": "capability", "capability": "fiscal.de", "v": 1,
  "environment": "production",              // | sandbox
  "tenantId": "<serverId>", "requestId": "req_9f1c…",
  "consumer": { "pluginId": "hellokiosk" },
  "occurredAt": 1786000000000,
  "payload": {
    "receiptType": "RECEIPT",
    "sale": { "type": "packet", "id": "pkt_123" },
    "amountsPerVatRate":     [ { "vatRate": 19, "amount": 1990 },        // ← platform türetti
                               { "vatRate": 7,  "amount": 700 } ],
    "amountsPerPaymentType": [ { "paymentType": "CASH",     "amount": 1690 },
                               { "paymentType": "NON_CASH", "amount": 1000 } ],
    "lines": [ { "id": "ln_1", "title": "Pizza", "quantity": 1, /* … orders[] ile AYNI şekil */ } ],
                                            // ⏳ satır listesi — YA TAM YA HİÇ (>200 satır → konmaz)
    "sequence": 14,                         // ⏳ satışın kaçıncı durum değişikliği (opsiyonel)
    "sequenceScope": "sq_5776130612ab"      // ⏳ opak satış hattı — karşılaştırma AYNI scope içinde
  }
}
Sağlayıcı ucu (saleKey teklemesi + fiscalResponse)
import { verifyAndParseCapability, fiscalResponse, fiscalSaleKey, receiptExtra } from '@restomenum/plugin-sdk';
import type { FiscalDePayload } 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, environment) => installStore.find(tenantId, environment)?.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(fiscalResponse(prior.status, {
  providerMessageId: prior.providerMessageId, receiptExtras: prior.receiptExtras,   // BLOĞU DA sakla
}));

// 3) SATIŞ ANAHTARI (tekleme): tipli referanstan kur — `packet:pkt_123`. Elle string birleştirme
//    YOK (tip öneki atlanırsa adı "5" olan masa ile id'si "5" olan paket aynı Vorgang sanılır).
//    requestId dedupe'u YETMEZ: POS kapanışı aynı satışı FARKLI requestId ile imzalatmaya gelir.
// ⚠️ type:'table' geldiğinde ANAHTAR YETMEZ: tableId kalıcıdır (masa kapanıp aynı id ile yeniden açılır)
//    → anahtara oturum ayracı ekle (`${fiscalSaleKey(sale)}:${isGunu}`), yoksa dünkü Beleg replay olur.
const saleKey = fiscalSaleKey(req.payload.sale);
const priorSale = await store.findBySale(saleKey);
if (priorSale) return res.json(fiscalResponse(priorSale.status, {   // Rechnungsdoppel — yeni Vorgang YOK
  providerMessageId: priorSale.providerMessageId, receiptExtras: priorSale.receiptExtras,
}));

// 4) Denge SAVUNMA KATMANI: platform Σ KDV == Σ tahsilat olmasını GARANTİ eder (dengesizse imza
//    atılmadan 400 plugin.fiscal.amountsInvalid döner) — yine de kendi kapını tut.
const sum = (rows) => rows.reduce((t, r) => t + r.amount, 0);
if (sum(req.payload.amountsPerVatRate) !== sum(req.payload.amountsPerPaymentType))
  return res.json(fiscalResponse('failed', { error: { code: 'unbalanced_receipt' } }));  // blok TAŞIMA

// 5) 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(fiscalResponse('ausfall', {
    receiptExtras: [receiptExtra('tse.ausfall', 'TSE-Ausfall: Beleg ohne TSE-Signatur')],
  }));

// 6) İmzala ve MALİ BLOĞU YANITTA DÖN. providerMessageId = Beleg-Nr.
const { belegNr, qr, sigCounter } = await signAtTse(req.payload);   // CENT tutarlar; negatif olabilir
const receiptExtras = [
  receiptExtra('tse.qr', qr, { type: 'qr' }),                  // BYTE-KESİN: kırpma/escape YOK
  receiptExtra('tse.txNumber', String(belegNr), { label: 'Beleg-Nr' }),
  receiptExtra('tse.signatureCounter', String(sigCounter), { label: 'Sig-Zähler' }),
];
await store.save(req.requestId, { status: 'signed', providerMessageId: String(belegNr), receiptExtras });
await store.saveBySale(saleKey, { status: 'signed', providerMessageId: String(belegNr), receiptExtras });
res.json(fiscalResponse('signed', { providerMessageId: String(belegNr), receiptExtras }));
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.
Denge artık platformun garantisidir — Σ amountsPerVatRate == Σ amountsPerPaymentType tutmuyorsa istek sağlayıcıya hiç gelmez (400 amountsInvalid). Sağlayıcıdaki denge kontrolü bir savunma katmanı olarak kalabilir. Çalışan referans: examples/sample-plugin/src/routes/capabilityRoute.mjs.
Platform RETRY YAPMAZ. Tekrar denemenin sorumluluğu idempotencyKey (tüketici) ve requestId (sağlayıcı) ile iki taraftadır. Tutarlar kuruş-integer'dır: 19.90 değil 1990.

Yanıt durumları — capability SENKRONDUR

statusAnlamıreceiptExtras
signedTSE imzaladı — mali blok hazır.Dolu
ausfallTSE arızası. HATA DEĞİLDİR: satış geçerli, KassenSichV fişe "Ausfall" basılmasını ister.Tek öğe (tse.ausfall)
acceptedSonuç taşımaz. Platform bunu TERMİNAL SAYMAZ: aynı key ile yeni çağrı sağlayıcıya tekrar gider (o requestId ile tekler).Genellikle boş
failedSağlayıcının iş reddi (HTTP 200, data.status içinde).Taşınmaz — imza atılmamıştır
accepted terminal DEĞİLDİR. Sonuç taşımaz; aynı idempotencyKey ile yapılan yeni çağrı sağlayıcıya tekrar gider (sağlayıcı onu requestId ile tekler). signed / ausfall gelene kadar fişi mali blok basılmış saymayın.
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

Mali blok receiptExtras ile taşınır ve iki yolu vardır — ikisi de aynı bloğu üretir:

YolNasılKim kullanır
capability yanıtıinvoke dönüşündeki receiptExtras (yukarıdaki yanıt örneği)Fişi kendi basan eklenti (self-servis kiosk) — kapanış gate'ini tetikleyemez
kapanış gate'itable.close / packet.close allow yanıtıPOS akışındaki mali eklenti
Gate yolu — allow + receiptExtras
{ "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.qr BYTE-KESİN basılır: kırpma / escape / satır kaydırma / normalize YASAK. Kırpılmış QR geçerli görünür ama doğrulanamaz — hiç QR olmamasından kötüdür. Sert alt/üst aralık kontrolü yoktur; yalnız 2000 karakterlik üst sınır vardır ve sınırı aşan öğe kırpılmaz, düşürülür.
failed yanıtında blok TAŞINMAZ (imza atılmamıştır); ausfall yanıtında blok tek öğedir: tse.ausfall — arıza fişte görünür kılınır.
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 (bunu platform üretir, sağlayıcının gönderdiği yok sayılır) ve fiş şablonu mali bloğu yalnız bu öğelere basar.
Gate yolunda scope yetmez — binding de aranır. Kapanış gate'i tenant-genel sorulur; tenant'ta iki fiskal sağlayıcı kuruluysa scope tek başına ikisine de tse.* yazdırırdı. Artık eklentinin o tenant'ta fiscal.de'nin bağlı sağlayıcısı olduğu da doğrulanır; değilse öğe sessizce düşer. Tek aday varsa davranış değişmez. Yazılmış bir key hiçbir yoldan üzerine yazılamaz.

Sepet satırında KDV oranı — taxRate

Almanya'da oran ürüne değil siparişe bağlı olabilir (yerinde tüketim %19 / götürü %7). Bu bilgi yalnız sipariş yüzeyinde doğar, platform türetemez — bu yüzden sepet satırı opsiyonel bir oran taşıyabilir. Dört uç aynı sepet şemasını kullanır: packets/create · packets/update-orders · tables/create · tables/update-orders.

AlanTipZorunluKural
taxRatenumberhayır0–100. Verilirse ürünün oranını ezer ve satırda dondurulur; verilmezse ürünün oranı kullanılır. Geçersiz değer → 400 (sessizce ürüne düşmez). Beyanı yapan eklenti satırda taxRateBy ile kaydedilir — bu iç denetim izidir, hiçbir okuma yanıtında dönmez (teyitli).
Fiyat otoritesi değişmedi. Sepet kalemi fiyat göndermez; fiyat, kuver ve total tenant'ın ürün kaydından hesaplanır. Yeni alan yalnız oranı etkiler ve satırda dondurulur: ürünün oranı sonradan değişse bile geçmiş satışın mali kaydı değişmez. Tenant başına kapatma bu turda yoktur; koruma katmanları 0–100 doğrulaması, satırda dondurma, taxRateBy izi ve orders:write scope'udur.

Aynı fişte iki oran

Beyan satır bazında olduğu için tek bir satışta birden çok KDV oranı bir arada gelebilir — götürü siparişte yemeğin indirimli orana çekilip alkolün %19'da kalması gibi. Paket seviyesinde tek bir bayrak bunu ifade edemezdi. Dondurulan oran, imzalamada kırılımın girdisidir: platform tutarları satış kaydından türetirken satırın beyan edilmiş oranını kullanır.

Sepet (tüketici gönderir) → sağlayıcıya giden kırılım
// Sepet (tüketici gönderir)
[ { "product": "POS-11", "quantity": 1, "taxRate": 19 },   // 10.00 ₺
  { "product": "POS-12", "quantity": 1, "taxRate": 7  } ]  //  5.00 ₺

// Sağlayıcıya giden kırılım (platform türetir)
"amountsPerVatRate": [
  { "vatRate": 19, "amount": 1000 },
  { "vatRate": 7,  "amount": 500  }
]  // kuruş-integer
Aralık neden serbest (0–100)? Platform çok ülkelidir (TR 20/10/1/0 · DE 19/7/10.7/5.5/0) ve tenant kaydında ülke alanı yok → ülkeye özel bir beyaz liste uygulanamıyor. Geçerli oranı siz beyan edersiniz; platform yalnız aralığı doğrular.
Değerlendirilen alternatif. Oran yerine olguyu taşımak (consumptionMode: dine_in | takeaway) mali kararı platformda tutardı ve daha doğru durur. Uygulanmadı: platformun ürün modelinde tek bir KDV oranı var; olgudan orana eşleme için ürünün ikinci bir oran taşıması, katalog ucunun ve panelin bu alanı alması ve tenant'ın her ürün için doldurması gerekirdi — doldurulmayan üründe akış ya sessizce yanlış orana ya da fail-closed'a düşerdi. Ürün modeli iki oran taşımaya başlarsa consumptionMode aynı alanın üstüne eklenebilir (olgu → oran eşlemesi platformda kalır), sözleşme kırılmadan.

Hata kodları

messageHTTPNe zaman
plugin.fiscal.missingParams400payload yok / obje değil (runtime bu kodu döndürmeye devam ediyor).
plugin.fiscal.saleRequired ★400sale yok / obje değil / type bilinmiyor / id kullanılamaz.
plugin.fiscal.saleTypeUnsupported ★400Tanınmayan sale.type — packet ve table AÇIK; ileride eklenecek kanallar için ayrıldı.
plugin.fiscal.saleNotFound ★404Satış çağıranın tenant'ında yok — yabancı adisyonun varlığı sızdırılmaz (403 DEĞİL).
plugin.fiscal.vatUnresolved ★400Bir satırın KDV oranı çözülemedi · details.lines[].
plugin.fiscal.paymentUnresolved ★400Ödeme satırının yöntem tanımı yok · details.payments[].
plugin.fiscal.saleEmpty / saleUnpaid ★400Satışta satır yok / tahsilat yok.
plugin.fiscal.amountsInvalid ★400Σ KDV ≠ Σ tahsilat, indirim ciroyu aşıyor ya da negatif, sayıya çevrilemeyen tutar.
plugin.fiscal.invalidReceiptType400receiptType whitelist dışı.
plugin.fiscal.idempotencyKeyRequired400idempotencyKey yok/boş/uzun (≤64).
plugin.fiscal.idempotencyKeyReused409Aynı key, FARKLI satış (parmak izi: receiptType + sale).
plugin.fiscal.duplicateInProgress409Aynı key eşzamanlı işleniyor.
plugin.fiscal.noProvider424Tenant TSE sağlayıcısı bağlamamış — hata değil, önkoşul eksikliği (retry etme).
plugin.fiscal.providerUnavailable503Sağlayıcı inaktif / askıda / erişilemez.
plugin.fiscal.timeout504Sağlayıcı yanıt penceresini aştı — AYNI key ile tekrar denenebilir.

★ = bu turda gelen kod. Hata gövdesi { success:false, message:"<i18n key>", details?:{…} }; HTTP kodu mesaj sonekinden türer: .notFound→404, .noProvider→424, .providerUnavailable→503, .timeout→504, .duplicateInProgress/.idempotencyKeyReused→409, diğer iş hataları→400. Scope reddi ortak koddur (plugin.scope.denied, 403): Yetenek hata kodları.

Kaldırılan kodlar: invalidVatRates · invalidPaymentTypes · invalidReference · invalidReferenceType — ilgili alanlar artık tüketiciden alınmıyor.
Rate limit: capability uçları cap:<id> kovasını paylaşır; aşımda 429 + Retry-After (Limitler).

Kapsam dışı

Storno/iade bu kanaldan imzalanamaz — tutarlar platformdan türetiliyor ve satış satırları negatif olamıyor (negatif tutar amountsInvalid alır).
Masa kanalı AÇIK ama anahtarı sana bırakır. sale.type: "table" kabul edilir; ancak masa id'si kalıcıdır (masa kapanınca kayıt silinir, aynı id ile yeniden açılır) — oturumu docNo ayırır. Sağlayıcıysan tekleme anahtarını yalnız table:<id> kurma; oturum ayracı (docNo + iş günü) ekle, yoksa dünkü oturumun Beleg'i bugünküne replay edilir. POS masa kapanışı kapanış gate'inden çalışmaya devam eder.

Geçiş notları

KimNe yapmalı
Tüketici
(kiosk / POS)
Payload'dan tutar alanlarını kaldır, sale: { type:"packet"|"table", id } gönder (kasa/terminal ayrımın varsa registerId ekle). Fişi kendin basıyorsan QR'ı yanıttaki receiptExtras'tan al. Yerinde/götürü ayrımı varsa sepet satırına taxRate ekle.
Sağlayıcı
(TSE)
Kod değişikliği gerekmez. Tekleme anahtarını payload.sale'den kur (fiscalSaleKey); reference'a düşen fallback'i kaldırabilirsin.