Files
Bookra/apps/backend/internal/notifications/service_test.go
T
Tomas DvorakandDevin 7509e82261 feat(reminders): SMS channel for reminder jobs
Tenants paying for the SMS add-on still got email-only reminders —
the channel column existed but nothing armed or dispatched sms jobs.
Booking creation now adds a second job when SMS is enabled and the
customer left a phone; dispatch routes sms jobs through an injected
sender adapter wired to the existing sms.Service, so monthly limits,
usage metering, and Stripe billing apply unchanged. Lifecycle handling
from the previous milestone clears and re-arms both channels together.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 13:03:11 +02:00

188 lines
5.8 KiB
Go

package notifications
import (
"context"
"testing"
"time"
"bookra/apps/backend/internal/config"
"bookra/apps/backend/internal/db"
"bookra/apps/backend/internal/domain"
)
func TestDispatchDueProcessesPendingEmailReminders(t *testing.T) {
repo := db.NewMemoryRepository()
startsAt := time.Now().UTC().Add(26 * time.Hour)
created, err := repo.CreateBooking(context.Background(), db.CreateBookingParams{
TenantID: "5d6b3551-0a3e-4b86-bdf0-e9df20a47148",
BookingMode: "appointment",
CustomerName: "Reminder Customer",
CustomerEmail: "[email protected]",
StartsAt: startsAt,
EndsAt: startsAt.Add(time.Hour),
Status: "confirmed",
Reference: "BK-TEST-REMINDER",
})
if err != nil {
t.Fatalf("create booking: %v", err)
}
if err := repo.CreateReminderJob(context.Background(), db.ReminderJobParams{
TenantID: "5d6b3551-0a3e-4b86-bdf0-e9df20a47148",
BookingID: created.ID,
Channel: "email",
ScheduledFor: time.Now().UTC().Add(-time.Minute),
}); err != nil {
t.Fatalf("create reminder job: %v", err)
}
service := NewService(config.Config{
Environment: "development",
EmailFrom: "[email protected]",
}, repo)
response, err := service.DispatchDue(context.Background(), 10)
if err != nil {
t.Fatalf("dispatch due: %v", err)
}
if response.ProcessedCount != 1 {
t.Fatalf("expected processed count 1, got %d", response.ProcessedCount)
}
if response.SentCount != 1 {
t.Fatalf("expected sent count 1, got %d", response.SentCount)
}
if response.FailedCount != 0 {
t.Fatalf("expected failed count 0, got %d", response.FailedCount)
}
pending, err := repo.ListDueReminderJobs(context.Background(), time.Now().UTC().Add(time.Hour), 10)
if err != nil {
t.Fatalf("list due reminder jobs: %v", err)
}
if len(pending) != 0 {
t.Fatalf("expected no pending jobs after dispatch, got %d", len(pending))
}
}
func TestDispatchDueFailsUnknownChannel(t *testing.T) {
repo := db.NewMemoryRepository()
startsAt := time.Now().UTC().Add(26 * time.Hour)
created, err := repo.CreateBooking(context.Background(), db.CreateBookingParams{
TenantID: "5d6b3551-0a3e-4b86-bdf0-e9df20a47148",
BookingMode: "appointment",
StartsAt: startsAt,
EndsAt: startsAt.Add(time.Hour),
Status: "confirmed",
Reference: "BK-TEST-PUSH",
})
if err != nil {
t.Fatalf("create booking: %v", err)
}
if err := repo.CreateReminderJob(context.Background(), db.ReminderJobParams{
TenantID: "5d6b3551-0a3e-4b86-bdf0-e9df20a47148",
BookingID: created.ID,
Channel: "push",
ScheduledFor: time.Now().UTC().Add(-time.Minute),
}); err != nil {
t.Fatalf("create reminder job: %v", err)
}
service := NewService(config.Config{Environment: "development"}, repo)
response, err := service.DispatchDue(context.Background(), 10)
if err != nil {
t.Fatalf("dispatch due: %v", err)
}
if response.ProcessedCount != 1 || response.FailedCount != 1 {
t.Fatalf("expected one failed job, got %+v", response)
}
}
func TestDispatchRequestContractShape(t *testing.T) {
request := domain.DispatchReminderJobsRequest{Limit: 20}
if request.Limit != 20 {
t.Fatalf("expected limit 20, got %d", request.Limit)
}
}
func TestDispatchDueSkipsNonConfirmedBooking(t *testing.T) {
repo := db.NewMemoryRepository()
startsAt := time.Now().UTC().Add(26 * time.Hour)
created, err := repo.CreateBooking(context.Background(), db.CreateBookingParams{
TenantID: "5d6b3551-0a3e-4b86-bdf0-e9df20a47148",
BookingMode: "appointment",
StartsAt: startsAt,
EndsAt: startsAt.Add(time.Hour),
Status: "confirmed",
Reference: "BK-TEST-SKIP",
})
if err != nil {
t.Fatalf("create booking: %v", err)
}
if err := repo.CreateReminderJob(context.Background(), db.ReminderJobParams{
TenantID: "5d6b3551-0a3e-4b86-bdf0-e9df20a47148",
BookingID: created.ID,
Channel: "email",
ScheduledFor: time.Now().UTC().Add(-time.Minute),
}); err != nil {
t.Fatalf("create reminder job: %v", err)
}
if err := repo.UpdateBookingStatus(context.Background(), created.ID, "cancelled"); err != nil {
t.Fatalf("cancel booking: %v", err)
}
service := NewService(config.Config{Environment: "development"}, repo)
response, err := service.DispatchDue(context.Background(), 10)
if err != nil {
t.Fatalf("dispatch due: %v", err)
}
if response.ProcessedCount != 1 || response.SkippedCount != 1 || response.SentCount != 0 {
t.Fatalf("expected 1 skipped, got %+v", response)
}
}
func TestDispatchDueSendsSmsReminder(t *testing.T) {
repo := db.NewMemoryRepository()
startsAt := time.Now().UTC().Add(26 * time.Hour)
created, err := repo.CreateBooking(context.Background(), db.CreateBookingParams{
TenantID: "5d6b3551-0a3e-4b86-bdf0-e9df20a47148",
BookingMode: "appointment",
CustomerPhone: "+420777000111",
StartsAt: startsAt,
EndsAt: startsAt.Add(time.Hour),
Status: "confirmed",
Reference: "BK-TEST-SMS",
})
if err != nil {
t.Fatalf("create booking: %v", err)
}
if err := repo.CreateReminderJob(context.Background(), db.ReminderJobParams{
TenantID: "5d6b3551-0a3e-4b86-bdf0-e9df20a47148",
BookingID: created.ID,
Channel: "sms",
ScheduledFor: time.Now().UTC().Add(-time.Minute),
}); err != nil {
t.Fatalf("create reminder job: %v", err)
}
var sentPhone, sentBody string
service := NewService(config.Config{Environment: "development"}, repo)
service.SetSMSSender(func(_ context.Context, tenantID, phone, body string) error {
sentPhone = phone
sentBody = body
return nil
})
response, err := service.DispatchDue(context.Background(), 10)
if err != nil {
t.Fatalf("dispatch due: %v", err)
}
if response.SentCount != 1 {
t.Fatalf("expected 1 sent, got %+v", response)
}
if sentPhone != "+420777000111" {
t.Fatalf("expected phone recipient, got %q", sentPhone)
}
if sentBody == "" {
t.Fatal("expected rendered sms body")
}
}