From 37a4042e531c4e7a99fad418db9a9598c803ed6c Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 22 Jul 2026 08:58:35 +0200 Subject: [PATCH] docs(12): add VALIDATION.md, resolve RESEARCH open questions --- .../12-tender-notifications/12-RESEARCH.md | 5 +- .../12-tender-notifications/12-VALIDATION.md | 96 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 .planning/phases/12-tender-notifications/12-VALIDATION.md diff --git a/.planning/phases/12-tender-notifications/12-RESEARCH.md b/.planning/phases/12-tender-notifications/12-RESEARCH.md index 548d4dd..b83d170 100644 --- a/.planning/phases/12-tender-notifications/12-RESEARCH.md +++ b/.planning/phases/12-tender-notifications/12-RESEARCH.md @@ -420,20 +420,23 @@ for (const pref of dueUsers) { | A4 | Fehlende Mandanten-SMTP → skip+log, Match bleibt `notifiedAt=null` (Retry) | Pitfall 6 | Niedrig — akzeptables MVP-Verhalten; Alternative wäre ein „dead-letter"-Flag, für MVP unnötig. | | A5 | Matching läuft am Ende von `pollDueSources` (kein separater Match-Scheduler) | Pattern C | Niedrig — CONTEXT nennt beides als Discretion; der Poll-Tick ist der natürliche Delta-Auslöser. | -## Open Questions +## Open Questions (RESOLVED) 1. **Re-Benachrichtigung bei Änderung (`contentHash`)?** - Was wir wissen: Phase 10 erkennt Änderungen via `contentHash` (SCHEMA-02), aktualisiert aber `pollDueSources` sammelt aktuell keine geänderten IDs. - Was unklar ist: Soll eine Fristverlängerung/Aufhebung eine „Notice aktualisiert"-Mail auslösen? - Empfehlung: Für MVP **nein** — nur genuin neue Tender benachrichtigen (A2). Beim Discuss/Planning bestätigen. + - **RESOLVED:** Nein — keine Re-Benachrichtigung bei `contentHash`-Änderung. Plan 12-01 sammelt in `pollDueSources` ausschließlich genuin NEUE Tender-IDs (delta-only) und übergibt nur diese an `matchDelta`; geänderte Bestands-Tender lösen keine Mail aus. 2. **Feste Digest-Uhrzeit pro Nutzer?** - Was wir wissen: D-01/D-03 fordern nur Intervall (daily/weekly/off), keine Uhrzeit. - Empfehlung: Ein globaler 07:00-Lauf; `digestHour` nur nachrüsten, falls gefordert (A1). + - **RESOLVED:** Ein einziger globaler Digest-Cron um 07:00 Europe/Berlin (Plan 12-02); kein per-Nutzer-`digestHour`. Nur das Intervall (daily/weekly/off) ist nutzerkonfigurierbar; `digestHour` bleibt bei Bedarf ein späterer Zusatz. 3. **Was passiert mit `notifiedAt=null`-Matches, wenn digestInterval='off' UND instantAlert=false?** - Was wir wissen: Solche Matches würden sich unbegrenzt ansammeln (nie versendet). - Empfehlung: Akzeptabel — sie sind einfach „stiller" State; die UI zeigt Treffer ohnehin. Optional: bei „off" den Match direkt als suppressed markieren. Für MVP: nichts tun (harmlos). Beim Planning entscheiden. + - **RESOLVED:** Nichts tun — die Matches bleiben stiller `notifiedAt=null`-State (kein Versand, keine Suppression-Markierung). Die Treffer sind weiterhin in der Phase-11-Trefferliste sichtbar; harmlos für MVP. ## Environment Availability diff --git a/.planning/phases/12-tender-notifications/12-VALIDATION.md b/.planning/phases/12-tender-notifications/12-VALIDATION.md new file mode 100644 index 0000000..d688357 --- /dev/null +++ b/.planning/phases/12-tender-notifications/12-VALIDATION.md @@ -0,0 +1,96 @@ +--- +phase: 12 +slug: tender-notifications +status: draft +nyquist_compliant: true +wave_0_complete: false +created: 2026-07-22 +--- + +# Phase 12 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. +> Extracted from 12-RESEARCH.md § Validation Architecture (Test framework, Requirements→Test map, Sampling rate, Wave-0 gaps). + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Vitest (API 3.x, Web 4.x — both existing, no install) | +| **Config file** | `apps/api/vitest.config.ts` (existing — not modified by this phase) | +| **Quick run command (API slice)** | `pnpm --filter api test -- ` | +| **Quick run command (Web slice)** | `pnpm --filter web test -- ` | +| **Full suite command** | `pnpm test` (turbo) bzw. `pnpm --filter api test && pnpm --filter web test` | +| **Estimated runtime** | ~10-15 seconds (scoped) | + +No new packages: `nodemailer`, `@nestjs/schedule`, `cron`, `prisma` are already in `apps/api/package.json` (legitimized in Phase 7/10). + +--- + +## Sampling Rate + +- **After every task commit:** Run the affected slice spec (`pnpm --filter api test -- `). +- **After every plan wave:** Run `pnpm --filter api test && pnpm --filter web test`. +- **Before `/gsd-verify-work`:** `pnpm test` must be green. +- **Max feedback latency:** ~15 seconds (scoped run). + +--- + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| +| 12-01-01 | 01 | 1 | NOTIFY-03 | T-12-01/03 | Schema TenderMatch (@@unique[tenderId,savedSearchId], EIN notifiedAt) + NotificationPref + instantAlert; Migration lokal angewendet | integration | `cd apps/api && npx prisma validate && npx prisma generate` (+ psql-Tabellencheck, manual) | ⚠️ migration | ⬜ pending | +| 12-01-02 | 01 | 1 | NOTIFY-03 | T-12-02/04 | matchDelta: delta-only (id IN newTenderIds), Match-Erzeugung, idempotenter Upsert (update:{} bewahrt notifiedAt) | unit | `pnpm --filter api test -- tender-matching.service` | ❌ W0 | ⬜ pending | +| 12-01-03 | 01 | 1 | NOTIFY-03 | T-12-02 | pollDueSources sammelt genuin neue Tender-IDs, ruft matchDelta nur mit diesen; Provider registriert | unit | `pnpm --filter api test -- tender-ingestion.service` | ⚠️ extend | ⬜ pending | +| 12-02-01 | 02 | 2 | NOTIFY-04 | T-12-06/08 | TenderMailService: getDecryptedSmtpConfig je Send, frischer Transport + close(), Skip bei fehlender Config, kein globaler Mailer | unit (mock nodemailer) | `pnpm --filter api test -- tender-mail.service` | ❌ W0 | ⬜ pending | +| 12-02-02 | 02 | 2 | NOTIFY-01/03 | T-12-05/07/09 | Digest-Cron: Fälligkeit daily/weekly/off + Default-daily, Multi-Tenant findMany, eine sektionierte Mail/Nutzer, notifiedAt-Stempel nur nach Erfolg, robust bei SMTP-Fehler | unit | `pnpm --filter api test -- tender-digest.scheduler` | ❌ W0 | ⬜ pending | +| 12-02-03 | 02 | 2 | NOTIFY-01/04 | — | SettingsModule-Import + Provider (TenderMailService, TenderDigestScheduler); DI-Graph auflösbar | typecheck | `cd apps/api && npx tsc --noEmit -p tsconfig.json` | n/a | ⬜ pending | +| 12-03-01 | 03 | 3 | NOTIFY-02/03 | T-12-11/12/13 | Instant-Dispatch: nur instantAlert=true, Bündelung pro Profil/Tick, notifiedAt='instant' nur nach Erfolg, catch-and-log | unit | `pnpm --filter api test -- tender-matching.service` | ⚠️ extend | ⬜ pending | +| 12-03-02 | 03 | 3 | NOTIFY-03 | T-12-10 | Instant+Digest = genau EINE Mail (kein Doppelversand über beide Kanäle) | integration | `pnpm --filter api test -- tender-notifications.integration` | ❌ W0 | ⬜ pending | +| 12-04-01 | 04 | 3 | NOTIFY-01/02 | T-12-14/15/16 | Pref-Service (Default daily, per-user Upsert) + @IsIn-DTO + GET/PUT-Routen VOR :id + instantAlert-Durchreichung; userId aus Auth-Context | integration | `pnpm --filter api test -- tender-notification-pref.service` (+ `npx tsc --noEmit`) | ❌ W0 | ⬜ pending | +| 12-04-02 | 04 | 3 | NOTIFY-01/02 | — | API-Client: fetch/saveNotificationPref + instantAlert in SavedSearch-Typ/Payloads | typecheck | `cd apps/web && npx tsc --noEmit` (+ grep notification-pref/instantAlert) | n/a | ⬜ pending | +| 12-04-03 | 04 | 3 | NOTIFY-01/02 | T-12-16 | Settings-Digest-Intervall-Auswahl (lädt+speichert); SavedSearchBar Sofort-Alert-Toggle pro Profil | component | `pnpm --filter web test -- SavedSearchBar` (+ `npx tsc --noEmit`) | ⚠️ extend | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +--- + +## Wave 0 Requirements + +- [ ] `apps/api/src/tenders/tender-matching.service.spec.ts` — matchDelta: delta-only (0 Matches bei 2188 Bestand), Match-Erzeugung, Idempotenz (update:{} bewahrt notifiedAt), leeres Delta (Plan 12-01 Task 2, RED first); in Plan 12-03 Task 1 um Instant-Dispatch erweitert (RED first) +- [ ] `apps/api/src/tenders/tender-mail.service.spec.ts` — getDecryptedSmtpConfig je Send, frischer Transport + close() im finally, Skip bei fehlender Config ohne Throw, sektionierter Digest-Body (Plan 12-02 Task 1, RED first) — `nodemailer` mocken wie im DKV-Vorbild +- [ ] `apps/api/src/tenders/tender-digest.scheduler.spec.ts` — Fälligkeit daily/weekly/off + Default-daily, Multi-Tenant findMany (kein findFirst), eine Mail/Nutzer, kein Doppelversand, Robustheit (Plan 12-02 Task 2, RED first) +- [ ] `apps/api/src/tenders/tender-notifications.integration.spec.ts` — instant-on ⇒ 1 Instant/0 Digest; instant-off ⇒ 0 Instant/1 Digest — genau eine Mail über beide Kanäle (Plan 12-03 Task 2) +- [ ] `apps/api/src/tenders/tender-notification-pref.service.spec.ts` — per-user CRUD, Default 'daily', Upsert auf @@unique userId, @IsIn-Validierung (Plan 12-04 Task 1, RED first) +- [ ] (extend) `apps/api/src/tenders/tender-ingestion.service.spec.ts` — matchDelta wird nur mit den genuin neuen Tender-IDs des Ticks aufgerufen (Plan 12-01 Task 3) +- [ ] (extend) `apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx` — Sofort-Alert-Toggle rendert Zustand + ruft updateSavedSearch({ instantAlert }) (Plan 12-04 Task 3) + +Vitest-Framework existiert bereits (api + web) — keine Framework-Installation nötig. + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| Migration `20260722100000_add_tender_notifications` auf DB angewendet | NOTIFY-03 | DB hat keinen Host-Port; lokale Anwendung via `docker compose exec -T db psql -U tessera -d tessera_dev` (MEMORY-Hinweis); kein Docker-Deploy auf dem Testserver durch Claude | `SELECT to_regclass('public."TenderMatch"'), to_regclass('public."TenderNotificationPref"')` ≠ NULL; `\d "TenderSavedSearch"` zeigt `instantAlert`; `prisma migrate status` grün | +| Tatsächlicher SMTP-Versand (Transportaufbau + Zustellung) | NOTIFY-04 | Realer Mailversand ist mit gemocktem nodemailer nicht beobachtbar; braucht lokales Relay | Digest-/Instant-Testlauf gegen Mailhog `localhost:1025`; Mail erscheint dort, Body ist nach Profil gegliedert (Digest) bzw. eine Sammelmail (Instant) | +| Zwei-Mandanten-Digest-Isolation | NOTIFY-01/03 | Echte Cross-Tenant-Isolation braucht zwei reale Nutzer in zwei Mandanten mit eigener SmtpConfig — im Unit-Test nur gemockt | Zwei Nutzer/zwei Mandanten daily → jeder erhält genau seine eigenen Treffer über seine eigene SMTP; kein Nutzer sieht Treffer des anderen | +| Ein-Mail-Garantie über beide Kanäle (End-to-End) | NOTIFY-03 | Automatisiert durch 12-03-02 (integration), aber finale Bestätigung am echten Pipeline-Lauf | Profil instant=on + user daily, ein neuer Match → insgesamt genau EINE Mail (die Instant-Mail); der folgende Digest sendet für dieses Paar nichts | +| Backfill-Nicht-Flut bei Profil-Anlage | NOTIFY-03 (D-07) | Beobachtung am realen Bestand (~2188 Tender) | Neues Profil bei vorhandenem Bestand anlegen → 0 un-benachrichtigte Matches, keine Mail-Flut; Treffer bleiben in der Trefferliste sichtbar | + +--- + +## Validation Sign-Off + +- [x] All tasks have `` verify or a documented Wave 0 / manual-gate dependency +- [x] Sampling continuity: no 3 consecutive tasks without automated verify +- [x] Wave 0 covers all MISSING references (spec scaffolds; RED-first for pure-logic units) +- [x] No watch-mode flags (all use `vitest run` via `pnpm --filter … test`) +- [x] Feedback latency < 15s (scoped runs) +- [x] `nyquist_compliant: true` set in frontmatter + +**Approval:** draft — pending execution (wave_0_complete: false)