Files
paliad/internal/templates/email
m 765bfe0648 feat(t-paliad-064): bundled-digest reminder service + settings UI (PR-3/4)
Replaces the per-deadline reminder model (overdue / tomorrow /
due_today_evening / weekly templates and four per-kind send paths) with
one bundled digest per (user, slot, local-date) — owner + project leads +
global_admins as audience tiers, three category sections per email.

Service rewrite (internal/services/reminder_service.go):
- RunOnce iterates users, evaluates morning/evening slot per user's tz,
  calls runSlotForUser for each match.
- runSlotForUser checks the slot+date dedup (migration 025), fetches the
  three pending-deadline categories visible to u (overdue / due_today /
  due_warning at u.reminder_warning_offset_days), composes a digest, and
  inserts the dedup row only on successful send.
- Audience filter applied per row in Go: due_warning to owner/lead,
  due_today to owner/lead (+global_admin in evening), overdue to
  owner/global_admin (NOT lead — system failure escalates past the team).
- Subject ladder: ÜBERFÄLLIG / SYSTEMAUSFALL when overdues are in the
  bundle; DRINGEND on evening when due_today still pending; "Frist-
  Erinnerung: N offen" otherwise. EN equivalents.
- Retired sendPerFrist, sendWeekly, deliverFristReminder, deliverWeekly,
  buildSubject, slotForKind, matchesLocalDueDate.

Templates:
- Added deadline_digest.html with three category sections (red/amber/
  neutral), DRINGEND wording on evening, IsOtherOwner attribution row.
- Removed deadline_reminder.html, deadline_due_today.html, deadline_weekly.html.

User schema (Go side):
- models.User gains ReminderWarningOffsetDays (int, default 7) and
  EscalationContactID (*uuid.UUID, nullable).
- userColumns SELECT updated; UpdateProfileInput accepts the new offset
  with 1..30 validation.

Settings → Notifications UI (PR-4):
- New reminder categories: overdue / due_today / due_warning. Legacy
  toggles (tomorrow, due_today_evening, weekly) removed and the legacy
  pref keys are explicitly deleted from the email_preferences object on
  next save so they don't linger.
- New "Vorwarnung (Tage vorher)" input (1..30, required), wired into the
  PATCH /api/me payload as reminder_warning_offset_days.
- Times-section copy refreshed: "Morgen-Slot" / "Abend-Slot (Eskalation)"
  with new hint text reflecting the bundled-digest model.
- DE + EN i18n strings added/updated.

Tests:
- TestCategorize, TestVisibleForCategory, TestBuildDigestSubject lock
  the boundary, recipient-rule, and subject-ladder logic.
- TestRunSlotForUser (live DB, skipped without TEST_DATABASE_URL) covers
  the morning/evening flow, slot+date dedup, and off-slot tick.
- TestRunSlotForUser_EmptyDigest enforces the no-spam rule.
- TestDeliverDigest_RendersTemplate runs the new template on the
  digestRow shape so a typo would fail before any SMTP I/O.
- TestRenderTemplateDeadlineDigest replaces the deleted reminder/weekly
  template tests.

go build/vet/test + bun run build all clean.
2026-04-28 13:17:30 +02:00
..