Zum Hauptinhalt springen

Lebenszyklus einer Transaktion

Auf dieser Seite wird beschrieben, wie eine Kasse auf demselben Android-Gerät, auf dem auch die wallee SoftPOS-APK installiert ist, in die wallee Terminal-API integriert wird, wie eine Zahlungstransaktion durch das System läuft und wie sich die Kasse nach einem Absturz, einem Neustart oder einem System-Reboot wiederherstellen sollte.

Architektur​

Die Till-App und die wallee SoftPOS-APK laufen auf demselben Android-Gerät. Das SDK stellt eine dienstbasierte API bereit; die Transportdetails werden intern vom SDK abgewickelt.


[ Till APK ] --(ATI SDK)--> [ wallee SoftPOS APK ] --(Netzwerk)--> [ wallee Portal ]


Das Portal ist die massgebliche Quelle für den Transaktionsstatus. Das SDK ist die Schnittstelle der Kasse zu diesem Status.

Bezeichner​

An jeder Transaktion sind drei Identifikatoren beteiligt. Sie befinden sich im Besitz von drei verschiedenen Parteien und existieren zu unterschiedlichen Zeitpunkten.

KennungEigentümerErstellt amZweck
merchantReferenceTillVor dem Aufruf von createTransactionTill-eigene Geschäftsreferenz für die Bestellung
transactionIdSDKVon createTransaction im TransactionContext zurückgegebenIdentifikator des Laufzeitkontexts für „execute“, „cancel“ und „recovery“
transactionReferenceNumberAcquirerNach der Ausführung im TransactionResult zurückgegebenReferenz des Acquirers oder Verarbeiters


Die Kasse muss alle drei Werte speichern, um eine Transaktion vollständig abzugleichen.

Happy Path​

  1. Till generiert eine merchantReference und speichert sie im lokalen Speicher mit dem Status PENDING_CREATE.
  2. Till ruft sdk.terminal.payments.createTransaction(request) auf.
  3. Das SDK gibt einen „TransactionContext“ zurück. Till speichert die „transactionId“ in derselben Zeile ab und aktualisiert den Status auf „PENDING_EXECUTE“.
  4. Till ruft „sdk.terminal.payments.executeTransaction(transactionId)“ auf.
  5. Das SDK gibt ein „TransactionResult“ zurück. Till speichert das Ergebnis und die „transactionReferenceNumber“ ab und aktualisiert den Status auf einen Terminalwert (APPROVED, DECLINED, CANCELLED, FAILED).
  6. Till schliesst die eigene Buchung ab (Kassenbeleg, Journal, Steuer-Speicher).

Anforderungen an die Persistenz an der Kasse​

Die Kasse muss den Transaktionsstatus in einem dauerhaften Speicher speichern. Der im Arbeitsspeicher gespeicherte Status geht beim Beenden des Prozesses, beim Neustart der App oder beim Neustart des Geräts verloren, was eine Wiederherstellung unmöglich macht, da die Kasse dann nicht mehr weiss, welche Referenzen abgefragt werden müssen.


Als Speicherort wird „Android Room“ empfohlen.


Mindestschema:

FeldAnmerkungen
merchantReferencePrimärer Geschäftsschlüssel; erforderlich vor jedem SDK-Aufruf
transactionIdWird nach der Rückgabe von „createTransaction“ ausgefüllt
transactionReferenceNumberWird nach der Rückgabe von „executeTransaction“ ausgefüllt
stateKassenseitige Zustandsmaschine (siehe unten)
createdAt, updatedAtZeitstempel
amount, currency, lineItemsFür den Abgleich und den Nachdruck von Belegen
rawResultPayloadPersistiertes „TransactionResult“ für die Revision


Zustandsmaschine auf der Till-Seite:

PENDING_CREATE -- createTransaction wurde zurückgegeben --> PENDING_EXECUTE

PENDING_EXECUTE -- Ergebnis empfangen --> RESULT_RECEIVED

RESULT_RECEIVED -- Buchung abgeschlossen --> GEBUCHT


Der Schreibvorgang in PENDING_CREATE muss vor dem Aufruf des SDK auf die Festplatte übertragen worden sein. Hierbei handelt es sich um ein „Write-Ahead“-Muster: Sollte die Kasse während des SDK-Aufrufs abstürzen, ist die dauerhaft gespeicherte Zeile der einzige Nachweis dafür, dass eine Transaktion versucht wurde.

Genesung​

Die Wiederherstellung wird beim Start der Anwendung ausgeführt, bevor die Kasse neue Bestellungen annimmt.


Für jede Zeile in der lokalen Datenbank, die sich nicht in einem vom Terminal bestätigten Status befindet, muss die Kasse einen Abgleich durchführen.


Die derzeit verfügbare API für die Wiederherstellung lautet:

val lastResult = sdk.terminal.payments.getLastResult()
if (lastResult.state == LastPaymentResultState.FOUND) {
val transactionResult = lastResult.result
}

Dieser Aufruf gibt das aktuellste endgültige Zahlungsergebnis zurück, das beim Terminal-Dienst vorliegt. Die beiden möglichen Zustände sind „FOUND“ und „NOT_FOUND“.

Einschränkungen bei der Wiederherstellung​

Die aktuelle Recovery-API weist die folgenden Einschränkungen auf. Integratoren müssen diese bei der Entwicklung ihrer kassenseitigen Logik berücksichtigen.

  1. getLastResult() nimmt keine Parameter entgegen. Die Funktion gibt das letzte im SDK gespeicherte Ergebnis zurück, nicht das Ergebnis für eine bestimmte merchantReference oder transactionId. Die Kasse muss überprüfen, ob die zurückgegebene transactionId mit der Zeile übereinstimmt, die sie wiederherstellen möchte.
  2. „LastPaymentResultState“ unterscheidet lediglich zwischen „FOUND“ und „NOT_FOUND“. Es gibt keinen expliziten Status „in Bearbeitung, noch nicht endgültig“. Eine „NOT_FOUND“-Antwort ist mehrdeutig: Sie kann bedeuten, dass „die Transaktion nie gestartet wurde“, „die Transaktion noch in Bearbeitung ist“ oder „das Ergebnis verworfen wurde“.
  3. Es gibt keine Bestätigungsmeldung von der Kasse an das SDK. Das SDK hat keine Kenntnis darüber, ob die Kasse das zurückgegebene Ergebnis gespeichert und verbucht hat. Das SDK kann keine doppelten Wiederherstellungsversuche auf Seiten der Kasse erkennen.
  4. Eine Suche anhand der merchantReference wird nicht unterstützt. Wenn die Kasse in dem Zeitfenster zwischen dem Aufruf von createTransaction und dem Empfang des TransactionContext abgestürzt ist, kennt die Kasse die transactionId noch nicht. Die Kasse muss sich auf getLastResult() verlassen und anhand der in dem zurückgegebenen TransactionResult enthaltenen merchantReference abgleichen.
  5. Die Aufbewahrungsrichtlinie des SDK für das letzte Ergebnis ist derzeit nicht festgelegt.

Angesichts der oben genannten Einschränkungen ist das sicherste Wiederherstellungsmuster:

on app start:
pendingRows = db.query(state != BOOKED)
if pendingRows is empty:
return


lastResult = sdk.terminal.payments.getLastResult()


for row in pendingRows:
if lastResult.state == FOUND
and lastResult.result.merchantReference == row.merchantReference:
row.state = RESULT_RECEIVED
row.transactionReferenceNumber = lastResult.result.transactionReferenceNumber
row.outcome = lastResult.result.outcome
db.commit(row)
performBooking(row)
row.state = BOOKED
db.commit(row)
else:
row.state = UNRESOLVED
db.commit(row)
escalateToCashier(row)




UNGELÖSTE Zeilen müssen an die Kasse oder das Backoffice weitergeleitet werden, damit sie manuell über das Wallee-Portal geklärt werden können. Die Kasse darf diese Zeilen nicht stillschweigend als fehlgeschlagen oder erfolgreich behandeln.

Stornierung während des Fluges​

cancelTransactionExecution(transactionId) fordert den Terminal-Dienst auf, eine laufende Ausführung zu beenden. Das Ergebnis der Stornierung durch den Benutzer oder das Terminal wird als TransactionResult zurückgegeben. TerminalCall.cancel() storniert lediglich das lokale Aufrufer-Handle; die Transaktion auf dem Terminal wird dadurch nicht beendet.


Wenn die Kasse während einer laufenden Transaktion neu gestartet wird, kann sie „cancelTransactionExecution“ nicht aufrufen, da das Handle für den laufenden Aufruf nicht mehr vorhanden ist. Die einzige Möglichkeit zur Wiederherstellung ist der Aufruf von „getLastResult()“.

Fehlermodell​

TypQuelleBedeutung
TerminalExceptionTerminalaufrufeInfrastruktur- oder technischer Fehler
TransactionResult mit nicht genehmigtem ErgebnisTerminalaufrufeGeschäftliches Ergebnis, einschliesslich Ablehnung, Stornierung durch den Benutzer, Stornierung durch das Terminal
AtiSdkExceptionSDK-EinrichtungFehler beim Verbindungsstatus oder bei der Einrichtung auf niedrigerer Ebene

Ablehnungen und Stornierungen sind keine Ausnahmen. Sie sind gültige „TransactionResult“-Werte und müssen von der Kasse als solche gespeichert werden.

Lebenszyklus einer Verbindung​

Die Kasse sollte das SDK in einem Halter im Anwendungsbereich speichern. Rufen Sie „sdk.connect()“ auf, sobald die App gestartet wird. Rufen Sie „sdk.disconnect()“ erst dann auf, wenn die Integration den Terminal-Dienst nicht mehr benötigt. Eine erneute Verbindung nach dem Beenden des Prozesses wird dadurch hergestellt, dass „connect()“ beim Start der App erneut aufgerufen wird, bevor die Wiederherstellung ausgeführt wird.

\