Files
kaffeekasse-saas/docs/backoffice.md
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

11 KiB
Raw Blame History

Back-Office (Platform-Admin)

Stand: 2026-07-16

Ergänzung außerhalb der ursprünglichen M0M9-Meilensteine: ein Betreiber-Zugang, der mandantenübergreifend alle Kunden einsehen kann getrennt von der normalen, streng mandantengebundenen Rollenlogik.

Design-Entscheidung: getrennt von tenant_memberships

Platform-Admin-Rechte sind bewusst kein Teil von saas_user_has_role()/tenant_memberships, sondern eine eigene, komplett separate Tabelle platform_admins (user_idusers.id). Das ist kein Zufall: Alle bestehenden Mandanten-Seiten und die automatisierten Isolationstests (scripts/check-m8-tenant-isolation.php, scripts/check-m8-role-matrix.php) verlassen sich darauf, dass ein Login niemals automatisch mandantenübergreifenden Zugriff bekommt. Ein Platform-Admin-Flag direkt in tenant_memberships oder users einzubauen hätte dieses Fundament unterlaufen können. Stattdessen prüft ausschließlich der neue Code-Pfad in app/platform-admin.php (app_require_platform_admin()) auf Back-Office-Seiten die Platform-Admin-Eigenschaft; alle bestehenden Seiten sind unverändert und bleiben strikt mandantengebunden.

Umgesetzte Dateien

database/migrations/0013_saas_platform_admins.sql
app/platform-admin.php
scripts/grant-platform-admin.php
backoffice.php
backoffice-mandant.php
backoffice-export.php

Erster Platform-Admin

Es gibt bewusst keine Weboberfläche, um den ersten Platform-Admin zu setzen das wäre ein öffentlich erreichbarer "werde Admin"-Endpunkt und ein erhebliches Risiko. Stattdessen: Person registriert sich normal als Mandant (oder nutzt einen bestehenden Login), danach per Shell-Zugriff auf dem Server:

php scripts/grant-platform-admin.php person@example.com

Das Skript prüft, dass der Account existiert, und legt nur dann den platform_admins-Eintrag an. Zugang entziehen aktuell nur per direktem DELETE FROM platform_admins WHERE user_id = ? (keine UI dafür aus demselben Grund wie beim Setzen).

Funktionsumfang

  • backoffice.php: Übersicht aller Mandanten mit Kürzel, Status, Erstelldatum, Teilnehmerzahl (aktiv/gesamt) und Saldensumme.
  • backoffice-mandant.php?tenant_id=X: Ansicht eines einzelnen Mandanten Einstellungen, Mitglieder mit Rolle, letzte 20 Buchungen, letzte 20 Admin-Aktionen, dazu die Funktions-Schalter (siehe unten). Absichtlich kein Schreibzugriff auf die eigentlichen Mandantendaten (Buchungen, Mitglieder, Einstellungen des Kunden) von hier aus, um das Risiko einer versehentlichen Fremdänderung auszuschließen.
  • backoffice-export.php: nutzt dieselbe app_export_tenant_data()- Funktion wie der Selbstbedienungs-Export, aber ausgelöst durch den Platform-Admin für einen beliebigen Mandanten.

Funktions-Schalter je Mandant

Stand: 2026-08-17. Zentralstelle des Betreibers, um einzelne Funktionen pro Mandant freizuschalten oder zu sperren gepflegt in backoffice-mandant.php, umgesetzt in app/features.php und der Tabelle tenant_features (Migration 0025_tenant_features.sql).

Schaltbar sind: FAQ-Seite, Striche selbst eintragen, Kaffeeliste als PDF, PayPal-Zahlungseingang, CSV-Import, Info-Mails und Erinnerungen, Jahresabschluss, Datenexport, eigenes Design.

Design-Entscheidungen:

  • Getrennt von tenant_settings. Dort stellt der Kunde ein, wie eine Funktion arbeitet; hier entscheidet der Betreiber, ob sie ihm überhaupt zur Verfügung steht. Beide Prüfungen stehen nebeneinander, z. B. beim Selbsteintrag: self_entry_enabled (Kunde) und Feature self_entry (Betreiber).
  • Ohne Zeile = freigeschaltet. tenant_features hält nur die Abweichungen vom Standard; ein neuer Mandant bekommt automatisch den vollen Funktionsumfang, ohne dass der Betreiber etwas einschalten muss. Fehlt die Tabelle (Migration noch nicht ausgerollt), bleibt ebenfalls alles freigeschaltet eine ausstehende Migration darf keine Funktion sperren.
  • Gesperrt heißt unsichtbar. Die betroffenen Menüpunkte und Schaltflächen verschwinden (footer.php, kaffeeliste.php, einzahlung.php, konto.php); ein direkter Aufruf der Seite endet mit einem Hinweis statt mit einem Fehler. Die POST-Verarbeitung der betroffenen Seiten hängt an derselben Prüfung, eine gesperrte Funktion lässt sich also auch nicht per Formular-POST auslösen.
  • Abgeschaltet vom Kunden heißt ebenfalls unsichtbar. Für Funktionen mit eigenem Schalter in den Mandant-Einstellungen zieht footer.php (und die Anleitung) app_feature_available() heran: erst die Freischaltung durch den Betreiber, dann der Schalter des Kunden. Die Zuordnung steht in app_feature_tenant_switch() aktuell paypal_inboxpaypal_enabled und self_entryself_entry_enabled. Wer PayPal nicht als Zahlungsweg anbietet, sieht den Menüpunkt „PayPal-Zahlungen" also gar nicht erst; beim direkten Aufruf erscheint statt des Betreiber-Hinweises app_feature_tenant_notice_html() mit dem Weg zurück in die Mandant-Einstellungen.
  • Ausnahme: offene Warteschlange. Schaltet ein Mandant PayPal ab, während noch nicht zugeordnete Zahlungen liegen, wären diese weder über Menü noch über URL erreichbar. paypal_inbox_accessible() (app/paypal-inbox.php) hält Menüpunkt und Seite deshalb offen, solange paypal_count_unmatched() etwas findet mit einem Hinweis auf der Seite. Ist die Warteschlange abgearbeitet, schließt sie sich von selbst. Eine Sperre durch den Betreiber sticht auch diese Ausnahme.
  • Jede Änderung landet als backoffice.features_updated im Audit-Log des betroffenen Mandanten wie jeder andere Back-Office-Zugriff auch.

Das Back-Office ist außerdem jetzt für Platform-Admins im Menü verlinkt (Gruppe „Betreiber" in footer.php); vorher war es nur über die direkte URL erreichbar.

Eigenes Design je Mandant

Stand: 2026-08-17, hängt am Feature branding. Der Kunde hinterlegt in mandant-einstellungen.php eine Akzentfarbe und ein Logo:

  • Die Farbe (tenant_settings.brand_color, validiert als #rrggbb) wird in header.php als kleines Stylesheet eingebettet (app/branding.php), das die Akzentfarbe des Templates überschreibt. Eine eigene CSS-Datei je Mandant wäre ein zusätzlicher Request für wenige Zeilen.
  • Das Logo (tenant_settings.brand_logo) liegt wie das PDF-Wasserzeichen in var/tenant_logos und ist damit nicht direkt per URL abrufbar sonst wäre jedes Kundenlogo unter einer ratbaren Adresse öffentlich. Ausgeliefert wird es über tenant-logo-anzeigen.php, das ausschließlich das Logo des Mandanten des angemeldeten Nutzers ausgibt.
  • Wasserzeichen (Ausdruck) und Marken-Logo (Oberfläche) sind getrennte Dateien mit eigenem Namenspräfix (logo_ bzw. brand_).
  • Sperrt der Betreiber branding, verschwindet der Abschnitt aus den Einstellungen und die Oberfläche fällt aufs Standarddesign zurück. Die gespeicherte Farbe bleibt erhalten und gilt nach einer erneuten Freischaltung wieder.

Verhältnis zum Selbstbedienungs-Export (datenexport.php)

Bewusste Entscheidung: Der bestehende Selbstbedienungs-Export für Mandanten-Owner/Admin (datenexport.php, aus M8) bleibt zusätzlich bestehen, statt ihn durch das Back-Office zu ersetzen. Begründung: Der Mandant ist im Auftragsverarbeitungs-Verhältnis der Verantwortliche (Controller), der Betreiber dieser App der Auftragsverarbeiter (Processor). Art. 15/20 DSGVO geben den betroffenen Personen ein Auskunfts-/Portabilitätsrecht, und Art. 28 DSGVO verpflichtet den Auftragsverarbeiter, den Verantwortlichen bei der Erfüllung dieser Rechte zu unterstützen sowie Daten am Vertragsende zurückzugeben ein jederzeit verfügbarer Selbstbedienungs-Export erfüllt genau das. Das Back-Office ergänzt das um einen Betreiber-seitigen Zugriff für Support, Migrationen oder eigene Nachweispflichten, ersetzt den Mandanten-Selbstexport aber nicht.

Audit-Trail

Jede Back-Office-Ansicht und jeder Back-Office-Export wird im Audit-Log des betroffenen Mandanten protokolliert (backoffice.tenant_viewed, backoffice.tenant_exported, mit dem Platform-Admin als actor_user_id). Ein Mandant sieht damit über sein eigenes Protokoll (mandant-einstellungen.php), wann der Betreiber auf seine Daten zugegriffen hat Transparenzpflicht statt stiller Einsicht.

Prüfstatus

  • scripts/check-m8-tenant-isolation.php: weiterhin grün mit 10 Assertions das Back-Office berührt die geprüften Pfade nicht.
  • scripts/check-m8-role-matrix.php: weiterhin grün mit 55 Assertions.
  • HTTP-Smoke: grün mit 30 geprüften Seiten (zwei neue Login-Schutz-Checks für backoffice.php/backoffice-mandant.php).
  • Golden Master: grün mit 104 Assertions.

Live getestet: eigens angelegter Test-Mandant, per CLI-Skript zum Platform-Admin gemacht, Back-Office-Übersicht zeigt korrekt alle Mandanten (inklusive der echten Bestandsmandanten), Detailansicht und Export für den eigenen Test-Mandanten funktionieren, Audit-Log-Einträge korrekt geschrieben. Kritischer Isolationstest bestätigt: derselbe Platform-Admin sieht auf regulären Mandanten-Seiten (kaffeeliste.php) weiterhin ausschließlich seinen eigenen Mandanten. Ein zweiter, eingeloggter, aber nicht privilegierter Testnutzer bekommt auf backoffice.php korrekt 403 Forbidden. Alle Testdaten anschließend vollständig entfernt.

Eingehende PayPal-Mails bei abgeschalteter Funktion

Stand: 2026-08-21. Die Mail-Verarbeitung (scripts/fetch-paypal-payments.phppaypal_reconcile()) prüft dieselben zwei Ebenen wie das Menü. Ist PayPal für den Mandanten abgeschaltet — vom Betreiber (paypal_inbox) oder vom Mandanten selbst (paypal_enabled) —, wird eine eingehende Zahlung geparkt: sie wird gespeichert (Status unmatched, Ergebnis parked), aber nicht automatisch gutgeschrieben.

  • Warum nicht wegwerfen: eine echte Zahlung würde unbemerkt verschwinden; das Geld ist ja trotzdem angekommen.
  • Warum nicht buchen: es soll gerade nichts über PayPal laufen — die Entscheidung gehört dem Mandanten.
  • Die Zuordnungsseite bleibt für offene Zahlungen erreichbar (siehe paypal_inbox_accessible()), der Mandant sieht die geparkte Zahlung also und kann sie zuordnen oder abhaken.

PayPal-Name lernen

Die automatische Zuordnung verlangt exakte Übereinstimmung des Zahlernamens mit display_name oder paypal_name des Mitglieds (imports_find_participant(), genau ein Treffer). Heißt jemand bei PayPal anders — Zweitname, Firmenkonto, anderer Nachname —, landet jede Zahlung in der Warteschlange, auch die zehnte.

paypal_assign_payment() hinterlegt den Zahlernamen deshalb beim Mitglied, sobald eine Zahlung von Hand zugeordnet wurde; die nächste Zahlung desselben Zahlers wird automatisch gebucht. Bewusst zurückhaltend (paypal_remember_payer_name()): ein bereits gepflegter PayPal-Name wird nie überschrieben, ein Name gleich dem Anzeigenamen bringt nichts, und passt der Name auch auf ein anderes Mitglied, wird nichts gelernt — sonst wäre die Zuordnung mehrdeutig und würde gar nicht mehr greifen. Was gelernt wurde, steht in der Erfolgsmeldung und im Audit-Log (paypal_import.manual_assigned, Feld learned_paypal_name).