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
+59
View File
@@ -103,11 +103,69 @@ function paypal_extract_labeled_amount(string $text, string $labelRegex): ?int
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.
*
* @return array{
* payer_name: string,
* payer_email: ?string,
* transaction_code: ?string,
* gross_cents: int,
* fee_cents: ?int,
@@ -163,6 +221,7 @@ function paypal_parse_notification(string $body): ?array
return [
'payer_name' => $payer,
'payer_email' => paypal_extract_payer_email($text, $payer),
'transaction_code' => $code,
'gross_cents' => $grossCents,
'fee_cents' => $feeCents,