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:
@@ -222,6 +222,15 @@ func (h *AuthHandler) VerifyEmail(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Send post-verification welcome email with tips (async)
|
||||
if h.emailService != nil {
|
||||
go func() {
|
||||
if err := h.emailService.SendPostVerificationEmail(user.Email, user.FirstName); err != nil {
|
||||
log.Error().Err(err).Str("email", user.Email).Msg("Failed to send post-verification email")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, responses.VerifyEmailResponse{
|
||||
Message: i18n.LocalizedMessage(c, "message.email_verified"),
|
||||
Verified: true,
|
||||
|
||||
45
internal/handlers/tracking_handler.go
Normal file
45
internal/handlers/tracking_handler.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/services"
|
||||
)
|
||||
|
||||
// TrackingHandler handles email tracking endpoints
|
||||
type TrackingHandler struct {
|
||||
onboardingService *services.OnboardingEmailService
|
||||
}
|
||||
|
||||
// NewTrackingHandler creates a new tracking handler
|
||||
func NewTrackingHandler(onboardingService *services.OnboardingEmailService) *TrackingHandler {
|
||||
return &TrackingHandler{
|
||||
onboardingService: onboardingService,
|
||||
}
|
||||
}
|
||||
|
||||
// 1x1 transparent GIF (43 bytes)
|
||||
var transparentGIF, _ = base64.StdEncoding.DecodeString("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")
|
||||
|
||||
// TrackEmailOpen handles email open tracking via tracking pixel
|
||||
// GET /api/track/open/:trackingID
|
||||
func (h *TrackingHandler) TrackEmailOpen(c *gin.Context) {
|
||||
trackingID := c.Param("trackingID")
|
||||
|
||||
if trackingID != "" && h.onboardingService != nil {
|
||||
// Record the open (async, don't block response)
|
||||
go func() {
|
||||
_ = h.onboardingService.RecordEmailOpened(trackingID)
|
||||
}()
|
||||
}
|
||||
|
||||
// Return 1x1 transparent GIF
|
||||
c.Header("Content-Type", "image/gif")
|
||||
c.Header("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("Expires", "0")
|
||||
c.Data(http.StatusOK, "image/gif", transparentGIF)
|
||||
}
|
||||
Reference in New Issue
Block a user