package scheduler import ( "context" "fmt" "html" "log/slog" "strings" "veola/internal/apify" "veola/internal/db" "veola/internal/email" "veola/internal/models" ) // emailClient builds a Resend client with credentials resolved from settings, // falling back to config.toml. Returns a client even when unconfigured; callers // check Configured() to skip silently. func (s *Scheduler) emailClient(ctx context.Context) *email.Client { apiKey := s.cfg.Resend.APIKey if v, _ := s.store.GetSetting(ctx, "resend_api_key"); v != "" { apiKey = v } from := s.cfg.Resend.From if v, _ := s.store.GetSetting(ctx, "resend_from"); v != "" { from = v } return email.New(apiKey, from) } // sendDealEmails mails a deal alert to every user who opted into deal-alert // email. Entirely best-effort: an unconfigured Resend account or a per-user // send failure is logged and swallowed. func (s *Scheduler) sendDealEmails(ctx context.Context, it models.Item, r apify.UnifiedResult) { client := s.emailClient(ctx) if !client.Configured() { return } recipients, err := s.store.UsersWithDealAlerts(ctx) if err != nil { slog.Error("load deal-alert recipients failed", "err", err) return } if len(recipients) == 0 { return } subject := fmt.Sprintf("Veola deal: %s", it.Name) price := fmt.Sprintf("%s%.2f", currencyPrefix(r.Currency), r.Price) target := "" if it.TargetPrice != nil { target = fmt.Sprintf("%s%.2f", currencyPrefix(r.Currency), *it.TargetPrice) } body := dealEmailHTML(it.Name, r.Title, r.Store, price, target, r.URL) text := dealEmailText(it.Name, r.Title, r.Store, price, target, r.URL) for _, u := range recipients { if err := client.Send(ctx, email.Message{ To: u.Email, Subject: subject, HTML: body, Text: text, }); err != nil { slog.Error("deal email send failed", "to", u.Email, "err", err) } } } // RunWeeklyDigest builds and mails the weekly digest to every opted-in user. // Public so it can be triggered manually as well as on the cron schedule. func (s *Scheduler) RunWeeklyDigest(ctx context.Context) { client := s.emailClient(ctx) if !client.Configured() { return } recipients, err := s.store.UsersWithWeeklyDigest(ctx) if err != nil { slog.Error("load digest recipients failed", "err", err) return } if len(recipients) == 0 { return } items, err := s.store.ListActiveItems(ctx) if err != nil { slog.Error("digest item load failed", "err", err) return } stats, err := s.store.GetDashboardStats(ctx) if err != nil { slog.Error("digest stats load failed", "err", err) return } subject := fmt.Sprintf("Veola weekly digest — %d items watched", len(items)) body := digestHTML(items, stats) text := digestText(items, stats) for _, u := range recipients { if err := client.Send(ctx, email.Message{ To: u.Email, Subject: subject, HTML: body, Text: text, }); err != nil { slog.Error("digest email send failed", "to", u.Email, "err", err) } } slog.Info("weekly digest sent", "recipients", len(recipients), "items", len(items)) } func dealEmailHTML(itemName, title, store, price, target, url string) string { var b strings.Builder fmt.Fprintf(&b, `
%s at %s
`, html.EscapeString(price), html.EscapeString(store)) if target != "" { fmt.Fprintf(&b, `Your target: %s
`, html.EscapeString(target)) } if title != "" { fmt.Fprintf(&b, `%s
`, html.EscapeString(title)) } if url != "" { fmt.Fprintf(&b, ``, html.EscapeString(url)) } b.WriteString(`%d active items. Estimated money saved vs 30-day average: $%.2f.
`, stats.ActiveItems, stats.MoneySaved) } b.WriteString(`| Item | Best price | Where |
|---|---|---|
| %s | %s | %s |