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.
| Kennung | Eigentümer | Erstellt am | Zweck |
|---|---|---|---|
| merchantReference | Till | Vor dem Aufruf von createTransaction | Till-eigene Geschäftsreferenz für die Bestellung |
| transactionId | SDK | Von createTransaction im TransactionContext zurückgegeben | Identifikator des Laufzeitkontexts für „execute“, „cancel“ und „recovery“ |
| transactionReferenceNumber | Acquirer | Nach der Ausführung im TransactionResult zurückgegeben | Referenz des Acquirers oder Verarbeiters |
Die Kasse muss alle drei Werte speichern, um eine Transaktion vollständig abzugleichen.
Happy Path
- Till generiert eine merchantReference und speichert sie im lokalen Speicher mit dem Status PENDING_CREATE.
- Till ruft sdk.terminal.payments.createTransaction(request) auf.
- Das SDK gibt einen „TransactionContext“ zurück. Till speichert die „transactionId“ in derselben Zeile ab und aktualisiert den Status auf „PENDING_EXECUTE“.
- Till ruft „sdk.terminal.payments.executeTransaction(transactionId)“ auf.
- 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).
- 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:
| Feld | Anmerkungen |
|---|---|
| merchantReference | Primärer Geschäftsschlüssel; erforderlich vor jedem SDK-Aufruf |
| transactionId | Wird nach der Rückgabe von „createTransaction“ ausgefüllt |
| transactionReferenceNumber | Wird nach der Rückgabe von „executeTransaction“ ausgefüllt |
| state | Kassenseitige Zustandsmaschine (siehe unten) |
| createdAt, updatedAt | Zeitstempel |
| amount, currency, lineItems | Für den Abgleich und den Nachdruck von Belegen |
| rawResultPayload | Persistiertes „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.
- 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.
- „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“.
- 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.
- Eine Suche anhand der merchantReference wird nicht unterstützt. Wenn die Kasse in dem Zeitfenster zwischen dem Aufruf von
createTransactionund dem Empfang desTransactionContextabgestürzt ist, kennt die Kasse dietransactionIdnoch nicht. Die Kasse muss sich aufgetLastResult()verlassen und anhand der in dem zurückgegebenenTransactionResultenthaltenenmerchantReferenceabgleichen. - Die Aufbewahrungsrichtlinie des SDK für das letzte Ergebnis ist derzeit nicht festgelegt.
Empfohlener Wiederherstellungsalgorithmus
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
| Typ | Quelle | Bedeutung |
|---|---|---|
| TerminalException | Terminalaufrufe | Infrastruktur- oder technischer Fehler |
| TransactionResult mit nicht genehmigtem Ergebnis | Terminalaufrufe | Geschäftliches Ergebnis, einschliesslich Ablehnung, Stornierung durch den Benutzer, Stornierung durch das Terminal |
| AtiSdkException | SDK-Einrichtung | Fehler 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.
\