Files

12 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, coverage, duration, completed, status
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed coverage duration completed status
12-tender-notifications 04 api+ui
nestjs
prisma
class-validator
nextjs
tender-radar
notifications
phase provides
12-tender-notifications (12-01/12-02/12-03) TenderNotificationPref + instantAlert schema fields, digest scheduler (daily default due-check), instant-dispatch mail path
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)
14-tender-i18n-and-admin-sources
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
created modified
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
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
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
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
NOTIFY-01
NOTIFY-02
id description requirement verification human_judgment
D1 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 NOTIFY-01
kind ref status
unit apps/api/src/tenders/tender-notification-pref.service.spec.ts#TenderNotificationPrefService (4 tests) pass
kind ref status
unit apps/api/src/tenders/tenders.controller.spec.ts#GET/PUT notification-pref (NOTIFY-01, per-user, T-12-14) pass
false
id description requirement verification human_judgment rationale
D2 Digest-interval selector in the tender-radar settings page loads current value on mount and persists changes NOTIFY-01
true 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 description requirement verification human_judgment
D3 Per-profile Sofort-Alert toggle (instantAlert) persists via the existing saved-search create/update path, default off NOTIFY-02
kind ref status
unit apps/api/src/tenders/tender-saved-search.service.spec.ts#instantAlert passthrough (3 tests) pass
kind ref status
unit apps/web/src/app/(portal)/modules/tender-radar/components/SavedSearchBar.test.tsx#Sofort-Alert toggle (2 tests) pass
false
id description requirement verification human_judgment
D4 All notification-pref and saved-search routes derive userId/tenantId exclusively from extractTriageContext(req), never body/query (IDOR) NOTIFY-01
kind ref status
unit apps/api/src/tenders/tenders.controller.spec.ts#derives userId from req.user ... never from a query param (V4/IDOR) pass
false
12min 2026-07-22 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.