Files
kaffeekasse-saas/docs/m5-app-kern.md
T
clemensandClaude Sonnet 5 4d5f32ae6d M5: Sammelerfassung (Striche/Einzahlungen) tenant-nativ machen
stricheintragen.php und einzahlung.php lasen ihren Mitarbeiter-Picker
bisher direkt aus der global unscoped kl_Mitarbeiter-Tabelle. Fuer jeden
Mandanten ausser dem Default-Mandanten zeigte das fremde Namen und
Schreiben schlug (sicher, aber unverstaendlich) am Tenant-Check in
ledger_mirror_legacy_* fehl.

- Picker kommt jetzt aus participants (tenant-scoped), Formularfelder
  nutzen participant_id statt MitarbeiterID.
- Schreibpfad pro Teilnehmer: mit legacy_mitarbeiter_id (Default-Mandant)
  weiterhin Dual-Write nach kl_Kaffeeverbrauch/kl_Einzahlungen plus
  Ledger-Spiegelung; ohne Legacy-Verknuepfung (jeder andere Mandant) direkt
  ueber neue ledger_record_consumption()/ledger_record_payment().
- Vorderseite/Rueckseite-Filter (100-Tage-Regel) bleiben fuer den
  Default-Mandanten exakt auf der bisherigen Legacy-Logik; andere
  Mandanten nutzen die neue ledger_fetch_participants_by_window_marks()
  mit tenant_settings.sheet_window_days.
- Nebenbei behoben: einzahlung.php verlinkte auf ?aktion=... statt
  ?action=..., wodurch die Vorderseite/Rueckseite-Buttons nie griffen.
  Preis-pro-Strich-Vorbelegung kommt jetzt aus tenant_settings statt der
  seit M3 nicht mehr gepflegten kl_config-Tabelle.
- Live getestet: isolierter Test-Mandant, Picker zeigt nur eigene
  Teilnehmer, Buchungen rein Ledger-nativ mit korrektem Saldo, 10-Striche-
  Schwelle korrekt sortiert, Default-Mandant-Dual-Write weiterhin gruen.

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

324 lines
15 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.
## 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).