Zum Hauptinhalt springen

Wallets

Stripe Wallet Deposits

Wallet-Einzahlungen laufen über Stripe PaymentIntents mit Stripe Elements im Frontend. Es wird keine Stripe-Checkout-Session mehr erstellt.

Der Ablauf:

  • Der Nutzer gibt im Wallet Management den gewünschten Aufladebetrag ein.
  • Erst nach Klick auf Einzahlen öffnet sich ein Dialog mit den von Stripe aktivierten Zahlungsmethoden.
  • Das Backend liest dafür die aktive/default Stripe Payment Method Configuration über Stripe und berechnet pro Methode eine geschätzte Stripe-Gebühr.
  • Der Nutzer wählt eine Methode aus.
  • Danach erstellt das Backend einen PaymentIntent über den Bruttobetrag gewünschtes Guthaben + geschätzte Stripe-Gebühr.
  • Karte, PayPal und andere normale Methoden werden über das Payment Element angezeigt.
  • Apple Pay und Google Pay werden getrennt angezeigt und über das Express Checkout Element bestätigt, wenn sie in Stripe aktiviert und im aktuellen Browser verfügbar sind.
  • Nach erfolgreicher Zahlung verarbeitet das Backend payment_intent.succeeded.
  • Die Wallet-Gutschrift basiert ausschließlich auf BalanceTransaction.net aus Stripe.
  • Der ursprünglich gewünschte Betrag und die geschätzte Gebühr werden niemals als finale Gutschrift verwendet.

Stripe-Connect-Auszahlungen

Wallet-Auszahlungen laufen über Stripe Connect Express. Beim ersten Auszahlungsversuch erstellt das Backend bei Bedarf ein verbundenes Stripe-Konto und leitet den Nutzer zum Stripe-Onboarding weiter.

Der Ablauf:

  • Der Nutzer gibt den Auszahlungsbetrag ein und fordert einen E-Mail-Code an.
  • Falls Stripe-Onboarding fehlt oder das verbundene Konto noch nicht payout-fähig ist, merkt sich das Frontend den Betrag für die laufende Browser-Session und öffnet Stripe-Onboarding.
  • Nach erfolgreicher Rückkehr öffnet das Auszahlungspopup automatisch wieder und fordert für den gemerkten Betrag direkt den E-Mail-Code an.
  • Erst nach gültigem E-Mail-Code prüft das Backend erneut Wallet-Guthaben, Connect-Onboarding und Stripe-Payout-Fähigkeit.
  • Zusätzlich prüft das Backend vor der Wallet-Buchung das verfügbare Stripe-Plattformguthaben (Balance.available) in STRIPE_CURRENCY.
  • Reicht das verfügbare Plattformguthaben nicht aus, wird keine Wallet-Auszahlung gebucht. Pending Stripe-Guthaben reicht dafür nicht aus.
  • Nur wenn diese Prüfungen erfolgreich sind, wird das User-Wallet belastet, ein Stripe-Transfer zum verbundenen Konto erstellt und anschließend ein Stripe-Payout auf dem verbundenen Konto ausgelöst.
  • Schlägt Stripe nach der Wallet-Buchung fehl, wird ein Transfer soweit möglich zurückgedreht und das Wallet über eine withdrawal_reversal-Buchung zurückgebucht.

Stripe-Dashboard-Begriffe sind dabei von App-Begriffen zu trennen: payout in Stripe-Reports meint häufig eine Auszahlung des Stripe-Kontos auf ein Bankkonto. Das ist nicht automatisch identisch mit einer Skin-to-go-User-Auszahlung über referenceType=stripe_withdrawal.

Lokale Stripe-Webhook-Tests

./scripts/local/stripe-listen-local.sh

Das Script hört standardmäßig auf payment_intent.succeeded, schreibt das aktuelle whsec_... nach secrets/stripe_webhook_secret und startet den Backend-Container neu, damit das neue Signing Secret geladen wird.

Falls zusätzliche Events benötigt werden:

STRIPE_EVENTS=payment_intent.succeeded,charge.succeeded ./scripts/local/stripe-listen-local.sh

Buchhaltungsdaten

Für die Buchhaltung werden Stripe-Finanzdaten auf WalletTransaction gespeichert, darunter:

  • PaymentIntent
  • Charge
  • BalanceTransaction
  • Zahlungsmethode
  • Bruttobetrag
  • Stripe-Gebühr
  • Netto-Betrag
  • geschätzte Gebühr
  • Gebührenabweichung

Stripe-Beträge liegen in Minor Units vor, zum Beispiel Cent.

Wallet-Ledger-Hash-Kette

Das Wallet-Ledger ist pro User als kryptografische Hash-Kette abgesichert. Es gibt bewusst keine globale Blockchain und keine Kette über alle Nutzer hinweg.

Die laufende Wallet-Anzeige bleibt O(1): User.walletBalance und User.walletReservedBalance bleiben die Quelle für Guthaben und reserviertes Guthaben. Die Hash-Kette ergänzt nur die Nachvollziehbarkeit der einzelnen WalletTransaction-Einträge.

Bei jeder Wallet-Buchung über applyWalletAdjustment(...) passiert innerhalb derselben Datenbanktransaktion:

  • Die betroffene User-Zeile wird mit PostgreSQL SELECT ... FOR UPDATE gesperrt.
  • ledgerSequence wird aus User.walletLedgerSequence + 1 gebildet.
  • previousHash übernimmt den bisherigen User.walletLedgerHeadHash.
  • transactionHash wird deterministisch mit SHA-256 aus den relevanten Buchungsdaten berechnet.
  • User.walletLedgerHeadHash und User.walletLedgerSequence werden auf den neuen Kopf gesetzt.

Die Hash-Berechnung verwendet eine stabile Feldreihenfolge, Beträge in Cent, Datumswerte als ISO-String und die vorab erzeugte WalletTransaction.id. Käufer- und Verkäuferbuchungen bei Trades schreiben zwei getrennte User-Ketten und referenzieren weiterhin dieselbe tradeId.

Neue Wallet-Transaktionen mit fachlicher Referenz sind zusätzlich idempotent abgesichert. Für gehashte Einträge erzwingt die Datenbank eindeutig:

  • userId
  • type
  • referenceType
  • referenceId

Damit kann derselbe Wallet-Vorgang nicht versehentlich zweimal gebucht werden. Unterschiedliche Buchungstypen zur selben fachlichen Referenz bleiben erlaubt, zum Beispiel trade_purchase, trade_sale und trade_reservation_release auf derselben tradeId.

Der Index ist bewusst partiell auf neue gehashte Transaktionen begrenzt, damit historische WalletTransaction-Zeilen ohne Backfill den Deploy nicht blockieren.

applyWalletAdjustment(...) schreibt kompakte strukturierte Logs für Start, berechnete Vorher-/Nachher-Salden, erfolgreiche WalletTransaction.id und Fehlerfälle. Diese Logs dürfen keine Stripe-Rohobjekte, Secrets, E-Mail-Adressen oder Usernamen enthalten.

Ledger-Verifikation

Admins können die Hash-Kette eines Users read-only prüfen:

GET /api/functions/admin/wallet-ledger/:userId/verify

Der Endpoint berechnet die gespeicherten Hashes neu, prüft Sequenzen, previousHash, User-Head und User-Sequence und gibt ein strukturiertes Ergebnis mit ok und Fehlerliste zurück. Er verändert keine Daten und führt keinen Backfill oder Repair aus.

Die Ledger-Felder auf bestehenden WalletTransaction-Datensätzen sind nullable. Ohne Backfill beginnt die überprüfbare Kette pro User mit der ersten neuen Wallet-Transaktion nach Einführung der Migration. Ein Backfill ist nur nötig, wenn auch die historische Wallet-Historie vor dieser Migration in die kryptografische Prüfung einbezogen werden soll.

Ein Backfill sollte als separater Wartungsschritt geplant werden, idealerweise bevor neue Wallet-Buchungen auf der Zielumgebung entstehen. Ein späterer Backfill müsste bereits erzeugte Hashes und User-Heads bewusst neu aufbauen.