Files
kaffeekasse-saas/docs/m5-app-kern.md
T
clemensandClaude Sonnet 5 8b0dcd2c70 FAQ pro Mandant statt fest codiertem AOK-Inhalt
faq.php zeigte bislang fuer jeden Mandanten denselben, komplett
AOK-spezifischen Inhalt (Kaffeemaschinen-Bedienung, interner
Ansprechpartner) - fuer andere Kunden unbrauchbar und inhaltlich falsch.

- Neue Tabelle faq_entries (tenant-scoped, Soft-Delete, sort_order).
- Migration uebernimmt die bisherigen AOK-Inhalte einmalig als FAQ des
  migrierten Default-Mandanten; andere Mandanten sehen sie nicht.
- Neue Mandanten bekommen bei der Registrierung automatisch eine kurze
  generische Starter-FAQ (faq_seed_default_entries), frei editierbar.
- faq.php: lesbar fuer alle angemeldeten Mitglieder eines Mandanten,
  Anlegen/Bearbeiten/Entfernen auf owner/admin beschraenkt.
- Landingpage verweist nicht mehr auf faq.php (jetzt interner,
  mandantengebundener Inhalt statt oeffentlicher Marketing-Seite).

Live getestet: AOK-Carry-over fuer Default-Mandant, frisch registrierter
Test-Mandant bekam isolierte generische Starter-FAQ, Anlegen mit
HTML-Payload (korrekt escaped), Bearbeiten und Soft-Delete geprueft.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 23:18:09 +02:00

363 lines
17 KiB
Markdown
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.
# M5 App-Kern
Stand: 2026-07-14 (fortgeführt)
M5 stellt die operativen App-Seiten schrittweise auf das neue tenant-sichere
Modell um. Der Start erfolgt bewusst read-only, damit Summen, Links und
Darstellung gegen die bestehende Legacy-Oberfläche vergleichbar bleiben.
## Ziel
- Kernseiten aus `app/ledger.php` lesen lassen.
- Bestehendes Tabellenlayout und Bediengefühl erhalten.
- Schreibende Legacy-Flows erst nach stabiler Leseparität umstellen.
- Tenant-Kontext serverseitig bestimmen, nicht aus Formularfeldern.
## Umgesetzte Schritte
Umgesetzte Dateien:
```text
kaffeeliste.php
teilnehmerauswertung.php
index.php
```
Umgesetzter Umfang `kaffeeliste.php`:
- Die Gesamtübersicht liest aktive Teilnehmer aus
`ledger_fetch_participant_summaries()`.
- Angezeigte Werte bleiben fachlich gleich: aktueller Stand, Gesamtausgabe,
Gesamtstriche und Gesamteinzahlungen.
- Links zur bestehenden `teilnehmerauswertung.php` bleiben über
`participants.legacy_mitarbeiter_id` erhalten.
- Owner, Admin und Treasurer dürfen die SaaS-Ansicht lesen.
- Der lokale Legacy-/Dev-Admin-Fallback nutzt nur ohne SaaS-Session den
Default-Tenant.
- Die Sidebar zeigt Legacy-Schreibseiten weiterhin nur für Legacy-Admins;
Ledger-Leseansichten sind für passende SaaS-Rollen sichtbar.
- Export, letzte Einträge, CSV-Upload und Info-Mail bleiben noch Legacy-Flows.
Umgesetzter Umfang `teilnehmerauswertung.php`:
- Die Detailauswertung liest Teilnehmerdaten über
`ledger_fetch_participant_summary_by_legacy_id()`.
- Die Route bleibt kompatibel zu bestehenden Links:
`teilnehmerauswertung.php?user_id=<legacy_mitarbeiter_id>`.
- Gesamtwerte, Jahresübersicht und letzte Buchungen werden aus
`ledger_entries` geladen.
- PayPal-Anzeige nutzt die neuen Tenant-Settings statt `kl_config`.
- Owner, Admin und Treasurer dürfen die SaaS-Ansicht lesen; der lokale
Legacy-/Dev-Admin-Fallback nutzt weiterhin den Default-Tenant.
Ergänzte Ledger-Helfer:
- `ledger_fetch_default_tenant()`
- `ledger_fetch_participant_summary_by_legacy_id()`
- `ledger_mirror_legacy_consumption()`
- Filter `legacy_mitarbeiter_ids` in `ledger_fetch_participant_summaries()`
Umgesetzter Umfang `index.php`:
- Das persönliche Dashboard liest den aktuellen Stand, Jahreswerte und letzte
Buchungen aus `ledger_entries`.
- Die Teilnehmerzuordnung erfolgt über den aktuellen SaaS-User oder im lokalen
Legacy-/Dev-Fallback über die E-Mail-Adresse des Legacy-Mitarbeiters.
- PayPal-Anzeige und Preis pro Strich nutzen die Tenant-Settings.
- Eigene Web-Striche bleiben mit dem bestehenden Legacy-Datensatz kompatibel
und werden direkt ins Ledger gespiegelt.
## Fortsetzung: Schreibseiten und Storno
Umgesetzte Dateien:
```text
stricheintragen.php
einzahlung.php
letzteneintraege.php
app/ledger.php
scripts/check-m4-ledger-migration.php
```
Umgesetzter Umfang `stricheintragen.php` und `einzahlung.php`:
- Beide Sammelerfassungsseiten waren zuvor ohne jede Zugriffsprüfung
erreichbar (nur CSRF-Schutz, kein `checkKaffeelisteAdmin`- oder Rollen-Check).
Das ist jetzt behoben: Zugriff erfordert SaaS-Rolle `owner`, `admin` oder
`treasurer`, oder im Legacy-/Dev-Fallback `checkKaffeelisteAdmin` mit dem
Default-Tenant.
- Jede eingetragene Legacy-Zeile (`kl_Kaffeeverbrauch` beziehungsweise
`kl_Einzahlungen`) wird innerhalb derselben Transaktion sofort über
`ledger_mirror_legacy_consumption()` beziehungsweise die neue
`ledger_mirror_legacy_payment()` ins Ledger gespiegelt. Schlägt die
Spiegelung fehl, wird die gesamte Sammelerfassung zurückgerollt statt
teilweise gespeichert zu werden.
- Ein vorbestehender Bug wurde nebenbei behoben: Bei einer POST-Anfrage blieb
`$sqlMitarbeiter` unbelegt, was einen PHP-Warning erzeugte und die
Mitarbeiterliste nach dem Speichern leer ließ.
Umgesetzter Umfang `letzteneintraege.php`:
- Der Seitenzugriff nutzte bisher ausschließlich den Legacy-Check
`checkKaffeelisteAdmin`, wodurch neu registrierte SaaS-Mandanten (ohne
Legacy-`kl_Mitarbeiter`-Zeile) die Seite nie hätten nutzen können. Jetzt gilt
dieselbe Rollen-/Fallback-Logik wie in `kaffeeliste.php`.
- Löschen erzeugt keinen harten Delete im Ledger mehr. Neue Funktion
`ledger_void_entry_by_legacy_id()` setzt `voided_at` auf dem gespiegelten
Ledger-Eintrag, bevor die Legacy-Zeile aus `kl_Einzahlungen` oder
`kl_Kaffeeverbrauch` entfernt wird (in einer Transaktion). Der Ledger-Eintrag
bleibt damit für die Revision erhalten; alle Lesepfade filtern bereits
konsistent auf `voided_at IS NULL`.
- Zwei nie aufgerufene Funktionen (`berechneGesamtausgabe`,
`berechneGesamtstriche`, `berechneGesamteinzahlungen`) wurden als toten Code
entfernt.
Angepasstes Check-Skript:
- `scripts/check-m4-ledger-migration.php` ging bisher davon aus, dass jeder
gespiegelte Ledger-Eintrag eine noch existierende Legacy-Zeile hat. Das ist
durch das Storno-Modell nicht mehr korrekt: Ein stornierter Eintrag hat
absichtlich keine Legacy-Zeile mehr, bleibt aber im Ledger stehen. Die
Prüfungen filtern jetzt zusätzlich auf `voided_at IS NULL`, sodass echte
Dateninkonsistenzen weiterhin erkannt werden, stornierte Einträge aber nicht
mehr als Fehler zählen.
Alle drei Flows wurden gegen die Remote-Dev-Datenbank live per HTTP getestet
(Sammeleintrag Striche, Sammeleinzahlung, Storno beider Buchungsarten über
`letzteneintraege.php`) und anschließend wieder auf den Ausgangsstand
zurückgesetzt.
## Fortsetzung: Mitgliederverwaltung
Umgesetzte Dateien:
```text
mitarbeiterverwalten.php
app/ledger.php
```
Umfang:
- Zugriff nutzte bisher ausschließlich `checkKaffeelisteAdmin` und hätte neue
SaaS-Mandanten ohne Legacy-Mitarbeiterzeile ausgeschlossen. Jetzt gilt die
gleiche Rollen-/Fallback-Logik wie in `kaffeeliste.php`, mit den Rollen
`owner` und `admin` (nicht `treasurer`, da Mitgliederpflege sensibler ist
als reine Zahlungsvorgänge).
- Anlegen, Bearbeiten, Aktivieren und Deaktivieren schreiben weiterhin zuerst
in `kl_Mitarbeiter` und spiegeln danach in derselben Transaktion über die
neue Funktion `ledger_mirror_legacy_participant()` nach `participants`.
Ledger-Ansichten (Dashboard, Kaffeeliste, Teilnehmerauswertung) sehen neue
oder geänderte Mitglieder damit sofort, ohne auf einen manuellen Lauf von
`scripts/backfill-default-tenant.php` zu warten.
- Eine gespeicherte-XSS-Lücke wurde behoben: Name und E-Mail wurden beim
Bearbeiten-Formular und in der Mitgliederliste bisher ungeschützt
ausgegeben (nur `paypalname` war escaped). Beide Stellen nutzen jetzt
`saas_html()`.
- Live gegen die Dev-Datenbank getestet: Anlegen mit einem
HTML/Skript-Payload im Namen (korrekt escaped in der Ausgabe, korrekt
gespiegelt in `participants`), Deaktivieren (spiegelt `active = 0`).
Bewusst nicht umgesetzt:
- Der Haken "Administrator" bleibt ein reines Legacy-Feld auf
`kl_Mitarbeiter.admin` und wird nicht automatisch in eine
`tenant_memberships`-Rolle übersetzt. Das würde einen Login-Account ohne
Einladung/Passwort-Setzung anlegen, was ein eigenes, sauber zu
bauendes Einladungs-Flow braucht (E-Mail-Versand, Token, Passwortsetzung).
Admin-Rollen für neue SaaS-Nutzer laufen bis dahin weiter über
`scripts/backfill-default-tenant.php` oder die Registrierung.
## Fortsetzung: Hinweise und rollenbasierter Zugang
Umgesetzte Dateien:
```text
database/migrations/0007_saas_notices.sql
app/notices.php
hinweise.php
header.php
mitarbeiterverwalten.php (grundlegend neu)
app/ledger.php (Participant-CRUD)
app/saas-auth.php (Zugangsvergabe/-entzug)
app/saas-mail.php (Einladungsmail)
```
### Hinweise auf Notices umgezogen
- Neue Tabelle `notices` (tenant-scoped, mit `deleted_at` für Soft-Delete)
ersetzt `kl_hinweise` als aktive Datenquelle. `kl_hinweise` bleibt
unangetastet als Golden-Master-Referenz.
- Die Migration übernimmt einmalig alle zum Migrationszeitpunkt noch
gültigen `kl_hinweise`-Einträge in `notices` des Default-Mandanten, damit
keine sichtbaren Banner verloren gehen.
- `hinweise.php` liest/schreibt jetzt ausschließlich `notices`, tenant-scoped
mit dem gleichen Rollen-/Legacy-Fallback-Muster wie andere Admin-Seiten.
Löschen ist ein Soft-Delete (`deleted_at`), kein Hard-Delete.
- `header.php` (Banner-Anzeige auf allen App-Seiten) löst den Mandanten
jetzt selbst auf (SaaS-Session oder Default-Tenant-Fallback) und zeigt den
aktuell gültigen Hinweis dieses Mandanten statt eines global-legacy
Hinweises.
### Mitgliederverwaltung: participant-nativ mit Rollen-Zugang
`mitarbeiterverwalten.php` wurde grundlegend umgebaut, nicht mehr additiv
gepatcht:
- Datenquelle ist jetzt `participants` (tenant-scoped), nicht mehr die
global unscoped `kl_Mitarbeiter`-Tabelle. Das behebt nebenbei ein
Datenleck: Da `kl_Mitarbeiter` keine `tenant_id` hat, hätte jeder
SaaS-Mandant mit Owner/Admin-Rolle über die vorherige Version dieser
Seite die komplette Mitgliederliste des Default-Mandanten sehen und
bearbeiten können.
- Für den Default-Mandanten wird weiterhin dual-write nach
`kl_Mitarbeiter` betrieben (`ledger_create_participant()`,
`ledger_update_participant()`, `ledger_set_participant_active()`), damit
`stricheintragen.php` und `einzahlung.php` (deren Mitarbeiter-Picker
weiterhin direkt aus `kl_Mitarbeiter` liest) neue Mitglieder sofort
anzeigen. Andere Mandanten haben keine Legacy-Schattentabelle und werden
rein participant-nativ verwaltet.
- Die Legacy-„Administrator"-Checkbox ist aus dem Anlegen-/Bearbeiten-
Formular entfernt. Stattdessen gibt es je Mitglied eine eigene
„Zugang"-Spalte: Rolle wählen (`member`, `treasurer`, `admin`) und
„Zugang gewähren", oder bei bestehendem Zugang Rolle ändern beziehungsweise
„Zugang entziehen". `owner` ist über diese UI nicht vergebbar oder
entziehbar (nur bei Registrierung gesetzt).
- Name und E-Mail eines Mitglieds (`participants.display_name`/`email`)
bleiben unabhängig vom Login: Ein Mitglied kann ohne jeden Zugang
existieren (nur für Kaffeeliste/Benachrichtigung), und ein Zugang kann
jederzeit gewährt oder entzogen werden, ohne den Mitgliedsdatensatz zu
berühren.
- Zugangsvergabe (`saas_grant_participant_access()`) legt bei Bedarf einen
`users`-Datensatz ohne Passwort an, setzt/aktualisiert die
`tenant_memberships`-Rolle und verknüpft `participants.user_id`.
Anschließend wird ein Einladungslink über den bestehenden
Passwort-Reset-Mechanismus verschickt (`saas_send_invite_mail()`, gleicher
Token-Typ `password_reset`, gleiche Zielseite
`passwort-zuruecksetzen.php` wie beim regulären Passwort-Reset).
- Zugangsentzug (`saas_revoke_participant_access()`) setzt die
`tenant_memberships`-Zeile auf `status = 'revoked'`, statt sie zu löschen
oder den `user_id`-Verweis zu entfernen. Der Zugang kann später erneut
gewährt werden, ohne den Account neu anzulegen. Der Login prüft bereits
überall auf `tm.status = 'active'`, wodurch ein entzogener Zugang sofort
wirkt.
- Live gegen die Dev-Datenbank getestet: Mitglied anlegen (inklusive
Dual-Write-Check), Zugang mit Rolle `treasurer` gewähren, Einladungsmail
geprüft, Passwort über den Einladungslink gesetzt, erfolgreicher Login,
Rollenschutz geprüft (kein Zugriff auf `mitarbeiterverwalten.php`, Zugriff
auf `kaffeeliste.php`), Zugang entzogen, anschließender Login-Versuch
korrekt mit „kein aktiver Mandant" abgelehnt.
## Fortsetzung: Sammelerfassung tenant-nativ
Umgesetzte Dateien:
```text
stricheintragen.php (Picker + Schreibpfad neu)
einzahlung.php (Picker + Schreibpfad neu)
app/ledger.php (ledger_record_consumption, ledger_record_payment,
ledger_fetch_participants_by_window_marks)
```
`stricheintragen.php` und `einzahlung.php` lasen ihren Mitarbeiter-Picker
bisher direkt aus der global unscoped `kl_Mitarbeiter`-Tabelle. Für jeden
Mandanten außer dem Default-Mandanten war das faktisch nutzlos: Die Liste
zeigte fremde (Default-Mandanten-)Namen an, und Schreiben schlug wegen des
Tenant-Checks in `ledger_mirror_legacy_*` sicher, aber ohne verständliche
Fehlermeldung fehl.
- Der Picker kommt jetzt aus `ledger_fetch_participant_summaries()`
(tenant-scoped), Formularfelder verwenden `participant_id` statt
`MitarbeiterID`.
- Beim Speichern wird pro Teilnehmer entschieden: Hat der Teilnehmer eine
`legacy_mitarbeiter_id` (Default-Mandant), läuft der Schreibpfad wie
bisher über `kl_Kaffeeverbrauch`/`kl_Einzahlungen` plus Ledger-Spiegelung.
Ohne Legacy-Verknüpfung (jeder andere Mandant) wird direkt über die neuen
Funktionen `ledger_record_consumption()`/`ledger_record_payment()` ins
Ledger gebucht, ohne Legacy-Tabellen zu berühren.
- Die "Vorderseite"/"Rückseite"-Filter (100-Tage-Listen-Regel, `>= 10`
beziehungsweise `< 10` Striche im Fenster) bleiben für den Default-
Mandanten exakt auf der bisherigen Legacy-Logik (Anker: jüngstes Datum in
`kl_Kaffeeverbrauch`). Andere Mandanten nutzen die neue, Ledger-native
Fensterprüfung `ledger_fetch_participants_by_window_marks()` mit dem in
`tenant_settings.sheet_window_days` konfigurierten Fenster.
- Nebenbei zwei Legacy-Bugs behoben: In `einzahlung.php` verlinkten die
Vorderseite/Rückseite/Alle-Buttons auf `?aktion=...`, ausgewertet wurde
aber `$_GET['action']` die Filter griffen also nie. Und in beiden
Dateien blieb nach einem POST-Speichern `$sqlMitarbeiter` unbelegt
(behoben bereits in der vorherigen Session, jetzt strukturell nicht mehr
möglich, da die Anzeige nach dem Speichern regulär über denselben
Picker-Code läuft).
- Preis-pro-Strich-Vorbelegung kommt jetzt aus `tenant_settings.mark_price_cents`
statt aus der mandantenunabhängigen `kl_config`-Tabelle. Das ist auch für
den Default-Mandanten eine Korrektur: `kl_config` wird seit M3 nicht mehr
über `mandant-einstellungen.php` gepflegt und kann veraltet sein.
- Live gegen die Dev-Datenbank getestet: eigener isolierter Test-Mandant
angelegt, Picker zeigt nur dessen Teilnehmer, Striche und Einzahlung rein
Ledger-nativ gebucht (korrekter Saldo), Vorder-/Rückseiten-Schwelle bei
10 Strichen korrekt ein-/aussortiert, Default-Mandant weiterhin mit
Dual-Write geprüft. Alle Testdaten anschließend entfernt.
## Noch offen
- Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im
Dashboard integriert).
- Export, Mail und Jahresprozesse bleiben M6-Themen.
## Nachtrag: FAQ pro Mandant
Umgesetzte Dateien:
```text
database/migrations/0012_saas_faq_entries.sql
app/faq.php
faq.php
app/saas-auth.php (Starter-FAQ bei Registrierung)
landing.php (Link auf die jetzt mandantengebundene faq.php entfernt)
```
Hintergrund: `faq.php` war komplett AOK-spezifischer Inhalt
(Kaffeemaschinen-Bedienung, Milch-/Zucker-Standort, interner
Ansprechpartner) und für jeden Mandanten identisch sichtbar nicht
brauchbar für andere Kunden.
- Neue Tabelle `faq_entries` (tenant-scoped, Soft-Delete via `deleted_at`,
`sort_order` für die Reihenfolge).
- Die Migration übernimmt die bisherigen AOK-Inhalte einmalig als erste
FAQ-Einträge des migrierten Default-Mandanten (`kl_hinweise`-Migration
als Vorbild). Andere Mandanten sehen diese Inhalte nicht.
- Neue Mandanten bekommen bei der Registrierung automatisch eine kurze,
generische Starter-FAQ (`faq_seed_default_entries()`, vier Fragen zu
Einrichtung, Rollen, PayPal, eigenem Stand) editierbar, nicht bindend.
- `faq.php` ist jetzt für alle angemeldeten Mitglieder eines Mandanten
lesbar (auch Legacy-Fallback über `checkKaffeelisteAccess`); Anlegen,
Bearbeiten und Entfernen einzelner Fragen ist auf `owner`/`admin`
beschränkt (beziehungsweise `checkKaffeelisteAdmin` im Legacy-Fallback).
- Die Landingpage verweist nicht mehr auf `faq.php` (das ist jetzt
interner, mandantengebundener App-Inhalt), sondern zeigt den
vollständigen generischen FAQ-Auszug direkt auf der Seite selbst.
Live getestet: Carry-over der AOK-Inhalte für den Default-Mandanten
geprüft, ein frisch registrierter Test-Mandant bekam korrekt die
generische Starter-FAQ (isoliert von den AOK-Inhalten), Anlegen mit
HTML/Skript-Payload im Text (korrekt escaped), Bearbeiten und
Soft-Delete-Löschen erfolgreich geprüft. Testdaten anschließend entfernt.
## Aktueller Prüfstatus
- M4 Ledger-Migration: grün mit 73 Assertions (Storno-fähig).
- M4 Ledger-Service: grün mit 115 Assertions.
- Golden Master: grün mit 104 Assertions.
- HTTP-Smoke: grün mit 23 geprüften Seiten.
- Live-Test der Schreibflows gegen die Dev-DB: Sammelstriche, Sammeleinzahlung
und Storno beider Buchungsarten erfolgreich geprüft.
- Live-Test der Mitgliederverwaltung gegen die Dev-DB: Anlegen (inklusive
XSS-Payload-Check), Deaktivieren, participants-Spiegelung erfolgreich
geprüft.
- Live-Test Hinweise/Notices: Carry-over-Migration, Anlegen mit HTML-Payload
(korrekt escaped), Soft-Delete, Banner-Anzeige auf Mandant geprüft.
- Live-Test Zugangsvergabe/-entzug: kompletter Flow von Einladung bis
Login-Sperre nach Entzug erfolgreich geprüft (siehe oben).
- Live-Test Sammelerfassung für Nicht-Default-Mandanten: eigener Test-Tenant,
Striche/Einzahlung Ledger-nativ gebucht, Vorder-/Rückseiten-Fenster
geprüft (siehe oben).