docs(11-02): complete region/plz/bundesland filter plan
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
---
|
||||
phase: 11-filter-engine-results-ui-saved-searches
|
||||
plan: 02
|
||||
subsystem: api, ui, database
|
||||
tags: [nestjs, prisma, class-validator, nuts, nextjs-app-router, vitest]
|
||||
|
||||
requires:
|
||||
- phase: 11-filter-engine-results-ui-saved-searches
|
||||
provides: "tender-query.builder.ts (buildTenderWhere/buildOrderBy), erweiterter TenderQueryDto, FilterPanel.tsx (Plan 11-01)"
|
||||
provides:
|
||||
- "geo/nuts-bundesland.ts: bundeslandFromRegion()/nutsPrefixFor() — NUTS-1-Präfix (DE1..DEG) -> 16 Bundesland-Namen, null-safe"
|
||||
- "TenderNormalizerService setzt bundesland aus region ab (statt hartcodiert null) — neue Ingests sofort filterbar"
|
||||
- "Backfill-Migration 20260721140000_tender_bundesland_backfill: 933/1671 Bestandszeilen mit bundesland befüllt + @@index([bundesland])"
|
||||
- "buildTenderWhere: plz-startsWith, bundesland-exact, region-startsWith Branches"
|
||||
- "FilterPanel.tsx: PLZ-Feld + 16-Länder-Bundesland-Dropdown, schreibt in URL-searchParams"
|
||||
affects: [11-03-cpv-filter, 11-04-detail-view, 11-05-triage, 11-06-saved-searches]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Reine, Nest-unabhängige geo/nuts-bundesland.ts-Datei (wie tender-query.builder.ts) — pure Function, isoliert testbar ohne DB"
|
||||
- "Handgeschriebene idempotente Backfill-Migration (UPDATE ... WHERE col IS NULL) statt destruktivem Rewrite — sicher mehrfach ausführbar"
|
||||
- "Kleine Konstante im Web gespiegelt (BUNDESLAND_OPTIONS in FilterPanel.tsx) statt Shared-Package für einen stabilen 16-Werte-Katalog"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- apps/api/src/tenders/geo/nuts-bundesland.ts
|
||||
- apps/api/src/tenders/geo/nuts-bundesland.spec.ts
|
||||
- apps/api/prisma/migrations/20260721140000_tender_bundesland_backfill/migration.sql
|
||||
modified:
|
||||
- apps/api/src/tenders/tender-normalizer.service.ts
|
||||
- apps/api/src/tenders/tender-normalizer.service.spec.ts
|
||||
- apps/api/src/tenders/tender.types.ts
|
||||
- apps/api/prisma/schema.prisma
|
||||
- apps/api/src/tenders/dto/tender-query.dto.ts
|
||||
- apps/api/src/tenders/tender-query.builder.ts
|
||||
- apps/api/src/tenders/tender-query.builder.spec.ts
|
||||
- apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx
|
||||
|
||||
key-decisions:
|
||||
- "bundesland-Filter matcht exakt gegen die (jetzt befüllte, indexierte) bundesland-Spalte statt region-startsWith — schneller und robuster nach dem Backfill; region bleibt als eigener, unabhängiger Präfixfilter erhalten (auch für Zeilen ohne bundesland-Derivation nützlich)."
|
||||
- "BUNDESLAND_OPTIONS wird im Web-Package als kleine Konstante gespiegelt (nicht aus @tessera/shared bezogen) — web nutzt @tessera/shared aktuell gar nicht, ein 16-Werte-Katalog rechtfertigt keine neue Cross-Package-Abhängigkeit (RESEARCH-Vorgabe: 'kleine Konstante spiegeln' explizit als Option genannt)."
|
||||
- "Migration wurde per docker exec psql lokal angewendet und danach via `prisma migrate resolve --applied` in der _prisma_migrations-Historie nachgezogen (DATABASE_URL musste für Host-Prisma-Aufrufe explizit auf die Container-IP gesetzt werden — .env im Container zeigt intern auf den Service-Namen, vom Host aus nicht auflösbar)."
|
||||
|
||||
requirements-completed: [FILTER-02]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "NUTS-1 (DE1..DEG) -> 16 Bundesland-Namen-Map, null-safe für fehlende/unbekannte region, Reverse-Lookup nutsPrefixFor(), gegen echte DB-Region-Stichproben (DE212/DE300/DE600/DE712/DEA22) validiert"
|
||||
requirement: "FILTER-02"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/tenders/geo/nuts-bundesland.spec.ts (7 tests)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "TenderNormalizerService setzt bundesland = bundeslandFromRegion(region) statt hartcodiertem null; region=null -> bundesland=null ohne Crash"
|
||||
requirement: "FILTER-02"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/tenders/tender-normalizer.service.spec.ts (8 tests, davon 2 neu für bundesland)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Backfill-Migration füllt bundesland für Bestandszeilen aus region-NUTS-1-Präfix; @@index([bundesland]) ergänzt; idempotent (zweiter Lauf: UPDATE 0)"
|
||||
requirement: "FILTER-02"
|
||||
verification:
|
||||
- kind: manual_procedural
|
||||
ref: "docker exec tessera-ctl-db-1 psql -U tessera -d tessera -c \"SELECT bundesland, count(*) FROM \\\"Tender\\\" GROUP BY bundesland\" -> 933/1671 Zeilen über alle 16 Länder befüllt, 738 NULL wo region selbst NULL ist; zweiter Migrationslauf UPDATE 0 (idempotent bestätigt); prisma migrate status -> up to date"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "buildTenderWhere: plz-startsWith, bundesland-exact (gegen befüllte Spalte), region-startsWith Branches — nur angehängt wenn gesetzt"
|
||||
requirement: "FILTER-02"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/tenders/tender-query.builder.spec.ts (17 tests, davon 4 neu für plz/bundesland/region)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "FilterPanel erweitert um PLZ-Eingabefeld + 16-Länder-Bundesland-Dropdown, schreibt in URL-searchParams (deep-linkbar, ResultsList/listTenders leiten alle Params bereits transparent weiter)"
|
||||
requirement: "FILTER-02"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "Visuelle/Interaktions-Verifikation des Dropdowns und der resultierenden Trefferliste erfordert einen Docker-Rebuild + Browser-Check (Docker-Stack läuft noch mit alten Images) — kein Component-Test in diesem Plan angefordert; Unit-Tests decken die Builder-Logik ab, nicht das Rendering."
|
||||
---
|
||||
|
||||
# Phase 11 Plan 02: NUTS-Bundesland-Ableitung, Backfill & Region/PLZ/Bundesland-Filter Summary
|
||||
|
||||
**NUTS-1-Präfix-Ableitung macht den bis dato konstant-leeren Bundesland-Filter erstmals funktionsfähig: Normalizer-Fix für neue Ingests + Backfill-Migration füllt 933/1671 Bestandszeilen über alle 16 Länder, verdrahtet in Query-Builder + FilterPanel.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~4 min
|
||||
- **Started:** 2026-07-21T15:59:14+02:00
|
||||
- **Completed:** 2026-07-21T16:03:21+02:00
|
||||
- **Tasks:** 3 (Task 1 war TDD, RED-then-GREEN)
|
||||
- **Files modified:** 12 (3 created, 9 modified)
|
||||
|
||||
## Accomplishments
|
||||
- `geo/nuts-bundesland.ts`: feste `NUTS1_BUNDESLAND`-Map (16 Einträge), `bundeslandFromRegion()` (null-safe, case-insensitive Präfix-Match) und `nutsPrefixFor()` (Reverse-Lookup) — gegen 5 echte `region`-Stichproben aus der Live-DB validiert (DE212→Bayern, DE300→Berlin, DE600→Hamburg, DE712→Hessen, DEA22→Nordrhein-Westfalen).
|
||||
- `TenderNormalizerService.normalize()` setzt `bundesland = bundeslandFromRegion(region)` statt der in Phase 10 hartcodierten `null`-Zuweisung — jede neue Ingestion bekommt sofort ein abgeleitetes Bundesland.
|
||||
- Handgeschriebene Backfill-Migration `20260721140000_tender_bundesland_backfill`: `CREATE INDEX IF NOT EXISTS "Tender_bundesland_idx"` + idempotenter `UPDATE ... CASE upper(left(region,3)) ... WHERE bundesland IS NULL AND region IS NOT NULL` — lokal angewendet, **933 von 1671 Zeilen** über alle 16 Bundesländer befüllt (738 bleiben NULL, weil `region` selbst NULL ist — kein Datenverlust, kein Crash).
|
||||
- `schema.prisma`: `@@index([bundesland])` ergänzt für Filter-Performance; `prisma generate` neu ausgeführt.
|
||||
- `buildTenderWhere` (Query-Builder) um drei konditionale AND-Branches erweitert: `plz` (startsWith), `bundesland` (exakt, gegen die jetzt befüllte indexierte Spalte), `region` (startsWith, unabhängig nutzbar).
|
||||
- `TenderQueryDto` um validierte `plz`/`region`/`bundesland`-Felder erweitert (`@IsString`, `plz` zusätzlich `@MaxLength(5)`).
|
||||
- `FilterPanel.tsx`: neues PLZ-Eingabefeld + Bundesland-Dropdown mit den 16 Ländernamen (im Web als kleine Konstante gespiegelt), schreibt in die URL-`searchParams` — `ResultsList`/`listTenders()` leiten sämtliche Params bereits transparent an die API weiter, keine zusätzliche Fetch-Verdrahtung nötig.
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1a: NUTS-Bundesland-Map RED** — `3dec35f` (test) — failing `nuts-bundesland.spec.ts` (7 Tests, Modul existiert noch nicht)
|
||||
2. **Task 1b: NUTS-Bundesland-Map GREEN** — `d12e1c9` (feat) — `nuts-bundesland.ts` implementiert, 7/7 Tests grün
|
||||
3. **Task 2: Normalizer + Backfill-Migration** — `e4db602` (feat) — Normalizer-Fix, Schema-Index, Backfill-Migration lokal angewendet + `prisma migrate resolve --applied`
|
||||
4. **Task 3: Query-Builder + DTO + FilterPanel** — `69e2529` (feat) — plz/bundesland/region-Branches, DTO-Felder, FilterPanel-Erweiterung
|
||||
|
||||
_Task 1 war TDD (RED-then-GREEN); Task 2 und 3 waren `type="auto"` ohne separaten RED-Schritt._
|
||||
|
||||
## Files Created/Modified
|
||||
- `apps/api/src/tenders/geo/nuts-bundesland.ts` - NUTS-1→Bundesland-Map + Ableitungs-/Reverse-Funktionen (neu)
|
||||
- `apps/api/src/tenders/geo/nuts-bundesland.spec.ts` - 7 Unit-Tests (neu)
|
||||
- `apps/api/prisma/migrations/20260721140000_tender_bundesland_backfill/migration.sql` - Index + idempotenter Backfill-UPDATE (neu)
|
||||
- `apps/api/src/tenders/tender-normalizer.service.ts` - `bundesland` aus `region` abgeleitet statt hartcodiert `null`
|
||||
- `apps/api/src/tenders/tender-normalizer.service.spec.ts` - 2 neue Testfälle (region→bundesland, region=null→bundesland=null)
|
||||
- `apps/api/src/tenders/tender.types.ts` - Dokumentationskommentar für `bundesland`/`region` aktualisiert (nicht mehr "deferred")
|
||||
- `apps/api/prisma/schema.prisma` - `@@index([bundesland])` ergänzt
|
||||
- `apps/api/src/tenders/dto/tender-query.dto.ts` - `plz`/`region`/`bundesland` validierte Felder ergänzt
|
||||
- `apps/api/src/tenders/tender-query.builder.ts` - drei neue konditionale `AND`-Branches
|
||||
- `apps/api/src/tenders/tender-query.builder.spec.ts` - 4 neue Testfälle (plz/bundesland/region gesetzt + alle drei omitted wenn leer)
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/components/FilterPanel.tsx` - PLZ-Feld + Bundesland-Dropdown (16 Länder)
|
||||
|
||||
## Decisions Made
|
||||
- `bundesland`-Filter matcht exakt gegen die jetzt befüllte, indexierte `bundesland`-Spalte — schneller als `region`-Präfix-Matching und die vom Plan empfohlene Post-Backfill-Variante; `region`-Präfixfilter bleibt als eigenständiger, von `bundesland` unabhängiger Branch erhalten (nützlich für Zeilen, deren `bundesland`-Ableitung z. B. wegen eines unbekannten Präfixes leer blieb).
|
||||
- `BUNDESLAND_OPTIONS` wird im Web-Package als kleine, dokumentierte Konstante gespiegelt statt über ein Shared-Package bezogen — `apps/web` nutzt `@tessera/shared` aktuell gar nicht, und ein stabiler 16-Werte-EU-Katalog rechtfertigt keine neue Cross-Package-Abhängigkeit (RESEARCH nennt "kleine Konstante spiegeln" explizit als gleichwertige Option).
|
||||
- Migration lokal per `docker exec tessera-ctl-db-1 psql -U tessera -d tessera` angewendet (kein Host-Port auf dem DB-Container) und danach via `prisma migrate resolve --applied` in die `_prisma_migrations`-Historie nachgezogen, damit `prisma migrate status` konsistent bleibt — `DATABASE_URL` musste für den Host-Prisma-CLI-Aufruf explizit auf die Container-IP (`172.19.0.2`) gesetzt werden, da der im API-Container gültige `.env`-Hostname vom Host aus nicht auflösbar ist.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written. Eine kleine Ergänzung: der veraltete "Deferred to Phase 11"-Dokumentationskommentar in `tender.types.ts` wurde konsistent mit der neuen Implementierung aktualisiert (Rule 1 — Bug/Doku-Inkonsistenz, keine funktionale Änderung, im selben Commit wie der Normalizer-Fix).
|
||||
|
||||
## Issues Encountered
|
||||
- `prisma migrate status`/`prisma migrate resolve` schlugen zunächst mit "Environment variable not found: DATABASE_URL" fehl, weil das Host-Shell-Environment die im Container gültige `.env` nicht automatisch lädt bzw. der dortige Hostname vom Host nicht auflösbar ist — gelöst durch expliziten `DATABASE_URL=postgresql://tessera:tessera_dev@172.19.0.2:5432/tessera`-Präfix (MEMORY-Hinweis "Lokale DB-Migrationen").
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - keine externen Service-Konfigurationen erforderlich. Keine neuen npm-Pakete.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Bundesland-Filter liefert jetzt echte Treffer (933/1671 Zeilen über alle 16 Länder) statt konstant 0 — Kern-Pitfall des Plans behoben.
|
||||
- `plz`/`region`/`bundesland` sind als AND-Branches im Builder etabliert; Plan 11-03 (CPV) fügt einen weiteren unabhängigen Branch nach demselben Muster hinzu, ohne bestehende Branches zu ändern.
|
||||
- **Manuelle UAT ausstehend** (wie in 11-01 SUMMARY dokumentiert): Der Docker-Stack läuft noch mit den alten Images. Für eine Live-Verifikation im Browser (Bundesland-Dropdown wählen → Trefferliste filtert; PLZ-Präfix eintragen → Trefferliste filtert) ist `docker compose build api web && docker compose up -d` erforderlich (User führt den Rebuild selbst aus). Alle automatisierten Tests sind grün: 106/106 API-Tests (`pnpm --filter @tessera/api test`), 114/114 Web-Tests (`pnpm --filter @tessera/web test`); `tsc --noEmit` fehlerfrei in beiden Apps.
|
||||
|
||||
---
|
||||
*Phase: 11-filter-engine-results-ui-saved-searches*
|
||||
*Completed: 2026-07-21*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
All 10 created/modified source files verified present on disk; all 4 task commits (3dec35f, d12e1c9, e4db602, 69e2529) verified in git log.
|
||||
Reference in New Issue
Block a user