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>
9.5 KiB
M6 Import, Export und Mail
Stand: 2026-07-15
M6 macht die Admin-/Treasurer-Flows produktionsreif: CSV-Import, PDF-/Listenexport, Mailversand und Jahresprozesse laufen tenant-sicher gegen das neue Modell statt gegen Legacy-Tabellen direkt.
CSV-Import
Umgesetzte Dateien:
database/migrations/0008_saas_payment_imports.sql
app/imports.php
csvupload.php
Ziel: Zahlungsimport mit echter Vorschau, Dublettenprüfung und Audit-Trail, statt sofortiger Verarbeitung ohne Bestätigungsschritt wie im Legacy-Stand.
Umfang:
- Neue Tabellen
payment_import_batchesundpayment_import_rows(siehe Kernschema im Umstrukturierungsplan) protokollieren jeden Import: Datei, Prüfsumme, Zeitpunkt, jede einzelne Zeile mit Rohwerten, erkanntem Mitglied, Status und – nach Bestätigung – der erzeugten Ledger-Zeile. - Zweistufiger Ablauf: Hochladen erzeugt eine Vorschau (
status = previewed) ohne jede Buchung; erst der zweite Schritt „Import bestätigen“ committet die alsmatchederkannten Zeilen (status = committed). - Zeilen werden klassifiziert:
matched(wird gebucht),duplicate(gleicher Teilnehmer, gleicher Betrag, gleicher Tag existiert bereits als nicht-stornierte Ledger-Zahlung),unmatched(kein Mitglied perpaypal_nameoderdisplay_namegefunden),invalid(Betrag oder Datum nicht parsebar). - Zuordnungsregel entspricht der Legacy-Logik: Name aus der CSV wird gegen
paypal_nameoderdisplay_nameverglichen (case-insensitiv). - Buchung folgt demselben Zweig-Muster wie die übrige Sammelerfassung: Für
Teilnehmer mit
legacy_mitarbeiter_id(Default-Mandant) wird zusätzlich einekl_Einzahlungen-Zeile geschrieben und gespiegelt; alle anderen Mandanten werden direkt überledger_record_payment()gebucht. - Zugriffskontrolle ergänzt: Die Seite hatte zuvor gar keine Prüfung außer
CSRF. Jetzt gilt dieselbe Rollen-/Legacy-Fallback-Logik wie bei
einzahlung.php(owner,admin,treasurer). - Upload-Härtung aus M2 (Größen-/Typprüfung, Speicherung außerhalb des
direkt erreichbaren Codes unter
var/uploads, Löschung nach Verarbeitung) bleibt erhalten.
Live gegen die Dev-Datenbank getestet: Upload mit drei Testzeilen (Treffer, unbekannter Name, ungültiger Betrag), Vorschau-Klassifizierung geprüft, Import bestätigt (Dual-Write und Ledger-Spiegelung korrekt), erneuter Upload derselben Datei erkennt die bereits importierte Zeile korrekt als Dublette. Testdaten anschließend entfernt.
PDF-/Listenexport
Umgesetzte Dateien:
TCPDF/include/ (neu vendort)
TCPDF/fonts/ (nur die 14 PDF-Standard-Fonts, neu vendort)
exportKaffeeliste.php
scripts/http-smoke.php
Ziel: Der bestehende zweiseitige Kaffeeliste-Ausdruck (Vieltrinker-/ Wenigtrinker-Seite mit Wasserzeichen) bleibt optisch erhalten, liest aber tenant-sicher aus dem Ledger statt aus Legacy-Tabellen.
Umfang:
- TCPDF war im Repo nur teilweise vorhanden (
include/-Verzeichnis und Font-Definitionen fehlten komplett, siehe M0/M1). Nachvendort wurden nurinclude/und die 14 PDF-Standard-Fonts (Helvetica, Courier, Times, Symbol, ZapfDingbats – die einzigen tatsächlich genutzten) aus dem offiziellen TCPDF-6.6.2-Release, nicht die vollen ~25 MB an Unicode-Fonts, die für diesen Export nicht gebraucht werden. exportKaffeeliste.phphatte zuvor überhaupt keine Zugriffskontrolle (nicht einmal einencheckKaffeelisteAdmin-Aufruf) und bandconfig.phpdirekt statt der gemeinsamen Bootstrap-Kette ein. Jetzt gilt dieselbe Rollen-/Legacy-Fallback-Logik wie bei den anderen Treasurer-Seiten. Zusätzlich nutzt die Seite bewusst keine Ledger-native Fensterprüfung mit dem alten Legacy-Anker (jüngstes Datum inkl_Kaffeeverbrauch) mehr, sondern durchgängigledger_fetch_participants_by_window_marks()mittenant_settings.sheet_window_days– für alle Mandanten einheitlich, da es sich um einen reinen Lesereport ohne Schreibpfad handelt (siehe Cutover-Notiz: keine Notwendigkeit, hier Legacy-Verhalten 1:1 zu bewahren).- Guthaben kommt direkt aus der vorab geladenen Teilnehmerzusammenfassung
(
balance_cents) statt aus zwei separaten Datenbankabfragen pro Zeile (N+1-Muster im Original). - Preis pro Strich kommt aus
tenant_settings.mark_price_centsstatt auskl_config. scripts/http-smoke.phpprüft den PDF-Export jetzt als regulären Check (gültige PDF-Antwort) statt als bekannten offenen Punkt.
Live getestet: gültiges zweiseitiges PDF (58 KB) mit korrektem
Content-Type: application/pdf, Namen und Salden im PDF-Textstream
stichprobenartig verifiziert (unkomprimierte Testausgabe), Vieltrinker-/
Wenigtrinker-Aufteilung stimmt mit den Ledger-Daten überein.
Mailversand
Umgesetzte Dateien:
database/migrations/0009_saas_outbound_emails.sql
app/saas-mail.php (Vorlage + Versandlog)
mailversenden.php
scripts/http-smoke.php
Ziel: Nachvollziehbarer Versandjob mit Dry-Run und Versandlog statt sofortigem, ungeprüftem Versand.
Umfang:
mailversenden.phphatte zuvor keine Zugriffskontrolle, versendete auf jeden GET-Request sofort echte E-Mails und nutzte PHPMailer, dessen Quelldateien im Repo gar nicht vorhanden sind (nurcomposer.jsonund Lizenzdateien) – der Aufruf wäre also ohnehin mit einem Fatal Error abgebrochen. Zusätzlich waren SMTP-Host, Absender, PayPal-Link und FAQ-URL fest auf einen einzelnen Alt-Kunden (AOK) codiert.- Statt PHPMailer zu vendoren, nutzt der Versand jetzt die in M3 bereits
gebaute Mail-Abstraktion (
saas_send_mail()), die je nachAPP_MAIL_TRANSPORTentweder wirklich permail()verschickt oder (Standard im Dev-Modus) invar/mailprotokolliert. Damit entfällt die fehlende Abhängigkeit vollständig, und "Dry-Run" beziehungsweise "Log" sind bereits strukturell dieselbe Mechanik. - Neue Tabelle
outbound_emailsprotokolliert jeden Versandversuch: Mandant, Mitglied, Vorlage, Betreff, Status (dry_run/sent/failed), Zeitpunkt, Fehlermeldung. Das ist das im Plan geforderte Versandlog. - Echter zweistufiger Schutz: Das Formular hat eine standardmäßig aktive
"Dry-Run"-Checkbox. Im Dry-Run wird für jeden Empfänger ein Log-Eintrag
geschrieben, aber
saas_send_mail()nicht aufgerufen. Erst mit deaktivierter Checkbox wird wirklich versendet. - Der Mailtext ist jetzt tenant-generisch (
saas_render_balance_mail_body()): Saldo, optionaler PayPal-Link nur wenn der Mandant PayPal aktiviert hat, Link zum eigenen Dashboard übersaas_app_url(). Keine hartcodierten Alt-Kunden-Inhalte mehr. - Zugriffskontrolle ergänzt (owner/admin/treasurer + Legacy-Fallback).
scripts/http-smoke.php:mailversenden.phpist jetzt ein regulärer Check statt eines übersprungenen unsicheren GET-Aufrufs, da GET keine Seiteneffekte mehr hat.
Live getestet: GET zeigt nur das Formular (kein Versand), Dry-Run
protokolliert alle aktiven Mitglieder ohne var/mail-Dateien zu erzeugen,
Live-Versand erzeugt für jeden Empfänger eine Log-Datei mit korrekt
personalisiertem Inhalt (Guthaben- und Schuldenfall geprüft), Versandlog in
der UI zeigt die Einträge. Testdaten anschließend entfernt.
Jahresabschluss
Umgesetzte Dateien:
app/saas-mail.php (Mailvorlage)
jahresauswertung.php
scripts/http-smoke.php
Ziel: Ein generisches, mandantenfähiges Feature statt der ursprünglichen AOK-spezifischen Alt-Kunden-Logik.
Hintergrund: jahresauswertung.php verband sich bisher mit fest codierten
(kaputten) Zugangsdaten selbst zur Datenbank statt über config.php, hatte
keinerlei Zugriffskontrolle und kein CSRF, verteilte bei jedem Aufruf sofort
einen hart codierten Bonus-Topf (490 Striche à 0,20 €) und verschickte
dabei über PHPMailer (dessen Quelldateien im Repo fehlen) Mails mit
AOK-spezifischem Text. Mit dem Nutzer wurde abgestimmt, daraus ein echtes,
wiederverwendbares Feature zu bauen statt es nur zu deaktivieren oder rein
lesend umzusetzen.
Umfang:
- Admin gibt einen frei wählbaren Gesamtbetrag ein; das System verteilt ihn
proportional zu den in diesem Kalenderjahr gemachten Strichen
(
year_marksausledger_fetch_participant_summaries()) auf alle aktiven Mitglieder. - Standardmäßig aktive Dry-Run-Checkbox zeigt Name, Jahresstriche, Anteil und berechneten Bonus, ohne zu buchen oder Mails zu verschicken.
- Bei bestätigtem Lauf wird pro Mitglied mit einem Anteil > 0 eine
Zahlung gebucht (gleiches Zweig-Muster wie überall: Default-Mandant per
Dual-Write nach
kl_Einzahlungenplus Spiegelung, andere Mandanten direkt überledger_record_payment()mitsource = year_end_bonus) und eine personalisierte Mail übersaas_send_mail()verschickt und im Versandlog (outbound_emails, Vorlageyear_end_bonus) protokolliert. - Zugriffskontrolle ergänzt (owner/admin/treasurer + Legacy-Fallback).
scripts/http-smoke.php:jahresauswertung.phpist jetzt ein regulärer Check statt eines übersprungenen unsicheren Aufrufs, da GET keine Seiteneffekte mehr hat.
Live getestet: Dry-Run mit 100 € (korrekte proportionale Verteilung, Summe der Einzelbeträge ergibt exakt den Gesamtbetrag, keine Buchungen oder Mails), anschließender Live-Lauf bucht korrekt (Dual-Write und Ledger-Spiegelung stimmen mit den berechneten Beträgen überein) und verschickt personalisierte Mails; Versandlog zeigt alle Einträge korrekt. Testdaten anschließend vollständig entfernt.
Prüfstatus
- Golden Master: grün mit 104 Assertions.
- M4 Ledger-Migration: grün mit 73 Assertions.
- M4 Ledger-Service: grün mit 115 Assertions.
- HTTP-Smoke: grün mit 26 geprüften Seiten, keine übersprungenen oder bekannten offenen Punkte mehr.