mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-04 20:43:01 +00:00
cleanup
This commit is contained in:
@@ -4,6 +4,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bookra/apps/backend/internal/config"
|
||||
@@ -23,36 +26,30 @@ type EmailMessage struct {
|
||||
To string
|
||||
Subject string
|
||||
Text string
|
||||
}
|
||||
|
||||
type SMSMessage struct {
|
||||
From string
|
||||
To string
|
||||
Text string
|
||||
HTML string
|
||||
}
|
||||
|
||||
type EmailProvider interface {
|
||||
Send(context.Context, EmailMessage) (DeliveryReceipt, error)
|
||||
}
|
||||
|
||||
type SMSProvider interface {
|
||||
Send(context.Context, SMSMessage) (DeliveryReceipt, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
cfg config.Config
|
||||
repo db.Repository
|
||||
emailProvider EmailProvider
|
||||
smsProvider SMSProvider
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewService(cfg config.Config, repo db.Repository) *Service {
|
||||
emailProvider := EmailProvider(noopEmailProvider{})
|
||||
if cfg.SMTPHost != "" {
|
||||
emailProvider = smtpEmailProvider{cfg: cfg}
|
||||
}
|
||||
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
repo: repo,
|
||||
emailProvider: noopEmailProvider{},
|
||||
smsProvider: noopSMSProvider{},
|
||||
emailProvider: emailProvider,
|
||||
now: func() time.Time { return time.Now().UTC() },
|
||||
}
|
||||
}
|
||||
@@ -86,15 +83,6 @@ func (s *Service) DispatchDue(ctx context.Context, limit int) (domain.DispatchRe
|
||||
provider = receipt.Provider
|
||||
externalID = receipt.ExternalID
|
||||
}
|
||||
case "sms":
|
||||
receipt, sendErr := s.smsProvider.Send(ctx, renderSMSMessage(s.cfg.SMSFrom, job))
|
||||
if sendErr != nil {
|
||||
status = "failed"
|
||||
errorMessage = sendErr.Error()
|
||||
} else {
|
||||
provider = receipt.Provider
|
||||
externalID = receipt.ExternalID
|
||||
}
|
||||
default:
|
||||
status = "failed"
|
||||
errorMessage = ErrUnsupportedChannel.Error()
|
||||
@@ -103,8 +91,6 @@ func (s *Service) DispatchDue(ctx context.Context, limit int) (domain.DispatchRe
|
||||
if provider == "unknown" {
|
||||
if job.Channel == "email" {
|
||||
provider = "noop-email"
|
||||
} else if job.Channel == "sms" {
|
||||
provider = "noop-sms"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,23 +120,53 @@ func (s *Service) DispatchDue(ctx context.Context, limit int) (domain.DispatchRe
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func renderEmailMessage(from string, job db.ReminderJobRecord) EmailMessage {
|
||||
subject, body := renderReminderCopy(job)
|
||||
return EmailMessage{
|
||||
From: from,
|
||||
To: job.CustomerEmail,
|
||||
Subject: subject,
|
||||
Text: body,
|
||||
// SendBookingConfirmation sends a booking confirmation email to the customer
|
||||
func (s *Service) SendBookingConfirmation(ctx context.Context, data BookingEmailData) error {
|
||||
if data.BusinessEmail == "" {
|
||||
data.BusinessEmail = s.cfg.EmailFrom
|
||||
}
|
||||
data.Type = EmailTypeConfirmation
|
||||
msg := RenderEmailMessage(data)
|
||||
_, err := s.emailProvider.Send(ctx, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
func renderSMSMessage(from string, job db.ReminderJobRecord) SMSMessage {
|
||||
subject, body := renderReminderCopy(job)
|
||||
return SMSMessage{
|
||||
From: from,
|
||||
To: job.CustomerEmail,
|
||||
Text: fmt.Sprintf("%s: %s", subject, body),
|
||||
// SendBookingReschedule sends a reschedule notification email to the customer
|
||||
func (s *Service) SendBookingReschedule(ctx context.Context, data BookingEmailData) error {
|
||||
if data.BusinessEmail == "" {
|
||||
data.BusinessEmail = s.cfg.EmailFrom
|
||||
}
|
||||
data.Type = EmailTypeReschedule
|
||||
msg := RenderEmailMessage(data)
|
||||
_, err := s.emailProvider.Send(ctx, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendBookingCancellation sends a cancellation notification email to the customer
|
||||
func (s *Service) SendBookingCancellation(ctx context.Context, data BookingEmailData) error {
|
||||
if data.BusinessEmail == "" {
|
||||
data.BusinessEmail = s.cfg.EmailFrom
|
||||
}
|
||||
data.Type = EmailTypeCancellation
|
||||
msg := RenderEmailMessage(data)
|
||||
_, err := s.emailProvider.Send(ctx, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendBusinessNotification sends a notification to the business about a new booking
|
||||
func (s *Service) SendBusinessNotification(ctx context.Context, businessEmail string, data BookingEmailData) error {
|
||||
if businessEmail == "" {
|
||||
return nil // Skip if no business email configured
|
||||
}
|
||||
data.Type = EmailTypeBusinessNotify
|
||||
msg := RenderEmailMessage(data)
|
||||
msg.To = businessEmail
|
||||
_, err := s.emailProvider.Send(ctx, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
func renderEmailMessage(from string, job db.ReminderJobRecord) EmailMessage {
|
||||
return RenderReminderEmail(from, job)
|
||||
}
|
||||
|
||||
func renderReminderCopy(job db.ReminderJobRecord) (string, string) {
|
||||
@@ -190,9 +206,6 @@ func localizedStartsAt(job db.ReminderJobRecord) string {
|
||||
}
|
||||
|
||||
func reminderRecipient(job db.ReminderJobRecord) string {
|
||||
if job.Channel == "email" {
|
||||
return job.CustomerEmail
|
||||
}
|
||||
return job.CustomerEmail
|
||||
}
|
||||
|
||||
@@ -208,14 +221,67 @@ func (noopEmailProvider) Send(_ context.Context, message EmailMessage) (Delivery
|
||||
}, nil
|
||||
}
|
||||
|
||||
type noopSMSProvider struct{}
|
||||
type smtpEmailProvider struct {
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
func (noopSMSProvider) Send(_ context.Context, message SMSMessage) (DeliveryReceipt, error) {
|
||||
if message.To == "" {
|
||||
return DeliveryReceipt{Provider: "noop-sms"}, errors.New("missing sms recipient")
|
||||
func (p smtpEmailProvider) Send(_ context.Context, message EmailMessage) (DeliveryReceipt, error) {
|
||||
if strings.TrimSpace(message.To) == "" {
|
||||
return DeliveryReceipt{Provider: "smtp"}, errors.New("missing email recipient")
|
||||
}
|
||||
|
||||
host := strings.TrimSpace(p.cfg.SMTPHost)
|
||||
if host == "" {
|
||||
return DeliveryReceipt{Provider: "smtp"}, errors.New("smtp host is not configured")
|
||||
}
|
||||
|
||||
address := net.JoinHostPort(host, strings.TrimSpace(p.cfg.SMTPPort))
|
||||
var auth smtp.Auth
|
||||
if p.cfg.SMTPUsername != "" {
|
||||
auth = smtp.PlainAuth("", p.cfg.SMTPUsername, p.cfg.SMTPPassword, host)
|
||||
}
|
||||
|
||||
// Build multipart email with both plain text and HTML
|
||||
var payload string
|
||||
if message.HTML != "" {
|
||||
boundary := "BOOKRA-BOUNDARY"
|
||||
payload = strings.Join([]string{
|
||||
fmt.Sprintf("From: %s", message.From),
|
||||
fmt.Sprintf("To: %s", message.To),
|
||||
fmt.Sprintf("Subject: %s", message.Subject),
|
||||
"MIME-Version: 1.0",
|
||||
fmt.Sprintf("Content-Type: multipart/alternative; boundary=\"%s\"", boundary),
|
||||
"",
|
||||
fmt.Sprintf("--%s", boundary),
|
||||
"Content-Type: text/plain; charset=UTF-8",
|
||||
"",
|
||||
message.Text,
|
||||
"",
|
||||
fmt.Sprintf("--%s", boundary),
|
||||
"Content-Type: text/html; charset=UTF-8",
|
||||
"",
|
||||
message.HTML,
|
||||
"",
|
||||
fmt.Sprintf("--%s--", boundary),
|
||||
}, "\r\n")
|
||||
} else {
|
||||
payload = strings.Join([]string{
|
||||
fmt.Sprintf("From: %s", message.From),
|
||||
fmt.Sprintf("To: %s", message.To),
|
||||
fmt.Sprintf("Subject: %s", message.Subject),
|
||||
"MIME-Version: 1.0",
|
||||
"Content-Type: text/plain; charset=UTF-8",
|
||||
"",
|
||||
message.Text,
|
||||
}, "\r\n")
|
||||
}
|
||||
|
||||
if err := smtp.SendMail(address, auth, message.From, []string{message.To}, []byte(payload)); err != nil {
|
||||
return DeliveryReceipt{Provider: "smtp"}, err
|
||||
}
|
||||
|
||||
return DeliveryReceipt{
|
||||
Provider: "noop-sms",
|
||||
ExternalID: fmt.Sprintf("noop-sms-%d", time.Now().UnixNano()),
|
||||
Provider: "smtp",
|
||||
ExternalID: fmt.Sprintf("smtp-%d", time.Now().UnixNano()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user