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:
Trey t
2025-12-08 14:36:50 -06:00
parent e152a6308a
commit 9761156597
17 changed files with 1707 additions and 18 deletions

View 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)
}