HpsPayments class

Kartenzahlung, deren Ausgang immer bekannt ist.

Der Unterschied zu HpsClient.payment: die Transaktionskennung steht VOR dem ersten Netzweg fest, und ein abgebrochener Vorgang wird ueber HpsClient.transactionStatus geklaert statt als Fehlschlag gemeldet.

Regel, von der nicht abgewichen wird: CardPaymentOutcome.declined entsteht ausschliesslich aus einer POSITIVEN Aussage -- entweder einem GEMESSENEN Ergebniscode des Terminals ungleich '0' (siehe TransactionResponse.isConclusive), oder einem nachweislich gelungenen HpsClient.abort. Ein Transportfehler, ein Zeitablauf oder eine Wissensluecke fuehren NIE dorthin: keines davon ist eine Aussage darueber, dass nichts belastet wurde. Ein Transportfehler trennt "nicht angekommen" nicht von "angekommen, Antwort verloren" -- genau diese Verwechslung hat am 24.08.2026 eine echte Belastung als unbelastet ausgewiesen und den Kunden ein zweites Mal belastet.

Am 27.08.2026 zeigte eine Messung, dass "gemessen" woertlich zu nehmen ist: TransactionResponse.isConclusive galt bis dahin fuer JEDEN Code ausser null und TransactionResponse.noStatementCode -- eine Negativliste, die sich als Positivliste ausgab. Ein neu aufgetretener Code (TransactionResponse.technicalErrorCode, 9900) war darueber schluessig und wurde declined, obwohl unter dem betroffenen Vorgang Geld geflossen sein kann -- derselbe Fehler wie am 24.08.2026, nur an anderer Stelle entstanden. TransactionResponse.isConclusive ist seither eine echte Positivliste: schluessig ist nur ein Code, dessen Bedeutung GEMESSEN und benannt ist. Jeder andere -- egal wie sehr er wie ein Fehlercode aussieht -- ist eine Wissensluecke.

Der Klaerweg, wie er am 26.08.2026 gemessen wurde

Gemessen an einem hobex-HPS (TID 3600335, HPS 1.10.0, Firmware 7.3.6):

Lage Zahlung transactionStatus abort
genehmigt 0 0, bleibt erhalten 100010, scheitert
Kartenfluss laeuft offen 9027 0, gelingt
Karte nicht aufgelegt 100003 9027 --
abgebrochen 100002 9027 --
nie gesehen -- 9027 --

Die Statusabfrage unterscheidet also NICHT zwischen "laeuft gerade", "nie angekommen" und "abgebrochen": alle drei antworten 9027. Wer diesen Code ueber != '0' als Ablehnung liest, meldet fuer einen laufenden Vorgang "gefahrlos wiederholbar" -- der Kunde legt die Karte auf, und die Wiederholung belastet ein zweites Mal.

Der Abbruch trennt, was die Statusabfrage nicht trennt. Deshalb steht er jetzt VORNE, nicht mehr hinter einer Statusabfrage, die den Zustand "laeuft noch" nie meldet:

  1. HpsClient.abort einmalig versuchen.
  2. responseCode == '0' -> der Vorgang war noch abbrechbar, also nicht abgeschlossen -> CardPaymentOutcome.declined, beweisbar.
  3. jeder andere Code (gemessen 100010) -> der Vorgang ist ueber den abbrechbaren Punkt hinaus -> JETZT die Statusabfrage pollen, sie liefert nun eine echte Aussage.
  4. Abbruch scheitert am Transport -> pollen wie in 3.
  5. Beim Pollen ist 9027 kein Ergebnis, sondern ein Grund weiterzumachen. Budget erschoepft -> CardPaymentOutcome.unresolved.

Bewusst in Kauf genommen: gelingt der Abbruch in dem Moment, in dem der Kunde die Karte auflegt, reisst er dessen Zahlung ab. Geldseitig ist das die sichere Richtung -- es ist dann nachweislich nichts belastet, und der Vorgang kann gefahrlos wiederholt werden. Die Alternative waere, den Ausgang offen zu lassen und den Mitarbeiter raten zu lassen; das hat am 24.08.2026 zur Doppelbelastung gefuehrt. Der Abbruch wird nur ausgeloest, wenn die Zahlung ohnehin schon ohne Antwort dasteht -- im Normalfall passiert er nie.

Die Kennung ist in JEDEM Ergebnis gesetzt, auch bei CardPaymentOutcome.unresolved: ohne sie sind Statusabfrage und Storno unerreichbar.

refund bekommt exakt dieselbe Klaerung wie pay, Abbruch eingeschlossen -- und das ist GEMESSEN, nicht nur analog geschlossen. Am 26.08.2026 nachgemessen: abort auf eine LAUFENDE Gutschrift antwortet ebenfalls mit responseCode '0', und die Gutschrift endet daraufhin mit 100002 "Aborted". Der Abbruch ist dort also derselbe Diskriminator wie bei einer Zahlung.

Dazu passt der Aufbau: die Kennung ist bei refund die des NEUEN Vorgangs (der Gutschrift selbst), eine Statusabfrage darauf liefert also genau deren Ausgang, und POST /api/transaction/abort/{tid}/{tx} kennt keinen Transaktionstyp -- er adressiert den laufenden Vorgang unter dieser Kennung. Ohne Abbruch haette die Klaerung einer Gutschrift gar keinen Diskriminator mehr und endete fast immer bei unresolved, weil die Statusabfrage auch hier 9027 antwortet. Ein abgerissener Gutschriftlauf kostet den Kunden nichts: er bekommt sein Geld einen Vorgang spaeter, waehrend eine falsche "gefahrlos wiederholbar"-Meldung eine doppelte Gutschrift ausloesen wuerde.

cancel ist die Ausnahme, und zwar in beiden Richtungen -- siehe _resolveCancel: die uebergebene Kennung ist die der URSPRUENGLICHEN Zahlung. Ein Abbruch darauf waere sinnlos (der Vorgang ist laengst abgeschlossen, gemessen: 100010), und der responseCode dieser Kennung bedeutet dort etwas anderes als bei pay.

Stoerung beim Host: die Klaerung darf nichts schliessen (11.09.2026)

Die Antwortcodeliste von hobex benennt Codes, bei denen der Host beteiligt war und das Terminal NICHT selbst storniert (HpsCodeEffect.hostUncertain, etwa 100007). Fuer sie gilt zweierlei:

  1. Die Zwei-9027-Regel (_ausGeschlossenerAntwort) greift nicht. Sie schliesst aus "das Terminal hat geantwortet und nichts gespeichert" auf "nichts belastet" -- das stimmt fuer einen Vorgang, der am Terminal endete, aber nicht fuer einen, dessen Ausgang beim Host liegt. Dasselbe gilt sinngemaess fuer cancel: ein unveraendertes '0' auf die Originalzahlung beweist dann nicht, dass die Aufhebung beim Host nicht ankam.
  2. Die Klaerung wartet trotzdem nicht das ganze Budget ab. Sagt die Statusabfrage zweimal in Folge nichts Neues (9027 oder erneut ein solcher Code), endet sie sofort als CardPaymentOutcome.unresolved -- das Terminal wird es auch in 90 Sekunden nicht wissen. Meldet sie dagegen '0', ist die Zahlung genehmigt, ganz normal.

Constructors

HpsPayments(HpsClient _client, {Duration resolveBudget = const Duration(seconds: 90), Duration maxBackoff = const Duration(seconds: 10), int maxTransportFailures = 3, Future<void> sleep(Duration)?, Stopwatch clock()?, HpsObserver? observer})

Properties

hashCode int
The hash code for this object.
no setterinherited
maxBackoff Duration
Obergrenze fuer den Abstand zwischen zwei Statusabfragen.
final
maxTransportFailures int
Nach so vielen Statusabfragen in Folge, die am Transport scheitern, wird abgebrochen -- ein offensichtlich unerreichbares Terminal soll den Mitarbeiter nicht das ganze Budget lang warten lassen. Das Ergebnis ist dann CardPaymentOutcome.unresolved, niemals CardPaymentOutcome.declined.
final
resolveBudget Duration
Wie lange insgesamt geklaert wird, bevor der Ausgang offen bleibt.
final
runtimeType Type
A representation of the runtime type of the object.
no setterinherited

Methods

cancel({required String transactionId, required num amount}) Future<HpsResult>
Aufhebung (Storno/Void) einer bestehenden Zahlung mit geklaertem Ausgang.
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
pay({required num amount, num? tip, String? reference, String? transactionId}) Future<HpsResult>
Kartenzahlung mit geklaertem Ausgang.
refund({required num amount, String? originalTransactionId, String? transactionId}) Future<HpsResult>
Gutschrift mit geklaertem Ausgang.
toString() String
A string representation of this object.
inherited

Operators

operator ==(Object other) bool
The equality operator.
inherited