package main import ( "context" "log/slog" "os" "os/signal" "syscall" "github.com/alchemistkay/guestguard/internal/config" "github.com/alchemistkay/guestguard/internal/natspub" "github.com/alchemistkay/guestguard/internal/notification" "github.com/alchemistkay/guestguard/internal/storage" ) func main() { if err := run(); err != nil { slog.Error("fatal", "err", err) os.Exit(1) } } func run() error { cfg, err := config.Load() if err != nil { return err } logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: levelFor(cfg.Env)})) slog.SetDefault(logger) logger = logger.With("service", "notifier") rootCtx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() logger.Info("connecting to database") db, err := storage.NewDB(rootCtx, cfg.DatabaseURL) if err != nil { return err } defer db.Close() logger.Info("connecting to nats", "url", cfg.NATSURL) natsClient, err := natspub.Connect(rootCtx, cfg.NATSURL, logger) if err != nil { return err } defer natsClient.Close() repo := notification.NewRepo(db) sender := notification.LogSender{} rsvpSub, err := natspub.NewRSVPConfirmedSubscriber( rootCtx, natsClient, "notifier-rsvp-confirmed", func(ctx context.Context, evt natspub.RSVPConfirmed) error { return handleRSVPConfirmed(ctx, logger, repo, sender, evt) }, logger, ) if err != nil { return err } rsvpCC, err := rsvpSub.Start(rootCtx) if err != nil { return err } defer rsvpCC.Stop() fraudSub, err := natspub.NewFraudScoredSubscriber( rootCtx, natsClient, "notifier-fraud-scored", func(ctx context.Context, evt natspub.FraudScored) error { return handleFraudScored(ctx, logger, repo, sender, evt) }, logger, ) if err != nil { return err } fraudCC, err := fraudSub.Start(rootCtx) if err != nil { return err } defer fraudCC.Stop() logger.Info("notifier started") <-rootCtx.Done() logger.Info("notifier shutting down") return nil } func handleRSVPConfirmed( ctx context.Context, logger *slog.Logger, repo *notification.Repo, sender notification.Sender, evt natspub.RSVPConfirmed, ) error { msg := notification.OutboundMessage{ GuestID: evt.GuestID, Channel: notification.ChannelEmail, Type: notification.TypeConfirmation, Subject: "Your RSVP is confirmed", Body: "Thanks for your response.", Metadata: map[string]any{ "rsvp_id": evt.RSVPID, "event_id": evt.EventID, "response": evt.Response, "plus_ones": evt.PlusOnes, "risk_score": evt.RiskScore, "submitted_at": evt.SubmittedAt, }, } providerID, sendErr := sender.Send(ctx, msg) status := notification.StatusSent errStr := "" if sendErr != nil { status = notification.StatusFailed errStr = sendErr.Error() } id, err := repo.Record(ctx, notification.RecordParams{ GuestID: evt.GuestID, Channel: msg.Channel, Type: msg.Type, Status: status, ProviderID: providerID, Error: errStr, }) if err != nil { return err } logger.Info("notification dispatched", "notification_id", id, "guest_id", evt.GuestID, "channel", msg.Channel, "type", msg.Type, "status", status, "provider_id", providerID, ) return nil } func handleFraudScored( ctx context.Context, logger *slog.Logger, repo *notification.Repo, sender notification.Sender, evt natspub.FraudScored, ) error { // Only alert when the score crosses a meaningful threshold; low/medium // scores are noise for the host. if evt.Score < 60 { return nil } msg := notification.OutboundMessage{ GuestID: evt.GuestID, Channel: notification.ChannelEmail, Type: notification.TypeReminder, // reusing existing enum; a fraud_alert type would be nicer Subject: "Suspicious access attempt detected", Body: "A flagged access attempt occurred for one of your guests.", Metadata: map[string]any{ "event_id": evt.EventID, "score": evt.Score, "risk": evt.Risk, "reasons": evt.Reasons, }, } providerID, sendErr := sender.Send(ctx, msg) status := notification.StatusSent errStr := "" if sendErr != nil { status = notification.StatusFailed errStr = sendErr.Error() } id, err := repo.Record(ctx, notification.RecordParams{ GuestID: evt.GuestID, Channel: msg.Channel, Type: msg.Type, Status: status, ProviderID: providerID, Error: errStr, }) if err != nil { return err } logger.Warn("fraud alert dispatched", "notification_id", id, "guest_id", evt.GuestID, "score", evt.Score, "risk", evt.Risk, "reasons", evt.Reasons, "status", status, "provider_id", providerID, ) return nil } func levelFor(env string) slog.Level { if env == "development" { return slog.LevelDebug } return slog.LevelInfo }