PayPal-Zahlungen auch ueber die Mailadresse zuordnen

Bisher verglich die Zuordnung ausschliesslich Namen: der Parser las die
Adresse des Zahlers gar nicht aus, und die Suche kannte nur paypal_name und
display_name. Eine Zahlung von genau der Adresse, die beim Mitglied
hinterlegt ist, blieb deshalb liegen.

Die Adresse wird jetzt ausgelesen (neue Spalte paypal_payments.payer_email),
in der Warteschlange angezeigt und zuerst geprueft - sie ist im Mandanten
eindeutig und aendert sich nicht, wenn jemand bei PayPal anders heisst.

Der heikle Teil ist das Aussortieren: In einer weitergeleiteten Mail stehen
mehrere Adressen. Die falsche zu nehmen wuerde fremdes Geld dem Mitglied
hinter dieser Adresse gutschreiben - typischerweise dem Kassenwart. Deshalb
fallen paypal.*-Adressen und die eigene Eingangsadresse raus (geprueft
gegen die tatsaechliche Empfaengeradresse, Plus-Adressierung ignoriert),
und es zaehlt nur eine Adresse in unmittelbarer Naehe des Zahlernamens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 22:17:48 +02:00
co-authored by Claude Opus 5
parent 25bdb6f34b
commit d7de0d92cf
6 changed files with 241 additions and 10 deletions
+98 -9
View File
@@ -124,14 +124,23 @@ function paypal_booking_note(array $payment): string
} }
/** /**
* Sucht den passenden Teilnehmer zu einer PayPal-Zahlung: zuerst ueber den * Sucht den passenden Teilnehmer zu einer PayPal-Zahlung: zuerst ueber die
* Zahlernamen (paypal_name/display_name), sonst ueber die Mitteilung (Mitglied * Mailadresse des Zahlers, dann ueber den Zahlernamen
* schreibt dort teils seinen Namen). Liefert null, wenn nicht eindeutig. * (paypal_name/display_name), zuletzt ueber die Mitteilung (Mitglied schreibt
* dort teils seinen Namen). Liefert null, wenn nicht eindeutig.
* *
* @return array{participant_id:int, display_name:string}|null * @return array{participant_id:int, display_name:string}|null
*/ */
function paypal_match_participant(PDO $pdo, int $tenantId, string $payerName, ?string $note): ?array function paypal_match_participant(PDO $pdo, int $tenantId, string $payerName, ?string $note, ?string $payerEmail = null): ?array
{ {
// Die Mailadresse zuerst: sie ist im Mandanten eindeutig (Unique Key auf
// tenant_id + email_norm) und aendert sich nicht, wenn jemand bei PayPal
// unter einem anderen Namen auftritt.
$byEmail = paypal_find_participant_by_email($pdo, $tenantId, $payerEmail);
if ($byEmail !== null) {
return $byEmail;
}
$byName = imports_find_participant($pdo, $tenantId, $payerName); $byName = imports_find_participant($pdo, $tenantId, $payerName);
if ($byName !== null) { if ($byName !== null) {
return $byName; return $byName;
@@ -143,6 +152,64 @@ function paypal_match_participant(PDO $pdo, int $tenantId, string $payerName, ?s
return null; return null;
} }
/**
* Gehoeren zwei Adressen zum selben Postfach? Plus-Adressierung wird dabei
* ignoriert, "zahlungen+ab12cd@host" ist also dasselbe Postfach wie
* "zahlungen@host". $kandidat darf mehrere Adressen enthalten (die
* Empfaengerzeilen einer weitergeleiteten Mail).
*/
function paypal_same_mailbox(string $adresse, string $kandidat): bool
{
$adresse = strtolower(trim($adresse));
if ($adresse === '' || !str_contains($adresse, '@')) {
return false;
}
[$lokal, $domain] = explode('@', $adresse, 2);
$lokal = explode('+', $lokal)[0];
if (!preg_match_all('/[\w.+-]+@[\w-]+(?:\.[\w-]+)+/u', strtolower($kandidat), $treffer)) {
return false;
}
foreach ($treffer[0] as $andere) {
[$andereLokal, $andereDomain] = explode('@', $andere, 2);
if (explode('+', $andereLokal)[0] === $lokal && $andereDomain === $domain) {
return true;
}
}
return false;
}
/**
* Mitglied ueber seine Mailadresse finden. Null, wenn keine Adresse vorliegt
* oder kein Mitglied dazu passt.
*/
function paypal_find_participant_by_email(PDO $pdo, int $tenantId, ?string $email): ?array
{
$email = strtolower(trim((string) $email));
if ($email === '') {
return null;
}
$stmt = $pdo->prepare(
'SELECT id, display_name
FROM participants
WHERE tenant_id = ? AND email_norm = ?
ORDER BY id
LIMIT 2'
);
$stmt->execute([$tenantId, $email]);
$rows = $stmt->fetchAll();
if (count($rows) !== 1) {
return null;
}
return [
'participant_id' => (int) $rows[0]['id'],
'display_name' => (string) $rows[0]['display_name'],
];
}
/** /**
* Verarbeitet eine geparste PayPal-Zahlung fuer einen Mandanten: * Verarbeitet eine geparste PayPal-Zahlung fuer einen Mandanten:
* - Dedup ueber den Transaktionscode (bereits verarbeitet -> uebersprungen). * - Dedup ueber den Transaktionscode (bereits verarbeitet -> uebersprungen).
@@ -168,13 +235,14 @@ function paypal_reconcile(PDO $pdo, int $tenantId, array $parsed, ?int $actorUse
// Dedup: Transaktionscode ist global eindeutig. INSERT IGNORE als Schranke. // Dedup: Transaktionscode ist global eindeutig. INSERT IGNORE als Schranke.
$insert = $pdo->prepare( $insert = $pdo->prepare(
'INSERT IGNORE INTO paypal_payments 'INSERT IGNORE INTO paypal_payments
(tenant_id, transaction_code, payer_name, note, gross_cents, fee_cents, net_cents, paid_at, status) (tenant_id, transaction_code, payer_name, payer_email, note, gross_cents, fee_cents, net_cents, paid_at, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
); );
$insert->execute([ $insert->execute([
$tenantId, $tenantId,
$code, $code,
(string) ($parsed['payer_name'] ?? ''), (string) ($parsed['payer_name'] ?? ''),
(string) ($parsed['payer_email'] ?? ''),
$parsed['note'] ?? null, $parsed['note'] ?? null,
(int) ($parsed['gross_cents'] ?? 0), (int) ($parsed['gross_cents'] ?? 0),
$parsed['fee_cents'] ?? null, $parsed['fee_cents'] ?? null,
@@ -199,7 +267,13 @@ function paypal_reconcile(PDO $pdo, int $tenantId, array $parsed, ?int $actorUse
} }
// Eindeutiger Match -> automatisch buchen. // Eindeutiger Match -> automatisch buchen.
$participant = paypal_match_participant($pdo, $tenantId, (string) ($parsed['payer_name'] ?? ''), $parsed['note'] ?? null); $participant = paypal_match_participant(
$pdo,
$tenantId,
(string) ($parsed['payer_name'] ?? ''),
$parsed['note'] ?? null,
$parsed['payer_email'] ?? null
);
if ($participant === null) { if ($participant === null) {
return ['status' => 'unmatched', 'payment_id' => $paymentId]; return ['status' => 'unmatched', 'payment_id' => $paymentId];
} }
@@ -263,7 +337,13 @@ function paypal_preview(PDO $pdo, int $tenantId, array $parsed): array
return ['status' => 'would_park', 'net_cents' => $netCents]; return ['status' => 'would_park', 'net_cents' => $netCents];
} }
$participant = paypal_match_participant($pdo, $tenantId, (string) ($parsed['payer_name'] ?? ''), $parsed['note'] ?? null); $participant = paypal_match_participant(
$pdo,
$tenantId,
(string) ($parsed['payer_name'] ?? ''),
$parsed['note'] ?? null,
$parsed['payer_email'] ?? null
);
if ($participant === null) { if ($participant === null) {
return ['status' => 'would_queue', 'net_cents' => $netCents]; return ['status' => 'would_queue', 'net_cents' => $netCents];
} }
@@ -302,6 +382,15 @@ function paypal_process_raw(PDO $pdo, string $recipient, string $fromHeader, str
return ['status' => 'not_a_payment']; return ['status' => 'not_a_payment'];
} }
// Die eigene Eingangsadresse steht in jeder weitergeleiteten Mail und darf
// niemals als Adresse des Zahlers durchgehen - sonst wuerde eine Zahlung
// dem Mitglied gutgeschrieben, dem diese Adresse gehoert. Die Pruefung
// haengt bewusst an der tatsaechlichen Empfaengeradresse und nicht nur an
// der Konfiguration.
if (paypal_same_mailbox((string) ($parsed['payer_email'] ?? ''), $recipient)) {
$parsed['payer_email'] = null;
}
$result = $previewOnly $result = $previewOnly
? paypal_preview($pdo, $tenantId, $parsed) ? paypal_preview($pdo, $tenantId, $parsed)
: paypal_reconcile($pdo, $tenantId, $parsed); : paypal_reconcile($pdo, $tenantId, $parsed);
@@ -318,7 +407,7 @@ function paypal_process_raw(PDO $pdo, string $recipient, string $fromHeader, str
function paypal_fetch_unmatched(PDO $pdo, int $tenantId): array function paypal_fetch_unmatched(PDO $pdo, int $tenantId): array
{ {
$stmt = $pdo->prepare( $stmt = $pdo->prepare(
"SELECT id, transaction_code, payer_name, note, net_cents, paid_at, created_at "SELECT id, transaction_code, payer_name, payer_email, note, net_cents, paid_at, created_at
FROM paypal_payments FROM paypal_payments
WHERE tenant_id = ? AND status = 'unmatched' WHERE tenant_id = ? AND status = 'unmatched'
ORDER BY created_at DESC, id DESC" ORDER BY created_at DESC, id DESC"
+59
View File
@@ -103,11 +103,69 @@ function paypal_extract_labeled_amount(string $text, string $labelRegex): ?int
return null; return null;
} }
/**
* Mailadresse des Zahlers aus der Benachrichtigung.
*
* Bewusst eng gefasst: In einer weitergeleiteten Mail stehen mehrere
* Adressen - die von PayPal selbst, die Eingangsadresse der Kaffeeliste und
* je nach Weiterleitung auch die des urspruenglichen Empfaengers. Wuerde
* einfach die erste beste genommen, koennte eine Zahlung beim Kassenwart
* statt beim Zahler landen. Genommen wird deshalb nur eine Adresse, die
* nahe am Namen des Zahlers steht (PayPal schreibt sie dort hin), und auch
* die erst nach Abzug aller bekannten Fremdadressen.
*
* @param string $text Bereits per paypal_mail_to_text() normalisierter Text
* @param string $payer Erkannter Name des Zahlers
*/
function paypal_extract_payer_email(string $text, string $payer): ?string
{
if (!preg_match_all('/[\w.+-]+@[\w-]+(?:\.[\w-]+)+/u', $text, $treffer, PREG_OFFSET_CAPTURE)) {
return null;
}
// Eingangsadresse der Kaffeeliste (zahlungen+<token>@…) und alles von
// PayPal selbst scheiden aus.
// Der Parser wird auch aus Testskripten ohne geladene Umgebung benutzt.
$inboxBase = function_exists('app_env') ? (string) (app_env('PAYPAL_INBOX_BASE') ?? '') : '';
$inboxLokalteil = $inboxBase !== '' ? strtolower(explode('@', $inboxBase)[0]) : '';
$namePosition = stripos($text, $payer);
$kandidaten = [];
foreach ($treffer[0] as [$adresse, $position]) {
$adresse = strtolower(rtrim($adresse, '.'));
$domain = substr($adresse, strpos($adresse, '@') + 1);
$lokalteil = substr($adresse, 0, strpos($adresse, '@'));
if (preg_match('/(^|\.)paypal\.[a-z.]+$/', $domain)) {
continue;
}
if ($inboxLokalteil !== '' && str_starts_with($lokalteil, $inboxLokalteil)) {
continue;
}
$abstand = $namePosition === false ? PHP_INT_MAX : abs($position - $namePosition);
$kandidaten[$adresse] = min($kandidaten[$adresse] ?? PHP_INT_MAX, $abstand);
}
if ($kandidaten === []) {
return null;
}
asort($kandidaten);
$beste = array_key_first($kandidaten);
// Steht keine Adresse in der Naehe des Zahlernamens, ist die Zuordnung zu
// unsicher - dann lieber keine liefern als die falsche.
return $kandidaten[$beste] <= 400 ? $beste : null;
}
/** /**
* Parst eine PayPal-Zahlungseingangs-Mail. * Parst eine PayPal-Zahlungseingangs-Mail.
* *
* @return array{ * @return array{
* payer_name: string, * payer_name: string,
* payer_email: ?string,
* transaction_code: ?string, * transaction_code: ?string,
* gross_cents: int, * gross_cents: int,
* fee_cents: ?int, * fee_cents: ?int,
@@ -163,6 +221,7 @@ function paypal_parse_notification(string $body): ?array
return [ return [
'payer_name' => $payer, 'payer_name' => $payer,
'payer_email' => paypal_extract_payer_email($text, $payer),
'transaction_code' => $code, 'transaction_code' => $code,
'gross_cents' => $grossCents, 'gross_cents' => $grossCents,
'fee_cents' => $feeCents, 'fee_cents' => $feeCents,
@@ -0,0 +1,8 @@
-- Mailadresse des Zahlers aus der PayPal-Benachrichtigung.
--
-- Die automatische Zuordnung ging bisher nur ueber den Namen; die Adresse
-- ist das deutlich verlaesslichere Merkmal (Mitglieder sind im Mandanten
-- ueber tenant_id + email_norm eindeutig). Leer, wenn die Mail keine
-- brauchbare Adresse enthaelt - dann bleibt es beim Namensvergleich.
ALTER TABLE paypal_payments
ADD COLUMN payer_email VARCHAR(255) NOT NULL DEFAULT '' AFTER payer_name;
+28
View File
@@ -222,3 +222,31 @@ Name auch auf ein anderes Mitglied, wird nichts gelernt — sonst wäre die
Zuordnung mehrdeutig und würde gar nicht mehr greifen. Was gelernt wurde, Zuordnung mehrdeutig und würde gar nicht mehr greifen. Was gelernt wurde,
steht in der Erfolgsmeldung und im Audit-Log steht in der Erfolgsmeldung und im Audit-Log
(`paypal_import.manual_assigned`, Feld `learned_paypal_name`). (`paypal_import.manual_assigned`, Feld `learned_paypal_name`).
### Zuordnung über die Mailadresse
Stand: 2026-08-21. `paypal_match_participant()` prüft in dieser Reihenfolge:
1. **Mailadresse des Zahlers** gegen `participants.email_norm` — im
Mandanten eindeutig (Unique Key auf `tenant_id + email_norm`) und
unabhängig davon, wie jemand bei PayPal heißt.
2. Zahlername gegen `paypal_name`/`display_name` (genau ein Treffer).
3. Mitteilung des Zahlers gegen dieselben Felder.
Die Adresse zieht `paypal_extract_payer_email()` aus der Mail und
`paypal_payments.payer_email` (Migration `0028`) hält sie fest; in der
Warteschlange steht sie unter dem Zahlernamen.
Das Heikle daran ist nicht das Finden, sondern das Aussortieren: In einer
weitergeleiteten Mail stehen mehrere Adressen — die von PayPal, die
Eingangsadresse der Kaffeeliste, je nach Weiterleitung die des
ursprünglichen Empfängers. Würde davon die falsche genommen, bekäme das
Mitglied hinter dieser Adresse fremdes Geld gutgeschrieben. Deshalb:
- alles auf `paypal.*` fällt raus,
- die eigene Eingangsadresse fällt raus — geprüft gegen die tatsächliche
Empfängeradresse der Mail (`paypal_same_mailbox()`, Plus-Adressierung
wird ignoriert), nicht nur gegen `PAYPAL_INBOX_BASE`,
- übrig bleibt nur eine Adresse, die **nahe am Namen des Zahlers** steht;
ist keine in Reichweite, wird keine genommen und es bleibt beim
Namensvergleich.
+4
View File
@@ -90,6 +90,7 @@ function paypal_payment_matches_query(array $payment, string $query): bool
$text = imports_normalize_match_text( $text = imports_normalize_match_text(
(string)$payment['payer_name'] . ' ' (string)$payment['payer_name'] . ' '
. (string)($payment['payer_email'] ?? '') . ' '
. (string)($payment['note'] ?? '') . ' ' . (string)($payment['note'] ?? '') . ' '
. (string)($payment['transaction_code'] ?? '') . (string)($payment['transaction_code'] ?? '')
); );
@@ -206,6 +207,9 @@ include "nav.php";
<td><?php echo saas_html((string)($z['paid_at'] ?? $z['created_at'])); ?></td> <td><?php echo saas_html((string)($z['paid_at'] ?? $z['created_at'])); ?></td>
<td> <td>
<strong><?php echo saas_html($z['payer_name']); ?></strong> <strong><?php echo saas_html($z['payer_name']); ?></strong>
<?php if (trim((string)($z['payer_email'] ?? '')) !== ''): ?>
<br><small><?php echo saas_html((string)$z['payer_email']); ?></small>
<?php endif; ?>
<?php if (($z['note'] ?? '') !== null && trim((string)$z['note']) !== ''): ?> <?php if (($z['note'] ?? '') !== null && trim((string)$z['note']) !== ''): ?>
<br><small><?php echo saas_html((string)$z['note']); ?></small> <br><small><?php echo saas_html((string)$z['note']); ?></small>
<?php endif; ?> <?php endif; ?>
+44 -1
View File
@@ -41,15 +41,18 @@ function paypal_flow_assert(string $label, bool $condition, array &$failures, in
* ankommt: HTML mit Detail-Link (daraus kommt der Transaktionscode) und den * ankommt: HTML mit Detail-Link (daraus kommt der Transaktionscode) und den
* Labels, an denen der Parser sich orientiert. * Labels, an denen der Parser sich orientiert.
*/ */
function paypal_flow_mail(string $payer, string $betrag, string $code, string $mitteilung, ?string $summe = null): string function paypal_flow_mail(string $payer, string $betrag, string $code, string $mitteilung, ?string $summe = null, ?string $payerEmail = null): string
{ {
$summeZeile = $summe !== null $summeZeile = $summe !== null
? "<tr><td>Gebühr</td><td>-0,35&nbsp;&euro; EUR</td></tr><tr><td>Summe</td><td>{$summe}&nbsp;&euro; EUR</td></tr>" ? "<tr><td>Gebühr</td><td>-0,35&nbsp;&euro; EUR</td></tr><tr><td>Summe</td><td>{$summe}&nbsp;&euro; EUR</td></tr>"
: ''; : '';
// PayPal schreibt die Adresse des Zahlers direkt neben seinen Namen.
$adresszeile = $payerEmail !== null ? "<p>{$payer} ({$payerEmail})</p>" : '';
return '<html><body>' return '<html><body>'
. '<p>Hallo,</p>' . '<p>Hallo,</p>'
. "<p>{$payer} hat dir {$betrag}&nbsp;&euro; EUR gesendet</p>" . "<p>{$payer} hat dir {$betrag}&nbsp;&euro; EUR gesendet</p>"
. $adresszeile
. "<p>Mitteilung von {$payer} {$mitteilung}</p>" . "<p>Mitteilung von {$payer} {$mitteilung}</p>"
. "<table><tr><td>Erhaltener Betrag</td><td>{$betrag}&nbsp;&euro; EUR</td></tr>" . "<table><tr><td>Erhaltener Betrag</td><td>{$betrag}&nbsp;&euro; EUR</td></tr>"
. $summeZeile . $summeZeile
@@ -129,6 +132,46 @@ try {
paypal_flow_assert('unbekannter Zahler landet in der Warteschlange', ($ergebnisB['status'] ?? '') === 'unmatched', $failures, $passes); 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); 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(
'<html><body><p>Unbekannt Zahler hat dir 1,00&nbsp;&euro; EUR gesendet</p>'
. '<p>service@paypal.de zahlungen+abc123@kaffeeliste.de</p>'
. '<p><a href="https://www.paypal.com/activities/details/TXNONLYFOREIGN01">Details</a></p></body></html>'
);
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 // --- Der PayPal-Name wird aus der manuellen Zuordnung gelernt: beim
// naechsten Mal soll dieselbe Person automatisch getroffen werden. // naechsten Mal soll dieselbe Person automatisch getroffen werden.
$abweichend = paypal_flow_participant($pdo, $tenantA, "Bernd Beispiel {$namensSuffix}", $suffix . 'x'); $abweichend = paypal_flow_participant($pdo, $tenantA, "Bernd Beispiel {$namensSuffix}", $suffix . 'x');