Automatische Verbuchung von PayPal-Zahlungen per Mail-Weiterleitung
Mandanten leiten ihre PayPal-Benachrichtigungsmails an ein zentrales IMAP-Postfach weiter. Die Zuordnung zum Mandanten erfolgt ueber Plus-Adressierung (zahlungen+<token>@...), der Token wird pro Mandant erzeugt und im Backend angezeigt. - Parser fuer PayPal-Eingangsmails, tolerant gegenueber falsch kodierten Waehrungssymbolen und HTML-Struktur weitergeleiteter Mails - Gutgeschrieben wird der Nettobetrag, also was tatsaechlich ankam (bei Waren & Dienstleistungen nach Abzug der PayPal-Gebuehr) - Deduplizierung ueber den Transaktionscode, damit doppelt weitergeleitete Mails nicht doppelt buchen - Eindeutiger Namens-Match bucht automatisch, alles andere landet in einer Warteschlange zur manuellen Zuordnung - Absenderpruefung gegen paypal.de/.com, da weitergeleitete Mails keine gueltige SPF/DKIM-Signatur mehr haben - IMAP-Zugang ausschliesslich ueber Server-/Env-Einstellungen, nicht pro Mandant konfigurierbar Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
include "functions.php";
|
||||
require_once __DIR__ . "/app/ledger.php";
|
||||
require_once __DIR__ . "/app/paypal-inbox.php";
|
||||
app_require_csrf();
|
||||
|
||||
$pdo = app_db_pdo();
|
||||
$saasUser = saas_current_user($pdo);
|
||||
$tenantId = 0;
|
||||
$hasAccess = false;
|
||||
|
||||
if ($saasUser !== null && saas_user_has_role(['owner', 'admin', 'treasurer'], $saasUser)) {
|
||||
$tenantId = (int)$saasUser['tenant_id'];
|
||||
$hasAccess = true;
|
||||
}
|
||||
|
||||
if (!$hasAccess && $saasUser === null && checkKaffeelisteAdmin($conn, $mailadress)) {
|
||||
$tenant = ledger_fetch_default_tenant($pdo);
|
||||
if ($tenant !== null) {
|
||||
$tenantId = (int)$tenant['id'];
|
||||
$hasAccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
$flash = null;
|
||||
if ($hasAccess && isset($_SESSION['flash_paypal'])) {
|
||||
$flash = $_SESSION['flash_paypal'];
|
||||
unset($_SESSION['flash_paypal']);
|
||||
}
|
||||
|
||||
if ($hasAccess && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$aktion = (string)($_POST['aktion'] ?? '');
|
||||
$paymentId = (int)($_POST['payment_id'] ?? 0);
|
||||
$actorUserId = $saasUser['user_id'] ?? null;
|
||||
|
||||
if ($aktion === 'zuordnen') {
|
||||
$participantId = (int)($_POST['participant_id'] ?? 0);
|
||||
if ($participantId <= 0) {
|
||||
$flash = ['type' => 'error', 'text' => 'Bitte ein Mitglied auswählen.'];
|
||||
} else {
|
||||
$result = paypal_assign_payment($pdo, $tenantId, $paymentId, $participantId, $actorUserId);
|
||||
$flash = $result['ok']
|
||||
? ['type' => 'success', 'text' => 'Zahlung wurde zugeordnet und als Einzahlung gebucht.']
|
||||
: ['type' => 'error', 'text' => $result['error'] ?? 'Die Zuordnung ist fehlgeschlagen.'];
|
||||
}
|
||||
} elseif ($aktion === 'ignorieren') {
|
||||
$result = paypal_ignore_payment($pdo, $tenantId, $paymentId, $actorUserId);
|
||||
$flash = $result['ok']
|
||||
? ['type' => 'success', 'text' => 'Zahlung wurde als erledigt markiert.']
|
||||
: ['type' => 'error', 'text' => $result['error'] ?? 'Die Aktion ist fehlgeschlagen.'];
|
||||
}
|
||||
|
||||
// Post-Redirect-Get gegen Doppelbuchung per Refresh.
|
||||
$_SESSION['flash_paypal'] = $flash;
|
||||
header('Location: paypal-zuordnung.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$inboxAddress = null;
|
||||
$offene = [];
|
||||
$mitglieder = [];
|
||||
if ($hasAccess) {
|
||||
$token = paypal_inbox_ensure_token($pdo, $tenantId);
|
||||
$inboxAddress = paypal_inbox_address_for_token($token);
|
||||
$offene = paypal_fetch_unmatched($pdo, $tenantId);
|
||||
$mitglieder = ledger_fetch_participant_summaries($pdo, $tenantId, ['active_only' => true]);
|
||||
}
|
||||
|
||||
include "header.php";
|
||||
include "headerline.php";
|
||||
include "nav.php";
|
||||
?>
|
||||
|
||||
<section id="banner">
|
||||
<div class="content">
|
||||
|
||||
<?php if (!$hasAccess): ?>
|
||||
<h2>Kein Zugriff</h2>
|
||||
<?php else: ?>
|
||||
|
||||
<h2>PayPal-Zahlungen zuordnen</h2>
|
||||
|
||||
<?php if ($flash !== null): ?>
|
||||
<div class="hint-box <?php echo $flash['type'] === 'success' ? 'success' : 'error'; ?>"><p><?php echo saas_html($flash['text']); ?></p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3>So richtet ihr die automatische Verbuchung ein</h3>
|
||||
<?php if ($inboxAddress !== null): ?>
|
||||
<p>Leitet die PayPal-Benachrichtigungsmails („Du hast eine Zahlung erhalten") automatisch an eure persönliche Eingangsadresse weiter:</p>
|
||||
<p><b><?php echo saas_html($inboxAddress); ?></b></p>
|
||||
<p>Eingehende Zahlungen werden dann automatisch dem passenden Mitglied gutgeschrieben (gebuchter Betrag = tatsächlich eingegangener Betrag nach PayPal-Gebühr). Zahlungen, die sich nicht eindeutig zuordnen lassen, erscheinen unten zur manuellen Zuordnung.</p>
|
||||
<p><small>Tipp: Am zuverlässigsten trefft ihr die Zuordnung, wenn beim Mitglied der PayPal-Name hinterlegt ist (unter „Mitglieder verwalten").</small></p>
|
||||
<?php else: ?>
|
||||
<div class="hint-box error"><p>Die zentrale Eingangsadresse ist noch nicht konfiguriert. Bitte wende dich an den Betreiber (Server-Einstellung <code>PAYPAL_INBOX_BASE</code>).</p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h3>Offene Zahlungen (<?php echo count($offene); ?>)</h3>
|
||||
<?php if ($offene === []): ?>
|
||||
<p>Keine offenen Zahlungen. Alles automatisch zugeordnet. 🎉</p>
|
||||
<?php else: ?>
|
||||
<table>
|
||||
<tr><th>Eingang</th><th>Zahler (PayPal)</th><th>Mitteilung</th><th>Betrag</th><th>Zuordnen</th><th></th></tr>
|
||||
<?php foreach ($offene as $z): ?>
|
||||
<tr>
|
||||
<td><?php echo saas_html((string)($z['paid_at'] ?? $z['created_at'])); ?></td>
|
||||
<td><?php echo saas_html($z['payer_name']); ?></td>
|
||||
<td><?php echo saas_html((string)($z['note'] ?? '')); ?></td>
|
||||
<td><?php echo saas_html(saas_format_money_cents((int)$z['net_cents'])); ?> €</td>
|
||||
<td>
|
||||
<form method="post" action="paypal-zuordnung.php" style="display:flex;gap:0.5em;align-items:center;margin:0">
|
||||
<?php echo app_csrf_field(); ?>
|
||||
<input type="hidden" name="aktion" value="zuordnen">
|
||||
<input type="hidden" name="payment_id" value="<?php echo (int)$z['id']; ?>">
|
||||
<select name="participant_id" required>
|
||||
<option value="">– Mitglied wählen –</option>
|
||||
<?php foreach ($mitglieder as $m): ?>
|
||||
<option value="<?php echo (int)$m['participant_id']; ?>"><?php echo saas_html($m['display_name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit">Buchen</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="paypal-zuordnung.php" style="margin:0" onsubmit="return confirm('Diese Zahlung als erledigt/ignoriert markieren?');">
|
||||
<?php echo app_csrf_field(); ?>
|
||||
<input type="hidden" name="aktion" value="ignorieren">
|
||||
<input type="hidden" name="payment_id" value="<?php echo (int)$z['id']; ?>">
|
||||
<button type="submit" class="alt">Ignorieren</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include "footer.php"; ?>
|
||||
Reference in New Issue
Block a user