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
provides/consumes: [{ capability: "fiscal.de" }] beyan edersiniz; capability:fiscal.de:consume|provide scope'ları otomatik türetilir.amountsPerVatRate / amountsPerPaymentType kaldırıldı), sale zorunlu oldu ve sepet satırına opsiyonel taxRate geldi.invoice.issue | fiscal.de | |
|---|---|---|
| Ne yapar | E-fatura/e-arşiv keser (GİB) | Fişi TSE ile imzalar (KassenSichV) |
| Çerçeve | TR vergi mevzuatı | Alman KassenSichV / DSFinV-K |
| Sonuç | Fatura numarası/URL, asenkron durum | Mali 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 | Kim alır | Ne sağlar |
|---|---|---|
| capability:fiscal.de:consume | Kasa/POS tarafı eklenti | POST /plugin-api/capabilities/fiscal.de/invoke çağırabilir |
| capability:fiscal.de:provide | TSE sağlayıcı eklenti | Platformdan imzalı type:"capability" isteği alır + receiptExtras'ta tse.* yazabilir |
providerIsPii: false): taşınan veri tutar, KDV oranı ve ödeme tipidir — müşteri referansı bile yoktur. Bu yüzden kurulumda dataConsent gerektirmez.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
}
}| Alan | Tip | Zorunlu | Kural |
|---|---|---|---|
| sale | object | evet | İ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.type | string | evet | ⏳ 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.id | string | evet | ≤100; doküman id'si olarak kullanılabilir olmalı — /, ., .., __x__ yasak. |
| receiptType | string | hayır | RECEIPT (varsayılan). DSFinV-K Belegtyp; yeni tip eklemek geriye-uyumlu. |
| registerId | string | hayı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. |
| idempotencyKey | string | evet | ≤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 | — | deprecated | sale 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.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.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.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{ "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": [ … ] } }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.
| Alan | Kaynak | Kural |
|---|---|---|
| sequence · sequenceScope | Satışı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. |
| amountsPerPaymentType | Satışı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. |
| amountsPerVatRate | Sipariş 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 · linesOmitted | Satışı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ü).
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).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.Σ 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.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.
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
}
}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.Σ 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.idempotencyKey (tüketici) ve requestId (sağlayıcı) ile iki taraftadır. Tutarlar kuruş-integer'dır: 19.90 değil 1990.| status | Anlamı | receiptExtras |
|---|---|---|
| signed | TSE imzaladı — mali blok hazır. | Dolu |
| ausfall | TSE arızası. HATA DEĞİLDİR: satış geçerli, KassenSichV fişe "Ausfall" basılmasını ister. | Tek öğe (tse.ausfall) |
| accepted | Sonuç 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ş |
| failed | Sağ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.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 receiptExtras ile taşınır ve iki yolu vardır — ikisi de aynı bloğu üretir:
| Yol | Nasıl | Kim 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'i | table.close / packet.close allow yanıtı | POS akışındaki mali eklenti |
{ "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.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.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.
| Alan | Tip | Zorunlu | Kural |
|---|---|---|---|
| taxRate | number | hayır | 0–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). |
taxRateBy izi ve orders:write scope'udur.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)
[ { "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ş-integerconsumptionMode: 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.| message | HTTP | Ne zaman |
|---|---|---|
| plugin.fiscal.missingParams | 400 | payload yok / obje değil (runtime bu kodu döndürmeye devam ediyor). |
| plugin.fiscal.saleRequired ★ | 400 | sale yok / obje değil / type bilinmiyor / id kullanılamaz. |
| plugin.fiscal.saleTypeUnsupported ★ | 400 | Tanınmayan sale.type — packet ve table AÇIK; ileride eklenecek kanallar için ayrıldı. |
| plugin.fiscal.saleNotFound ★ | 404 | Satış çağıranın tenant'ında yok — yabancı adisyonun varlığı sızdırılmaz (403 DEĞİL). |
| plugin.fiscal.vatUnresolved ★ | 400 | Bir 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 ★ | 400 | Satış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.invalidReceiptType | 400 | receiptType whitelist dışı. |
| plugin.fiscal.idempotencyKeyRequired | 400 | idempotencyKey yok/boş/uzun (≤64). |
| plugin.fiscal.idempotencyKeyReused | 409 | Aynı key, FARKLI satış (parmak izi: receiptType + sale). |
| plugin.fiscal.duplicateInProgress | 409 | Aynı key eşzamanlı işleniyor. |
| plugin.fiscal.noProvider | 424 | Tenant TSE sağlayıcısı bağlamamış — hata değil, önkoşul eksikliği (retry etme). |
| plugin.fiscal.providerUnavailable | 503 | Sağlayıcı inaktif / askıda / erişilemez. |
| plugin.fiscal.timeout | 504 | Sağ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ı.
invalidVatRates · invalidPaymentTypes · invalidReference · invalidReferenceType — ilgili alanlar artık tüketiciden alınmıyor.amountsInvalid alı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.| Kim | Ne 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. |