M0 Check DB Testdata
@@ -70,6 +70,13 @@ CREATE TABLE IF NOT EXISTS CoffeeSurveyResponses (
|
|||||||
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) 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)
|
INSERT INTO kl_config (id, KostenproStrich, paypaluse, paypallink, strichperweb)
|
||||||
VALUES (1, 0.20, 0, '', 1)
|
VALUES (1, 0.20, 0, '', 1)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
@@ -77,3 +84,12 @@ ON DUPLICATE KEY UPDATE
|
|||||||
paypaluse = VALUES(paypaluse),
|
paypaluse = VALUES(paypaluse),
|
||||||
strichperweb = VALUES(strichperweb);
|
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);
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ Design-Erhalt und fachliche Vergleichstests.
|
|||||||
| Code-Inventar | begonnen | Siehe `code-inventory.md` |
|
| Code-Inventar | begonnen | Siehe `code-inventory.md` |
|
||||||
| Prozesslandkarte | begonnen | In `code-inventory.md` enthalten |
|
| Prozesslandkarte | begonnen | In `code-inventory.md` enthalten |
|
||||||
| Sicherheitsbaseline | begonnen | Siehe `security-baseline.md` |
|
| Sicherheitsbaseline | begonnen | Siehe `security-baseline.md` |
|
||||||
| DB-Schema-Export | vorbereitet | Siehe `schema-export.sql` |
|
| DB-Schema-Export | nicht aus Alt-DB verfuegbar | Siehe `legacy-db-status.md` |
|
||||||
| Golden-Master-Auswertungen | vorbereitet | Siehe `golden-master-queries.sql` |
|
| Golden-Master-Auswertungen | nicht aus Alt-DB verfuegbar | Siehe `legacy-db-status.md` |
|
||||||
| Design-/Screenshot-Referenz | vorbereitet | Siehe `screenshot-checklist.md` |
|
| Design-/Screenshot-Referenz | teilweise geliefert | Siehe `screenshot-checklist.md` |
|
||||||
|
|
||||||
## Lokal abgeschlossen
|
## Lokal abgeschlossen
|
||||||
|
|
||||||
@@ -25,6 +25,10 @@ Design-Erhalt und fachliche Vergleichstests.
|
|||||||
- Sicherheitsrisiken fuer M0 inventarisiert.
|
- Sicherheitsrisiken fuer M0 inventarisiert.
|
||||||
- SQL-Vorlagen fuer Schema- und Golden-Master-Export angelegt.
|
- SQL-Vorlagen fuer Schema- und Golden-Master-Export angelegt.
|
||||||
- Screenshot-Checkliste fuer Design-Erhalt 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
|
## Lokale Einschraenkungen
|
||||||
|
|
||||||
@@ -38,17 +42,17 @@ Design-Erhalt und fachliche Vergleichstests.
|
|||||||
|
|
||||||
Diese Punkte kann ich ohne externe Informationen nicht abschliessen:
|
Diese Punkte kann ich ohne externe Informationen nicht abschliessen:
|
||||||
|
|
||||||
1. Live- oder Staging-DB-Schema exportieren.
|
1. Falls spaeter doch noch ein MS-SQL-Dump auftaucht: Schema und
|
||||||
2. Tabellen, Spalten, Indizes, Constraints und Rowcounts verifizieren.
|
Golden-Master-Werte nachtraeglich exportieren.
|
||||||
3. Golden-Master-Ergebnisse gegen echte Daten berechnen.
|
2. MySQL-Dev-Schema als neue rekonstruierte Baseline weiter verifizieren.
|
||||||
4. Screenshots der echten App-Zustaende aufnehmen, falls die App lokal nicht
|
3. Fehlende Screenshot-Zustaende nachreichen oder bewusst als optional markieren.
|
||||||
lauffaehig ist.
|
4. Produktivumgebung dokumentieren: PHP-Version, Webserver, SQL-Server-Version,
|
||||||
5. Produktivumgebung dokumentieren: PHP-Version, Webserver, SQL-Server-Version,
|
|
||||||
Auth-Setup, Cron/Job-Ausfuehrung.
|
Auth-Setup, Cron/Job-Ausfuehrung.
|
||||||
|
|
||||||
## Was ich von dir brauche
|
## 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.
|
- Idealerweise: einen anonymisierten SQL-Server-Schemaexport plus Rowcounts.
|
||||||
- Alternativ: Zugriffsdaten zu einer Test-/Staging-DB, nicht zu Produktion.
|
- Alternativ: Zugriffsdaten zu einer Test-/Staging-DB, nicht zu Produktion.
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|
||||||
@@ -16,23 +16,38 @@ aktuellen Umgebung geliefert werden.
|
|||||||
|
|
||||||
## Kernseiten
|
## Kernseiten
|
||||||
|
|
||||||
| Prioritaet | Seite | Rolle | Zweck |
|
| Prioritaet | Seite | Rolle | Status | Zweck |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| Hoch | `index.php` | Mitglied | Dashboard, Saldo, Jahreswerte, eigene Striche, PayPal |
|
| 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 | Dashboard mit Admin-Sidebar |
|
| Hoch | `index.php` | Admin | aktuell identisch wie Mitglied | Dashboard mit Admin-Sidebar |
|
||||||
| Hoch | `kaffeeliste.php` | Admin | Gesamtuebersicht und Aktionsleiste |
|
| Hoch | `kaffeeliste.php` | Admin | vorhanden: `m0-admin-kaffeeliste-desktop.png` | Gesamtuebersicht und Aktionsleiste |
|
||||||
| Hoch | `stricheintragen.php` | Admin | Sammelerfassung Striche |
|
| Hoch | `stricheintragen.php` | Admin | vorhanden: `m0-admin-stricheintragen-desktop.png` | Sammelerfassung Striche |
|
||||||
| Hoch | `einzahlung.php` | Admin | Sammelerfassung Einzahlungen |
|
| Hoch | `einzahlung.php` | Admin | vorhanden: `m0-admin-einzahlung-desktop.png` | Sammelerfassung Einzahlungen |
|
||||||
| Hoch | `mitarbeiterverwalten.php` | Admin | Mitgliederliste und Formular |
|
| Hoch | `mitarbeiterverwalten.php` | Admin | vorhanden: `m0-admin-mitglieder-desktop.png` | Mitgliederliste und Formular |
|
||||||
| Hoch | `letzteneintraege.php` | Admin | Korrekturansicht mit Loeschaktionen |
|
| Hoch | `letzteneintraege.php` | Admin | vorhanden: `m0-admin-letzteneintraege-desktop.png` | Korrekturansicht mit Loeschaktionen |
|
||||||
| Mittel | `csvupload.php` | Admin | CSV-Upload-Formular und Ergebniszustand |
|
| Mittel | `csvupload.php` | Admin | vorhanden: `m0-admin-csv-upload-desktop.png` | CSV-Upload-Formular und Ergebniszustand |
|
||||||
| Mittel | `hinweise.php` | Admin | Hinweise anlegen und Liste |
|
| Mittel | `hinweise.php` | Admin | vorhanden: `m0-admin-hinweise-desktop.png` | Hinweise anlegen und Liste |
|
||||||
| Mittel | `faq.php` | Mitglied | FAQ-Inhalte und Typografie |
|
| Mittel | `faq.php` | Mitglied | vorhanden: `m0-member-faq-desktop.png` | FAQ-Inhalte und Typografie |
|
||||||
| Mittel | `namenanpassen.php` | Mitglied | Formular fuer Anzeigenamen |
|
| Mittel | `namenanpassen.php` | Mitglied | offen | Formular fuer Anzeigenamen |
|
||||||
| Niedrig | `teilnehmerauswertung.php` | Admin | Detailauswertung pro Teilnehmer |
|
| Niedrig | `teilnehmerauswertung.php` | Admin | offen | Detailauswertung pro Teilnehmer |
|
||||||
| Niedrig | `mailausgebe.php` | Admin | Mail-/Adressausgabe |
|
| Niedrig | `mailausgebe.php` | Admin | offen | Mail-/Adressausgabe |
|
||||||
| Niedrig | `umfrage.php` | Mitglied | Randmodul, falls SaaS-relevant |
|
| Niedrig | `umfrage.php` | Mitglied | offen, falls SaaS-relevant | Randmodul, falls SaaS-relevant |
|
||||||
| Niedrig | `umfrageergebnisse.php` | Admin | 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
|
## Zustaende
|
||||||
|
|
||||||
@@ -71,4 +86,3 @@ docs/m0/screenshots/
|
|||||||
Vor dem Commit sollten Screenshots anonymisiert sein. Wenn echte Namen,
|
Vor dem Commit sollten Screenshots anonymisiert sein. Wenn echte Namen,
|
||||||
E-Mails, PayPal-Daten oder Salden sichtbar sind, besser ausserhalb des Repos
|
E-Mails, PayPal-Daten oder Salden sichtbar sind, besser ausserhalb des Repos
|
||||||
ablegen und nur als Referenz verwenden.
|
ablegen und nur als Referenz verwenden.
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 210 KiB |
@@ -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.
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/dev-db.php';
|
||||||
|
require __DIR__ . '/golden-master-data.php';
|
||||||
|
|
||||||
|
$pdo = dev_pdo();
|
||||||
|
$expected = gm_expected_summaries();
|
||||||
|
$members = gm_members();
|
||||||
|
$emails = array_keys($expected);
|
||||||
|
$failures = [];
|
||||||
|
$passes = 0;
|
||||||
|
|
||||||
|
function gm_assert_equal(string $label, mixed $expected, mixed $actual, array &$failures, int &$passes): void
|
||||||
|
{
|
||||||
|
if ($expected === $actual) {
|
||||||
|
$passes++;
|
||||||
|
echo "PASS {$label}\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$failures[] = "{$label}: expected " . var_export($expected, true) . ', got ' . var_export($actual, true);
|
||||||
|
echo "FAIL {$label}\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
function gm_fetch_member_summaries(PDO $pdo, array $emails): array
|
||||||
|
{
|
||||||
|
$placeholders = implode(',', array_fill(0, count($emails), '?'));
|
||||||
|
$year = gm_current_year();
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
m.Email AS email,
|
||||||
|
COALESCE(p.total_payments, 0) AS total_payments,
|
||||||
|
COALESCE(c.total_costs, 0) AS total_costs,
|
||||||
|
COALESCE(c.total_marks, 0) AS total_marks,
|
||||||
|
COALESCE(p.total_payments, 0) - COALESCE(c.total_costs, 0) AS balance,
|
||||||
|
COALESCE(yp.year_payments, 0) AS year_payments,
|
||||||
|
COALESCE(yc.year_costs, 0) AS year_costs,
|
||||||
|
COALESCE(yc.year_marks, 0) AS year_marks
|
||||||
|
FROM kl_Mitarbeiter m
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT MitarbeiterID, SUM(Betrag) AS total_payments
|
||||||
|
FROM kl_Einzahlungen
|
||||||
|
GROUP BY MitarbeiterID
|
||||||
|
) p ON p.MitarbeiterID = m.MitarbeiterID
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT MitarbeiterID, SUM(Kosten) AS total_costs, SUM(AnzahlStriche) AS total_marks
|
||||||
|
FROM kl_Kaffeeverbrauch
|
||||||
|
GROUP BY MitarbeiterID
|
||||||
|
) c ON c.MitarbeiterID = m.MitarbeiterID
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT MitarbeiterID, SUM(Betrag) AS year_payments
|
||||||
|
FROM kl_Einzahlungen
|
||||||
|
WHERE YEAR(Datum) = ?
|
||||||
|
GROUP BY MitarbeiterID
|
||||||
|
) yp ON yp.MitarbeiterID = m.MitarbeiterID
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT MitarbeiterID, SUM(Kosten) AS year_costs, SUM(AnzahlStriche) AS year_marks
|
||||||
|
FROM kl_Kaffeeverbrauch
|
||||||
|
WHERE YEAR(Datum) = ?
|
||||||
|
GROUP BY MitarbeiterID
|
||||||
|
) yc ON yc.MitarbeiterID = m.MitarbeiterID
|
||||||
|
WHERE m.Email IN ({$placeholders})
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $pdo->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";
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
function dev_required_env(string $name): string
|
||||||
|
{
|
||||||
|
$value = getenv($name);
|
||||||
|
if ($value === false || trim($value) === '') {
|
||||||
|
fwrite(STDERR, "Missing environment variable: {$name}\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dev_pdo(): PDO
|
||||||
|
{
|
||||||
|
$dsn = sprintf(
|
||||||
|
'mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4',
|
||||||
|
dev_required_env('DB_HOST'),
|
||||||
|
getenv('DB_PORT') ?: '3306',
|
||||||
|
dev_required_env('DB_NAME')
|
||||||
|
);
|
||||||
|
|
||||||
|
return new PDO($dsn, dev_required_env('DB_USER'), dev_required_env('DB_PASS'), [
|
||||||
|
PDO::ATTR_ERRMODE => 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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
function gm_current_year(): int
|
||||||
|
{
|
||||||
|
return (int)date('Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
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<string, array{name: string, email: string, active: int, admin: int, paypalname: ?string}>
|
||||||
|
*/
|
||||||
|
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<string, list<array{amount_cents: int, date_key: string}>>
|
||||||
|
*/
|
||||||
|
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<string, list<array{marks: int, unit_price_cents: int, date_key: string, entry_type: ?int}>>
|
||||||
|
*/
|
||||||
|
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<string, array{total_payments: int, total_costs: int, total_marks: int, balance: int, year_payments: int, year_costs: int, year_marks: int}>
|
||||||
|
*/
|
||||||
|
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<string>
|
||||||
|
*/
|
||||||
|
function gm_expected_front_sheet_emails(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'gm-heavy@test.local',
|
||||||
|
'gm-negative@test.local',
|
||||||
|
'gm-zero@test.local',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/dev-db.php';
|
||||||
|
require __DIR__ . '/golden-master-data.php';
|
||||||
|
|
||||||
|
$pdo = dev_pdo();
|
||||||
|
dev_apply_schema($pdo);
|
||||||
|
|
||||||
|
$dates = gm_dates();
|
||||||
|
$members = gm_members();
|
||||||
|
$payments = gm_payments();
|
||||||
|
$consumptions = gm_consumptions();
|
||||||
|
|
||||||
|
$emails = array_values(array_map(static fn (array $member): string => $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";
|
||||||