Add onboarding email campaign system with post-verification welcome email
Implements automated onboarding emails to encourage user engagement: - Post-verification welcome email with 5 tips (sent after email verification) - "No Residence" email (2+ days after registration with no property) - "No Tasks" email (5+ days after first residence with no tasks) Key features: - Each onboarding email type sent only once per user (enforced by unique constraint) - Email open tracking via tracking pixel endpoint - Daily scheduled job at 10:00 AM UTC to process eligible users - Admin panel UI for viewing sent emails, stats, and manual sending - Admin can send any email type to users from the user detail Testing section New files: - internal/models/onboarding_email.go - Database model with tracking - internal/services/onboarding_email_service.go - Business logic and eligibility queries - internal/handlers/tracking_handler.go - Email open tracking endpoint - internal/admin/handlers/onboarding_handler.go - Admin API endpoints - admin/src/app/(dashboard)/onboarding-emails/ - Admin UI pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,29 +18,38 @@ import (
|
||||
|
||||
// Task types
|
||||
const (
|
||||
TypeTaskReminder = "notification:task_reminder"
|
||||
TypeOverdueReminder = "notification:overdue_reminder"
|
||||
TypeDailyDigest = "notification:daily_digest"
|
||||
TypeSendEmail = "email:send"
|
||||
TypeSendPush = "push:send"
|
||||
TypeTaskReminder = "notification:task_reminder"
|
||||
TypeOverdueReminder = "notification:overdue_reminder"
|
||||
TypeDailyDigest = "notification:daily_digest"
|
||||
TypeSendEmail = "email:send"
|
||||
TypeSendPush = "push:send"
|
||||
TypeOnboardingEmails = "email:onboarding"
|
||||
)
|
||||
|
||||
// Handler handles background job processing
|
||||
type Handler struct {
|
||||
db *gorm.DB
|
||||
pushClient *push.Client
|
||||
emailService *services.EmailService
|
||||
notificationService *services.NotificationService
|
||||
config *config.Config
|
||||
db *gorm.DB
|
||||
pushClient *push.Client
|
||||
emailService *services.EmailService
|
||||
notificationService *services.NotificationService
|
||||
onboardingService *services.OnboardingEmailService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewHandler creates a new job handler
|
||||
func NewHandler(db *gorm.DB, pushClient *push.Client, emailService *services.EmailService, notificationService *services.NotificationService, cfg *config.Config) *Handler {
|
||||
// Create onboarding email service
|
||||
var onboardingService *services.OnboardingEmailService
|
||||
if emailService != nil {
|
||||
onboardingService = services.NewOnboardingEmailService(db, emailService, cfg.Server.BaseURL)
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
db: db,
|
||||
pushClient: pushClient,
|
||||
emailService: emailService,
|
||||
notificationService: notificationService,
|
||||
onboardingService: onboardingService,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
@@ -514,3 +523,41 @@ func NewSendPushTask(userID uint, title, message string, data map[string]string)
|
||||
}
|
||||
return asynq.NewTask(TypeSendPush, payload), nil
|
||||
}
|
||||
|
||||
// HandleOnboardingEmails processes onboarding email campaigns
|
||||
// Sends emails to:
|
||||
// 1. Users who registered 2+ days ago but haven't created a residence
|
||||
// 2. Users who created a residence 5+ days ago but haven't created any tasks
|
||||
// Each email type is only sent once per user, ever.
|
||||
func (h *Handler) HandleOnboardingEmails(ctx context.Context, task *asynq.Task) error {
|
||||
log.Info().Msg("Processing onboarding emails...")
|
||||
|
||||
if h.onboardingService == nil {
|
||||
log.Warn().Msg("Onboarding email service not configured, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send no-residence emails (users without any residences after 2 days)
|
||||
noResCount, err := h.onboardingService.CheckAndSendNoResidenceEmails()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to process no-residence onboarding emails")
|
||||
// Continue to next type, don't return error
|
||||
} else {
|
||||
log.Info().Int("count", noResCount).Msg("Sent no-residence onboarding emails")
|
||||
}
|
||||
|
||||
// Send no-tasks emails (users with residence but no tasks after 5 days)
|
||||
noTasksCount, err := h.onboardingService.CheckAndSendNoTasksEmails()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to process no-tasks onboarding emails")
|
||||
} else {
|
||||
log.Info().Int("count", noTasksCount).Msg("Sent no-tasks onboarding emails")
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("no_residence_sent", noResCount).
|
||||
Int("no_tasks_sent", noTasksCount).
|
||||
Msg("Onboarding email processing completed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user