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.netaus 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) inSTRIPE_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 PostgreSQLSELECT ... FOR UPDATEgesperrt. ledgerSequencewird ausUser.walletLedgerSequence + 1gebildet.previousHashübernimmt den bisherigenUser.walletLedgerHeadHash.transactionHashwird deterministisch mit SHA-256 aus den relevanten Buchungsdaten berechnet.User.walletLedgerHeadHashundUser.walletLedgerSequencewerden 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:
userIdtypereferenceTypereferenceId
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.