Zum Hauptinhalt springen

PayPal-Zahlungsannahme

Dieser Artikel erklärt, wie SpeamCore eine PayPal-Zahlung zu einer Rechnung entgegennimmt und den zugehörigen offenen Posten automatisch ausgleicht. Er richtet sich an Administratoren und an die KI-Wissensbasis. Die operativen Seiten — die öffentliche Bezahlseite, die Rechnungsausgang-Einstellungen und die PayPal-Anbindung — verlinken auf dieses Konzept, statt die Mechanik zu wiederholen.

Das Problem

Eine versendete Rechnung bleibt so lange ein offener Posten, bis eine Zahlung eingeht und einem Beleg zugeordnet wird. Bei Banküberweisung dauert das Tage, und der Zahlungseingang muss später manuell oder über die Auto-Allocation zugeordnet werden. Wer dem Kunden eine Sofort-Zahlung per PayPal anbieten will, braucht dafür:

  • eine Seite, die der Kunde ohne Login öffnen kann,
  • eine sichere Zuordnung „dieser Zahler ↔ diese Rechnung",
  • und einen automatischen Ausgleich, damit der offene Posten ohne Nacharbeit verschwindet.

Lösung — Orders API v2 plus öffentliche Bezahlseite

Der Kunde öffnet die Bezahlseite, weist sich mit Rechnungs- und Kundennummer aus, bezahlt per PayPal, und SpeamCore bucht im Hintergrund eine Transaktion samt Zuordnung gegen die Rechnung — der offene Posten ist sofort ausgeglichen.

Architektur / Komponenten

Backend

  • PayPalConfig (paypal_configs) — REST-App-Zugangsdaten (clientId, verschlüsseltes clientSecret), environment (live/sandbox), Schalter paymentsEnabled, Gebührensätze feeRatePercent/feeFixed, Konten-Verknüpfungen (transactionAccountId, payoutBankAccountId, feeAccountId) und webhookId.
  • PayPalOrder (paypal_orders) — protokolliert jede Order: paypalOrderId, paypalCaptureId, status, amount, surchargeAmount, Beleg-Bezug (documentType/documentId) und nach dem Buchen transactionId.
  • InvoicePaymentLink (invoice_payment_links) — der öffentliche Zugriffsschlüssel: eine zufällige id (= ref in /pay/:ref) mit salesDocumentId und active-Flag.
  • publicPayment.service — autorisiert die anonyme Anfrage und liefert die Anzeige-Daten.
  • paypalOrders.service — legt die Order an, captured sie und führt das Settlement aus.
  • paypalWebhook.router — nimmt PayPal-Webhooks öffentlich entgegen (Signaturprüfung).
  • surchargeInvoice.service — erzeugt bei aktivierter Gebühren-Weitergabe die separate Gebühren-Rechnung.

Frontend

  • PublicPaymentPage — die Route /pay/:ref (layout: null, kein App-Rahmen).
  • Integrations-Manager — auf der Seite Zahlungskonten; hier werden die PayPal-Zugangsdaten hinterlegt und getestet.
  • Rechnungsausgang-Einstellungen — Seite /settings/rechnungsausgang: Bezahlseite an/aus, erlaubte Methoden, QR-Modus, Gebühren-Weitergabe.

Storage / Dauerhaftigkeit

  • Tabellen paypal_configs, paypal_orders, invoice_payment_links.
  • Das clientSecret wird verschlüsselt gespeichert und nie wieder im Klartext ausgegeben.

Wichtige Eigenschaften

Idempotenz über externalId

Das Settlement bucht eine Transaktion mit externalId = paypal:{captureId} (in Backticks, damit klar ist: der Wert enthält die Capture-ID). Vor dem Buchen prüft SpeamCore, ob bereits eine Transaktion mit dieser externalId existiert. Dadurch ist es egal, ob das Settlement durch die direkte Capture-Antwort oder durch den später eintreffenden Webhook ausgelöst wird — gebucht wird genau einmal. Dieselbe externalId liefert später auch der PayPal-Reporting-Sync zurück, sodass Zahlung, Gebühr und Auszahlung nie doppelt gezählt werden.

Automatischer Ausgleich des offenen Postens

Nach erfolgreicher Capture erzeugt settlePayment:

  1. eine Transaktion (classification: 'income') auf das PayPal-transactionAccountId,
  2. eine Zuordnung (TransactionAllocation) gegen die Rechnung, die über bookAllocation gebucht wird — der offene Posten ist damit ausgeglichen,
  3. und stempelt die Capture-ID als paypalTransactionCode auf die Original-Rechnung.

Skonto

Liegt der Zahlungszeitpunkt im offenen Skonto-Fenster, berechnet SpeamCore den rabattierten Betrag und zieht genau diesen über PayPal ein. Da weniger als der volle offene Betrag eingeht, wird die Zuordnung als Skonto-Zahlung gebucht (Abzug auf das Skonto-Konto inkl. USt-Korrektur), sodass der offene Posten trotzdem vollständig ausgeglichen wird.

Gebühren-Weitergabe (optional)

Ist in den Rechnungsausgang-Einstellungen paypalSurchargeEnabled aktiv und ein Gebühren-Ertragskonto (paypalSurchargeAccountId) hinterlegt, wird die PayPal-Gebühr als exakter Aufschlag (Gross-up aus feeRatePercent/feeFixed) zusätzlich eingezogen. Beim Settlement entsteht dafür eine separate Gebühren-Rechnung, die ebenfalls gegen die Zahlung gebucht wird. Die ursprüngliche Rechnung bleibt vom Aufschlag unberührt.

Sicherheit der öffentlichen Seite

Die Seite ist nur scheinbar offen:

  • Der ref ist eine zufällige, nicht erratbare UUID (InvoicePaymentLink.id).
  • Zusätzlich muss der Aufrufer Rechnungsnummer UND Kundennummer mitliefern (beide NumberCircleAssignment.fullNumber) — entweder eingetippt oder als Parameter aus dem Druck-QR.
  • Die ganze Seite ist über die Einstellung publicPaymentPageEnabled schaltbar; ist sie aus, antwortet der Server mit „Not found".
  • Beträge und der Beleg-Bezug werden immer serverseitig aus dem Link abgeleitet, nie vom Client übernommen. Fehler sind generisch formuliert (keine Enumeration).

Edge-Cases

Teilzahlung

Wird nicht unterstützt. Eine Order deckt immer den kompletten offenen Betrag (ggf. Skonto-rabattiert) ab.

Webhook ohne gültige Signatur

Schlägt die Offline-Signaturprüfung fehl, antwortet der Endpoint mit 401 und bucht nichts. Fehlt eine aktive Config mit webhookId, wird das Event ignoriert.

Erstattung / Rückbuchung

PAYMENT.CAPTURE.REFUNDED und PAYMENT.CAPTURE.REVERSED setzen den PayPalOrder.status auf refunded; PAYMENT.CAPTURE.DENIED auf denied. Die GL-seitige Rückabwicklung läuft über den Reporting-Sync.

Mehrere Zahlungsversuche

Jeder Versuch erzeugt eine eigene PayPalOrder. Erst eine abgeschlossene Capture löst das (idempotente) Settlement aus.

Anti-Pattern (was NICHT machen)

  • ❌ Die öffentliche Bezahlseite aktivieren, ohne unter Methoden mindestens bank oder paypal freizugeben.
  • paymentsEnabled setzen, ohne im Integrations-Manager gültige Zugangsdaten und einen Webhook zu hinterlegen.
  • ❌ Teilzahlungen über PayPal erwarten — die Order deckt stets den vollen offenen Betrag.
  • ❌ Die Gebühren-Weitergabe ohne hinterlegtes Gebühren-Ertragskonto aktivieren (der Aufschlag greift dann nicht).

KI-Chat-Anwendungen

Welche Rechnungen wurden zuletzt per PayPal bezahlt?
Aktiviere die öffentliche Bezahlseite und gib Banküberweisung und PayPal frei.

Verwandte Doku