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

@@ -275,6 +275,116 @@ The Casera Team
return s.SendEmail(to, subject, htmlBody, textBody)
}
// SendPostVerificationEmail sends a welcome email after user verifies their email address
func (s *EmailService) SendPostVerificationEmail(to, firstName string) error {
subject := "You're All Set! Getting Started with Casera"
name := firstName
if name == "" {
name = "there"
}
bodyContent := fmt.Sprintf(`
%s
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 24px 0;">Your email is now verified and your Casera account is ready to go! Here are some tips to help you get the most out of the app.</p>
<!-- Tip 1 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 16px;">
<tr>
<td style="background: #F0FDF4; border-left: 4px solid #22C55E; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #166534; margin: 0 0 8px 0;">&#127968; Start with Your Property</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">Add your home or rental property to begin tracking everything in one place. You can add multiple properties and even share access with family members or co-owners.</p>
</td>
</tr>
</table>
<!-- Tip 2 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 16px;">
<tr>
<td style="background: #EFF6FF; border-left: 4px solid #3B82F6; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #1E40AF; margin: 0 0 8px 0;">&#128197; Set Up Recurring Tasks</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">Create recurring tasks for regular maintenance like HVAC filter changes, gutter cleaning, or lawn care. Casera will remind you when they're due so nothing falls through the cracks.</p>
</td>
</tr>
</table>
<!-- Tip 3 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 16px;">
<tr>
<td style="background: #FEF3C7; border-left: 4px solid #F59E0B; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #92400E; margin: 0 0 8px 0;">&#128221; Track Your Maintenance History</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">When you complete a task, add notes and photos to build a maintenance history. This is invaluable for warranty claims, selling your home, or just remembering when something was last serviced.</p>
</td>
</tr>
</table>
<!-- Tip 4 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 16px;">
<tr>
<td style="background: #F5F3FF; border-left: 4px solid #8B5CF6; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #5B21B6; margin: 0 0 8px 0;">&#128196; Store Important Documents</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">Upload warranties, appliance manuals, insurance policies, and receipts. When you need them, they'll be right at your fingertips instead of buried in a drawer somewhere.</p>
</td>
</tr>
</table>
<!-- Tip 5 -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin-bottom: 24px;">
<tr>
<td style="background: #FDF2F8; border-left: 4px solid #EC4899; border-radius: 0 8px 8px 0; padding: 16px 20px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 15px; font-weight: 700; color: #9D174D; margin: 0 0 8px 0;">&#128119; Save Your Contractors</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 0;">Add your trusted plumber, electrician, and other service providers to your contractor list. You'll never have to dig through old emails or papers to find their contact info again.</p>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #6B7280; margin: 0;">We're excited to have you on board. If you have any questions or feedback, we'd love to hear from you at support@casera.app.</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Happy homeowning!<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
</td>
</tr>
%s`,
emailHeader("You're Verified!"),
name,
emailFooter(time.Now().Year()))
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
textBody := fmt.Sprintf(`
You're All Set! Getting Started with Casera
Hi %s,
Your email is now verified and your Casera account is ready to go! Here are some tips to help you get the most out of the app.
1. START WITH YOUR PROPERTY
Add your home or rental property to begin tracking everything in one place. You can add multiple properties and even share access with family members or co-owners.
2. SET UP RECURRING TASKS
Create recurring tasks for regular maintenance like HVAC filter changes, gutter cleaning, or lawn care. Casera will remind you when they're due so nothing falls through the cracks.
3. TRACK YOUR MAINTENANCE HISTORY
When you complete a task, add notes and photos to build a maintenance history. This is invaluable for warranty claims, selling your home, or just remembering when something was last serviced.
4. STORE IMPORTANT DOCUMENTS
Upload warranties, appliance manuals, insurance policies, and receipts. When you need them, they'll be right at your fingertips instead of buried in a drawer somewhere.
5. SAVE YOUR CONTRACTORS
Add your trusted plumber, electrician, and other service providers to your contractor list. You'll never have to dig through old emails or papers to find their contact info again.
We're excited to have you on board. If you have any questions or feedback, we'd love to hear from you at support@casera.app.
Happy homeowning!
The Casera Team
`, name)
return s.SendEmail(to, subject, htmlBody, textBody)
}
// SendVerificationEmail sends an email verification code
func (s *EmailService) SendVerificationEmail(to, firstName, code string) error {
subject := "Casera - Verify Your Email"
@@ -605,6 +715,162 @@ The Casera Team
return s.SendEmailWithAttachment(to, subject, htmlBody, textBody, attachment)
}
// SendNoResidenceOnboardingEmail sends an onboarding email to users who haven't created a residence
func (s *EmailService) SendNoResidenceOnboardingEmail(to, firstName, baseURL, trackingID string) error {
subject := "Get started with Casera - Add your first property"
name := firstName
if name == "" {
name = "there"
}
trackingPixel := fmt.Sprintf(`<img src="%s/api/track/open/%s" width="1" height="1" style="display:none;" alt="" />`, baseURL, trackingID)
bodyContent := fmt.Sprintf(`
%s
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 24px 0;">We noticed you haven't added your first property to Casera yet. Adding a property is the first step to staying on top of your home maintenance!</p>
<!-- Benefits Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0">
<tr>
<td style="background: #F8FAFC; border-radius: 12px; padding: 24px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; font-weight: 700; color: #1a1a1a; margin: 0 0 16px 0;">Why add a property?</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#127968; <strong>Track all your homes</strong> - Manage single-family homes, apartments, or investment properties</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128197; <strong>Never miss maintenance</strong> - Set up recurring tasks with smart reminders</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128196; <strong>Store important documents</strong> - Keep warranties, manuals, and records in one place</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128119; <strong>Manage contractors</strong> - Keep your trusted pros organized and accessible</p>
</td>
</tr>
</table>
<!-- CTA Button -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin: 32px 0;">
<tr>
<td style="text-align: center;">
<a href="casera://add-property" style="display: inline-block; background: linear-gradient(135deg, #0079FF 0%%, #14B8A6 100%%); color: #FFFFFF; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; font-weight: 700; text-decoration: none; padding: 16px 32px; border-radius: 12px;">Add Your First Property</a>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #6B7280; margin: 24px 0 0 0;">Just open the Casera app and tap the + button to get started. It only takes a minute!</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Best regards,<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
%s
</td>
</tr>
%s`,
emailHeader("Get Started!"),
name,
trackingPixel,
emailFooter(time.Now().Year()))
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
textBody := fmt.Sprintf(`
Get started with Casera - Add your first property
Hi %s,
We noticed you haven't added your first property to Casera yet. Adding a property is the first step to staying on top of your home maintenance!
Why add a property?
- Track all your homes: Manage single-family homes, apartments, or investment properties
- Never miss maintenance: Set up recurring tasks with smart reminders
- Store important documents: Keep warranties, manuals, and records in one place
- Manage contractors: Keep your trusted pros organized and accessible
Just open the Casera app and tap the + button to get started. It only takes a minute!
Best regards,
The Casera Team
`, name)
return s.SendEmail(to, subject, htmlBody, textBody)
}
// SendNoTasksOnboardingEmail sends an onboarding email to users who have a property but no tasks
func (s *EmailService) SendNoTasksOnboardingEmail(to, firstName, baseURL, trackingID string) error {
subject := "Stay on top of home maintenance with Casera"
name := firstName
if name == "" {
name = "there"
}
trackingPixel := fmt.Sprintf(`<img src="%s/api/track/open/%s" width="1" height="1" style="display:none;" alt="" />`, baseURL, trackingID)
bodyContent := fmt.Sprintf(`
%s
<!-- Body -->
<tr>
<td style="background: #FFFFFF; padding: 40px 30px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0 0 20px 0;">Hi %s,</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; line-height: 1.6; color: #4B5563; margin: 0 0 24px 0;">Great job adding your property to Casera! Now it's time to set up your first maintenance task and start tracking your home care.</p>
<!-- Benefits Box -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0">
<tr>
<td style="background: #F8FAFC; border-radius: 12px; padding: 24px;">
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; font-weight: 700; color: #1a1a1a; margin: 0 0 16px 0;">Task ideas to get you started:</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#127777;&#65039; <strong>HVAC Filter Replacement</strong> - Monthly or quarterly</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128167; <strong>Water Heater Flush</strong> - Annually</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#127807; <strong>Lawn Care</strong> - Weekly or bi-weekly</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128374; <strong>Gutter Cleaning</strong> - Seasonal</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #4B5563; margin: 12px 0;">&#128293; <strong>Smoke Detector Test</strong> - Monthly</p>
</td>
</tr>
</table>
<!-- CTA Button -->
<table role="presentation" width="100%%" cellspacing="0" cellpadding="0" style="margin: 32px 0;">
<tr>
<td style="text-align: center;">
<a href="casera://add-task" style="display: inline-block; background: linear-gradient(135deg, #0079FF 0%%, #14B8A6 100%%); color: #FFFFFF; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; font-weight: 700; text-decoration: none; padding: 16px 32px; border-radius: 12px;">Create Your First Task</a>
</td>
</tr>
</table>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.6; color: #6B7280; margin: 24px 0 0 0;">Set up recurring tasks and Casera will remind you when they're due. No more forgotten maintenance!</p>
<p style="font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; color: #6B7280; margin: 30px 0 0 0;">Best regards,<br><strong style="color: #1a1a1a;">The Casera Team</strong></p>
%s
</td>
</tr>
%s`,
emailHeader("Track Your Tasks!"),
name,
trackingPixel,
emailFooter(time.Now().Year()))
htmlBody := fmt.Sprintf(baseEmailTemplate(), subject, bodyContent)
textBody := fmt.Sprintf(`
Stay on top of home maintenance with Casera
Hi %s,
Great job adding your property to Casera! Now it's time to set up your first maintenance task and start tracking your home care.
Task ideas to get you started:
- HVAC Filter Replacement: Monthly or quarterly
- Water Heater Flush: Annually
- Lawn Care: Weekly or bi-weekly
- Gutter Cleaning: Seasonal
- Smoke Detector Test: Monthly
Set up recurring tasks and Casera will remind you when they're due. No more forgotten maintenance!
Best regards,
The Casera Team
`, name)
return s.SendEmail(to, subject, htmlBody, textBody)
}
// EmailTemplate represents an email template
type EmailTemplate struct {
name string