@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,474 @@
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 { KasseneckValidationError } from '../client/errors.js';
22
+ import { alsSecret } from './secret.js';
23
+ // ---------------------------------------------------------------------------
24
+ // Kleine Helfer — bewusst hier und nicht in einem Sammelmodul: sie gehoeren zur
25
+ // Auswertung dieser Antworten und zu nichts sonst.
26
+ // ---------------------------------------------------------------------------
27
+ /** Ein Objekt aus der Antwort, oder ein leeres — nie ein Cast auf gut Glueck. */
28
+ function objekt(wert) {
29
+ return wert !== null && typeof wert === 'object' && !Array.isArray(wert)
30
+ ? wert
31
+ : {};
32
+ }
33
+ function liste(wert) {
34
+ return Array.isArray(wert) ? wert : [];
35
+ }
36
+ function text(wert, rueckfall = '') {
37
+ return typeof wert === 'string' ? wert : rueckfall;
38
+ }
39
+ function textOderNull(wert) {
40
+ return typeof wert === 'string' ? wert : null;
41
+ }
42
+ function zahlOderNull(wert) {
43
+ return typeof wert === 'number' && Number.isFinite(wert) ? wert : null;
44
+ }
45
+ function jaNein(wert, rueckfall = false) {
46
+ return typeof wert === 'boolean' ? wert : rueckfall;
47
+ }
48
+ /**
49
+ * Verlangt ein Feld der Antwort. Der Fehler nennt das Feld und den Vorgang,
50
+ * damit ein Aufrufer nicht raten muss, welcher der Aufrufe etwas anderes
51
+ * schickte als zugesagt.
52
+ */
53
+ function verlangt(wert, vorgang, feld) {
54
+ if (wert === null || typeof wert !== 'object' || Array.isArray(wert)) {
55
+ throw new KasseneckValidationError(vorgang, `Antwort enthaelt kein ${feld}`, 'response');
56
+ }
57
+ return wert;
58
+ }
59
+ /** Eine Pflichteingabe des Aufrufers — der Fehler geht raus, bevor etwas gesendet wird. */
60
+ function pflicht(wert, vorgang, feld) {
61
+ const s = typeof wert === 'string' ? wert.trim() : '';
62
+ if (!s)
63
+ throw new KasseneckValidationError(vorgang, `${feld} fehlt`, 'request');
64
+ return s;
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Partner
68
+ // ---------------------------------------------------------------------------
69
+ /**
70
+ * Wer bin ich, in welcher Umgebung, mit welchen Rechten — und welche Apps
71
+ * gehoeren mir. `apps[].id` ist die `appId` fuer [createPartnerCustomer].
72
+ *
73
+ * Der guenstigste Selbsttest beim Hochfahren: er beweist Schluessel, Umgebung
74
+ * und Rechte in einem Aufruf.
75
+ */
76
+ export async function getPartnerInfo(rufen) {
77
+ const daten = objekt(await rufen('getPartnerInfo'));
78
+ const partner = verlangt(daten['partner'], 'getPartnerInfo', 'partner');
79
+ const key = objekt(daten['key']);
80
+ return {
81
+ partner: {
82
+ id: text(partner['id']),
83
+ name: text(partner['name']),
84
+ status: text(partner['status'], 'active'),
85
+ // Fehlt das Feld, gilt NEIN. Eine Berechtigung, die man nicht
86
+ // ausdruecklich hat, hat man nicht — ein `true` aus Kulanz erzeugte
87
+ // hier einen Aufruf, der `zugang_nicht_erlaubt` bekommt.
88
+ canCreateAccess: jaNein(partner['canCreateAccess']),
89
+ },
90
+ env: text(daten['env']) === 'test' ? 'test' : 'live',
91
+ scopes: liste(daten['scopes']).filter((s) => typeof s === 'string'),
92
+ key: {
93
+ hint: textOderNull(key['hint']),
94
+ label: textOderNull(key['label']),
95
+ createdAt: zahlOderNull(key['createdAt']),
96
+ scopes: liste(key['scopes']).filter((s) => typeof s === 'string'),
97
+ },
98
+ apps: liste(daten['apps']).map((eintrag) => {
99
+ const a = objekt(eintrag);
100
+ return {
101
+ id: text(a['id']),
102
+ name: text(a['name']),
103
+ status: text(a['status']),
104
+ platform: textOderNull(a['platform']),
105
+ distributions: liste(a['distributions']),
106
+ platforms: liste(a['platforms']).filter((p) => typeof p === 'string'),
107
+ symbol: a['symbol'] ? { url: text(objekt(a['symbol'])['url']) } : null,
108
+ published: jaNein(a['published']),
109
+ listingAllowed: jaNein(a['listingAllowed']),
110
+ };
111
+ }),
112
+ };
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Betriebe
116
+ // ---------------------------------------------------------------------------
117
+ /**
118
+ * Legt einen Betrieb an.
119
+ *
120
+ * **Ohne Panel-Zugang**, solange nicht `access:{invite:true}` dabeisteht:
121
+ * viele Betriebe arbeiten ausschliesslich in der App des Partners. Fuer die
122
+ * Einladung braucht das Partner-Konto ausserdem
123
+ * `partner.canCreateAccess`.
124
+ *
125
+ * **`env` waehlt die Umgebung.** Ohne Angabe entscheidet der Schluessel; ein
126
+ * Live-Schluessel darf mit `env:"test"` einen Testbetrieb anlegen, ein
127
+ * Test-Schluessel niemals einen Live-Betrieb (`live_not_allowed`).
128
+ *
129
+ * **`idempotencyKey` benutzen.** Ein verlorener Antwortweg ist kein
130
+ * Sonderfall, und ohne Schluessel legt der zweite Versuch einen zweiten Betrieb
131
+ * an. Mit Schluessel kommt die gespeicherte Antwort zurueck (`replayed:true`)
132
+ * — auch dann, wenn der Rumpf inzwischen abweicht. Die eigene Kundennummer ist
133
+ * der natuerliche Wert dafuer.
134
+ */
135
+ export async function createPartnerCustomer(rufen, optionen) {
136
+ const appId = pflicht(optionen?.appId, 'createPartnerCustomer', 'appId');
137
+ const betrieb = optionen?.business;
138
+ if (betrieb === null || typeof betrieb !== 'object') {
139
+ throw new KasseneckValidationError('createPartnerCustomer', 'business fehlt', 'request');
140
+ }
141
+ const daten = objekt(await rufen('createPartnerCustomer', {
142
+ appId,
143
+ business: betrieb,
144
+ idempotencyKey: optionen.idempotencyKey,
145
+ access: optionen.access,
146
+ env: optionen.env,
147
+ }));
148
+ const customerId = textOderNull(daten['customerId']);
149
+ if (!customerId) {
150
+ throw new KasseneckValidationError('createPartnerCustomer', 'Antwort enthaelt keine customerId', 'response');
151
+ }
152
+ const zugang = objekt(daten['access']);
153
+ return {
154
+ customerId,
155
+ status: text(daten['status'], 'created'),
156
+ env: text(daten['env']) === 'test' ? 'test' : 'live',
157
+ companyName: text(daten['companyName']),
158
+ appId: text(daten['appId'], appId),
159
+ access: { invited: jaNein(zugang['invited']), sentTo: textOderNull(zugang['sentTo']) },
160
+ nextSteps: liste(daten['nextSteps']).filter((s) => typeof s === 'string'),
161
+ replayed: jaNein(daten['replayed']),
162
+ };
163
+ }
164
+ /** Betriebe dieses Partners, seitenweise. `cursor` aus der Antwort setzt fort. */
165
+ export async function listPartnerCustomers(rufen, optionen = {}) {
166
+ if (optionen.limit !== undefined && (!Number.isInteger(optionen.limit) || optionen.limit < 1 || optionen.limit > 200)) {
167
+ throw new KasseneckValidationError('listPartnerCustomers', 'limit muss zwischen 1 und 200 liegen', 'request');
168
+ }
169
+ const daten = objekt(await rufen('listPartnerCustomers', {
170
+ status: optionen.status,
171
+ limit: optionen.limit,
172
+ cursor: optionen.cursor,
173
+ }));
174
+ return {
175
+ customers: liste(daten['customers']).map(kundenZeile),
176
+ cursor: textOderNull(daten['cursor']),
177
+ total: zahlOderNull(daten['total']) ?? 0,
178
+ };
179
+ }
180
+ function kundenZeile(eintrag) {
181
+ const k = objekt(eintrag);
182
+ return {
183
+ customerId: text(k['customerId']),
184
+ companyName: text(k['companyName']),
185
+ status: text(k['status']),
186
+ appId: textOderNull(k['appId']),
187
+ env: text(k['env']) === 'test' ? 'test' : 'live',
188
+ createdAt: zahlOderNull(k['createdAt']),
189
+ avv: avvStand(k['avv']),
190
+ };
191
+ }
192
+ /**
193
+ * Der Vertragsstand, **falls** die Antwort ihn ueberhaupt fuehrt — heute tut
194
+ * sie das nicht, dann bleibt es bei `null`. Kein erfundenes `offen`: „nicht
195
+ * mitgeliefert" und „nicht bestaetigt" duerfen fuer einen Aufrufer nicht
196
+ * dasselbe sein.
197
+ */
198
+ function avvStand(wert) {
199
+ if (wert === null || typeof wert !== 'object' || Array.isArray(wert))
200
+ return null;
201
+ const a = wert;
202
+ return {
203
+ status: text(a['status']),
204
+ version: textOderNull(a['version']),
205
+ confirmedAt: zahlOderNull(a['confirmedAt']),
206
+ mode: textOderNull(a['mode']),
207
+ };
208
+ }
209
+ /** Ein Betrieb mit allem, was der Partner ueber ihn sehen darf — nie Geheimnisse. */
210
+ export async function getPartnerCustomer(rufen, customerId) {
211
+ const id = pflicht(customerId, 'getPartnerCustomer', 'customerId');
212
+ const daten = objekt(await rufen('getPartnerCustomer', { customerId: id }));
213
+ const k = verlangt(daten['customer'], 'getPartnerCustomer', 'customer');
214
+ const fon = objekt(k['fon']);
215
+ const zugang = k['access'];
216
+ return {
217
+ ...kundenZeile(k),
218
+ statusAt: zahlOderNull(k['statusAt']),
219
+ liveEnabled: jaNein(k['liveEnabled']),
220
+ createdAt: zahlOderNull(k['createdAt']),
221
+ createdVia: textOderNull(k['createdVia']),
222
+ business: objekt(k['business']),
223
+ fon: { configured: jaNein(fon['configured']), verifiedAt: zahlOderNull(fon['verifiedAt']) },
224
+ access: zugang === null || typeof zugang !== 'object'
225
+ ? null
226
+ : {
227
+ email: textOderNull(objekt(zugang)['email']),
228
+ invitedAt: zahlOderNull(objekt(zugang)['invitedAt']),
229
+ acceptedAt: zahlOderNull(objekt(zugang)['acceptedAt']),
230
+ },
231
+ };
232
+ }
233
+ /**
234
+ * Schickt dem Betrieb den Einrichtungs-Link fuer seinen FinanzOnline-Zugang.
235
+ * Ohne diesen Zugang gibt es live keine Signatureinheit (`fon_missing`).
236
+ *
237
+ * Die Antwort nennt den Empfaenger **maskiert** — die Adresse gibt das Backend
238
+ * nie im Klartext aus.
239
+ */
240
+ /**
241
+ * Ist diese E-Mail-Adresse noch als Kasseneck-Zugang frei?
242
+ *
243
+ * Nur noetig, wenn der Betrieb einen eigenen Zugang zum Kundenpanel bekommen
244
+ * soll (`access.invite: true`) — dann wird die Adresse sein Login und darf
245
+ * noch keines sein. Ohne Einladung ist eine belegte Adresse kein Hindernis.
246
+ *
247
+ * Der Sinn ist der Zeitpunkt: ohne diese Frage faellt `email_taken` erst nach
248
+ * einem ganzen ausgefuellten Formular auf. Die Antwort sagt NUR ja oder nein —
249
+ * nie, wem die Adresse gehoert.
250
+ */
251
+ export async function checkPartnerCustomerEmail(rufen, email) {
252
+ const adresse = typeof email === 'string' ? email.trim() : '';
253
+ if (!adresse)
254
+ throw new KasseneckValidationError('checkPartnerCustomerEmail', 'email fehlt', 'request');
255
+ const daten = objekt(await rufen('checkPartnerCustomerEmail', { email: adresse }));
256
+ return daten['available'] === true;
257
+ }
258
+ export async function sendPartnerCustomerFonLink(rufen, customerId) {
259
+ const id = pflicht(customerId, 'sendPartnerCustomerFonLink', 'customerId');
260
+ const daten = objekt(await rufen('sendPartnerCustomerFonLink', { customerId: id }));
261
+ return {
262
+ customerId: text(daten['customerId'], id),
263
+ sentTo: text(daten['sentTo']),
264
+ expiresAt: zahlOderNull(daten['expiresAt']) ?? 0,
265
+ };
266
+ }
267
+ // ---------------------------------------------------------------------------
268
+ // Signatur
269
+ // ---------------------------------------------------------------------------
270
+ function antrag(eintrag) {
271
+ const a = objekt(eintrag);
272
+ const fehler = a['error'];
273
+ return {
274
+ requestId: text(a['requestId']),
275
+ status: text(a['status']),
276
+ statusText: text(a['statusText']),
277
+ art: text(a['art'], 'signature_card'),
278
+ vdaId: textOderNull(a['vdaId']),
279
+ signatureId: textOderNull(a['signatureId']),
280
+ error: fehler === null || typeof fehler !== 'object'
281
+ ? null
282
+ : {
283
+ code: textOderNull(objekt(fehler)['code']),
284
+ message: textOderNull(objekt(fehler)['message']),
285
+ rc: textOderNull(objekt(fehler)['rc']),
286
+ },
287
+ requestedVia: textOderNull(a['requestedVia']),
288
+ createdAt: zahlOderNull(a['createdAt']),
289
+ updatedAt: zahlOderNull(a['updatedAt']),
290
+ history: liste(a['history']).map((h) => {
291
+ const e = objekt(h);
292
+ return {
293
+ von: textOderNull(e['von']),
294
+ nach: text(e['nach']),
295
+ at: zahlOderNull(e['at']) ?? 0,
296
+ reason: textOderNull(e['reason']),
297
+ };
298
+ }),
299
+ };
300
+ }
301
+ /**
302
+ * Beantragt die Signatureinheit. Kasseneck laesst die Karte beim
303
+ * Vertrauensdiensteanbieter **auf diesen Betrieb** ausstellen und meldet sie
304
+ * bei FinanzOnline an; einen Vorrat fertiger Karten gibt es nicht.
305
+ *
306
+ * Der Antrag erzeugt sofort ein Signatur-OBJEKT: `antrag.requestId` ist
307
+ * zugleich die `signaturId`, auf die sich eine Kasse beruft — auch solange
308
+ * noch keine Karte zugewiesen ist.
309
+ *
310
+ * **Je Betrieb laeuft nur ein Antrag.** Ein zweiter Aufruf liefert den
311
+ * laufenden zurueck (`replayed:true`) und ist damit folgenlos wiederholbar.
312
+ * Eine WEITERE Signatur (Ersatzkarte, zweiter Standort) entsteht nur mit
313
+ * `additional:true` — hoechstens zehn je Betrieb (`signature_limit`). Der
314
+ * Abschluss kommt als Ereignis `signature.ready`, nicht als Antwort auf diesen
315
+ * Aufruf.
316
+ */
317
+ export async function requestCustomerSignature(rufen, customerId, optionen = {}) {
318
+ const id = pflicht(customerId, 'requestCustomerSignature', 'customerId');
319
+ const daten = objekt(await rufen('requestCustomerSignature', {
320
+ customerId: id,
321
+ art: optionen.art,
322
+ additional: optionen.additional,
323
+ }));
324
+ return {
325
+ request: antrag(verlangt(daten['request'], 'requestCustomerSignature', 'request')),
326
+ replayed: jaNein(daten['replayed']),
327
+ note: textOderNull(daten['note']),
328
+ };
329
+ }
330
+ /** Stand der Signatur eines Betriebs samt aller Antraege und des FON-Zugangs. */
331
+ export async function getCustomerSignatureStatus(rufen, customerId) {
332
+ const id = pflicht(customerId, 'getCustomerSignatureStatus', 'customerId');
333
+ const daten = objekt(await rufen('getCustomerSignatureStatus', { customerId: id }));
334
+ const signatur = objekt(daten['signatur']);
335
+ const fon = objekt(daten['fon']);
336
+ return {
337
+ signatur: {
338
+ ready: jaNein(signatur['ready']),
339
+ signatureId: textOderNull(signatur['signatureId']),
340
+ vdaId: textOderNull(signatur['vdaId']),
341
+ },
342
+ requests: liste(daten['requests']).map(antrag),
343
+ fon: { present: jaNein(fon['present']), verifiedAt: zahlOderNull(fon['verifiedAt']) },
344
+ };
345
+ }
346
+ // ---------------------------------------------------------------------------
347
+ // Kassen
348
+ // ---------------------------------------------------------------------------
349
+ function kasse(eintrag) {
350
+ const k = objekt(eintrag);
351
+ const fehler = k['lastError'];
352
+ return {
353
+ cashregisterId: text(k['cashregisterId']),
354
+ name: textOderNull(k['name']),
355
+ status: text(k['status']),
356
+ statusText: text(k['statusText']),
357
+ automatic: jaNein(k['automatic'], true),
358
+ step: textOderNull(k['step']),
359
+ stepText: textOderNull(k['stepText']),
360
+ completedSteps: liste(k['completedSteps']).filter((s) => typeof s === 'string'),
361
+ steps: liste(k['steps']).map((s) => ({ key: text(objekt(s)['key']), text: text(objekt(s)['text']) })),
362
+ signatureId: textOderNull(k['signatureId']),
363
+ attempts: zahlOderNull(k['attempts']) ?? 0,
364
+ lastError: fehler === null || typeof fehler !== 'object'
365
+ ? null
366
+ : {
367
+ code: textOderNull(objekt(fehler)['code']),
368
+ message: textOderNull(objekt(fehler)['message']),
369
+ rc: textOderNull(objekt(fehler)['rc']),
370
+ step: textOderNull(objekt(fehler)['step']),
371
+ at: zahlOderNull(objekt(fehler)['at']),
372
+ },
373
+ createdAt: zahlOderNull(k['createdAt']),
374
+ };
375
+ }
376
+ /**
377
+ * Legt eine Kasse an.
378
+ *
379
+ * **Jede Kasse bezieht sich auf eine Signatur.** Ohne eine einzige — auch eine
380
+ * noch laufende zaehlt — entsteht keine (`signature_missing`); bei mehreren
381
+ * muss `signaturId` dastehen (`signature_ambiguous`).
382
+ *
383
+ * **Darf vor der fertigen Signatur aufgerufen werden:** die Kasse bleibt dann
384
+ * auf `entwurf` und geht von selbst live, sobald IHRE Signatur bereit ist
385
+ * (`automatic:true`, Vorgabe). `inbetriebnahme.reason` sagt, warum gerade
386
+ * nichts lief: `signature_not_ready` oder `automatik_aus`.
387
+ *
388
+ * Hoechstens 20 Kassen je Betrieb (`cashregister_limit`); ohne gebuchtes Modul
389
+ * `module_inactive`.
390
+ */
391
+ export async function createCustomerCashregister(rufen, optionen) {
392
+ const id = pflicht(optionen?.customerId, 'createCustomerCashregister', 'customerId');
393
+ const daten = objekt(await rufen('createCustomerCashregister', {
394
+ customerId: id,
395
+ automatic: optionen.automatic,
396
+ signatureRequestId: optionen.signatureRequestId,
397
+ }));
398
+ const ib = objekt(daten['activation']);
399
+ return {
400
+ cashregister: kasse(verlangt(daten['cashregister'], 'createCustomerCashregister', 'cashregister')),
401
+ activation: {
402
+ started: jaNein(ib['started']),
403
+ ok: typeof ib['ok'] === 'boolean' ? ib['ok'] : null,
404
+ step: textOderNull(ib['step']),
405
+ reason: textOderNull(ib['reason']),
406
+ },
407
+ };
408
+ }
409
+ /**
410
+ * Nimmt eine Kasse in Betrieb — von Hand, wenn `automatic:false` gilt oder
411
+ * ein Lauf abgebrochen ist.
412
+ *
413
+ * **Jeder Schritt der Kette ist idempotent**, der Startbeleg entsteht nach
414
+ * RKSV genau einmal und ein vorhandener wird erkannt. Ein Wiederholungsaufruf
415
+ * setzt deshalb an der Bruchstelle an und macht nichts doppelt; eine bereits
416
+ * laufende Kasse antwortet mit `unchanged:true`. Das ist der eine
417
+ * veraendernde Aufruf dieses Clients, der ohne Idempotenzschluessel gefahrlos
418
+ * wiederholbar ist — weil der Server ihn so gebaut hat.
419
+ */
420
+ export async function activateCashregister(rufen, customerId, cashregisterId) {
421
+ const kunde = pflicht(customerId, 'activateCashregister', 'customerId');
422
+ const kassenId = pflicht(cashregisterId, 'activateCashregister', 'cashregisterId');
423
+ const daten = objekt(await rufen('activateCashregister', { customerId: kunde, cashregisterId: kassenId }));
424
+ return {
425
+ cashregister: kasse(verlangt(daten['cashregister'], 'activateCashregister', 'cashregister')),
426
+ unchanged: jaNein(daten['unchanged']),
427
+ };
428
+ }
429
+ /** Die Kassen eines Betriebs samt Stand der Inbetriebnahme — **nie** Token. */
430
+ export async function listCustomerCashregisters(rufen, customerId) {
431
+ const id = pflicht(customerId, 'listCustomerCashregisters', 'customerId');
432
+ const daten = objekt(await rufen('listCustomerCashregisters', { customerId: id }));
433
+ return {
434
+ customerId: text(daten['customerId'], id),
435
+ cashregisters: liste(daten['cashregisters']).map(kasse),
436
+ signatureReady: jaNein(daten['signatureReady']),
437
+ };
438
+ }
439
+ /**
440
+ * Holt die **Geheimnisse des Betriebs**: seinen `api_key` und die Token seiner
441
+ * Kassen. Damit signiert eine App in seinem Namen Belege — und ein Beleg ist
442
+ * nach RKSV nicht zuruecknehmbar.
443
+ *
444
+ * Braucht den Scope `credentials:read`, der **nicht** zum Standardsatz gehoert
445
+ * und keinem bestehenden Schluessel nachtraeglich hinzugefuegt wird; dafuer
446
+ * wird ein eigener Schluessel angelegt. Jeder Abruf wird mitgeschrieben
447
+ * (Partner, Schluessel, Zeitpunkt) und ist fuer den Betrieb sichtbar.
448
+ *
449
+ * **Nur verschluesselt speichern. Nie protokollieren, nie in eine Mail, nie in
450
+ * einen Fehlerbericht.** Die Werte kommen darum als [KasseneckSecret] und
451
+ * nicht als `string` zurueck: `console.log`, `JSON.stringify` und jede
452
+ * Zeichenketten-Umwandlung zeigen eine Maske, heraus kommt man nur ueber
453
+ * `.reveal()`.
454
+ */
455
+ export async function getCustomerCredentials(rufen, customerId) {
456
+ const id = pflicht(customerId, 'getCustomerCredentials', 'customerId');
457
+ const daten = objekt(await rufen('getCustomerCredentials', { customerId: id }));
458
+ return {
459
+ customerId: text(daten['customerId'], id),
460
+ companyName: text(daten['companyName']),
461
+ env: text(daten['env']) === 'test' ? 'test' : 'live',
462
+ apiKey: alsSecret('apiKey', daten['apiKey']),
463
+ cashregisters: liste(daten['cashregisters']).map((eintrag) => {
464
+ const k = objekt(eintrag);
465
+ return {
466
+ cashregisterId: text(k['cashregisterId']),
467
+ name: textOderNull(k['name']),
468
+ live: jaNein(k['live']),
469
+ cashregisterToken: alsSecret('cashregisterToken', k['cashregisterToken']),
470
+ };
471
+ }),
472
+ note: text(daten['note']),
473
+ };
474
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Die Fehlercodes der Partner-API und das, was ein Integrator daraufhin tun
3
+ * muss.
4
+ *
5
+ * Das Backend antwortet auf jeden fachlichen Ausgang mit HTTP 200 und legt
6
+ * seine Entscheidung in `data.code` (siehe `docs/api/partner.md` im Backend).
7
+ * Der Transport hebt den Code an `KasseneckApiError.code` — hier steht, was er
8
+ * bedeutet.
9
+ *
10
+ * **Warum die Texte hier stehen und nicht nur im Backend:** die Meldung des
11
+ * Servers sagt, WAS ist. Sie sagt nicht, was der Aufrufer als naechstes tut,
12
+ * und sie kann es auch nicht — dafuer muesste sie seinen Ablauf kennen. Diese
13
+ * Datei ist deshalb kein zweiter Abdruck der Doku, sondern die
14
+ * Handlungsanweisung daneben.
15
+ *
16
+ * **Der Katalog ist vollstaendig.** Die Quelle ist `docs/api/fehlercodes.json`
17
+ * im Backend (Abzug aus `partner-core.FEHLER_KATALOG`); ein Code, den nur eine
18
+ * Seite kennt, ist fuer einen Aufrufer nicht von „gibt es nicht" zu
19
+ * unterscheiden. Deshalb stehen hier BEIDE Flaechen: die der Schnittstelle
20
+ * ([PARTNER_FEHLER_CODES]) und die des Partner-Portals
21
+ * ([PARTNER_PORTAL_FEHLER_CODES]).
22
+ */
23
+ /**
24
+ * Alle Codes, die die **Schnittstelle** kennt. Als Liste und nicht nur als
25
+ * Typ, damit ein Aufrufer sie zur Laufzeit durchgehen kann (Katalogseite,
26
+ * Selbsttest der eigenen Fehlerbehandlung).
27
+ *
28
+ * Reihenfolge und Bestand wie im Abzug des Backends.
29
+ */
30
+ export declare const PARTNER_FEHLER_CODES: readonly ["validation", "rate_limited", "app_not_found", "app_not_accepted", "kein_partnerbetrieb", "live_not_allowed", "customer_exists", "customer_conflict", "customer_limit", "zugang_nicht_erlaubt", "email_taken", "no_email", "fon_missing", "signature_pending", "request_not_found", "signature_missing", "signature_unknown", "signature_ambiguous", "signature_not_ready", "signature_limit", "signature_failed", "module_inactive", "cashregister_limit", "cashregister_not_found", "activation_failed", "webhook_limit", "webhook_inactive", "event_not_subscribed"];
31
+ /**
32
+ * Die Codes, die nur im **Partner-Portal** entstehen — beim Pflegen der App,
33
+ * der Schluessel, der Mitglieder und der Signaturkarten.
34
+ *
35
+ * Sie stehen hier, obwohl kein Aufruf dieses Clients sie ausloest: der
36
+ * Fehlerkatalog ist eine Liste, und eine halbe Liste ist schlimmer als keine.
37
+ * Wer eine Katalogseite baut oder eine fremde Antwort einsortiert, findet
38
+ * damit jeden Code des Backends wieder.
39
+ */
40
+ export declare const PARTNER_PORTAL_FEHLER_CODES: readonly ["app_locked", "version_locked", "invalid_transition", "no_accepted_app", "consent", "key_limit", "last_owner", "auth_user_exists", "card_missing", "card_duplicate", "card_not_verified", "already_assigned"];
41
+ export type PartnerFehlerCode = typeof PARTNER_FEHLER_CODES[number];
42
+ export type PartnerPortalFehlerCode = typeof PARTNER_PORTAL_FEHLER_CODES[number];
43
+ /** Ein Code aus einer der beiden Flaechen. */
44
+ export type PartnerCode = PartnerFehlerCode | PartnerPortalFehlerCode;
45
+ export declare function istPartnerFehlerCode(wert: unknown): wert is PartnerFehlerCode;
46
+ export declare function istPartnerPortalFehlerCode(wert: unknown): wert is PartnerPortalFehlerCode;
47
+ /**
48
+ * Der Handlungssatz zu einem Code — aus beiden Flaechen. `undefined` fuer
49
+ * einen Code, den dieses Paket nicht kennt; ein erfundener Satz waere
50
+ * schlimmer als keiner.
51
+ */
52
+ export declare function partnerFehlerRat(code: string): string | undefined;
53
+ /** Der Fehlercode eines geworfenen Fehlers — `undefined`, wenn es keiner der unseren ist. */
54
+ export declare function partnerFehlerCode(error: unknown): string | undefined;
55
+ /** Kurzform fuer `catch (e) { if (istPartnerFehler(e, 'signature_missing')) … }`. */
56
+ export declare function istPartnerFehler(error: unknown, code: PartnerCode): boolean;
57
+ /** Ein Feldfehler aus `data.errors[]` einer `validation`-Antwort. */
58
+ export interface PartnerFeldFehler {
59
+ /**
60
+ * Der Feldpfad, so wie er im gesendeten Betrieb steht — verschachtelt und je
61
+ * Kontakt: `address.land`, `tax_details.ustid`, `contacts.1.abteilung`.
62
+ */
63
+ field: string;
64
+ message: string;
65
+ }
66
+ /** Die Feldfehler einer `validation`-Antwort; leer, wenn es keine sind. */
67
+ export declare function partnerFeldFehler(error: unknown): PartnerFeldFehler[];
68
+ /**
69
+ * Wie lange `rate_limited` noch gilt, in Sekunden. `undefined`, wenn der
70
+ * Fehler kein `rate_limited` ist oder das Backend keine Angabe macht.
71
+ */
72
+ export declare function partnerWartezeitSek(error: unknown): number | undefined;