@kreiseck/kasseneck-api 0.27.1 → 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/CHANGELOG.md +12 -0
- package/README.de.md +77 -0
- package/README.md +738 -624
- package/fixtures/hobex-hps-codes.json +1 -1
- package/fixtures/kasse-texte.json +1 -1
- package/fixtures/oberflaeche.json +1 -1
- package/fixtures/rechnung-api.schema.json +1 -1
- package/fixtures/rechnung-texte.json +1 -1
- package/package.json +16 -3
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
|
|
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
|
|
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/
|
|
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/
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
[
|
|
63
|
-
[
|
|
64
|
-
|
|
65
|
-
##
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
##
|
|
102
|
+
## What a fiscal cash register in Austria must do
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
|
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` |
|
|
108
|
-
| `registerUserAuth` |
|
|
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
|
-
//
|
|
114
|
-
const
|
|
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
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
const
|
|
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: () =>
|
|
202
|
+
getSessionId: () => sessionHolder.currentId(),
|
|
123
203
|
cashregisterId: 'kasse-1',
|
|
124
204
|
});
|
|
125
205
|
```
|
|
126
206
|
|
|
127
|
-
###
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
138
|
-
const
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
184
|
-
auth: apiKeyAuth({ apiKey: 'kr_live_…', cashregisterToken: 'cb_live_…' }),
|
|
185
|
-
});
|
|
266
|
+
## Errors
|
|
186
267
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
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
|
|
227
|
-
receipt:
|
|
228
|
-
reason: 'fehleingabe', //
|
|
229
|
-
items: [{ index: 0, quantity: 1 }], //
|
|
230
|
-
note: '
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`
|
|
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
|
-
|
|
250
|
-
`
|
|
251
|
-
|
|
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:
|
|
259
|
-
} catch (
|
|
260
|
-
if (isKasseneckApiError(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
**
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
444
|
+
### The receipt sheet: the same receipt everywhere
|
|
313
445
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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:
|
|
335
|
-
|
|
336
|
-
// env: 'test'
|
|
337
|
-
//
|
|
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
|
-
// …
|
|
554
|
+
// … wait for the event customer.fon_verified …
|
|
342
555
|
await partner.requestCustomerSignature(customerId);
|
|
343
|
-
// …
|
|
344
|
-
await partner.createCustomerCashregister({ customerId }); //
|
|
556
|
+
// … wait for signature.ready …
|
|
557
|
+
await partner.createCustomerCashregister({ customerId }); // automatic: true is the default
|
|
345
558
|
```
|
|
346
559
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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 (
|
|
355
|
-
if (istPartnerFehler(
|
|
356
|
-
//
|
|
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
|
-
###
|
|
575
|
+
### A test event is not a cash register
|
|
363
576
|
|
|
364
|
-
`sendPartnerWebhookTest(webhookId, 'cashregister.live')`
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
371
|
-
if (!
|
|
583
|
+
const checked = await parseWebhookEvent({ secret, signatureHeader, body });
|
|
584
|
+
if (!checked.ok) return reply(400);
|
|
372
585
|
|
|
373
|
-
if (
|
|
586
|
+
if (checked.event.test) return reply(200); // test event: do nothing else
|
|
374
587
|
```
|
|
375
588
|
|
|
376
|
-
|
|
589
|
+
Without that line someone tells their customer the register is ready.
|
|
377
590
|
|
|
378
|
-
###
|
|
591
|
+
### Credentials are a third party's secrets
|
|
379
592
|
|
|
380
|
-
`getCustomerCredentials`
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
`string
|
|
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
|
|
599
|
+
const credentials = await partner.getCustomerCredentials(customerId);
|
|
388
600
|
|
|
389
|
-
console.log(
|
|
390
|
-
JSON.stringify(
|
|
391
|
-
`${
|
|
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
|
-
|
|
605
|
+
storeEncrypted(credentials.apiKey.reveal()); // the only way out
|
|
394
606
|
```
|
|
395
607
|
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
###
|
|
611
|
+
### Verifying incoming webhooks
|
|
401
612
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
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
|
|
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
|
|
629
|
+
body: req.body, // Buffer, not req.body after JSON.parse
|
|
419
630
|
});
|
|
420
|
-
if (!
|
|
631
|
+
if (!result.ok) return res.status(400).send(result.reason);
|
|
421
632
|
|
|
422
|
-
//
|
|
423
|
-
//
|
|
633
|
+
// Answer within 10 s, work afterwards. Deliveries can repeat:
|
|
634
|
+
// deduplicate on event.id.
|
|
424
635
|
res.sendStatus(200);
|
|
425
|
-
await
|
|
636
|
+
await handle(result.event);
|
|
426
637
|
});
|
|
427
638
|
```
|
|
428
639
|
|
|
429
|
-
##
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
650
|
+
const invoices = createRechnungApi({ apiKey: process.env.KASSENECK_API_KEY! });
|
|
444
651
|
|
|
445
|
-
// 1.
|
|
446
|
-
let
|
|
652
|
+
// 1. Create the customer once; externalId is your own customer number.
|
|
653
|
+
let customer;
|
|
447
654
|
try {
|
|
448
|
-
|
|
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 (
|
|
454
|
-
if (!istRechnungFehler(
|
|
455
|
-
|
|
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.
|
|
459
|
-
const { invoice, replayed } = await
|
|
460
|
-
idempotencyKey: `
|
|
461
|
-
customerId:
|
|
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: '
|
|
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.
|
|
470
|
-
const pdf = await
|
|
471
|
-
const xml = await
|
|
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.
|
|
474
|
-
await
|
|
475
|
-
idempotencyKey: `
|
|
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: '
|
|
684
|
+
items: [{ description: 'Discount on consulting', quantity: 1, unitPriceCents: 2000, vatRate: 20 }],
|
|
479
685
|
});
|
|
480
686
|
```
|
|
481
687
|
|
|
482
|
-
**
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
`
|
|
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
|
|
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
|
-
**
|
|
493
|
-
|
|
494
|
-
(`customer.language`),
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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 [
|
|
501
|
-
const { invoice } = await
|
|
502
|
-
idempotencyKey: `
|
|
503
|
-
customerId:
|
|
504
|
-
|
|
505
|
-
language: 'en',
|
|
506
|
-
brandId:
|
|
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
|
-
//
|
|
510
|
-
const
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
**
|
|
521
|
-
`INVOICE_UNITS` (`piece`, `hour`, `day`, `flat_rate`, `kilogram`,
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
`RECHNUNG_EINHEITEN_CODES` (`C62`, `HUR`, …).
|
|
525
|
-
`validation`
|
|
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
|
-
**
|
|
528
|
-
|
|
529
|
-
|
|
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
|
|
533
|
-
idempotencyKey: `
|
|
534
|
-
customerId:
|
|
535
|
-
|
|
536
|
-
items: [{ description: '
|
|
537
|
-
payment: { method: 'card', reference:
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
//
|
|
541
|
-
await
|
|
542
|
-
idempotencyKey: `
|
|
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`
|
|
548
|
-
|
|
758
|
+
`reference` is stored but **not printed**; card data does not belong on an
|
|
759
|
+
invoice anyway.
|
|
549
760
|
|
|
550
|
-
**
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
(`
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
`
|
|
698
|
-
|
|
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 {
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
742
|
-
//
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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 #
|
|
767
|
-
npm run build # ESM
|
|
768
|
-
npm run check:consumer #
|
|
769
|
-
npm run check:erreichbar #
|
|
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
|
-
|
|
773
|
-
|
|
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
|
|
862
|
+
### `check:erreichbar`: the only check that talks to `api.kasseneck.at`
|
|
777
863
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
784
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
ID
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
| `
|
|
805
|
-
| `
|
|
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
|
-
|
|
919
|
+
They make `npm test` fail as soon as a value arrives in only one of the two
|
|
920
|
+
languages.
|
|
809
921
|
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
829
|
-
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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**
|
|
841
|
-
[Kreiseck Software Solutions](https://kreiseck.com)
|
|
842
|
-
|
|
843
|
-
|
|
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).
|