Files
kaffeekasse-saas/paypal-zuordnung.php
T
clemensandClaude Opus 5 25bdb6f34b PayPal-Name aus der manuellen Zuordnung lernen
Die automatische Zuordnung verlangt exakte Uebereinstimmung des
Zahlernamens mit Anzeigename oder PayPal-Name des Mitglieds. Heisst jemand
bei PayPal anders, landet jede Zahlung in der Warteschlange - auch die
zehnte, obwohl der Fall laengst einmal von Hand geklaert wurde.

Nach einer manuellen Zuordnung wird der Zahlername deshalb beim Mitglied
hinterlegt. Zurueckhaltend: ein gepflegter PayPal-Name wird nie
ueberschrieben, ein Name gleich dem Anzeigenamen bringt nichts, und passt
der Name auch auf ein anderes Mitglied, wird nichts gelernt - das wuerde
die Zuordnung mehrdeutig machen und damit blockieren.

Was gelernt wurde, steht in der Erfolgsmeldung und im Audit-Log; eine
stillschweigende Aenderung am Mitglied waere sonst schwer nachvollziehbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:03:12 +02:00

257 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['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 (($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"; ?>