- Update SMTP configuration to use Hostinger email settings - Add Jitsi configuration for custom meet domain - Enhance Makefile with comprehensive Docker and development commands - Update email template support email address - Improve environment example file with more detailed configuration Standardizes configuration across development and production environments, enhancing deployment flexibility and maintainability.
404 lines
16 KiB
Go
404 lines
16 KiB
Go
package templates
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"time"
|
|
|
|
"attune-heart-therapy/internal/models"
|
|
)
|
|
|
|
// EmailTemplate represents an email template with subject and body
|
|
type EmailTemplate struct {
|
|
Subject string
|
|
Body string
|
|
}
|
|
|
|
// TemplateData contains data for email template rendering
|
|
type TemplateData struct {
|
|
User *models.User
|
|
Booking *models.Booking
|
|
Amount float64
|
|
PaymentID string
|
|
JoinURL string
|
|
ReminderText string
|
|
CompanyName string
|
|
SupportEmail string
|
|
}
|
|
|
|
// EmailTemplateService handles email template rendering
|
|
type EmailTemplateService struct {
|
|
templates map[models.NotificationType]*template.Template
|
|
baseData TemplateData
|
|
}
|
|
|
|
// NewEmailTemplateService creates a new email template service
|
|
func NewEmailTemplateService() *EmailTemplateService {
|
|
service := &EmailTemplateService{
|
|
templates: make(map[models.NotificationType]*template.Template),
|
|
baseData: TemplateData{
|
|
CompanyName: "Attune Heart Therapy",
|
|
SupportEmail: "hello@attunehearttherapy.com",
|
|
},
|
|
}
|
|
|
|
service.initializeTemplates()
|
|
return service
|
|
}
|
|
|
|
// initializeTemplates initializes all email templates
|
|
func (s *EmailTemplateService) initializeTemplates() {
|
|
// Welcome email template
|
|
welcomeTemplate := `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Welcome to {{.CompanyName}}</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background-color: #4a90e2; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }
|
|
.content { background-color: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; }
|
|
.footer { text-align: center; margin-top: 20px; font-size: 12px; color: #666; }
|
|
.button { display: inline-block; background-color: #4a90e2; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; margin: 10px 0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Welcome to {{.CompanyName}}!</h1>
|
|
</div>
|
|
<div class="content">
|
|
<h2>Hello {{.User.FirstName}}!</h2>
|
|
<p>Thank you for registering with us. We're excited to help you on your wellness journey.</p>
|
|
<p>You can now book video conference sessions with our therapists through our platform.</p>
|
|
<p>Here's what you can do next:</p>
|
|
<ul>
|
|
<li>Browse available appointment slots</li>
|
|
<li>Book your first therapy session</li>
|
|
<li>Complete your profile for a personalized experience</li>
|
|
</ul>
|
|
<p>If you have any questions, please don't hesitate to contact us at {{.SupportEmail}}.</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>Best regards,<br>The {{.CompanyName}} Team</p>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
// Payment success template
|
|
paymentSuccessTemplate := `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Payment Successful</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background-color: #28a745; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }
|
|
.content { background-color: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; }
|
|
.footer { text-align: center; margin-top: 20px; font-size: 12px; color: #666; }
|
|
.success-icon { font-size: 48px; color: #28a745; text-align: center; margin: 20px 0; }
|
|
.booking-details { background-color: white; padding: 20px; border-radius: 4px; margin: 20px 0; }
|
|
.detail-row { display: flex; justify-content: space-between; margin: 10px 0; padding: 8px 0; border-bottom: 1px solid #eee; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Payment Successful!</h1>
|
|
</div>
|
|
<div class="content">
|
|
<div class="success-icon">✓</div>
|
|
<h2>Dear {{.User.FirstName}},</h2>
|
|
<p>Your payment has been successfully processed and your booking is confirmed.</p>
|
|
|
|
<div class="booking-details">
|
|
<h3>Booking Details:</h3>
|
|
<div class="detail-row">
|
|
<strong>Date & Time:</strong>
|
|
<span>{{.Booking.ScheduledAt.Format "January 2, 2006 at 3:04 PM"}}</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<strong>Duration:</strong>
|
|
<span>{{.Booking.Duration}} minutes</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<strong>Amount Paid:</strong>
|
|
<span>${{printf "%.2f" .Booking.Amount}}</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<strong>Payment ID:</strong>
|
|
<span>{{.Booking.PaymentID}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<p>You will receive meeting details closer to your appointment time.</p>
|
|
<p>If you need to reschedule or have any questions, please contact us at {{.SupportEmail}}.</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>Best regards,<br>The {{.CompanyName}} Team</p>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
// Payment failed template
|
|
paymentFailedTemplate := `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Payment Failed</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background-color: #dc3545; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }
|
|
.content { background-color: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; }
|
|
.footer { text-align: center; margin-top: 20px; font-size: 12px; color: #666; }
|
|
.error-icon { font-size: 48px; color: #dc3545; text-align: center; margin: 20px 0; }
|
|
.booking-details { background-color: white; padding: 20px; border-radius: 4px; margin: 20px 0; }
|
|
.button { display: inline-block; background-color: #4a90e2; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; margin: 10px 0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Payment Failed</h1>
|
|
</div>
|
|
<div class="content">
|
|
<div class="error-icon">✗</div>
|
|
<h2>Dear {{.User.FirstName}},</h2>
|
|
<p>Unfortunately, your payment could not be processed and your booking was not confirmed.</p>
|
|
|
|
<div class="booking-details">
|
|
<h3>Attempted Booking Details:</h3>
|
|
<ul>
|
|
<li><strong>Date & Time:</strong> {{.Booking.ScheduledAt.Format "January 2, 2006 at 3:04 PM"}}</li>
|
|
<li><strong>Duration:</strong> {{.Booking.Duration}} minutes</li>
|
|
<li><strong>Amount:</strong> ${{printf "%.2f" .Booking.Amount}}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<p>Please try booking again or contact us at {{.SupportEmail}} if you continue to experience issues.</p>
|
|
<p>Common reasons for payment failure:</p>
|
|
<ul>
|
|
<li>Insufficient funds</li>
|
|
<li>Incorrect card details</li>
|
|
<li>Card expired or blocked</li>
|
|
<li>Bank security restrictions</li>
|
|
</ul>
|
|
</div>
|
|
<div class="footer">
|
|
<p>Best regards,<br>The {{.CompanyName}} Team</p>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
// Meeting info template
|
|
meetingInfoTemplate := `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Meeting Information</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background-color: #17a2b8; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }
|
|
.content { background-color: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; }
|
|
.footer { text-align: center; margin-top: 20px; font-size: 12px; color: #666; }
|
|
.meeting-details { background-color: white; padding: 20px; border-radius: 4px; margin: 20px 0; }
|
|
.join-button { display: inline-block; background-color: #28a745; color: white; padding: 15px 30px; text-decoration: none; border-radius: 4px; margin: 15px 0; font-weight: bold; }
|
|
.checklist { background-color: #e9f7ef; padding: 15px; border-radius: 4px; margin: 15px 0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Your Therapy Session Details</h1>
|
|
</div>
|
|
<div class="content">
|
|
<h2>Dear {{.User.FirstName}},</h2>
|
|
<p>Here are the details for your upcoming therapy session:</p>
|
|
|
|
<div class="meeting-details">
|
|
<h3>Meeting Information:</h3>
|
|
<ul>
|
|
<li><strong>Date & Time:</strong> {{.Booking.ScheduledAt.Format "January 2, 2006 at 3:04 PM"}}</li>
|
|
<li><strong>Duration:</strong> {{.Booking.Duration}} minutes</li>
|
|
<li><strong>Meeting Room:</strong> {{.Booking.JitsiRoomID}}</li>
|
|
</ul>
|
|
<div style="text-align: center;">
|
|
<a href="{{if .JoinURL}}{{.JoinURL}}{{else}}{{.Booking.JitsiRoomURL}}{{end}}" class="join-button">Join Meeting</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="checklist">
|
|
<h3>Important Notes:</h3>
|
|
<ul>
|
|
<li>✓ Please join the meeting 5 minutes before the scheduled time</li>
|
|
<li>✓ Ensure you have a stable internet connection</li>
|
|
<li>✓ Test your camera and microphone beforehand</li>
|
|
<li>✓ Find a quiet, private space for the session</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<p>If you need to reschedule or have any questions, please contact us at {{.SupportEmail}} as soon as possible.</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>Best regards,<br>The {{.CompanyName}} Team</p>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
// Reminder template
|
|
reminderTemplate := `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Session Reminder</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
.header { background-color: #ffc107; color: #333; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }
|
|
.content { background-color: #f9f9f9; padding: 30px; border-radius: 0 0 8px 8px; }
|
|
.footer { text-align: center; margin-top: 20px; font-size: 12px; color: #666; }
|
|
.reminder-icon { font-size: 48px; text-align: center; margin: 20px 0; }
|
|
.session-details { background-color: white; padding: 20px; border-radius: 4px; margin: 20px 0; }
|
|
.join-button { display: inline-block; background-color: #28a745; color: white; padding: 15px 30px; text-decoration: none; border-radius: 4px; margin: 15px 0; font-weight: bold; }
|
|
.checklist { background-color: #fff3cd; padding: 15px; border-radius: 4px; margin: 15px 0; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Session Reminder</h1>
|
|
</div>
|
|
<div class="content">
|
|
<div class="reminder-icon">⏰</div>
|
|
<h2>Dear {{.User.FirstName}},</h2>
|
|
<p>This is a friendly reminder that you have a therapy session scheduled {{.ReminderText}}.</p>
|
|
|
|
<div class="session-details">
|
|
<h3>Session Details:</h3>
|
|
<ul>
|
|
<li><strong>Date & Time:</strong> {{.Booking.ScheduledAt.Format "January 2, 2006 at 3:04 PM"}}</li>
|
|
<li><strong>Duration:</strong> {{.Booking.Duration}} minutes</li>
|
|
</ul>
|
|
<div style="text-align: center;">
|
|
<a href="{{if .JoinURL}}{{.JoinURL}}{{else}}{{.Booking.JitsiRoomURL}}{{end}}" class="join-button">Join Meeting</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="checklist">
|
|
<h3>Preparation Checklist:</h3>
|
|
<ul>
|
|
<li>✓ Test your camera and microphone</li>
|
|
<li>✓ Ensure stable internet connection</li>
|
|
<li>✓ Find a quiet, private space</li>
|
|
<li>✓ Have any notes or questions ready</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<p>We look forward to seeing you soon!</p>
|
|
</div>
|
|
<div class="footer">
|
|
<p>Best regards,<br>The {{.CompanyName}} Team</p>
|
|
</div>
|
|
</body>
|
|
</html>`
|
|
|
|
// Parse and store templates
|
|
s.templates[models.NotificationTypeWelcome] = template.Must(template.New("welcome").Parse(welcomeTemplate))
|
|
s.templates[models.NotificationTypePaymentSuccess] = template.Must(template.New("payment_success").Parse(paymentSuccessTemplate))
|
|
s.templates[models.NotificationTypePaymentFailed] = template.Must(template.New("payment_failed").Parse(paymentFailedTemplate))
|
|
s.templates[models.NotificationTypeMeetingInfo] = template.Must(template.New("meeting_info").Parse(meetingInfoTemplate))
|
|
s.templates[models.NotificationTypeReminder] = template.Must(template.New("reminder").Parse(reminderTemplate))
|
|
}
|
|
|
|
// RenderTemplate renders an email template with the provided data
|
|
func (s *EmailTemplateService) RenderTemplate(notificationType models.NotificationType, data TemplateData) (*EmailTemplate, error) {
|
|
tmpl, exists := s.templates[notificationType]
|
|
if !exists {
|
|
return nil, fmt.Errorf("template not found for notification type: %s", notificationType)
|
|
}
|
|
|
|
// Merge base data with provided data
|
|
mergedData := s.baseData
|
|
if data.User != nil {
|
|
mergedData.User = data.User
|
|
}
|
|
if data.Booking != nil {
|
|
mergedData.Booking = data.Booking
|
|
}
|
|
mergedData.Amount = data.Amount
|
|
mergedData.PaymentID = data.PaymentID
|
|
mergedData.JoinURL = data.JoinURL
|
|
mergedData.ReminderText = data.ReminderText
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, mergedData); err != nil {
|
|
return nil, fmt.Errorf("failed to render template: %w", err)
|
|
}
|
|
|
|
// Generate subject based on notification type
|
|
subject := s.getSubjectForType(notificationType, mergedData)
|
|
|
|
return &EmailTemplate{
|
|
Subject: subject,
|
|
Body: buf.String(),
|
|
}, nil
|
|
}
|
|
|
|
// getSubjectForType returns the appropriate subject line for each notification type
|
|
func (s *EmailTemplateService) getSubjectForType(notificationType models.NotificationType, data TemplateData) string {
|
|
switch notificationType {
|
|
case models.NotificationTypeWelcome:
|
|
return fmt.Sprintf("Welcome to %s!", data.CompanyName)
|
|
case models.NotificationTypePaymentSuccess:
|
|
return "Payment Successful - Booking Confirmed"
|
|
case models.NotificationTypePaymentFailed:
|
|
return "Payment Failed - Booking Not Confirmed"
|
|
case models.NotificationTypeMeetingInfo:
|
|
return "Meeting Information - Your Therapy Session"
|
|
case models.NotificationTypeReminder:
|
|
if data.Booking != nil {
|
|
timeUntil := time.Until(data.Booking.ScheduledAt)
|
|
if timeUntil > 24*time.Hour {
|
|
return "Reminder: Your Therapy Session is Tomorrow"
|
|
} else if timeUntil > time.Hour {
|
|
return "Reminder: Your Therapy Session is Today"
|
|
} else {
|
|
return "Reminder: Your Therapy Session Starts Soon"
|
|
}
|
|
}
|
|
return "Reminder: Your Therapy Session is Coming Up"
|
|
case models.NotificationTypeCancellation:
|
|
return "Booking Cancelled - Confirmation"
|
|
case models.NotificationTypeReschedule:
|
|
return "Booking Rescheduled - New Time Confirmed"
|
|
default:
|
|
return "Notification from " + data.CompanyName
|
|
}
|
|
}
|
|
|
|
// GetReminderText generates appropriate reminder text based on time until meeting
|
|
func GetReminderText(scheduledAt time.Time) string {
|
|
timeUntil := time.Until(scheduledAt)
|
|
|
|
if timeUntil > 24*time.Hour {
|
|
return "tomorrow"
|
|
} else if timeUntil > time.Hour {
|
|
hours := int(timeUntil.Hours())
|
|
if hours == 1 {
|
|
return "in 1 hour"
|
|
}
|
|
return fmt.Sprintf("in %d hours", hours)
|
|
} else {
|
|
minutes := int(timeUntil.Minutes())
|
|
if minutes <= 1 {
|
|
return "now"
|
|
}
|
|
return fmt.Sprintf("in %d minutes", minutes)
|
|
}
|
|
}
|