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>
46 lines
1.3 KiB
Go
46 lines
1.3 KiB
Go
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)
|
|
}
|