diff --git a/apps/api/.env.dev.example b/apps/api/.env.dev.example index 08eedf35..d4933254 100644 --- a/apps/api/.env.dev.example +++ b/apps/api/.env.dev.example @@ -27,4 +27,14 @@ CLIENT_URL=http://localhost:5173 # For monitoring GRAFANA_URL=http://grafana:3000 -MONITORING_DISCORD_WEBHOOK= \ No newline at end of file +MONITORING_DISCORD_WEBHOOK= + +# AWS SES (email sending) +AWS_ACCESS_KEY= +AWS_ACCESS_KEY_SECRET= +AWS_REGION=us-east-1 +EMAIL_TEMPLATE_DIRECTORY="../../internal/emailutils/templates/" + +# Optional: route SES at the local LocalStack emulator instead of real AWS, +# for end-to-end email testing. Requires: docker compose up localstack +# AWS_ENDPOINT_URL=http://localstack:4566 diff --git a/apps/api/internal/domains/email/campaign_service.go b/apps/api/internal/domains/email/campaign_service.go index e6bd8633..0a827c48 100644 --- a/apps/api/internal/domains/email/campaign_service.go +++ b/apps/api/internal/domains/email/campaign_service.go @@ -171,6 +171,14 @@ func (s *EmailCampaignService) UpdateCampaignStatus( return nil, ErrEmailCampaignSentAtRequired } + // A draft has no send time, so unscheduling clears it. The handler cannot do + // this itself: ScheduledAt is a *time.Time, so an omitted field and an explicit + // null both arrive as nil and it has to assume "leave it alone". + if params.Status == sqlc.EmailCampaignStatusDraft { + params.ScheduledAtDoUpdate = true + params.ScheduledAt = nil + } + return s.emailCampaignRepo.UpdateEmailCampaignStatus(ctx, params) } diff --git a/apps/api/internal/domains/email/service.go b/apps/api/internal/domains/email/service.go index bda2bed4..128689f0 100644 --- a/apps/api/internal/domains/email/service.go +++ b/apps/api/internal/domains/email/service.go @@ -206,12 +206,14 @@ func (s *EmailService) SendHtmlEmail(recipient string, subject string, templateD template, err := template.ParseFiles(templateFilePath) if err != nil { - s.logger.Err(err).Msg("Failed to parse email template for recipient") + s.logger.Err(err).Str("Template", templateFilePath).Msg("Failed to parse email template for recipient") + return err } err = template.Execute(&body, templateData) if err != nil { - s.logger.Err(err).Msg("Failed to inject template variables for recipient '%s'.") + s.logger.Err(err).Str("Template", templateFilePath).Msg("Failed to inject template variables for recipient") + return err } err = s.SESClient.SendHTMLEmail([]string{recipient}, "SwampHacks ", subject, body.String()) diff --git a/docker-compose.yml b/docker-compose.yml index 916e8dd5..bfb9628d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -87,6 +87,16 @@ services: retries: 5 start_period: 5s + # Local SES emulator for end-to-end email testing. + # Set AWS_ENDPOINT_URL=http://localstack:4566 in apps/api/.env.dev to use it. + localstack: + image: localstack/localstack:3.8 + container_name: localstack + ports: + - "4566:4566" + environment: + - SERVICES=ses + # loki: # image: grafana/loki:3.7.1 # container_name: loki