Merge: fix reminder_service SQL alias mismatch (t-paliad-032)
This commit is contained in:
@@ -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},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
</div>
|
||||
<div style="color:#57534e;font-size:13px;">
|
||||
{{if eq .Lang "en"}}Matter:{{else}}Akte:{{end}}
|
||||
<strong style="color:#1c1917;">{{.AkteAktenzeichen}}</strong>
|
||||
{{if .AkteTitle}} — {{.AkteTitle}}{{end}}
|
||||
<strong style="color:#1c1917;">{{.ProjectReference}}</strong>
|
||||
{{if .ProjectTitle}} — {{.ProjectTitle}}{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<td style="padding:10px 12px;font-size:13px;border-bottom:1px solid #f5f5f4;">
|
||||
<a href="{{.URL}}" style="color:#1c1917;text-decoration:none;font-weight:500;">{{.Title}}</a>
|
||||
</td>
|
||||
<td style="padding:10px 12px;font-size:13px;color:#57534e;border-bottom:1px solid #f5f5f4;">{{.AkteAktenzeichen}}</td>
|
||||
<td style="padding:10px 12px;font-size:13px;color:#57534e;border-bottom:1px solid #f5f5f4;">{{.ProjectReference}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user