diff --git a/.env.local.example b/.env.local.example index a57ede91..910f564e 100644 --- a/.env.local.example +++ b/.env.local.example @@ -360,3 +360,5 @@ TWILIO_MESSAGING_SERVICE_SID= TWILIO_PUBLIC_BASE_URL= # Example only; replace with the externally reachable canonical HTTPS origin. # TWILIO_PUBLIC_BASE_URL=https://api.example.com +# Country assumed for phone numbers entered without a country code (ISO 3166 alpha-2). +NOTIFICATION_DEFAULT_PHONE_REGION=US diff --git a/backend/internal/cmd/serve/serve.go b/backend/internal/cmd/serve/serve.go index db84428f..bf581280 100644 --- a/backend/internal/cmd/serve/serve.go +++ b/backend/internal/cmd/serve/serve.go @@ -40,7 +40,9 @@ import ( "github.com/trakrf/platform/backend/internal/logger" "github.com/trakrf/platform/backend/internal/mustering" "github.com/trakrf/platform/backend/internal/notification" + "github.com/trakrf/platform/backend/internal/notification/outbox" "github.com/trakrf/platform/backend/internal/notification/resend" + "github.com/trakrf/platform/backend/internal/notification/routing" "github.com/trakrf/platform/backend/internal/notification/twilio" "github.com/trakrf/platform/backend/internal/readercontrol" authservice "github.com/trakrf/platform/backend/internal/services/auth" @@ -124,6 +126,45 @@ func Run(ctx context.Context, info buildinfo.Info, frontendFS fs.FS) error { } log.Info().Bool("enabled", emailConfig.Enabled).Msg("Notification email integration configured") + // The delivery planner turns a detected alarm or move into queued + // deliveries. Built at boot so a bad phone region or a broken template + // refuses to start; detection wires it in later, and the queue runtime + // that would run its trigger jobs is not started yet. + routingConfig, err := routing.ConfigFromEnv() + if err != nil { + return err + } + templates, err := routing.NewTemplateRenderer() + if err != nil { + return err + } + jobInserter, err := outbox.NewInserter(store.Pool().(*pgxpool.Pool)) + if err != nil { + return err + } + planningMetrics, err := routing.NewMetrics(prometheus.DefaultRegisterer) + if err != nil { + return err + } + outboxMetrics, err := outbox.NewMetrics(prometheus.DefaultRegisterer) + if err != nil { + return err + } + deliveryPlanner, err := routing.NewDeliveryPlanner(routing.Deps{ + Store: store, + Jobs: jobInserter, + Renderer: templates, + Metrics: planningMetrics, + OutboxMetrics: outboxMetrics, + PhoneRegion: routingConfig.PhoneRegion, + Log: *log, + }) + if err != nil { + return err + } + _ = deliveryPlanner + log.Info().Str("phone_region", routingConfig.PhoneRegion).Msg("notification delivery planner constructed") + // Say it at boot as well as on /health (TRA-1190). The endpoint is what a // test suite can check; this line is what a developer actually sees, and // `just dev` completing against a schema two migrations behind is the root diff --git a/backend/internal/notification/routing/config.go b/backend/internal/notification/routing/config.go new file mode 100644 index 00000000..3e989c15 --- /dev/null +++ b/backend/internal/notification/routing/config.go @@ -0,0 +1,30 @@ +package routing + +import ( + "fmt" + "os" + "strings" + + "github.com/trakrf/platform/backend/internal/notification/phone" +) + +// DefaultPhoneRegion is the country assumed for phone numbers entered without +// a country code, when NOTIFICATION_DEFAULT_PHONE_REGION is not set. +const DefaultPhoneRegion = "US" + +// Config holds the delivery planner's environment settings. +type Config struct { + PhoneRegion string +} + +// ConfigFromEnv reads the planner settings and rejects an unknown phone region. +func ConfigFromEnv() (Config, error) { + region := strings.ToUpper(strings.TrimSpace(os.Getenv("NOTIFICATION_DEFAULT_PHONE_REGION"))) + if region == "" { + region = DefaultPhoneRegion + } + if err := phone.ValidateRegion(region); err != nil { + return Config{}, fmt.Errorf("NOTIFICATION_DEFAULT_PHONE_REGION %q: %w", region, err) + } + return Config{PhoneRegion: region}, nil +} diff --git a/backend/internal/notification/routing/config_test.go b/backend/internal/notification/routing/config_test.go new file mode 100644 index 00000000..6d329a62 --- /dev/null +++ b/backend/internal/notification/routing/config_test.go @@ -0,0 +1,29 @@ +package routing + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConfigFromEnv(t *testing.T) { + t.Run("unset defaults to US", func(t *testing.T) { + t.Setenv("NOTIFICATION_DEFAULT_PHONE_REGION", "") + cfg, err := ConfigFromEnv() + require.NoError(t, err) + assert.Equal(t, "US", cfg.PhoneRegion) + }) + t.Run("lower case is upper-cased", func(t *testing.T) { + t.Setenv("NOTIFICATION_DEFAULT_PHONE_REGION", "ug") + cfg, err := ConfigFromEnv() + require.NoError(t, err) + assert.Equal(t, "UG", cfg.PhoneRegion) + }) + t.Run("unknown region is an error naming the variable", func(t *testing.T) { + t.Setenv("NOTIFICATION_DEFAULT_PHONE_REGION", "ZZ") + _, err := ConfigFromEnv() + require.Error(t, err) + assert.Contains(t, err.Error(), "NOTIFICATION_DEFAULT_PHONE_REGION") + }) +}