package notification import ( "context" "fmt" "log/slog" "github.com/google/uuid" ) // Router dispatches an OutboundMessage to the channel-appropriate sender. // The notifier worker holds one Router and stays oblivious to whether the // concrete backend is SES, Twilio, or a logger stub. type Router struct { email Sender sms Sender } func NewRouter(email, sms Sender) *Router { return &Router{email: email, sms: sms} } func (r *Router) Send(ctx context.Context, msg OutboundMessage) (string, error) { switch msg.Channel { case ChannelEmail: if r.email == nil { return "", fmt.Errorf("no email sender configured") } return r.email.Send(ctx, msg) case ChannelSMS: if r.sms == nil { return "", fmt.Errorf("no sms sender configured") } return r.sms.Send(ctx, msg) } return "", fmt.Errorf("router: unknown channel %q", msg.Channel) } // LogGuestEmailDispatcher is the dev-mode dispatcher that renders the // templated email and logs both bodies. Useful for local docker-compose // before SES is configured — gives engineers the rendered output without // needing a real inbox. type LogGuestEmailDispatcher struct { logger *slog.Logger tpls *Templates } func NewLogGuestEmailDispatcher(logger *slog.Logger, tpls *Templates) *LogGuestEmailDispatcher { return &LogGuestEmailDispatcher{logger: logger, tpls: tpls} } func (d *LogGuestEmailDispatcher) SendGuest(_ context.Context, to, subject string, name TemplateName, data map[string]any) (string, error) { if data == nil { data = map[string]any{} } data["Subject"] = subject _, text, err := d.tpls.Render(name, data) if err != nil { return "", err } id := "log:" + uuid.NewString() d.logger.Info("guest email (stub)", "to", to, "subject", subject, "template", string(name), "provider_message_id", id, "text_body", text, ) return id, nil }