PayPal-Mailabruf: Parken statt Buchen, echter Probelauf, Regressionstest
Die Mail-Verarbeitung ignorierte bisher die PayPal-Schalter: eingehende Zahlungen wurden auch dann automatisch gutgeschrieben, wenn der Betreiber die Funktion gesperrt oder der Mandant PayPal abgeschaltet hatte. Jetzt wird die Zahlung in dem Fall geparkt - gespeichert, aber ohne Buchung. Wegwerfen liesse eine echte Zahlung unbemerkt verschwinden, buchen widersprache der Abschaltung; die Zuordnungsseite bleibt fuer offene Zahlungen ja erreichbar. --dry-run war bisher irrefuehrend: es liess die Verarbeitung samt Buchung laufen und uebersprang nur das Setzen des Gelesen-Flags - ausgerechnet beim ersten Testlauf haette es also echtes Geld verbucht. Der Probelauf nutzt jetzt paypal_preview(), das nichts schreibt und meldet, was passieren wuerde (would_book/would_queue/would_park/duplicate). scripts/check-paypal-inbox-flow.php deckt die Kette ohne IMAP ab: Absenderpruefung, Token, Parser, Zuordnung, Netto-Buchung, Dedup, beide Park-Faelle und die Schreibfreiheit der Vorschau. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+61
-2
@@ -148,6 +148,8 @@ function paypal_match_participant(PDO $pdo, int $tenantId, string $payerName, ?s
|
||||
* - Dedup ueber den Transaktionscode (bereits verarbeitet -> uebersprungen).
|
||||
* - Eindeutiger Match -> automatisch als Netto-Einzahlung gebucht.
|
||||
* - Kein eindeutiger Match -> in der Warteschlange ('unmatched') abgelegt.
|
||||
* - PayPal fuer den Mandanten abgeschaltet -> geparkt ('parked'): gespeichert,
|
||||
* aber ohne automatische Buchung.
|
||||
*
|
||||
* @param array $parsed Ergebnis von paypal_parse_notification()
|
||||
* @return array{status:string, payment_id?:int, participant?:string}
|
||||
@@ -185,6 +187,17 @@ function paypal_reconcile(PDO $pdo, int $tenantId, array $parsed, ?int $actorUse
|
||||
}
|
||||
$paymentId = (int) $pdo->lastInsertId();
|
||||
|
||||
// Ist PayPal fuer diesen Mandanten abgeschaltet - vom Betreiber oder vom
|
||||
// Mandanten selbst -, wird die Zahlung nur geparkt: gespeichert, aber
|
||||
// nicht automatisch gutgeschrieben. Wegwerfen waere schlechter (eine
|
||||
// echte Zahlung ginge unbemerkt verloren), automatisch buchen ebenfalls
|
||||
// (es soll ja gerade nichts ueber PayPal laufen). Die Zuordnungsseite
|
||||
// bleibt fuer offene Zahlungen erreichbar, dort entscheidet der Mandant.
|
||||
require_once __DIR__ . '/features.php';
|
||||
if (!app_feature_available($pdo, $tenantId, 'paypal_inbox')) {
|
||||
return ['status' => 'parked', 'payment_id' => $paymentId];
|
||||
}
|
||||
|
||||
// Eindeutiger Match -> automatisch buchen.
|
||||
$participant = paypal_match_participant($pdo, $tenantId, (string) ($parsed['payer_name'] ?? ''), $parsed['note'] ?? null);
|
||||
if ($participant === null) {
|
||||
@@ -218,6 +231,50 @@ function paypal_reconcile(PDO $pdo, int $tenantId, array $parsed, ?int $actorUse
|
||||
return ['status' => 'booked', 'payment_id' => $paymentId, 'participant' => (string) $participant['display_name']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vorschau ohne jeden Schreibzugriff: was wuerde mit dieser Zahlung
|
||||
* passieren? Gedacht fuer den Probelauf des Abrufskripts (--dry-run). Der
|
||||
* Probelauf soll wirklich nichts anfassen - eine Vorschau, die im
|
||||
* Hintergrund bucht, waere schlimmer als gar keine.
|
||||
*
|
||||
* @param array $parsed Ergebnis von paypal_parse_notification()
|
||||
* @return array{status:string, participant?:string, net_cents?:int}
|
||||
*/
|
||||
function paypal_preview(PDO $pdo, int $tenantId, array $parsed): array
|
||||
{
|
||||
$code = (string) ($parsed['transaction_code'] ?? '');
|
||||
if ($code === '') {
|
||||
return ['status' => 'no_code'];
|
||||
}
|
||||
$netCents = (int) ($parsed['net_cents'] ?? 0);
|
||||
if ($netCents <= 0) {
|
||||
return ['status' => 'no_amount'];
|
||||
}
|
||||
|
||||
// Dedup wie beim echten Lauf: der Transaktionscode ist global eindeutig.
|
||||
$stmt = $pdo->prepare('SELECT id FROM paypal_payments WHERE transaction_code = ? LIMIT 1');
|
||||
$stmt->execute([$code]);
|
||||
if ($stmt->fetchColumn() !== false) {
|
||||
return ['status' => 'duplicate'];
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/features.php';
|
||||
if (!app_feature_available($pdo, $tenantId, 'paypal_inbox')) {
|
||||
return ['status' => 'would_park', 'net_cents' => $netCents];
|
||||
}
|
||||
|
||||
$participant = paypal_match_participant($pdo, $tenantId, (string) ($parsed['payer_name'] ?? ''), $parsed['note'] ?? null);
|
||||
if ($participant === null) {
|
||||
return ['status' => 'would_queue', 'net_cents' => $netCents];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'would_book',
|
||||
'participant' => (string) $participant['display_name'],
|
||||
'net_cents' => $netCents,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet eine rohe PayPal-Mail vollstaendig: Tenant per Plus-Token,
|
||||
* Absenderpruefung, Parsing, Abgleich. Fuer den IMAP-Cron und Tests.
|
||||
@@ -227,7 +284,7 @@ function paypal_reconcile(PDO $pdo, int $tenantId, array $parsed, ?int $actorUse
|
||||
* @param string $rawBody (dekodierter) Mail-Body
|
||||
* @return array{status:string, tenant_id?:int}
|
||||
*/
|
||||
function paypal_process_raw(PDO $pdo, string $recipient, string $fromHeader, string $rawBody): array
|
||||
function paypal_process_raw(PDO $pdo, string $recipient, string $fromHeader, string $rawBody, bool $previewOnly = false): array
|
||||
{
|
||||
if (!preg_match('/@paypal\.(de|com)/i', $fromHeader)) {
|
||||
return ['status' => 'not_from_paypal'];
|
||||
@@ -245,7 +302,9 @@ function paypal_process_raw(PDO $pdo, string $recipient, string $fromHeader, str
|
||||
return ['status' => 'not_a_payment'];
|
||||
}
|
||||
|
||||
$result = paypal_reconcile($pdo, $tenantId, $parsed);
|
||||
$result = $previewOnly
|
||||
? paypal_preview($pdo, $tenantId, $parsed)
|
||||
: paypal_reconcile($pdo, $tenantId, $parsed);
|
||||
$result['tenant_id'] = $tenantId;
|
||||
|
||||
return $result;
|
||||
|
||||
Reference in New Issue
Block a user