Sicherheits-/Korrektheits-Fixes und neue Funktionen
Bugfixes aus dem Code-Review: - XSS: unescaptes $_SERVER['PHP_SELF'] in csvupload.php und letzteneintraege.php durch feste Seitennamen ersetzt. - Stripe-Webhook: Event-Deduplizierung (neue Tabelle stripe_webhook_events) gegen doppelte Verarbeitung/Dolibarr-Rechnungen bei Retry-Zustellung. - Stripe-Webhook: Tarifwechsel aus dem Kundenportal wird lokal nachgezogen (plan_code aus dem Preis-lookup_key bei subscription.updated). - Post-Redirect-Get fuer jahresauswertung, mailversenden und die Selbst-Stricheintragung - verhindert Doppelbuchung/Doppelversand per Browser-Refresh. - Jahresbonus: Mails erst nach erfolgreichem Commit; Restcent-Ausgleich beim letzten Empfaenger, damit die Summe exakt stimmt. - Verschachtelte HTML-Dokumente in csvupload/einzahlung/stricheintragen entfernt (Layout kommt aus header.php). - Rate-Limit fuer den Versand von E-Mail-Verifizierungslinks. Neue Funktionen: - Mitglieder koennen ihren zuletzt selbst eingetragenen Strich wieder stornieren (nur eigene Web-Eintraege, Kassenwart-Eintraege bleiben). - Monatsuebersicht des eigenen Verbrauchs im Mitglieder-Dashboard. - Automatische Zahlungserinnerung: opt-in pro Mandant ab der Warnschwelle, mit Intervall; Cron-Skript scripts/send-payment-reminders.php. - CSV-Import fuer Mitgliederlisten inkl. herunterladbarer Vorlage (mitglieder-vorlage.php), Semikolon-/Komma- und BOM-Erkennung. - Logo als PDF-Wasserzeichen pro Mandant (Upload in den Mandant- Einstellungen, geschuetzt unter var/tenant_logos/, ersetzt den Text-Wasserzeichen im Ausdruck). Robusterer Teilnehmer-Lookup im Dashboard ueber user_id (Fallback E-Mail).
This commit is contained in:
@@ -621,6 +621,60 @@ function ledger_void_entry_by_legacy_id(PDO $pdo, int $tenantId, string $legacyT
|
||||
return $stmt->rowCount() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storniert den juengsten, noch nicht stornierten Web-Selbsteintrag eines
|
||||
* Teilnehmers (Quelle 'self_entry' bei SaaS-nativen Mandanten bzw. 'legacy_web'
|
||||
* beim migrierten Default-Mandanten). Bewusst nur eigene Web-Eintraege - vom
|
||||
* Kassenwart per Sammelerfassung gesetzte Striche ('manual_bulk'/'legacy_manual')
|
||||
* bleiben unangetastet. Beim Default-Mandanten wird zusaetzlich die
|
||||
* kl_Kaffeeverbrauch-Zeile geloescht, damit sie nicht erneut gespiegelt wird
|
||||
* und auf den Legacy-Sammelseiten verschwindet.
|
||||
*
|
||||
* @return array{ok: bool, marks?: int, error?: string}
|
||||
*/
|
||||
function ledger_void_own_self_entry(PDO $pdo, int $tenantId, int $participantId): array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT id, marks_count, legacy_table, legacy_id
|
||||
FROM ledger_entries
|
||||
WHERE tenant_id = ?
|
||||
AND participant_id = ?
|
||||
AND type = 'consumption'
|
||||
AND voided_at IS NULL
|
||||
AND source IN ('self_entry', 'legacy_web')
|
||||
ORDER BY booked_at DESC, id DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
$stmt->execute([$tenantId, $participantId]);
|
||||
$entry = $stmt->fetch();
|
||||
|
||||
if ($entry === false) {
|
||||
return ['ok' => false, 'error' => 'Es gibt keinen selbst eingetragenen Strich, der storniert werden könnte.'];
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
if ($entry['legacy_table'] === 'kl_Kaffeeverbrauch' && $entry['legacy_id'] !== null) {
|
||||
$pdo->prepare('DELETE FROM kl_Kaffeeverbrauch WHERE VerbrauchID = ?')
|
||||
->execute([(int)$entry['legacy_id']]);
|
||||
}
|
||||
|
||||
$pdo->prepare("UPDATE ledger_entries SET voided_at = NOW() WHERE id = ? AND voided_at IS NULL")
|
||||
->execute([(int)$entry['id']]);
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
|
||||
return ['ok' => false, 'error' => 'Der Strich konnte nicht storniert werden.'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'marks' => (int)($entry['marks_count'] ?? 0)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a consumption entry straight into the ledger, without any legacy
|
||||
* kl_Kaffeeverbrauch row. Used for tenants that have no legacy shadow table
|
||||
@@ -709,6 +763,40 @@ function ledger_fetch_participants_by_window_marks(PDO $pdo, int $tenantId, int
|
||||
*
|
||||
* @return list<array{id: int, tenant_id: int, participant_id: int, display_name: string, type: string, amount_cents: int, marks_count: ?int, unit_price_cents: ?int, booked_at: string, source: string, note: ?string, legacy_table: ?string, legacy_id: ?int, voided_at: ?string}>
|
||||
*/
|
||||
/**
|
||||
* Verbrauch eines Teilnehmers pro Monat eines Jahres (nur nicht stornierte
|
||||
* Consumption-Buchungen). Fuer die Monatsuebersicht im Mitglieder-Dashboard.
|
||||
*
|
||||
* @return array<int, array{marks: int, cost_cents: int}> Schluessel 1..12
|
||||
*/
|
||||
function ledger_fetch_monthly_consumption(PDO $pdo, int $tenantId, int $participantId, int $year): array
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT MONTH(booked_at) AS m,
|
||||
COALESCE(SUM(marks_count), 0) AS marks,
|
||||
COALESCE(SUM(-amount_cents), 0) AS cost_cents
|
||||
FROM ledger_entries
|
||||
WHERE tenant_id = ?
|
||||
AND participant_id = ?
|
||||
AND type = 'consumption'
|
||||
AND voided_at IS NULL
|
||||
AND YEAR(booked_at) = ?
|
||||
GROUP BY MONTH(booked_at)"
|
||||
);
|
||||
$stmt->execute([$tenantId, $participantId, $year]);
|
||||
|
||||
$months = [];
|
||||
for ($m = 1; $m <= 12; $m++) {
|
||||
$months[$m] = ['marks' => 0, 'cost_cents' => 0];
|
||||
}
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$m = (int)$row['m'];
|
||||
$months[$m] = ['marks' => (int)$row['marks'], 'cost_cents' => (int)$row['cost_cents']];
|
||||
}
|
||||
|
||||
return $months;
|
||||
}
|
||||
|
||||
function ledger_fetch_recent_entries(PDO $pdo, int $tenantId, array $options = []): array
|
||||
{
|
||||
$limit = isset($options['limit']) ? (int)$options['limit'] : 100;
|
||||
|
||||
Reference in New Issue
Block a user