mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-03 20:13:00 +00:00
7d3e3448cf
Implement a complete SMS messaging system including: - Integration with SMS Manager.cz API for sending messages. - Metered billing via Stripe using monthly aggregate invoice items. - Backend services for managing SMS settings, usage logging, and monthly reporting. - Database migrations for tenant settings, usage logs, and monthly reports. - Frontend dashboard components for SMS configuration, usage tracking, and history. - Support for customer phone numbers in the booking flow. Includes new migrations, backend services, and frontend UI components.
551 lines
18 KiB
Go
551 lines
18 KiB
Go
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"
|
|
EmailTypeUsageWarning EmailType = "usage_warning"
|
|
EmailTypeTrialEnding EmailType = "trial_ending"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
type UsageNotificationData struct {
|
|
Type EmailType
|
|
TenantName string
|
|
TenantSlug string
|
|
BusinessEmail string
|
|
BrandColor string
|
|
AdminEmail string
|
|
Locale string
|
|
PlanCode string
|
|
LocationCount int
|
|
LocationLimit int
|
|
UsagePercent int
|
|
UpgradeURL string
|
|
DashboardURL string
|
|
}
|
|
|
|
func RenderUsageNotificationEmail(data UsageNotificationData) EmailMessage {
|
|
subject := renderUsageSubject(data)
|
|
htmlBody := renderUsageHTML(data)
|
|
textBody := renderUsageText(data)
|
|
|
|
return EmailMessage{
|
|
From: data.BusinessEmail,
|
|
To: data.AdminEmail,
|
|
Subject: subject,
|
|
Text: textBody,
|
|
HTML: htmlBody,
|
|
}
|
|
}
|
|
|
|
func renderUsageSubject(data UsageNotificationData) string {
|
|
if data.Locale == "cs" {
|
|
switch data.Type {
|
|
case EmailTypeUsageWarning:
|
|
return "⚠️ Blížíte se limitu lokací - Upgrade na vyšší plán"
|
|
case EmailTypeTrialEnding:
|
|
return "⏰ Vaše zkušební období končí - Pokračujte s Bookra"
|
|
}
|
|
}
|
|
switch data.Type {
|
|
case EmailTypeUsageWarning:
|
|
return "⚠️ You're nearing your location limit - Upgrade your plan"
|
|
case EmailTypeTrialEnding:
|
|
return "⏰ Your trial period is ending - Continue with Bookra"
|
|
}
|
|
return "Bookra notification"
|
|
}
|
|
|
|
func renderUsageHTML(data UsageNotificationData) string {
|
|
cs := data.Locale == "cs"
|
|
upgradeBtn := `<a href="` + data.UpgradeURL + `" style="display:inline-block;background:#4f46e5;color:#fff;padding:12px 24px;text-decoration:none;border-radius:8px;font-weight:600;margin:8px 0;">` + map[bool]string{true: "Upgradeovat", false: "Upgrade"}[cs] + `</a>`
|
|
dashboardBtn := `<a href="` + data.DashboardURL + `" style="display:inline-block;background:#f3f4f6;color:#374151;padding:12px 24px;text-decoration:none;border-radius:8px;font-weight:600;margin:8px 0;">` + map[bool]string{true: "Otevřít dashboard", false: "Open dashboard"}[cs] + `</a>`
|
|
|
|
if data.Type == EmailTypeUsageWarning {
|
|
var msg string
|
|
if cs {
|
|
msg = fmt.Sprintf("Váš plán %s umožňuje pouze %d lokací. Aktuálně používáte %d (%d%%).", data.PlanCode, data.LocationLimit, data.LocationCount, data.UsagePercent)
|
|
} else {
|
|
msg = fmt.Sprintf("Your %s plan allows only %d locations. You're currently using %d (%d%%).", data.PlanCode, data.LocationLimit, data.LocationCount, data.UsagePercent)
|
|
}
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:600px;margin:0 auto;padding:20px;color:#1f2937;">
|
|
<div style="text-align:center;margin-bottom:24px;"><span style="font-size:32px;">📍</span></div>
|
|
<h2 style="text-align:center;color:#1f2937;margin-bottom:16px;">` + map[bool]string{true: "Blížíte se limitu lokací", false: "You're nearing your location limit"}[cs] + `</h2>
|
|
<p style="color:#6b7280;text-align:center;margin-bottom:24px;">` + msg + `</p>
|
|
<div style="text-align:center;margin-bottom:24px;">` + upgradeBtn + `</div>
|
|
<p style="color:#9ca3af;font-size:14px;text-align:center;">` + map[bool]string{true: "Přidejte další lokace s vyšším plánem", false: "Add more locations with a higher plan"}[cs] + `</p>
|
|
<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0;">
|
|
<p style="color:#9ca3af;font-size:12px;text-align:center;">Powered by <a href="https://bookra.eu" style="color:#4f46e5;">Bookra</a></p>
|
|
</body></html>`
|
|
}
|
|
|
|
// Trial ending email
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:600px;margin:0 auto;padding:20px;color:#1f2937;">
|
|
<div style="text-align:center;margin-bottom:24px;"><span style="font-size:32px;">🎉</span></div>
|
|
<h2 style="text-align:center;color:#1f2937;margin-bottom:16px;">` + map[bool]string{true: "Děkujeme, že používáte Bookra!", false: "Thank you for using Bookra!"}[cs] + `</h2>
|
|
<p style="color:#6b7280;text-align:center;margin-bottom:24px;">` + map[bool]string{true: "Vaše zkušební období brzy končí. Pokud se vám naše služba líbí, můžete pokračovat s vybraným plánem.", false: "Your trial period is ending soon. If you like our service, you can continue with your chosen plan."}[cs] + `</p>
|
|
<div style="text-align:center;margin-bottom:24px;">` + upgradeBtn + `</div>
|
|
<p style="color:#6b7280;text-align:center;margin-bottom:16px;">` + map[bool]string{true: "Pokud se vám služba nelíbí, můžete ji kdykoliv zrušit. Nechceme vám brát peníze, pokud nejste spokojeni.", false: "If you don't like our service, you can cancel anytime. We don't want to take your money if you're not happy."}[cs] + `</p>
|
|
<p style="color:#9ca3af;text-align:center;margin-bottom:24px;">` + map[bool]string{true: "Zrušit můžete zde:", false: "Cancel here:"}[cs] + ` ` + dashboardBtn + `</p>
|
|
<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0;">
|
|
<p style="color:#9ca3af;font-size:12px;text-align:center;">Powered by <a href="https://bookra.eu" style="color:#4f46e5;">Bookra</a></p>
|
|
</body></html>`
|
|
}
|
|
|
|
func renderUsageText(data UsageNotificationData) string {
|
|
cs := data.Locale == "cs"
|
|
if data.Type == EmailTypeUsageWarning {
|
|
if cs {
|
|
return fmt.Sprintf("Blížíte se limitu lokací! Váš plán %s umožňuje pouze %d lokací. Aktuálně používáte %d (%d%%). Upgradeujte na vyšší plán: %s", data.PlanCode, data.LocationLimit, data.LocationCount, data.UsagePercent, data.UpgradeURL)
|
|
}
|
|
return fmt.Sprintf("You're nearing your location limit! Your %s plan allows only %d locations. You're currently using %d (%d%%). Upgrade: %s", data.PlanCode, data.LocationLimit, data.LocationCount, data.UsagePercent, data.UpgradeURL)
|
|
}
|
|
if cs {
|
|
return "Vaše zkušební období končí. Pokud se vám služba líbí, můžete pokračovat. Pokud ne, můžete zrušit. Nechceme vám brát peníze, pokud nejste spokojeni. Dashboard: " + data.DashboardURL
|
|
}
|
|
return "Your trial period is ending. If you like our service, you can continue. If not, you can cancel - we don't want your money if you're not happy. Dashboard: " + data.DashboardURL
|
|
}
|
|
|
|
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 <p> tags and preserve line breaks
|
|
html := "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
|
|
html += "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
|
|
html += fmt.Sprintf("<h2 style='color: %s; margin-bottom: 20px;'>%s</h2>", data.BrandColor, data.TenantName)
|
|
|
|
// Convert text to simple HTML
|
|
paragraphs := splitParagraphs(textBody)
|
|
for _, p := range paragraphs {
|
|
if len(p) > 0 {
|
|
html += fmt.Sprintf("<p style='margin-bottom: 10px;'>%s</p>", p)
|
|
}
|
|
}
|
|
|
|
// Add management button
|
|
if data.ManagementURL != "" {
|
|
html += fmt.Sprintf("<div style='margin-top: 30px;'><a href='%s' style='display: inline-block; background: %s; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;'>Manage Booking</a></div>", data.ManagementURL, data.BrandColor)
|
|
}
|
|
|
|
html += "</div></body></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)
|
|
}
|
|
|
|
// ============================================
|
|
// SMS USAGE EMAIL TEMPLATE
|
|
// ============================================
|
|
|
|
type SMSUsageEmailData struct {
|
|
TenantName string
|
|
TenantSlug string
|
|
BusinessEmail string
|
|
YearMonth string
|
|
MessageCount int
|
|
TotalCostCents int
|
|
Locale string
|
|
}
|
|
|
|
func RenderSMSUsageEmail(data SMSUsageEmailData) EmailMessage {
|
|
cs := data.Locale == "cs"
|
|
year := data.YearMonth[:4]
|
|
month := data.YearMonth[5:]
|
|
monthLabel := month + "/" + year
|
|
if cs {
|
|
monthLabel = month + "." + year
|
|
}
|
|
|
|
totalFormatted := fmt.Sprintf("%.2f Kč", float64(data.TotalCostCents)/100.0)
|
|
|
|
subject := fmt.Sprintf("Bookra SMS Usage - %s (%s)", monthLabel, totalFormatted)
|
|
if cs {
|
|
subject = fmt.Sprintf("Bookra SMS Přehled - %s (%s)", monthLabel, totalFormatted)
|
|
}
|
|
|
|
textBody := fmt.Sprintf(
|
|
"SMS Usage Summary for %s\n\nPeriod: %s\nMessages sent: %d\nTotal cost: %s\n\nThis amount will be added to your next invoice.",
|
|
data.TenantName, monthLabel, data.MessageCount, totalFormatted,
|
|
)
|
|
if cs {
|
|
textBody = fmt.Sprintf(
|
|
"Přehled SMS pro %s\n\nObdobí: %s\nOdeslaných zpráv: %d\nCelková cena: %s\n\nTato částka bude přidána k vaší další faktuře.",
|
|
data.TenantName, monthLabel, data.MessageCount, totalFormatted,
|
|
)
|
|
}
|
|
|
|
htmlBody := fmt.Sprintf(`<!DOCTYPE html>
|
|
<html>
|
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
|
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:600px;margin:0 auto;padding:20px;color:#1f2937;">
|
|
<div style="text-align:center;margin-bottom:24px;"><span style="font-size:32px;">📱</span></div>
|
|
<h2 style="text-align:center;color:#1f2937;margin-bottom:16px;">%s</h2>
|
|
<p style="color:#6b7280;text-align:center;margin-bottom:24px;">%s</p>
|
|
<table style="width:100%%;border-collapse:collapse;margin-bottom:24px;">
|
|
<tr style="border-bottom:1px solid #e5e7eb;">
|
|
<td style="padding:12px 0;color:#6b7280;">%s</td>
|
|
<td style="padding:12px 0;text-align:right;font-weight:600;">%s</td>
|
|
</tr>
|
|
<tr style="border-bottom:1px solid #e5e7eb;">
|
|
<td style="padding:12px 0;color:#6b7280;">%s</td>
|
|
<td style="padding:12px 0;text-align:right;font-weight:600;">%d</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding:12px 0;color:#1f2937;font-weight:600;">%s</td>
|
|
<td style="padding:12px 0;text-align:right;font-weight:700;font-size:18px;">%s</td>
|
|
</tr>
|
|
</table>
|
|
<p style="color:#9ca3af;font-size:14px;text-align:center;">%s</p>
|
|
<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0;">
|
|
<p style="color:#9ca3af;font-size:12px;text-align:center;">Powered by <a href="https://bookra.eu" style="color:#4f46e5;">Bookra</a></p>
|
|
</body></html>`,
|
|
ifCS(cs, "SMS Usage Summary", "Přehled SMS využití"),
|
|
fmt.Sprintf(ifCS(cs, "Your SMS usage for %s", "Vaše SMS využití za %s"), data.TenantName),
|
|
ifCS(cs, "Period", "Období"), monthLabel,
|
|
ifCS(cs, "Messages sent", "Odeslaných zpráv"), data.MessageCount,
|
|
ifCS(cs, "Total cost (excl. VAT)", "Celková cena (bez DPH)"), totalFormatted,
|
|
ifCS(cs, "This amount will be added to your next Stripe invoice.", "Tato částka bude přidána k vaší další faktuře Stripe."),
|
|
)
|
|
|
|
return EmailMessage{
|
|
From: "",
|
|
To: data.BusinessEmail,
|
|
Subject: subject,
|
|
Text: textBody,
|
|
HTML: htmlBody,
|
|
}
|
|
}
|
|
|
|
func ifCS(cs bool, en, csText string) string {
|
|
if cs {
|
|
return csText
|
|
}
|
|
return en
|
|
}
|