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:
- HpsClient.abort einmalig versuchen.
responseCode == '0'-> der Vorgang war noch abbrechbar, also nicht abgeschlossen -> CardPaymentOutcome.declined, beweisbar.- jeder andere Code (gemessen
100010) -> der Vorgang ist ueber den abbrechbaren Punkt hinaus -> JETZT die Statusabfrage pollen, sie liefert nun eine echte Aussage. - Abbruch scheitert am Transport -> pollen wie in 3.
- Beim Pollen ist
9027kein 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:
- 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. - Die Klaerung wartet trotzdem nicht das ganze Budget ab. Sagt die
Statusabfrage zweimal in Folge nichts Neues (
9027oder 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