diff --git a/database/mysql-dev-schema.sql b/database/mysql-dev-schema.sql index 136f50f..31632f7 100644 --- a/database/mysql-dev-schema.sql +++ b/database/mysql-dev-schema.sql @@ -70,6 +70,13 @@ CREATE TABLE IF NOT EXISTS CoffeeSurveyResponses ( CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +CREATE TABLE IF NOT EXISTS dev_baseline_metadata ( + meta_key VARCHAR(100) PRIMARY KEY, + meta_value TEXT NOT NULL, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + INSERT INTO kl_config (id, KostenproStrich, paypaluse, paypallink, strichperweb) VALUES (1, 0.20, 0, '', 1) ON DUPLICATE KEY UPDATE @@ -77,3 +84,12 @@ ON DUPLICATE KEY UPDATE paypaluse = VALUES(paypaluse), strichperweb = VALUES(strichperweb); +INSERT INTO dev_baseline_metadata (meta_key, meta_value) +VALUES + ('baseline_kind', 'reconstructed_mysql_dev'), + ('legacy_database_status', 'no_ms_sql_legacy_snapshot_available'), + ('legacy_behavior_source', 'code_and_screenshots'), + ('visual_reference_path', 'docs/m0/screenshots'), + ('schema_source', 'database/mysql-dev-schema.sql') +ON DUPLICATE KEY UPDATE + meta_value = VALUES(meta_value); diff --git a/docs/m0/README.md b/docs/m0/README.md index b5b5c9a..fa0d7d5 100644 --- a/docs/m0/README.md +++ b/docs/m0/README.md @@ -13,9 +13,9 @@ Design-Erhalt und fachliche Vergleichstests. | Code-Inventar | begonnen | Siehe `code-inventory.md` | | Prozesslandkarte | begonnen | In `code-inventory.md` enthalten | | Sicherheitsbaseline | begonnen | Siehe `security-baseline.md` | -| DB-Schema-Export | vorbereitet | Siehe `schema-export.sql` | -| Golden-Master-Auswertungen | vorbereitet | Siehe `golden-master-queries.sql` | -| Design-/Screenshot-Referenz | vorbereitet | Siehe `screenshot-checklist.md` | +| DB-Schema-Export | nicht aus Alt-DB verfuegbar | Siehe `legacy-db-status.md` | +| Golden-Master-Auswertungen | nicht aus Alt-DB verfuegbar | Siehe `legacy-db-status.md` | +| Design-/Screenshot-Referenz | teilweise geliefert | Siehe `screenshot-checklist.md` | ## Lokal abgeschlossen @@ -25,6 +25,10 @@ Design-Erhalt und fachliche Vergleichstests. - Sicherheitsrisiken fuer M0 inventarisiert. - SQL-Vorlagen fuer Schema- und Golden-Master-Export angelegt. - Screenshot-Checkliste fuer Design-Erhalt angelegt. +- Erste Screenshot-Referenzen unter `docs/m0/screenshots/` abgelegt. +- Festgelegt: Ein alter MS-SQL-Datenbankstand steht aktuell nicht zur + Verfuegung; M0 arbeitet deshalb mit Code, Screenshots und rekonstruierter + MySQL-Dev-Datenbank als Ausgangsbasis. ## Lokale Einschraenkungen @@ -38,17 +42,17 @@ Design-Erhalt und fachliche Vergleichstests. Diese Punkte kann ich ohne externe Informationen nicht abschliessen: -1. Live- oder Staging-DB-Schema exportieren. -2. Tabellen, Spalten, Indizes, Constraints und Rowcounts verifizieren. -3. Golden-Master-Ergebnisse gegen echte Daten berechnen. -4. Screenshots der echten App-Zustaende aufnehmen, falls die App lokal nicht - lauffaehig ist. -5. Produktivumgebung dokumentieren: PHP-Version, Webserver, SQL-Server-Version, +1. Falls spaeter doch noch ein MS-SQL-Dump auftaucht: Schema und + Golden-Master-Werte nachtraeglich exportieren. +2. MySQL-Dev-Schema als neue rekonstruierte Baseline weiter verifizieren. +3. Fehlende Screenshot-Zustaende nachreichen oder bewusst als optional markieren. +4. Produktivumgebung dokumentieren: PHP-Version, Webserver, SQL-Server-Version, Auth-Setup, Cron/Job-Ausfuehrung. ## Was ich von dir brauche -Bitte stelle eines der folgenden Pakete bereit: +Falls ein alter MS-SQL-Stand spaeter noch auftaucht, bitte eines der folgenden +Pakete bereitstellen: - Idealerweise: einen anonymisierten SQL-Server-Schemaexport plus Rowcounts. - Alternativ: Zugriffsdaten zu einer Test-/Staging-DB, nicht zu Produktion. diff --git a/docs/m0/legacy-db-status.md b/docs/m0/legacy-db-status.md new file mode 100644 index 0000000..e182d82 --- /dev/null +++ b/docs/m0/legacy-db-status.md @@ -0,0 +1,47 @@ +# M0 Legacy-Datenbankstatus + +Stand: 2026-07-11 + +## Entscheidung + +Ein alter MS-SQL-Datenbankstand steht aktuell nicht zur Verfuegung. Deshalb +koennen `schema-export.sql` und `golden-master-queries.sql` derzeit nicht gegen +eine echte Legacy-Datenbank ausgefuehrt werden. + +Fuer die weitere Umstrukturierung gilt daher: + +- Der Legacy-Code ist die primaere Quelle fuer das fachliche Verhalten. +- Die Screenshots unter `docs/m0/screenshots/` sind die visuelle Referenz. +- Die MySQL-Testdatenbank ist die rekonstruierte Entwicklungsbasis. +- Fachliche Golden-Master-Werte werden aus rekonstruierten Testfaellen statt aus + historischer Produktivdatenbank aufgebaut. + +## Konsequenz fuer M1 + +M1 wird nicht als Vergleich gegen echte Alt-Daten gestartet, sondern als +rekonstruierter Golden-Master: + +1. Fachliche Regeln aus dem Code ableiten. +2. Kleine, kontrollierte Testdaten erzeugen. +3. Erwartete Salden, Jahreswerte, Striche, Einzahlungen und CSV-Importfaelle + manuell beziehungsweise per Testskript definieren. +4. Neue Implementierung gegen diese erwarteten Werte testen. + +Wenn spaeter doch noch ein MS-SQL-Dump oder Schemaexport auftaucht, werden die +urspruenglichen M0-SQL-Dateien nachtraeglich genutzt und die Baseline ergaenzt. + +## Aktuelle rekonstruierte Basis + +- MySQL-Dev-Schema: `database/mysql-dev-schema.sql` +- Initialisierung: `scripts/init-mysql-dev.php` +- Dev-Start: `scripts/run-dev-server.sh` +- MySQL-Kompatibilitaet: `lib/sqlsrv_mysql_compat.php` + +## Offene Punkte + +- Soll die MySQL-Testdatenbank ab jetzt die verbindliche Entwicklungsdatenbank + fuer M1/M2 sein? +- Soll das Ziel weiterhin eine angepasste Legacy-PHP-App sein oder ein sauberer + SaaS-Neuaufbau mit uebernommener Fachlogik? +- Welche Testfaelle muessen fachlich auf jeden Fall abgedeckt werden? + diff --git a/docs/m0/screenshot-checklist.md b/docs/m0/screenshot-checklist.md index 0cdc878..5181304 100644 --- a/docs/m0/screenshot-checklist.md +++ b/docs/m0/screenshot-checklist.md @@ -16,23 +16,38 @@ aktuellen Umgebung geliefert werden. ## Kernseiten -| Prioritaet | Seite | Rolle | Zweck | -| --- | --- | --- | --- | -| Hoch | `index.php` | Mitglied | Dashboard, Saldo, Jahreswerte, eigene Striche, PayPal | -| Hoch | `index.php` | Admin | Dashboard mit Admin-Sidebar | -| Hoch | `kaffeeliste.php` | Admin | Gesamtuebersicht und Aktionsleiste | -| Hoch | `stricheintragen.php` | Admin | Sammelerfassung Striche | -| Hoch | `einzahlung.php` | Admin | Sammelerfassung Einzahlungen | -| Hoch | `mitarbeiterverwalten.php` | Admin | Mitgliederliste und Formular | -| Hoch | `letzteneintraege.php` | Admin | Korrekturansicht mit Loeschaktionen | -| Mittel | `csvupload.php` | Admin | CSV-Upload-Formular und Ergebniszustand | -| Mittel | `hinweise.php` | Admin | Hinweise anlegen und Liste | -| Mittel | `faq.php` | Mitglied | FAQ-Inhalte und Typografie | -| Mittel | `namenanpassen.php` | Mitglied | Formular fuer Anzeigenamen | -| Niedrig | `teilnehmerauswertung.php` | Admin | Detailauswertung pro Teilnehmer | -| Niedrig | `mailausgebe.php` | Admin | Mail-/Adressausgabe | -| Niedrig | `umfrage.php` | Mitglied | Randmodul, falls SaaS-relevant | -| Niedrig | `umfrageergebnisse.php` | Admin | Randmodul, falls SaaS-relevant | +| Prioritaet | Seite | Rolle | Status | Zweck | +| --- | --- | --- | --- | --- | +| Hoch | `index.php` | Mitglied | vorhanden: `m0-member-dashboard-desktop.png`, `m0-member-dashboard-mobile.png` | Dashboard, Saldo, Jahreswerte, eigene Striche, PayPal | +| Hoch | `index.php` | Admin | aktuell identisch wie Mitglied | Dashboard mit Admin-Sidebar | +| Hoch | `kaffeeliste.php` | Admin | vorhanden: `m0-admin-kaffeeliste-desktop.png` | Gesamtuebersicht und Aktionsleiste | +| Hoch | `stricheintragen.php` | Admin | vorhanden: `m0-admin-stricheintragen-desktop.png` | Sammelerfassung Striche | +| Hoch | `einzahlung.php` | Admin | vorhanden: `m0-admin-einzahlung-desktop.png` | Sammelerfassung Einzahlungen | +| Hoch | `mitarbeiterverwalten.php` | Admin | vorhanden: `m0-admin-mitglieder-desktop.png` | Mitgliederliste und Formular | +| Hoch | `letzteneintraege.php` | Admin | vorhanden: `m0-admin-letzteneintraege-desktop.png` | Korrekturansicht mit Loeschaktionen | +| Mittel | `csvupload.php` | Admin | vorhanden: `m0-admin-csv-upload-desktop.png` | CSV-Upload-Formular und Ergebniszustand | +| Mittel | `hinweise.php` | Admin | vorhanden: `m0-admin-hinweise-desktop.png` | Hinweise anlegen und Liste | +| Mittel | `faq.php` | Mitglied | vorhanden: `m0-member-faq-desktop.png` | FAQ-Inhalte und Typografie | +| Mittel | `namenanpassen.php` | Mitglied | offen | Formular fuer Anzeigenamen | +| Niedrig | `teilnehmerauswertung.php` | Admin | offen | Detailauswertung pro Teilnehmer | +| Niedrig | `mailausgebe.php` | Admin | offen | Mail-/Adressausgabe | +| Niedrig | `umfrage.php` | Mitglied | offen, falls SaaS-relevant | Randmodul, falls SaaS-relevant | +| Niedrig | `umfrageergebnisse.php` | Admin | offen, falls SaaS-relevant | Randmodul, falls SaaS-relevant | + +## Gelieferte Dateien + +```text +docs/m0/screenshots/m0-admin-einzahlung-desktop.png +docs/m0/screenshots/m0-admin-csv-upload-desktop.png +docs/m0/screenshots/m0-admin-hinweise-desktop.png +docs/m0/screenshots/m0-admin-kaffeeliste-desktop.png +docs/m0/screenshots/m0-admin-letzteneintraege-desktop.png +docs/m0/screenshots/m0-admin-mitglieder-desktop.png +docs/m0/screenshots/m0-admin-stricheintragen-desktop.png +docs/m0/screenshots/m0-member-dashboard-desktop.png +docs/m0/screenshots/m0-member-dashboard-mobile.png +docs/m0/screenshots/m0-member-faq-desktop.png +``` ## Zustaende @@ -71,4 +86,3 @@ docs/m0/screenshots/ Vor dem Commit sollten Screenshots anonymisiert sein. Wenn echte Namen, E-Mails, PayPal-Daten oder Salden sichtbar sind, besser ausserhalb des Repos ablegen und nur als Referenz verwenden. - diff --git a/docs/m0/screenshots/m0-admin-csv-upload-desktop.png b/docs/m0/screenshots/m0-admin-csv-upload-desktop.png new file mode 100644 index 0000000..6855dac Binary files /dev/null and b/docs/m0/screenshots/m0-admin-csv-upload-desktop.png differ diff --git a/docs/m0/screenshots/m0-admin-einzahlung-desktop.png b/docs/m0/screenshots/m0-admin-einzahlung-desktop.png new file mode 100644 index 0000000..f4b4e2f Binary files /dev/null and b/docs/m0/screenshots/m0-admin-einzahlung-desktop.png differ diff --git a/docs/m0/screenshots/m0-admin-hinweise-desktop.png b/docs/m0/screenshots/m0-admin-hinweise-desktop.png new file mode 100644 index 0000000..4e77f6c Binary files /dev/null and b/docs/m0/screenshots/m0-admin-hinweise-desktop.png differ diff --git a/docs/m0/screenshots/m0-admin-kaffeeliste-desktop.png b/docs/m0/screenshots/m0-admin-kaffeeliste-desktop.png new file mode 100644 index 0000000..78ce5ce Binary files /dev/null and b/docs/m0/screenshots/m0-admin-kaffeeliste-desktop.png differ diff --git a/docs/m0/screenshots/m0-admin-letzteneintraege-desktop.png b/docs/m0/screenshots/m0-admin-letzteneintraege-desktop.png new file mode 100644 index 0000000..5556c8c Binary files /dev/null and b/docs/m0/screenshots/m0-admin-letzteneintraege-desktop.png differ diff --git a/docs/m0/screenshots/m0-admin-mitglieder-desktop.png b/docs/m0/screenshots/m0-admin-mitglieder-desktop.png new file mode 100644 index 0000000..b073278 Binary files /dev/null and b/docs/m0/screenshots/m0-admin-mitglieder-desktop.png differ diff --git a/docs/m0/screenshots/m0-admin-stricheintragen-desktop.png b/docs/m0/screenshots/m0-admin-stricheintragen-desktop.png new file mode 100644 index 0000000..f43491e Binary files /dev/null and b/docs/m0/screenshots/m0-admin-stricheintragen-desktop.png differ diff --git a/docs/m0/screenshots/m0-member-dashboard-desktop.png b/docs/m0/screenshots/m0-member-dashboard-desktop.png new file mode 100644 index 0000000..9634a5f Binary files /dev/null and b/docs/m0/screenshots/m0-member-dashboard-desktop.png differ diff --git a/docs/m0/screenshots/m0-member-dashboard-mobile.png b/docs/m0/screenshots/m0-member-dashboard-mobile.png new file mode 100644 index 0000000..483de08 Binary files /dev/null and b/docs/m0/screenshots/m0-member-dashboard-mobile.png differ diff --git a/docs/m0/screenshots/m0-member-faq-desktop.png b/docs/m0/screenshots/m0-member-faq-desktop.png new file mode 100644 index 0000000..b8d6171 Binary files /dev/null and b/docs/m0/screenshots/m0-member-faq-desktop.png differ diff --git a/docs/m1-reconstructed-golden-master.md b/docs/m1-reconstructed-golden-master.md new file mode 100644 index 0000000..64779b5 --- /dev/null +++ b/docs/m1-reconstructed-golden-master.md @@ -0,0 +1,146 @@ +# M1 Rekonstruierter Golden Master + +Stand: 2026-07-11 + +Da kein alter MS-SQL-Datenbankstand verfuegbar ist, wird M1 als rekonstruierter +Golden Master aufgebaut. Die Referenz entsteht aus Legacy-Code, Screenshots und +kontrollierten Testdaten in der MySQL-Dev-Datenbank. + +## Ziel + +Die wichtigsten fachlichen Regeln werden mit kleinen, nachvollziehbaren +Testfaellen festgeschrieben. Diese Tests dienen spaeter als Sicherheitsnetz fuer +die SaaS-Umstrukturierung. + +## Testdaten-Gruppen + +### Teilnehmer + +- Aktives normales Mitglied. +- Aktiver Admin. +- Inaktives Mitglied. +- Mitglied mit PayPal-Namen. +- Mitglied ohne Buchungen. + +### Buchungen + +- Einzahlung mit positivem Betrag. +- Mehrere Einzahlungen an unterschiedlichen Tagen. +- Strichbuchung mit einem Strich. +- Strichbuchung mit zwei Strichen. +- Sammelerfassung fuer mehrere Teilnehmer. +- Web-Strich-Eintrag mit `Eintragsart = 2`. + +### Salden + +- Positiver Kontostand. +- Negativer Kontostand. +- Nullsaldo. +- Jahreswerte fuer aktuelles Jahr. +- Altdaten aus Vorjahr, die nicht in Jahreswerte fallen. + +### CSV-Import + +- Treffer ueber Name. +- Treffer ueber `paypalname`. +- Dublette gleicher Teilnehmer, Betrag und Datum. +- Unbekannter Name. +- Negativer oder leerer Betrag. + +### Hinweise + +- Aktiver Hinweis. +- Abgelaufener Hinweis. +- Neuer Hinweis ueber Admin-Formular. + +## Erwartete Referenzwerte + +Fuer jeden Testfall sollen erwartete Werte dokumentiert werden: + +- Summe Einzahlungen. +- Summe Kosten. +- Summe Striche. +- Aktueller Stand. +- Jahresstriche. +- Jahreseinzahlungen. +- Jahresausgaben. +- Sichtbarkeit in Listen und Filtern. + +## Umsetzungsvorschlag + +1. `database/mysql-dev-schema.sql` um stabile Seed-Daten erweitern oder separates + Seed-Skript anlegen. Erledigt mit `scripts/seed-golden-master.php`. +2. `scripts/init-mysql-dev.php` so erweitern, dass es die Testdaten idempotent + anlegt. Entschieden: Golden-Master-Daten bleiben in einem separaten Skript, + damit normale Dev-Daten nicht zwangsweise ueberschrieben werden. +3. Ein erstes Vergleichsskript `scripts/check-golden-master.php` erstellen. + Erledigt. +4. Die wichtigsten Seiten per HTTP abrufen und auf erwartete Texte/Werte pruefen. +5. Danach M2 starten: technisches Fundament fuer SaaS-Struktur. + +## Skripte + +- `scripts/dev-db.php`: gemeinsame DB-Verbindung fuer Dev-Skripte. +- `scripts/golden-master-data.php`: definierte Testdaten und erwartete Werte. +- `scripts/seed-golden-master.php`: legt Golden-Master-Testdaten idempotent an. +- `scripts/check-golden-master.php`: prueft Salden, Jahreswerte, Rollen, + Listenfilter, CSV-Lookup und Hinweise. + +## Ausfuehrung + +Mit normal installierter PHP-CLI: + +```bash +php scripts/seed-golden-master.php +php scripts/check-golden-master.php +``` + +In der aktuellen lokalen Umgebung mit entpacktem PHP: + +```bash +LD_LIBRARY_PATH="$PWD/.local/php/usr/lib/x86_64-linux-gnu" \ + "$PWD/.local/php/usr/bin/php8.3" \ + -c "$PWD/.local/php-dev.ini" \ + scripts/seed-golden-master.php + +LD_LIBRARY_PATH="$PWD/.local/php/usr/lib/x86_64-linux-gnu" \ + "$PWD/.local/php/usr/bin/php8.3" \ + -c "$PWD/.local/php-dev.ini" \ + scripts/check-golden-master.php +``` + +Die Skripte erwarten die bekannten Dev-Umgebungsvariablen `DB_HOST`, `DB_NAME`, +`DB_USER` und `DB_PASS`. + +## Abgedeckte Testfaelle + +| Testfall | Fixture | +| --- | --- | +| Admin mit Buchung | `gm-admin@test.local` | +| Positiver Saldo mit Vorjahresdaten | `gm-positive@test.local` | +| Negativer Saldo | `gm-negative@test.local` | +| Nullsaldo | `gm-zero@test.local` | +| Inaktives Mitglied | `gm-inactive@test.local` | +| PayPal-Name fuer CSV-Lookup | `gm-paypal@test.local` | +| Mitglied ohne Buchungen | `gm-empty@test.local` | +| Vieltrinker fuer Vorderseite | `gm-heavy@test.local` | +| Aktiver Hinweis | `[GM] Aktiver Hinweis fuer Golden-Master-Test` | +| Abgelaufener Hinweis | `[GM] Abgelaufener Hinweis fuer Golden-Master-Test` | + +## Akzeptanzkriterien + +- Testdaten sind reproduzierbar. +- Tests koennen mehrfach laufen, ohne Duplikate zu erzeugen. +- Salden und Jahreswerte sind automatisch pruefbar. +- CSV-Importfaelle sind fachlich abgedeckt. +- Die wichtigsten UI-Seiten liefern HTTP 200 ohne PHP-Warnings. + +## Aktueller Pruefstatus + +Stand 2026-07-11: + +- `scripts/seed-golden-master.php` erfolgreich gegen die MySQL-Dev-Datenbank + ausgefuehrt. +- `scripts/check-golden-master.php` erfolgreich ausgefuehrt. +- Zweiter Seed+Check-Lauf erfolgreich, Idempotenz bestaetigt. +- Ergebnis: 104 Assertions bestanden. diff --git a/scripts/check-golden-master.php b/scripts/check-golden-master.php new file mode 100644 index 0000000..a7008bc --- /dev/null +++ b/scripts/check-golden-master.php @@ -0,0 +1,191 @@ +prepare($sql); + $stmt->execute(array_merge([$year, $year], $emails)); + + $actual = []; + while ($row = $stmt->fetch()) { + $actual[$row['email']] = [ + 'total_payments' => gm_decimal_to_cents($row['total_payments']), + 'total_costs' => gm_decimal_to_cents($row['total_costs']), + 'total_marks' => (int)$row['total_marks'], + 'balance' => gm_decimal_to_cents($row['balance']), + 'year_payments' => gm_decimal_to_cents($row['year_payments']), + 'year_costs' => gm_decimal_to_cents($row['year_costs']), + 'year_marks' => (int)$row['year_marks'], + ]; + } + + return $actual; +} + +function gm_fetch_sheet_emails(PDO $pdo, array $emails, string $mode): array +{ + $placeholders = implode(',', array_fill(0, count($emails), '?')); + $stmt = $pdo->prepare( + "SELECT MAX(DATE(v.Datum)) + FROM kl_Kaffeeverbrauch v + JOIN kl_Mitarbeiter m ON m.MitarbeiterID = v.MitarbeiterID + WHERE m.Email IN ({$placeholders}) + AND DATE(v.Datum) < CURDATE()" + ); + $stmt->execute($emails); + $referenceDate = $stmt->fetchColumn() ?: date('Y-m-d'); + + $operator = $mode === 'front' ? '>=' : '<'; + $sql = " + SELECT m.Email AS email + FROM kl_Mitarbeiter m + LEFT JOIN kl_Kaffeeverbrauch v + ON m.MitarbeiterID = v.MitarbeiterID + AND v.Datum >= DATE_ADD(?, INTERVAL -100 DAY) + WHERE m.aktiv = 1 + AND m.Email IN ({$placeholders}) + GROUP BY m.MitarbeiterID, m.Email + HAVING COALESCE(SUM(v.AnzahlStriche), 0) {$operator} 10 + ORDER BY m.Email + "; + $stmt = $pdo->prepare($sql); + $stmt->execute(array_merge([$referenceDate], $emails)); + + return $stmt->fetchAll(PDO::FETCH_COLUMN); +} + +$actualSummaries = gm_fetch_member_summaries($pdo, $emails); +gm_assert_equal('member summary count', count($expected), count($actualSummaries), $failures, $passes); + +foreach ($expected as $email => $summary) { + gm_assert_equal("summary exists {$email}", true, isset($actualSummaries[$email]), $failures, $passes); + if (!isset($actualSummaries[$email])) { + continue; + } + + foreach ($summary as $key => $expectedValue) { + gm_assert_equal("{$email} {$key}", $expectedValue, $actualSummaries[$email][$key], $failures, $passes); + } +} + +$stmt = $pdo->prepare('SELECT Email, aktiv, admin, paypalname FROM kl_Mitarbeiter WHERE Email IN (' . implode(',', array_fill(0, count($emails), '?')) . ')'); +$stmt->execute($emails); +$memberRows = []; +while ($row = $stmt->fetch()) { + $memberRows[$row['Email']] = $row; +} +foreach ($members as $member) { + $row = $memberRows[$member['email']] ?? null; + gm_assert_equal("member flags exist {$member['email']}", true, $row !== null, $failures, $passes); + if ($row === null) { + continue; + } + gm_assert_equal("{$member['email']} active", $member['active'], (int)$row['aktiv'], $failures, $passes); + gm_assert_equal("{$member['email']} admin", $member['admin'], (int)$row['admin'], $failures, $passes); + gm_assert_equal("{$member['email']} paypalname", $member['paypalname'], $row['paypalname'], $failures, $passes); +} + +$front = gm_fetch_sheet_emails($pdo, $emails, 'front'); +$back = gm_fetch_sheet_emails($pdo, $emails, 'back'); +gm_assert_equal('front sheet emails', gm_expected_front_sheet_emails(), $front, $failures, $passes); +gm_assert_equal('back sheet emails', gm_expected_back_sheet_emails(), $back, $failures, $passes); + +$stmt = $pdo->prepare('SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Name = ? OR paypalname = ?'); +$stmt->execute(['GM PayPal Alias', 'GM PayPal Alias']); +$paypalId = $stmt->fetchColumn(); +gm_assert_equal('csv paypalname lookup finds participant', true, $paypalId !== false, $failures, $passes); + +$stmt = $pdo->prepare( + 'SELECT COUNT(*) + FROM kl_Einzahlungen + WHERE MitarbeiterID = ? + AND Betrag = ? + AND DATE(Datum) = DATE(?)' +); +$stmt->execute([$paypalId ?: 0, gm_cents_to_decimal(750), gm_dates()['recent']]); +gm_assert_equal('csv duplicate basis exists', 1, (int)$stmt->fetchColumn(), $failures, $passes); + +$stmt = $pdo->prepare('SELECT COUNT(*) FROM kl_Mitarbeiter WHERE Name = ? OR paypalname = ?'); +$stmt->execute(['GM Unbekannt', 'GM Unbekannt']); +gm_assert_equal('csv unknown lookup misses participant', 0, (int)$stmt->fetchColumn(), $failures, $passes); + +$activeNoticeCount = (int)$pdo + ->query("SELECT COUNT(*) FROM kl_hinweise WHERE nachricht LIKE '[GM]%' AND gueltig_bis >= NOW()") + ->fetchColumn(); +$expiredNoticeCount = (int)$pdo + ->query("SELECT COUNT(*) FROM kl_hinweise WHERE nachricht LIKE '[GM]%' AND gueltig_bis < NOW()") + ->fetchColumn(); +gm_assert_equal('active GM notice count', 1, $activeNoticeCount, $failures, $passes); +gm_assert_equal('expired GM notice count', 1, $expiredNoticeCount, $failures, $passes); + +if ($failures !== []) { + echo "\nGolden master check failed with " . count($failures) . " failure(s):\n"; + foreach ($failures as $failure) { + echo "- {$failure}\n"; + } + exit(1); +} + +echo "\nGolden master check passed with {$passes} assertions.\n"; + diff --git a/scripts/dev-db.php b/scripts/dev-db.php new file mode 100644 index 0000000..d8c9715 --- /dev/null +++ b/scripts/dev-db.php @@ -0,0 +1,42 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => true, + ]); +} + +function dev_apply_schema(PDO $pdo): void +{ + $schema = file_get_contents(__DIR__ . '/../database/mysql-dev-schema.sql'); + if ($schema === false) { + fwrite(STDERR, "Could not read schema file.\n"); + exit(1); + } + + $pdo->exec($schema); +} + diff --git a/scripts/golden-master-data.php b/scripts/golden-master-data.php new file mode 100644 index 0000000..55b6fa3 --- /dev/null +++ b/scripts/golden-master-data.php @@ -0,0 +1,269 @@ + + */ +function gm_dates(): array +{ + $year = gm_current_year(); + + return [ + 'recent' => date('Y-m-d 10:00:00', strtotime('-1 day')), + 'current' => sprintf('%04d-01-15 09:00:00', $year), + 'previous' => sprintf('%04d-12-20 09:00:00', $year - 1), + 'notice_active' => date('Y-m-d 12:00:00', strtotime('+30 days')), + 'notice_expired' => date('Y-m-d 12:00:00', strtotime('-30 days')), + ]; +} + +/** + * @return array + */ +function gm_members(): array +{ + return [ + 'admin' => [ + 'name' => 'GM Admin', + 'email' => 'gm-admin@test.local', + 'active' => 1, + 'admin' => 1, + 'paypalname' => null, + ], + 'positive' => [ + 'name' => 'GM Positiv', + 'email' => 'gm-positive@test.local', + 'active' => 1, + 'admin' => 0, + 'paypalname' => null, + ], + 'negative' => [ + 'name' => 'GM Negativ', + 'email' => 'gm-negative@test.local', + 'active' => 1, + 'admin' => 0, + 'paypalname' => null, + ], + 'zero' => [ + 'name' => 'GM Nullsaldo', + 'email' => 'gm-zero@test.local', + 'active' => 1, + 'admin' => 0, + 'paypalname' => null, + ], + 'inactive' => [ + 'name' => 'GM Inaktiv', + 'email' => 'gm-inactive@test.local', + 'active' => 0, + 'admin' => 0, + 'paypalname' => null, + ], + 'paypal' => [ + 'name' => 'GM Paypal', + 'email' => 'gm-paypal@test.local', + 'active' => 1, + 'admin' => 0, + 'paypalname' => 'GM PayPal Alias', + ], + 'empty' => [ + 'name' => 'GM Ohne Buchungen', + 'email' => 'gm-empty@test.local', + 'active' => 1, + 'admin' => 0, + 'paypalname' => null, + ], + 'heavy' => [ + 'name' => 'GM Vieltrinker', + 'email' => 'gm-heavy@test.local', + 'active' => 1, + 'admin' => 0, + 'paypalname' => null, + ], + ]; +} + +/** + * @return array> + */ +function gm_payments(): array +{ + return [ + 'admin' => [ + ['amount_cents' => 300, 'date_key' => 'current'], + ], + 'positive' => [ + ['amount_cents' => 2000, 'date_key' => 'current'], + ['amount_cents' => 500, 'date_key' => 'previous'], + ], + 'negative' => [ + ['amount_cents' => 100, 'date_key' => 'current'], + ], + 'zero' => [ + ['amount_cents' => 200, 'date_key' => 'current'], + ], + 'inactive' => [ + ['amount_cents' => 1000, 'date_key' => 'current'], + ], + 'paypal' => [ + ['amount_cents' => 750, 'date_key' => 'recent'], + ], + 'heavy' => [ + ['amount_cents' => 500, 'date_key' => 'current'], + ], + ]; +} + +/** + * @return array> + */ +function gm_consumptions(): array +{ + return [ + 'admin' => [ + ['marks' => 1, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => 2], + ], + 'positive' => [ + ['marks' => 5, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => 2], + ['marks' => 2, 'unit_price_cents' => 20, 'date_key' => 'previous', 'entry_type' => null], + ], + 'negative' => [ + ['marks' => 10, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null], + ['marks' => 5, 'unit_price_cents' => 20, 'date_key' => 'previous', 'entry_type' => null], + ], + 'zero' => [ + ['marks' => 10, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null], + ], + 'inactive' => [ + ['marks' => 20, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null], + ], + 'paypal' => [ + ['marks' => 5, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => 2], + ], + 'heavy' => [ + ['marks' => 12, 'unit_price_cents' => 20, 'date_key' => 'recent', 'entry_type' => null], + ], + ]; +} + +/** + * @return array + */ +function gm_expected_summaries(): array +{ + return [ + 'gm-admin@test.local' => [ + 'total_payments' => 300, + 'total_costs' => 20, + 'total_marks' => 1, + 'balance' => 280, + 'year_payments' => 300, + 'year_costs' => 20, + 'year_marks' => 1, + ], + 'gm-positive@test.local' => [ + 'total_payments' => 2500, + 'total_costs' => 140, + 'total_marks' => 7, + 'balance' => 2360, + 'year_payments' => 2000, + 'year_costs' => 100, + 'year_marks' => 5, + ], + 'gm-negative@test.local' => [ + 'total_payments' => 100, + 'total_costs' => 300, + 'total_marks' => 15, + 'balance' => -200, + 'year_payments' => 100, + 'year_costs' => 200, + 'year_marks' => 10, + ], + 'gm-zero@test.local' => [ + 'total_payments' => 200, + 'total_costs' => 200, + 'total_marks' => 10, + 'balance' => 0, + 'year_payments' => 200, + 'year_costs' => 200, + 'year_marks' => 10, + ], + 'gm-inactive@test.local' => [ + 'total_payments' => 1000, + 'total_costs' => 400, + 'total_marks' => 20, + 'balance' => 600, + 'year_payments' => 1000, + 'year_costs' => 400, + 'year_marks' => 20, + ], + 'gm-paypal@test.local' => [ + 'total_payments' => 750, + 'total_costs' => 100, + 'total_marks' => 5, + 'balance' => 650, + 'year_payments' => 750, + 'year_costs' => 100, + 'year_marks' => 5, + ], + 'gm-empty@test.local' => [ + 'total_payments' => 0, + 'total_costs' => 0, + 'total_marks' => 0, + 'balance' => 0, + 'year_payments' => 0, + 'year_costs' => 0, + 'year_marks' => 0, + ], + 'gm-heavy@test.local' => [ + 'total_payments' => 500, + 'total_costs' => 240, + 'total_marks' => 12, + 'balance' => 260, + 'year_payments' => 500, + 'year_costs' => 240, + 'year_marks' => 12, + ], + ]; +} + +/** + * @return list + */ +function gm_expected_front_sheet_emails(): array +{ + return [ + 'gm-heavy@test.local', + 'gm-negative@test.local', + 'gm-zero@test.local', + ]; +} + +/** + * @return list + */ +function gm_expected_back_sheet_emails(): array +{ + return [ + 'gm-admin@test.local', + 'gm-empty@test.local', + 'gm-paypal@test.local', + 'gm-positive@test.local', + ]; +} + +function gm_cents_to_decimal(int $cents): string +{ + return number_format($cents / 100, 2, '.', ''); +} + +function gm_decimal_to_cents(mixed $value): int +{ + return (int)round(((float)$value) * 100); +} + diff --git a/scripts/seed-golden-master.php b/scripts/seed-golden-master.php new file mode 100644 index 0000000..588e47a --- /dev/null +++ b/scripts/seed-golden-master.php @@ -0,0 +1,129 @@ + $member['email'], $members)); +$placeholders = implode(',', array_fill(0, count($emails), '?')); + +$pdo->beginTransaction(); + +try { + $step = 'select existing fixture ids'; + $stmt = $pdo->prepare("SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email IN ({$placeholders})"); + $stmt->execute($emails); + $ids = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN)); + + if ($ids !== []) { + $idPlaceholders = implode(',', array_fill(0, count($ids), '?')); + $step = 'delete fixture payments'; + $stmt = $pdo->prepare("DELETE FROM kl_Einzahlungen WHERE MitarbeiterID IN ({$idPlaceholders})"); + $stmt->execute($ids); + $step = 'delete fixture consumption'; + $stmt = $pdo->prepare("DELETE FROM kl_Kaffeeverbrauch WHERE MitarbeiterID IN ({$idPlaceholders})"); + $stmt->execute($ids); + } + + $step = 'delete fixture members'; + $stmt = $pdo->prepare("DELETE FROM kl_Mitarbeiter WHERE Email IN ({$placeholders})"); + $stmt->execute($emails); + + $step = 'delete fixture notices'; + $pdo->exec("DELETE FROM kl_hinweise WHERE nachricht LIKE '[GM]%'"); + $step = 'delete fixture survey locks'; + $pdo->exec("DELETE FROM CoffeeSurveyVotedEmails WHERE EmailNorm LIKE 'gm-%@test.local'"); + + $insertMember = $pdo->prepare( + 'INSERT INTO kl_Mitarbeiter (Name, Email, paypalname, aktiv, admin) + VALUES (?, ?, ?, ?, ?)' + ); + $selectMemberId = $pdo->prepare('SELECT MitarbeiterID FROM kl_Mitarbeiter WHERE Email = ?'); + $memberIds = []; + + foreach ($members as $key => $member) { + $step = 'insert member ' . $key; + $insertMember->execute([ + $member['name'], + $member['email'], + $member['paypalname'], + $member['active'], + $member['admin'], + ]); + $selectMemberId->execute([$member['email']]); + $memberIds[$key] = (int)$selectMemberId->fetchColumn(); + } + + $insertPayment = $pdo->prepare( + 'INSERT INTO kl_Einzahlungen (MitarbeiterID, Betrag, Datum) + VALUES (?, ?, ?)' + ); + foreach ($payments as $memberKey => $entries) { + foreach ($entries as $entry) { + $step = 'insert payment ' . $memberKey; + $insertPayment->execute([ + $memberIds[$memberKey], + gm_cents_to_decimal($entry['amount_cents']), + $dates[$entry['date_key']], + ]); + } + } + + $insertConsumption = $pdo->prepare( + 'INSERT INTO kl_Kaffeeverbrauch + (MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum, Eintragsart) + VALUES (?, ?, ?, ?, ?, ?)' + ); + foreach ($consumptions as $memberKey => $entries) { + foreach ($entries as $entry) { + $step = 'insert consumption ' . $memberKey; + $costCents = $entry['marks'] * $entry['unit_price_cents']; + $insertConsumption->execute([ + $memberIds[$memberKey], + $entry['marks'], + gm_cents_to_decimal($costCents), + gm_cents_to_decimal($entry['unit_price_cents']), + $dates[$entry['date_key']], + $entry['entry_type'], + ]); + } + } + + $step = 'insert notices'; + $insertNotice = $pdo->prepare('INSERT INTO kl_hinweise (nachricht, gueltig_bis) VALUES (?, ?)'); + $insertNotice->execute(['[GM] Aktiver Hinweis fuer Golden-Master-Test', $dates['notice_active']]); + $insertNotice->execute(['[GM] Abgelaufener Hinweis fuer Golden-Master-Test', $dates['notice_expired']]); + + $metadata = [ + 'golden_master_seed' => 'seeded', + 'golden_master_seed_year' => (string)gm_current_year(), + 'golden_master_seed_members' => (string)count($members), + 'golden_master_seed_source' => 'scripts/seed-golden-master.php', + ]; + $insertMeta = $pdo->prepare( + 'INSERT INTO dev_baseline_metadata (meta_key, meta_value) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value)' + ); + foreach ($metadata as $key => $value) { + $step = 'insert metadata ' . $key; + $insertMeta->execute([$key, $value]); + } + + $pdo->commit(); +} catch (Throwable $e) { + $pdo->rollBack(); + fwrite(STDERR, "Golden master seed failed at {$step}: {$e->getMessage()}\n"); + exit(1); +} + +echo "Golden master fixtures seeded: " . count($members) . " members.\n";