Files
kaffeekasse-saas/docs/saas-umstrukturierungsplan.md
T
clemensandClaude Sonnet 5 536ef2ead2 M6: Jahresabschluss als generisches Feature statt AOK-spezifischem Bonus-Skript
jahresauswertung.php verband sich bisher mit fest codierten (kaputten)
Zugangsdaten selbst zur Datenbank statt ueber config.php, hatte keine
Zugriffskontrolle und kein CSRF, und verteilte bei jedem Aufruf sofort
einen hart codierten Bonus-Topf (490 Striche a 0,20 Euro) per PHPMailer
(dessen Quelldateien im Repo fehlen) mit AOK-spezifischem Mailtext.

Nach Abstimmung mit dem Kunden als generisches, mandantenfaehiges Feature
neu gebaut statt nur deaktiviert oder rein lesend umgesetzt:

- Admin gibt einen frei waehlbaren Gesamtbetrag ein, das System verteilt
  ihn proportional zu den Jahresstrichen auf alle aktiven Mitglieder.
- Standardmaessig aktive Dry-Run-Checkbox zeigt die Verteilung, ohne zu
  buchen oder Mails zu verschicken.
- Bestaetigter Lauf bucht ueber dasselbe Zweig-Muster wie ueberall
  (Default-Mandant Dual-Write, andere Mandanten ledger_record_payment)
  und verschickt personalisierte Mails ueber saas_send_mail(), protokolliert
  im outbound_emails-Versandlog.
- Zugriffskontrolle ergaenzt (owner/admin/treasurer + Legacy-Fallback).
- http-smoke.php: jahresauswertung.php jetzt regulaerer Check statt
  uebersprungenem unsicherem Aufruf; damit sind keine Seiten mehr
  uebersprungen oder als bekannter offener Punkt markiert (26/26 gruen).

Live getestet: Dry-Run mit korrekter proportionaler Verteilung (Summe
ergibt exakt den Gesamtbetrag), Live-Lauf bucht und versendet korrekt,
Testdaten anschliessend vollstaendig entfernt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 17:21:34 +02:00

775 lines
28 KiB
Markdown

# SaaS-Umstrukturierungsplan Kaffeeliste
Stand: 2026-07-13
Dieses Dokument beschreibt den geplanten Umbau der bestehenden Kaffeelisten-App zu
einer mehrkundenfähigen SaaS-Anwendung mit öffentlicher Landingpage,
Kundenregistrierung und geschützter App. Die bestehende Bedienlogik und das
visuelle Grunddesign sollen bewusst erhalten bleiben.
## Zielbild
- Mehrere Kunden können eigene Kaffeelisten betreiben.
- Jeder Kunde hat isolierte Daten, Einstellungen, Mitglieder und Buchungen.
- Neue Kunden können sich über eine öffentliche Landingpage registrieren.
- Die operative App bleibt optisch nah am aktuellen Bestand: Sidebar, Tabellen,
schlichte Formulare, HTML5-UP-Anmutung.
- Authentifizierung, Rollen, Mandantenkontext und Sicherheitsprüfungen werden
zentralisiert.
- Finanznahe Vorgänge werden nachvollziehbar und revisionsfreundlich
gespeichert.
## Nicht-Ziele für den ersten Umbau
- Kein kompletter Design-Relaunch.
- Keine verspielte Marketing-App statt der bestehenden Arbeitsoberfläche.
- Kein gleichzeitiger Neubau aller Randmodule, wenn diese für den MVP nicht
benötigt werden.
- Kein Dual-Write zwischen Legacy und neuer App als Standardbetrieb.
- Keine Mandanten-ID aus Formularfeldern oder URL-Parametern als alleinige
Sicherheitsbasis.
## Ist-Zustand
Die aktuelle App ist eine flache PHP/sqlsrv-Legacy-App im Webroot.
Wichtige Bereiche:
- `index.php`: persönliches Dashboard.
- `stricheintragen.php`: Sammelerfassung von Kaffee-Strichen.
- `einzahlung.php`: Sammelerfassung von Einzahlungen.
- `kaffeeliste.php`: Gesamtübersicht.
- `mitarbeiterverwalten.php`: Mitglieder- und Adminpflege.
- `letzteneintraege.php`: Korrektur beziehungsweise Löschung letzter Buchungen.
- `csvupload.php`: Zahlungsimport.
- `exportKaffeeliste.php`: PDF-/Listenexport.
- `mailversenden.php`: Massenmail.
- `hinweise.php`: Hinweise.
- `faq.php`: FAQ.
Zentrale technische Beobachtungen:
- Authentifizierung hängt an Windows/IIS `AUTH_USER` und LDAP.
- Die Mailadresse des Nutzers wird in `functionsLDAP.php` ermittelt.
- Rollen liegen als Boolean `admin` direkt auf `kl_Mitarbeiter`.
- Datenzugriff findet direkt in den einzelnen PHP-Seiten per `sqlsrv_query`
statt.
- Es gibt kein sichtbares Migrationssystem und kein DB-Schema im Repository.
- Fachliche Tabellen haben aktuell keinen Mandantenbezug.
- Das Design kommt vor allem aus `assets/css/main.css` und dem HTML5-UP-Layout.
- Die App-Navigation sitzt faktisch in `footer.php`; `nav.php` ist leer.
## Grundsatzentscheidung
Der Umbau sollte nicht mit einer rein kosmetischen Landingpage starten, sondern
mit einem stabilen SaaS-Fundament:
1. Bestand dokumentieren und fachliche Ergebnisse einfrieren.
2. Mandantenmodell, Auth und Rollen sauber aufbauen.
3. Bestehende Fachlogik schrittweise tenant-sicher übernehmen.
4. Landingpage und App-Shell trennen, ohne das App-Design neu zu erfinden.
So bleibt die vertraute Kaffeelisten-Oberfläche erhalten, während die
Datenbasis und Sicherheit SaaS-fähig werden.
## Zielarchitektur
### Public-Bereich
Der Public-Bereich ist ohne Login erreichbar und lädt keine Mandantendaten.
Empfohlene Routen:
- `/`: Landingpage.
- `/preise` oder später `/pricing`: optional, falls Tarife eingeführt werden.
- `/faq`: öffentliche FAQ oder FAQ-Auszug.
- `/registrieren` beziehungsweise aktuell `register.php`: Kundenregistrierung.
- `/login` beziehungsweise aktuell `login.php`: Login.
- `/passwort-vergessen` beziehungsweise aktuell `passwort-vergessen.php`:
Passwort-Reset.
Landingpage-Inhalte:
- Hero mit Name `Kaffeeliste` und kurzer Nutzenbeschreibung.
- Drei Kernabläufe: Kaffee nehmen, Strich setzen, bei Bedarf bezahlen.
- Kurzer Blick auf Funktionen: Mitglieder, Guthaben, Einzahlungen, Export,
Hinweise.
- App-Screenshot oder Demo-Ansicht im bestehenden Stil.
- FAQ-Auszug.
- Call-to-Action zu Registrierung und Login.
### Geschützte App
Die geschützte App bleibt die operative Arbeitsoberfläche.
Empfohlene Routen:
- `/app`: Dashboard oder Weiterleitung auf `/app/dashboard`.
- `/app/dashboard`: Meine Kaffeeliste.
- `/app/striche`: Eigene Striche erfassen, falls erlaubt.
- `/app/einzahlungen`: Zahlungen und PayPal-Optionen.
- `/app/mitglieder`: Mitgliederverwaltung.
- `/app/liste`: Gesamtübersicht.
- `/app/buchungen`: Letzte Einträge und Korrekturen.
- `/app/importe`: CSV-Importe.
- `/app/export/pdf`: PDF-/Listenexport.
- `/app/hinweise`: Hinweise.
- `/app/einstellungen`: Tenant-Einstellungen.
Die App sollte eine Sidebar behalten, aber besser gruppiert werden:
- Persönlich: Meine Kaffeeliste, Namensanpassung, FAQ.
- Erfassung: Striche, Einzahlungen.
- Auswertung: Kaffeeliste, Buchungen, Export.
- Administration: Mitglieder, Hinweise, Einstellungen, Importe.
- Konto: Kundenkonto und Logout. Public-Links wie Login und Registrierung
gehören nicht in die App-Sidebar.
### Webspace- und Host-Strategie
Empfohlen für den Start:
- `kaffeeliste.de` oder `www.kaffeeliste.de` für Landingpage, Registrierung
und Login.
- `app.kaffeeliste.de` für die geschützte App.
- Keine Wildcard-Subdomains für Kunden.
- Mandantenauswahl nach Login über Session und `tenant_memberships`.
- Kundeneigene feste Domains oder Subdomains erst später gezielt einrichten,
wenn DNS und Zertifikat pro Domain sauber geprüft sind.
Damit bleibt der Betrieb webspace-tauglich: Für den Start reichen zwei feste
Hosts mit normalen Let's-Encrypt-Zertifikaten. Die fachliche Tenant-Auflösung
ist in M3 vorbereitet; produktive Host-Rewrites, Cookie-Domain und Zertifikats-
Details gehören zur M8-Betriebshärtung.
## Datenmodell
Empfohlen wird ein Shared-Database/Shared-Schema-Modell mit verpflichtendem
`tenant_id` auf allen fachlichen Tabellen.
Kernschema:
```text
tenants(
id, slug, name, status, timezone, locale, currency_code,
created_at, updated_at
)
tenant_settings(
tenant_id, mark_price_cents, self_entry_enabled, paypal_enabled,
paypal_url_template, sheet_window_days, negative_warning_cents,
positive_highlight_cents, branding_json, updated_by_user_id
)
users(
id, email_norm, password_hash, name, email_verified_at,
mfa_enabled, disabled_at, last_login_at, created_at, updated_at
)
tenant_memberships(
id, tenant_id, user_id, role, status, invited_at, joined_at,
created_at, updated_at,
UNIQUE(tenant_id, user_id)
)
participants(
id, tenant_id, user_id NULL, display_name, email_norm NULL,
paypal_name NULL, active, legacy_mitarbeiter_id,
created_at, updated_at,
UNIQUE(tenant_id, email_norm)
)
ledger_entries(
id, tenant_id, participant_id, type, amount_cents, marks_count,
unit_price_cents, booked_at, source, note, created_by_user_id,
import_batch_id NULL, legacy_table, legacy_id,
voided_at NULL, reversal_of_entry_id NULL,
created_at
)
notices(
id, tenant_id, message, valid_from, valid_until,
created_by_user_id, deleted_at, created_at, updated_at
)
payment_import_batches(
id, tenant_id, original_filename, checksum, status,
created_by_user_id, created_at
)
payment_import_rows(
id, batch_id, row_number, participant_id NULL, raw_name,
amount_cents, booked_at, status, raw_json
)
audit_log(
id, tenant_id, actor_user_id, action, subject_type, subject_id,
metadata_json, ip, created_at
)
outbound_emails(
id, tenant_id, participant_id, template, subject, status,
sent_at, error, created_at
)
```
Wichtige Modellierungsregeln:
- `users` sind Login-Konten.
- `participants` sind Kaffee-Teilnehmer.
- Ein Teilnehmer kann, muss aber nicht, ein Login-Konto haben.
- Rollen gehören in `tenant_memberships`, nicht direkt auf Teilnehmer.
- Buchungen sollten nicht hart gelöscht werden.
- Korrekturen laufen über Storno- oder Reversal-Einträge.
- Legacy-IDs werden bei Migration gespeichert, damit Ergebnisse vergleichbar
bleiben.
- Geldwerte sollten als Cent-Integer gespeichert werden, nicht als Float.
## Rollenmodell
Empfohlene Rollen:
- `owner`: Kunde verwalten, Billing, Admins, Löschung, Grundeinstellungen.
- `admin`: Mitglieder, Hinweise, fachliche Einstellungen, Auswertungen.
- `treasurer`: Zahlungen, CSV-Import, Korrekturen, Exporte, Mails.
- `member`: eigenes Dashboard, eigene Striche, eigener Anzeigename.
- `viewer`: lesende Auswertungen.
Alle Rollen sind tenant-scoped. Ein User kann also bei Kunde A Admin und bei
Kunde B nur Mitglied sein.
## Authentifizierung und Sicherheit
SaaS-Basis:
- E-Mail/Passwort-Login mit `password_hash`.
- E-Mail-Verifikation.
- Passwort-Reset.
- Sichere Session-Cookies.
- CSRF-Schutz für alle schreibenden Aktionen.
- Rate-Limits für Login, Registrierung und Passwort-Reset.
- Zentrale Funktionen oder Middleware: `requireLogin`, `requireTenant`,
`requireRole`, `csrfToken`.
Tenant-Auflösung:
- Primär serverseitig aus Subdomain, Custom Domain oder tenantgebundener
Auswahl nach Login.
- Nicht allein aus Hidden Inputs oder frei manipulierbaren IDs.
- Jede fachliche Query braucht einen Tenant-Scope.
Später möglich:
- LDAP/AD oder SSO pro Kunde als optionaler Identity Provider.
- MFA für Owner/Admins.
- Row-Level-Security in der Datenbank.
## Design-Leitplanken
Das bestehende Design soll erhalten bleiben.
Beibehalten:
- Grüner Akzent `#38761d`.
- Weiß/Grau als ruhige Grundfläche.
- Roboto Slab für Überschriften.
- Open Sans für Fließtext.
- Sidebar für die geschützte App.
- Tabellen als primäre Darstellung für operative Daten.
- Schlichte Formularfelder und Buttons.
Verbessern ohne Relaunch:
- Hinweise als wiederverwendbare Komponente statt Inline-Style.
- Einheitliche Erfolgs-, Fehler- und Warnmeldungen.
- Dashboardwerte als kompakte Statusbereiche.
- Aktionsleisten über Tabellen.
- Sidebar-Gruppierung und aktive Navigation.
- FAQ strukturieren, aber Inhalte nicht stark verändern.
Nicht tun:
- Kein komplett neues Farbsystem.
- Keine dekorative Marketingoptik in der App.
- Keine App-Tabellen in Kartenlandschaften auflösen.
- Keine Navigationsstruktur, die die heutigen Kernabläufe versteckt.
## Meilensteinplan
Übersicht:
| Meilenstein | Schwerpunkt | Hauptergebnis |
| --- | --- | --- |
| M0 | Baseline | Bestand, Sicherheit und Designreferenz sind dokumentiert |
| M1 | Golden Master | Legacy-Ergebnisse sind als Vergleichsbasis eingefroren; HTTP-Smoke prüft sichere Seiten |
| M2 | Technisches Fundament | Abgeschlossen: Migrationen, Bootstrap, Session, CSRF-Helper und Legacy-Schreibseitenschutz stehen |
| M3 | SaaS-Basis | Abgeschlossen: Tenants, User, Registrierung, Login, Rollen, Mail-Links und zentrale Mandantenauswahl funktionieren |
| M4 | Datenmigration | Gestartet: Ledger-Tabelle, Legacy-Backfill, Paritätscheck, Ledger-Service und Preview sind umgesetzt |
| M5 | App-Kern | Weit fortgeschritten: Kernseiten lesen und schreiben tenant-sicher gegen das Ledger, inklusive Hinweise und rollenbasiertem Zugang; offen ist ein eigener Zahlungs-Screen |
| M6 | Betriebsflows | Abgeschlossen: Import, Export, Mail und Jahresabschluss sind auditierbar |
| M7 | Landingpage | Public-Seite und Auth-Seiten sind im gemeinsamen Stil nutzbar; spätere Ausbaustufen folgen |
| M8 | Härtung | Betrieb, Datenschutz, Monitoring und Isolation sind geprüft |
| M9 | Cutover | Produktivumstellung ist vorbereitet und Legacy ist read-only |
### M0: Planungs- und Sicherheitsbaseline
Ziel:
Den aktuellen Zustand verlässlich dokumentieren, bevor umgebaut wird.
Arbeitsartefakte:
- `docs/m0/README.md`
- `docs/m0/code-inventory.md`
- `docs/m0/security-baseline.md`
- `docs/m0/schema-export.sql`
- `docs/m0/golden-master-queries.sql`
- `docs/m0/screenshot-checklist.md`
Schritte:
- Live-DB-Schema exportieren.
- Tabellen, Spalten, Indizes und Constraints dokumentieren.
- Bekannte Jobs/Skripte erfassen, zum Beispiel Mailversand und Jahresauswertung.
- Secrets inventarisieren und rotieren, insbesondere hart codierte Zugangsdaten.
- Bestehende Seiten und UI-Zustände per Screenshots festhalten.
- Fachliche Kernregeln dokumentieren: Saldo, Jahreswerte, Preis pro Strich,
100-Tage-Listen, CSV-Deduplizierung, PDF-Ausgabe.
Ergebnis:
- Datenkatalog.
- Prozesslandkarte.
- Designreferenz.
- Liste kritischer Sicherheitsrisiken.
Abhängigkeiten:
- Zugriff auf echte oder anonymisierte Datenbank.
- Kenntnis der Produktivumgebung.
### M1: Golden-Master-Validierung
Ziel:
Sicherstellen, dass spätere neue Berechnungen dieselben Ergebnisse wie der
Bestand liefern.
Schritte:
- Referenzdatensatz aus Legacy erzeugen. Erledigt als rekonstruierter Golden
Master in der MySQL-Dev-Datenbank.
- Pro Mitglied berechnen: Gesamteinzahlungen, Gesamtausgaben, Gesamtstriche,
aktueller Stand, Jahreswerte. Erledigt mit `scripts/check-golden-master.php`.
- CSV-Importfälle sammeln: Treffer, Dubletten, unbekannte Namen. Erledigt im
Golden-Master-Check.
- Testfälle für Guthaben, Schulden, Nullsaldo und inaktive Teilnehmer
anlegen. Erledigt.
- Sichere GET-Seiten per HTTP-Smoke prüfen. Erledigt mit
`scripts/http-smoke.php`.
- PDF-/Export-Summen als Referenz sichern. Offen, weil die TCPDF-Kopie im Repo
unvollständig ist.
Ergebnis:
- Golden-Master-Daten.
- Vergleichsqueries oder Vergleichsskript.
- HTTP-Smoke-Test für sichere UI-Seiten.
- Akzeptanzkriterien für Migration und neue App.
- Dokumentierte offene Punkte: PDF-Export/TCPDF, PHPMailer-Abhängigkeit,
GET-Nebenwirkungen bei Mailversand und Jahresauswertung.
Abhängigkeiten:
- M0 abgeschlossen.
- PDF-/Mail-/Jahresprozesse werden in M6 gezielt neu gestaltet, statt sie in M1
per GET-Smoke auszuführen.
### M2: Technisches Fundament
Ziel:
Eine saubere Basis für App, Public-Seiten, Auth und Mandanten schaffen.
Schritte:
- Projektstruktur festlegen: Public-Routes, App-Routes, Views/Templates,
Services/Repositories.
- Zentrales Bootstrap für Config, DB-Verbindung, Session und Fehlerbehandlung.
Begonnen mit `app/bootstrap.php`.
- Versionierte Migrationen einführen. Begonnen mit
`database/migrations/0001_legacy_mysql_baseline.sql` und
`scripts/migrate.php`.
- Layouts trennen: Public-Layout und App-Layout. Noch offen; Header/Footer
bleiben vorerst Legacy-Wrapper.
- Bestehende Assets weiterverwenden.
- CSRF- und Session-Basis einziehen. Session und CSRF-Helper sind vorhanden;
globale Erzwingung erfolgt schrittweise pro POST-Seite. Erste Seiten sind
abgesichert: `hinweise.php`, `mitarbeiterverwalten.php`,
`namenanpassen.php`, `index.php`, `stricheintragen.php`, `einzahlung.php`,
`letzteneintraege.php`, `csvupload.php`.
- CSV-Upload außerhalb des Webroots speichern. In M2 als Legacy-Härtung
umgesetzt: temporär unter `var/uploads`, Dateityp-/Größenprüfung und
Löschung nach Verarbeitung.
- Konfigurationswerte aus Code in Umgebung oder Settings verschieben.
Ergebnis:
- Grundgerüst für neue SaaS-App.
- Kein fachlicher Rewrite, aber klare Struktur für die Migration.
- Dokumentation: `docs/m2-technical-foundation.md`.
- Status: abgeschlossen für den M2-Scope.
Abhängigkeiten:
- Entscheidung, ob PHP nativ weitergeführt oder ein Framework genutzt wird.
- M2 bleibt bewusst ohne Tenant-/User-/Rollen-Tabellen; diese starten in M3.
### M3: Mandanten, Registrierung und Login
Ziel:
Mehrkundenfähigkeit und Kunden-Onboarding technisch aktivieren.
Schritte:
- Tabellen für `tenants`, `users`, `tenant_memberships` und
`tenant_settings` anlegen. Vorbereitung dokumentiert in
`docs/m3-saas-basis-vorbereitung.md`: erster Schritt erledigt.
- Zusätzlich `participants` als getrennte Kaffee-Teilnehmer-Tabelle anlegen:
erledigt.
- Default-Tenant für den aktuellen Bestand anlegen: erledigt.
- Bestehende `kl_Mitarbeiter` idempotent in `participants` spiegeln:
erledigt.
- Registrierung: Tenant + Owner-User + Default-Settings erzeugen: erster
Flow erledigt.
- Login und Logout bauen: erster Flow erledigt.
- Passwort-Reset und E-Mail-Verifikation bauen: Dev-Flow mit Single-Use-Tokens
und Mail-Transport-Abstraktion erledigt.
- Tenant-Auflösung definieren: zentrale App-Domain mit Session-Kontext und
Mandantenauswahl erledigt; feste Domains vorbereitet, keine Wildcards.
- Rollenprüfung zentralisieren: erster Owner/Admin-Check erledigt.
- Erste Admin-/Owner-Seite für Grundeinstellungen: erledigt.
- Erste Public-Landingpage mit CTA zu Login und Registrierung: erledigt.
- Login, Registrierung, Passwort-Reset und E-Mail-Verifikation im Public-Stil:
erledigt.
- App-Sidebar ohne Public-Login-/Registrierungslinks: erledigt.
Ergebnis:
- Ein neuer Kunde kann sich registrieren und in seine eigene App gelangen.
- Rollen und Tenant-Kontext sind serverseitig verfügbar.
- Reset- und Verifikationslinks können im Dev-Modus geloggt und produktiv über
einen konfigurierten Mail-Transport versendet werden.
- Status: abgeschlossen für den M3-Scope.
Abhängigkeiten:
- M2 abgeschlossen.
### M4: Migration des fachlichen Kerns
Ziel:
Bestehende Kaffeelisten-Daten tenant-sicher übernehmen.
Schritte:
- Initialen Tenant für den bisherigen Bestand anlegen: erledigt in M3.
- `kl_Mitarbeiter` nach `participants` migrieren: erledigt in M3.
- `admin`-Informationen in `tenant_memberships` oder Admin-Rollen überführen:
erledigt in M3.
- `kl_config` nach `tenant_settings` migrieren: erledigt in M3.
- `ledger_entries` als tenant-sichere Buchungstabelle anlegen: erledigt.
- `kl_Einzahlungen` und `kl_Kaffeeverbrauch` additiv nach `ledger_entries`
spiegeln: erster Backfill erledigt.
- Legacy-IDs speichern: erledigt über `legacy_table` und `legacy_id`.
- Salden gegen Golden-Master vergleichen: Kontrollskript erledigt.
- Ledger-Service für Tenant-Summen, Teilnehmer-Summen und letzte Buchungen:
erledigt.
- Read-only Ledger-Preview für Browservergleich: erledigt.
Ergebnis:
- Bestehender Datenbestand kann im neuen Modell abgebildet werden.
- Saldenparität ist über `scripts/check-m4-ledger-migration.php`
nachweisbar.
- Erste App-Abfragen können über `app/ledger.php` tenant-sicher aus dem neuen
Modell lesen.
- `ledger-preview.php` zeigt die neue Ledger-Auswertung ohne Schreibzugriffe.
- Dokumentation: `docs/m4-data-migration.md`.
Abhängigkeiten:
- M1 und M3 abgeschlossen.
### M5: Geschützte App-Funktionen
Ziel:
Die operativen Kernseiten im neuen Modell bereitstellen.
Schritte:
- Dashboard `Meine Kaffeeliste` umsetzen: read-only Ledger-Stand in
`index.php` erledigt; eigene Web-Striche werden als Kompatibilitätsbrücke
ins Ledger gespiegelt.
- Eigene Stricherfassung umsetzen: erledigt über die Selbsteintragung im
Dashboard (`index.php`).
- Zahlungs-/PayPal-Bereich umsetzen: PayPal-Anzeige und -Links sind über
`tenant_settings` im Dashboard umgesetzt, erledigt als Teil von
`index.php`. Ein eigener `/app/einzahlungen`-Screen wie ursprünglich in der
Zielarchitektur skizziert existiert nicht separat; die Funktion ist bewusst
im Dashboard gebündelt statt als eigene Route.
- Mitgliederverwaltung tenant- und rollenbasiert umsetzen: erledigt.
`mitarbeiterverwalten.php` verwaltet jetzt `participants` als primäre,
tenant-scoped Quelle (nicht mehr die global unscoped `kl_Mitarbeiter`-
Tabelle) und erlaubt Admins, Mitgliedern unabhängig von Name/E-Mail einen
Login-Zugang mit Rolle zu gewähren oder zu entziehen (Einladung per Mail
über den bestehenden Passwort-Reset-Mechanismus, Entzug als Statuswechsel
statt Delete). Nebenbei behoben: eine gespeicherte XSS-Lücke bei Name/
E-Mail und ein Mandanten-Datenleck, weil die alte Version jedem
SaaS-Mandanten die komplette Default-Mandanten-Mitgliederliste zeigte.
- Gesamtübersicht umsetzen: erster read-only Stand in `kaffeeliste.php`
erledigt.
- Teilnehmerauswertung umsetzen: read-only Stand in `teilnehmerauswertung.php`
erledigt.
- Letzte Einträge und Korrekturen als Storno statt Delete umsetzen: erledigt
in `letzteneintraege.php` über `ledger_void_entry_by_legacy_id()`; Legacy-
Zeilen werden weiterhin gelöscht, der gespiegelte Ledger-Eintrag bleibt für
die Revision erhalten.
- Sammelerfassung (`stricheintragen.php`, `einzahlung.php`) tenant-sicher
absichern und ans Ledger anbinden: erledigt. Beide Seiten hatten zuvor
keine Zugriffskontrolle außer CSRF; das ist behoben. Zusätzlich lasen
beide Seiten ihre Mitarbeiterliste aus der global unscoped
`kl_Mitarbeiter`-Tabelle, was sie für jeden Mandanten außer dem
Default-Mandanten unbrauchbar machte. Der Picker kommt jetzt aus
`participants` (tenant-scoped); der Schreibpfad nutzt für den
Default-Mandanten weiterhin Dual-Write nach `kl_Kaffeeverbrauch`/
`kl_Einzahlungen`, für alle anderen Mandanten schreibt er direkt und
ausschließlich ins Ledger.
- Hinweise als tenant-spezifische Notices umsetzen: erledigt. Neue Tabelle
`notices` mit Soft-Delete, `hinweise.php` und die Banner-Anzeige in
`header.php` sind tenant-scoped umgestellt; `kl_hinweise` bleibt nur noch
als Golden-Master-Referenz bestehen.
Ergebnis:
- Ein Kunde kann seine Kaffeeliste operativ nutzen, inklusive
Mitgliederpflege und Korrekturen.
- App sieht weiterhin nach bestehender Kaffeeliste aus.
- M5-Stand ist in `docs/m5-app-kern.md` dokumentiert.
Abhängigkeiten:
- M4 abgeschlossen.
### M6: Import, Export und Mail
Ziel:
Admin- und Treasurer-Flows produktionsreif machen.
Schritte:
- CSV-Import mit Vorschau, Dublettenprüfung und Audit bauen: erledigt.
Neue Tabellen `payment_import_batches`/`payment_import_rows`,
zweistufiger Ablauf (Vorschau vor Buchung), Zuordnung per PayPal-Name
oder Anzeigename, Dublettenprüfung gegen bestehende Ledger-Zahlungen.
`csvupload.php` hatte zuvor keine Zugriffskontrolle; das ist behoben.
- Uploads außerhalb des Webroots speichern: erledigt (bereits in M2,
weiterhin genutzt).
- PDF-/Listenexport aus neuem Datenmodell bauen: erledigt. TCPDF war nur
teilweise vendort (`include/` und Fonts fehlten); nachvendort wurden
`include/` und die 14 PDF-Standard-Fonts aus dem offiziellen
TCPDF-6.6.2-Release. `exportKaffeeliste.php` hatte zuvor keine
Zugriffskontrolle; das ist behoben, die Seite liest jetzt tenant-sicher
aus dem Ledger.
- Mailversand als nachvollziehbaren Versandjob mit Dry-Run und Versandlog
gestalten: erledigt. `mailversenden.php` versendete zuvor ungeprüft bei
jedem GET-Request echte Mails über PHPMailer (dessen Quelldateien im Repo
gar nicht vorhanden waren) mit fest codierten Alt-Kunden-Inhalten (SMTP,
PayPal-Link, FAQ-URL). Ersetzt durch die bestehende `saas_send_mail()`-
Abstraktion aus M3, eine neue `outbound_emails`-Tabelle als Versandlog
und eine standardmäßig aktive Dry-Run-Option.
- Jahresauswertung beziehungsweise Jahresbuchungen tenant-sicher abbilden:
erledigt. Die ursprüngliche Seite verband sich mit fest codierten,
kaputten Zugangsdaten selbst zur Datenbank (an `config.php` vorbei),
hatte keine Zugriffskontrolle und verteilte bei jedem Aufruf sofort einen
hart codierten Bonus-Topf mit AOK-spezifischem Mailtext. Nach Abstimmung
mit dem Kunden als generisches Feature umgesetzt: frei wählbarer
Gesamtbetrag, proportionale Verteilung nach Jahresstrichen, Dry-Run,
Buchung und Mailversand mit Protokoll.
Ergebnis:
- Kassenverwaltung ist für reale Betriebsabläufe vollständig.
- Import/Export/Mail sind auditierbar.
- Stand: abgeschlossen für den M6-Scope. Dokumentation:
`docs/m6-import-export-mail.md`.
Abhängigkeiten:
- M5 für die Kernansichten.
### M7: Öffentliche Landingpage
Ziel:
Werbliche Einstiegseite und Registrierung verfügbar machen, ohne die App-Optik
zu verwischen.
Schritte:
- Public-Layout mit bestehender Typografie und grünem Akzent bauen. Erster
Stand als `landing.php` und `assets/css/public.css` erledigt.
- Landingpage-Inhalte erstellen. Erster Stand erledigt.
- Demo-Screenshot oder Demo-Ansicht einbinden.
- CTA zu Registrierung und Login. Erledigt.
- Login, Registrierung und Passwort-Reset in den Public-Stil integrieren.
Erledigt.
- App-Sidebar von Public-Links trennen. Erledigt.
- FAQ-Auszug strukturieren.
- Keine App-Sidebar im Public-Bereich.
Ergebnis:
- Interessenten verstehen das Produkt und können sich registrieren.
- Bestehende App bleibt optisch eigenständig und arbeitsorientiert.
Abhängigkeiten:
- M3 für Registrierung.
- M5 für echte App-Screens oder Demo.
### M8: Härtung, Datenschutz und Betrieb
Ziel:
Die SaaS-App für mehrere Kunden sicher betreiben.
Schritte:
- Backups und Restore-Prozess definieren.
- Monitoring und Fehlerlogging einrichten.
- Audit-Log für Admin-Aktionen prüfen.
- Datenexport pro Tenant.
- Lösch-/Anonymisierungsprozess für Teilnehmer und Kunden.
- Rate-Limits und Security Headers.
- Mandanten-Isolation testen.
- Rollenmatrix testen.
Ergebnis:
- SaaS ist betrieblich und datenschutzseitig belastbarer.
Abhängigkeiten:
- M3 bis M6.
### M9: Cutover und Legacy-Archiv
Ziel:
Produktive Umstellung ohne Datenverlust.
Schritte:
- Legacy für Schreibzugriffe sperren.
- Finalen Delta-Export ziehen.
- Migration ausführen.
- Rowcounts, Salden, PDF-Stichproben und Importhistorie prüfen.
- DNS oder Reverse Proxy umschalten.
- Legacy 30 bis 90 Tage read-only als Archiv behalten.
- Rollback-Fenster und Vorgehen dokumentieren.
Ergebnis:
- Neuer SaaS-Betrieb ist live.
- Alter Bestand bleibt für Rückfragen lesbar.
Abhängigkeiten:
- M8 abgeschlossen.
## Empfohlene MVP-Reihenfolge
Für den ersten produktnahen MVP sollten nicht alle Randmodule gleichzeitig
umgebaut werden.
MVP-Paket:
1. Tenant, User, Login, Registrierung.
2. Mail-Links, zentrale Mandantenauswahl und erste Landingpage.
3. Teilnehmer, Settings, Ledger.
4. Dashboard, Striche, Einzahlungen, Gesamtübersicht.
5. Mitgliederverwaltung.
6. Hinweise.
7. CSV-Import, Export.
Später:
- Umfrage-Modul.
- SSO/LDAP pro Kunde.
- Billing/Tarife.
- MFA.
- Custom Domains.
- Erweiterte Mandanten-Branding-Optionen.
## Validierung
Fachliche Prüfungen:
- Saldo je Teilnehmer alt gegen neu.
- Jahreswerte alt gegen neu.
- Gesamtstriche alt gegen neu.
- Einzahlungen alt gegen neu.
- Export-Summen alt gegen neu.
- CSV-Import: identische Treffer- und Dublettenlogik.
Sicherheitsprüfungen:
- Fremde Tenant-IDs liefern keine Daten.
- Fremde Teilnehmer-IDs liefern keine Daten.
- Admin-Funktionen sind ohne Rolle blockiert.
- Alle POST-Aktionen brauchen CSRF.
- Uploads sind nicht direkt ausführbar.
- Ausgabe von Namen, Mails und Hinweisen ist escaped.
UX-Prüfungen:
- Kernseiten bleiben optisch wiedererkennbar.
- Sidebar bleibt für App-Nutzer vertraut.
- Landingpage zeigt keine App-Adminnavigation.
- Mobile Darstellung bleibt bedienbar.
- Tabellen bleiben lesbar.
## Risiken und Gegenmaßnahmen
| Risiko | Gegenmaßnahme |
| --- | --- |
| Tenant-Leak durch vergessene Query-Filter | Zentrale Repository-/Query-Schicht, Tests mit zwei Tenants |
| Falsche Salden nach Migration | Golden-Master-Vergleich vor Cutover |
| Harte Deletes verlieren Audit-Historie | Storno-/Reversal-Modell für Buchungen |
| Login und Teilnehmer werden vermischt | `users` und `participants` strikt trennen |
| Bestehendes Design driftet weg | Designreferenz und App-Komponenten definieren |
| CSV-Upload unsicher | Upload außerhalb Webroot, Dateitypprüfung, Importvorschau |
| Mailversand nicht nachvollziehbar | Versandlog, Dry-Run, Job-Status |
| Unvollständige Vendor-Kopien für PDF/Mail | Dependencies sauber vendoren oder ersetzen, Smoke-Tests danach erweitern. TCPDF in M6 nachvendort (nur `include/` und die 14 Standard-Fonts); PHPMailer weiterhin unvollständig, siehe M6-Mailversand |
| AD/LDAP blockiert SaaS-Onboarding | E-Mail/Passwort als Basis, SSO später optional |
| Kein DB-Schema im Repo | Schema exportieren und Migrationen einführen |
## Offene Entscheidungen
- Soll die neue App weiterhin nativ in PHP entstehen oder mit einem Framework?
- Soll SQL Server bleiben oder mittelfristig eine andere Datenbank genutzt
werden?
- Wie sollen Kunden-Tenants adressiert werden: Subdomain, Pfad oder Auswahl nach
Login?
- ~~Welche Rolle soll `treasurer` haben: eigene Rolle oder Teil von `admin`?~~
Entschieden mit der M5-Zugangsvergabe: `treasurer` ist eine eigene,
über `mitarbeiterverwalten.php` vergebbare Rolle, getrennt von `admin`.
- Wird das Umfrage-Modul Teil des SaaS-Produkts oder nur archiviert?
- Braucht der MVP schon Tarife/Billing oder erstmal nur Registrierung?
- Sollen bestehende Kunden per Einladung oder per Self-Service onboarden?
## Nächster konkreter Arbeitsschritt
Als nächstes sollte M0 gestartet werden:
1. Produktiv-/Testdatenbankschema exportieren.
2. Screenshot-Referenz der wichtigsten Seiten erstellen.
3. Golden-Master-Auswertungen definieren.
4. Entscheidung treffen, ob der neue SaaS-Kern im bestehenden PHP-Stil oder mit
einem Framework aufgebaut wird.