{$payer} ({$payerEmail})
" : ''; return '' . 'Hallo,
' . "{$payer} hat dir {$betrag} € EUR gesendet
" . $adresszeile . "Mitteilung von {$payer} {$mitteilung}
" . "| Erhaltener Betrag | {$betrag} € EUR |
| Transaktionsdatum | 21. August 2026 |
Transaktionscode ' . $code . '
' . ''; } $pdo = dev_pdo(); $suffix = bin2hex(random_bytes(4)); // Fuer Personennamen: der Parser erkennt nur Buchstaben, Punkt, Apostroph und // Bindestrich als Namensbestandteile - ein Testname mit Ziffern wuerde die // Mail faelschlich als "keine Zahlung" durchfallen lassen. $namensSuffix = strtr($suffix, '0123456789', 'abcdefghij'); $tenantIds = []; function paypal_flow_tenant(PDO $pdo, string $suffix, string $key, array &$tenantIds): int { $pdo->prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)') ->execute(["ppflow-{$key}-{$suffix}", "PayPal Flow {$key}", 'active']); $tenantId = (int) $pdo->lastInsertId(); $tenantIds[] = $tenantId; $pdo->prepare('INSERT INTO tenant_settings (tenant_id, paypal_enabled) VALUES (?, 1)')->execute([$tenantId]); return $tenantId; } function paypal_flow_participant(PDO $pdo, int $tenantId, string $name, string $suffix): int { $mail = strtolower(str_replace(' ', '-', $name)) . "-{$suffix}@test.local"; $pdo->prepare('INSERT INTO participants (tenant_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, 1)') ->execute([$tenantId, $name, $mail, $mail]); return (int) $pdo->lastInsertId(); } function paypal_flow_balance(PDO $pdo, int $participantId): int { $stmt = $pdo->prepare('SELECT COALESCE(SUM(amount_cents), 0) FROM ledger_entries WHERE participant_id = ?'); $stmt->execute([$participantId]); return (int) $stmt->fetchColumn(); } try { // --- Fall 1: alles freigeschaltet, Name eindeutig -> automatisch buchen $tenantA = paypal_flow_tenant($pdo, $suffix, 'a', $tenantIds); $participantA = paypal_flow_participant($pdo, $tenantA, "Erika Musterfrau {$namensSuffix}", $suffix); $tokenA = paypal_inbox_ensure_token($pdo, $tenantA); $empfaengerA = "zahlungen+{$tokenA}@kaffeeliste.de"; $codeA = 'TXNAUTO' . strtoupper(bin2hex(random_bytes(4))); $mailA = paypal_flow_mail("Erika Musterfrau {$namensSuffix}", '12,50', $codeA, 'Kaffeekasse', '12,15'); // Erst die Vorschau: sie darf nichts anlegen. $vorschau = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailA, true); paypal_flow_assert('Vorschau meldet die geplante Buchung', ($vorschau['status'] ?? '') === 'would_book', $failures, $passes); paypal_flow_assert('Vorschau nennt das Mitglied', str_contains((string) ($vorschau['participant'] ?? ''), 'Erika'), $failures, $passes); $zahlungenNachVorschau = (int) $pdo->query('SELECT COUNT(*) FROM paypal_payments')->fetchColumn(); $ergebnisA = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailA); paypal_flow_assert('eindeutiger Name wird automatisch gebucht', ($ergebnisA['status'] ?? '') === 'booked', $failures, $passes); paypal_flow_assert('Vorschau hat nichts gespeichert', $zahlungenNachVorschau + 1 === (int) $pdo->query('SELECT COUNT(*) FROM paypal_payments')->fetchColumn(), $failures, $passes); // Gebucht wird die "Summe" (nach Gebuehr), nicht der Sendebetrag. paypal_flow_assert('gebucht wird der Netto-Betrag (12,15 EUR)', paypal_flow_balance($pdo, $participantA) === 1215, $failures, $passes); // --- Dedup: dieselbe Mail ein zweites Mal $ergebnisDoppelt = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailA); paypal_flow_assert('dieselbe Transaktion wird nicht doppelt gebucht', ($ergebnisDoppelt['status'] ?? '') === 'duplicate', $failures, $passes); paypal_flow_assert('Guthaben bleibt nach der Dublette unveraendert', paypal_flow_balance($pdo, $participantA) === 1215, $failures, $passes); // --- Fall 2: unbekannter Zahler -> Warteschlange $codeB = 'TXNQUEUE' . strtoupper(bin2hex(random_bytes(4))); $mailB = paypal_flow_mail('Unbekannter Zahler', '5,00', $codeB, 'ohne Zuordnung'); $ergebnisB = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailB); paypal_flow_assert('unbekannter Zahler landet in der Warteschlange', ($ergebnisB['status'] ?? '') === 'unmatched', $failures, $passes); paypal_flow_assert('Warteschlange enthaelt die Zahlung', paypal_count_unmatched($pdo, $tenantA) === 1, $failures, $passes); // --- Zuordnung ueber die Mailadresse: der Zahler heisst bei PayPal ganz // anders, benutzt aber die Adresse, die beim Mitglied hinterlegt ist. $perMailId = paypal_flow_participant($pdo, $tenantA, "Karin Kassierer {$namensSuffix}", $suffix . 'm'); $perMailAdresse = strtolower("karin-kassierer-{$namensSuffix}") . '-' . $suffix . 'm@test.local'; $codeM = 'TXNMAIL' . strtoupper(bin2hex(random_bytes(4))); $mailM = paypal_flow_mail('Voellig Anderer Name', '6,00', $codeM, 'per Adresse', null, $perMailAdresse); $ergebnisM = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailM); paypal_flow_assert('Zahlung wird ueber die Mailadresse zugeordnet', ($ergebnisM['status'] ?? '') === 'booked', $failures, $passes); paypal_flow_assert('Betrag landet beim Mitglied mit dieser Adresse', paypal_flow_balance($pdo, $perMailId) === 600, $failures, $passes); // Die Adresse wird auch gespeichert, damit in der Warteschlange sichtbar // ist, worauf die Zuordnung beruht. $gespeicherteAdresse = $pdo->prepare('SELECT payer_email FROM paypal_payments WHERE transaction_code = ?'); $gespeicherteAdresse->execute([$codeM]); paypal_flow_assert('Mailadresse des Zahlers wird gespeichert', (string) $gespeicherteAdresse->fetchColumn() === $perMailAdresse, $failures, $passes); // Fremde Adressen (PayPal selbst, eigene Eingangsadresse) duerfen nie als // Adresse des Zahlers durchgehen - sonst bekaeme das Mitglied hinter der // Eingangsadresse fremdes Geld gutgeschrieben. putenv('PAYPAL_INBOX_BASE=zahlungen@kaffeeliste.de'); $nurFremdadressen = paypal_parse_notification( 'Unbekannt Zahler hat dir 1,00 € EUR gesendet
' . 'service@paypal.de – zahlungen+abc123@kaffeeliste.de
' . '' ); paypal_flow_assert('PayPal- und Eingangsadresse werden nicht als Zahleradresse genommen', ($nurFremdadressen['payer_email'] ?? null) === null, $failures, $passes); putenv('PAYPAL_INBOX_BASE'); // Auch ohne gesetzte Basisadresse: die tatsaechliche Empfaengeradresse der // Mail wird verworfen. $codeSelf = 'TXNSELF' . strtoupper(bin2hex(random_bytes(4))); $mailSelf = paypal_flow_mail('Noch Ein Zahler', '1,00', $codeSelf, 'eigene Adresse', null, $empfaengerA); $ergebnisSelf = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailSelf); paypal_flow_assert('eigene Empfaengeradresse wird nicht zur Zahleradresse', ($ergebnisSelf['status'] ?? '') === 'unmatched', $failures, $passes); $selfAdresse = $pdo->prepare('SELECT payer_email FROM paypal_payments WHERE transaction_code = ?'); $selfAdresse->execute([$codeSelf]); paypal_flow_assert('eigene Empfaengeradresse wird nicht gespeichert', (string) $selfAdresse->fetchColumn() === '', $failures, $passes); paypal_flow_assert('Plus-Adressierung wird beim Vergleich ignoriert', paypal_same_mailbox('zahlungen@kaffeeliste.de', 'zahlungen+ab12cd@kaffeeliste.de'), $failures, $passes); paypal_flow_assert('fremde Adresse gilt nicht als eigenes Postfach', !paypal_same_mailbox('erika@example.com', 'zahlungen+ab12cd@kaffeeliste.de'), $failures, $passes); // --- Der PayPal-Name wird aus der manuellen Zuordnung gelernt: beim // naechsten Mal soll dieselbe Person automatisch getroffen werden. $abweichend = paypal_flow_participant($pdo, $tenantA, "Bernd Beispiel {$namensSuffix}", $suffix . 'x'); $codeE = 'TXNLERN' . strtoupper(bin2hex(random_bytes(4))); $zahlername = "Bernd B. Beispiel-Handel {$namensSuffix}"; $mailE = paypal_flow_mail($zahlername, '4,00', $codeE, 'erste Zahlung'); $ergebnisE = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailE); paypal_flow_assert('abweichender Zahlername landet zunaechst in der Warteschlange', ($ergebnisE['status'] ?? '') === 'unmatched', $failures, $passes); $zuordnung = paypal_assign_payment($pdo, $tenantA, (int) $ergebnisE['payment_id'], $abweichend, null); paypal_flow_assert('manuelle Zuordnung bucht die Zahlung', ($zuordnung['ok'] ?? false) === true, $failures, $passes); paypal_flow_assert('manuelle Zuordnung merkt sich den PayPal-Namen', ($zuordnung['learned_paypal_name'] ?? null) === $zahlername, $failures, $passes); $codeF = 'TXNAUTO2' . strtoupper(bin2hex(random_bytes(4))); $mailF = paypal_flow_mail($zahlername, '4,00', $codeF, 'zweite Zahlung'); $ergebnisF = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailF); paypal_flow_assert('zweite Zahlung desselben Zahlers wird automatisch gebucht', ($ergebnisF['status'] ?? '') === 'booked', $failures, $passes); paypal_flow_assert('beide Zahlungen sind gutgeschrieben', paypal_flow_balance($pdo, $abweichend) === 800, $failures, $passes); // Ein bereits gepflegter PayPal-Name wird nicht ueberschrieben. $codeG = 'TXNKEEP' . strtoupper(bin2hex(random_bytes(4))); $mailG = paypal_flow_mail('Ganz Anderer Name', '2,00', $codeG, 'fremder Name'); $ergebnisG = paypal_process_raw($pdo, $empfaengerA, 'service@paypal.de', $mailG); $zuordnungG = paypal_assign_payment($pdo, $tenantA, (int) $ergebnisG['payment_id'], $abweichend, null); paypal_flow_assert('gepflegter PayPal-Name bleibt unveraendert', ($zuordnungG['learned_paypal_name'] ?? null) === null, $failures, $passes); $gespeichert = $pdo->prepare('SELECT paypal_name FROM participants WHERE id = ?'); $gespeichert->execute([$abweichend]); paypal_flow_assert('gespeichert ist weiterhin der erste Zahlername', (string) $gespeichert->fetchColumn() === $zahlername, $failures, $passes); // --- Fall 3: Mandant hat PayPal abgeschaltet -> parken, nicht buchen $tenantB = paypal_flow_tenant($pdo, $suffix, 'b', $tenantIds); $participantB = paypal_flow_participant($pdo, $tenantB, "Max Mustermann {$namensSuffix}", $suffix); $tokenB = paypal_inbox_ensure_token($pdo, $tenantB); $empfaengerB = "zahlungen+{$tokenB}@kaffeeliste.de"; $pdo->prepare('UPDATE tenant_settings SET paypal_enabled = 0 WHERE tenant_id = ?')->execute([$tenantB]); $codeC = 'TXNPARK' . strtoupper(bin2hex(random_bytes(4))); $mailC = paypal_flow_mail("Max Mustermann {$namensSuffix}", '7,00', $codeC, 'trotzdem gezahlt'); $vorschauC = paypal_process_raw($pdo, $empfaengerB, 'service@paypal.de', $mailC, true); paypal_flow_assert('Vorschau meldet das Parken', ($vorschauC['status'] ?? '') === 'would_park', $failures, $passes); $ergebnisC = paypal_process_raw($pdo, $empfaengerB, 'service@paypal.de', $mailC); paypal_flow_assert('abgeschaltetes PayPal parkt die Zahlung', ($ergebnisC['status'] ?? '') === 'parked', $failures, $passes); paypal_flow_assert('geparkte Zahlung wird nicht gebucht', paypal_flow_balance($pdo, $participantB) === 0, $failures, $passes); paypal_flow_assert('geparkte Zahlung steht in der Warteschlange', paypal_count_unmatched($pdo, $tenantB) === 1, $failures, $passes); // --- Fall 4: Betreiber hat die Funktion gesperrt -> ebenfalls parken $tenantC = paypal_flow_tenant($pdo, $suffix, 'c', $tenantIds); paypal_flow_participant($pdo, $tenantC, "Lisa Beispiel {$namensSuffix}", $suffix); $tokenC = paypal_inbox_ensure_token($pdo, $tenantC); $pdo->prepare('INSERT INTO tenant_features (tenant_id, feature_key, enabled) VALUES (?, ?, 0)') ->execute([$tenantC, 'paypal_inbox']); $codeD = 'TXNLOCK' . strtoupper(bin2hex(random_bytes(4))); $mailD = paypal_flow_mail("Lisa Beispiel {$namensSuffix}", '3,00', $codeD, 'gesperrt'); $ergebnisD = paypal_process_raw($pdo, "zahlungen+{$tokenC}@kaffeeliste.de", 'service@paypal.de', $mailD); paypal_flow_assert('Betreiber-Sperre parkt die Zahlung ebenfalls', ($ergebnisD['status'] ?? '') === 'parked', $failures, $passes); // --- Fall 5: alles, was gar nicht erst verarbeitet werden darf $fremd = paypal_process_raw($pdo, $empfaengerA, 'no-reply@beispiel.de', $mailA); paypal_flow_assert('Mail von fremdem Absender wird abgelehnt', ($fremd['status'] ?? '') === 'not_from_paypal', $failures, $passes); // Absenderpruefung: PayPal verschickt aus mehreren Domains, eine // Lookalike-Domain darf aber nie durchgehen. foreach ([ '"PayPal"