@kreiseck/kasseneck-api 0.7.1 → 0.8.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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Was die Kasse selbst sagt — auf dem Bildschirm, in beiden Huellen.
3
+ *
4
+ * Die Browser-Kasse und die Kassen-App sind eine Kasse. Was der Kassier bei
5
+ * einem Fehler liest, steht hier genau einmal; beide Apps beziehen den Satz
6
+ * von hier, und wer ihn aendert, aendert ihn auf beiden Seiten oder gar nicht.
7
+ *
8
+ * Nicht hier: die fachlichen Saetze des Backends („Kopplungs-Code ist
9
+ * abgelaufen.", „PIN falsch, noch 2 Versuche"). Die gehen unveraendert durch.
10
+ *
11
+ * Ein Satz sagt, was los ist, und was zu tun ist. Nie Innereien.
12
+ *
13
+ * Schluessel: `bereich.name`. Ein Schluessel wird nie umgedeutet — wer den
14
+ * Sinn aendert, legt einen neuen an. `nur` nennt die Seite, wenn ein Satz nur
15
+ * auf einer Plattform vorkommen kann.
16
+ */
17
+ const MELDUNGEN_ROH = {
18
+ // --- Transport -----------------------------------------------------------
19
+ 'netz.keine_verbindung': { text: 'Keine Verbindung zum Server. Bitte die Internetverbindung prüfen und erneut versuchen.' },
20
+ 'netz.zeitablauf': { text: 'Der Server antwortet nicht. Bitte die Internetverbindung prüfen und erneut versuchen.' },
21
+ 'server.unerwartet': { text: 'Der Server hat unerwartet geantwortet (HTTP {status}). Bitte den Support verständigen.', platzhalter: ['status'] },
22
+ // --- Kopplung ------------------------------------------------------------
23
+ 'kopplung.code_fehlt': { text: 'Bitte den Kopplungs-Code eingeben.' },
24
+ 'kopplung.fehlgeschlagen': { text: 'Die Kopplung ist fehlgeschlagen. Bitte erneut versuchen.' },
25
+ 'kopplung.unvollstaendig': { text: 'Die Kopplung ist unvollständig zurückgekommen. Bitte im Panel einen neuen Code erzeugen und noch einmal versuchen.' },
26
+ 'geraet.browser_speicher': { text: 'Dieses Gerät konnte nicht gespeichert werden — der Browser-Speicher steht nicht zur Verfügung. Bitte den privaten Modus verlassen.', nur: ['web'] },
27
+ // --- Anmeldung und Sitzung -----------------------------------------------
28
+ 'anmeldung.pin_fehlt': { text: 'Bitte die PIN eingeben.' },
29
+ 'anmeldung.fehlgeschlagen': { text: 'Die Anmeldung ist fehlgeschlagen. Bitte erneut versuchen.' },
30
+ 'anmeldung.nicht_abgeschlossen': { text: 'Die Anmeldung konnte nicht abgeschlossen werden. Bitte erneut versuchen.' },
31
+ 'anmeldung.wird_abgeschlossen': { text: 'Anmeldung wird gerade abgeschlossen — bitte gleich noch einmal drücken.' },
32
+ 'sitzung.abgelaufen': { text: 'Die Sitzung ist abgelaufen. Bitte erneut anmelden.' },
33
+ 'sitzung.abgelaufen_ohne_netz': { text: 'Keine Verbindung zum Server — die Sitzung ist abgelaufen. Bitte erneut anmelden.' },
34
+ 'sitzung.keine': { text: 'Keine Sitzung.' },
35
+ 'rechte.nicht_aenderbar': { text: 'Das darfst du nicht ändern — das Recht dafür vergibt der Inhaber im Panel.' },
36
+ // --- Verkauf und Positionen ----------------------------------------------
37
+ 'kasse.laden_fehlgeschlagen': { text: 'Die Kasse konnte nicht geladen werden.' },
38
+ 'artikel.laden_fehlgeschlagen': { text: 'Die Artikel konnten nicht geladen werden.' },
39
+ 'artikel.keine_freigegeben': { text: 'Keine Artikel an dieser Kasse. Im Panel unter „Kacheln & Gruppen“ freischalten.' },
40
+ 'artikel.keine_gefunden': { text: 'Keine Artikel gefunden.' },
41
+ 'artikel.steuersatz_unbekannt': { text: '„{name}“ hat einen Steuersatz, den die Kasse nicht kennt.', platzhalter: ['name'] },
42
+ 'position.betrag_fehlt': { text: 'Bitte einen Betrag eingeben.' },
43
+ 'position.bezeichnung_fehlt': { text: 'Bitte eine Bezeichnung eingeben — sie steht am Beleg.' },
44
+ 'rabatt.prozent_ungueltig': { text: 'Bitte einen Prozentwert zwischen 0,1 und 100 eingeben.' },
45
+ // --- Abschluss -----------------------------------------------------------
46
+ 'abschluss.fehlgeschlagen': { text: 'Der Abschluss ist fehlgeschlagen.' },
47
+ 'abschluss.unklar': { text: 'Unklar, ob der Beleg entstanden ist — die Antwort kam nicht an. Bitte nicht noch einmal abschließen: der Beleg kann bereits erstellt und signiert sein. Im Panel unter „Belege“ nachsehen; nur wenn er dort fehlt, den Verkauf erneut abschließen.' },
48
+ 'abschluss.nullbeleg_fehlgeschlagen': { text: 'Der Nullbeleg konnte nicht erstellt werden.' },
49
+ 'abschluss.signatur_ausgefallen': { text: 'Die Signatureinheit hat nicht geantwortet. Der Beleg ist gültig und trägt den Vermerk „Sicherheitseinrichtung ausgefallen“.' },
50
+ // --- Kartenzahlung -------------------------------------------------------
51
+ 'kartenzahlung.nicht_moeglich': { text: 'Kartenzahlung nicht möglich.' },
52
+ 'kartenzahlung.nicht_zustande': { text: 'Die Kartenzahlung ist nicht zustande gekommen.' },
53
+ 'kartenzahlung.nicht_gestartet': { text: 'Kartenzahlung nicht gestartet: {grund}', platzhalter: ['grund'] },
54
+ 'kartenzahlung.unklar': { text: 'Unklar, ob die Kartenzahlung durchgegangen ist — die Verbindung zum Terminal riss ab. Bitte am Terminal-Beleg nachsehen, BEVOR neu kassiert wird: die Karte kann belastet sein.' },
55
+ // Die Kennung ist der einzige Anker, um die Zahlung am Terminal-Beleg wiederzufinden — liegt sie vor, gilt dieser Satz statt kartenzahlung.unklar.
56
+ 'kartenzahlung.unklar_mit_kennung': { text: 'Unklar, ob die Kartenzahlung durchgegangen ist — die Verbindung zum Terminal riss ab. Bitte am Terminal-Beleg nachsehen, BEVOR neu kassiert wird: die Karte kann belastet sein. Kennung der Zahlung: {kennung}.', platzhalter: ['kennung'] },
57
+ 'kartenzahlung.connect_nicht_verbunden': { text: 'Kartenzahlung nicht möglich: Kasseneck Connect ist nicht verbunden — Einstellungen → Kasseneck Connect.', nur: ['web'] },
58
+ 'terminal.keines_gefunden': { text: 'Kein Hobex-Terminal gefunden — ist es eingeschaltet und im selben Netz wie dieser Rechner?', nur: ['web'] },
59
+ 'terminal.nicht_bereit': { text: 'Terminal antwortet, ist aber nicht betriebsbereit: {antwort} — TID prüfen.', platzhalter: ['antwort'], nur: ['web'] },
60
+ 'gptom.app_fehlt': { text: 'Die GP-Tom-App ist auf diesem Gerät nicht da.', nur: ['app'] },
61
+ 'gptom.suche_fehlgeschlagen': { text: 'GP Tom: Suche nach der App fehlgeschlagen: {grund}', platzhalter: ['grund'], nur: ['app'] },
62
+ 'gptom.zahlung_fehlgeschlagen': { text: 'GP Tom: Zahlung fehlgeschlagen: {grund}', platzhalter: ['grund'], nur: ['app'] },
63
+ 'gptom.terminal_antwortet_nicht': { text: 'Das Terminal hat nicht geantwortet: {grund}', platzhalter: ['grund'], nur: ['app'] },
64
+ 'gptom.zahlung_nicht_abgeschlossen': { text: 'Die Zahlung wurde nicht abgeschlossen ({code}).', platzhalter: ['code'], nur: ['app'] },
65
+ // --- Belege und Storno ---------------------------------------------------
66
+ 'belege.laden_fehlgeschlagen': { text: 'Die Belege konnten nicht geladen werden.' },
67
+ 'belege.keine_im_zeitraum': { text: 'Keine Belege in diesem Zeitraum.' },
68
+ 'beleg.laden_fehlgeschlagen': { text: 'Der Beleg konnte nicht geladen werden.' },
69
+ 'beleg.nicht_gefunden': { text: 'Der Beleg wurde nicht gefunden.' },
70
+ 'storno.fehlgeschlagen': { text: 'Das Storno ist fehlgeschlagen.' },
71
+ 'storno.grund_fehlt': { text: 'Bitte einen Grund wählen.' },
72
+ 'storno.position_fehlt': { text: 'Bitte mindestens eine Position wählen.' },
73
+ // --- Druck ---------------------------------------------------------------
74
+ 'druck.fehlgeschlagen': { text: 'Der Ausdruck ist fehlgeschlagen.' },
75
+ 'druck.nicht_moeglich': { text: 'Druck nicht möglich: {grund}', platzhalter: ['grund'] },
76
+ 'druck.testdruck_fehlgeschlagen': { text: 'Der Testdruck ist fehlgeschlagen.' },
77
+ 'druck.testdruck_nicht_moeglich': { text: 'Der Testdruck ist fehlgeschlagen: {grund}', platzhalter: ['grund'] },
78
+ 'druck.kein_drucker': { text: 'Kein Bondrucker eingerichtet — in den Einstellungen unter Drucker & Lade.' },
79
+ 'druck.drucker_nicht_erreichbar': { text: 'Drucker nicht erreichbar.' },
80
+ 'druck.job_abgelaufen': { text: 'Drucker hat den Beleg nicht abgeholt (abgelaufen).', nur: ['web'] },
81
+ 'druck.kein_drucker_gefunden': { text: 'Kein Drucker gefunden — ist er eingeschaltet und im selben Netz wie dieser Rechner?', nur: ['web'] },
82
+ 'druck.nur_chrome': { text: '{weg}-Druck geht nur in Chrome oder Edge (Windows, Mac, Android) — nicht in Safari und nicht am iPad.', platzhalter: ['weg'], nur: ['web'] },
83
+ 'druck.kein_weg_drucker': { text: 'Kein {weg}-Drucker verbunden — „{weg}-Drucker verbinden“ und den Drucker im Dialog wählen.', platzhalter: ['weg'], nur: ['web'] },
84
+ 'bluetooth.aus': { text: 'Bluetooth ist ausgeschaltet. Bitte einschalten und erneut suchen.', nur: ['app'] },
85
+ 'bluetooth.freigabe_fehlt': { text: 'Bitte die Freigabe in den Geräte-Einstellungen erteilen.', nur: ['app'] },
86
+ 'bluetooth.suche_fehlgeschlagen': { text: 'Die Suche ist fehlgeschlagen: {grund}', platzhalter: ['grund'], nur: ['app'] },
87
+ // --- Kasseneck Connect (nur Browser) -------------------------------------
88
+ 'connect.antwortet_nicht': { text: 'Kasseneck Connect antwortet nicht — läuft das Programm auf diesem Rechner?', nur: ['web'] },
89
+ 'connect.code_abgelaufen': { text: 'Der Kopplungs-Code ist abgelaufen — im Agent einen neuen erzeugen („kasseneck-connect pair“).', nur: ['web'] },
90
+ 'connect.zu_viele_versuche': { text: 'Zu viele Fehlversuche — eine Minute warten und noch einmal versuchen.', nur: ['web'] },
91
+ 'connect.drucker_antwortet_nicht': { text: 'Der Drucker antwortet nicht — Strom, Netzwerk und IP prüfen.', nur: ['web'] },
92
+ 'connect.nicht_gekoppelt': { text: 'Diese Kasse ist mit Kasseneck Connect nicht gekoppelt — in den Einstellungen „Koppeln“ drücken.', nur: ['web'] },
93
+ 'connect.drucker_unbekannt': { text: 'Diesen Drucker kennt Kasseneck Connect nicht (mehr) — bitte neu suchen.', nur: ['web'] },
94
+ 'connect.ursprung_nicht_freigegeben': { text: 'Kasseneck Connect nimmt von dieser Adresse nichts an (Ursprung nicht freigegeben).', nur: ['web'] },
95
+ 'connect.nicht_installiert': { text: 'Kasseneck Connect nicht gefunden — bitte installieren.', nur: ['web'] },
96
+ // --- Einstellungen -------------------------------------------------------
97
+ 'einstellungen.laden_fehlgeschlagen': { text: 'Die Einstellungen konnten nicht geladen werden.' },
98
+ 'einstellungen.speichern_fehlgeschlagen': { text: 'Die Einstellung konnte nicht gespeichert werden.' },
99
+ 'einstellungen.noch_nicht_geladen': { text: 'Noch nicht geladen — bitte kurz warten oder neu anmelden.' },
100
+ 'einstellungen.ip_ungueltig': { text: 'Keine gültige IP-Adresse (z. B. 192.168.1.50).' },
101
+ 'entkopplung.fehlgeschlagen': { text: 'Entkoppeln fehlgeschlagen.' },
102
+ 'logo.hochladen_fehlgeschlagen': { text: 'Hochladen fehlgeschlagen.' },
103
+ 'logo.entfernen_fehlgeschlagen': { text: 'Entfernen fehlgeschlagen.' },
104
+ // --- Nur App -------------------------------------------------------------
105
+ 'app.nicht_im_browser': { text: 'Die Kassen-App läuft nicht im Browser — dafür gibt es kasse.kasseneck.at.', nur: ['app'] },
106
+ 'app.im_browser_oeffnen': { text: 'Bitte im Browser öffnen: {ziel}', platzhalter: ['ziel'], nur: ['app'] },
107
+ };
108
+ // Auf den gemeinsamen Typ gebracht: `Object.entries(MELDUNGEN)` liefert sonst
109
+ // pro Schluessel den engsten Literaltyp, und `platzhalter`/`nur` waeren nur
110
+ // auf manchen Zweigen der Vereinigung vorhanden.
111
+ export const MELDUNGEN = MELDUNGEN_ROH;
112
+ /**
113
+ * In welcher Reihenfolge ein Fehler eingeordnet wird — auf beiden Seiten
114
+ * dieselbe. Die Arten:
115
+ * api — HTTP 200, `status:'error'`: der Satz des Backends, woertlich
116
+ * klartext — schon fuer den Bildschirm geschrieben: sein eigener Text
117
+ * zeitablauf — die Frist lief ab, die Anfrage war draussen
118
+ * netz — keine Verbindung zustande gekommen oder abgerissen
119
+ * unerwartet — der Server hat geantwortet, aber nicht wie zugesagt
120
+ * (HTML statt JSON, 500, fehlende Huelle); `status` = HTTP-Code
121
+ * sonst — alles Uebrige ist technisch: der Ersatzsatz des Vorgangs
122
+ */
123
+ export const FEHLERREGELN = [
124
+ { art: 'api', verhalten: 'server_text' },
125
+ { art: 'klartext', verhalten: 'eigener_text' },
126
+ { art: 'zeitablauf', schluessel: 'netz.zeitablauf' },
127
+ { art: 'netz', schluessel: 'netz.keine_verbindung' },
128
+ { art: 'unerwartet', schluessel: 'server.unerwartet' },
129
+ { art: 'sonst', verhalten: 'ersatz' },
130
+ ];
131
+ /** Der Satz zum Schluessel, Platzhalter ersetzt. Fehlt ein Wert, wirft es — ein `{status}` am Tresen waere schlimmer. */
132
+ export function meldung(schluessel, werte = {}) {
133
+ const eintrag = MELDUNGEN[schluessel];
134
+ return eintrag.text.replace(/\{([a-z]+)\}/g, (_, name) => {
135
+ const wert = werte[name];
136
+ if (wert === undefined)
137
+ throw new Error(`meldung(${schluessel}): Platzhalter {${name}} ohne Wert`);
138
+ return String(wert);
139
+ });
140
+ }
141
+ /** Gilt der Satz auf dieser Seite? */
142
+ export function meldungGiltFuer(schluessel, seite) {
143
+ const eintrag = MELDUNGEN[schluessel];
144
+ return eintrag.nur === undefined || eintrag.nur.includes(seite);
145
+ }
@@ -23,5 +23,5 @@ export { type HpsPaymentEvent, type HpsPaymentEventKind, type HpsPaymentObserver
23
23
  export { type CardPaymentOutcome, type HpsPaymentResult, mayRetrySafely, } from './outcome.js';
24
24
  export { type HpsCancelOptions, type HpsPaymentOptions, type HpsPayments, type HpsPaymentsOptions, type HpsRefundOptions, createHpsPayments, } from './payments.js';
25
25
  export { MAX_TRANSACTION_ID_LENGTH, createHpsTransactionIdGenerator, isValidHpsTransactionId, newHpsTransactionId, type HpsTransactionIdGeneratorOptions, } from './transaction-id.js';
26
- export { ABORTED_CODE, APPROVED_CODE, CARD_NOT_PRESENT_CODE, HPS_MEASURED_CODES, INVALID_TRANSACTION_CODE, NOT_ABORTABLE_CODE, INVALID_AMOUNT_CODE, AMOUNT_OUT_OF_RANGE_CODE, INVALID_TID_CODE, NO_STATEMENT_CODE, TECHNICAL_ERROR_CODE, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, isInProgress, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, parseHpsTransactionResponse, type HpsMeasuredCode, type HpsTransactionResponse, } from './transaction-response.js';
26
+ export { ABORTED_CODE, APPROVED_CODE, CARD_NOT_PRESENT_CODE, HPS_MEASURED_CODES, INVALID_TRANSACTION_CODE, NOT_ABORTABLE_CODE, INVALID_AMOUNT_CODE, AMOUNT_OUT_OF_RANGE_CODE, INVALID_TID_CODE, WRONG_PIN_CODE, NO_STATEMENT_CODE, TECHNICAL_ERROR_CODE, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, isInProgress, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, parseHpsTransactionResponse, type HpsMeasuredCode, type HpsTransactionResponse, } from './transaction-response.js';
27
27
  export { hobexReceiptFromHps } from './receipt.js';
@@ -22,5 +22,5 @@ export { HpsClarifyTimeoutError, HpsConnectException, HpsConnectTerminalError, H
22
22
  export { mayRetrySafely, } from './outcome.js';
23
23
  export { createHpsPayments, } from './payments.js';
24
24
  export { MAX_TRANSACTION_ID_LENGTH, createHpsTransactionIdGenerator, isValidHpsTransactionId, newHpsTransactionId, } from './transaction-id.js';
25
- export { ABORTED_CODE, APPROVED_CODE, CARD_NOT_PRESENT_CODE, HPS_MEASURED_CODES, INVALID_TRANSACTION_CODE, NOT_ABORTABLE_CODE, INVALID_AMOUNT_CODE, AMOUNT_OUT_OF_RANGE_CODE, INVALID_TID_CODE, NO_STATEMENT_CODE, TECHNICAL_ERROR_CODE, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, isInProgress, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, parseHpsTransactionResponse, } from './transaction-response.js';
25
+ export { ABORTED_CODE, APPROVED_CODE, CARD_NOT_PRESENT_CODE, HPS_MEASURED_CODES, INVALID_TRANSACTION_CODE, NOT_ABORTABLE_CODE, INVALID_AMOUNT_CODE, AMOUNT_OUT_OF_RANGE_CODE, INVALID_TID_CODE, WRONG_PIN_CODE, NO_STATEMENT_CODE, TECHNICAL_ERROR_CODE, TERMINAL_BUSY_HTTP_STATUS, TRANSACTION_CANCELED_CODE, isApproved, isCanceled, isConclusive, isInProgress, isNoStatement, isNotAbortable, isTechnicalError, isUnknownCode, parseHpsTransactionResponse, } from './transaction-response.js';
26
26
  export { hobexReceiptFromHps } from './receipt.js';
@@ -18,8 +18,23 @@ export type CardPaymentOutcome = 'approved' | 'declined' | 'unresolved';
18
18
  export interface HpsPaymentResult {
19
19
  readonly outcome: CardPaymentOutcome;
20
20
  readonly transactionId: string;
21
- /** Die letzte Antwort des Terminals, sofern eine schluessige ankam. */
21
+ /**
22
+ * Die Antwort des Terminals, die den Ausgang FESTGESCHRIEBEN hat -- nur
23
+ * bei `approved` und `declined`. Bei `unresolved` fehlt sie: eine
24
+ * Nicht-Aussage darf nicht als Beleg mitgegeben werden. Was das Terminal
25
+ * zuletzt gesagt hat, steht dann in [lastResponse].
26
+ */
22
27
  readonly response?: HpsTransactionResponse;
28
+ /**
29
+ * Die letzte Antwort des Terminals bei `unresolved`, sofern ueberhaupt
30
+ * eine ankam -- auch wenn sie nichts entschied (9027, 9900, ein
31
+ * unbekannter Code). NIE ein Beleg, sondern Material fuer Anzeige und
32
+ * Katalog: am 02.09.2026 sah der Bediener bei einer Antwort `55`
33
+ * "PIN falsch" nur "Ausgang unklar", musste raten und buchte eine
34
+ * abgelehnte Zahlung als bezahlt. Der Klartext des Terminals haette die
35
+ * Entscheidung getragen. Bei schluessigem Ausgang nicht gesetzt.
36
+ */
37
+ readonly lastResponse?: HpsTransactionResponse;
23
38
  /**
24
39
  * Verlauf der Klaerung, in Reihenfolge — der Nachweis, der im
25
40
  * Belastungsstreit gelesen wird. Behauptet nie eine Ursache, die nicht
@@ -67,7 +67,7 @@ export function createHpsPayments(client, target, options = {}) {
67
67
  return `Antwort mit technischem Fehler (${TECHNICAL_ERROR_CODE}) -- keine Aussage ueber den Vorgang, Ausgang wird geklaert`;
68
68
  }
69
69
  if (isUnknownCode(res)) {
70
- return `Terminal nennt einen unbekannten Code (${res.responseCode}) -- Ausgang wird geklaert`;
70
+ return `Terminal nennt einen unbekannten Code (${res.responseCode})${klartext(res)} -- Ausgang wird geklaert`;
71
71
  }
72
72
  return `Antwort ohne Aussage (${res.responseCode}) -- Ausgang wird geklaert`;
73
73
  }
@@ -80,10 +80,21 @@ export function createHpsPayments(client, target, options = {}) {
80
80
  return `Status: technischer Fehler (${TECHNICAL_ERROR_CODE}) -- keine Aussage ueber den Vorgang`;
81
81
  }
82
82
  if (isUnknownCode(status)) {
83
- return `Status: unbekannter Code (${status.responseCode}) -- keine Aussage`;
83
+ return `Status: unbekannter Code (${status.responseCode})${klartext(status)} -- keine Aussage`;
84
84
  }
85
85
  return null;
86
86
  }
87
+ /**
88
+ * Der Klartext des Terminals zu einem UNBEKANNTEN Code, als Zusatz fuer den
89
+ * Nachweis -- oder leer, wenn keiner mitkam. Nur hier, nicht bei den
90
+ * gemessenen Codes: deren Bedeutung ist benannt. Bei einem unbekannten Code
91
+ * ist er dagegen das Einzige, was ein Mensch lesen kann -- "PIN falsch"
92
+ * neben `55` haette am 02.09.2026 gereicht, um nicht "bezahlt" zu buchen.
93
+ */
94
+ function klartext(res) {
95
+ const text = res.responseText?.trim();
96
+ return text ? ` "${text}"` : '';
97
+ }
87
98
  /** Ordnet eine Terminal-Antwort ein. `null`, wenn sie nichts entscheidet. */
88
99
  function fromResponse(res, id, steps) {
89
100
  if (!isConclusive(res))
@@ -185,9 +196,19 @@ export function createHpsPayments(client, target, options = {}) {
185
196
  }
186
197
  return null;
187
198
  }
188
- function open(id, steps) {
199
+ /**
200
+ * [letzteAntwort] ist die letzte Antwort, die das Terminal in dieser
201
+ * Klaerung gab -- als `lastResponse` fuer Anzeige und Katalog,
202
+ * ausdruecklich NICHT als `response`.
203
+ */
204
+ function open(id, steps, letzteAntwort) {
189
205
  emit('resolved', steps[steps.length - 1], id);
190
- return { outcome: 'unresolved', transactionId: id, steps: [...steps] };
206
+ return {
207
+ outcome: 'unresolved',
208
+ transactionId: id,
209
+ ...(letzteAntwort ? { lastResponse: letzteAntwort } : {}),
210
+ steps: [...steps],
211
+ };
191
212
  }
192
213
  /**
193
214
  * Fuehrt [call] aus, aber hoechstens so lange, wie vom [resolveBudgetMs]
@@ -260,7 +281,7 @@ export function createHpsPayments(client, target, options = {}) {
260
281
  * [NO_STATEMENT_CODE] (`9027`) beim Pollen einen Aussagewert, den es sonst
261
282
  * nicht hat. Siehe [ausGeschlossenerAntwort].
262
283
  */
263
- async function resolve(id, steps, antwortMitCode = false) {
284
+ async function resolve(id, steps, antwortMitCode = false, letzteAntwort) {
264
285
  emit('resolving', 'Ausgang offen, Klaerung laeuft', id);
265
286
  const start = now();
266
287
  const elapsedMs = () => now() - start;
@@ -281,6 +302,7 @@ export function createHpsPayments(client, target, options = {}) {
281
302
  try {
282
303
  status = await withinBudget(elapsedMs, () => client.status({ ...target, transactionId: id }));
283
304
  transportFailures = 0;
305
+ letzteAntwort = status;
284
306
  }
285
307
  catch (e) {
286
308
  transportFailures += 1;
@@ -309,7 +331,7 @@ export function createHpsPayments(client, target, options = {}) {
309
331
  wait = nextWait(wait);
310
332
  }
311
333
  steps.push('Ausgang bleibt offen');
312
- return open(id, steps);
334
+ return open(id, steps, letzteAntwort);
313
335
  }
314
336
  /**
315
337
  * Liest `9027` beim Pollen als "nicht genehmigt" -- aber NUR, wenn das
@@ -369,7 +391,7 @@ export function createHpsPayments(client, target, options = {}) {
369
391
  * Budget, Backoff und Transportfehler-Deckelung sind unveraendert aus
370
392
  * [resolve] uebernommen.
371
393
  */
372
- async function resolveCancel(id, steps) {
394
+ async function resolveCancel(id, steps, letzteAntwort) {
373
395
  emit('resolving', 'Ausgang offen, Klaerung laeuft', id);
374
396
  const start = now();
375
397
  const elapsedMs = () => now() - start;
@@ -390,6 +412,7 @@ export function createHpsPayments(client, target, options = {}) {
390
412
  status = await withinBudget(elapsedMs, () => client.status({ ...target, transactionId: id }));
391
413
  transportFailures = 0;
392
414
  answeredQueries += 1;
415
+ letzteAntwort = status;
393
416
  }
394
417
  catch (e) {
395
418
  transportFailures += 1;
@@ -413,7 +436,7 @@ export function createHpsPayments(client, target, options = {}) {
413
436
  wait = nextWait(wait);
414
437
  }
415
438
  steps.push('Ausgang bleibt offen');
416
- return open(id, steps);
439
+ return open(id, steps, letzteAntwort);
417
440
  }
418
441
  async function pay(paymentOptions) {
419
442
  const id = paymentOptions.transactionId ?? newHpsTransactionId();
@@ -452,7 +475,7 @@ export function createHpsPayments(client, target, options = {}) {
452
475
  return settled;
453
476
  steps.push(offeneAntwort(res));
454
477
  }
455
- return resolve(id, steps, res?.responseCode !== undefined);
478
+ return resolve(id, steps, res?.responseCode !== undefined, res);
456
479
  }
457
480
  /**
458
481
  * Gutschrift mit geklaertem Ausgang -- EXAKT derselbe Klaerweg wie [pay]
@@ -496,7 +519,7 @@ export function createHpsPayments(client, target, options = {}) {
496
519
  // Dieselbe Klaerfunktion wie [pay]: die Kennung ist die des NEUEN
497
520
  // Vorgangs, eine Statusabfrage darauf liefert also genau dessen Ausgang,
498
521
  // und der Abbruch ist derselbe Diskriminator wie bei einer Zahlung.
499
- return resolve(id, steps, res?.responseCode !== undefined);
522
+ return resolve(id, steps, res?.responseCode !== undefined, res);
500
523
  }
501
524
  /**
502
525
  * Aufhebung (Storno/Void) einer bestehenden Zahlung mit geklaertem Ausgang.
@@ -541,7 +564,7 @@ export function createHpsPayments(client, target, options = {}) {
541
564
  if (!isCanceled(res))
542
565
  steps.push(offeneAntwort(res));
543
566
  }
544
- return resolveCancel(id, steps);
567
+ return resolveCancel(id, steps, res);
545
568
  }
546
569
  return { pay, refund, cancel };
547
570
  }
@@ -72,6 +72,18 @@ export declare const INVALID_AMOUNT_CODE = "9003";
72
72
  export declare const AMOUNT_OUT_OF_RANGE_CODE = "100019";
73
73
  /** Siehe [HPS_MEASURED_CODES]: Terminal-Kennung unbekannt. */
74
74
  export declare const INVALID_TID_CODE = "100108";
75
+ /**
76
+ * Siehe [HPS_MEASURED_CODES]: Host-Ablehnung, falsche PIN -- nichts belastet.
77
+ *
78
+ * Zweistellig, weil ein Antwortcode des HOSTS (ISO 8583, 55 = "Incorrect
79
+ * PIN"), kein `9xxx`-Terminalcode und kein `100xxx`-Code der HPS-Anwendung.
80
+ * Daraus folgt KEINE Regel fuer andere zweistellige Codes: in derselben
81
+ * Familie stehen Genehmigungen (`08`, `10`, `11`, `85`). Jeder andere
82
+ * Host-Code bleibt eine Wissensluecke, bis er gemessen ist -- und die
83
+ * Zwei-9027-Regel in `payments.ts` faengt ihn nicht, weil die Statusabfrage
84
+ * dann nicht 9027 antwortet, sondern mit dem Code selbst.
85
+ */
86
+ export declare const WRONG_PIN_CODE = "55";
75
87
  /**
76
88
  * HTTP `409` ("Terminal is busy"): das Terminal serialisiert und weist eine
77
89
  * zweite Anfrage ab, waehrend eine erste noch laeuft. Am 27.08.2026 gemessen:
@@ -84,6 +84,15 @@ export const HPS_MEASURED_CODES = [
84
84
  + 'nicht; der Vorgang wird abgewiesen, bevor etwas geschieht',
85
85
  conclusive: true,
86
86
  },
87
+ {
88
+ code: '55',
89
+ meaning: '"PIN falsch" -- Host-Ablehnung wegen falscher PIN, die erste '
90
+ + 'gemessene Host-Ablehnung ueberhaupt (02.09.2026 im Betrieb, TID '
91
+ + '3556988, HPS 1.11.4, Firmware 2.3.9): die Zahlung antwortete direkt '
92
+ + 'damit, die Statusabfrage danach elfmal in Folge ebenso -- eine '
93
+ + 'Host-Ablehnung bleibt am Terminal abrufbar; nichts belastet',
94
+ conclusive: true,
95
+ },
87
96
  ];
88
97
  /** `responseCode` einer genehmigten Zahlung. */
89
98
  export const APPROVED_CODE = '0';
@@ -107,6 +116,18 @@ export const INVALID_AMOUNT_CODE = '9003';
107
116
  export const AMOUNT_OUT_OF_RANGE_CODE = '100019';
108
117
  /** Siehe [HPS_MEASURED_CODES]: Terminal-Kennung unbekannt. */
109
118
  export const INVALID_TID_CODE = '100108';
119
+ /**
120
+ * Siehe [HPS_MEASURED_CODES]: Host-Ablehnung, falsche PIN -- nichts belastet.
121
+ *
122
+ * Zweistellig, weil ein Antwortcode des HOSTS (ISO 8583, 55 = "Incorrect
123
+ * PIN"), kein `9xxx`-Terminalcode und kein `100xxx`-Code der HPS-Anwendung.
124
+ * Daraus folgt KEINE Regel fuer andere zweistellige Codes: in derselben
125
+ * Familie stehen Genehmigungen (`08`, `10`, `11`, `85`). Jeder andere
126
+ * Host-Code bleibt eine Wissensluecke, bis er gemessen ist -- und die
127
+ * Zwei-9027-Regel in `payments.ts` faengt ihn nicht, weil die Statusabfrage
128
+ * dann nicht 9027 antwortet, sondern mit dem Code selbst.
129
+ */
130
+ export const WRONG_PIN_CODE = '55';
110
131
  /**
111
132
  * HTTP `409` ("Terminal is busy"): das Terminal serialisiert und weist eine
112
133
  * zweite Anfrage ab, waehrend eine erste noch laeuft. Am 27.08.2026 gemessen:
@@ -1,11 +1,22 @@
1
1
  {
2
- "version": "0.7.1",
2
+ "version": "0.8.0",
3
3
  "gemessenAn": {
4
4
  "tid": "3600335",
5
5
  "hpsVersion": "1.10.0",
6
6
  "firmware": "7.3.6",
7
7
  "zeitraum": "26.-28.08.2026"
8
8
  },
9
+ "ergaenztAn": [
10
+ {
11
+ "tid": "3556988",
12
+ "hpsVersion": "1.11.4",
13
+ "firmware": "2.3.9",
14
+ "zeitraum": "02.09.2026",
15
+ "codes": [
16
+ "55"
17
+ ]
18
+ }
19
+ ],
9
20
  "codes": [
10
21
  {
11
22
  "code": "0",
@@ -61,6 +72,11 @@
61
72
  "code": "100108",
62
73
  "meaning": "\"Invalid TID\" -- die Terminal-Kennung gibt es an diesem Geraet nicht; der Vorgang wird abgewiesen, bevor etwas geschieht",
63
74
  "conclusive": true
75
+ },
76
+ {
77
+ "code": "55",
78
+ "meaning": "\"PIN falsch\" -- Host-Ablehnung wegen falscher PIN, die erste gemessene Host-Ablehnung ueberhaupt (02.09.2026 im Betrieb, TID 3556988, HPS 1.11.4, Firmware 2.3.9): die Zahlung antwortete direkt damit, die Statusabfrage danach elfmal in Folge ebenso -- eine Host-Ablehnung bleibt am Terminal abrufbar; nichts belastet",
79
+ "conclusive": true
64
80
  }
65
81
  ],
66
82
  "terminalBusyHttpStatus": 409
@@ -0,0 +1,32 @@
1
+ {
2
+ "version": 1,
3
+ "faelle": [
4
+ { "name": "api: der Satz des Backends, woertlich",
5
+ "fehler": { "art": "api", "serverMessage": "Kopplungs-Code ist abgelaufen." },
6
+ "ersatz": "Ersatz.", "erwartet": "Kopplungs-Code ist abgelaufen." },
7
+ { "name": "api: auch ein Satz, der wie ein Ersatz aussieht, bleibt der des Backends",
8
+ "fehler": { "art": "api", "serverMessage": "PIN falsch, noch 2 Versuche." },
9
+ "ersatz": "Die Anmeldung ist fehlgeschlagen. Bitte erneut versuchen.", "erwartet": "PIN falsch, noch 2 Versuche." },
10
+ { "name": "klartext: sein eigener Text",
11
+ "fehler": { "art": "klartext", "text": "Eigener Satz fuer den Schirm." },
12
+ "ersatz": "Ersatz.", "erwartet": "Eigener Satz fuer den Schirm." },
13
+ { "name": "zeitablauf: der Server antwortet nicht",
14
+ "fehler": { "art": "zeitablauf" },
15
+ "ersatz": "Ersatz.", "erwartet": { "schluessel": "netz.zeitablauf" } },
16
+ { "name": "netz: keine Verbindung",
17
+ "fehler": { "art": "netz" },
18
+ "ersatz": "Ersatz.", "erwartet": { "schluessel": "netz.keine_verbindung" } },
19
+ { "name": "unerwartet: HTML statt JSON (fehlender Rewrite)",
20
+ "fehler": { "art": "unerwartet", "status": 404 },
21
+ "ersatz": "Ersatz.", "erwartet": { "schluessel": "server.unerwartet", "werte": { "status": 404 } } },
22
+ { "name": "unerwartet: 500er",
23
+ "fehler": { "art": "unerwartet", "status": 500 },
24
+ "ersatz": "Ersatz.", "erwartet": { "schluessel": "server.unerwartet", "werte": { "status": 500 } } },
25
+ { "name": "unerwartet: HTTP 200 ohne Huelle",
26
+ "fehler": { "art": "unerwartet", "status": 200 },
27
+ "ersatz": "Ersatz.", "erwartet": { "schluessel": "server.unerwartet", "werte": { "status": 200 } } },
28
+ { "name": "sonst: Technisches bleibt beim Ersatz",
29
+ "fehler": { "art": "sonst" },
30
+ "ersatz": "Die Kopplung ist fehlgeschlagen. Bitte erneut versuchen.", "erwartet": "Die Kopplung ist fehlgeschlagen. Bitte erneut versuchen." }
31
+ ]
32
+ }