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:
+98
-9
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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; ?>
|
||||||
|
|||||||
@@ -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 € EUR</td></tr><tr><td>Summe</td><td>{$summe} € EUR</td></tr>"
|
? "<tr><td>Gebühr</td><td>-0,35 € EUR</td></tr><tr><td>Summe</td><td>{$summe} € 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} € EUR gesendet</p>"
|
. "<p>{$payer} hat dir {$betrag} € EUR gesendet</p>"
|
||||||
|
. $adresszeile
|
||||||
. "<p>Mitteilung von {$payer} {$mitteilung}</p>"
|
. "<p>Mitteilung von {$payer} {$mitteilung}</p>"
|
||||||
. "<table><tr><td>Erhaltener Betrag</td><td>{$betrag} € EUR</td></tr>"
|
. "<table><tr><td>Erhaltener Betrag</td><td>{$betrag} € 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 € 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');
|
||||||
|
|||||||
Reference in New Issue
Block a user