Files
kaffeekasse-saas/paypal-zuordnung.php
T
clemensandClaude Opus 5 d7de0d92cf 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>
2026-08-21 22:17:48 +02:00

261 lines
10 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";
require_once __DIR__ . "/app/features.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;
}
// Gesperrte Funktion: $hasAccess faellt zurueck, damit auch die
// POST-Verarbeitung weiter unten nicht mehr laeuft. Der Menuepunkt ist in
// diesem Fall ausgeblendet - die Seite bleibt aber ueber die direkte URL
// erreichbar und muss deshalb selbst pruefen.
$sperrHinweis = null;
$restarbeitHinweis = false;
if ($hasAccess && !app_feature_enabled($pdo, $tenantId, 'paypal_inbox')) {
// Der Betreiber hat die Funktion fuer diesen Mandanten nicht freigeschaltet.
$sperrHinweis = app_feature_notice_html('paypal_inbox');
$hasAccess = false;
} elseif ($hasAccess && !app_feature_available($pdo, $tenantId, 'paypal_inbox')) {
// Der Mandant selbst bietet PayPal nicht mehr als Zahlungsweg an. Offene
// Zahlungen muessen trotzdem noch zugeordnet werden koennen - erst mit
// leerer Warteschlange schliesst die Seite.
if (paypal_count_unmatched($pdo, $tenantId) > 0) {
$restarbeitHinweis = true;
} else {
$sperrHinweis = app_feature_tenant_notice_html('paypal_inbox');
$hasAccess = false;
}
}
$flash = null;
if ($hasAccess && isset($_SESSION['flash_paypal'])) {
$flash = $_SESSION['flash_paypal'];
unset($_SESSION['flash_paypal']);
}
$suchbegriff = trim((string)($_SERVER['REQUEST_METHOD'] === 'POST' ? ($_POST['return_q'] ?? '') : ($_GET['q'] ?? '')));
$returnQuery = $suchbegriff !== '' ? '?q=' . urlencode($suchbegriff) : '';
function paypal_select_options(array $members, array $suggestions = [], int $selectedId = 0): string
{
$suggestedIds = [];
foreach ($suggestions as $suggestion) {
$suggestedIds[(int)$suggestion['participant_id']] = true;
}
$html = '<option value="">- Mitglied wählen -</option>';
if ($suggestions !== []) {
$html .= '<optgroup label="Vorschläge">';
foreach ($suggestions as $suggestion) {
$id = (int)$suggestion['participant_id'];
$html .= '<option value="' . $id . '"' . ($selectedId === $id ? ' selected' : '') . '>'
. saas_html((string)$suggestion['display_name'])
. '</option>';
}
$html .= '</optgroup>';
}
$html .= '<optgroup label="Alle aktiven Mitglieder">';
foreach ($members as $member) {
$id = (int)$member['participant_id'];
if (isset($suggestedIds[$id])) {
continue;
}
$html .= '<option value="' . $id . '"' . ($selectedId === $id ? ' selected' : '') . '>'
. saas_html((string)$member['display_name'])
. '</option>';
}
$html .= '</optgroup>';
return $html;
}
function paypal_payment_matches_query(array $payment, string $query): bool
{
$query = imports_normalize_match_text($query);
if ($query === '') {
return true;
}
$text = imports_normalize_match_text(
(string)$payment['payer_name'] . ' '
. (string)($payment['payer_email'] ?? '') . ' '
. (string)($payment['note'] ?? '') . ' '
. (string)($payment['transaction_code'] ?? '')
);
return str_contains($text, $query);
}
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);
$erfolgstext = 'Zahlung wurde zugeordnet und als Einzahlung gebucht.';
// Wurde dabei der PayPal-Name gelernt, gehoert das in die Meldung -
// sonst wirkt die stillschweigende Aenderung am Mitglied wie Magie.
if (($result['learned_paypal_name'] ?? null) !== null) {
$erfolgstext .= ' Der PayPal-Name „' . $result['learned_paypal_name']
. '" ist jetzt bei diesem Mitglied hinterlegt künftige Zahlungen'
. ' werden automatisch zugeordnet.';
}
$flash = $result['ok']
? ['type' => 'success', 'text' => $erfolgstext]
: ['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' . $returnQuery);
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]);
}
$offeneGefiltert = array_values(array_filter($offene, static fn(array $payment): bool => paypal_payment_matches_query($payment, $suchbegriff)));
include "header.php";
include "headerline.php";
include "nav.php";
?>
<section id="banner">
<div class="content">
<?php if ($sperrHinweis !== null): ?>
<?php echo $sperrHinweis; ?>
<?php elseif (!$hasAccess): ?>
<h2>Kein Zugriff</h2>
<?php else: ?>
<h2>PayPal-Zahlungen zuordnen</h2>
<?php if ($restarbeitHinweis): ?>
<div class="hint-box warning"><p><b>PayPal ist in euren Mandant-Einstellungen deaktiviert.</b><br>
Diese Seite bleibt geöffnet, bis die unten stehenden Zahlungen zugeordnet oder abgehakt sind.
Danach verschwindet sie aus dem Menü bis PayPal unter
<a href="mandant-einstellungen.php">Mandant-Einstellungen</a> wieder eingeschaltet wird.</p></div>
<?php endif; ?>
<?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>
<form method="get" action="paypal-zuordnung.php" class="admin-filter-bar">
<label for="paypal-q">Suche</label>
<input type="search" name="q" id="paypal-q" value="<?php echo saas_html($suchbegriff); ?>" placeholder="Zahler, Mitteilung oder Transaktion">
<button type="submit">Filtern</button>
<a class="button alt" href="paypal-zuordnung.php">Zurücksetzen</a>
</form>
<?php if ($offene === []): ?>
<p>Keine offenen Zahlungen. Alles automatisch zugeordnet.</p>
<?php elseif ($offeneGefiltert === []): ?>
<p>Keine offenen Zahlungen für diesen Filter gefunden.</p>
<?php else: ?>
<p><?php echo count($offeneGefiltert); ?> von <?php echo count($offene); ?> offenen Zahlungen angezeigt.</p>
<div class="table-wrapper">
<table class="admin-table">
<tr><th>Eingang</th><th>Zahlung</th><th>Betrag</th><th>Vorschläge</th><th>Zuordnen</th><th></th></tr>
<?php foreach ($offeneGefiltert as $z): ?>
<?php
$suggestions = imports_suggest_participants($pdo, $tenantId, (string)$z['payer_name'], (string)($z['note'] ?? ''), 4);
$selectedId = count($suggestions) === 1 ? (int)$suggestions[0]['participant_id'] : 0;
?>
<tr>
<td><?php echo saas_html((string)($z['paid_at'] ?? $z['created_at'])); ?></td>
<td>
<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']) !== ''): ?>
<br><small><?php echo saas_html((string)$z['note']); ?></small>
<?php endif; ?>
<br><small>Transaktion: <?php echo saas_html((string)$z['transaction_code']); ?></small>
</td>
<td><?php echo saas_html(saas_format_money_cents((int)$z['net_cents'])); ?> €</td>
<td>
<?php if ($suggestions === []): ?>
<span class="status-badge muted">Kein Vorschlag</span>
<?php else: ?>
<?php foreach ($suggestions as $suggestion): ?>
<span class="status-badge info"><?php echo saas_html((string)$suggestion['display_name']); ?></span>
<?php endforeach; ?>
<?php endif; ?>
</td>
<td>
<form method="post" action="paypal-zuordnung.php" class="inline-admin-form">
<?php echo app_csrf_field(); ?>
<input type="hidden" name="aktion" value="zuordnen">
<input type="hidden" name="payment_id" value="<?php echo (int)$z['id']; ?>">
<input type="hidden" name="return_q" value="<?php echo saas_html($suchbegriff); ?>">
<select name="participant_id" required>
<?php echo paypal_select_options($mitglieder, $suggestions, $selectedId); ?>
</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']; ?>">
<input type="hidden" name="return_q" value="<?php echo saas_html($suchbegriff); ?>">
<button type="submit" class="alt">Ignorieren</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</section>
<?php include "footer.php"; ?>