@kreiseck/kasseneck-api 0.6.45 → 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.
Files changed (70) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +158 -2
  3. package/dist/cjs/client/aufrufe.d.ts +1 -1
  4. package/dist/cjs/client/aufrufe.js +20 -0
  5. package/dist/cjs/client/errors.d.ts +31 -1
  6. package/dist/cjs/client/errors.js +98 -1
  7. package/dist/cjs/client/transport.js +7 -7
  8. package/dist/cjs/kasse/index.d.ts +1 -0
  9. package/dist/cjs/kasse/index.js +3 -0
  10. package/dist/cjs/kasse/trinkgeld.d.ts +10 -0
  11. package/dist/cjs/kasse/trinkgeld.js +27 -0
  12. package/dist/cjs/partner/ablauf.d.ts +47 -0
  13. package/dist/cjs/partner/ablauf.js +99 -0
  14. package/dist/cjs/partner/api.d.ts +68 -0
  15. package/dist/cjs/partner/api.js +44 -0
  16. package/dist/cjs/partner/auth.d.ts +33 -0
  17. package/dist/cjs/partner/auth.js +52 -0
  18. package/dist/cjs/partner/betrieb.d.ts +48 -0
  19. package/dist/cjs/partner/betrieb.js +136 -0
  20. package/dist/cjs/partner/endpunkte.d.ts +142 -0
  21. package/dist/cjs/partner/endpunkte.js +488 -0
  22. package/dist/cjs/partner/fehler.d.ts +72 -0
  23. package/dist/cjs/partner/fehler.js +199 -0
  24. package/dist/cjs/partner/index.d.ts +28 -0
  25. package/dist/cjs/partner/index.js +84 -0
  26. package/dist/cjs/partner/secret.d.ts +66 -0
  27. package/dist/cjs/partner/secret.js +95 -0
  28. package/dist/cjs/partner/typen.d.ts +399 -0
  29. package/dist/cjs/partner/typen.js +33 -0
  30. package/dist/cjs/partner/webhook-signatur.d.ts +95 -0
  31. package/dist/cjs/partner/webhook-signatur.js +158 -0
  32. package/dist/cjs/partner/webhooks.d.ts +211 -0
  33. package/dist/cjs/partner/webhooks.js +269 -0
  34. package/dist/cjs/register/pairing.d.ts +8 -1
  35. package/dist/cjs/register/pairing.js +8 -1
  36. package/dist/esm/client/aufrufe.d.ts +1 -1
  37. package/dist/esm/client/aufrufe.js +20 -0
  38. package/dist/esm/client/errors.d.ts +31 -1
  39. package/dist/esm/client/errors.js +97 -1
  40. package/dist/esm/client/transport.js +8 -8
  41. package/dist/esm/kasse/index.d.ts +1 -0
  42. package/dist/esm/kasse/index.js +1 -0
  43. package/dist/esm/kasse/trinkgeld.d.ts +10 -0
  44. package/dist/esm/kasse/trinkgeld.js +24 -0
  45. package/dist/esm/partner/ablauf.d.ts +47 -0
  46. package/dist/esm/partner/ablauf.js +95 -0
  47. package/dist/esm/partner/api.d.ts +68 -0
  48. package/dist/esm/partner/api.js +41 -0
  49. package/dist/esm/partner/auth.d.ts +33 -0
  50. package/dist/esm/partner/auth.js +48 -0
  51. package/dist/esm/partner/betrieb.d.ts +48 -0
  52. package/dist/esm/partner/betrieb.js +132 -0
  53. package/dist/esm/partner/endpunkte.d.ts +142 -0
  54. package/dist/esm/partner/endpunkte.js +474 -0
  55. package/dist/esm/partner/fehler.d.ts +72 -0
  56. package/dist/esm/partner/fehler.js +189 -0
  57. package/dist/esm/partner/index.d.ts +28 -0
  58. package/dist/esm/partner/index.js +27 -0
  59. package/dist/esm/partner/secret.d.ts +66 -0
  60. package/dist/esm/partner/secret.js +90 -0
  61. package/dist/esm/partner/typen.d.ts +399 -0
  62. package/dist/esm/partner/typen.js +30 -0
  63. package/dist/esm/partner/webhook-signatur.d.ts +95 -0
  64. package/dist/esm/partner/webhook-signatur.js +153 -0
  65. package/dist/esm/partner/webhooks.d.ts +211 -0
  66. package/dist/esm/partner/webhooks.js +257 -0
  67. package/dist/esm/register/pairing.d.ts +8 -1
  68. package/dist/esm/register/pairing.js +8 -1
  69. package/fixtures/oberflaeche.json +132 -3
  70. package/package.json +13 -1
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Die Aufrufe der Partner-API — Betriebe, Signatur, Kassen.
3
+ *
4
+ * Jede Funktion nimmt den Transport als ersten Parameter und ist einzeln
5
+ * importierbar; die Fassade [createPartnerApi] bindet ihn nur einmal.
6
+ *
7
+ * **Was hier geprueft wird und was nicht.** Vor dem Senden prueft dieser Client
8
+ * nur, was er ohne den Server wissen kann: dass eine Kennung ueberhaupt da ist,
9
+ * dass eine Liste nicht leer ist, dass eine Zahl im erlaubten Bereich liegt.
10
+ * Die fachliche Pruefung der Betriebsdaten (Steuernummer samt Pruefziffer, UID,
11
+ * PLZ, Gericht) macht das Backend mit `@kreiseck/validator` — sie hier zu
12
+ * wiederholen hiesse, zwei Wahrheiten zu haben, von denen eine veraltet.
13
+ * Ein Formfehler kommt als `KasseneckApiError` mit `code:"validation"` zurueck;
14
+ * `partnerFeldFehler(fehler)` macht `data.errors[]` daraus. **Es entsteht dabei
15
+ * nichts** — der Aufruf ist folgenlos wiederholbar.
16
+ *
17
+ * Nach dem Senden wird nichts hart gecastet: fehlt ein zugesagtes Feld, wirft
18
+ * der Aufruf `KasseneckValidationError` mit `scope:'response'` statt spaeter
19
+ * einen `TypeError` an unpassender Stelle.
20
+ */
21
+ import type { InternerTransport } from '../client/aufrufe.js';
22
+ import type { ActivateCashregisterResult, CreateCashregisterOptions, CreateCashregisterResult, CreateCustomerOptions, CreateCustomerResult, CustomerCredentials, FonLinkResult, KassenListe, Kunde, KundenListe, ListCustomersOptions, PartnerInfo, RequestSignatureResult, SignaturStand } from './typen.js';
23
+ /**
24
+ * Wer bin ich, in welcher Umgebung, mit welchen Rechten — und welche Apps
25
+ * gehoeren mir. `apps[].id` ist die `appId` fuer [createPartnerCustomer].
26
+ *
27
+ * Der guenstigste Selbsttest beim Hochfahren: er beweist Schluessel, Umgebung
28
+ * und Rechte in einem Aufruf.
29
+ */
30
+ export declare function getPartnerInfo(rufen: InternerTransport): Promise<PartnerInfo>;
31
+ /**
32
+ * Legt einen Betrieb an.
33
+ *
34
+ * **Ohne Panel-Zugang**, solange nicht `access:{invite:true}` dabeisteht:
35
+ * viele Betriebe arbeiten ausschliesslich in der App des Partners. Fuer die
36
+ * Einladung braucht das Partner-Konto ausserdem
37
+ * `partner.canCreateAccess`.
38
+ *
39
+ * **`env` waehlt die Umgebung.** Ohne Angabe entscheidet der Schluessel; ein
40
+ * Live-Schluessel darf mit `env:"test"` einen Testbetrieb anlegen, ein
41
+ * Test-Schluessel niemals einen Live-Betrieb (`live_not_allowed`).
42
+ *
43
+ * **`idempotencyKey` benutzen.** Ein verlorener Antwortweg ist kein
44
+ * Sonderfall, und ohne Schluessel legt der zweite Versuch einen zweiten Betrieb
45
+ * an. Mit Schluessel kommt die gespeicherte Antwort zurueck (`replayed:true`)
46
+ * — auch dann, wenn der Rumpf inzwischen abweicht. Die eigene Kundennummer ist
47
+ * der natuerliche Wert dafuer.
48
+ */
49
+ export declare function createPartnerCustomer(rufen: InternerTransport, optionen: CreateCustomerOptions): Promise<CreateCustomerResult>;
50
+ /** Betriebe dieses Partners, seitenweise. `cursor` aus der Antwort setzt fort. */
51
+ export declare function listPartnerCustomers(rufen: InternerTransport, optionen?: ListCustomersOptions): Promise<KundenListe>;
52
+ /** Ein Betrieb mit allem, was der Partner ueber ihn sehen darf — nie Geheimnisse. */
53
+ export declare function getPartnerCustomer(rufen: InternerTransport, customerId: string): Promise<Kunde>;
54
+ /**
55
+ * Schickt dem Betrieb den Einrichtungs-Link fuer seinen FinanzOnline-Zugang.
56
+ * Ohne diesen Zugang gibt es live keine Signatureinheit (`fon_missing`).
57
+ *
58
+ * Die Antwort nennt den Empfaenger **maskiert** — die Adresse gibt das Backend
59
+ * nie im Klartext aus.
60
+ */
61
+ /**
62
+ * Ist diese E-Mail-Adresse noch als Kasseneck-Zugang frei?
63
+ *
64
+ * Nur noetig, wenn der Betrieb einen eigenen Zugang zum Kundenpanel bekommen
65
+ * soll (`access.invite: true`) — dann wird die Adresse sein Login und darf
66
+ * noch keines sein. Ohne Einladung ist eine belegte Adresse kein Hindernis.
67
+ *
68
+ * Der Sinn ist der Zeitpunkt: ohne diese Frage faellt `email_taken` erst nach
69
+ * einem ganzen ausgefuellten Formular auf. Die Antwort sagt NUR ja oder nein —
70
+ * nie, wem die Adresse gehoert.
71
+ */
72
+ export declare function checkPartnerCustomerEmail(rufen: InternerTransport, email: string): Promise<boolean>;
73
+ export declare function sendPartnerCustomerFonLink(rufen: InternerTransport, customerId: string): Promise<FonLinkResult>;
74
+ /**
75
+ * Beantragt die Signatureinheit. Kasseneck laesst die Karte beim
76
+ * Vertrauensdiensteanbieter **auf diesen Betrieb** ausstellen und meldet sie
77
+ * bei FinanzOnline an; einen Vorrat fertiger Karten gibt es nicht.
78
+ *
79
+ * Der Antrag erzeugt sofort ein Signatur-OBJEKT: `antrag.requestId` ist
80
+ * zugleich die `signaturId`, auf die sich eine Kasse beruft — auch solange
81
+ * noch keine Karte zugewiesen ist.
82
+ *
83
+ * **Je Betrieb laeuft nur ein Antrag.** Ein zweiter Aufruf liefert den
84
+ * laufenden zurueck (`replayed:true`) und ist damit folgenlos wiederholbar.
85
+ * Eine WEITERE Signatur (Ersatzkarte, zweiter Standort) entsteht nur mit
86
+ * `additional:true` — hoechstens zehn je Betrieb (`signature_limit`). Der
87
+ * Abschluss kommt als Ereignis `signature.ready`, nicht als Antwort auf diesen
88
+ * Aufruf.
89
+ */
90
+ export declare function requestCustomerSignature(rufen: InternerTransport, customerId: string, optionen?: {
91
+ art?: string;
92
+ additional?: boolean;
93
+ }): Promise<RequestSignatureResult>;
94
+ /** Stand der Signatur eines Betriebs samt aller Antraege und des FON-Zugangs. */
95
+ export declare function getCustomerSignatureStatus(rufen: InternerTransport, customerId: string): Promise<SignaturStand>;
96
+ /**
97
+ * Legt eine Kasse an.
98
+ *
99
+ * **Jede Kasse bezieht sich auf eine Signatur.** Ohne eine einzige — auch eine
100
+ * noch laufende zaehlt — entsteht keine (`signature_missing`); bei mehreren
101
+ * muss `signaturId` dastehen (`signature_ambiguous`).
102
+ *
103
+ * **Darf vor der fertigen Signatur aufgerufen werden:** die Kasse bleibt dann
104
+ * auf `entwurf` und geht von selbst live, sobald IHRE Signatur bereit ist
105
+ * (`automatic:true`, Vorgabe). `inbetriebnahme.reason` sagt, warum gerade
106
+ * nichts lief: `signature_not_ready` oder `automatik_aus`.
107
+ *
108
+ * Hoechstens 20 Kassen je Betrieb (`cashregister_limit`); ohne gebuchtes Modul
109
+ * `module_inactive`.
110
+ */
111
+ export declare function createCustomerCashregister(rufen: InternerTransport, optionen: CreateCashregisterOptions): Promise<CreateCashregisterResult>;
112
+ /**
113
+ * Nimmt eine Kasse in Betrieb — von Hand, wenn `automatic:false` gilt oder
114
+ * ein Lauf abgebrochen ist.
115
+ *
116
+ * **Jeder Schritt der Kette ist idempotent**, der Startbeleg entsteht nach
117
+ * RKSV genau einmal und ein vorhandener wird erkannt. Ein Wiederholungsaufruf
118
+ * setzt deshalb an der Bruchstelle an und macht nichts doppelt; eine bereits
119
+ * laufende Kasse antwortet mit `unchanged:true`. Das ist der eine
120
+ * veraendernde Aufruf dieses Clients, der ohne Idempotenzschluessel gefahrlos
121
+ * wiederholbar ist — weil der Server ihn so gebaut hat.
122
+ */
123
+ export declare function activateCashregister(rufen: InternerTransport, customerId: string, cashregisterId: string): Promise<ActivateCashregisterResult>;
124
+ /** Die Kassen eines Betriebs samt Stand der Inbetriebnahme — **nie** Token. */
125
+ export declare function listCustomerCashregisters(rufen: InternerTransport, customerId: string): Promise<KassenListe>;
126
+ /**
127
+ * Holt die **Geheimnisse des Betriebs**: seinen `api_key` und die Token seiner
128
+ * Kassen. Damit signiert eine App in seinem Namen Belege — und ein Beleg ist
129
+ * nach RKSV nicht zuruecknehmbar.
130
+ *
131
+ * Braucht den Scope `credentials:read`, der **nicht** zum Standardsatz gehoert
132
+ * und keinem bestehenden Schluessel nachtraeglich hinzugefuegt wird; dafuer
133
+ * wird ein eigener Schluessel angelegt. Jeder Abruf wird mitgeschrieben
134
+ * (Partner, Schluessel, Zeitpunkt) und ist fuer den Betrieb sichtbar.
135
+ *
136
+ * **Nur verschluesselt speichern. Nie protokollieren, nie in eine Mail, nie in
137
+ * einen Fehlerbericht.** Die Werte kommen darum als [KasseneckSecret] und
138
+ * nicht als `string` zurueck: `console.log`, `JSON.stringify` und jede
139
+ * Zeichenketten-Umwandlung zeigen eine Maske, heraus kommt man nur ueber
140
+ * `.reveal()`.
141
+ */
142
+ export declare function getCustomerCredentials(rufen: InternerTransport, customerId: string): Promise<CustomerCredentials>;
@@ -0,0 +1,488 @@
1
+ "use strict";
2
+ /**
3
+ * Die Aufrufe der Partner-API — Betriebe, Signatur, Kassen.
4
+ *
5
+ * Jede Funktion nimmt den Transport als ersten Parameter und ist einzeln
6
+ * importierbar; die Fassade [createPartnerApi] bindet ihn nur einmal.
7
+ *
8
+ * **Was hier geprueft wird und was nicht.** Vor dem Senden prueft dieser Client
9
+ * nur, was er ohne den Server wissen kann: dass eine Kennung ueberhaupt da ist,
10
+ * dass eine Liste nicht leer ist, dass eine Zahl im erlaubten Bereich liegt.
11
+ * Die fachliche Pruefung der Betriebsdaten (Steuernummer samt Pruefziffer, UID,
12
+ * PLZ, Gericht) macht das Backend mit `@kreiseck/validator` — sie hier zu
13
+ * wiederholen hiesse, zwei Wahrheiten zu haben, von denen eine veraltet.
14
+ * Ein Formfehler kommt als `KasseneckApiError` mit `code:"validation"` zurueck;
15
+ * `partnerFeldFehler(fehler)` macht `data.errors[]` daraus. **Es entsteht dabei
16
+ * nichts** — der Aufruf ist folgenlos wiederholbar.
17
+ *
18
+ * Nach dem Senden wird nichts hart gecastet: fehlt ein zugesagtes Feld, wirft
19
+ * der Aufruf `KasseneckValidationError` mit `scope:'response'` statt spaeter
20
+ * einen `TypeError` an unpassender Stelle.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.getPartnerInfo = getPartnerInfo;
24
+ exports.createPartnerCustomer = createPartnerCustomer;
25
+ exports.listPartnerCustomers = listPartnerCustomers;
26
+ exports.getPartnerCustomer = getPartnerCustomer;
27
+ exports.checkPartnerCustomerEmail = checkPartnerCustomerEmail;
28
+ exports.sendPartnerCustomerFonLink = sendPartnerCustomerFonLink;
29
+ exports.requestCustomerSignature = requestCustomerSignature;
30
+ exports.getCustomerSignatureStatus = getCustomerSignatureStatus;
31
+ exports.createCustomerCashregister = createCustomerCashregister;
32
+ exports.activateCashregister = activateCashregister;
33
+ exports.listCustomerCashregisters = listCustomerCashregisters;
34
+ exports.getCustomerCredentials = getCustomerCredentials;
35
+ const errors_js_1 = require("../client/errors.js");
36
+ const secret_js_1 = require("./secret.js");
37
+ // ---------------------------------------------------------------------------
38
+ // Kleine Helfer — bewusst hier und nicht in einem Sammelmodul: sie gehoeren zur
39
+ // Auswertung dieser Antworten und zu nichts sonst.
40
+ // ---------------------------------------------------------------------------
41
+ /** Ein Objekt aus der Antwort, oder ein leeres — nie ein Cast auf gut Glueck. */
42
+ function objekt(wert) {
43
+ return wert !== null && typeof wert === 'object' && !Array.isArray(wert)
44
+ ? wert
45
+ : {};
46
+ }
47
+ function liste(wert) {
48
+ return Array.isArray(wert) ? wert : [];
49
+ }
50
+ function text(wert, rueckfall = '') {
51
+ return typeof wert === 'string' ? wert : rueckfall;
52
+ }
53
+ function textOderNull(wert) {
54
+ return typeof wert === 'string' ? wert : null;
55
+ }
56
+ function zahlOderNull(wert) {
57
+ return typeof wert === 'number' && Number.isFinite(wert) ? wert : null;
58
+ }
59
+ function jaNein(wert, rueckfall = false) {
60
+ return typeof wert === 'boolean' ? wert : rueckfall;
61
+ }
62
+ /**
63
+ * Verlangt ein Feld der Antwort. Der Fehler nennt das Feld und den Vorgang,
64
+ * damit ein Aufrufer nicht raten muss, welcher der Aufrufe etwas anderes
65
+ * schickte als zugesagt.
66
+ */
67
+ function verlangt(wert, vorgang, feld) {
68
+ if (wert === null || typeof wert !== 'object' || Array.isArray(wert)) {
69
+ throw new errors_js_1.KasseneckValidationError(vorgang, `Antwort enthaelt kein ${feld}`, 'response');
70
+ }
71
+ return wert;
72
+ }
73
+ /** Eine Pflichteingabe des Aufrufers — der Fehler geht raus, bevor etwas gesendet wird. */
74
+ function pflicht(wert, vorgang, feld) {
75
+ const s = typeof wert === 'string' ? wert.trim() : '';
76
+ if (!s)
77
+ throw new errors_js_1.KasseneckValidationError(vorgang, `${feld} fehlt`, 'request');
78
+ return s;
79
+ }
80
+ // ---------------------------------------------------------------------------
81
+ // Partner
82
+ // ---------------------------------------------------------------------------
83
+ /**
84
+ * Wer bin ich, in welcher Umgebung, mit welchen Rechten — und welche Apps
85
+ * gehoeren mir. `apps[].id` ist die `appId` fuer [createPartnerCustomer].
86
+ *
87
+ * Der guenstigste Selbsttest beim Hochfahren: er beweist Schluessel, Umgebung
88
+ * und Rechte in einem Aufruf.
89
+ */
90
+ async function getPartnerInfo(rufen) {
91
+ const daten = objekt(await rufen('getPartnerInfo'));
92
+ const partner = verlangt(daten['partner'], 'getPartnerInfo', 'partner');
93
+ const key = objekt(daten['key']);
94
+ return {
95
+ partner: {
96
+ id: text(partner['id']),
97
+ name: text(partner['name']),
98
+ status: text(partner['status'], 'active'),
99
+ // Fehlt das Feld, gilt NEIN. Eine Berechtigung, die man nicht
100
+ // ausdruecklich hat, hat man nicht — ein `true` aus Kulanz erzeugte
101
+ // hier einen Aufruf, der `zugang_nicht_erlaubt` bekommt.
102
+ canCreateAccess: jaNein(partner['canCreateAccess']),
103
+ },
104
+ env: text(daten['env']) === 'test' ? 'test' : 'live',
105
+ scopes: liste(daten['scopes']).filter((s) => typeof s === 'string'),
106
+ key: {
107
+ hint: textOderNull(key['hint']),
108
+ label: textOderNull(key['label']),
109
+ createdAt: zahlOderNull(key['createdAt']),
110
+ scopes: liste(key['scopes']).filter((s) => typeof s === 'string'),
111
+ },
112
+ apps: liste(daten['apps']).map((eintrag) => {
113
+ const a = objekt(eintrag);
114
+ return {
115
+ id: text(a['id']),
116
+ name: text(a['name']),
117
+ status: text(a['status']),
118
+ platform: textOderNull(a['platform']),
119
+ distributions: liste(a['distributions']),
120
+ platforms: liste(a['platforms']).filter((p) => typeof p === 'string'),
121
+ symbol: a['symbol'] ? { url: text(objekt(a['symbol'])['url']) } : null,
122
+ published: jaNein(a['published']),
123
+ listingAllowed: jaNein(a['listingAllowed']),
124
+ };
125
+ }),
126
+ };
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // Betriebe
130
+ // ---------------------------------------------------------------------------
131
+ /**
132
+ * Legt einen Betrieb an.
133
+ *
134
+ * **Ohne Panel-Zugang**, solange nicht `access:{invite:true}` dabeisteht:
135
+ * viele Betriebe arbeiten ausschliesslich in der App des Partners. Fuer die
136
+ * Einladung braucht das Partner-Konto ausserdem
137
+ * `partner.canCreateAccess`.
138
+ *
139
+ * **`env` waehlt die Umgebung.** Ohne Angabe entscheidet der Schluessel; ein
140
+ * Live-Schluessel darf mit `env:"test"` einen Testbetrieb anlegen, ein
141
+ * Test-Schluessel niemals einen Live-Betrieb (`live_not_allowed`).
142
+ *
143
+ * **`idempotencyKey` benutzen.** Ein verlorener Antwortweg ist kein
144
+ * Sonderfall, und ohne Schluessel legt der zweite Versuch einen zweiten Betrieb
145
+ * an. Mit Schluessel kommt die gespeicherte Antwort zurueck (`replayed:true`)
146
+ * — auch dann, wenn der Rumpf inzwischen abweicht. Die eigene Kundennummer ist
147
+ * der natuerliche Wert dafuer.
148
+ */
149
+ async function createPartnerCustomer(rufen, optionen) {
150
+ const appId = pflicht(optionen?.appId, 'createPartnerCustomer', 'appId');
151
+ const betrieb = optionen?.business;
152
+ if (betrieb === null || typeof betrieb !== 'object') {
153
+ throw new errors_js_1.KasseneckValidationError('createPartnerCustomer', 'business fehlt', 'request');
154
+ }
155
+ const daten = objekt(await rufen('createPartnerCustomer', {
156
+ appId,
157
+ business: betrieb,
158
+ idempotencyKey: optionen.idempotencyKey,
159
+ access: optionen.access,
160
+ env: optionen.env,
161
+ }));
162
+ const customerId = textOderNull(daten['customerId']);
163
+ if (!customerId) {
164
+ throw new errors_js_1.KasseneckValidationError('createPartnerCustomer', 'Antwort enthaelt keine customerId', 'response');
165
+ }
166
+ const zugang = objekt(daten['access']);
167
+ return {
168
+ customerId,
169
+ status: text(daten['status'], 'created'),
170
+ env: text(daten['env']) === 'test' ? 'test' : 'live',
171
+ companyName: text(daten['companyName']),
172
+ appId: text(daten['appId'], appId),
173
+ access: { invited: jaNein(zugang['invited']), sentTo: textOderNull(zugang['sentTo']) },
174
+ nextSteps: liste(daten['nextSteps']).filter((s) => typeof s === 'string'),
175
+ replayed: jaNein(daten['replayed']),
176
+ };
177
+ }
178
+ /** Betriebe dieses Partners, seitenweise. `cursor` aus der Antwort setzt fort. */
179
+ async function listPartnerCustomers(rufen, optionen = {}) {
180
+ if (optionen.limit !== undefined && (!Number.isInteger(optionen.limit) || optionen.limit < 1 || optionen.limit > 200)) {
181
+ throw new errors_js_1.KasseneckValidationError('listPartnerCustomers', 'limit muss zwischen 1 und 200 liegen', 'request');
182
+ }
183
+ const daten = objekt(await rufen('listPartnerCustomers', {
184
+ status: optionen.status,
185
+ limit: optionen.limit,
186
+ cursor: optionen.cursor,
187
+ }));
188
+ return {
189
+ customers: liste(daten['customers']).map(kundenZeile),
190
+ cursor: textOderNull(daten['cursor']),
191
+ total: zahlOderNull(daten['total']) ?? 0,
192
+ };
193
+ }
194
+ function kundenZeile(eintrag) {
195
+ const k = objekt(eintrag);
196
+ return {
197
+ customerId: text(k['customerId']),
198
+ companyName: text(k['companyName']),
199
+ status: text(k['status']),
200
+ appId: textOderNull(k['appId']),
201
+ env: text(k['env']) === 'test' ? 'test' : 'live',
202
+ createdAt: zahlOderNull(k['createdAt']),
203
+ avv: avvStand(k['avv']),
204
+ };
205
+ }
206
+ /**
207
+ * Der Vertragsstand, **falls** die Antwort ihn ueberhaupt fuehrt — heute tut
208
+ * sie das nicht, dann bleibt es bei `null`. Kein erfundenes `offen`: „nicht
209
+ * mitgeliefert" und „nicht bestaetigt" duerfen fuer einen Aufrufer nicht
210
+ * dasselbe sein.
211
+ */
212
+ function avvStand(wert) {
213
+ if (wert === null || typeof wert !== 'object' || Array.isArray(wert))
214
+ return null;
215
+ const a = wert;
216
+ return {
217
+ status: text(a['status']),
218
+ version: textOderNull(a['version']),
219
+ confirmedAt: zahlOderNull(a['confirmedAt']),
220
+ mode: textOderNull(a['mode']),
221
+ };
222
+ }
223
+ /** Ein Betrieb mit allem, was der Partner ueber ihn sehen darf — nie Geheimnisse. */
224
+ async function getPartnerCustomer(rufen, customerId) {
225
+ const id = pflicht(customerId, 'getPartnerCustomer', 'customerId');
226
+ const daten = objekt(await rufen('getPartnerCustomer', { customerId: id }));
227
+ const k = verlangt(daten['customer'], 'getPartnerCustomer', 'customer');
228
+ const fon = objekt(k['fon']);
229
+ const zugang = k['access'];
230
+ return {
231
+ ...kundenZeile(k),
232
+ statusAt: zahlOderNull(k['statusAt']),
233
+ liveEnabled: jaNein(k['liveEnabled']),
234
+ createdAt: zahlOderNull(k['createdAt']),
235
+ createdVia: textOderNull(k['createdVia']),
236
+ business: objekt(k['business']),
237
+ fon: { configured: jaNein(fon['configured']), verifiedAt: zahlOderNull(fon['verifiedAt']) },
238
+ access: zugang === null || typeof zugang !== 'object'
239
+ ? null
240
+ : {
241
+ email: textOderNull(objekt(zugang)['email']),
242
+ invitedAt: zahlOderNull(objekt(zugang)['invitedAt']),
243
+ acceptedAt: zahlOderNull(objekt(zugang)['acceptedAt']),
244
+ },
245
+ };
246
+ }
247
+ /**
248
+ * Schickt dem Betrieb den Einrichtungs-Link fuer seinen FinanzOnline-Zugang.
249
+ * Ohne diesen Zugang gibt es live keine Signatureinheit (`fon_missing`).
250
+ *
251
+ * Die Antwort nennt den Empfaenger **maskiert** — die Adresse gibt das Backend
252
+ * nie im Klartext aus.
253
+ */
254
+ /**
255
+ * Ist diese E-Mail-Adresse noch als Kasseneck-Zugang frei?
256
+ *
257
+ * Nur noetig, wenn der Betrieb einen eigenen Zugang zum Kundenpanel bekommen
258
+ * soll (`access.invite: true`) — dann wird die Adresse sein Login und darf
259
+ * noch keines sein. Ohne Einladung ist eine belegte Adresse kein Hindernis.
260
+ *
261
+ * Der Sinn ist der Zeitpunkt: ohne diese Frage faellt `email_taken` erst nach
262
+ * einem ganzen ausgefuellten Formular auf. Die Antwort sagt NUR ja oder nein —
263
+ * nie, wem die Adresse gehoert.
264
+ */
265
+ async function checkPartnerCustomerEmail(rufen, email) {
266
+ const adresse = typeof email === 'string' ? email.trim() : '';
267
+ if (!adresse)
268
+ throw new errors_js_1.KasseneckValidationError('checkPartnerCustomerEmail', 'email fehlt', 'request');
269
+ const daten = objekt(await rufen('checkPartnerCustomerEmail', { email: adresse }));
270
+ return daten['available'] === true;
271
+ }
272
+ async function sendPartnerCustomerFonLink(rufen, customerId) {
273
+ const id = pflicht(customerId, 'sendPartnerCustomerFonLink', 'customerId');
274
+ const daten = objekt(await rufen('sendPartnerCustomerFonLink', { customerId: id }));
275
+ return {
276
+ customerId: text(daten['customerId'], id),
277
+ sentTo: text(daten['sentTo']),
278
+ expiresAt: zahlOderNull(daten['expiresAt']) ?? 0,
279
+ };
280
+ }
281
+ // ---------------------------------------------------------------------------
282
+ // Signatur
283
+ // ---------------------------------------------------------------------------
284
+ function antrag(eintrag) {
285
+ const a = objekt(eintrag);
286
+ const fehler = a['error'];
287
+ return {
288
+ requestId: text(a['requestId']),
289
+ status: text(a['status']),
290
+ statusText: text(a['statusText']),
291
+ art: text(a['art'], 'signature_card'),
292
+ vdaId: textOderNull(a['vdaId']),
293
+ signatureId: textOderNull(a['signatureId']),
294
+ error: fehler === null || typeof fehler !== 'object'
295
+ ? null
296
+ : {
297
+ code: textOderNull(objekt(fehler)['code']),
298
+ message: textOderNull(objekt(fehler)['message']),
299
+ rc: textOderNull(objekt(fehler)['rc']),
300
+ },
301
+ requestedVia: textOderNull(a['requestedVia']),
302
+ createdAt: zahlOderNull(a['createdAt']),
303
+ updatedAt: zahlOderNull(a['updatedAt']),
304
+ history: liste(a['history']).map((h) => {
305
+ const e = objekt(h);
306
+ return {
307
+ von: textOderNull(e['von']),
308
+ nach: text(e['nach']),
309
+ at: zahlOderNull(e['at']) ?? 0,
310
+ reason: textOderNull(e['reason']),
311
+ };
312
+ }),
313
+ };
314
+ }
315
+ /**
316
+ * Beantragt die Signatureinheit. Kasseneck laesst die Karte beim
317
+ * Vertrauensdiensteanbieter **auf diesen Betrieb** ausstellen und meldet sie
318
+ * bei FinanzOnline an; einen Vorrat fertiger Karten gibt es nicht.
319
+ *
320
+ * Der Antrag erzeugt sofort ein Signatur-OBJEKT: `antrag.requestId` ist
321
+ * zugleich die `signaturId`, auf die sich eine Kasse beruft — auch solange
322
+ * noch keine Karte zugewiesen ist.
323
+ *
324
+ * **Je Betrieb laeuft nur ein Antrag.** Ein zweiter Aufruf liefert den
325
+ * laufenden zurueck (`replayed:true`) und ist damit folgenlos wiederholbar.
326
+ * Eine WEITERE Signatur (Ersatzkarte, zweiter Standort) entsteht nur mit
327
+ * `additional:true` — hoechstens zehn je Betrieb (`signature_limit`). Der
328
+ * Abschluss kommt als Ereignis `signature.ready`, nicht als Antwort auf diesen
329
+ * Aufruf.
330
+ */
331
+ async function requestCustomerSignature(rufen, customerId, optionen = {}) {
332
+ const id = pflicht(customerId, 'requestCustomerSignature', 'customerId');
333
+ const daten = objekt(await rufen('requestCustomerSignature', {
334
+ customerId: id,
335
+ art: optionen.art,
336
+ additional: optionen.additional,
337
+ }));
338
+ return {
339
+ request: antrag(verlangt(daten['request'], 'requestCustomerSignature', 'request')),
340
+ replayed: jaNein(daten['replayed']),
341
+ note: textOderNull(daten['note']),
342
+ };
343
+ }
344
+ /** Stand der Signatur eines Betriebs samt aller Antraege und des FON-Zugangs. */
345
+ async function getCustomerSignatureStatus(rufen, customerId) {
346
+ const id = pflicht(customerId, 'getCustomerSignatureStatus', 'customerId');
347
+ const daten = objekt(await rufen('getCustomerSignatureStatus', { customerId: id }));
348
+ const signatur = objekt(daten['signatur']);
349
+ const fon = objekt(daten['fon']);
350
+ return {
351
+ signatur: {
352
+ ready: jaNein(signatur['ready']),
353
+ signatureId: textOderNull(signatur['signatureId']),
354
+ vdaId: textOderNull(signatur['vdaId']),
355
+ },
356
+ requests: liste(daten['requests']).map(antrag),
357
+ fon: { present: jaNein(fon['present']), verifiedAt: zahlOderNull(fon['verifiedAt']) },
358
+ };
359
+ }
360
+ // ---------------------------------------------------------------------------
361
+ // Kassen
362
+ // ---------------------------------------------------------------------------
363
+ function kasse(eintrag) {
364
+ const k = objekt(eintrag);
365
+ const fehler = k['lastError'];
366
+ return {
367
+ cashregisterId: text(k['cashregisterId']),
368
+ name: textOderNull(k['name']),
369
+ status: text(k['status']),
370
+ statusText: text(k['statusText']),
371
+ automatic: jaNein(k['automatic'], true),
372
+ step: textOderNull(k['step']),
373
+ stepText: textOderNull(k['stepText']),
374
+ completedSteps: liste(k['completedSteps']).filter((s) => typeof s === 'string'),
375
+ steps: liste(k['steps']).map((s) => ({ key: text(objekt(s)['key']), text: text(objekt(s)['text']) })),
376
+ signatureId: textOderNull(k['signatureId']),
377
+ attempts: zahlOderNull(k['attempts']) ?? 0,
378
+ lastError: fehler === null || typeof fehler !== 'object'
379
+ ? null
380
+ : {
381
+ code: textOderNull(objekt(fehler)['code']),
382
+ message: textOderNull(objekt(fehler)['message']),
383
+ rc: textOderNull(objekt(fehler)['rc']),
384
+ step: textOderNull(objekt(fehler)['step']),
385
+ at: zahlOderNull(objekt(fehler)['at']),
386
+ },
387
+ createdAt: zahlOderNull(k['createdAt']),
388
+ };
389
+ }
390
+ /**
391
+ * Legt eine Kasse an.
392
+ *
393
+ * **Jede Kasse bezieht sich auf eine Signatur.** Ohne eine einzige — auch eine
394
+ * noch laufende zaehlt — entsteht keine (`signature_missing`); bei mehreren
395
+ * muss `signaturId` dastehen (`signature_ambiguous`).
396
+ *
397
+ * **Darf vor der fertigen Signatur aufgerufen werden:** die Kasse bleibt dann
398
+ * auf `entwurf` und geht von selbst live, sobald IHRE Signatur bereit ist
399
+ * (`automatic:true`, Vorgabe). `inbetriebnahme.reason` sagt, warum gerade
400
+ * nichts lief: `signature_not_ready` oder `automatik_aus`.
401
+ *
402
+ * Hoechstens 20 Kassen je Betrieb (`cashregister_limit`); ohne gebuchtes Modul
403
+ * `module_inactive`.
404
+ */
405
+ async function createCustomerCashregister(rufen, optionen) {
406
+ const id = pflicht(optionen?.customerId, 'createCustomerCashregister', 'customerId');
407
+ const daten = objekt(await rufen('createCustomerCashregister', {
408
+ customerId: id,
409
+ automatic: optionen.automatic,
410
+ signatureRequestId: optionen.signatureRequestId,
411
+ }));
412
+ const ib = objekt(daten['activation']);
413
+ return {
414
+ cashregister: kasse(verlangt(daten['cashregister'], 'createCustomerCashregister', 'cashregister')),
415
+ activation: {
416
+ started: jaNein(ib['started']),
417
+ ok: typeof ib['ok'] === 'boolean' ? ib['ok'] : null,
418
+ step: textOderNull(ib['step']),
419
+ reason: textOderNull(ib['reason']),
420
+ },
421
+ };
422
+ }
423
+ /**
424
+ * Nimmt eine Kasse in Betrieb — von Hand, wenn `automatic:false` gilt oder
425
+ * ein Lauf abgebrochen ist.
426
+ *
427
+ * **Jeder Schritt der Kette ist idempotent**, der Startbeleg entsteht nach
428
+ * RKSV genau einmal und ein vorhandener wird erkannt. Ein Wiederholungsaufruf
429
+ * setzt deshalb an der Bruchstelle an und macht nichts doppelt; eine bereits
430
+ * laufende Kasse antwortet mit `unchanged:true`. Das ist der eine
431
+ * veraendernde Aufruf dieses Clients, der ohne Idempotenzschluessel gefahrlos
432
+ * wiederholbar ist — weil der Server ihn so gebaut hat.
433
+ */
434
+ async function activateCashregister(rufen, customerId, cashregisterId) {
435
+ const kunde = pflicht(customerId, 'activateCashregister', 'customerId');
436
+ const kassenId = pflicht(cashregisterId, 'activateCashregister', 'cashregisterId');
437
+ const daten = objekt(await rufen('activateCashregister', { customerId: kunde, cashregisterId: kassenId }));
438
+ return {
439
+ cashregister: kasse(verlangt(daten['cashregister'], 'activateCashregister', 'cashregister')),
440
+ unchanged: jaNein(daten['unchanged']),
441
+ };
442
+ }
443
+ /** Die Kassen eines Betriebs samt Stand der Inbetriebnahme — **nie** Token. */
444
+ async function listCustomerCashregisters(rufen, customerId) {
445
+ const id = pflicht(customerId, 'listCustomerCashregisters', 'customerId');
446
+ const daten = objekt(await rufen('listCustomerCashregisters', { customerId: id }));
447
+ return {
448
+ customerId: text(daten['customerId'], id),
449
+ cashregisters: liste(daten['cashregisters']).map(kasse),
450
+ signatureReady: jaNein(daten['signatureReady']),
451
+ };
452
+ }
453
+ /**
454
+ * Holt die **Geheimnisse des Betriebs**: seinen `api_key` und die Token seiner
455
+ * Kassen. Damit signiert eine App in seinem Namen Belege — und ein Beleg ist
456
+ * nach RKSV nicht zuruecknehmbar.
457
+ *
458
+ * Braucht den Scope `credentials:read`, der **nicht** zum Standardsatz gehoert
459
+ * und keinem bestehenden Schluessel nachtraeglich hinzugefuegt wird; dafuer
460
+ * wird ein eigener Schluessel angelegt. Jeder Abruf wird mitgeschrieben
461
+ * (Partner, Schluessel, Zeitpunkt) und ist fuer den Betrieb sichtbar.
462
+ *
463
+ * **Nur verschluesselt speichern. Nie protokollieren, nie in eine Mail, nie in
464
+ * einen Fehlerbericht.** Die Werte kommen darum als [KasseneckSecret] und
465
+ * nicht als `string` zurueck: `console.log`, `JSON.stringify` und jede
466
+ * Zeichenketten-Umwandlung zeigen eine Maske, heraus kommt man nur ueber
467
+ * `.reveal()`.
468
+ */
469
+ async function getCustomerCredentials(rufen, customerId) {
470
+ const id = pflicht(customerId, 'getCustomerCredentials', 'customerId');
471
+ const daten = objekt(await rufen('getCustomerCredentials', { customerId: id }));
472
+ return {
473
+ customerId: text(daten['customerId'], id),
474
+ companyName: text(daten['companyName']),
475
+ env: text(daten['env']) === 'test' ? 'test' : 'live',
476
+ apiKey: (0, secret_js_1.alsSecret)('apiKey', daten['apiKey']),
477
+ cashregisters: liste(daten['cashregisters']).map((eintrag) => {
478
+ const k = objekt(eintrag);
479
+ return {
480
+ cashregisterId: text(k['cashregisterId']),
481
+ name: textOderNull(k['name']),
482
+ live: jaNein(k['live']),
483
+ cashregisterToken: (0, secret_js_1.alsSecret)('cashregisterToken', k['cashregisterToken']),
484
+ };
485
+ }),
486
+ note: text(daten['note']),
487
+ };
488
+ }