ETL-QS-Suite, CI-Pipeline und korrigierte Pfade/DB
Some checks failed
ETL-QS / etl-tests (push) Failing after 44s

- Pfad-Defaults im DAG auf das Repo-Checkout /opt/airflow/git/current
  umgestellt (include-Skripte + etl_cache) und Ziel-DB auf
  analytics_pg_duckdb festgelegt
- tests/: Fixture-Generator (>=4 Dateien je Quell-Ordner mit Dubletten/
  Edge-Cases) und End-to-End-Runner mit 45 Pruefungen gegen erwartete
  Ergebnisse, inkl. README
- .forgejo/workflows: CI laeuft die ETL-QS bei Aenderungen an ETL-Skript,
  tests/ oder Geo-Referenz (python:3.12-Container, runs-on docker)
- .gitignore: .venv_test/ und generierte tests/fixtures/ ausgeschlossen

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Pascal Beyer 2026-06-14 14:11:55 +02:00
parent 9cacfd7ae2
commit 82c393408f
8 changed files with 691 additions and 0 deletions

101
tests/README.md Normal file
View file

@ -0,0 +1,101 @@
# Qualitätssicherung HVB-ETL
Tests für die Transformationslogik in
[`include/02_etl_angebote_zuschlaege.py`](../include/02_etl_angebote_zuschlaege.py).
Die Suite erzeugt deterministische Excel-Eingabedaten (≥4 Dateien je
Quell-Ordner, inkl. Dubletten und Edge-Cases), führt das ETL-Skript real aus
und prüft die erzeugten Output-CSVs gegen exakt berechnete Erwartungswerte.
## Ausführen
```bash
# Einmalig: Umgebung (Python ≥ 3.10, das ETL nutzt `str | None`-Syntax)
python3.12 -m venv .venv_test
.venv_test/bin/pip install pandas openpyxl
# Komplette QS (erzeugt Fixtures, läuft ETL, prüft Ergebnisse):
.venv_test/bin/python tests/run_tests.py
```
Exit-Code `0` = alle Prüfungen bestanden (CI-tauglich). Bei Fehlern bleibt das
temporäre Arbeitsverzeichnis zur Analyse erhalten; bei Erfolg wird es gelöscht.
Nur die Eingabedaten erzeugen (ohne ETL/Prüfung):
```bash
.venv_test/bin/python tests/generate_fixtures.py # -> tests/fixtures/input/
```
## Dateien
| Datei | Zweck |
|-------|-------|
| `generate_fixtures.py` | Erzeugt die Excel-Testdaten (4 Dateien/Ordner). |
| `run_tests.py` | Baut Workdir, führt ETL aus, 45 Prüfungen. |
| `fixtures/` | Generierte Eingabedaten (nicht versioniert). |
## Testdatensatz (Soll-Bild)
9 Angebote (6 aus Export, 3 nur aus Archiv), 8 eindeutige Kunden, 3 Zuschläge.
### `taifun_export/` — 4 Dateien, „jüngster Export gewinnt"
- **A-1001** liegt in 3 Dateien (Stände 500/2000/4000 via `_dateistand.csv`).
Erwartung: jüngste Version (Stand 4000) gewinnt → Beschreibung „1000 m³",
Kunde „Müller GmbH". 8 Rohzeilen → **6 eindeutige** Angebote.
- **A-1004**: Kundenadresse leer, nur Standort „80331 München" →
Standortadresse hat Vorrang (`adress_quelle = standort`).
- **A-1006**: Land `CH`, PLZ „1010 Wien" (kein dt. Format) → kein Geo-Treffer.
### `pflege/` — 4 Dateien, UNION + gültiger Status
- 8 Status-Events → **7 nach Dedup** (A-1003/Auftrag exakt doppelt).
- **A-1001**: neuestes Datum 2024-03-05 mit Gleichstand „Verhandlung" (Rang 3)
vs. „Angebot" (Rang 2) → **höherer Rang gewinnt: Verhandlung** (Ampel orange).
- **A-1003**: Pflege „Auftrag" überschreibt Export-Abbruchtext → Auftrag (grün).
- **A-9999**: Waisen-Status ohne Angebot → erscheint in `staging_status_historie`,
**nicht** in `staging_angebote`.
### `archiv/` — 4 Dateien, Alt-Angebote ergänzen
- **B-9001/9002/9003** nicht im Export → werden als `herkunft_stamm = archiv`
ergänzt. **B-9001** über zwei Dateien dupliziert → nur einmal.
- **A-1002** ist bereits im Export → Archiv-Version wird **verworfen**
(Export-Kunde „Bauer AG" bleibt, nicht „Bauer AG ARCHIV").
### Status-Ableitung (ohne Pflege)
- **A-1004**: gültige Projektnummer `P-67890`, keine Pflege → `Auftrag`
(`status_quelle = export_projekt`).
- **A-1006**: Projekttext „kein Interesse" (kein Muster) → `Abgelehnt` (schwarz).
- **A-1005**: weder Pflege noch Projekt → Default `Angebot` (gelb).
- **Status-Konflikt**: A-1002 hat Projektnummer `P-12345`, Pflege sagt aber
„Angebot" → `status_konflikt = True` (genau 1 Konflikt).
### Kunden-Dedup
- „Müller GmbH | 70001" = A-1001 + A-1005 → **1 Kunde**, aktuellstes Angebot =
A-1005 (jüngstes Datum). Gesamt **8 eindeutige Kunden**.
### `zuschlaege/` — 4 Dateien, zwei Blätter, Kopfzeile in Zeile 4
- Blätter `Zuschläge_Detailliert` + `Zuschläge_Kompakt` (jeweils `header=3`).
- 6 Detailzeilen → **5 nach Dedup** je (Zuschlags-Nr, Flurstück); Z-100/F1
doppelt. **3 eindeutige Zuschläge**.
- Gebotsmenge wird aus dem Kompakt-Blatt gemerged (Z-100 → 5000).
- PLZ **04109** behält die führende Null.
- `plz_mit_eigenem_angebot`: 10115 (eigenes Angebot) → True; 99999 → False.
- Lead-Ampel durchgängig `lila`.
## Erwartete Kennzahlen (Übersicht)
| Output-CSV | Zeilen | Kernaussage |
|------------|-------:|-------------|
| `staging_angebote` | 9 | 6 Export + 3 Archiv, Nummern eindeutig |
| `staging_status_historie` | 7 | inkl. Waise A-9999, Dublette entfernt |
| `staging_zuschlaege` | 5 | 3 eindeutige Zuschläge |
| `v_angebote_karte` | 9 | 8/9 mit Koordinaten (A-1006 Ausland ohne) |
| `v_angebote_karte_aktuell` | 8 | 1 Zeile je Kunde |
| `v_kunden_pipeline` | 8 | Top-Kunde Müller GmbH (2 Angebote) |
| `v_zuschlaege_karte` | 5 | 4/5 mit Koordinaten, Ampel lila |
Status-Verteilung: `Angebot 5, Auftrag 2, Verhandlung 1, Abgelehnt 1`.
> Hinweis: Die Geo-Prüfungen nutzen `etl_cache/geo_plz_koordinaten.csv`. Diese
> Datei hat keine Spalte `kreis`, daher bleibt `landkreis` im Test leer — das
> ist erwartet und wird vom ETL korrekt behandelt.