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>
257 lines
10 KiB
PHP
257 lines
10 KiB
PHP
<?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"; ?>
|