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: "support@attuneheart.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="{{.Booking.JitsiRoomURL}}" 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="{{.Booking.JitsiRoomURL}}" 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)
|
||
|
|
}
|
||
|
|
}
|