Zurück zur Startseite
Leistungen

Backend Engineering, Integration

Branche

E-Commerce-SaaS

Jahr

2022-2024

Drei Zahlungsverträge, ein Weg zur fertigen Bestellung

Eine SaaS-Plattform verkaufte digitale Download-Codes und nahm Geld auf drei Wegen an. Braintree beantwortete eine Kartenzahlung in einem Aufruf. PayPal schickte den Kunden zur Freigabe weg und zur Ausführung zurück. Sofort leitete zur Bank weiter und meldete sich per Webhook, manchmal Stunden später. Alle drei mussten dieselbe Bestellung abschließen, und jeder tat es ein bisschen anders.

DIE HERAUSFORDERUNG

Drei Verträge, drei Kopien desselben Codes

Die Finalisierungslogik (Codes erzeugen, Rechnung senden, Empfehlungsprovision berechnen) existierte dreimal, einmal pro Gateway. Wurde eine Kopie korrigiert, drifteten die anderen, sichtbar als sporadische Fehler in der Provisionsberechnung. Bestellungen konnten außerdem hängen bleiben: ein abgelaufener PayPal-Redirect, ein Sofort-Webhook, der nie ankam, oder ein Kunde, der sein Code-Paket mitten in der Zahlung änderte, ließen eine Bestellung im Schwebezustand zurück. Die größte Lücke lag im PayPal-Pfad: Zwischen der Freigabe des Kunden auf paypal.com und dem Ausführungsaufruf prüfte nichts, ob die Bestellung noch kostete, was PayPal mitgeteilt worden war. Ein Gutschein in diesem Fenster wurde zum alten Preis abgerechnet.

Das Gateway entscheidet, wann das Geld sicher ist. Der Abschluss der Bestellung muss funktionieren, wann immer das ist.

DIE LÖSUNG

Ein Adapter pro Gateway, eine Finalisierung

Jedes Gateway bekommt einen Adapter für seine Mechanik, und sonst nichts. Braintrees Adapter belastet und antwortet. PayPals legt die Zahlung an, übergibt an den Redirect und prüft bei der Rückkehr den Preis erneut, bevor er ausführt. Der von Sofort legt die Transaktion an, sperrt das Code-Paket gegen Änderungen und wartet auf die Benachrichtigung. Jede sichere Belastung, egal aus welchem Adapter, ruft dieselbe Finalisierung auf: Codes erzeugen, Rechnung senden, Provision buchen. Die Alternative, ein einzelner Aggregator, war nicht verfügbar: Stripe bediente den Markt der Plattform damals nicht, und Sofort war für Banküberweisungen im DACH-Raum Pflicht. Adapter kosten einen Fehlerpfad pro Anbieter; dafür existiert die Bestelllogik einmal und wird einmal getestet.

Der PayPal-Return-Handler. Der Vergleich passiert vor execute(), und die Finalisierung ist ein Aufruf:

Python
def finalize_order(code_package, method, transaction_id):
    """Shared by all three gateways."""
    code_package.paid = True
    code_package.payment_method = method
    code_package.transaction_id = transaction_id
    code_package.save()

    generate_codes(code_package)              # idempotent
    send_invoice_email(code_package)
    create_referral_commission(code_package)  # unique per payment


def execute_paypal_payment(request):
    payment_id = request.GET.get('paymentId')
    payer_id = request.GET.get('PayerID')
    code_package = CodePackage.objects.get(
        pk=request.GET.get('package_id')
    )
    payment = paypalrestsdk.Payment.find(payment_id)

    # The order may have changed during the redirect
    current_price = code_package.calculated_price
    stored_price = Decimal(
        payment.transactions[0].amount.total
    )
    if current_price != stored_price:
        send_admin_alert(
            f'Price mismatch: {current_price} vs {stored_price}'
        )
        payment.void()
        return redirect_with_error('payment-cancelled')

    if payment.execute({'payer_id': payer_id}):
        finalize_order(code_package, 'paypal', payment_id)
        return redirect('payment-success')

    return redirect_with_error('payment-failed')

Die drei Flows laufen lassen

Starte ein beliebiges Gateway; die gemeinsame Finalisierung unten läuft, sobald eine Belastung sicher ist, egal über welchen Pfad. Schalte 'Preis nach Freigabe geändert' ein und starte PayPal erneut: Die erneute Prüfung storniert die Zahlung vor execute() und die Finalisierung beginnt gar nicht.

Live-SimulationBereit
  1. Karte tokenisierenSDK-Nonce, keine Kartendaten am Server
  2. Braintree.sale()Betrag 49,00 EUR, submit_for_settlement
  1. Zahlung anlegen
  2. Redirect zu paypal.com
  3. Kunde gibt frei
  4. Preis erneut prüfen
  5. Zahlung ausführen
  1. Transaktion anlegen
  2. Redirect zur Bank, Paket sperren
  3. Auf Webhook warten
  4. Webhook: pending
  5. Webhook: received
Gemeinsame Finalisierungüber Kreditkarte
  1. generate_codes()50 Codes, idempotent
  2. send_invoice_email()in die Warteschlange gestellt
  3. create_referral_commission()49,00 × 10 % = 4,90 EUR, einmal pro Zahlung
1Abgeschlossene Bestellungen
0Blockierte Manipulationen
1generate_codes()-Aufrufe
0Doppelte Code-Sets

Der Sofort-Benachrichtigungs-Handler. Zwei Benachrichtigungen für eine Zahlung, eine idempotente Code-Erzeugung:

Python
@csrf_exempt
def sofort_notification(request):
    xml = xmltodict.parse(request.body)
    txn_id = xml['status_notification']['transaction']
    status = xml['status_notification']['status']

    payment = SofortPayment.objects.get(transaction_id=txn_id)
    code_package = payment.code_package

    if status == 'pending':
        finalize_order(code_package, 'sofort', txn_id)

    elif status == 'received':
        # Arrives after 'pending' for the same payment.
        # generate_codes() is idempotent: same codes again.
        generate_codes(code_package)
        code_package.paid = True
        code_package.save()

    elif status in ('loss', 'refunded'):
        code_package.editable = False
        code_package.lock_reason = f'Payment {status}'
        code_package.save()

    return HttpResponse(status=200)
Fehlerfälle

Wenn ein Flow abbricht

Eine Sofort-Zahlung sperrt das Code-Paket in dem Moment, in dem sie startet, damit der Kunde nicht ändern kann, wofür die Bank gleich zahlt. Wird die Bankseite verlassen oder kommt die Benachrichtigung nie, beendet ein valid_time-Timestamp die Sperre: Der nächste Zugriff danach gibt die Bearbeitung wieder frei. Kein Cron-Job, keine hängenden Bestellungen.

Sofort sendet zwei Benachrichtigungen für eine erfolgreiche Zahlung, 'pending' und 'received', und beide können die Code-Erzeugung auslösen. generate_codes() ist idempotent, der zweite Lauf erzeugt dieselben Codes, und eine Unique-Constraint auf die Zahlungsreferenz überspringt einen zweiten Provisions-Insert. Zwei Webhooks, eine Bestellung, keine doppelten Codes.

Die Empfehlungsprovision wird als Decimal(float(amount) * REFERRAL_SHARE) berechnet und mit int(referral_share * 100) / 100.0 auf Cent gerundet. Gleitkommazahlen würden sonst Ein-Cent-Abweichungen zwischen Provisionsdatensatz und Auszahlung erzeugen. Die Formel bedient alle drei Gateways, weil sie nur einmal existiert.

DAS ERGEBNIS

Was der gemeinsame Pfad geändert hat

Die drei Gateways verhalten sich weiterhin wie drei verschiedene Firmen, aber der Bestellung ist egal, wer bezahlt hat. Manipulationsversuche am Preis im PayPal-Pfad werden vor der Ausführung erkannt und geloggt, statt in der Buchhaltung aufzufallen. Sofort-Bestellungen sind Sekunden nach der Bankbestätigung abgeschlossen, wann immer diese eintrifft. Die sporadischen Provisionsfehler verschwanden mit dem duplizierten Code, der sie verursacht hatte. Der Checkout läuft über alle drei Schienen, ohne eine Transaktion verloren zu haben.

WICHTIGE KENNZAHLEN

3Zahlungsanbieter hinter einem Checkout
0Verlorene Transaktionen über alle drei Schienen
100%PayPal-Rückkehrer mit Preisprüfung vor der Ausführung
KUNDENSTIMME

"Zahlungs-Edge-Cases waren früher jede Woche ein Debugging-Thema. Jetzt laufen die drei Anbieter durch einen Pfad, den wir verstehen, und die PayPal-Prüfung hat im ersten Monat zwei Exploit-Versuche gestoppt."

CTO

E-Commerce-SaaS · Digitale Produkte

FÜR DEIN PROJEKT

  • Wann es passt

    Du nimmst Zahlungen über mehr als einen Anbieter an oder planst das, und jeder hat eine eigene Vorstellung davon, wann eine Zahlung final ist: sofortige Antwort, Redirect und Rückkehr oder asynchrone Benachrichtigung.

  • Was du prüfen solltest

    Jede Stelle im Code, die eine Bestellung als bezahlt markiert, und was danach läuft. Gibt es diese Liste mehr als einmal, oder führt ein Redirect-Flow aus, ohne aktuellen und gespeicherten Preis zu vergleichen, hast du genau die zwei Lücken, die dieses Projekt geschlossen hat.

  • Was es braucht

    Keine neue Infrastruktur: eine Finalisierungsfunktion, die jeder Adapter aufruft, einen gespeicherten Betrag für den Vergleich bei der Rückkehr, eine Sperre mit Ablauf für asynchrone Flows und idempotente Code-Erzeugung mit Unique-Constraint auf der Provision. Hier war das eine Django-Backend-Integration über 2022-2024.

FAQ

TECHNOLOGIE-STACK

Django
Python
PostgreSQL

Daniel hat bei der Gründung und Umsetzung der Unternehmen Solvster und VIARES einen entscheidenden Beitrag geleistet. Er war verantwortlich für den gesamten technischen Aufbau der Onlinepräsenz, inklusive der Serversetups, der Entwicklung und Optimierung der Website sowie der Sicherstellung der Funktionalität. Besonders beeindruckt hat mich, wie proaktiv und lösungsorientiert er immer an Verbesserungen gearbeitet hat. Daniel war stets sehr responsiv und zuverlässig - es war eine Freude, mit ihm zusammenzuarbeiten. Seine Expertise und sein Engagement haben maßgeblich zum Erfolg beider Projekte beigetragen!

Dietmar Eglhofer - Geschäftsführer, Solvster & VIARES

Schließe dich Dietmar an und starte dein nächstes Projekt mit Zuversicht.

Schreibe mirSchreibe mir