Files
kaffeekasse-saas/docs/m6-import-export-mail.md
clemens 5e6262b6a8 Kaffeeliste-Ausdruck: adaptives Layout nach Mitgliederzahl
Der PDF-Export war bisher immer zweiseitig (Vieltrinker-/Wenigtrinker)
mit fester Mindestzeilenzahl, unabhängig von der tatsächlichen
Mitgliederzahl. Umgestellt auf:

- Bis 49 aktive Mitglieder: eine Seite ohne Trennung.
- Ab 50 Mitgliedern: weiterhin zwei Seiten (Vorder-/Rückseite).
- Zeilenhöhe richtet sich nach der Mitgliederzahl (16-40px), statt fest
  16px für alle.
- Neue Mandanten-Einstellungen: freie Zeilen für neue Mitglieder an/aus,
  Trennmodus bei zwei Seiten (Trinkverhalten oder alphabetisch).

Migration 0015 ergänzt die dafür nötigen tenant_settings-Spalten.
Gegen die Dev-DB verifiziert: 8 Mitglieder -> einseitiges PDF,
53 Mitglieder (temporäre Testdaten) -> zweiseitiges PDF, Einstellungen
inkl. Validierung geprüft.
2026-07-19 22:42:12 +02:00

233 lines
11 KiB
Markdown
Raw Permalink 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.
# 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.
### Nachtrag: Adaptives Layout nach Mitgliederzahl (2026-07-19)
Der Export war bis dahin immer zweiseitig mit fester Mindestzeilenzahl
(63/64 Zeilen a 16px), unabhängig von der tatsächlichen Mitgliederzahl.
Umgestellt auf:
- Bis 49 aktive Mitglieder: eine Seite, keine Vieltrinker-/Wenigtrinker-
Trennung. Ab 50 (`EXPORT_MULTI_PAGE_THRESHOLD` in
`exportKaffeeliste.php`): zwei Seiten wie bisher.
- Zeilenhöhe wird aus einem festen Platzbudget pro Seite errechnet
(`export_row_height_px()`), begrenzt auf 1640px weniger Mitglieder
ergeben größere statt winziger Zeilen.
- Neue Mandanten-Einstellungen (`tenant_settings.pdf_show_empty_rows`,
`tenant_settings.pdf_split_mode`, Migration
`0015_saas_pdf_export_settings.sql`, Formular in
`mandant-einstellungen.php`): Admin legt fest, ob freie Zeilen für neue
Mitglieder aufgefüllt werden, und ob die Zweiseiten-Trennung nach
Trinkverhalten (wie bisher) oder alphabetisch erfolgt.
- Verifiziert gegen die Dev-DB: 8 aktive Mitglieder ergeben ein
einseitiges PDF (`/Count 1`), 53 aktive Mitglieder ein zweiseitiges
(`/Count 2`); `saas_update_tenant_settings()` speichert und validiert
die neuen Felder korrekt (ungültiger `pdf_split_mode` wird abgelehnt).
- RFID-Kartenerfassung als künftige Erfassungsmethode ist weiterhin nur
im Backlog vermerkt (`docs/backlog-druck-und-landingpage.md`), nicht
umgesetzt und nicht beworben.
## 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.