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>
207 lines
9.5 KiB
Markdown
207 lines
9.5 KiB
Markdown
# 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:
|
||
|
||
```text
|
||
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_batches` und `payment_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 als `matched` erkannten 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 per
|
||
`paypal_name` oder `display_name` gefunden), `invalid` (Betrag oder Datum
|
||
nicht parsebar).
|
||
- Zuordnungsregel entspricht der Legacy-Logik: Name aus der CSV wird gegen
|
||
`paypal_name` oder `display_name` verglichen (case-insensitiv).
|
||
- Buchung folgt demselben Zweig-Muster wie die übrige Sammelerfassung: Für
|
||
Teilnehmer mit `legacy_mitarbeiter_id` (Default-Mandant) wird zusätzlich
|
||
eine `kl_Einzahlungen`-Zeile geschrieben und gespiegelt; alle anderen
|
||
Mandanten werden direkt über `ledger_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:
|
||
|
||
```text
|
||
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 nur
|
||
`include/` 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.php` hatte zuvor überhaupt keine Zugriffskontrolle
|
||
(nicht einmal einen `checkKaffeelisteAdmin`-Aufruf) und band `config.php`
|
||
direkt 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 in `kl_Kaffeeverbrauch`)
|
||
mehr, sondern durchgängig `ledger_fetch_participants_by_window_marks()`
|
||
mit `tenant_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_cents` statt aus
|
||
`kl_config`.
|
||
- `scripts/http-smoke.php` prü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:
|
||
|
||
```text
|
||
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.php` hatte zuvor keine Zugriffskontrolle, versendete auf
|
||
jeden GET-Request sofort echte E-Mails und nutzte PHPMailer, dessen
|
||
Quelldateien im Repo gar nicht vorhanden sind (nur `composer.json` und
|
||
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 nach
|
||
`APP_MAIL_TRANSPORT` entweder wirklich per `mail()` verschickt oder
|
||
(Standard im Dev-Modus) in `var/mail` protokolliert. Damit entfällt die
|
||
fehlende Abhängigkeit vollständig, und "Dry-Run" beziehungsweise
|
||
"Log" sind bereits strukturell dieselbe Mechanik.
|
||
- Neue Tabelle `outbound_emails` protokolliert 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 über `saas_app_url()`. Keine hartcodierten
|
||
Alt-Kunden-Inhalte mehr.
|
||
- Zugriffskontrolle ergänzt (owner/admin/treasurer + Legacy-Fallback).
|
||
- `scripts/http-smoke.php`: `mailversenden.php` ist 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:
|
||
|
||
```text
|
||
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_marks` aus `ledger_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_Einzahlungen` plus Spiegelung, andere Mandanten
|
||
direkt über `ledger_record_payment()` mit `source = year_end_bonus`) und
|
||
eine personalisierte Mail über `saas_send_mail()` verschickt und im
|
||
Versandlog (`outbound_emails`, Vorlage `year_end_bonus`) protokolliert.
|
||
- Zugriffskontrolle ergänzt (owner/admin/treasurer + Legacy-Fallback).
|
||
- `scripts/http-smoke.php`: `jahresauswertung.php` ist 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.
|