docs(12-04): complete digest-interval + Sofort-Alert UI plan
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
---
|
||||
phase: 12-tender-notifications
|
||||
plan: 04
|
||||
subsystem: api+ui
|
||||
tags: [nestjs, prisma, class-validator, nextjs, tender-radar, notifications]
|
||||
|
||||
requires:
|
||||
- phase: 12-tender-notifications (12-01/12-02/12-03)
|
||||
provides: "TenderNotificationPref + instantAlert schema fields, digest scheduler (daily default due-check), instant-dispatch mail path"
|
||||
provides:
|
||||
- "GET/PUT /modules/tender-radar/notification-pref (per-user digestInterval CRUD, IDOR-safe)"
|
||||
- "instantAlert passthrough on saved-search create/update DTOs + service"
|
||||
- "Web UI: digest-interval selector (settings page) + Sofort-Alert toggle per profile (SavedSearchBar)"
|
||||
affects: [14-tender-i18n-and-admin-sources]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Per-user pref CRUD via upsert on @@unique(userId) — same scoping convention as TenderSavedSearchService/TenderTriageService, no forTenant()/RLS"
|
||||
- "Route declared before @Get(':id') to avoid the NestJS route-order shadowing pitfall"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- apps/api/src/tenders/dto/notification-pref.dto.ts
|
||||
- apps/api/src/tenders/tender-notification-pref.service.ts
|
||||
- apps/api/src/tenders/tender-notification-pref.service.spec.ts
|
||||
modified:
|
||||
- apps/api/src/tenders/dto/saved-search.dto.ts
|
||||
- apps/api/src/tenders/tender-saved-search.service.ts
|
||||
- apps/api/src/tenders/tender-saved-search.service.spec.ts
|
||||
- apps/api/src/tenders/tenders.controller.ts
|
||||
- apps/api/src/tenders/tenders.controller.spec.ts
|
||||
- apps/api/src/tenders/tenders.module.ts
|
||||
- apps/web/src/lib/tender-radar-api.ts
|
||||
- "apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx"
|
||||
- "apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.tsx"
|
||||
- "apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx"
|
||||
|
||||
key-decisions:
|
||||
- "Digest-interval selector kept inline in settings/page.tsx (already a 'use client' shell) rather than split into a separate component — no standalone unit-test coverage was called for in this plan for that piece"
|
||||
- "instantAlert toggle uses a plain HTML checkbox with aria-label 'Sofort-Alert für {name}', not a shadcn switch, matching the existing minimal chip-based SavedSearchBar UI"
|
||||
|
||||
patterns-established:
|
||||
- "Notification-pref GET/PUT routes declared immediately after the saved-searches DELETE route and before @Get(':id') — same static-route-before-param-route convention as source-config/coverage/triage/saved-searches"
|
||||
|
||||
requirements-completed: [NOTIFY-01, NOTIFY-02]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "Per-user digest interval (Täglich/Wöchentlich/Aus) is readable/writable via GET/PUT /modules/tender-radar/notification-pref, defaults to 'daily', IDOR-safe"
|
||||
requirement: "NOTIFY-01"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/tenders/tender-notification-pref.service.spec.ts#TenderNotificationPrefService (4 tests)"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/api/src/tenders/tenders.controller.spec.ts#GET/PUT notification-pref (NOTIFY-01, per-user, T-12-14)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "Digest-interval selector in the tender-radar settings page loads current value on mount and persists changes"
|
||||
requirement: "NOTIFY-01"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "UI persistence-across-reload behavior is the end-of-phase UAT item (settings page reload check) — not covered by an automated component test in this plan"
|
||||
- id: D3
|
||||
description: "Per-profile Sofort-Alert toggle (instantAlert) persists via the existing saved-search create/update path, default off"
|
||||
requirement: "NOTIFY-02"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/tenders/tender-saved-search.service.spec.ts#instantAlert passthrough (3 tests)"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx#Sofort-Alert toggle (2 tests)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "All notification-pref and saved-search routes derive userId/tenantId exclusively from extractTriageContext(req), never body/query (IDOR)"
|
||||
requirement: "NOTIFY-01"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "apps/api/src/tenders/tenders.controller.spec.ts#derives userId from req.user ... never from a query param (V4/IDOR)"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
|
||||
duration: 12min
|
||||
completed: 2026-07-22
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 12 Plan 04: Digest-Intervall + Sofort-Alert Bedienoberfläche Summary
|
||||
|
||||
**Per-user digest-interval CRUD (GET/PUT notification-pref, default 'daily') plus instantAlert passthrough on saved-search create/update — both wired into a settings-page selector and a per-profile SavedSearchBar toggle, closing the loop from 12-01/02/03's pipeline to a user-controllable UI.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~12 min
|
||||
- **Completed:** 2026-07-22
|
||||
- **Tasks:** 3/3 completed
|
||||
- **Files modified:** 10 (3 created, 7 modified)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- `TenderNotificationPrefService` (per-user digestInterval CRUD, default `'daily'` when no row exists, upsert on `@@unique(userId)`) + `UpdateNotificationPrefDto` (`@IsIn(['daily','weekly','off'])`)
|
||||
- `GET`/`PUT /modules/tender-radar/notification-pref`, declared before `@Get(':id')` (route-order pitfall avoided, regression test added)
|
||||
- `instantAlert` now flows through `Create/UpdateSavedSearchDto` and `TenderSavedSearchService.create/update` (optional, `@IsBoolean`, falls back to the Prisma column default `false` when omitted)
|
||||
- `tender-radar-api.ts`: `fetchNotificationPref`/`saveNotificationPref`, and `SavedSearch`/`CreateSavedSearchPayload`/`UpdateSavedSearchPayload` extended with `instantAlert`
|
||||
- Settings page: "Benachrichtigungen" section with a Täglich/Wöchentlich/Aus select, loads via `fetchNotificationPref` on mount, saves via `saveNotificationPref` on change
|
||||
- `SavedSearchBar`: per-profile Sofort-Alert checkbox (`aria-label="Sofort-Alert für {name}"`), reflects `profile.instantAlert`, calls `updateSavedSearch(id, { instantAlert })` + reloads on toggle
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Backend — Pref-Service + Routen + instantAlert-Durchreichung** - `9e7ba53` (feat, TDD RED→GREEN)
|
||||
2. **Task 2: Frontend API-Client — Pref-Funktionen + instantAlert in SavedSearch** - `73a7e49` (feat)
|
||||
3. **Task 3: Frontend UI — Digest-Intervall-Auswahl + Sofort-Alert-Toggle** - `d60080e` (feat)
|
||||
|
||||
_Note: no separate metadata commit yet — this SUMMARY/STATE/ROADMAP commit follows._
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `apps/api/src/tenders/dto/notification-pref.dto.ts` - `UpdateNotificationPrefDto` (`@IsIn`)
|
||||
- `apps/api/src/tenders/tender-notification-pref.service.ts` - per-user digestInterval CRUD, default `'daily'`, upsert on `userId`
|
||||
- `apps/api/src/tenders/tender-notification-pref.service.spec.ts` - RED-first TDD spec (4 tests)
|
||||
- `apps/api/src/tenders/dto/saved-search.dto.ts` - `instantAlert?: boolean` on both Create/Update DTOs
|
||||
- `apps/api/src/tenders/tender-saved-search.service.ts` - passes `instantAlert` through create/update when supplied
|
||||
- `apps/api/src/tenders/tender-saved-search.service.spec.ts` - 3 new instantAlert passthrough tests
|
||||
- `apps/api/src/tenders/tenders.controller.ts` - `getNotificationPref`/`setNotificationPref` routes + constructor DI
|
||||
- `apps/api/src/tenders/tenders.controller.spec.ts` - fake pref service added to all 20 controller instantiations + new route-order/IDOR tests
|
||||
- `apps/api/src/tenders/tenders.module.ts` - registers `TenderNotificationPrefService`, doc comment updated
|
||||
- `apps/web/src/lib/tender-radar-api.ts` - `NotificationPref` type + `fetchNotificationPref`/`saveNotificationPref`; `instantAlert` added to `SavedSearch`/payloads
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/settings/page.tsx` - Benachrichtigungen section (digest-interval select)
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.tsx` - per-profile Sofort-Alert checkbox
|
||||
- `apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx` - checkbox-state + toggle-calls-updateSavedSearch tests
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- Kept the digest-interval selector inline in `settings/page.tsx` rather than extracting a separate `NotificationPrefForm.tsx` component — the page is already a `'use client'` shell (unlike `SourceConfigForm`'s admin-only split), and this plan's file list/test scope didn't call for standalone unit coverage of that piece.
|
||||
- Used a plain checkbox (not a switch) for the Sofort-Alert toggle to match the existing minimal chip-based `SavedSearchBar` styling.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] Updated `tenders.controller.spec.ts` fakes for the new constructor parameter**
|
||||
- **Found during:** Task 1 (adding `TenderNotificationPrefService` to `TendersController`'s constructor)
|
||||
- **Issue:** Adding a 5th required constructor parameter broke all 20 existing `new TendersController(...)` call sites in the spec file (missing-argument TS errors)
|
||||
- **Fix:** Added a `makeFakeNotificationPrefService()` helper and appended it as the 5th argument to every existing constructor call; also added two new tests (route-declaration-order guard + GET/PUT IDOR-scoping proof) for the new routes
|
||||
- **Files modified:** `apps/api/src/tenders/tenders.controller.spec.ts`
|
||||
- **Verification:** `npx tsc --noEmit` clean; full API test suite (19 files / 209 tests) green
|
||||
- **Committed in:** `9e7ba53` (Task 1 commit)
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (blocking — test compilation)
|
||||
**Impact on plan:** Necessary to keep the existing test suite compiling and passing after the planned constructor change. No scope creep — this is the direct, unavoidable consequence of the plan's own "Injiziere TenderNotificationPrefService in den Controller-Konstruktor" instruction.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## IDOR Mitigation (T-12-14/15/16)
|
||||
|
||||
- `UpdateNotificationPrefDto` and both saved-search DTOs carry no `userId`/`tenantId` field — both routes derive them exclusively from `extractTriageContext(req)` (cookie-backed auth context), never from body/query.
|
||||
- `TenderNotificationPrefService` scopes every read/write by `userId` via the `@@unique(userId)` constraint — a foreign `userId` can never read or overwrite another user's pref (proven in `tender-notification-pref.service.spec.ts`: "getForUser() is scoped strictly by userId").
|
||||
- `digestInterval` is validated server-side to exactly `'daily'|'weekly'|'off'` via `@IsIn` (T-12-15) before it reaches the service/scheduler due-check logic.
|
||||
- `instantAlert` validated via `@IsBoolean`; ownership of the saved-search profile is still enforced in `TenderSavedSearchService.update()` (pre-existing NotFound-collapse pattern, T-11-14) — unchanged by this plan.
|
||||
- No new package installs; no new trust boundary beyond the two already-modeled routes (matches `12-04-PLAN.md`'s threat register — no `## Threat Flags` needed).
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required. Both `TenderNotificationPref.digestInterval` and `TenderSavedSearch.instantAlert` columns already existed in the local dev DB (seeded in Plan 12-01) — no migration needed for this plan.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- NOTIFY-01/02 are now fully wired end-to-end: user sets digest interval / Sofort-Alert in the web UI → the 12-01/02/03 matching/digest/instant pipeline reads and honors those settings.
|
||||
- Remaining end-of-phase UAT (per `12-04-PLAN.md` `<verification>`): manually confirm in the running app that (a) setting the interval to "Wöchentlich" and reloading the settings page keeps the value, and (b) toggling a profile's Sofort-Alert and reloading keeps it on. Requires a docker rebuild/restart of the web+api containers to pick up these changes (per project convention, the user performs the rebuild, not this agent).
|
||||
- Phase 12 is otherwise code-complete pending that UAT + docker rebuild (consistent with the phase-level pause note in `eb10bd3`).
|
||||
|
||||
---
|
||||
*Phase: 12-tender-notifications*
|
||||
*Completed: 2026-07-22*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
All 12 listed files/paths verified present on disk; all 3 task commit hashes (`9e7ba53`, `73a7e49`, `d60080e`) verified present in git log.
|
||||
Reference in New Issue
Block a user