Files
kaffeekasse-saas/paypal-zuordnung.php
T
clemensandClaude Opus 5 6cd7fcc079 Offene PayPal-Zahlungen bleiben nach dem Abschalten erreichbar
Wird PayPal in den Mandant-Einstellungen abgeschaltet, waehrend noch
Zahlungen unzugeordnet in der Warteschlange liegen, kaeme ohne diese
Ausnahme niemand mehr an sie heran - weder ueber das Menue noch ueber die
URL.

paypal_inbox_accessible() haelt Menuepunkt und Seite deshalb offen, solange
paypal_count_unmatched() etwas findet; die Seite weist per Hinweis darauf
hin, dass sie nach dem Abarbeiten verschwindet. Eine Sperre durch den
Betreiber sticht die Ausnahme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 23:42:52 +02:00

249 lines
9.7 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);
$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' . $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"; ?>