diff --git a/internal/services/mail_service_test.go b/internal/services/mail_service_test.go index adb7b1d..cc61b6e 100644 --- a/internal/services/mail_service_test.go +++ b/internal/services/mail_service_test.go @@ -44,9 +44,9 @@ func TestRenderTemplateDeadlineReminder(t *testing.T) { "Kind": "tomorrow", "Title": "Schriftsatz einreichen", "DueDate": "2026-04-21", - "AkteAktenzeichen": "2026/0042", - "AkteTitle": "Mustermann ./. Musterfrau", - "DeadlineURL": "https://paliad.de/deadlines/123", + "ProjectReference": "2026/0042", + "ProjectTitle": "Mustermann ./. Musterfrau", + "DeadlineURL": "https://paliad.de/deadlines/123", }, }) if err != nil { @@ -110,8 +110,8 @@ func TestRenderTemplateDeadlineWeekly(t *testing.T) { "Count": 2, "DeadlinesURL": "https://paliad.de/deadlines", "Items": []map[string]any{ - {"DueDate": "2026-04-20", "Title": "Heute f.", "AkteAktenzeichen": "2026/0001", "URL": "https://paliad.de/deadlines/a", "Overdue": true}, - {"DueDate": "2026-04-24", "Title": "Später f.", "AkteAktenzeichen": "2026/0002", "URL": "https://paliad.de/deadlines/b", "Overdue": false}, + {"DueDate": "2026-04-20", "Title": "Heute f.", "ProjectReference": "2026/0001", "URL": "https://paliad.de/deadlines/a", "Overdue": true}, + {"DueDate": "2026-04-24", "Title": "Später f.", "ProjectReference": "2026/0002", "URL": "https://paliad.de/deadlines/b", "Overdue": false}, }, }, }) diff --git a/internal/services/reminder_service.go b/internal/services/reminder_service.go index 5950991..f18e34f 100644 --- a/internal/services/reminder_service.go +++ b/internal/services/reminder_service.go @@ -130,14 +130,14 @@ func (s *ReminderService) RunOnce(ctx context.Context) { } // fristReminderRow is the projection needed to render a per-Deadline email. -// We join the parent Akte for its Aktenzeichen / title and the user row for +// We join the parent Project for its reference / title and the user row for // the preferred language and notification preferences. type fristReminderRow struct { - DeadlineID uuid.UUID `db:"deadline_id"` - DeadlineTitle string `db:"deadline_title"` + DeadlineID uuid.UUID `db:"deadline_id"` + DeadlineTitle string `db:"deadline_title"` DueDate time.Time `db:"due_date"` - AkteAktenzeichen string `db:"akte_aktenzeichen"` - AkteTitle string `db:"akte_title"` + ProjectReference string `db:"project_reference"` + ProjectTitle string `db:"project_title"` UserID uuid.UUID `db:"user_id"` UserEmail string `db:"user_email"` UserDisplayName string `db:"user_display_name"` @@ -169,19 +169,19 @@ func (s *ReminderService) sendPerFrist(ctx context.Context, today time.Time, kin } query := ` - SELECT f.id AS deadline_id, - f.title AS frist_title, - f.due_date AS due_date, - COALESCE(a.reference, '') AS akte_aktenzeichen, - a.title AS akte_title, - u.id AS user_id, - u.email AS user_email, - u.display_name AS user_display_name, - u.lang AS user_lang, - u.email_preferences AS user_email_preferences + SELECT f.id AS deadline_id, + f.title AS deadline_title, + f.due_date AS due_date, + COALESCE(p.reference, '') AS project_reference, + p.title AS project_title, + u.id AS user_id, + u.email AS user_email, + u.display_name AS user_display_name, + u.lang AS user_lang, + u.email_preferences AS user_email_preferences FROM paliad.deadlines f - JOIN paliad.projects a ON a.id = f.project_id - JOIN paliad.users u ON u.id = f.created_by + JOIN paliad.projects p ON p.id = f.project_id + JOIN paliad.users u ON u.id = f.created_by WHERE f.status = 'pending' AND ` + cond + ` AND NOT EXISTS ( @@ -223,9 +223,9 @@ func (s *ReminderService) deliverFristReminder(ctx context.Context, kind string, "Kind": kind, "Title": r.DeadlineTitle, "DueDate": r.DueDate.Format("2006-01-02"), - "AkteAktenzeichen": r.AkteAktenzeichen, - "AkteTitle": r.AkteTitle, - "DeadlineURL": fmt.Sprintf("%s/deadlines/%s", s.baseURL, r.DeadlineID), + "ProjectReference": r.ProjectReference, + "ProjectTitle": r.ProjectTitle, + "DeadlineURL": fmt.Sprintf("%s/deadlines/%s", s.baseURL, r.DeadlineID), } if err := s.mail.SendTemplate(TemplateData{ To: r.UserEmail, @@ -249,10 +249,10 @@ type weeklyRow struct { UserLang string `db:"user_lang"` UserEmailPreferences json.RawMessage `db:"user_email_preferences"` - DeadlineID uuid.UUID `db:"deadline_id"` - DeadlineTitle string `db:"deadline_title"` + DeadlineID uuid.UUID `db:"deadline_id"` + DeadlineTitle string `db:"deadline_title"` DueDate time.Time `db:"due_date"` - AkteAktenzeichen string `db:"akte_aktenzeichen"` + ProjectReference string `db:"project_reference"` } // reminderEnabled reports whether the user's email_preferences allow a given @@ -288,18 +288,18 @@ func (s *ReminderService) sendWeekly(ctx context.Context, today time.Time) error end := today.AddDate(0, 0, 7) query := ` - SELECT u.id AS user_id, - u.email AS user_email, - u.display_name AS user_display_name, - u.lang AS user_lang, - u.email_preferences AS user_email_preferences, - f.id AS deadline_id, - f.title AS frist_title, - f.due_date AS due_date, - COALESCE(a.reference, '') AS akte_aktenzeichen + SELECT u.id AS user_id, + u.email AS user_email, + u.display_name AS user_display_name, + u.lang AS user_lang, + u.email_preferences AS user_email_preferences, + f.id AS deadline_id, + f.title AS deadline_title, + f.due_date AS due_date, + COALESCE(p.reference, '') AS project_reference FROM paliad.deadlines f - JOIN paliad.projects a ON a.id = f.project_id - JOIN paliad.users u ON u.id = f.created_by + JOIN paliad.projects p ON p.id = f.project_id + JOIN paliad.users u ON u.id = f.created_by WHERE f.status = 'pending' AND f.due_date >= $1 AND f.due_date < $2 @@ -371,7 +371,7 @@ func (s *ReminderService) deliverWeekly(ctx context.Context, today time.Time, ro items = append(items, map[string]any{ "DueDate": r.DueDate.Format("2006-01-02"), "Title": r.DeadlineTitle, - "AkteAktenzeichen": r.AkteAktenzeichen, + "ProjectReference": r.ProjectReference, "URL": fmt.Sprintf("%s/deadlines/%s", s.baseURL, r.DeadlineID), "Overdue": r.DueDate.Before(today), }) diff --git a/internal/services/reminder_service_test.go b/internal/services/reminder_service_test.go index 416ab5c..cb951e9 100644 --- a/internal/services/reminder_service_test.go +++ b/internal/services/reminder_service_test.go @@ -1,8 +1,17 @@ package services import ( + "context" "encoding/json" + "os" "testing" + "time" + + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" + + "mgit.msbls.de/m/patholo/internal/db" ) func TestReminderEnabled(t *testing.T) { @@ -48,3 +57,99 @@ func TestReminderEnabled(t *testing.T) { }) } } + +// TestSendPerFrist_ScansCleanly is the regression guard for the German→English +// rename: the prior `frist_title` / `akte_aktenzeichen` / `akte_title` SQL +// aliases didn't match the renamed struct tags, so sqlx.SelectContext returned +// "missing destination name" errors and every reminder scan silently failed. +// +// This test seeds one project + one due-today deadline owned by a user, then +// calls sendPerFrist with the overdue kind. A scan error would surface as a +// non-nil return; success means every aliased column maps to a struct field. +// +// Skips when TEST_DATABASE_URL is unset, mirroring the rest of the live-DB +// suite. +func TestSendPerFrist_ScansCleanly(t *testing.T) { + url := os.Getenv("TEST_DATABASE_URL") + if url == "" { + t.Skip("TEST_DATABASE_URL not set — skipping live DB test") + } + if err := db.ApplyMigrations(url); err != nil { + t.Fatalf("apply migrations: %v", err) + } + pool, err := sqlx.Connect("postgres", url) + if err != nil { + t.Fatalf("connect: %v", err) + } + defer pool.Close() + + ctx := context.Background() + today := time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC) + + userID := uuid.New() + projectID := uuid.New() + deadlineID := uuid.New() + + // Cleanup before and after — leave no rows behind on shared dev DBs. + cleanup := func() { + pool.ExecContext(ctx, `DELETE FROM paliad.reminder_log WHERE user_id = $1`, userID) + pool.ExecContext(ctx, `DELETE FROM paliad.deadlines WHERE id = $1`, deadlineID) + pool.ExecContext(ctx, `DELETE FROM paliad.project_teams WHERE project_id = $1`, projectID) + pool.ExecContext(ctx, `DELETE FROM paliad.projects WHERE id = $1`, projectID) + pool.ExecContext(ctx, `DELETE FROM paliad.users WHERE id = $1`, userID) + pool.ExecContext(ctx, `DELETE FROM auth.users WHERE id = $1`, userID) + } + cleanup() + defer cleanup() + + // auth.users → paliad.users (the JOIN on f.created_by = u.id needs both). + if _, err := pool.ExecContext(ctx, + `INSERT INTO auth.users (id, email) VALUES ($1, $2)`, + userID, "scan-test@hlc.com"); err != nil { + t.Fatalf("seed auth.users: %v", err) + } + if _, err := pool.ExecContext(ctx, + `INSERT INTO paliad.users (id, email, display_name, office, role, lang, email_preferences) + VALUES ($1, $2, $3, 'munich', 'associate', 'de', '{}'::jsonb)`, + userID, "scan-test@hlc.com", "Scan Test"); err != nil { + t.Fatalf("seed paliad.users: %v", err) + } + + // Root project (path is rewritten by the BEFORE-INSERT trigger; we still + // have to pass a placeholder because the column is NOT NULL). + if _, err := pool.ExecContext(ctx, + `INSERT INTO paliad.projects (id, type, path, title, reference, status, created_by) + VALUES ($1, 'project', $1::text, 'Scan Test Project', '2026/9999', 'active', $2)`, + projectID, userID); err != nil { + t.Fatalf("seed paliad.projects: %v", err) + } + + // Due-today deadline owned by the seeded user — matches both the overdue + // (due_date <= today) and tomorrow (due_date = today+1) queries depending + // on the kind passed below. + if _, err := pool.ExecContext(ctx, + `INSERT INTO paliad.deadlines (id, project_id, title, due_date, source, status, created_by) + VALUES ($1, $2, 'Schriftsatz einreichen', $3, 'manual', 'pending', $4)`, + deadlineID, projectID, today, userID); err != nil { + t.Fatalf("seed paliad.deadlines: %v", err) + } + + // MailService with no SMTP env is a silent no-op — perfect for a scan-path + // test; we only care that the SELECT scans without error. + mail, err := NewMailService() + if err != nil { + t.Fatalf("NewMailService: %v", err) + } + svc := NewReminderService(pool, mail, nil, "https://paliad.test") + svc.clock = func() time.Time { return today.Add(12 * time.Hour) } + + if err := svc.sendPerFrist(ctx, today, "overdue"); err != nil { + t.Fatalf("sendPerFrist(overdue): %v", err) + } + if err := svc.sendPerFrist(ctx, today, "tomorrow"); err != nil { + t.Fatalf("sendPerFrist(tomorrow): %v", err) + } + if err := svc.sendWeekly(ctx, today); err != nil { + t.Fatalf("sendWeekly: %v", err) + } +} diff --git a/internal/templates/email/deadline_reminder.html b/internal/templates/email/deadline_reminder.html index 826a6ad..d50e511 100644 --- a/internal/templates/email/deadline_reminder.html +++ b/internal/templates/email/deadline_reminder.html @@ -27,8 +27,8 @@
{{if eq .Lang "en"}}Matter:{{else}}Akte:{{end}} - {{.AkteAktenzeichen}} - {{if .AkteTitle}} — {{.AkteTitle}}{{end}} + {{.ProjectReference}} + {{if .ProjectTitle}} — {{.ProjectTitle}}{{end}}
diff --git a/internal/templates/email/deadline_weekly.html b/internal/templates/email/deadline_weekly.html index 4e5a486..abaf33e 100644 --- a/internal/templates/email/deadline_weekly.html +++ b/internal/templates/email/deadline_weekly.html @@ -25,7 +25,7 @@ {{.Title}} - {{.AkteAktenzeichen}} + {{.ProjectReference}} {{end}}