package reports import ( "context" "errors" "strings" "testing" "time" "bookra/apps/backend/internal/bookings" "bookra/apps/backend/internal/db" "bookra/apps/backend/internal/notifications" ) func newTestService(repo db.Repository) *Service { return NewService(repo, bookings.NewService(repo, nil, "http://localhost:3000"), "http://localhost:3000", "noreply@bookra.dev") } func TestDispatchDigestsSendsLocalizedSummary(t *testing.T) { repo := db.NewMemoryRepository() svc := newTestService(repo) ctx := context.Background() tenantID := "5d6b3551-0a3e-4b86-bdf0-e9df20a47148" serviceID := "d5d76a61-3d49-467c-8dd4-bf61ee754e39" _, err := repo.CreateBooking(ctx, db.CreateBookingParams{ TenantID: tenantID, ServiceID: &serviceID, BookingMode: "appointment", CustomerName: "Klára", CustomerEmail: "klara@example.com", StartsAt: time.Now().UTC().Add(-26 * time.Hour), EndsAt: time.Now().UTC().Add(-25 * time.Hour), Status: "confirmed", Reference: "BK-DIG-1", }) if err != nil { t.Fatalf("seed booking: %v", err) } var sent []notifications.EmailMessage capture := func(_ context.Context, msg notifications.EmailMessage) error { sent = append(sent, msg) return nil } response, err := svc.DispatchDigests(ctx, 7, tenantID, capture) if err != nil { t.Fatalf("dispatch: %v", err) } if response.TenantCount != 1 || response.SentCount != 1 { t.Fatalf("expected 1 tenant / 1 sent, got %+v", response) } if len(sent) != 1 { t.Fatalf("expected 1 email, got %d", len(sent)) } msg := sent[0] if msg.To != "owner@bookra.dev" || msg.From != "noreply@bookra.dev" { t.Fatalf("bad addressing: %+v", msg) } // Demo tenant locale is cs — subject should be the Czech digest. if !strings.Contains(msg.Subject, "přehled") { t.Fatalf("expected Czech subject, got %q", msg.Subject) } if !strings.Contains(msg.Text, "Rezervace celkem") || !strings.Contains(msg.Text, "1200 Kč") { t.Fatalf("digest body missing totals: %q", msg.Text) } if !strings.Contains(msg.Text, "Signature treatment") { t.Fatalf("digest body missing service line: %q", msg.Text) } } func TestDispatchDigestsSkipsZeroActivityTenant(t *testing.T) { // NewMemoryRepository seeds the tenant, staff, and services but no // bookings — the trailing window is empty, so the digest must skip. repo := db.NewMemoryRepository() svc := newTestService(repo) calls := 0 response, err := svc.DispatchDigests(context.Background(), 7, "", func(context.Context, notifications.EmailMessage) error { calls++ return nil }) if err != nil { t.Fatalf("dispatch: %v", err) } if response.TenantCount != 1 || response.SkippedCount != 1 || calls != 0 { t.Fatalf("expected 1 skipped tenant and no sends, got %+v (calls=%d)", response, calls) } } func TestDispatchDigestsCountsSendFailures(t *testing.T) { repo := db.NewMemoryRepository() svc := newTestService(repo) ctx := context.Background() tenantID := "5d6b3551-0a3e-4b86-bdf0-e9df20a47148" serviceID := "d5d76a61-3d49-467c-8dd4-bf61ee754e39" if _, err := repo.CreateBooking(ctx, db.CreateBookingParams{ TenantID: tenantID, ServiceID: &serviceID, BookingMode: "appointment", CustomerName: "Klára", CustomerEmail: "klara@example.com", StartsAt: time.Now().UTC().Add(-2 * time.Hour), EndsAt: time.Now().UTC().Add(-time.Hour), Status: "confirmed", Reference: "BK-DIG-2", }); err != nil { t.Fatalf("seed booking: %v", err) } response, err := svc.DispatchDigests(ctx, 7, tenantID, func(context.Context, notifications.EmailMessage) error { return errors.New("smtp down") }) if err != nil { t.Fatalf("dispatch: %v", err) } if response.FailedCount != 1 || response.SentCount != 0 { t.Fatalf("expected 1 failed send, got %+v", response) } }