@kreiseck/kasseneck-api 0.26.0 → 0.27.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,328 +1,541 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/kreiseck-at/kasseneck-api/main/doc/kasseneck.gif" alt="Kasseneck — RKSV-Registrierkasse aus Österreich" width="420">
2
+ <img src="https://raw.githubusercontent.com/kreiseck-at/kasseneck-api/main/doc/kasseneck.gif" alt="Kasseneck: RKSV fiscal cash register from Austria" width="420">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">@kreiseck/kasseneck-api</h1>
6
6
 
7
7
  <p align="center">
8
- <b>Austrian fiscal cash register (RKSV) for JavaScript and TypeScript — signed receipts, card payments, receipt printing.</b>
8
+ <b>Austrian fiscal cash register (RKSV) for JavaScript and TypeScript: signed receipts, card payments, receipt printing.</b>
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
12
  <a href="https://www.npmjs.com/package/@kreiseck/kasseneck-api"><img src="https://img.shields.io/npm/v/%40kreiseck%2Fkasseneck-api?color=136B6B&label=npm" alt="npm"></a>
13
13
  <img src="https://img.shields.io/badge/RKSV-%C2%A7%20131b%20BAO-136B6B" alt="RKSV">
14
- <img src="https://img.shields.io/badge/Lizenz-Apache--2.0-136B6B" alt="Apache-2.0">
14
+ <img src="https://img.shields.io/badge/License-Apache--2.0-136B6B" alt="Apache-2.0">
15
15
  <a href="https://kasseneck.at"><img src="https://img.shields.io/badge/Kasseneck-kasseneck.at-132A2A" alt="kasseneck.at"></a>
16
- <a href="https://kreiseck.com"><img src="https://img.shields.io/badge/von-Kreiseck-132A2A" alt="Kreiseck Software Solutions"></a>
16
+ <a href="https://kreiseck.com"><img src="https://img.shields.io/badge/by-Kreiseck-132A2A" alt="Kreiseck Software Solutions"></a>
17
17
  </p>
18
18
 
19
- **Kasseneck** ist eine österreichische Registrierkasse nach RKSV. Dieses Paket ist
20
- der JavaScript-/TypeScript-Client dafür: Ihr Code stellt Belege aus, storniert sie,
21
- nimmt Kartenzahlungen entgegen und druckt Bons. Es ist der Zwilling des
22
- Flutter-Pakets [`kasseneck_api`](https://pub.dev/packages/kasseneck_api):
23
- dieselben Endpunkte, dieselben Modelle, dieselben Enum-Werte, im Test
24
- gegeneinander geprüft.
25
-
26
- ## Was eine Registrierkasse in Österreich können muss
27
-
28
- Die Registrierkassen- und Belegerteilungspflicht steht in § 131b der
29
- Bundesabgabenordnung, die technischen Anforderungen an die Sicherheitseinrichtung
30
- in der Registrierkassensicherheitsverordnung (RKSV). Daraus ergibt sich eine
31
- ganze Kette von Aufgaben — die Tabelle zeigt, welche davon **diese Software**
32
- übernimmt und welche beim Betrieb selbst bleiben:
19
+ <p align="center">
20
+ Deutsch: <a href="https://github.com/kreiseck-at/kasseneck-api/blob/main/README.de.md">README.de.md</a>
21
+ </p>
33
22
 
34
- | Aufgabe | Wo sie erledigt wird |
35
- | --- | --- |
36
- | [Signaturerstellungseinheit](https://kasseneck.at/wissen/signaturerstellungseinheit) — jede Barzahlung wird signiert | Kasseneck, nichts zu installieren |
37
- | [Verkettung und DEP](https://kasseneck.at/wissen/dep) — jeder Beleg trägt den vorigen, das Protokoll ist exportierbar | Kasseneck-Backend |
38
- | [Startbeleg, Monatsbeleg, Jahresbeleg](https://kasseneck.at/wissen/startbeleg-monatsbeleg-jahresbeleg) | Kasseneck, automatisch |
39
- | [Meldungen an FinanzOnline](https://kasseneck.at/wissen/finanzonline) — Anmeldung, Ausfall, Außerbetriebnahme | Kasseneck-Backend |
40
- | [Belegerteilungspflicht](https://kasseneck.at/wissen/belegerteilungspflicht) — jeder Kunde bekommt einen Beleg | **dieses Paket** — Bon, PDF, Bildschirm oder Link |
41
- | [Ausfall der Signatureinheit](https://kasseneck.at/wissen/ausfall) — Sammelbeleg, Meldung, Nachsignatur | Kasseneck, automatisch |
42
- | [Kassennachschau](https://kasseneck.at/wissen/kassennachschau) — der Prüfer verlangt das DEP | Kasseneck, Export auf Knopfdruck |
43
- | Anmeldung der Kasse, Aufbewahrung, steuerliche Würdigung | **beim Unternehmer** |
44
-
45
- Kurz: Sie bauen die Kassenoberfläche, nicht die Sicherheitseinrichtung. Ein Aufruf
46
- von `createReceipt(...)` erzeugt einen signierten, verketteten und im DEP
47
- abgelegten Beleg. Die Signaturkette wird gegen das offizielle Prüfwerkzeug des
48
- BMF getestet.
49
-
50
- > **Kein Rechts- oder Steuerrat.** Dieser Abschnitt beschreibt, was die Software
51
- > tut. Er ersetzt keine Beratung und begründet keine Zusicherung, dass ein
52
- > bestimmter Betrieb damit alle Pflichten erfüllt. Verbindlich sind die
53
- > Bundesabgabenordnung, die RKSV und die Erlässe des BMF; die Verantwortung für
54
- > Anmeldung, Betrieb und Aufbewahrung bleibt beim Unternehmer. Ausführlicher und
55
- > mit Quellen: [kasseneck.at/wissen](https://kasseneck.at/wissen).
56
- > Stand: September 2026.
57
-
58
- ## Erst ausprobieren
59
-
60
- Es gibt eine Test-Umgebung mit eigenem Schlüssel und Test-Signaturen, getrennt
61
- vom Echtbetrieb — die aktuellen Konditionen stehen auf
62
- [kasseneck.at/preise](https://kasseneck.at/preise), die Schnittstelle ist unter
63
- [kasseneck.at/api-doku](https://kasseneck.at/api-doku) beschrieben.
64
-
65
- ## Lieber eine fertige Kasse?
66
-
67
- Dieses Paket ist für alle, die eine eigene Anwendung bauen. Wer einfach kassieren
68
- will, muss nichts davon programmieren:
69
-
70
- - **[Kasseneck — die fertige Registrierkasse](https://kasseneck.at)** für Telefon,
71
- Tablet und Browser, inklusive Signaturerstellungseinheit und
72
- FinanzOnline-Anmeldung.
73
- - **[Lösungen nach Branche](https://kasseneck.at/branchen)** — vom Lokal bis zum Taxi.
74
- - **[Preise](https://kasseneck.at/preise)**
75
- - **[Kontakt](https://kasseneck.at/kontakt)** — auch für Kassenwechsel,
76
- Partnerschaften und eigene Integrationen.
77
-
78
- ## Was drin ist
79
-
80
- Belege ausstellen und stornieren, Belege und Kassen auflisten, Berichte
81
- herunterladen, Status bei FinanzOnline abfragen, Stripe-Zahllinks und
82
- Hobex-Cloud-Zahlungen, das Beleg-Layout und die ESC/POS-Erzeugung für den
83
- Bondrucker — und unter `./partner` die **Partner-API**: Betriebe anlegen und bis
84
- zur laufenden Kasse begleiten.
85
-
86
- **Es läuft im Browser und in Node.** ESM ist das Hauptformat, CommonJS liegt
87
- daneben; beides mit eigenen Typdeklarationen. Node ab 20.18 (`fetch` muss
88
- vorhanden sein). Es gibt keine Laufzeitabhängigkeiten.
89
-
90
- ## Installation
23
+ **Kasseneck** is a fiscal cash register (*Registrierkasse*) for Austria that
24
+ implements the RKSV, the Austrian cash register security regulation. This
25
+ package is its JavaScript and TypeScript client: your code issues receipts,
26
+ the Kasseneck backend handles receipt signing and chaining, and the package
27
+ returns the results as typed objects. It also cancels receipts, takes card
28
+ payments, lays out and prints receipts on thermal printers, and covers the
29
+ partner and invoice APIs. It is the twin of the Flutter package
30
+ [`kasseneck_api`](https://pub.dev/packages/kasseneck_api): same endpoints, same
31
+ models, same enum values, checked against each other in tests.
32
+
33
+ ## Contents
34
+
35
+ - [Quick start](#quick-start)
36
+ - [What a fiscal cash register in Austria must do](#what-a-fiscal-cash-register-in-austria-must-do)
37
+ - [Try it first, or use the ready-made register](#try-it-first-or-use-the-ready-made-register)
38
+ - [Requirements and subpaths](#requirements-and-subpaths)
39
+ - [Authentication](#authentication)
40
+ - [Amounts are integer cents](#amounts-are-integer-cents)
41
+ - [Errors](#errors)
42
+ - [Receipts](#receipts)
43
+ - [Cancellation (Storno)](#cancellation-storno)
44
+ - [Sending a receipt by email](#sending-a-receipt-by-email)
45
+ - [Receipt printing: QR code, logo, printers](#receipt-printing-qr-code-logo-printers)
46
+ - [Card payments](#card-payments)
47
+ - [Partner API (`./partner`)](#partner-api-partner)
48
+ - [Invoice API (`./rechnung`)](#invoice-api-rechnung)
49
+ - [Development](#development)
50
+ - [Contract files for the twin packages](#contract-files-for-the-twin-packages)
51
+ - [Glossary](#glossary)
52
+ - [License](#license)
53
+
54
+ ## Quick start
91
55
 
92
56
  ```bash
93
57
  npm install @kreiseck/kasseneck-api
94
58
  ```
95
59
 
96
- React ist eine optionale Peer-Abhängigkeit und nur für den Unterpfad `./react`
97
- nötig.
60
+ Sell two items, then build the printed receipt (*Beleg*) from the response:
61
+
62
+ ```ts
63
+ import {
64
+ createKasseneckApi,
65
+ apiKeyAuth,
66
+ KeckPaymentMethod,
67
+ VatRate,
68
+ } from '@kreiseck/kasseneck-api';
69
+ import { buildReceiptLayout, renderReceiptGrid, escPosLayoutBytes } from '@kreiseck/kasseneck-api/receipt';
70
+
71
+ const api = createKasseneckApi({
72
+ auth: apiKeyAuth({ apiKey: 'kr_live_…', cashregisterToken: 'cb_live_…' }),
73
+ });
74
+
75
+ // Sell, and get the company data for the receipt header in the same call.
76
+ const { receipt, company, testKasse, testSignatur, pruefangaben } = await api.sellReceiptWithCompany({
77
+ paymentMethod: KeckPaymentMethod.cash,
78
+ items: [
79
+ { name: 'Café Latte', quantity: 2, vat: VatRate.vat20, priceCents: 390 },
80
+ { name: 'Marmeladeweckerl', quantity: 1, vat: VatRate.vat10, priceCents: 250 },
81
+ ],
82
+ // Optional tip in cents; the backend books it as a signed tip line.
83
+ // With its own payment method or recipients: { cents, paymentMethod, recipients }.
84
+ tip: 100,
85
+ });
86
+
87
+ // Layout: a plain data model (lines, alignment, columns, QR code).
88
+ // testKasse / testSignatur add the "not a valid receipt" banner where needed.
89
+ const layout = buildReceiptLayout(receipt, company, { paperSize: 'mm58', testKasse, testSignatur, pruefangaben });
90
+
91
+ // Character grid: exactly 32 (58 mm) or 48 (80 mm) characters per line.
92
+ // Screen, printed receipt and PDF all use this one grid.
93
+ const grid = renderReceiptGrid(layout); // grid.lines[i].text, .bold, .kind, .qr
94
+
95
+ // ESC/POS bytes for a thermal printer; they print exactly the grid lines.
96
+ const bytes = escPosLayoutBytes(layout);
97
+ ```
98
+
99
+ The receipt returned by `sellReceiptWithCompany` (or `sellReceipt`) has already
100
+ been signed, chained and stored in the data capture log (*DEP*) by the backend.
98
101
 
99
- ## Die zwei Anmeldewege
102
+ ## What a fiscal cash register in Austria must do
100
103
 
101
- Der Client nimmt eine **austauschbare Anmeldung** entgegen — ein Objekt, das
102
- pro Anfrage die Kopfzeilen liefert. Es gibt zwei, und keiner ist der
103
- bevorzugte:
104
+ The obligation to use a fiscal cash register and to issue receipts is set out
105
+ in § 131b of the Austrian Federal Fiscal Code (*Bundesabgabenordnung*, BAO).
106
+ The technical requirements for the security device are in the cash register
107
+ security regulation (*Registrierkassensicherheitsverordnung*, RKSV). The table
108
+ shows which of the resulting tasks **this software** takes over and which stay
109
+ with the business:
104
110
 
105
- | Weg | Wer | Wie |
111
+ | Task | Where it is handled |
112
+ | --- | --- |
113
+ | [Signature creation unit (*Signaturerstellungseinheit*)](https://kasseneck.at/wissen/signaturerstellungseinheit): every receipt is signed | Kasseneck, nothing to install |
114
+ | [Chaining and the data capture log (*DEP*)](https://kasseneck.at/wissen/dep): each receipt carries a value derived from the previous one, the encrypted turnover counter (*Umsatzzähler*) runs along, the log can be exported | Kasseneck backend |
115
+ | [Start receipt, monthly receipt, annual receipt (*Startbeleg, Monatsbeleg, Jahresbeleg*)](https://kasseneck.at/wissen/startbeleg-monatsbeleg-jahresbeleg) | Kasseneck, automatic |
116
+ | [Notifications to FinanzOnline](https://kasseneck.at/wissen/finanzonline): registration, failure, decommissioning (*Außerbetriebnahme*) with its final receipt (*Schlussbeleg*) | Kasseneck backend submits them |
117
+ | [Obligation to issue receipts (*Belegerteilungspflicht*)](https://kasseneck.at/wissen/belegerteilungspflicht): every customer gets a receipt | **this package**: printed receipt, PDF, screen or link |
118
+ | [Signature unit failure (*Ausfall der Signatureinheit*)](https://kasseneck.at/wissen/ausfall): collective receipt, notification, subsequent signing | Kasseneck, automatic |
119
+ | [Cash register audit (*Kassennachschau*)](https://kasseneck.at/wissen/kassennachschau): the auditor asks for the DEP | Kasseneck, export on request |
120
+ | Duty to register the cash register, retention of records, tax assessment | **the business owner** |
121
+
122
+ In short: you build the point-of-sale interface, not the security device. The
123
+ signature chain is tested against the official verification tool of the
124
+ Austrian Federal Ministry of Finance (BMF).
125
+
126
+ > **Not legal or tax advice.** This section describes what the software does.
127
+ > It does not replace professional advice and is no assurance that a particular
128
+ > business meets all its obligations with it. The BAO, the RKSV and the rulings
129
+ > of the BMF are binding. Registering and operating the cash register and
130
+ > retaining the records remain the duty of the business owner, also where the
131
+ > software submits the notifications. More detail with sources (in German):
132
+ > [kasseneck.at/wissen](https://kasseneck.at/wissen).
133
+ > As of: September 2026.
134
+
135
+ ## Try it first, or use the ready-made register
136
+
137
+ There is a test environment with its own key and test signatures, separate
138
+ from live operation. The HTTP interface is described at
139
+ [kasseneck.at/api-doku](https://kasseneck.at/api-doku).
140
+
141
+ This package is for people who build their own application. If you just want
142
+ to take payments, there is nothing to program:
143
+
144
+ - **[Kasseneck, the ready-made fiscal cash register](https://kasseneck.at)** for
145
+ phone, tablet and browser, including the signature creation unit and the
146
+ FinanzOnline registration.
147
+ - **[Solutions by industry](https://kasseneck.at/branchen)**
148
+ - **[Pricing](https://kasseneck.at/preise)**
149
+ - **[Contact](https://kasseneck.at/kontakt)**, also for switching registers,
150
+ partnerships and custom integrations.
151
+
152
+ ## Requirements and subpaths
153
+
154
+ **Runs in the browser and in Node.js.** ESM is the main format, CommonJS ships
155
+ alongside it, both with their own type declarations. Node 20.18 or later
156
+ (`fetch` must be available). There are no runtime dependencies. React 18 or
157
+ later is an optional peer dependency, needed only for `./react`.
158
+
159
+ Import only the subpath you need, so a Node program never pulls in the React
160
+ adapter:
161
+
162
+ | Subpath | Contents |
163
+ |-----------|--------|
164
+ | `@kreiseck/kasseneck-api` | Endpoints, authentication, transport, models, enums, errors: everything that talks to the backend. |
165
+ | `…/receipt` | Receipt layout as a data model (framework-free), the character grid, the receipt sheet with logo, and the bridges to ESC/POS and Epson ePOS (XML and direct printing over HTTP). |
166
+ | `…/printing` | ESC/POS generation (byte sequences for thermal printers), QR sizing, printing over WebUSB. |
167
+ | `…/payments` | Stripe payment links, Hobex cloud (both HTTP endpoints of the backend), and Hobex **HPS** via **Kasseneck Connect** (local device agent that talks to the terminal). |
168
+ | `…/register` | Sign-in for the browser register: pair and unpair a device, list its users and sessions, sign in by PIN, renew and end the session. |
169
+ | `…/kasse` | Tile register: register settings (business-wide and per device), article groups and articles for tiles, discount distribution per VAT rate, scopes of register permissions, network printers and print jobs, tip recipients, the register's message catalogue. |
170
+ | `…/partner` | Partner API: create businesses, FinanzOnline link, signature, cash registers, credentials, webhooks with signature verification. **Belongs on a server.** |
171
+ | `…/rechnung` | Invoice API: create and search customers, issue finalised invoices, credit notes and cancellation, PDF and e-invoice XML, the contract as data. **Belongs on a server.** |
172
+ | `…/rechnung/rechnen` | Pure calculation core for invoice totals (integers, no transport, no dependency beyond types). Safe to run in the browser. |
173
+ | `…/react` | Thin React adapter that renders a receipt layout or a receipt sheet. Needs React. |
174
+ | `…/fixtures/*` | Golden receipts (JSON): inputs `belege/<name>.json`, promised line output `erwartet/<name>.lines.json`, `manifest.json` with checksums. The backend, the browser register and the Flutter package check against the same files. |
175
+
176
+ Every exported function carries its documentation as TSDoc in the shipped type
177
+ declarations, so your editor shows it on hover. That is also the reference for
178
+ the partner and invoice endpoints; this README shows how to use the client.
179
+
180
+ ## Authentication
181
+
182
+ The client takes an **exchangeable authentication**: an object that supplies
183
+ the headers for each request. There are two, and neither is preferred:
184
+
185
+ | Mode | For | How |
106
186
  |-----|-----|-----|
107
- | `apiKeyAuth` | Geräte, POS-Apps, Dritte | `api_key` als Bearer + `cashregister-token`-Kopfzeile |
108
- | `registerUserAuth` | Browser-Kasse | ID-Token des Anmeldediensts als Bearer + `register-session`-Kopfzeile, Kasse als Parameter |
187
+ | `apiKeyAuth` | devices, POS apps, third parties | `api_key` as bearer token plus the `cashregister-token` header |
188
+ | `registerUserAuth` | browser register | ID token of the sign-in service as bearer token plus the `register-session` header, cash register as the `cashregisterId` parameter |
109
189
 
110
190
  ```ts
111
191
  import { apiKeyAuth, registerUserAuth } from '@kreiseck/kasseneck-api';
112
192
 
113
- // Gerät/POS: der api_key gehört auf ein Gerät, nie in einen Browser.
114
- const geraet = apiKeyAuth({ apiKey: 'kr_live_…', cashregisterToken: 'cb_live_…' });
193
+ // Device/POS: the api_key belongs on a device, never in a browser.
194
+ const device = apiKeyAuth({ apiKey: 'kr_live_…', cashregisterToken: 'cb_live_…' });
115
195
 
116
- // Browser-Kasse: das Paket kennt den Anmeldedienst nicht — es bekommt Funktionen, die
117
- // ein gültiges Token bzw. die laufende Sitzung liefern. Beide werden bei JEDEM
118
- // Aufruf befragt (ID-Tokens laufen nach einer Stunde ab, die Kassen-Sitzung
119
- // nach 90 Sekunden).
120
- const kasse = registerUserAuth({
196
+ // Browser register: the package does not know the sign-in service. It gets
197
+ // functions that return a valid token and the current session. Both are asked
198
+ // on EVERY call (ID tokens expire after one hour, the register session after
199
+ // 90 seconds).
200
+ const register = registerUserAuth({
121
201
  getIdToken: () => auth.currentUser!.getIdToken(),
122
- getSessionId: () => sitzungHalter.aktuelleId(),
202
+ getSessionId: () => sessionHolder.currentId(),
123
203
  cashregisterId: 'kasse-1',
124
204
  });
125
205
  ```
126
206
 
127
- ### Und ein dritter Fall: gar keine Anmeldung
207
+ ### Calls without any authentication
208
+
209
+ Six calls run **without any identity**, because they are how an identity comes
210
+ into being or how a device manages itself. They live in `…/register` and take
211
+ **no authentication**, only the connection settings (base URL, timeout,
212
+ `fetch`):
128
213
 
129
- Drei Endpunkte laufen **ohne jede Identität** — sie sind der Weg, auf dem eine
130
- Identität überhaupt erst entsteht: ein Gerät koppeln, seine Kassen-Benutzer
131
- auflisten, einen davon per PIN anmelden. Sie stehen unter `…/register` und
132
- nehmen deshalb **keine Anmeldung** entgegen, sondern nur die Verbindungsangaben:
214
+ | Call | Proof instead of authentication |
215
+ |---|---|
216
+ | `pairRegisterDevice` | the pairing code from the panel |
217
+ | `listRegisterUsersForDevice`, `listRegisterSessionsForDevice` | the device credentials from pairing |
218
+ | `unpairRegisterDevice` | the device credentials; locks the device in the backend |
219
+ | `registerUserLogin` | device credentials, user and PIN |
220
+ | `registerPinLogin` | device credentials and PIN, without choosing a user |
133
221
 
134
222
  ```ts
135
223
  import { pairRegisterDevice, registerUserLogin } from '@kreiseck/kasseneck-api/register';
136
224
 
137
- const geraet = await pairRegisterDevice({ code: 'K7NPQR34', label: 'Schank' });
138
- const sitzung = await registerUserLogin({ ...geraet, userId: 'ru-1', pin: '1234' });
225
+ const paired = await pairRegisterDevice({ code: 'K7NPQR34', label: 'Bar' });
226
+ const session = await registerUserLogin({ ...paired, userId: 'ru-1', pin: '1234' });
139
227
  ```
140
228
 
141
- Mit `sitzung.customToken` meldet sich der Verbraucher beim Anmeldedienst an; das
142
- daraus entstehende ID-Token und `sitzung.sessionId` ergeben `registerUserAuth`.
229
+ With `session.customToken` the app signs in to the sign-in service; the
230
+ resulting ID token together with `session.sessionId` feeds `registerUserAuth`.
231
+ `renewRegisterSession` and `endRegisterSession` then run with
232
+ `registerUserAuth` and are also available on `createKasseneckApi`.
143
233
 
144
- Eine anmeldungsfreie Anmeldung gibt es dafür **nicht** — sie wäre ein
145
- Schlupfloch, mit dem sich jeder andere Aufruf des Pakets ohne Anmeldung bauen
146
- ließe. Die drei bringen ihren Transport selbst mit.
234
+ The package deliberately exports **no** "empty" authentication: it would be a
235
+ loophole for building every other call without authentication. The six calls
236
+ build their transport internally.
147
237
 
148
- **Welcher Weg für welchen Endpunkt gilt, entscheidet das Backend, nicht dieses
149
- Paket.** Die Endpunkt-Module nennen im Kommentar jeweils, was dort gilt. Zwei
150
- Fälle, die regelmäßig überraschen:
238
+ ### Which mode works for which endpoint
151
239
 
152
- - `listMyCashregisters` und `listMyReceipts` laufen über den Kunden-Pfad und
153
- brauchen ein **ID-Token** — mit `apiKeyAuth` sind sie nicht erreichbar.
154
- - `getFirstReceiptDate` und die beiden Bericht-Downloads stehen dem
155
- Kassen-Benutzer **nicht** offen; sie brauchen `apiKeyAuth`.
240
+ **The backend decides, not this package.** The endpoint modules say in their
241
+ comments what applies. Cases that regularly surprise:
156
242
 
157
- ## Beträge sind ganze Cent
243
+ - `listMyCashregisters` and `listMyReceipts` need an **ID token**
244
+ (`registerUserAuth` works); with `apiKeyAuth` they are not reachable.
245
+ - `getFirstReceiptDate` and the two report downloads (`downloadDailyReport`,
246
+ `downloadMonthlyReport`) are **not** open to register users; they need
247
+ `apiKeyAuth`. The same holds for the Stripe and Hobex cloud endpoints.
158
248
 
159
- Geld wird in diesem Paket ausnahmslos als **ganzzahliger Cent-Betrag**
160
- gerechnet — `priceCents`, `valueCents`, `totalCents`, `amountCents`. Es gibt
161
- keine Euro-Fließkommazahlen in der Oberfläche. Wo das Backend Euro liefert
162
- (z. B. `total` in der Belegliste) oder erwartet (Hobex), wird genau an dieser
163
- Grenze einmal umgerechnet.
249
+ ## Amounts are integer cents
164
250
 
165
- Der Grund ist nicht Ordnungsliebe: Netto, Umsatzsteuer und Brutto einer
166
- Belegzeile müssen sich exakt aufheben, und ein Storno muss seinen Beleg auf den
167
- Cent spiegeln. Mit Fließkomma geht das in etwa jedem hundertsten Betrag schief.
251
+ In the receipt API, money is always an **integer amount in cents**:
252
+ `priceCents`, `valueCents`, `totalCents`, `amountCents`. Where the backend
253
+ returns euros (for example `total` in the receipt list) or expects them
254
+ (Hobex), the package converts exactly once at that boundary.
168
255
 
169
- Aus demselben Grund ist `quantity` eine **ganze** Menge. Eine gebrochene wird
170
- abgelehnt, bevor etwas gesendet wird.
256
+ The reason: net, VAT (*USt*) and gross of a receipt line have to add up
257
+ exactly, and a cancellation has to mirror its receipt to the cent. With
258
+ floating point that goes wrong for roughly one amount in a hundred.
171
259
 
172
- ## Schnellstart: Beleg verkaufen und drucken
260
+ For the same reason a receipt line's `quantity` is a **whole number**. A
261
+ fractional one is rejected before anything is sent.
173
262
 
174
- ```ts
175
- import {
176
- createKasseneckApi,
177
- apiKeyAuth,
178
- KeckPaymentMethod,
179
- VatRate,
180
- } from '@kreiseck/kasseneck-api';
181
- import { buildReceiptLayout, renderReceiptGrid, escPosLayoutBytes } from '@kreiseck/kasseneck-api/receipt';
263
+ The invoice API has its own units: unit prices in cents or micro-euros,
264
+ quantities with up to three decimals. See [Invoice API](#invoice-api-rechnung).
182
265
 
183
- const api = createKasseneckApi({
184
- auth: apiKeyAuth({ apiKey: 'kr_live_…', cashregisterToken: 'cb_live_…' }),
185
- });
266
+ ## Errors
186
267
 
187
- // Verkaufen — und in einem Aufruf die Firmendaten für den Belegkopf mitnehmen.
188
- const { receipt, company } = await api.sellReceiptWithCompany({
189
- paymentMethod: KeckPaymentMethod.cash,
190
- items: [
191
- { name: 'Café Latte', quantity: 2, vat: VatRate.vat20, priceCents: 390 },
192
- { name: 'Marmeladeweckerl', quantity: 1, vat: VatRate.vat10, priceCents: 250 },
193
- ],
194
- // Trinkgeld in Cent (optional): das Backend bucht daraus eine signierte
195
- // Position „Trinkgeld“ — Mitarbeiter 0 % als Durchläufer, Inhaber als Umsatz.
196
- // Als Objekt mit eigener Zahlart/Empfängern: { cents, paymentMethod, recipients }.
197
- tip: 100,
198
- });
268
+ A failure always arrives as a **thrown error**, never as a return value; you
269
+ never check a `status` field yourself. There are five error classes, each with
270
+ a type guard:
199
271
 
200
- // Layout bauen (reines Datenmodell: Zeilen, Ausrichtung, Spalten, QR-Code) …
201
- // Layout-Regelwerk: gespeicherte Belege tragen ihre Version (`layoutRegeln`);
202
- // ohne Angabe gilt das aktuelle (2: Nullbelege mit Block „Prüfangaben“ —
203
- // die Registrierdaten dafür liefert `getReceiptWithCompany` als `pruefangaben`).
204
- const layout = buildReceiptLayout(receipt, company, { paperSize: 'mm58' });
272
+ | Class | Guard | Meaning |
273
+ |---|---|---|
274
+ | `KasseneckApiError` | `isKasseneckApiError` | The backend received the request and rejected it (locked register, missing module, invalid parameter). Retrying does not help. Carries `code` (where the endpoint defines one), `serverMessage` (German display text) and `details`. |
275
+ | `KasseneckHttpError` | `isKasseneckHttpError` | The response was not a usable envelope: HTTP 404/500, empty body, HTML instead of JSON. `reason` tells the cases apart. |
276
+ | `KasseneckNetworkError` | `isKasseneckNetworkError` | No response at all: network down, DNS, aborted connection or timeout (`timedOut`). |
277
+ | `KasseneckAuthError` | `isKasseneckAuthError` | The request was never sent because authentication failed (missing credentials, or the token or session provider threw). |
278
+ | `KasseneckValidationError` | `isKasseneckValidationError` | Wrong shape. `scope: 'request'`: your input breaks a rule the package knows before sending. `scope: 'response'`: the backend reported success but the payload lacked what the call promises. |
205
279
 
206
- // … das Zeichenraster (exakt 32/48 Zeichen je Zeile — die eine Wahrheit für
207
- // Bildschirm, Bondruck und PDF: Spalten in ganzen Zeichen, rechte Spalte bündig,
208
- // wortweiser Umbruch) …
209
- const grid = renderReceiptGrid(layout); // grid.lines[i].text, .bold, .kind, .qr
280
+ **None of them ever contains a secret**: no key, no token, neither the sent nor
281
+ the received body.
210
282
 
211
- // … und daraus die Bytes für den Bondrucker (druckt genau die Rasterzeilen).
212
- // Der Transport zum Drucker ist bewusst nicht Teil dieses Pakets.
213
- const bytes = escPosLayoutBytes(layout);
214
- ```
283
+ **Decide on the code, not the text.** Where an endpoint has a catalogue of
284
+ error codes (cancellation, receipt email, partner and invoice API), branch on
285
+ `error.code`. The German `serverMessage` is for display and may change.
286
+
287
+ ## Receipts
288
+
289
+ The facade from `createKasseneckApi` offers `sellReceipt` and
290
+ `sellReceiptWithCompany`, `zeroReceipt` for a zero receipt (*Nullbeleg*),
291
+ `getReceipt` and `getReceiptWithCompany`, `listMyReceipts`, `cancelReceipt`,
292
+ `sendReceiptEmail`, the report downloads, the FinanzOnline status queries
293
+ (`getCashboxStatus`, `getSignatureStatus`) and the Stripe and Hobex cloud
294
+ payments.
295
+
296
+ **Layout rule sets.** `buildReceiptLayout` sets receipts according to a
297
+ numbered rule set. Without the `regelwerk` option the current one applies
298
+ (`AKTUELLES_REGELWERK`, currently 2: zero receipts carry a block
299
+ "Prüfangaben" with the registration data, which `getReceiptWithCompany`
300
+ returns as `pruefangaben`). The backend stores the rule set with each receipt,
301
+ and `getReceiptWithCompany` also returns the backend-built `layout` in that
302
+ rule set, so an old receipt looks the way it did when it was issued.
215
303
 
216
- Ein Misserfolg kommt immer als **geworfener Fehler**, nie als Rückgabewert;
217
- niemand muss selbst auf ein `status`-Feld prüfen. Es gibt fünf Fehlerarten mit
218
- je einem Wächter (`isKasseneckApiError` und Geschwister): fachlicher Fehler,
219
- HTTP-/Formfehler, Netz-/Zeitfehler, Anmeldefehler, Formfehler der Ein- oder
220
- Ausgabe. **In keinem davon steht je ein Geheimnis** — weder Schlüssel noch
221
- Token, weder gesendete noch empfangene Rümpfe.
304
+ Times on receipts are read as **Vienna wall-clock time**
305
+ (`parseServerTimeStamp`), never through `new Date(text)`.
222
306
 
223
- ## Storno: voll oder in Teilen
307
+ ## Cancellation (Storno)
308
+
309
+ A cancellation (*Storno*) refers to the original receipt, in full or in part:
224
310
 
225
311
  ```ts
226
- const ergebnis = await api.cancelReceipt({
227
- receipt: beleg, // oder: cashregisterId + originalReceiptId
228
- reason: 'fehleingabe', // Katalog: CANCELLATION_REASONS
229
- items: [{ index: 0, quantity: 1 }], // weglassen = Vollstorno der Restmengen
230
- note: 'Kunde wollte nur eine', // intern, wird nie gedruckt
312
+ const result = await api.cancelReceipt({
313
+ receipt: original, // or: cashregisterId + originalReceiptId
314
+ reason: 'fehleingabe', // catalogue: CANCELLATION_REASONS
315
+ items: [{ index: 0, quantity: 1 }], // omit = cancel all remaining quantities
316
+ note: 'Customer only wanted one', // internal, stored, never printed
231
317
  });
232
- ergebnis.receipt; // der signierte Storno-Beleg (receiptType cancellation)
233
- ergebnis.cancellationOf; // Bezug auf das Original
234
- ergebnis.remaining; // Restmengen des Originals danach
318
+ result.receipt; // the signed cancellation receipt (receiptType cancellation)
319
+ result.cancellationOf; // reference to the original
320
+ result.remaining; // remaining quantities of the original afterwards
235
321
  ```
236
322
 
237
- **Veraltet:** `createCancelReceipt` (Storno über `createReceipt` mit frei
238
- übergebenen, negierten Positionen) bleibt aus Kompatibilität erreichbar, ist aber
239
- `@deprecated` — kein Bezug zum Original, keine Restmengen, kein Schutz vor
240
- doppeltem Storno, keine Gutscheine. Das Backend legt bei diesem Weg
241
- `deprecation` in die Antwort.
242
-
243
- Der Server negiert die Positionen, prüft Restmengen und Rechte („nur eigene
244
- Belege" oder „alle") und verkettet Original und Storno. Ein Storno-Beleg lässt
245
- sich nicht stornieren, ein voll stornierter Beleg nicht noch einmal. Am
246
- gelesenen Original liefert `remainingQuantities(receipt)` die Reste vorab (für
247
- den Storno-Dialog); die Wahrheit hat der Server.
323
+ The server negates the lines, checks remaining quantities and permissions
324
+ ("own receipts only" or "all") and chains original and cancellation.
325
+ Cancellation, zero, start and training receipts cannot be cancelled, and a
326
+ fully cancelled receipt cannot be cancelled again. On a receipt you have read,
327
+ `remainingQuantities(receipt)` gives the remaining quantities up front (for the
328
+ cancellation dialog); the server has the final word.
248
329
 
249
- **Fehler entscheidet man am Code, nicht am Text.** Jeder fachliche Fehler von
250
- `cancelReceipt` trägt `KasseneckApiError.code` aus `CANCELLATION_ERROR_CODES`
251
- (z. B. `bereits_storniert`, `menge_ueber_rest`, `nur_eigene_belege`). Die
252
- deutsche Meldung (`serverMessage`) ist Anzeige und darf sich ändern.
330
+ Every business error of `cancelReceipt` carries `KasseneckApiError.code` from
331
+ `CANCELLATION_ERROR_CODES` (for example `bereits_storniert`,
332
+ `menge_ueber_rest`, `nur_eigene_belege`):
253
333
 
254
334
  ```ts
255
335
  import { isKasseneckApiError, isCancellationErrorCode } from '@kreiseck/kasseneck-api';
256
336
 
257
337
  try {
258
- await api.cancelReceipt({ receipt: beleg, reason: 'fehleingabe' });
259
- } catch (fehler) {
260
- if (isKasseneckApiError(fehler) && isCancellationErrorCode(fehler.code)) {
261
- switch (fehler.code) {
262
- case 'bereits_storniert': // Beleg im Dialog als „storniert" zeigen, Knopf sperren
263
- case 'menge_ueber_rest': // Restmengen neu laden (jemand war schneller)
264
- case 'nur_eigene_belege': // Chef holen
265
- break;
266
- }
338
+ await api.cancelReceipt({ receipt: original, reason: 'fehleingabe' });
339
+ } catch (error) {
340
+ if (!isKasseneckApiError(error) || !isCancellationErrorCode(error.code)) throw error;
341
+ switch (error.code) {
342
+ case 'bereits_storniert': // show the receipt as cancelled, disable the button
343
+ case 'menge_ueber_rest': // reload the remaining quantities (someone was faster)
344
+ case 'nur_eigene_belege': // ask a manager
345
+ showHint(error.code);
346
+ break;
347
+ default:
348
+ throw error;
267
349
  }
268
- throw fehler;
269
350
  }
270
351
  ```
271
352
 
272
- **Gutscheine.** Ein Wertgutschein wird nur beim Vollstorno (ohne `items`)
273
- gespiegelt — er ist unteilbar. Ein Rabattgutschein ist am Original bereits in
274
- den Umsatz eingerechnet; **jeder** Storno nimmt ihn anteilig der stornierten
275
- Menge zurück: bei 3 Stück à 10 € mit 6 € Rabatt sind 8 € je Stück Entgelt, der
276
- Storno-Beleg trägt dann „−10,00" plus eine Zeile „Gutschein-Ausgleich +2,00".
277
- Was ein Storno gewährt hat, steht am Eintrag in `receipt.cancellations[]` als
278
- `promoAdjustmentCents` (Cent je Steuertopf) — die Kasse kann es im Dialog
279
- zeigen, rechnen muss sie nichts.
353
+ **Deprecated:** `createCancelReceipt` (cancellation through `createReceipt`
354
+ with freely passed, negated lines) remains available for compatibility but is
355
+ `@deprecated`: no reference to the original, no remaining quantities, no
356
+ protection against double cancellation, no voucher handling. The backend adds
357
+ `deprecation` to its response on this path.
358
+
359
+ **Vouchers.** A value voucher is only mirrored on a full cancellation (without
360
+ `items`); it cannot be split. A discount voucher is already part of the
361
+ original's turnover, and **every** cancellation takes it back in proportion to
362
+ the cancelled quantity. Example: 3 items at € 10 with a € 6 discount means € 8
363
+ was paid per item, so the cancellation receipt shows "−10,00" plus a line
364
+ "Gutschein-Ausgleich +2,00". What a cancellation granted is stored on its entry
365
+ in `receipt.cancellations[]` as `promoAdjustmentCents` (cents per VAT bucket
366
+ of the backend). The register can show it in the dialog; it does not have to
367
+ calculate anything.
368
+
369
+ **Printed receipt.** The header of a cancellation receipt names the original,
370
+ its date and the reason: "STORNOBELEG / Stornobuchung zu Beleg KASSE1-ID-42 /
371
+ vom 11.08.2026, 09:02 Uhr / Grund: Fehleingabe". The date comes from
372
+ `cancellationOf.timeStamp` (backend since 2026-09-04); older receipts without
373
+ it omit that line.
374
+
375
+ ## Sending a receipt by email
376
+
377
+ ```ts
378
+ const confirmation = await api.sendReceiptEmail({
379
+ fullReceiptId: original.fullReceiptId, // or: await api.generateFullReceiptId(receiptId)
380
+ to: 'guest@example.at',
381
+ sprache: 'de', // optional; the backend currently only uses 'de'
382
+ });
383
+ confirmation.to; // address as the backend logged it (trimmed, lower case)
384
+ confirmation.at; // time, ISO with Vienna offset
385
+ confirmation.via; // 'eigen' | 'plattform' | 'plattform-fallback' | null
386
+ ```
387
+
388
+ The email contains a **link to the public receipt page**, not a PDF
389
+ attachment: the receipt page uses the same line model as screen and printed
390
+ receipt and offers a PDF there. The receipt itself stays untouched (BAO § 131,
391
+ RKSV); the backend keeps the sending log next to it.
392
+
393
+ The cash register comes from the authentication (`cashregister-token` header or
394
+ the parameter set by `registerUserAuth`), not from the options. A receipt of
395
+ another register therefore gets the same answer as a receipt that does not
396
+ exist. The codes are in `RECEIPT_EMAIL_ERROR_CODES` (`isReceiptEmailErrorCode`):
397
+ `adresse_ungueltig`, `beleg_nicht_gefunden`, `zu_oft` (5 emails per receipt in
398
+ 24 hours, 30 per register per hour) and `versand_fehlgeschlagen`.
280
399
 
281
- **Bon.** Der Kopfblock des Storno-Bons nennt Bezug, Datum des Originals und
282
- Grund: „STORNOBELEG / Stornobuchung zu Beleg KASSE1-ID-42 / vom 11.08.2026,
283
- 09:02 Uhr / Grund: Fehleingabe". Das Datum kommt aus `cancellationOf.timeStamp`
284
- (Backend seit 2026-09-04); Altbelege ohne bleiben ohne die Zeile.
400
+ ## Receipt printing: QR code, logo, printers
285
401
 
286
- ## Beleg per E-Mail an den Gast
402
+ ### The QR code fits the paper
403
+
404
+ The native QR command is given a module size in dots and does not check
405
+ whether the symbol plus quiet zone fits the roll. Too wide means, on most
406
+ thermal printers, not "cut off" but **no QR code at all**: on a mandatory
407
+ receipt the worst possible result. So this package calculates the size instead
408
+ of setting it:
287
409
 
288
410
  ```ts
289
- const bestaetigung = await api.sendReceiptEmail({
290
- fullReceiptId: beleg.fullReceiptId, // oder api.generateFullReceiptId(receiptId)
291
- to: 'gast@example.at',
292
- sprache: 'de', // optional; heute wertet das Backend nur 'de' aus
411
+ import { qrGroesseFuer, QR_DRUCK_PUNKTE } from '@kreiseck/kasseneck-api/printing';
412
+
413
+ const qrSize = qrGroesseFuer({ nutzlast: receipt.qr, papierbreitePunkte: QR_DRUCK_PUNKTE.mm58 });
414
+ // qrSize.punkte: dots per module; null = does not fit even with the exception size
415
+ // qrSize.unterMindestmass: printed, but below 4 dots per module
416
+ ```
417
+
418
+ On the receipt path this happens automatically. `qrGroesse` is a **cap**, not
419
+ a target: the largest size that fits is printed, at most the cap. `auto` (the
420
+ default) caps at 6 dots per module, as in the Dart twin and on the Epson path;
421
+ `klein` caps at 4, `mittel` at 6, `gross` at 8. All print paths use error
422
+ correction level M.
423
+
424
+ ```ts
425
+ import { escPosLayoutErgebnis } from '@kreiseck/kasseneck-api/receipt';
426
+
427
+ const { bytes, qrFehler, qrAusweich } = escPosLayoutErgebnis(layout, {
428
+ qrGroesse: 'gross', // 'auto' | 'klein' | 'mittel' | 'gross'
429
+ qrModus: 'nativeModel1', // older printers that only support model 1
430
+ qrMatrix: matrixFor, // fallback: the QR code as an image instead of none
293
431
  });
294
- bestaetigung.to; // Adresse, wie das Backend sie protokolliert hat
295
- bestaetigung.at; // Zeitpunkt, ISO mit Wiener Zonenoffset
296
- bestaetigung.via; // 'eigen' | 'plattform' | 'plattform-fallback' | null
297
432
  ```
298
433
 
299
- Verschickt wird ein **Link auf die öffentliche Belegseite**, kein PDF im
300
- Anhang: die Belegseite führt dasselbe Zeilenmodell wie Bildschirm und Bon und
301
- liefert dort auf Wunsch ein PDF. Der Beleg selbst bleibt unberührt (BAO §131 /
302
- RKSV) — das Versandprotokoll führt das Backend neben ihm.
434
+ The Epson ePOS path (`eposPrintXml` / `eposDirectPrint`) calculates the same
435
+ way; `eposPrintXmlErgebnis` returns `{ xml, qrFehler, qrAusweich }`. The default
436
+ there is also `auto`.
303
437
 
304
- Die Kasse kommt aus der Anmeldung (Kopfzeile `cashregister-token` bzw. der
305
- Parameter, den `registerUserAuth` setzt), nicht aus den Optionen; ein Beleg
306
- einer anderen Kasse ist deshalb dieselbe Auskunft wie ein Beleg, den es nicht
307
- gibt. Auch hier gilt: **am Code entscheiden, nicht am Text.** Die Codes stehen
308
- in `RECEIPT_EMAIL_ERROR_CODES` (`isReceiptEmailErrorCode`):
309
- `adresse_ungueltig`, `beleg_nicht_gefunden`, `zu_oft` (5 Mails je Beleg in 24
310
- Stunden, 30 je Kasse und Stunde) und `versand_fehlgeschlagen`.
438
+ `qrFehler` means "receipt without QR code": tell the customer. `qrAusweich`
439
+ means "printed, but the configured mode does not suit this device": tell the
440
+ manager. The image fallback only runs with a `qrMatrix` function that turns
441
+ the payload into a finished matrix; the package itself neither encodes QR
442
+ codes nor processes images for it.
311
443
 
312
- ## Partner-API (`./partner`)
444
+ ### The receipt sheet: the same receipt everywhere
313
445
 
314
- Für Softwarehäuser, die Kasseneck in ihr eigenes Produkt einbauen: Betriebe
315
- anlegen, bis zur laufenden Kasse begleiten und danach in ihrem Namen Belege
316
- signieren.
446
+ Screen, ESC/POS, ePOS and PDF all set the same **sheet**: grid lines, company
447
+ logo, QR code and the Kasseneck logo at the end, with sizes as a share of the
448
+ sheet width and in lines (one line = two character widths).
317
449
 
318
- **Was die Endpunkte tun, steht in der Referenz** —
319
- `docs/api/partner.md` (ausführlich) und `docs/api/partner.llms.txt` (kompakt,
320
- für Werkzeuge und Sprachmodelle). Dieses README wiederholt sie nicht; hier
321
- steht, wie man den Client benutzt.
450
+ ```tsx
451
+ import { BelegBlattView } from '@kreiseck/kasseneck-api/react';
322
452
 
323
- Der Partner-Schlüssel (`pk_live_…`) gehört auf einen **Server**. Er kann
324
- Betriebe anlegen und — mit dem Zusatz-Scope `credentials:read` — deren
325
- Geheimnisse holen.
453
+ const { receipt, company, logoStufe } = await api.getReceiptWithCompany(receiptId);
454
+ const layout = buildReceiptLayout(receipt, company);
455
+
456
+ <BelegBlattView
457
+ layout={layout}
458
+ logo={company.logoUrl ? { url: company.logoUrl, stufe: logoStufe } : null}
459
+ marke={company.showKreiseckLogo}
460
+ renderQr={(data) => <QrSvg data={data} />}
461
+ />
462
+ ```
463
+
464
+ ```ts
465
+ import { escPosLayoutBytes, logoMass, logoRaster } from '@kreiseck/kasseneck-api/receipt';
466
+
467
+ const logoSize = logoMass({ stufe: 'M', pxBreite: image.width, pxHoehe: image.height }, 48);
468
+ const raster = logoRaster(imageData.data, image.width, image.height, logoSize, 48);
469
+ escPosLayoutBytes(layout, { paperSize: 'mm80', logo: { stufe: 'M', pxBreite: image.width, pxHoehe: image.height, raster }, marke: true });
470
+ ```
471
+
472
+ Logo sizes: S 42 % × 5 lines, M 62 % × 8, L 80 % × 12, XL 94 % × 16, always
473
+ fitted, never scaled up. Banners (TESTKASSE, STORNOBELEG, …) are framed by
474
+ `=` lines, the same on every path. The package rasterises finished RGBA pixels;
475
+ loading and decoding the image (PNG, JPEG) stays with your application.
476
+
477
+ ### Getting the bytes to the printer
478
+
479
+ The package generates ESC/POS bytes and ePOS XML, and it ships three ways to
480
+ deliver them: **WebUSB** (`usbConnectPrinter`, `usbPrint` in `…/printing`, for
481
+ Chromium-based browsers), **Epson ePOS over HTTP** (`eposDirectPrint` in
482
+ `…/receipt`) and **print jobs** for network printers managed by the backend
483
+ (`listMyPrinters`, `createPrintJob` in `…/kasse`). Bluetooth, serial ports and
484
+ raw TCP sockets are up to your application. PDF generation is not part of the
485
+ package.
486
+
487
+ ## Card payments
488
+
489
+ `…/payments` covers three ways to take card payments from a browser or a Node
490
+ process:
491
+
492
+ - **Stripe payment links** (`createStripeLink`, `stripeCaptureIntent`): remote
493
+ payment by link or QR code, through the backend.
494
+ - **Hobex cloud** (`hobexPay`, `hobexRefund`): a terminal registered with
495
+ Hobex, controlled over the network through the backend. Amounts are passed in
496
+ cents; the package converts to euros for Hobex.
497
+ - **Hobex HPS via Kasseneck Connect**, described below.
498
+
499
+ ### Hobex HPS via Kasseneck Connect
500
+
501
+ A browser has no raw TCP sockets, so **direct** terminal contact as in the
502
+ Flutter package `kasseneck_api` (`HpsClient`) stays out of reach of this
503
+ package. **Kasseneck Connect** is a local device agent with a plain HTTP
504
+ interface that talks to the terminal on behalf of the register, and that is
505
+ the way in:
506
+
507
+ ```ts
508
+ import { createHpsConnectClient, createHpsPayments } from '@kreiseck/kasseneck-api/payments';
509
+
510
+ const client = createHpsConnectClient({ token: pairingToken });
511
+ const terminal = createHpsPayments(client, { host: '192.168.1.50', tid: '3600335' });
512
+
513
+ const payment = await terminal.pay({ amountCents: 1050 });
514
+ // payment.outcome: 'approved' | 'declined' | 'unresolved', never guessed.
515
+ // payment.transactionId is ALWAYS set, also for 'unresolved'.
516
+
517
+ // Refund, and void of an earlier payment, work the same way:
518
+ await terminal.refund({ amountCents: 1050, originalTransactionId: payment.transactionId });
519
+ await terminal.cancel({ transactionId: payment.transactionId, amountCents: 1050 });
520
+ ```
521
+
522
+ The outcome is always one of three: `approved`, `declined` (provably nothing
523
+ charged) or `unresolved` (outcome unknown; a retry could charge a second time).
524
+ What that means and why it is built this way is documented in
525
+ `src/payments/hobex-hps/payments.ts`; that documentation is authoritative, not
526
+ this README.
527
+
528
+ Terminals that can only be driven through a vendor's Android SDK cannot be
529
+ reached from a browser and are not part of this package.
530
+
531
+ ## Partner API (`./partner`)
532
+
533
+ For software vendors who build Kasseneck into their own product: create
534
+ businesses, accompany them until the cash register is live, and then sign
535
+ receipts on their behalf.
536
+
537
+ The partner key (`pk_live_…`) belongs on a **server**. It can create
538
+ businesses and, with the extra scope `credentials:read`, fetch their secrets.
326
539
 
327
540
  ```ts
328
541
  import { createPartnerApi, istPartnerFehler } from '@kreiseck/kasseneck-api/partner';
@@ -331,78 +544,76 @@ const partner = createPartnerApi({ partnerKey: process.env.KASSENECK_PARTNER_KEY
331
544
 
332
545
  const { customerId } = await partner.createPartnerCustomer({
333
546
  appId: 'app_…',
334
- idempotencyKey: kundennummer, // die eigene — schützt vor Doppelanlage
335
- betrieb: { /* Stammdaten, siehe Referenz */ } as never,
336
- // env: 'test' — auch mit einem LIVE-Schlüssel erlaubt: so probt man die
337
- // ganze Kette, ohne sich einen zweiten Schlüssel zu holen. Umgekehrt nie.
547
+ idempotencyKey: customerNumber, // your own number; protects against duplicates
548
+ business, // master data (type Betrieb): name, legal form, address, tax details, contacts
549
+ // env: 'test' is allowed even with a LIVE key: that is how you rehearse the
550
+ // whole chain without a second key. Never the other way round.
338
551
  });
339
552
 
340
553
  await partner.sendPartnerCustomerFonLink(customerId);
341
- // … auf das Ereignis customer.fon_verified warten …
554
+ // … wait for the event customer.fon_verified …
342
555
  await partner.requestCustomerSignature(customerId);
343
- // … auf signature.ready warten …
344
- await partner.createCustomerCashregister({ customerId }); // automatisch:true ist Vorgabe
556
+ // … wait for signature.ready …
557
+ await partner.createCustomerCashregister({ customerId }); // automatic: true is the default
345
558
  ```
346
559
 
347
- Die Reihenfolge ist hart, und jeder Schritt beschwert sich mit einem eigenen
348
- Code, wenn ein vorheriger fehlt. Sie steht als Daten im Paket
349
- (`PARTNER_ABLAUF`), und zu jedem Code gibt es einen Handlungssatz:
560
+ The order is strict, and each step complains with its own code if an earlier
561
+ one is missing. The order ships as data (`PARTNER_ABLAUF`), and for every code
562
+ there is an action hint:
350
563
 
351
564
  ```ts
352
565
  try {
353
566
  await partner.activateCashregister(customerId, cashregisterId);
354
- } catch (fehler) {
355
- if (istPartnerFehler(fehler, 'signature_not_ready')) {
356
- // Die Signatur DIESER Kasse ist noch nicht bereit — auf signature.ready warten.
567
+ } catch (error) {
568
+ if (istPartnerFehler(error, 'signature_not_ready')) {
569
+ // The signature of THIS register is not ready yet: wait for signature.ready.
357
570
  console.error(partner.fehlerRat('signature_not_ready'));
358
571
  }
359
572
  }
360
573
  ```
361
574
 
362
- ### Eine Probe ist keine Kasse
575
+ ### A test event is not a cash register
363
576
 
364
- `sendPartnerWebhookTest(webhookId, 'cashregister.live')` löst genau das
365
- Ereignis aus, das der eigene Handler behandeln soll — eine Leitungsprobe
366
- beweist nichts über die Behandlung des Ernstfalls. Damit niemand eine Probe
367
- für echt hält, trägt sie `test: true` im Umschlag:
577
+ `sendPartnerWebhookTest(webhookId, 'cashregister.live')` fires exactly the
578
+ event your handler is meant to handle; a mere connectivity check proves
579
+ nothing about handling the real case. So that nobody takes a test for real, it
580
+ carries `test: true` in the envelope:
368
581
 
369
582
  ```ts
370
- const geprueft = await parseWebhookEvent({ secret, signatureHeader, body, });
371
- if (!geprueft.ok) return antwort(400);
583
+ const checked = await parseWebhookEvent({ secret, signatureHeader, body });
584
+ if (!checked.ok) return reply(400);
372
585
 
373
- if (geprueft.event.test) return antwort(200); // Probe: nichts weiter tun
586
+ if (checked.event.test) return reply(200); // test event: do nothing else
374
587
  ```
375
588
 
376
- Ohne diese Zeile schreibt jemand seinem Kunden, die Kasse sei fertig.
589
+ Without that line someone tells their customer the register is ready.
377
590
 
378
- ### Zugangsdaten sind Geheimnisse eines Dritten
591
+ ### Credentials are a third party's secrets
379
592
 
380
- `getCustomerCredentials` liefert den `api_key` des Betriebs und die Token
381
- seiner Kassen. Wer sie hat, kann in seinem Namen Belege signieren — und ein
382
- Beleg ist nach RKSV nicht zurücknehmbar. Sie kommen deshalb **nicht als
383
- `string`**, sondern in einer Hülle, die sich nicht versehentlich ausgeben
384
- lässt:
593
+ `getCustomerCredentials` returns the business's `api_key` and the tokens of its
594
+ cash registers. Whoever holds them can sign receipts in its name, and under the
595
+ RKSV a receipt cannot be taken back. They therefore do **not** come as
596
+ `string`, but in a wrapper that cannot be printed by accident:
385
597
 
386
598
  ```ts
387
- const zugang = await partner.getCustomerCredentials(customerId);
599
+ const credentials = await partner.getCustomerCredentials(customerId);
388
600
 
389
- console.log(zugang); // [apiKey «verborgen»] — kein Klartext
390
- JSON.stringify(zugang); // ebenso
391
- `${zugang.apiKey}`; // ebenso
601
+ console.log(credentials.apiKey); // [apiKey «verborgen»], no plain text
602
+ JSON.stringify(credentials); // every secret inside is masked the same way
603
+ `${credentials.apiKey}`; // same
392
604
 
393
- speichereVerschluesselt(zugang.apiKey.reveal()); // der einzige Weg heraus
605
+ storeEncrypted(credentials.apiKey.reveal()); // the only way out
394
606
  ```
395
607
 
396
- Nur verschlüsselt speichern, nie protokollieren, nie in eine Mail oder einen
397
- Fehlerbericht. Jeder Abruf wird mitgeschrieben und ist für den Betrieb
398
- sichtbar.
608
+ Store them encrypted only, never log them, never put them in an email or an
609
+ error report. Every fetch is recorded and visible to the business.
399
610
 
400
- ### Eingehende Webhooks prüfen
611
+ ### Verifying incoming webhooks
401
612
 
402
- Das ist die Stelle, an der Integrationen am häufigsten scheitern — deshalb
403
- liegt sie fertig im Paket. Vier Dinge müssen stimmen: der **rohe** Rumpf, das
404
- Zeitfenster gegen Wiedereinspielung, ein zeitkonstanter Vergleich, und jede
405
- Ausnahme als Ablehnung.
613
+ This is where integrations fail most often, so it ships ready-made. Four
614
+ things must hold: the **raw** body, a time window against replays (300 seconds
615
+ by default), a constant-time comparison, and every exception treated as a
616
+ rejection.
406
617
 
407
618
  ```ts
408
619
  import express from 'express';
@@ -410,435 +621,338 @@ import { parseWebhookEvent } from '@kreiseck/kasseneck-api/partner';
410
621
 
411
622
  const app = express();
412
623
 
413
- // express.raw VOR jedem JSON-Parser: signiert sind die Bytes, die ankommen.
624
+ // express.raw BEFORE any JSON parser: the signature covers the bytes as received.
414
625
  app.post('/kasseneck-webhook', express.raw({ type: '*/*' }), async (req, res) => {
415
- const ergebnis = await parseWebhookEvent({
626
+ const result = await parseWebhookEvent({
416
627
  secret: process.env.KASSENECK_WEBHOOK_SECRET!,
417
628
  signatureHeader: req.header('X-Kasseneck-Signature'),
418
- body: req.body, // Buffer — nicht req.body nach JSON.parse
629
+ body: req.body, // Buffer, not req.body after JSON.parse
419
630
  });
420
- if (!ergebnis.ok) return res.status(400).send(ergebnis.reason);
631
+ if (!result.ok) return res.status(400).send(result.reason);
421
632
 
422
- // Innerhalb von 10 s antworten, Arbeit danach. Zustellungen können sich
423
- // wiederholen: auf event.id entdoppeln.
633
+ // Answer within 10 s, work afterwards. Deliveries can repeat:
634
+ // deduplicate on event.id.
424
635
  res.sendStatus(200);
425
- await verarbeite(ergebnis.event);
636
+ await handle(result.event);
426
637
  });
427
638
  ```
428
639
 
429
- ## Rechnungs-API (`./rechnung`)
430
-
431
- Für Shops, Buchhaltungs- und Branchensoftware: **Rechnungen** (§ 11 UStG) —
432
- keine Belege — mit dem `api_key` eines Kontos ausstellen. Eine Rechnung ist
433
- nach dem Aufruf **festgeschrieben**: sie trägt ihre fortlaufende Nummer, ist
434
- unveränderlich und lässt sich nur noch per Gutschrift korrigieren.
640
+ ## Invoice API (`./rechnung`)
435
641
 
436
- **Was die Endpunkte tun, steht in der Referenz** — `docs/api/rechnungen.md`
437
- im Backend. Hier steht, wie man den Client benutzt. Der Schlüssel gehört auf
438
- einen **Server**.
642
+ For shops, accounting and industry software: issue **invoices** (*Rechnung*,
643
+ § 11 UStG), not receipts, with the `api_key` of an account. An invoice is
644
+ **finalised** by the call: it carries its sequential number, cannot be changed
645
+ and can only be corrected by a credit note. The key belongs on a **server**.
439
646
 
440
647
  ```ts
441
648
  import { createRechnungApi, istRechnungFehler } from '@kreiseck/kasseneck-api/rechnung';
442
649
 
443
- const rechnungen = createRechnungApi({ apiKey: process.env.KASSENECK_API_KEY! });
650
+ const invoices = createRechnungApi({ apiKey: process.env.KASSENECK_API_KEY! });
444
651
 
445
- // 1. Kunde einmal anlegen — externalId ist die eigene Kundennummer.
446
- let kunde;
652
+ // 1. Create the customer once; externalId is your own customer number.
653
+ let customer;
447
654
  try {
448
- kunde = await rechnungen.createCustomer({
655
+ customer = await invoices.createCustomer({
449
656
  type: 'company', name: 'Café Muster GmbH', country: 'AT',
450
657
  street: 'Hauptplatz', houseNumber: '3', zip: '1010', city: 'Wien',
451
658
  externalId: 'shop-4711',
452
659
  });
453
- } catch (fehler) {
454
- if (!istRechnungFehler(fehler, 'customer_exists')) throw fehler;
455
- kunde = await rechnungen.getCustomer({ externalId: 'shop-4711' });
660
+ } catch (error) {
661
+ if (!istRechnungFehler(error, 'customer_exists')) throw error;
662
+ customer = await invoices.getCustomer({ externalId: 'shop-4711' });
456
663
  }
457
664
 
458
- // 2. Ausstellen. Beträge in ganzen Cent; das Rechnungsdatum setzt der Server.
459
- const { invoice, replayed } = await rechnungen.issueInvoice({
460
- idempotencyKey: `bestellung-${bestellnummer}`, // gleiche Bestellung = gleiche Rechnung
461
- customerId: kunde.id,
462
- taxScheme: 'normal',
665
+ // 2. Issue. The server sets the invoice date and derives the tax case.
666
+ const { invoice, replayed } = await invoices.issueInvoice({
667
+ idempotencyKey: `order-${orderNumber}`, // same order = same invoice
668
+ customerId: customer.id,
463
669
  priceMode: 'net',
464
670
  serviceStart: '2026-09-14',
465
- items: [{ description: 'Beratung', quantity: 2, unit: 'Std', unitPriceCents: 5000, vatRate: 20 }],
671
+ items: [{ description: 'Consulting', quantity: 2, unit: 'hour', unitPriceCents: 5000, vatRate: 20 }],
466
672
  });
467
673
  // invoice.number, invoice.totals.grossCents (12000), invoice.statusUrl
468
674
 
469
- // 3. Dateien holen.
470
- const pdf = await rechnungen.getInvoicePdf(invoice.id); // Uint8Array, mit Factur-X
471
- const xml = await rechnungen.getInvoiceXml(invoice.id, 'ubl'); // Peppol-UBL als Text
675
+ // 3. Fetch the files.
676
+ const pdf = await invoices.getInvoicePdf(invoice.id); // Uint8Array, with Factur-X
677
+ const xml = await invoices.getInvoiceXml(invoice.id, 'ubl'); // Peppol UBL as text
472
678
 
473
- // 4. Korrigieren — nur per Gutschrift.
474
- await rechnungen.createCreditNote({
475
- idempotencyKey: `nachlass-${bestellnummer}`,
679
+ // 4. Correct, only by credit note.
680
+ await invoices.createCreditNote({
681
+ idempotencyKey: `discount-${orderNumber}`,
476
682
  invoiceId: invoice.id,
477
683
  reason: 'price_reduction',
478
- items: [{ description: 'Nachlass Beratung', quantity: 1, unitPriceCents: 2000, vatRate: 20 }],
684
+ items: [{ description: 'Discount on consulting', quantity: 1, unitPriceCents: 2000, vatRate: 20 }],
479
685
  });
480
686
  ```
481
687
 
482
- **Vor dem ersten Ausstellen die Einrichtung abfragen.** Die Rechnungs-API muss
483
- für das Konto von Kasseneck freigegeben sein (live), und Firmenname, Anschrift,
484
- UID, Bankverbindung und Nummernformat müssen stehen — sonst antwortet
485
- `issueInvoice` mit `invoice_api_not_enabled` bzw. `invoice_setup_incomplete`:
688
+ **Prices and quantities.** Each line takes exactly one of `unitPriceCents`
689
+ (whole cents) and `unitPriceMicros` (micro-euros, 10⁻⁶ €, for prices below one
690
+ cent), in the invoice's `priceMode` (net or gross). `quantity` allows up to
691
+ three decimals, `discountPct` up to two. `taxScheme` is optional: the server
692
+ derives the tax case and checks a given value against it.
693
+
694
+ **Check the setup before the first invoice.** The invoicing module must be
695
+ active, the invoice API enabled for the account by Kasseneck, the account live,
696
+ and company name, address, VAT ID, bank account and number format must be
697
+ filled in. Otherwise `issueInvoice` answers with `invoice_api_not_enabled` or
698
+ `invoice_setup_incomplete`:
486
699
 
487
700
  ```ts
488
- const status = await rechnungen.getInvoiceSetupStatus();
701
+ const status = await invoices.getInvoiceSetupStatus();
489
702
  if (!status.ready) console.warn(status.missing.map((m) => m.message).join('\n'));
490
703
  ```
491
704
 
492
- **Sprache und Marke.** Eine Rechnung hat eine Nummer und **eine** Sprache (`de`
493
- oder `en`, `INVOICE_LANGUAGES`): die der Anfrage, sonst die des Kunden
494
- (`customer.language`), sonst Deutsch. Sie wird beim Ausstellen eingefroren;
495
- Gutschriften übernehmen Sprache und Marke ihrer Rechnung. Behörden bekommen
496
- immer Deutsch (`language_not_allowed`). Datum und Beträge bleiben in jeder
497
- Sprache österreichisch formatiert.
705
+ **Language and brand.** An invoice has one number and **one** language (`de`
706
+ or `en`, `INVOICE_LANGUAGES`): the one in the request, otherwise the customer's
707
+ (`customer.language`), otherwise German. It is frozen when the invoice is
708
+ issued; credit notes take over language and brand of their invoice. Public
709
+ authorities always get German (`language_not_allowed`). Dates and amounts keep
710
+ Austrian formatting in every language.
498
711
 
499
712
  ```ts
500
- const [marke] = await rechnungen.listBrands(); // [{ id, name, isDefault }]
501
- const { invoice } = await rechnungen.issueInvoice({
502
- idempotencyKey: `bestellung-${bestellnummer}`,
503
- customerId: kunde.id,
504
- taxScheme: 'normal', priceMode: 'net', serviceStart: '2026-09-15',
505
- language: 'en', // sonst die Sprache des Kunden
506
- brandId: marke.id, // sonst die Standardmarke
713
+ const [brand] = await invoices.listBrands(); // [{ id, name, isDefault }]
714
+ const { invoice } = await invoices.issueInvoice({
715
+ idempotencyKey: `order-${orderNumber}`,
716
+ customerId: customer.id,
717
+ priceMode: 'net', serviceStart: '2026-09-15',
718
+ language: 'en', // otherwise the customer's language
719
+ brandId: brand.id, // otherwise the default brand
507
720
  items: [{ description: 'Consulting', quantity: 2, unitPriceCents: 5000, vatRate: 20 }],
508
721
  });
509
- // Dieselbe Rechnung als deutsche Übersetzung — KEINE zweite Rechnung:
510
- const kopie = await rechnungen.getInvoicePdf(invoice.id, { language: 'de' });
722
+ // The same invoice as a German translation, NOT a second invoice:
723
+ const copy = await invoices.getInvoicePdf(invoice.id, { language: 'de' });
511
724
  ```
512
725
 
513
- Die Übersetzungskopie trägt dieselbe Nummer, ist auf jeder Seite als
514
- „Übersetzung – keine eigene Rechnung" gekennzeichnet und hat keine eingebettete
515
- E-Rechnung. Eine zweite Rechnung mit eigener Nummer für dieselbe Leistung
516
- wäre umsatzsteuerlich ein Problem (UStR Rz 1527); die Kopie ist das nicht (Rz 1528).
517
- Die Texte beider Sprachen liegen als `RECHNUNG_TEXTE` bzw.
518
- `fixtures/rechnung-texte.json` im Paket.
726
+ The translated copy carries the same number, is marked on every page as a
727
+ translation that is not an invoice of its own ("Übersetzung – keine eigene
728
+ Rechnung"), and has no embedded e-invoice. The texts of both languages ship as
729
+ `RECHNUNG_TEXTE` and `fixtures/rechnung-texte.json`.
519
730
 
520
- **Einheiten sind Schlüssel, kein freier Text.** `items[].unit` nimmt einen Wert aus
521
- `INVOICE_UNITS` (`piece`, `hour`, `day`, `flat_rate`, `kilogram`, `square_metre`, …;
522
- ohne Angabe `piece`). Gedruckt wird das Kürzel in der Sprache der Rechnung — `Stk`
523
- bzw. `pcs` —, und die E-Rechnung trägt den UN/ECE-Code aus
524
- `RECHNUNG_EINHEITEN_CODES` (`C62`, `HUR`, …). Freier Text wie `"Std"` ist
525
- `validation` mit Feld `items[0].unit`.
731
+ **Units are keys, not free text.** `items[].unit` takes a value from
732
+ `INVOICE_UNITS` (`piece`, `hour`, `day`, `flat_rate`, `kilogram`,
733
+ `square_metre`, …; default `piece`). The printed abbreviation follows the
734
+ invoice language (`Stk` or `pcs`), and the e-invoice carries the UN/ECE code
735
+ from `RECHNUNG_EINHEITEN_CODES` (`C62`, `HUR`, …). Free text such as `"Std"` is
736
+ a `validation` error on field `items[0].unit`.
526
737
 
527
- **Schon bezahlt?** Wer online kassiert und danach die Rechnung stellt, gibt die
528
- Zahlung gleich mit: sie entsteht in derselben Transaktion wie das Festschreiben,
529
- und das PDF trägt dann keinen Zahlungskasten und keinen Giro-QR.
738
+ **Already paid?** If you take the payment online and invoice afterwards, pass
739
+ the payment along: it is recorded in the same transaction as the
740
+ finalisation, and the PDF then has no payment box and no giro QR code.
530
741
 
531
742
  ```ts
532
- const { invoice } = await rechnungen.issueInvoice({
533
- idempotencyKey: `bestellung-${bestellnummer}`,
534
- customerId: kunde.id,
535
- taxScheme: 'normal', priceMode: 'net', serviceStart: '2026-09-16',
536
- items: [{ description: 'Beratung', quantity: 2, unitPriceCents: 5000, vatRate: 20, unit: 'hour' }],
537
- payment: { method: 'card', reference: zahlung.id }, // ohne amountCents: voll bezahlt
538
- }); // invoice.openCents === 0
539
-
540
- // Trifft das Geld erst später ein (Überweisung, Teilzahlung):
541
- await rechnungen.recordInvoicePayment({
542
- idempotencyKey: `zahlung-${zahlung.id}`, // Pflicht: sonst bucht eine Wiederholung zweimal
743
+ const { invoice } = await invoices.issueInvoice({
744
+ idempotencyKey: `order-${orderNumber}`,
745
+ customerId: customer.id,
746
+ priceMode: 'net', serviceStart: '2026-09-16',
747
+ items: [{ description: 'Consulting', quantity: 2, unitPriceCents: 5000, vatRate: 20, unit: 'hour' }],
748
+ payment: { method: 'card', reference: payment.id }, // without amountCents: paid in full
749
+ }); // invoice.openCents === 0
750
+
751
+ // If the money arrives later (bank transfer, partial payment):
752
+ await invoices.recordInvoicePayment({
753
+ idempotencyKey: `payment-${payment.id}`, // required: otherwise a retry books twice
543
754
  invoiceId: invoice.id, method: 'transfer', amountCents: 12000, paidAt: '2026-09-20',
544
755
  });
545
756
  ```
546
757
 
547
- `reference` wird gespeichert, aber **nicht gedruckt**; Kartendaten gehören
548
- ohnehin nicht auf eine Rechnung.
758
+ `reference` is stored but **not printed**; card data does not belong on an
759
+ invoice anyway.
549
760
 
550
- **Vorab rechnen.** Wer kassiert, bevor die Rechnung entsteht, braucht den
551
- Betrag, den die Rechnung später ausweist. `rechnungSummen` rechnet ihn genau
552
- wie der Server; `previewInvoice` fragt den Server selbst — ein Probelauf, der
553
- prüft wie das Ausstellen, aber nichts festschreibt und den `idempotencyKey`
554
- nicht verbraucht:
761
+ **Cash sales.** The API treats `method: 'cash'`, and `method: 'card'` with
762
+ `onSite: true` (terminal at the point of sale), as a cash sale (*Barumsatz*,
763
+ § 131b (1) no. 3 BAO). A card payment in an online shop is sent without
764
+ `onSite`:
555
765
 
556
766
  ```ts
557
- import { rechnungSummen } from '@kreiseck/kasseneck-api/rechnung';
558
-
559
- const posten = [
560
- { description: 'Maniküre', quantity: 1, unitPriceCents: 1479, vatRate: 20 as const },
561
- { description: 'Lack', quantity: 1, unitPriceCents: 1500, vatRate: 20 as const },
562
- ];
563
- rechnungSummen(posten, 'gross');
564
- // { netCents: 2483, vatCents: 496, grossCents: 2979, byRate: [{ rate: 20, … }] }
565
-
566
- const anfrage = { idempotencyKey: `bestellung-${bestellnummer}`, customerId: kunde.id,
567
- priceMode: 'gross' as const, serviceStart: '2026-09-16', items: posten };
568
- const { preview, notice } = await rechnungen.previewInvoice(anfrage);
569
- // preview.totals, preview.taxScheme, preview.taxSchemeReason — dann:
570
- await rechnungen.issueInvoice(anfrage);
571
- ```
572
-
573
- Im **Brutto-Modus** ist das Brutto je Satz der vereinbarte Preis: Netto =
574
- round(B × 100 / (100 + Satz)), USt = B − Netto. Im **Netto-Modus** wird die USt
575
- je Satz aus der Nettosumme gerundet. Gerundet wird kaufmännisch (halber Cent
576
- aufwärts), je Satz, dann summiert. Die Prüffälle liegen in
577
- `fixtures/rechnung-summen.json`. Die Summen sind auch bei Gutschriften positiv —
578
- das Vorzeichen steht im Belegtyp (`docType: 'GU'`).
579
-
580
- **Vorab rechnen, ohne Server.** `@kreiseck/kasseneck-api/rechnung/rechnen` ist
581
- der reine Rechenkern dahinter: kein Transport, kein Zugangsschlüssel, läuft
582
- auch im Browser (Panel). Er rechnet intern mit ganzen Zahlen (BigInt) statt
583
- Gleitkomma und rundet genau einmal je USt-Satz — Preise in Millionstel Euro
584
- (`unitPriceMicros`), Mengen in Tausendstel (`quantityMilli`), Rabatt und Satz
585
- in Hundertstel-Prozent (`discountBp`, `vatRateBp`):
586
-
587
- ```ts
588
- import { rechnungRechnen } from '@kreiseck/kasseneck-api/rechnung/rechnen';
589
-
590
- rechnungRechnen(
591
- [{ unitPriceMicros: 14_790_000, quantityMilli: 1000, vatRateBp: 2000 }],
592
- { priceMode: 'gross' },
593
- );
594
- // { netCents: 1233, vatCents: 246, grossCents: 1479, byRate: [{ rateBp: 2000, … }], lines: […] }
767
+ payment: { method: 'card', onSite: true } // terminal at the point of sale
768
+ payment: { method: 'card' } // card payment in the online shop
595
769
  ```
596
770
 
597
- `positionAusEuro(item)` wandelt eine Euro-Position (`unitPrice`, `quantity`,
598
- `vatRate`, `discountPct`) verlustfrei in diese Form um oder nennt Feld und
599
- Grund, wenn das nicht geht. Die Prüffälle liegen in
600
- `fixtures/rechnung-rechnen.json`, `fixtures/rechnung-rechnen-zufall.json` und
601
- `fixtures/position-aus-euro.json`. **Noch nicht zusammengeführt** mit
602
- `rechnungSummen`: bis 0.24.0 rechnen beide parallel und weichen an
603
- Halbcent-Grenzen bewusst voneinander ab — **je USt-Satz** bis zu 1 Cent bei
604
- einmal gerundeten Werten (Netto und USt im Netto-Modus, Brutto im
605
- Brutto-Modus) und bis zu 2 Cent bei abgeleiteten (Summe zweier Rundungen). Bei
606
- mehreren Sätzen summiert sich das: eine Rechnung über drei Sätze kann deshalb
607
- 3 Cent auseinanderliegen. Der Kern rundet dort richtig, `rechnungSummen`
608
- rechnet weiterhin wie der Server heute. Verbindlich
609
- für den ausgewiesenen Betrag bleibt bis dahin `previewInvoice`.
610
-
611
- **Hinweise** (`notice`) sind immer eine Liste — bei `issueInvoice`,
612
- `previewInvoice` und `recordInvoicePayment`. Eine ig. Lieferung trägt
613
- `recapitulative_statement_due` (Zusammenfassende Meldung), eine bar bezahlte
614
- Rechnung zusätzlich `cash_receipt_required`.
615
-
616
- **Barumsatz ist nicht nur Bargeld.** Als Barzahlung gilt auch die Karte **vor
617
- Ort** an der Kasse (§ 131b Abs. 1 Z 3 UStG) — dieselbe Karte im Internet
618
- dagegen nicht. Weil `card` und `online` beides sein können, sagt es das
619
- Fremdsystem selbst:
771
+ For those cases the response's `notice` list contains `cash_receipt_required`:
772
+ a cash sale needs a receipt (§ 132a BAO), from the fiscal cash register where
773
+ the business is obliged to use one. The note on the invoice does not replace
774
+ it. `transfer` with `onSite` is a field error. Whether a payment counts as a
775
+ cash sale is for the business to assess; the flag only tells the API how it
776
+ was classified.
620
777
 
621
- ```ts
622
- payment: { method: 'card', onSite: true } // Terminal an der Kasse
623
- payment: { method: 'card' } // Kartenzahlung im Shop
624
- ```
778
+ **Notices** (`notice`) are always a list, for `issueInvoice`,
779
+ `previewInvoice` and `recordInvoicePayment`. An intra-Community supply carries
780
+ `recapitulative_statement_due` (recapitulative statement).
625
781
 
626
- Bei `cash` (immer) und bei `onSite: true` trägt die Antwort in der Liste
627
- `notice` den Eintrag `cash_receipt_required`: ein Barumsatz braucht einen Beleg
628
- (§ 132a BAO), bei Registrierkassenpflicht über die Registrierkasse — der
629
- Vermerk an der Rechnung ersetzt ihn nicht. `transfer` mit `onSite` ist ein
630
- Feldfehler, eine Überweisung erfolgt nicht vor Ort.
782
+ **After a timeout, retry with the same `idempotencyKey`**, never with a new one:
783
+ you then get the invoice that was already issued (`replayed: true`). The same
784
+ key with different data gives `idempotency_conflict`.
631
785
 
632
- **Nach einem Zeitlimit mit demselben `idempotencyKey` wiederholen**, nie mit
633
- einem neuen: dann kommt die schon ausgestellte Rechnung zurück
634
- (`replayed: true`). Derselbe Schlüssel mit anderen Daten ergibt
635
- `idempotency_conflict`.
786
+ Validation errors arrive as `validation` with field paths
787
+ (`rechnungFeldFehler(error)` → `[{ field: 'items[0].vatRate', message }]`).
788
+ The contract itself ships as data (`RECHNUNG_ANFRAGEN`) and as a JSON Schema at
789
+ `@kreiseck/kasseneck-api/fixtures/rechnung-api.schema.json`; the backend
790
+ validates against exactly this file.
636
791
 
637
- Formfehler kommen als `validation` mit Feldpfaden (`rechnungFeldFehler(fehler)`
638
- → `[{ field: 'items[0].vatRate', message }]`). Der Vertrag selbst liegt als
639
- Daten im Paket (`RECHNUNG_ANFRAGEN`) und als JSON Schema unter
640
- `@kreiseck/kasseneck-api/fixtures/rechnung-api.schema.json`; das Backend prüft
641
- gegen genau diese Datei.
792
+ ### Calculating invoice totals in advance
642
793
 
643
- ## Unterpfade
644
-
645
- | Unterpfad | Inhalt |
646
- |-----------|--------|
647
- | `@kreiseck/kasseneck-api` | Endpunkte, Anmeldung, Transport, Modelle, Enums, Fehler — alles, was mit dem Backend spricht. |
648
- | `…/receipt` | Beleg-Layout als Datenmodell (framework-frei) und die Brücke zu ESC/POS. |
649
- | `…/printing` | ESC/POS-Erzeugung: Bytefolgen für Bondrucker, ohne jeden Transport. |
650
- | `…/payments` | Stripe-Zahllinks, Hobex-Cloud (beides HTTP-Endpunkte des Backends) und Hobex **HPS** über **Kasseneck Connect** (lokaler Geräte-Agent, spricht mit dem Terminal). |
651
- | `…/register` | Anmeldung der Browser-Kasse: Gerät koppeln und entkoppeln, Benutzer auflisten, per PIN anmelden, Sitzung erneuern und beenden. |
652
- | `…/kasse` | Kachel-Kasse: Kassen-Einstellungen (betriebsweit / je Gerät), Artikelgruppen und Artikel für Kacheln, Rabattverteilung je Steuersatz, Reichweiten der Kassen-Rechte |
653
- | `…/partner` | Partner-API: Betriebe anlegen, FinanzOnline-Link, Signatur, Kassen, Zugangsdaten, Webhooks samt Signaturprüfung. **Gehört auf einen Server.** |
654
- | `…/rechnung` | Rechnungs-API: Kunden anlegen und suchen, Rechnungen festgeschrieben ausstellen, Gutschrift und Storno, PDF und E-Rechnung-XML; der Vertrag als Daten. **Gehört auf einen Server.** |
655
- | `…/rechnung/rechnen` | Reiner Rechenkern für Rechnungssummen (Ganzzahlen, kein Transport, keine Abhängigkeit außer Typen) — darf auch im Browser laufen. |
656
- | `…/react` | Dünner React-Adapter, der ein Beleg-Layout zeichnet. Braucht React. |
657
- | `…/fixtures/*` | Golden-Belege (JSON): Eingaben `belege/<name>.json`, zugesagte Zeilenausgabe `erwartet/<name>.lines.json`, `manifest.json` mit Prüfsummen — dieselben Dateien prüfen Backend, Browser-Kasse und Flutter-Paket. |
658
-
659
- So zieht sich niemand den React-Adapter in ein Node-Programm.
660
-
661
- ## Der QR-Code passt aufs Papier
662
-
663
- Der native QR-Befehl bekommt eine Modulgröße in Druckpunkten mit und rechnet
664
- selbst nicht nach, ob das Symbol samt Ruhezone auf die Rolle geht. Zu breit
665
- heißt bei den meisten Bondruckern nicht „abgeschnitten", sondern **gar kein
666
- QR** — auf einem Pflichtbeleg der schlechteste aller Ausgänge. Deshalb rechnet
667
- dieses Paket die Größe, statt sie zu setzen:
794
+ If you charge before the invoice exists, you need the amount the invoice will
795
+ show. `previewInvoice` asks the server itself: a dry run that validates like
796
+ issuing but finalises nothing and does not use up the `idempotencyKey`. Its
797
+ result is authoritative.
668
798
 
669
799
  ```ts
670
- import { qrGroesseFuer, QR_DRUCK_PUNKTE } from '@kreiseck/kasseneck-api/printing';
671
-
672
- const mass = qrGroesseFuer({ nutzlast: beleg.qr, papierbreitePunkte: QR_DRUCK_PUNKTE.mm58 });
673
- // mass.punkte: Punkte je Modul, null = passt auch mit der Ausnahmegröße nicht
674
- // mass.unterMindestmass: gedruckt, aber unter 4 Punkten je Modul
675
- ```
676
-
677
- Am Belegweg passiert das von selbst. `qrGroesse` ist ein **Deckel**, keine
678
- Vorgabe: gedruckt wird die größte Größe, die noch passt, höchstens aber der
679
- Deckel. `auto` (Vorgabe) deckelt bei 6 Punkten je Modul — wie im Dart-Zwilling
680
- und am Epson-Weg; `klein` deckelt bei 4. Alle Druckwege setzen den QR mit
681
- Fehlerkorrektur M.
682
-
683
- ```ts
684
- import { escPosLayoutErgebnis } from '@kreiseck/kasseneck-api/receipt';
685
-
686
- const { bytes, qrFehler, qrAusweich } = escPosLayoutErgebnis(layout, {
687
- qrGroesse: 'gross', // 'auto' | 'klein' | 'mittel' | 'gross'
688
- qrModus: 'nativeModel1', // ältere Drucker, die nur Modell 1 können
689
- qrMatrix: rasterFuer, // Notausgang: der QR als Bild statt gar nicht
690
- });
800
+ const items = [
801
+ { description: 'Manicure', quantity: 1, unitPriceCents: 1479, vatRate: 20 as const },
802
+ { description: 'Nail polish', quantity: 1, unitPriceCents: 1500, vatRate: 20 as const },
803
+ ];
804
+ const request = { idempotencyKey: `order-${orderNumber}`, customerId: customer.id,
805
+ priceMode: 'gross' as const, serviceStart: '2026-09-16', items };
806
+ const { preview, notice } = await invoices.previewInvoice(request);
807
+ // preview.totals, preview.taxScheme, preview.taxSchemeReason, then:
808
+ await invoices.issueInvoice(request);
691
809
  ```
692
810
 
693
- Der Epson-Weg (`eposPrintXml` / `eposDirectPrint`) rechnet genauso;
694
- `eposPrintXmlErgebnis` gibt dort `{ xml, qrFehler, qrAusweich }`. Die Vorgabe
695
- ist auch dort `auto`.
696
-
697
- `qrFehler` heißt „Beleg ohne QR" — das gehört dem Kunden gesagt. `qrAusweich`
698
- heißt „gedruckt, aber der eingestellte Weg taugt für dieses Gerät nicht" — das
699
- gehört dem Chef gesagt. Den Bildweg fährt das Paket nur mit einem `qrMatrix`,
700
- das die Nutzlast in ein fertiges Raster übersetzt: hier wird bewusst weder ein
701
- QR gerechnet noch ein Bild verarbeitet.
702
-
703
- ## Das Beleg-Blatt: überall derselbe Beleg
704
-
705
- Bildschirm, Bon, ePOS und PDF setzen dasselbe **Blatt**: Rasterzeilen, Firmenlogo,
706
- QR und die Marke „erstellt mit Kasseneck", mit Größen als Anteil der Blattbreite
707
- und in Zeilen (eine Zeile = zwei Zeichenbreiten).
708
-
709
- ```tsx
710
- import { BelegBlattView } from '@kreiseck/kasseneck-api/react';
711
-
712
- <BelegBlattView layout={layout} logo={{ url: company.logoUrl, stufe: 'M' }} marke={company.showKreiseckLogo} renderQr={(d) => <QrSvg data={d} />} />
713
- ```
811
+ **Without the server.** `@kreiseck/kasseneck-api/rechnung/rechnen` is the pure
812
+ calculation core: no transport, no key, runs in the browser too. It calculates
813
+ with integers (BigInt) instead of floating point and rounds exactly once per
814
+ VAT rate. Prices are in micro-euros (`unitPriceMicros`), quantities in
815
+ thousandths (`quantityMilli`), discount and VAT rate in hundredths of a
816
+ percent (`discountBp`, `vatRateBp`):
714
817
 
715
818
  ```ts
716
- import { escPosLayoutBytes, logoMass, logoRaster } from '@kreiseck/kasseneck-api/receipt';
717
-
718
- const mass = logoMass({ stufe: 'M', pxBreite: bild.width, pxHoehe: bild.height }, 48);
719
- const raster = logoRaster(imageData.data, bild.width, bild.height, mass, 48);
720
- escPosLayoutBytes(layout, { paperSize: 'mm80', logo: { stufe: 'M', pxBreite: bild.width, pxHoehe: bild.height, raster }, marke: true });
721
- ```
722
-
723
- Logo-Stufen: S 42 % × 5 Zeilen, M 62 % × 8, L 80 % × 12, XL 94 % × 16 — eingepasst,
724
- nie hochgerechnet. Der Aufdruck (TESTKASSE, STORNOBELEG …) ist ein Rahmen aus
725
- `=`-Zeilen, auf jedem Weg gleich.
726
-
727
- ## Hobex HPS über Kasseneck Connect
728
-
729
- Ein Browser hat weiterhin keine rohen TCP-Sockets — ein **direkter**
730
- Terminal-Kontakt wie beim Flutter-Paket `kasseneck_api` (`HpsClient`) bleibt
731
- deshalb außerhalb der Reichweite dieses Pakets. **Kasseneck Connect** ist aber
732
- ein lokaler Geräte-Agent mit gewöhnlicher HTTP-Schnittstelle, der für die Kasse
733
- mit dem Terminal spricht — und darüber geht es:
819
+ import { rechnungRechnen, positionAusEuro } from '@kreiseck/kasseneck-api/rechnung/rechnen';
734
820
 
735
- ```ts
736
- import { createHpsConnectClient, createHpsPayments } from '@kreiseck/kasseneck-api/payments';
737
-
738
- const client = createHpsConnectClient({ token: kopplungsToken });
739
- const zahlweg = createHpsPayments(client, { host: '192.168.1.50', tid: '3600335' });
821
+ rechnungRechnen(
822
+ [{ unitPriceMicros: 14_790_000, quantityMilli: 1000, vatRateBp: 2000 }],
823
+ { priceMode: 'gross' },
824
+ );
825
+ // { netCents: 1233, vatCents: 246, grossCents: 1479, byRate: [{ rateBp: 2000, … }], lines: […] }
740
826
 
741
- const ergebnis = await zahlweg.pay({ amountCents: 1050 });
742
- // ergebnis.outcome: 'approved' | 'declined' | 'unresolved' — nie geraten.
743
- // ergebnis.transactionId ist IMMER gesetzt, auch bei 'unresolved'.
827
+ positionAusEuro({ unitPrice: 14.79, quantity: 1, vatRate: 20 });
828
+ // { ok: true, position: { unitPriceMicros: 14790000, quantityMilli: 1000, discountBp: 0, vatRateBp: 2000 } }
744
829
  ```
745
830
 
746
- Der Ausgang ist immer einer von drei: `approved`, `declined` (beweisbar nichts
747
- belastet) oder `unresolved` (Ausgang unbekannt, eine Wiederholung könnte ein
748
- zweites Mal belasten). Was das bedeutet und warum es so gebaut ist, steht in
749
- `src/payments/hobex-hps/payments.ts` — dort ist die Dokumentation der Maßstab,
750
- nicht dieses README.
751
-
752
- **Nur `pay` — bewusst kein `refund`/`cancel`.** Kasseneck Connect exponiert
753
- dafür (noch) keinen Endpunkt; eine Gutschrift oder ein Storno am HPS-Terminal
754
- braucht weiterhin die Flutter-App. **myPOS** und **SumUp** bleiben
755
- Android-SDKs ohne Entsprechung hier.
756
-
757
- ## Was hier grundsätzlich nicht dazugehört
758
-
759
- Die Druckeransteuerung selbst (dieses Paket erzeugt die Bytes, es verschickt
760
- sie nicht) und die PDF-Erzeugung. Bilder dekodieren (PNG/JPEG): das Paket
761
- rastert fertige RGBA-Pixel, das Laden des Bilds bleibt bei der Anwendung.
762
-
763
- ## Entwicklung
831
+ `positionAusEuro(item)` converts a euro line (`unitPrice`, `quantity`,
832
+ `vatRate`, `discountPct`) into this form without loss, or names the field and
833
+ the reason when that is not possible. Since 23 September 2026 the server
834
+ calculates every new invoice with this core. In **gross mode** the gross amount
835
+ per rate is the agreed price: net = round(gross × 100 / (100 + rate)),
836
+ VAT = gross − net. In **net mode** the VAT per rate is rounded from the net
837
+ sum. Rounding is commercial (half a cent rounds away from zero). Totals are
838
+ positive for credit notes too; the sign is in the document type
839
+ (`docType: 'GU'`). Test cases: `fixtures/rechnung-rechnen.json`,
840
+ `fixtures/rechnung-rechnen-zufall.json`, `fixtures/position-aus-euro.json`.
841
+
842
+ **`rechnungSummen` still uses the previous formula.** The older helper in
843
+ `…/rechnung` works on `unitPriceCents` lines and does not yet run through the
844
+ core. At half-cent boundaries it can differ from an invoice issued today by
845
+ one cent per VAT rate, and by a few cents across several rates. Example:
846
+ € 21.35 net at 10 % gives € 23.48 gross with `rechnungSummen`, but € 23.49
847
+ with the core and on the invoice. Use `rechnungRechnen` or `previewInvoice` in
848
+ new code. Test cases for the old formula: `fixtures/rechnung-summen.json`.
849
+
850
+ ## Development
764
851
 
765
852
  ```bash
766
- npm test # Testsuite in drei Zeitzonen (Wien, UTC, Kiritimati)
767
- npm run build # ESM- und CJS-Bau nach dist/, inkl. Prüfung der exports
768
- npm run check:consumer # baut den Tarball und übersetzt zwei Verbraucher (CJS/ESM)
769
- npm run check:erreichbar # fragt die öffentliche Adresse: antwortet dort zu jedem Aufruf eine Function?
853
+ npm test # test suite in three time zones (Vienna, UTC, Kiritimati)
854
+ npm run build # ESM and CJS build into dist/, including a check of the exports
855
+ npm run check:consumer # packs the tarball and compiles two consumers (CJS/ESM)
856
+ npm run check:erreichbar # asks the public address: is there a function behind every call?
770
857
  ```
771
858
 
772
- Die drei Zeitzonen sind kein Übereifer: Zeitfehler sind auf einer Wiener
773
- Maschine zufällig richtig. Belegzeiten werden konsequent als **Wiener
774
- Wanduhrzeit** gedeutet (`parseServerTimeStamp`), nie über `new Date(text)`.
859
+ The three time zones are not overkill: time bugs happen to be correct on a
860
+ machine in Vienna.
775
861
 
776
- ### `check:erreichbar` — spricht als einzige mit `api.kasseneck.at`
862
+ ### `check:erreichbar`: the only check that talks to `api.kasseneck.at`
777
863
 
778
- Testsuite und `check:consumer` laufen gegen Attrappen bzw. gegen den Tarball;
779
- keine von beiden setzt je einen Aufruf ab. Fehlt einem Aufruf die
780
- Hosting-Weiterleitung, liefert die veröffentlichte Adresse die
781
- HTML-Auffangseite statt der Function — und das sieht keine Attrappe.
864
+ The test suite and `check:consumer` run against mocks or against the tarball;
865
+ neither ever makes a call. If a call lacks its hosting rewrite, the published
866
+ address returns the HTML fallback page instead of the function, and no mock
867
+ sees that.
782
868
 
783
- Die Prüfung braucht **keine Zugangsdaten**. Ein Aufruf ohne Anmeldung
784
- antwortet, wenn dort eine Function steht, mit
869
+ The check needs **no credentials**. A call without authentication answers,
870
+ when there is a function behind it, with
785
871
  `{"status":"error","message":"Ungültiger Request: Authorization key erwartet."}`.
786
- Genau das ist der Beweis: Der Aufruf wurde angenommen und die Anmeldung
787
- geprüft. Eine HTML-Seite oder ein 404 ist der Beweis, dass dort keine Function
788
- steht. Deshalb prüft das Skript auf ein `status`-Feld und nicht auf Erfolg.
789
-
790
- Bewusst außerhalb von `npm test`: Sie braucht Netz. Ist keines da, sagt sie es
791
- und endet mit 0. Aufrufe, die unter `/v1` absichtlich keine Weiterleitung
792
- haben — der Kassen-Weg über `kasse.kasseneck.at/api`, die Aufrufe mit
793
- ID-Token — stehen mit Grund in `scripts/erreichbarkeit-ausnahmen.json`.
794
- Wird eine Ausnahme erreichbar, schlägt die Prüfung an: Sonst sänke die Zahl nie.
795
-
796
- ## Vertragsdateien für die Zwillinge
797
-
798
- Dieses Paket ist die Quelle für das Dart-Paket `kasseneck_api` und den
799
- Backend-Validator `kasse-settings-core.js`. Drei Dateien in `fixtures/` reisen
800
- im Tarball mit und sagen in Maschinenform, worauf sich beide Seiten geeinigt haben:
801
-
802
- | Datei | Inhalt |
872
+ That is the proof: the call was accepted and authentication was checked. An
873
+ HTML page or a 404 proves that there is no function. That is why the script
874
+ looks for a `status` field and not for success.
875
+
876
+ It is deliberately outside `npm test` because it needs the network. Without a
877
+ network it says so and exits with 0. Calls that deliberately have no rewrite
878
+ under `/v1` (the register path via `kasse.kasseneck.at/api`, the calls with an
879
+ ID token) are listed with a reason in `scripts/erreichbarkeit-ausnahmen.json`.
880
+ If an exception becomes reachable, the check fails; otherwise the list would
881
+ never shrink.
882
+
883
+ ## Contract files for the twin packages
884
+
885
+ This package is the source for the Dart package `kasseneck_api` and for
886
+ validators in the backend. Files in `fixtures/` ship in the tarball and state
887
+ in machine form what both sides agreed on, among them:
888
+
889
+ | File | Contents | Generated by |
890
+ |---|---|---|
891
+ | `kasse-settings-standard.json` | field names and defaults of the register settings | `npm run fixtures:kasse` |
892
+ | `oberflaeche.json` | call names, enum values, permission keys, key actions, partner lists | `npm run fixtures:oberflaeche` |
893
+ | `hobex-hps-codes.json` | measured HPS result codes, their meaning and whether they settle an outcome (the contract behind `isConclusive`) | `npm run fixtures:hobex-hps-codes` |
894
+ | `kasse-texte.json` | the register's message catalogue | `npm run fixtures:texte` |
895
+ | `rechnung-texte.json` | invoice texts in both languages | `npm run fixtures:rechnungstexte` |
896
+ | `rechnung-api.schema.json` | JSON Schema of the invoice API | `npm run fixtures:rechnung` |
897
+
898
+ They are generated and never edited by hand. CI regenerates the register
899
+ settings and `oberflaeche.json` and fails if they differ from the committed
900
+ files; the test suite checks the others against the code.
901
+
902
+ `oberflaeche.json`, `hobex-hps-codes.json`, `kasse-texte.json` and
903
+ `rechnung-texte.json` carry the package version. **After every `npm version`,
904
+ regenerate them and commit them along**, otherwise the tests fail.
905
+
906
+ ### And the other direction
907
+
908
+ The contract in `fixtures/` is checked **over there**: the Dart repository
909
+ pulls it and holds its lists against it. A gap would therefore only show up in
910
+ the next twin run in the other repository, on another day. Against that there
911
+ are two hand-maintained snapshots of the Dart side under `test/fixtures/`,
912
+ each with a `_quelle` field:
913
+
914
+ | File | Checks |
803
915
  |---|---|
804
- | `kasse-settings-standard.json` | Feldnamen und Standardwerte der Kassen-Einstellungen |
805
- | `oberflaeche.json` | Aufrufnamen, Enum-Werte, Rechte-Schlüssel, Tasten-Aktionen |
806
- | `hobex-hps-codes.json` | Gemessene HPS-Ergebniscodes, ihre Bedeutung und ob sie einen Ausgang festschreiben — der Vertrag hinter `.../payments/hobex-hps`s `isConclusive`. |
916
+ | `dart-enums.json` | receipt type, VAT rate, payment method, card provider, voucher, Stripe mode |
917
+ | `dart-partner.json` | environments, error codes (API and portal), webhook events, fields of the webhook envelope including the `test` flag, business fields, retry plan |
807
918
 
808
- | `oberflaeche.json` | Aufrufnamen, Enum-Werte, Rechte-Schlüssel, Tasten-Aktionen, Partner-Listen |
919
+ They make `npm test` fail as soon as a value arrives in only one of the two
920
+ languages.
809
921
 
810
- Alle drei werden erzeugt (`npm run fixtures:kasse`, `npm run fixtures:oberflaeche`,
811
- `npm run fixtures:hobex-hps-codes`) und nie von Hand geändert; die CI prüft
812
- nach jedem Lauf, dass sie zum Code passen.
922
+ ## Glossary
813
923
 
814
- `oberflaeche.json` und `hobex-hps-codes.json` tragen die Paketversion. **Nach
815
- jedem `npm version` müssen deshalb alle drei Dateien neu erzeugt und
816
- mitcommittet werden**, sonst wird die CI rot.
924
+ German terms used in this package, in its identifiers and on the linked pages:
817
925
 
818
- ### Und die Gegenrichtung
819
-
820
- Der Vertrag in `fixtures/` wird **drüben** geprüft: das Dart-Repo zieht ihn und
821
- hält seine Listen dagegen. Eine Lücke fiele hier deshalb erst im nächsten
822
- Zwillingslauf im anderen Repo auf — an einem anderen Tag. Dagegen stehen zwei
823
- von Hand gepflegte Abzüge der Dart-Seite unter `test/fixtures/`, jeder mit
824
- `_quelle`:
825
-
826
- | Datei | prüft |
926
+ | German | English |
827
927
  |---|---|
828
- | `dart-enums.json` | Belegtyp, Steuersatz, Zahlungsart, Kartenanbieter, Gutschein, Stripe-Modus |
829
- | `dart-partner.json` | Umgebungen, Fehlercodes (API und Portal), Webhook-Ereignisse, Felder des Webhook-Umschlags samt der Marke `test`, Betriebsfelder, Wiederholungsplan |
830
-
831
- Sie machen `npm test` rot, sobald ein Wert nur in einer der beiden Sprachen
832
- ankommt.
833
-
834
- ## Lizenz
835
-
836
- Apache-2.0 — siehe `LICENSE` und `NOTICE`.
928
+ | Beleg | receipt |
929
+ | Startbeleg | start receipt |
930
+ | Nullbeleg | zero receipt |
931
+ | Monatsbeleg | monthly receipt |
932
+ | Jahresbeleg | annual receipt |
933
+ | Schlussbeleg | final receipt |
934
+ | Storno | cancellation |
935
+ | Signaturerstellungseinheit | signature creation unit |
936
+ | DEP (Datenerfassungsprotokoll) | data capture log (DEP) |
937
+ | Kassennachschau | cash register audit |
938
+ | Belegerteilungspflicht | obligation to issue receipts |
939
+ | Registrierkasse | fiscal cash register |
940
+ | Umsatzzähler | turnover counter |
941
+ | Außerbetriebnahme | decommissioning |
942
+ | Ausfall der Signatureinheit | signature unit failure |
943
+ | Rechnung | invoice |
944
+ | USt | VAT |
945
+ | FinanzOnline | name of the online portal of the Austrian tax administration |
946
+ | BMF | name of the Austrian Federal Ministry of Finance |
947
+
948
+ ## License
949
+
950
+ Apache-2.0, see `LICENSE` and `NOTICE`.
837
951
 
838
952
  ---
839
953
 
840
- **Kasseneck** ist ein Produkt von
841
- [Kreiseck Software Solutions](https://kreiseck.com) aus Salzburg — Apps,
842
- Kassensysteme und Automatisierungen. Fragen zur Schnittstelle, zu eigenen
843
- Integrationen oder zu einer Partnerschaft:
954
+ **Kasseneck** is a product of
955
+ [Kreiseck Software Solutions](https://kreiseck.com) from Salzburg, Austria:
956
+ apps, point-of-sale systems and automation. Questions about the interface,
957
+ custom integrations or a partnership:
844
958
  [kasseneck.at/kontakt](https://kasseneck.at/kontakt).