mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
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>
188 lines
5.8 KiB
Go
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")
|
|
}
|
|
}
|