mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
notification_cursors table stores one read-cursor per user per tenant; GET /v1/activity returns unreadCount + readAt, POST /v1/activity/read persists the cursor. Reminder dispatches now append reminder.sent / reminder.failed system events so delivery history reaches the feed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
202 lines
6.3 KiB
Go
202 lines
6.3 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))
|
|
}
|
|
|
|
events, err := repo.ListActivityEvents(context.Background(), "5d6b3551-0a3e-4b86-bdf0-e9df20a47148", 10)
|
|
if err != nil {
|
|
t.Fatalf("list activity: %v", err)
|
|
}
|
|
for _, event := range events {
|
|
if event.Action == "reminder.sent" && event.ActorKind == "system" {
|
|
if event.EntityID == nil || *event.EntityID != created.ID {
|
|
t.Fatalf("expected entity id %s, got %v", created.ID, event.EntityID)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("expected system reminder.sent activity event")
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|