diff --git a/app/saas-mail.php b/app/saas-mail.php index 6e147a7..b8964dd 100644 --- a/app/saas-mail.php +++ b/app/saas-mail.php @@ -210,6 +210,24 @@ function saas_fetch_outbound_email_log(PDO $pdo, int $tenantId, int $limit = 50) return $stmt->fetchAll(); } +function saas_render_year_end_bonus_mail_body(string $displayName, int $yearMarks, int $bonusCents, string $dashboardUrl): string +{ + $bonus = saas_format_money_cents($bonusCents) . ' €'; + $lines = [ + "Hallo {$displayName},", + '', + "vielen Dank für deine Nutzung der Kaffeeliste in diesem Jahr!", + "Du hast dieses Jahr {$yearMarks} Striche gemacht.", + "Dafür wurden dir {$bonus} auf deinem Kaffeelisten-Konto gutgeschrieben.", + '', + "Deinen aktuellen Stand findest du hier: {$dashboardUrl}", + '', + 'Deine Kaffeeliste', + ]; + + return implode("\n", $lines); +} + function saas_send_invite_mail(string $to, string $tenantName, string $role, string $token): array { $link = saas_app_url('passwort-zuruecksetzen.php?token=' . urlencode($token)); diff --git a/docs/m6-import-export-mail.md b/docs/m6-import-export-mail.md index a3a1b5f..c514175 100644 --- a/docs/m6-import-export-mail.md +++ b/docs/m6-import-export-mail.md @@ -149,10 +149,58 @@ 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 25 geprüften Seiten (PDF-Export und Mailversand - jetzt regulär statt bekannter offener Punkte). +- HTTP-Smoke: grün mit 26 geprüften Seiten, keine übersprungenen oder + bekannten offenen Punkte mehr. diff --git a/docs/saas-umstrukturierungsplan.md b/docs/saas-umstrukturierungsplan.md index e7df204..e06ed78 100644 --- a/docs/saas-umstrukturierungsplan.md +++ b/docs/saas-umstrukturierungsplan.md @@ -306,7 +306,7 @@ Nicht tun: | M3 | SaaS-Basis | Abgeschlossen: Tenants, User, Registrierung, Login, Rollen, Mail-Links und zentrale Mandantenauswahl funktionieren | | M4 | Datenmigration | Gestartet: Ledger-Tabelle, Legacy-Backfill, Paritätscheck, Ledger-Service und Preview sind umgesetzt | | M5 | App-Kern | Weit fortgeschritten: Kernseiten lesen und schreiben tenant-sicher gegen das Ledger, inklusive Hinweise und rollenbasiertem Zugang; offen ist ein eigener Zahlungs-Screen | -| M6 | Betriebsflows | Weit fortgeschritten: CSV-Import, PDF-Export und Mailversand stehen; Jahresprozesse offen | +| M6 | Betriebsflows | Abgeschlossen: Import, Export, Mail und Jahresabschluss sind auditierbar | | M7 | Landingpage | Public-Seite und Auth-Seiten sind im gemeinsamen Stil nutzbar; spätere Ausbaustufen folgen | | M8 | Härtung | Betrieb, Datenschutz, Monitoring und Isolation sind geprüft | | M9 | Cutover | Produktivumstellung ist vorbereitet und Legacy ist read-only | @@ -585,13 +585,21 @@ Schritte: PayPal-Link, FAQ-URL). Ersetzt durch die bestehende `saas_send_mail()`- Abstraktion aus M3, eine neue `outbound_emails`-Tabelle als Versandlog und eine standardmäßig aktive Dry-Run-Option. -- Jahresauswertung beziehungsweise Jahresbuchungen tenant-sicher abbilden. +- Jahresauswertung beziehungsweise Jahresbuchungen tenant-sicher abbilden: + erledigt. Die ursprüngliche Seite verband sich mit fest codierten, + kaputten Zugangsdaten selbst zur Datenbank (an `config.php` vorbei), + hatte keine Zugriffskontrolle und verteilte bei jedem Aufruf sofort einen + hart codierten Bonus-Topf mit AOK-spezifischem Mailtext. Nach Abstimmung + mit dem Kunden als generisches Feature umgesetzt: frei wählbarer + Gesamtbetrag, proportionale Verteilung nach Jahresstrichen, Dry-Run, + Buchung und Mailversand mit Protokoll. Ergebnis: - Kassenverwaltung ist für reale Betriebsabläufe vollständig. - Import/Export/Mail sind auditierbar. -- Stand: gestartet. Dokumentation: `docs/m6-import-export-mail.md`. +- Stand: abgeschlossen für den M6-Scope. Dokumentation: + `docs/m6-import-export-mail.md`. Abhängigkeiten: diff --git a/jahresauswertung.php b/jahresauswertung.php index ef2f317..185f118 100644 --- a/jahresauswertung.php +++ b/jahresauswertung.php @@ -1,177 +1,189 @@ "database", - "Uid" => "user", - "PWD" => "password", - "TrustServerCertificate"=>true -); -$stricheAnzupassen = 490; // Anzahl der neuen Striche -$betragProStrich = 0.20; // Betrag pro Strich in Euro +include "functions.php"; +require_once __DIR__ . "/app/ledger.php"; +require_once __DIR__ . "/app/saas-mail.php"; +app_require_csrf(); +include "header.php"; +include "headerline.php"; +include "nav.php"; -// Testmodus aktivieren -$testmodus = false; - - - -use PHPMailer\PHPMailer\PHPMailer; -use PHPMailer\PHPMailer\SMTP; -use PHPMailer\PHPMailer\Exception; - -require 'PHPMailer/src/Exception.php'; -require 'PHPMailer/src/PHPMailer.php'; -require 'PHPMailer/src/SMTP.php'; - -// PHPMailer konfigurieren -function sendeMail($empfaenger, $betreff, $inhalt, $testmodus) -{ - $mail = new PHPMailer(true); - - try { - // Server-Einstellungen - $mail->isSMTP(); - $mail->Host = 'smtpv.aoknds.aok'; // SMTP-Server - $mail->Timeout = 180; - $mail->SMTPAuth = false; - $mail->Port = 25; - - // Absender - $mail->setFrom('kaffeelistesb3@nds.aok.de', 'Kaffeeliste'); - - // Empfänger - if ($testmodus) { - $mail->addAddress('kaffeelistesb3@nds.aok.de'); // Testadresse - } else { - $mail->addAddress($empfaenger); // Tatsächlicher Empfänger - } - - // Inhalt - $mail->isHTML(true); - $mail->Subject = $betreff; - $mail->Body = utf8_decode($inhalt); - - // Senden - $mail->send(); - echo "E-Mail erfolgreich gesendet an: " . ($testmodus ? 'kaffeelistesb3@nds.aok.de' : $empfaenger) . "\n"; - } catch (Exception $e) { - echo "E-Mail konnte nicht gesendet werden. Fehler: {$mail->ErrorInfo}\n"; - } -} - -// Verbindung herstellen -$conn = sqlsrv_connect($serverName, $connectionOptions); -if ($conn === false) { - die(print_r(sqlsrv_errors(), true)); -} - -// Aktuelles Jahr ermitteln -$currentYear = date("Y"); - -// SQL-Abfrage: Gesamtanzahl der Striche pro Mitarbeiter im aktuellen Jahr mit Namen und E-Mail -$sql = " - SELECT - m.MitarbeiterID, - m.Name, - m.Email, - SUM(v.AnzahlStriche) AS GesamtStriche - FROM kl_Kaffeeverbrauch v - JOIN kl_Mitarbeiter m ON v.MitarbeiterID = m.MitarbeiterID - WHERE YEAR(v.Datum) = ? AND m.aktiv = 1 - GROUP BY m.MitarbeiterID, m.Name, m.Email -"; -$params = [$currentYear]; -$stmt = sqlsrv_query($conn, $sql, $params); - -if ($stmt === false) { - die(print_r(sqlsrv_errors(), true)); -} - -// Ergebnisse verarbeiten -$mitarbeiterDaten = []; -$gesamtStriche = 0; - -while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { - $mitarbeiterDaten[] = $row; - $gesamtStriche += $row['GesamtStriche']; -} - -echo $gesamtStriche; - -// Neue Striche proportional verteilen -$verteilung = []; - -foreach ($mitarbeiterDaten as $mitarbeiter) { - $mitarbeiterID = $mitarbeiter['MitarbeiterID']; - $anteil = $mitarbeiter['GesamtStriche'] / $gesamtStriche; - $neueStriche = round($anteil * $stricheAnzupassen); - $betrag = $neueStriche * $betragProStrich; - - $verteilung[] = [ - 'MitarbeiterID' => $mitarbeiterID, - 'Name' => $mitarbeiter['Name'], - 'Email' => $mitarbeiter['Email'], - 'JahrStriche' => $mitarbeiter['GesamtStriche'], - 'NeueStriche' => $neueStriche, - 'Betrag' => $betrag - ]; - - // SQL-Befehl vorbereiten - $insertSql = " - INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) - VALUES (?, ?, GETDATE()) - "; - $insertParams = [$mitarbeiterID, $betrag]; - - if ($testmodus) { - // SQL-Befehl und Parameter anzeigen - echo "SQL-Befehl: $insertSql\n"; - echo "Parameter: " . json_encode($insertParams) . "\n"; - // E-Mail vorbereiten - - } else { - // SQL-Befehl ausführen - $insertStmt = sqlsrv_query($conn, $insertSql, $insertParams); - if ($insertStmt === false) { - die(print_r(sqlsrv_errors(), true)); - } - // E-Mail vorbereiten - - } - - - - $betreff = "Kaffeeliste - Weihnachten"; - $inhalt = " -

Hallo {$mitarbeiter['Name']},

-

vielen Dank für deine Nutzung der Kaffeeliste in diesem Jahr!

-

Du hast dieses Jahr {$mitarbeiter['GesamtStriche']} Kaffee bezogen.

-

Deswegen wurden dir $betrag Euro auf deinem Konto gutgeschrieben.

-

-

Wir wünschen dir eine frohe Weihnachtszeit und einen guten Rutsch ins neue Jahr.

-

Deine ARGE Kaffeeliste

- "; - $empfaenger = $mitarbeiter['Email']; - // E-Mail senden - sendeMail($empfaenger, $betreff, $inhalt, $testmodus); - - -} - - - -// Sortiere die Verteilung nach JahrStriche (absteigend) -usort($verteilung, function ($a, $b) { - return $b['JahrStriche'] <=> $a['JahrStriche']; -}); - -// Ergebnisse ausgeben -header('Content-Type: application/json'); -#echo json_encode($verteilung, JSON_PRETTY_PRINT); - -// Verbindung schließen -sqlsrv_close($conn); ?> + + + + diff --git a/scripts/http-smoke.php b/scripts/http-smoke.php index 1f35677..18a82e1 100644 --- a/scripts/http-smoke.php +++ b/scripts/http-smoke.php @@ -142,16 +142,17 @@ $checks = [ 'path' => 'mailversenden.php', 'contains' => ['Info-Mail versenden', 'Dry-Run', 'Versandlog'], ], + [ + 'label' => 'Jahresabschluss', + 'path' => 'jahresauswertung.php', + 'contains' => ['Jahresabschluss', 'Dry-Run', 'Jahresstriche gesamt'], + ], ]; $knownIssueChecks = [ ]; $skippedUnsafe = [ - [ - 'path' => 'jahresauswertung.php', - 'reason' => 'GET kann Jahresbuchungen schreiben und E-Mails versenden; PHPMailer-Abhängigkeit ist zudem unvollständig.', - ], ]; function smoke_fetch(string $url): array