mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-04 04:22:59 +00:00
feat(sms): implement SMS messaging and metered billing
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.
This commit is contained in:
@@ -458,3 +458,93 @@ func RenderReminderEmail(from string, job db.ReminderJobRecord) EmailMessage {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -334,6 +334,14 @@ func (s *Service) SendUsageWarning(ctx context.Context, tenantID string, locatio
|
||||
return err
|
||||
}
|
||||
|
||||
// SendRawEmail sends a pre-built email message
|
||||
func (s *Service) SendRawEmail(ctx context.Context, msg EmailMessage) (DeliveryReceipt, error) {
|
||||
if msg.From == "" {
|
||||
msg.From = s.cfg.EmailFrom
|
||||
}
|
||||
return s.emailProvider.Send(ctx, msg)
|
||||
}
|
||||
|
||||
func (s *Service) SendTrialEndingEmail(ctx context.Context, tenantID string, daysRemaining int) error {
|
||||
tenant, err := s.repo.GetTenantByID(ctx, tenantID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user