package notifications import ( "fmt" "time" "bookra/apps/backend/internal/db" ) type EmailType string const ( EmailTypeConfirmation EmailType = "confirmation" EmailTypeReminder EmailType = "reminder" EmailTypeReschedule EmailType = "reschedule" EmailTypeCancellation EmailType = "cancellation" EmailTypeBusinessNotify EmailType = "business_notify" ) type BookingEmailData struct { Type EmailType TenantName string TenantSlug string BusinessEmail string BusinessPhone string BusinessAddress string BrandColor string CustomerName string CustomerEmail string Service string Location string Reference string StartsAt time.Time EndsAt time.Time Timezone string Locale string Notes string ManagementURL string AddToCalendarURL string } func RenderEmailMessage(data BookingEmailData) EmailMessage { subject := renderSubject(data) htmlBody := renderHTMLBody(data) textBody := renderTextBody(data) return EmailMessage{ From: data.BusinessEmail, To: data.CustomerEmail, Subject: subject, Text: textBody, HTML: htmlBody, } } func renderSubject(data BookingEmailData) string { localizedTime := formatLocalizedTime(data.StartsAt, data.Timezone, data.Locale) switch data.Type { case EmailTypeConfirmation: if data.Locale == "cs" { return fmt.Sprintf("Potvrzení rezervace %s - %s", data.Reference, data.TenantName) } return fmt.Sprintf("Booking Confirmation %s - %s", data.Reference, data.TenantName) case EmailTypeReminder: if data.Locale == "cs" { return fmt.Sprintf("Připomínka: Máte rezervaci zítra v %s", localizedTime) } return fmt.Sprintf("Reminder: You have a booking tomorrow at %s", localizedTime) case EmailTypeReschedule: if data.Locale == "cs" { return fmt.Sprintf("Vaše rezervace byla přesunuta - %s", data.Reference) } return fmt.Sprintf("Your booking has been rescheduled - %s", data.Reference) case EmailTypeCancellation: if data.Locale == "cs" { return fmt.Sprintf("Vaše rezervace byla zrušena - %s", data.Reference) } return fmt.Sprintf("Your booking has been cancelled - %s", data.Reference) case EmailTypeBusinessNotify: if data.Locale == "cs" { return fmt.Sprintf("Nová rezervace od %s - %s", data.CustomerName, data.Reference) } return fmt.Sprintf("New booking from %s - %s", data.CustomerName, data.Reference) default: return "Booking Update" } } func renderTextBody(data BookingEmailData) string { localizedTime := formatLocalizedDateTime(data.StartsAt, data.Timezone, data.Locale) switch data.Type { case EmailTypeConfirmation: if data.Locale == "cs" { return fmt.Sprintf(`Dobrý den %s, vaše rezervace byla potvrzena. Detaily rezervace: - Služba: %s - Datum a čas: %s - Místo: %s - Reference: %s Pro správu rezervace navštivte: %s Děkujeme, %s %s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName, data.BusinessEmail) } return fmt.Sprintf(`Hello %s, Your booking has been confirmed. Booking Details: - Service: %s - Date & Time: %s - Location: %s - Reference: %s Manage your booking at: %s Thank you, %s %s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName, data.BusinessEmail) case EmailTypeReminder: if data.Locale == "cs" { return fmt.Sprintf(`Dobrý den %s, připomínáme vám zítřejší rezervaci. - Služba: %s - Čas: %s - Místo: %s - Reference: %s Pro správu rezervace: %s %s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName) } return fmt.Sprintf(`Hello %s, This is a reminder for your booking tomorrow. - Service: %s - Time: %s - Location: %s - Reference: %s Manage booking: %s %s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName) case EmailTypeReschedule: if data.Locale == "cs" { return fmt.Sprintf(`Dobrý den %s, vaše rezervace byla přesunuta na nový termín. Nové detaily: - Služba: %s - Datum a čas: %s - Místo: %s - Reference: %s Pro správu rezervace: %s %s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName) } return fmt.Sprintf(`Hello %s, Your booking has been rescheduled. New details: - Service: %s - Date & Time: %s - Location: %s - Reference: %s Manage booking: %s %s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName) case EmailTypeCancellation: if data.Locale == "cs" { return fmt.Sprintf(`Dobrý den %s, vaše rezervace byla zrušena. Zrušená rezervace: - Služba: %s - Datum a čas: %s - Reference: %s Pokud jste rezervaci nezrušili vy, kontaktujte nás: %s %s`, data.CustomerName, data.Service, localizedTime, data.Reference, data.BusinessEmail, data.TenantName) } return fmt.Sprintf(`Hello %s, Your booking has been cancelled. Cancelled booking: - Service: %s - Date & Time: %s - Reference: %s If you didn't cancel this, please contact us: %s %s`, data.CustomerName, data.Service, localizedTime, data.Reference, data.BusinessEmail, data.TenantName) case EmailTypeBusinessNotify: if data.Locale == "cs" { return fmt.Sprintf(`Nová rezervace od %s Detaily: - Služba: %s - Datum a čas: %s - Reference: %s - Email: %s Spravovat v administraci: https://bookra.eu/dashboard`, data.CustomerName, data.Service, localizedTime, data.Reference, data.CustomerEmail) } return fmt.Sprintf(`New booking from %s Details: - Service: %s - Date & Time: %s - Reference: %s - Email: %s Manage in dashboard: https://bookra.eu/dashboard`, data.CustomerName, data.Service, localizedTime, data.Reference, data.CustomerEmail) default: return "Booking update" } } func renderHTMLBody(data BookingEmailData) string { // For now, return simple HTML version. In production, this would use proper HTML templates textBody := renderTextBody(data) // Simple conversion: wrap paragraphs in

tags and preserve line breaks html := "" html += "

" html += fmt.Sprintf("

%s

", data.BrandColor, data.TenantName) // Convert text to simple HTML paragraphs := splitParagraphs(textBody) for _, p := range paragraphs { if len(p) > 0 { html += fmt.Sprintf("

%s

", p) } } // Add management button if data.ManagementURL != "" { html += fmt.Sprintf("
Manage Booking
", data.ManagementURL, data.BrandColor) } html += "
" return html } func formatLocalizedTime(t time.Time, timezone, locale string) string { loc, err := time.LoadLocation(timezone) if err != nil { loc = time.UTC } localTime := t.In(loc) if locale == "cs" { return localTime.Format("15:04") } return localTime.Format("3:04 PM") } func formatLocalizedDateTime(t time.Time, timezone, locale string) string { loc, err := time.LoadLocation(timezone) if err != nil { loc = time.UTC } localTime := t.In(loc) if locale == "cs" { return localTime.Format("02.01.2006 15:04") } return localTime.Format("Jan 02, 2006 3:04 PM") } func splitParagraphs(text string) []string { var paragraphs []string current := "" for _, line := range splitLines(text) { trimmed := trimSpace(line) if trimmed == "" { if current != "" { paragraphs = append(paragraphs, current) current = "" } } else { if current != "" { current += " " } current += trimmed } } if current != "" { paragraphs = append(paragraphs, current) } return paragraphs } func splitLines(s string) []string { var lines []string start := 0 for i := 0; i < len(s); i++ { if s[i] == '\n' { lines = append(lines, s[start:i]) start = i + 1 } } lines = append(lines, s[start:]) return lines } func trimSpace(s string) string { start := 0 end := len(s) for start < end && (s[start] == ' ' || s[start] == '\t' || s[start] == '\r' || s[start] == '\n') { start++ } for end > start && (s[end-1] == ' ' || s[end-1] == '\t' || s[end-1] == '\r' || s[end-1] == '\n') { end-- } return s[start:end] } // RenderReminderEmail renders the legacy reminder email from a job record func RenderReminderEmail(from string, job db.ReminderJobRecord) EmailMessage { data := BookingEmailData{ Type: EmailTypeReminder, TenantName: job.TenantName, TenantSlug: "", // Not available in job record CustomerName: job.CustomerName, CustomerEmail: job.CustomerEmail, Reference: job.Reference, StartsAt: job.StartsAt, Timezone: job.Timezone, Locale: job.Locale, Service: "Service", // Legacy Location: "Location", // Legacy } return RenderEmailMessage(data) }