Files
kaffeekasse-saas/paypal-zuordnung.php
T
clemensandClaude Opus 4.8 ea7d9a4714 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>
2026-07-21 18:33:49 +02:00

143 lines
5.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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"; ?>