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,34 @@
package models
import (
"time"
)
// OnboardingEmailType represents the type of onboarding email
type OnboardingEmailType string
const (
// OnboardingEmailNoResidence is sent when a user has not created a residence after 2 days
OnboardingEmailNoResidence OnboardingEmailType = "no_residence"
// OnboardingEmailNoTasks is sent when a user has created a residence but no tasks after 5 days
OnboardingEmailNoTasks OnboardingEmailType = "no_tasks"
)
// OnboardingEmail tracks sent onboarding emails per user
// Each email type can only be sent once per user (enforced by unique constraint on user_id + email_type)
type OnboardingEmail struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
EmailType OnboardingEmailType `gorm:"type:varchar(50);not null;index" json:"email_type"`
SentAt time.Time `gorm:"not null" json:"sent_at"`
OpenedAt *time.Time `json:"opened_at"`
TrackingID string `gorm:"type:varchar(64);uniqueIndex" json:"tracking_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName returns the table name for OnboardingEmail
func (OnboardingEmail) TableName() string {
return "onboarding_emails"
}