@kreiseck/kasseneck-api 0.6.46 → 0.7.0
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 +154 -0
- package/README.md +158 -2
- package/dist/cjs/client/aufrufe.d.ts +1 -1
- package/dist/cjs/client/aufrufe.js +19 -0
- package/dist/cjs/client/errors.d.ts +31 -1
- package/dist/cjs/client/errors.js +98 -1
- package/dist/cjs/client/transport.js +7 -7
- package/dist/cjs/partner/ablauf.d.ts +47 -0
- package/dist/cjs/partner/ablauf.js +99 -0
- package/dist/cjs/partner/api.d.ts +68 -0
- package/dist/cjs/partner/api.js +44 -0
- package/dist/cjs/partner/auth.d.ts +33 -0
- package/dist/cjs/partner/auth.js +52 -0
- package/dist/cjs/partner/betrieb.d.ts +48 -0
- package/dist/cjs/partner/betrieb.js +136 -0
- package/dist/cjs/partner/endpunkte.d.ts +142 -0
- package/dist/cjs/partner/endpunkte.js +488 -0
- package/dist/cjs/partner/fehler.d.ts +72 -0
- package/dist/cjs/partner/fehler.js +199 -0
- package/dist/cjs/partner/index.d.ts +28 -0
- package/dist/cjs/partner/index.js +84 -0
- package/dist/cjs/partner/secret.d.ts +66 -0
- package/dist/cjs/partner/secret.js +95 -0
- package/dist/cjs/partner/typen.d.ts +399 -0
- package/dist/cjs/partner/typen.js +33 -0
- package/dist/cjs/partner/webhook-signatur.d.ts +95 -0
- package/dist/cjs/partner/webhook-signatur.js +158 -0
- package/dist/cjs/partner/webhooks.d.ts +211 -0
- package/dist/cjs/partner/webhooks.js +269 -0
- package/dist/esm/client/aufrufe.d.ts +1 -1
- package/dist/esm/client/aufrufe.js +19 -0
- package/dist/esm/client/errors.d.ts +31 -1
- package/dist/esm/client/errors.js +97 -1
- package/dist/esm/client/transport.js +8 -8
- package/dist/esm/partner/ablauf.d.ts +47 -0
- package/dist/esm/partner/ablauf.js +95 -0
- package/dist/esm/partner/api.d.ts +68 -0
- package/dist/esm/partner/api.js +41 -0
- package/dist/esm/partner/auth.d.ts +33 -0
- package/dist/esm/partner/auth.js +48 -0
- package/dist/esm/partner/betrieb.d.ts +48 -0
- package/dist/esm/partner/betrieb.js +132 -0
- package/dist/esm/partner/endpunkte.d.ts +142 -0
- package/dist/esm/partner/endpunkte.js +474 -0
- package/dist/esm/partner/fehler.d.ts +72 -0
- package/dist/esm/partner/fehler.js +189 -0
- package/dist/esm/partner/index.d.ts +28 -0
- package/dist/esm/partner/index.js +27 -0
- package/dist/esm/partner/secret.d.ts +66 -0
- package/dist/esm/partner/secret.js +90 -0
- package/dist/esm/partner/typen.d.ts +399 -0
- package/dist/esm/partner/typen.js +30 -0
- package/dist/esm/partner/webhook-signatur.d.ts +95 -0
- package/dist/esm/partner/webhook-signatur.js +153 -0
- package/dist/esm/partner/webhooks.d.ts +211 -0
- package/dist/esm/partner/webhooks.js +257 -0
- package/fixtures/oberflaeche.json +131 -3
- package/package.json +13 -1
|
@@ -85,6 +85,84 @@ function unbedenklich(wert, geheimnisse) {
|
|
|
85
85
|
}
|
|
86
86
|
return wert;
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Grenzen fuer die gesiebte Fehler-Nutzlast (siehe [fehlerDetails]). Sie stehen
|
|
90
|
+
* als benannte Konstanten hier, weil ein Test sie namentlich prueft — eine
|
|
91
|
+
* spaeter heraufgesetzte Grenze soll auffallen und nicht als Zahl im Code
|
|
92
|
+
* untergehen.
|
|
93
|
+
*/
|
|
94
|
+
const DETAIL_TIEFE = 4;
|
|
95
|
+
const DETAIL_EINTRAEGE = 50;
|
|
96
|
+
const DETAIL_TEXT_MAX = 300;
|
|
97
|
+
/** Schluessel eines Detail-Objekts: bezeichner-foermig, sonst faellt der Eintrag weg. */
|
|
98
|
+
const DETAIL_SCHLUESSEL = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
99
|
+
/**
|
|
100
|
+
* Siebt das `data` einer Fehlerantwort zu einer Form, die man gefahrlos an
|
|
101
|
+
* einem Fehler mitfuehren kann.
|
|
102
|
+
*
|
|
103
|
+
* **Warum ueberhaupt:** die Partner-API legt ihre Entscheidung nicht in den
|
|
104
|
+
* Text, sondern in `data.code` (`vertrag_offen`, `signature_not_ready`,
|
|
105
|
+
* `activation_failed` samt `data.schritt`). Ohne diese Felder muesste ein
|
|
106
|
+
* Aufrufer die deutsche `message` nach Zeichenketten durchsuchen — genau die
|
|
107
|
+
* Kopplung, die beim naechsten Formulierungsschliff still bricht.
|
|
108
|
+
*
|
|
109
|
+
* **Warum gesiebt und nicht durchgereicht:** siehe Modulkommentar. Der Rumpf
|
|
110
|
+
* kommt ueber fremde Proxys, und ein Fehler landet in Protokollen. Deshalb
|
|
111
|
+
* ueberlebt nur, was flach, klein und bezeichner-foermig benannt ist — und
|
|
112
|
+
* kein Wert, der mit einem der gesendeten Geheimnisse ueberlappt. Damit gilt
|
|
113
|
+
* hier dieselbe Zusage wie fuer [causeDigest], und `geheimnisse` hat aus
|
|
114
|
+
* demselben Grund **keinen** Vorgabewert.
|
|
115
|
+
*/
|
|
116
|
+
export function fehlerDetails(daten, geheimnisse) {
|
|
117
|
+
const gesiebt = sieben(daten, geheimnisse, 0);
|
|
118
|
+
return gesiebt !== null && typeof gesiebt === 'object' && !Array.isArray(gesiebt)
|
|
119
|
+
? gesiebt
|
|
120
|
+
: {};
|
|
121
|
+
}
|
|
122
|
+
function sieben(wert, geheimnisse, tiefe) {
|
|
123
|
+
if (wert === null || typeof wert === 'boolean')
|
|
124
|
+
return wert;
|
|
125
|
+
// Nur endliche Zahlen: NaN und Infinity ueberstehen JSON.stringify nicht und
|
|
126
|
+
// staenden in einem Fehlerbericht als `null` ohne jede Aussage.
|
|
127
|
+
if (typeof wert === 'number')
|
|
128
|
+
return Number.isFinite(wert) ? wert : undefined;
|
|
129
|
+
if (typeof wert === 'string') {
|
|
130
|
+
if (wert.length > DETAIL_TEXT_MAX)
|
|
131
|
+
return undefined;
|
|
132
|
+
for (const geheim of geheimnisse) {
|
|
133
|
+
if (geheim && (geheim.includes(wert) || wert.includes(geheim)))
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
return wert;
|
|
137
|
+
}
|
|
138
|
+
if (tiefe >= DETAIL_TIEFE)
|
|
139
|
+
return undefined;
|
|
140
|
+
if (Array.isArray(wert)) {
|
|
141
|
+
const liste = [];
|
|
142
|
+
for (const eintrag of wert.slice(0, DETAIL_EINTRAEGE)) {
|
|
143
|
+
const s = sieben(eintrag, geheimnisse, tiefe + 1);
|
|
144
|
+
if (s !== undefined)
|
|
145
|
+
liste.push(s);
|
|
146
|
+
}
|
|
147
|
+
return liste;
|
|
148
|
+
}
|
|
149
|
+
if (typeof wert !== 'object')
|
|
150
|
+
return undefined;
|
|
151
|
+
const raus = {};
|
|
152
|
+
let gezaehlt = 0;
|
|
153
|
+
for (const [schluessel, eintrag] of Object.entries(wert)) {
|
|
154
|
+
if (gezaehlt >= DETAIL_EINTRAEGE)
|
|
155
|
+
break;
|
|
156
|
+
if (!DETAIL_SCHLUESSEL.test(schluessel))
|
|
157
|
+
continue;
|
|
158
|
+
const s = sieben(eintrag, geheimnisse, tiefe + 1);
|
|
159
|
+
if (s === undefined)
|
|
160
|
+
continue;
|
|
161
|
+
raus[schluessel] = s;
|
|
162
|
+
gezaehlt += 1;
|
|
163
|
+
}
|
|
164
|
+
return raus;
|
|
165
|
+
}
|
|
88
166
|
/** Fachlicher Fehler: HTTP 200, aber `status: 'error'` im Rumpf. */
|
|
89
167
|
export class KasseneckApiError extends Error {
|
|
90
168
|
name = 'KasseneckApiError';
|
|
@@ -92,10 +170,28 @@ export class KasseneckApiError extends Error {
|
|
|
92
170
|
functionName;
|
|
93
171
|
/** Meldung des Backends, unveraendert (`message` aus der Huelle). */
|
|
94
172
|
serverMessage;
|
|
95
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Maschinenlesbarer Fehlercode aus `data.code`, sofern die Antwort einen
|
|
175
|
+
* fuehrt (`vertrag_offen`, `rate_limited`, …). Die aelteren Endpunkte des
|
|
176
|
+
* Backends antworten ohne Code — dann `undefined`.
|
|
177
|
+
*/
|
|
178
|
+
code;
|
|
179
|
+
/**
|
|
180
|
+
* Die uebrige Fehler-Nutzlast, gesiebt (siehe [fehlerDetails]): `schritt`,
|
|
181
|
+
* `rc`, `retryAfterSec`, `errors[]` und was der jeweilige Endpunkt sonst
|
|
182
|
+
* beilegt. Immer ein Objekt, notfalls ein leeres.
|
|
183
|
+
*/
|
|
184
|
+
details;
|
|
185
|
+
constructor(functionName, serverMessage, details = {}) {
|
|
96
186
|
super(`${functionName} fehlgeschlagen: ${serverMessage}`);
|
|
97
187
|
this.functionName = functionName;
|
|
98
188
|
this.serverMessage = serverMessage;
|
|
189
|
+
this.details = details;
|
|
190
|
+
// Der Code steht in `details` und zusaetzlich als eigenes Feld: das eine
|
|
191
|
+
// ist die Nutzlast, das andere die Frage, die ein Aufrufer wirklich
|
|
192
|
+
// stellt. Nur Bezeichner gelten — ein Freitext waere kein Code.
|
|
193
|
+
const code = details['code'];
|
|
194
|
+
this.code = typeof code === 'string' && BEZEICHNER.test(code) ? code : undefined;
|
|
99
195
|
}
|
|
100
196
|
}
|
|
101
197
|
const GRUND_TEXT = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KasseneckApiError, KasseneckAuthError, KasseneckHttpError, KasseneckNetworkError, KasseneckValidationError, causeDigest, } from './errors.js';
|
|
1
|
+
import { KasseneckApiError, KasseneckAuthError, KasseneckHttpError, KasseneckNetworkError, KasseneckValidationError, causeDigest, fehlerDetails, } from './errors.js';
|
|
2
2
|
/**
|
|
3
3
|
* Transport zum Kasseneck-Backend: ein Aufruf ist ein POST an
|
|
4
4
|
* `<basis>/<funktionsname>` mit dem JSON-Rumpf `{ "params": { … } }` — wie im
|
|
@@ -129,7 +129,7 @@ function createCore(options) {
|
|
|
129
129
|
if (antwort.status !== 200) {
|
|
130
130
|
throw new KasseneckHttpError(fehlerName, antwort.status, inhaltstyp, 'server-error');
|
|
131
131
|
}
|
|
132
|
-
return auswerten(koerper, fehlerName, antwort.status, inhaltstyp);
|
|
132
|
+
return auswerten(koerper, fehlerName, antwort.status, inhaltstyp, geheimnisse);
|
|
133
133
|
}
|
|
134
134
|
finally {
|
|
135
135
|
// Ohne Abraeumen haelt der Wecker den Node-Prozess bis zum Zeitlimit wach.
|
|
@@ -154,7 +154,7 @@ const alsBytes = async (antwort, functionName) => {
|
|
|
154
154
|
return new Uint8Array(await antwort.arrayBuffer());
|
|
155
155
|
};
|
|
156
156
|
/** Auswertung des JSON-Wegs: Huelle aufloesen, Nutzlast zurueckgeben. */
|
|
157
|
-
function jsonAuswerten(text, functionName, statusCode, inhaltstyp) {
|
|
157
|
+
function jsonAuswerten(text, functionName, statusCode, inhaltstyp, geheimnisse) {
|
|
158
158
|
if (!text.trim()) {
|
|
159
159
|
throw new KasseneckHttpError(functionName, statusCode, inhaltstyp, 'empty-body');
|
|
160
160
|
}
|
|
@@ -176,7 +176,7 @@ function jsonAuswerten(text, functionName, statusCode, inhaltstyp) {
|
|
|
176
176
|
}
|
|
177
177
|
// Alles, was nicht ausdruecklich Erfolg ist, gilt als fachlicher Fehler —
|
|
178
178
|
// ein unbekannter Statuswert darf nie stillschweigend als Erfolg durchgehen.
|
|
179
|
-
throw fachfehler(functionName, huelle.message);
|
|
179
|
+
throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse);
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
182
182
|
* Auswertung des Binaerwegs. Der Kern der Zusage: **ein Aufrufer bekommt nie
|
|
@@ -191,7 +191,7 @@ function jsonAuswerten(text, functionName, statusCode, inhaltstyp) {
|
|
|
191
191
|
* das Backend hier erzeugt (pdf-lib). Alles, was nicht so anfaengt, ist kein
|
|
192
192
|
* Bericht — und wird dann daraufhin angesehen, ob es die Fehlerhuelle ist.
|
|
193
193
|
*/
|
|
194
|
-
function pdfAuswerten(bytes, functionName, statusCode, inhaltstyp) {
|
|
194
|
+
function pdfAuswerten(bytes, functionName, statusCode, inhaltstyp, geheimnisse) {
|
|
195
195
|
if (bytes.length === 0) {
|
|
196
196
|
throw new KasseneckHttpError(functionName, statusCode, inhaltstyp, 'empty-body');
|
|
197
197
|
}
|
|
@@ -222,7 +222,7 @@ function pdfAuswerten(bytes, functionName, statusCode, inhaltstyp) {
|
|
|
222
222
|
}
|
|
223
223
|
// Derselbe fachliche Fehler wie auf dem JSON-Weg — fuer den Aufrufer macht
|
|
224
224
|
// es keinen Unterschied, ob er ein PDF oder eine Nutzlast erwartet hat.
|
|
225
|
-
throw fachfehler(functionName, huelle.message);
|
|
225
|
+
throw fachfehler(functionName, huelle.message, huelle.data, geheimnisse);
|
|
226
226
|
}
|
|
227
227
|
/** `%PDF` am Anfang — die Kennung jeder PDF-Datei. */
|
|
228
228
|
function istPdf(bytes) {
|
|
@@ -235,9 +235,9 @@ function alsHuelle(wert) {
|
|
|
235
235
|
}
|
|
236
236
|
return wert;
|
|
237
237
|
}
|
|
238
|
-
function fachfehler(functionName, message) {
|
|
238
|
+
function fachfehler(functionName, message, daten, geheimnisse) {
|
|
239
239
|
const meldung = typeof message === 'string' && message.trim() ? message : 'Unbekannter Fehler';
|
|
240
|
-
return new KasseneckApiError(functionName, meldung);
|
|
240
|
+
return new KasseneckApiError(functionName, meldung, fehlerDetails(daten, geheimnisse));
|
|
241
241
|
}
|
|
242
242
|
/**
|
|
243
243
|
* Nutzlast aus Auth- und Aufruferparametern. Auth-Parameter bilden die
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Der Weg vom Vertragsabschluss bis zum ersten Beleg — als Daten, nicht als
|
|
3
|
+
* Fliesstext.
|
|
4
|
+
*
|
|
5
|
+
* **Warum das im Paket steht und nicht nur in der Doku:** die Kette hat eine
|
|
6
|
+
* harte Reihenfolge, und jeder Schritt scheitert mit einem eigenen Code, wenn
|
|
7
|
+
* ein vorheriger fehlt (`fon_missing`, `signature_missing`,
|
|
8
|
+
* `signature_not_ready`).
|
|
9
|
+
* Wer die Reihenfolge nur aus einer Fehlermeldung lernt, lernt sie einmal je
|
|
10
|
+
* Fehler. Hier steht sie vorher — abfragbar, ausgebbar, und in
|
|
11
|
+
* [naechsterSchritt] auch beantwortbar.
|
|
12
|
+
*
|
|
13
|
+
* **Ohne Vertragsschritt.** Auftragsverarbeitungsvertraege wirken in diesem
|
|
14
|
+
* Weg nicht mehr (Stand 2026-08-31): kein Aufruf meldet einen, keine Kasse
|
|
15
|
+
* bleibt deswegen stehen.
|
|
16
|
+
*
|
|
17
|
+
* Zwei Dinge laufen bewusst **parallel**: der Signaturantrag und das Anlegen
|
|
18
|
+
* der Kasse. Eine mit `automatic:true` angelegte Kasse wartet, bis die
|
|
19
|
+
* Signatur bereit ist, und geht dann von selbst live. Der Kundenstatus nennt
|
|
20
|
+
* darum immer nur den **weitesten erreichten** Meilenstein, nicht die einzige
|
|
21
|
+
* laufende Arbeit.
|
|
22
|
+
*/
|
|
23
|
+
import type { KundenStatus } from './typen.js';
|
|
24
|
+
/** Ein Schritt der Kette. */
|
|
25
|
+
export interface AblaufSchritt {
|
|
26
|
+
key: string;
|
|
27
|
+
/** Was in diesem Schritt passiert. */
|
|
28
|
+
text: string;
|
|
29
|
+
/** Der Aufruf, der ihn ausloest — `null`, wenn hier nur gewartet wird. */
|
|
30
|
+
aufruf: string | null;
|
|
31
|
+
/** Das Ereignis, das seinen Abschluss meldet — `null`, wenn es sofort feststeht. */
|
|
32
|
+
wartetAuf: string | null;
|
|
33
|
+
/** Der Fehlercode, mit dem ein spaeterer Aufruf sich beschwert, wenn dieser Schritt fehlt. */
|
|
34
|
+
fehltCode: string | null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Die Kette in ihrer Reihenfolge. Sie beschreibt den **Live-Weg**; mit einem
|
|
38
|
+
* `pk_test_`-Schluessel entfallen die FinanzOnline-Schritte, und die Signatur
|
|
39
|
+
* ist sofort bereit (AT100-Testkarte — die damit erzeugten Belege sind keine
|
|
40
|
+
* gueltigen RKSV-Belege).
|
|
41
|
+
*/
|
|
42
|
+
export declare const PARTNER_ABLAUF: readonly AblaufSchritt[];
|
|
43
|
+
/**
|
|
44
|
+
* Der Schritt, an dem ein Betrieb mit diesem Status steht — `null` fuer
|
|
45
|
+
* `gesperrt` und fuer jeden Status, den dieses Paket nicht kennt.
|
|
46
|
+
*/
|
|
47
|
+
export declare function naechsterSchritt(status: KundenStatus): AblaufSchritt | null;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Der Weg vom Vertragsabschluss bis zum ersten Beleg — als Daten, nicht als
|
|
3
|
+
* Fliesstext.
|
|
4
|
+
*
|
|
5
|
+
* **Warum das im Paket steht und nicht nur in der Doku:** die Kette hat eine
|
|
6
|
+
* harte Reihenfolge, und jeder Schritt scheitert mit einem eigenen Code, wenn
|
|
7
|
+
* ein vorheriger fehlt (`fon_missing`, `signature_missing`,
|
|
8
|
+
* `signature_not_ready`).
|
|
9
|
+
* Wer die Reihenfolge nur aus einer Fehlermeldung lernt, lernt sie einmal je
|
|
10
|
+
* Fehler. Hier steht sie vorher — abfragbar, ausgebbar, und in
|
|
11
|
+
* [naechsterSchritt] auch beantwortbar.
|
|
12
|
+
*
|
|
13
|
+
* **Ohne Vertragsschritt.** Auftragsverarbeitungsvertraege wirken in diesem
|
|
14
|
+
* Weg nicht mehr (Stand 2026-08-31): kein Aufruf meldet einen, keine Kasse
|
|
15
|
+
* bleibt deswegen stehen.
|
|
16
|
+
*
|
|
17
|
+
* Zwei Dinge laufen bewusst **parallel**: der Signaturantrag und das Anlegen
|
|
18
|
+
* der Kasse. Eine mit `automatic:true` angelegte Kasse wartet, bis die
|
|
19
|
+
* Signatur bereit ist, und geht dann von selbst live. Der Kundenstatus nennt
|
|
20
|
+
* darum immer nur den **weitesten erreichten** Meilenstein, nicht die einzige
|
|
21
|
+
* laufende Arbeit.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Die Kette in ihrer Reihenfolge. Sie beschreibt den **Live-Weg**; mit einem
|
|
25
|
+
* `pk_test_`-Schluessel entfallen die FinanzOnline-Schritte, und die Signatur
|
|
26
|
+
* ist sofort bereit (AT100-Testkarte — die damit erzeugten Belege sind keine
|
|
27
|
+
* gueltigen RKSV-Belege).
|
|
28
|
+
*/
|
|
29
|
+
export const PARTNER_ABLAUF = [
|
|
30
|
+
{
|
|
31
|
+
key: 'business',
|
|
32
|
+
text: 'Betrieb anlegen (idempotencyKey = eigene Kundennummer).',
|
|
33
|
+
aufruf: 'createPartnerCustomer',
|
|
34
|
+
wartetAuf: 'customer.created',
|
|
35
|
+
fehltCode: null,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'fon',
|
|
39
|
+
text: 'FinanzOnline einrichten: Link an den Betrieb, der Betrieb traegt seinen Zugang ein.',
|
|
40
|
+
aufruf: 'sendPartnerCustomerFonLink',
|
|
41
|
+
wartetAuf: 'customer.fon_verified',
|
|
42
|
+
fehltCode: 'fon_missing',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'signature',
|
|
46
|
+
text: 'Signatureinheit beantragen. Kasseneck weist eine Karte zu und meldet sie bei FinanzOnline an.',
|
|
47
|
+
aufruf: 'requestCustomerSignature',
|
|
48
|
+
wartetAuf: 'signature.ready',
|
|
49
|
+
fehltCode: 'signature_missing',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'cashregister',
|
|
53
|
+
text: 'Kasse anlegen. Mit automatic:true (Vorgabe) geht sie von selbst live, sobald die Signatur bereit ist — ' +
|
|
54
|
+
'sie darf deshalb schon vorher angelegt werden.',
|
|
55
|
+
aufruf: 'createCustomerCashregister',
|
|
56
|
+
wartetAuf: 'cashregister.live',
|
|
57
|
+
fehltCode: 'cashregister_not_found',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'zugangsdaten',
|
|
61
|
+
text: 'Zugangsdaten des Betriebs holen (Scope credentials:read). Geheimnisse — nur verschluesselt speichern.',
|
|
62
|
+
aufruf: 'getCustomerCredentials',
|
|
63
|
+
wartetAuf: null,
|
|
64
|
+
fehltCode: null,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: 'belege',
|
|
68
|
+
text: 'Belege signieren: Bearer = apiKey des Betriebs, Kopfzeile cashregister-token = Token der Kasse.',
|
|
69
|
+
aufruf: 'createReceipt',
|
|
70
|
+
wartetAuf: null,
|
|
71
|
+
fehltCode: null,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* Welcher Meilenstein einem Kundenstatus entspricht. Die Zuordnung ist
|
|
76
|
+
* absichtlich grob: der Status nennt den weitesten erreichten Punkt, nicht die
|
|
77
|
+
* laufende Arbeit — fuer den genauen Verlauf sind die Ereignisse `signature.*`
|
|
78
|
+
* und `cashregister.*` da.
|
|
79
|
+
*/
|
|
80
|
+
const STATUS_SCHRITT = {
|
|
81
|
+
created: 'fon',
|
|
82
|
+
fon_configured: 'signature',
|
|
83
|
+
signature_requested: 'signature',
|
|
84
|
+
signature_ready: 'cashregister',
|
|
85
|
+
cashregister_created: 'cashregister',
|
|
86
|
+
live: 'zugangsdaten',
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Der Schritt, an dem ein Betrieb mit diesem Status steht — `null` fuer
|
|
90
|
+
* `gesperrt` und fuer jeden Status, den dieses Paket nicht kennt.
|
|
91
|
+
*/
|
|
92
|
+
export function naechsterSchritt(status) {
|
|
93
|
+
const key = STATUS_SCHRITT[status];
|
|
94
|
+
return key ? PARTNER_ABLAUF.find((s) => s.key === key) ?? null : null;
|
|
95
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fassade ueber den Partner-Aufrufen: Schluessel einmal binden, dann rufen.
|
|
3
|
+
*
|
|
4
|
+
* Wie [createKasseneckApi] bewusst **keine** Klasse — die Aufrufe sind freie
|
|
5
|
+
* Funktionen und bleiben einzeln importierbar.
|
|
6
|
+
*/
|
|
7
|
+
import { type FetchLike } from '../client/transport.js';
|
|
8
|
+
import { type CreateWebhookOptions, type CreateWebhookResult, type PartnerWebhook, type WebhookListe, type PartnerWebhookEventType, type WebhookPatch, type WebhookTestResult, type WebhookZustellung } from './webhooks.js';
|
|
9
|
+
import type { ActivateCashregisterResult, CreateCashregisterOptions, CreateCashregisterResult, CreateCustomerOptions, CreateCustomerResult, CustomerCredentials, FonLinkResult, KassenListe, Kunde, KundenListe, ListCustomersOptions, PartnerInfo, RequestSignatureResult, SignaturStand } from './typen.js';
|
|
10
|
+
export interface PartnerApiOptions {
|
|
11
|
+
/** Partner-Schluessel `pk_test_…` / `pk_live_…`. Gehoert auf einen Server. */
|
|
12
|
+
partnerKey: string;
|
|
13
|
+
/** Abweichende Basis-URL; Vorgabe `https://api.kasseneck.at/v1`. */
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
/** Zeitlimit je Aufruf in Millisekunden. */
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
/** Eigene `fetch`-Umsetzung (Tests, Proxys). */
|
|
18
|
+
fetch?: FetchLike;
|
|
19
|
+
}
|
|
20
|
+
export interface PartnerApi {
|
|
21
|
+
getPartnerInfo(): Promise<PartnerInfo>;
|
|
22
|
+
createPartnerCustomer(optionen: CreateCustomerOptions): Promise<CreateCustomerResult>;
|
|
23
|
+
listPartnerCustomers(optionen?: ListCustomersOptions): Promise<KundenListe>;
|
|
24
|
+
getPartnerCustomer(customerId: string): Promise<Kunde>;
|
|
25
|
+
sendPartnerCustomerFonLink(customerId: string): Promise<FonLinkResult>;
|
|
26
|
+
requestCustomerSignature(customerId: string, optionen?: {
|
|
27
|
+
art?: string;
|
|
28
|
+
additional?: boolean;
|
|
29
|
+
}): Promise<RequestSignatureResult>;
|
|
30
|
+
getCustomerSignatureStatus(customerId: string): Promise<SignaturStand>;
|
|
31
|
+
createCustomerCashregister(optionen: CreateCashregisterOptions): Promise<CreateCashregisterResult>;
|
|
32
|
+
activateCashregister(customerId: string, cashregisterId: string): Promise<ActivateCashregisterResult>;
|
|
33
|
+
listCustomerCashregisters(customerId: string): Promise<KassenListe>;
|
|
34
|
+
/** Geheimnisse des Betriebs — siehe `getCustomerCredentials` in endpunkte.ts. */
|
|
35
|
+
getCustomerCredentials(customerId: string): Promise<CustomerCredentials>;
|
|
36
|
+
/**
|
|
37
|
+
* Ist diese Adresse als Kasseneck-Zugang noch frei? Nur noetig, wenn der
|
|
38
|
+
* Betrieb einen eigenen Login bekommen soll — sonst faellt `email_taken`
|
|
39
|
+
* erst nach dem ganzen Formular auf.
|
|
40
|
+
*/
|
|
41
|
+
checkPartnerCustomerEmail(email: string): Promise<boolean>;
|
|
42
|
+
createPartnerWebhook(optionen: CreateWebhookOptions): Promise<CreateWebhookResult>;
|
|
43
|
+
listPartnerWebhooks(): Promise<WebhookListe>;
|
|
44
|
+
updatePartnerWebhook(webhookId: string, patch: WebhookPatch): Promise<PartnerWebhook>;
|
|
45
|
+
deletePartnerWebhook(webhookId: string): Promise<string>;
|
|
46
|
+
/**
|
|
47
|
+
* Neues Secret fuer denselben Endpunkt — dieselbe `webhookId`, dieselben
|
|
48
|
+
* Ereignisse. Das alte gilt ab der Antwort nicht mehr.
|
|
49
|
+
*/
|
|
50
|
+
rotatePartnerWebhookSecret(webhookId: string): Promise<CreateWebhookResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Eine Probe an einen Endpunkt — ohne `event` die Leitungsprobe
|
|
53
|
+
* `webhook.test`, mit `event` genau der Fall, den der Empfaenger behandeln
|
|
54
|
+
* soll. Jede Probe traegt `test: true` im Umschlag.
|
|
55
|
+
*/
|
|
56
|
+
sendPartnerWebhookTest(webhookId: string, event?: PartnerWebhookEventType | (string & {})): Promise<WebhookTestResult>;
|
|
57
|
+
listPartnerWebhookDeliveries(optionen?: {
|
|
58
|
+
webhookId?: string;
|
|
59
|
+
limit?: number;
|
|
60
|
+
}): Promise<WebhookZustellung[]>;
|
|
61
|
+
/**
|
|
62
|
+
* Der Handlungssatz zu einem beliebigen Fehlercode der Partner-API. Gehoert
|
|
63
|
+
* in die eigene Fehlermeldung, damit ein Anwender nicht in der Doku
|
|
64
|
+
* nachschlagen muss.
|
|
65
|
+
*/
|
|
66
|
+
fehlerRat(code: string): string | undefined;
|
|
67
|
+
}
|
|
68
|
+
export declare function createPartnerApi(optionen: PartnerApiOptions): PartnerApi;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fassade ueber den Partner-Aufrufen: Schluessel einmal binden, dann rufen.
|
|
3
|
+
*
|
|
4
|
+
* Wie [createKasseneckApi] bewusst **keine** Klasse — die Aufrufe sind freie
|
|
5
|
+
* Funktionen und bleiben einzeln importierbar.
|
|
6
|
+
*/
|
|
7
|
+
import { createTransport } from '../client/transport.js';
|
|
8
|
+
import { partnerKeyAuth } from './auth.js';
|
|
9
|
+
import { partnerFehlerRat } from './fehler.js';
|
|
10
|
+
import { activateCashregister, createCustomerCashregister, createPartnerCustomer, getCustomerCredentials, getCustomerSignatureStatus, getPartnerCustomer, getPartnerInfo, checkPartnerCustomerEmail, listCustomerCashregisters, listPartnerCustomers, requestCustomerSignature, sendPartnerCustomerFonLink, } from './endpunkte.js';
|
|
11
|
+
import { createPartnerWebhook, deletePartnerWebhook, listPartnerWebhookDeliveries, listPartnerWebhooks, rotatePartnerWebhookSecret, sendPartnerWebhookTest, updatePartnerWebhook, } from './webhooks.js';
|
|
12
|
+
export function createPartnerApi(optionen) {
|
|
13
|
+
const rufen = createTransport({
|
|
14
|
+
auth: partnerKeyAuth({ partnerKey: optionen.partnerKey }),
|
|
15
|
+
baseUrl: optionen.baseUrl,
|
|
16
|
+
timeoutMs: optionen.timeoutMs,
|
|
17
|
+
fetch: optionen.fetch,
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
getPartnerInfo: () => getPartnerInfo(rufen),
|
|
21
|
+
createPartnerCustomer: (o) => createPartnerCustomer(rufen, o),
|
|
22
|
+
listPartnerCustomers: (o) => listPartnerCustomers(rufen, o),
|
|
23
|
+
getPartnerCustomer: (id) => getPartnerCustomer(rufen, id),
|
|
24
|
+
sendPartnerCustomerFonLink: (id) => sendPartnerCustomerFonLink(rufen, id),
|
|
25
|
+
requestCustomerSignature: (id, o) => requestCustomerSignature(rufen, id, o),
|
|
26
|
+
getCustomerSignatureStatus: (id) => getCustomerSignatureStatus(rufen, id),
|
|
27
|
+
createCustomerCashregister: (o) => createCustomerCashregister(rufen, o),
|
|
28
|
+
activateCashregister: (kunde, kasse) => activateCashregister(rufen, kunde, kasse),
|
|
29
|
+
listCustomerCashregisters: (id) => listCustomerCashregisters(rufen, id),
|
|
30
|
+
getCustomerCredentials: (id) => getCustomerCredentials(rufen, id),
|
|
31
|
+
checkPartnerCustomerEmail: (email) => checkPartnerCustomerEmail(rufen, email),
|
|
32
|
+
createPartnerWebhook: (o) => createPartnerWebhook(rufen, o),
|
|
33
|
+
listPartnerWebhooks: () => listPartnerWebhooks(rufen),
|
|
34
|
+
updatePartnerWebhook: (id, patch) => updatePartnerWebhook(rufen, id, patch),
|
|
35
|
+
deletePartnerWebhook: (id) => deletePartnerWebhook(rufen, id),
|
|
36
|
+
rotatePartnerWebhookSecret: (id) => rotatePartnerWebhookSecret(rufen, id),
|
|
37
|
+
sendPartnerWebhookTest: (id, event) => sendPartnerWebhookTest(rufen, id, event),
|
|
38
|
+
listPartnerWebhookDeliveries: (o) => listPartnerWebhookDeliveries(rufen, o),
|
|
39
|
+
fehlerRat: (code) => partnerFehlerRat(code),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anmeldung eines Partner-Servers: der Partner-Schluessel als Bearer.
|
|
3
|
+
*
|
|
4
|
+
* Ein dritter Weg neben `apiKeyAuth` (Geraete) und `registerUserAuth`
|
|
5
|
+
* (Browser-Kasse) — und der einzige, mit dem die Partner-Endpunkte antworten.
|
|
6
|
+
* Er traegt **keine** Kopfzeile `cashregister-token`: ein Partner arbeitet nie
|
|
7
|
+
* an einer Kasse, sondern ueber Betriebe.
|
|
8
|
+
*
|
|
9
|
+
* **Der Schluessel gehoert auf einen Server.** Er kann Betriebe anlegen und —
|
|
10
|
+
* mit `credentials:read` — deren Geheimnisse holen. In einer Browser- oder
|
|
11
|
+
* Mobil-App ist er ausgeliefert, nicht hinterlegt.
|
|
12
|
+
*/
|
|
13
|
+
import type { KasseneckAuth } from '../client/auth.js';
|
|
14
|
+
import type { PartnerEnv } from './typen.js';
|
|
15
|
+
/**
|
|
16
|
+
* Die Umgebung eines Partner-Schluessels, ohne Netzaufruf — `null`, wenn es
|
|
17
|
+
* keiner ist. Nuetzlich fuer die Zusicherung „auf diesem Server laeuft nur
|
|
18
|
+
* `pk_live_`" beim Hochfahren.
|
|
19
|
+
*/
|
|
20
|
+
export declare function partnerKeyEnv(schluessel: string): PartnerEnv | null;
|
|
21
|
+
export interface PartnerKeyAuthOptions {
|
|
22
|
+
/** Partner-Schluessel `pk_test_…` / `pk_live_…`. */
|
|
23
|
+
partnerKey: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Anmeldung per Partner-Schluessel.
|
|
27
|
+
*
|
|
28
|
+
* Die Form wird hier geprueft und nicht erst vom Server: ein vertauschter
|
|
29
|
+
* `kr_live_`-Schluessel (der eines Betriebs) faellt sonst als nichtssagendes
|
|
30
|
+
* „ungueltiger Schluessel" auf, obwohl er tadellos ist — nur eben fuer einen
|
|
31
|
+
* anderen Weg. Die Meldung nennt nie den Wert, nur seine Art.
|
|
32
|
+
*/
|
|
33
|
+
export declare function partnerKeyAuth(options: PartnerKeyAuthOptions): KasseneckAuth;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anmeldung eines Partner-Servers: der Partner-Schluessel als Bearer.
|
|
3
|
+
*
|
|
4
|
+
* Ein dritter Weg neben `apiKeyAuth` (Geraete) und `registerUserAuth`
|
|
5
|
+
* (Browser-Kasse) — und der einzige, mit dem die Partner-Endpunkte antworten.
|
|
6
|
+
* Er traegt **keine** Kopfzeile `cashregister-token`: ein Partner arbeitet nie
|
|
7
|
+
* an einer Kasse, sondern ueber Betriebe.
|
|
8
|
+
*
|
|
9
|
+
* **Der Schluessel gehoert auf einen Server.** Er kann Betriebe anlegen und —
|
|
10
|
+
* mit `credentials:read` — deren Geheimnisse holen. In einer Browser- oder
|
|
11
|
+
* Mobil-App ist er ausgeliefert, nicht hinterlegt.
|
|
12
|
+
*/
|
|
13
|
+
import { KasseneckAuthError } from '../client/errors.js';
|
|
14
|
+
/**
|
|
15
|
+
* Form eines Partner-Schluessels: `pk_test_…` bzw. `pk_live_…`. Der Rest ist
|
|
16
|
+
* opak — die Laenge kann sich aendern, das Praefix nicht (an ihm haengt die
|
|
17
|
+
* Umgebung).
|
|
18
|
+
*/
|
|
19
|
+
const SCHLUESSEL_FORM = /^pk_(test|live)_[A-Za-z0-9_-]{16,}$/;
|
|
20
|
+
/**
|
|
21
|
+
* Die Umgebung eines Partner-Schluessels, ohne Netzaufruf — `null`, wenn es
|
|
22
|
+
* keiner ist. Nuetzlich fuer die Zusicherung „auf diesem Server laeuft nur
|
|
23
|
+
* `pk_live_`" beim Hochfahren.
|
|
24
|
+
*/
|
|
25
|
+
export function partnerKeyEnv(schluessel) {
|
|
26
|
+
const treffer = SCHLUESSEL_FORM.exec(typeof schluessel === 'string' ? schluessel.trim() : '');
|
|
27
|
+
return treffer ? treffer[1] : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Anmeldung per Partner-Schluessel.
|
|
31
|
+
*
|
|
32
|
+
* Die Form wird hier geprueft und nicht erst vom Server: ein vertauschter
|
|
33
|
+
* `kr_live_`-Schluessel (der eines Betriebs) faellt sonst als nichtssagendes
|
|
34
|
+
* „ungueltiger Schluessel" auf, obwohl er tadellos ist — nur eben fuer einen
|
|
35
|
+
* anderen Weg. Die Meldung nennt nie den Wert, nur seine Art.
|
|
36
|
+
*/
|
|
37
|
+
export function partnerKeyAuth(options) {
|
|
38
|
+
const schluessel = typeof options?.partnerKey === 'string' ? options.partnerKey.trim() : '';
|
|
39
|
+
if (!schluessel) {
|
|
40
|
+
throw new KasseneckAuthError('partnerKeyAuth: partnerKey fehlt');
|
|
41
|
+
}
|
|
42
|
+
if (!partnerKeyEnv(schluessel)) {
|
|
43
|
+
throw new KasseneckAuthError('partnerKeyAuth: partnerKey hat nicht die Form pk_test_… / pk_live_… — ein Betriebsschluessel (kr_…) passt hier nicht');
|
|
44
|
+
}
|
|
45
|
+
// Pro Aufruf ein frisches Objekt: der Transport darf daran schreiben, ohne
|
|
46
|
+
// die naechste Anfrage zu vergiften (wie apiKeyAuth).
|
|
47
|
+
return () => ({ headers: { Authorization: `Bearer ${schluessel}` }, params: {} });
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Was ein Betrieb ueber die Schnittstelle mitbringen darf — Feld fuer Feld.
|
|
3
|
+
*
|
|
4
|
+
* **Unbekannte Felder werden abgewiesen, nicht stillschweigend verworfen.**
|
|
5
|
+
* Vorher verschwand ein `iban` oder ein vertippter Feldname spurlos: der
|
|
6
|
+
* Partner glaubte, er habe die Steuernummer geschickt, und Kasseneck hatte
|
|
7
|
+
* nichts. Ein Feld, das man schickt und das nichts bewirkt, ist der teuerste
|
|
8
|
+
* Fehler in einer Schnittstelle, weil ihn niemand bemerkt.
|
|
9
|
+
*
|
|
10
|
+
* Seitdem antwortet `createPartnerCustomer` mit `validation` und dem genauen
|
|
11
|
+
* Feldpfad — verschachtelt und je Kontakt: `address.land`,
|
|
12
|
+
* `tax_details.ustid`, `contacts.1.abteilung`.
|
|
13
|
+
*
|
|
14
|
+
* Zwei Dinge halten diese Seite dagegen:
|
|
15
|
+
*
|
|
16
|
+
* - der Typ [Betrieb] (typen.ts) hat genau diese Felder, und TypeScript meldet
|
|
17
|
+
* ein ueberzaehliges schon beim Tippen;
|
|
18
|
+
* - [unbekannteBetriebsfelder] beantwortet dieselbe Frage zur Laufzeit — fuer
|
|
19
|
+
* Daten, die aus einer Datenbank oder einem Formular kommen und deshalb nie
|
|
20
|
+
* durch die Typpruefung gelaufen sind.
|
|
21
|
+
*
|
|
22
|
+
* **Die Wahrheit bleibt der Server.** Dieser Client weist nichts von sich aus
|
|
23
|
+
* ab: eine spaetere Backend-Fassung darf ein Feld ergaenzen, ohne dass ein
|
|
24
|
+
* aelterer Client es blockiert. [unbekannteBetriebsfelder] ist die Vorschau,
|
|
25
|
+
* nicht das Tor.
|
|
26
|
+
*
|
|
27
|
+
* Quelle der Liste: `partner-core.BETRIEB_FELDER` im Backend.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Jedes erlaubte Feld als Pfad. `[]` markiert eine Liste — im Fehlerpfad des
|
|
31
|
+
* Servers steht dort der Index (`contacts.0.name`).
|
|
32
|
+
*
|
|
33
|
+
* Bewusst flach und nicht verschachtelt: so ist es EINE Liste, die der
|
|
34
|
+
* Zwilling Zeile fuer Zeile nachhalten kann. Das Schema fuer die Pruefung
|
|
35
|
+
* entsteht daraus (siehe unten) und nicht daneben.
|
|
36
|
+
*/
|
|
37
|
+
export declare const BETRIEB_FELDER: readonly ["companyName", "legalForm", "state", "industry", "companyRegister", "court", "web", "phone", "email", "billingEmail", "address.street", "address.number", "address.zip", "address.city", "taxDetails.taxNumber", "taxDetails.vatId", "taxDetails.gln", "taxDetails.smallBusiness", "contacts[].name", "contacts[].email", "contacts[].phone", "contacts[].roles", "taxAdvisor.name", "taxAdvisor.email", "taxAdvisor.phone", "taxAdvisor.mayContact"];
|
|
38
|
+
export type BetriebFeld = typeof BETRIEB_FELDER[number];
|
|
39
|
+
/**
|
|
40
|
+
* Die Feldpfade eines Betriebs, die [BETRIEB_FELDER] nicht kennt — dieselbe
|
|
41
|
+
* Ableitung wie im Backend, also dieselben Pfade wie in `data.errors[].field`.
|
|
42
|
+
* Leer heisst: aus dieser Sicht ist nichts ueberzaehlig.
|
|
43
|
+
*
|
|
44
|
+
* Sagt **nichts** ueber die Werte: Steuernummer, UID, PLZ und Gericht prueft
|
|
45
|
+
* das Backend mit `@kreiseck/validator`. Diese Funktion beantwortet nur die
|
|
46
|
+
* Frage „schicke ich etwas, das dort niemand erwartet?".
|
|
47
|
+
*/
|
|
48
|
+
export declare function unbekannteBetriebsfelder(business: unknown): string[];
|