mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
refactor(db): replace sqlc with handwritten pgx repos
- Finish Paddle-to-Stripe billing migration; drop paddle/stripe libs - Renumber migrations; add 00012 memberships+manage_token, 00013 location_type_capacity - Add tests: admin, auth, bookings customer service - Remove graphify-out artifacts and stale env files Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
2039669e2c
commit
f2f7439050
93 files changed
+6987
-69060
No files matched your search
@@ -126,7 +126,7 @@ func (s *Service) SendBookingConfirmation(ctx context.Context, data BookingEmail
|
||||
data.BusinessEmail = s.cfg.EmailFrom
|
||||
}
|
||||
data.Type = EmailTypeConfirmation
|
||||
msg := RenderEmailMessage(data)
|
||||
msg := s.renderWithOverride(ctx, data)
|
||||
_, err := s.emailProvider.Send(ctx, msg)
|
||||
return err
|
||||
}
|
||||
@@ -137,7 +137,7 @@ func (s *Service) SendBookingReschedule(ctx context.Context, data BookingEmailDa
|
||||
data.BusinessEmail = s.cfg.EmailFrom
|
||||
}
|
||||
data.Type = EmailTypeReschedule
|
||||
msg := RenderEmailMessage(data)
|
||||
msg := s.renderWithOverride(ctx, data)
|
||||
_, err := s.emailProvider.Send(ctx, msg)
|
||||
return err
|
||||
}
|
||||
@@ -148,11 +148,73 @@ func (s *Service) SendBookingCancellation(ctx context.Context, data BookingEmail
|
||||
data.BusinessEmail = s.cfg.EmailFrom
|
||||
}
|
||||
data.Type = EmailTypeCancellation
|
||||
msg := RenderEmailMessage(data)
|
||||
msg := s.renderWithOverride(ctx, data)
|
||||
_, err := s.emailProvider.Send(ctx, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// renderWithOverride renders the default message and, when the tenant has an
|
||||
// enabled custom template for this email type, swaps in the stored subject and
|
||||
// body with {{variable}} substitution.
|
||||
func (s *Service) renderWithOverride(ctx context.Context, data BookingEmailData) EmailMessage {
|
||||
msg := RenderEmailMessage(data)
|
||||
if s.repo == nil || data.TenantID == "" {
|
||||
return msg
|
||||
}
|
||||
tpl, err := s.repo.GetEmailTemplate(ctx, data.TenantID, string(data.Type))
|
||||
if err != nil || !tpl.IsEnabled {
|
||||
return msg
|
||||
}
|
||||
vars := map[string]string{
|
||||
"customerName": data.CustomerName,
|
||||
"customerEmail": data.CustomerEmail,
|
||||
"service": data.Service,
|
||||
"location": data.Location,
|
||||
"reference": data.Reference,
|
||||
"startsAt": formatLocalizedDateTime(data.StartsAt, data.Timezone, data.Locale),
|
||||
"tenantName": data.TenantName,
|
||||
"manageUrl": data.ManagementURL,
|
||||
}
|
||||
if tpl.Subject != "" {
|
||||
msg.Subject = substituteTemplateVars(tpl.Subject, vars)
|
||||
}
|
||||
if tpl.BodyText != "" {
|
||||
msg.Text = substituteTemplateVars(tpl.BodyText, vars)
|
||||
if tpl.BodyHTML == "" {
|
||||
msg.HTML = wrapTextAsHTML(msg.Text, data.BrandColor, data.TenantName, data.ManagementURL)
|
||||
}
|
||||
}
|
||||
if tpl.BodyHTML != "" {
|
||||
msg.HTML = substituteTemplateVars(tpl.BodyHTML, vars)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func substituteTemplateVars(body string, vars map[string]string) string {
|
||||
for key, value := range vars {
|
||||
body = strings.ReplaceAll(body, "{{"+key+"}}", value)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
func wrapTextAsHTML(text, brandColor, tenantName, manageURL string) string {
|
||||
if brandColor == "" {
|
||||
brandColor = "#c25e3a"
|
||||
}
|
||||
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>", brandColor, tenantName)
|
||||
for _, p := range splitParagraphs(text) {
|
||||
if len(p) > 0 {
|
||||
html += fmt.Sprintf("<p style='margin-bottom: 10px;'>%s</p>", p)
|
||||
}
|
||||
}
|
||||
if manageURL != "" {
|
||||
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>", manageURL, brandColor)
|
||||
}
|
||||
return html + "</div></body></html>"
|
||||
}
|
||||
|
||||
// SendBusinessNotification sends a notification to the business about a new booking
|
||||
func (s *Service) SendBusinessNotification(ctx context.Context, businessEmail string, data BookingEmailData) error {
|
||||
if businessEmail == "" {
|
||||
@@ -311,22 +373,24 @@ func (s *Service) SendUsageWarning(ctx context.Context, tenantID string, locatio
|
||||
return fmt.Errorf("failed to get tenant: %w", err)
|
||||
}
|
||||
|
||||
// Use a placeholder admin email - in production, would get from tenant owner
|
||||
adminEmail := "admin@" + tenant.Slug + ".bookra.eu"
|
||||
ownerEmail, err := s.repo.GetTenantOwnerEmail(ctx, tenantID)
|
||||
if err != nil || ownerEmail == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
emailData := UsageNotificationData{
|
||||
Type: EmailTypeUsageWarning,
|
||||
TenantName: tenant.Name,
|
||||
TenantSlug: tenant.Slug,
|
||||
BusinessEmail: s.cfg.EmailFrom,
|
||||
AdminEmail: adminEmail,
|
||||
AdminEmail: ownerEmail,
|
||||
Locale: tenant.Locale,
|
||||
PlanCode: tenant.PlanCode,
|
||||
LocationCount: locationCount,
|
||||
LocationLimit: locationLimit,
|
||||
UsagePercent: usagePercent,
|
||||
UpgradeURL: "https://bookra.eu/pricing",
|
||||
DashboardURL: "https://bookra.eu/dashboard",
|
||||
UpgradeURL: strings.TrimRight(s.cfg.FrontendURL, "/") + "/dashboard/billing",
|
||||
DashboardURL: strings.TrimRight(s.cfg.FrontendURL, "/") + "/dashboard",
|
||||
}
|
||||
|
||||
msg := RenderUsageNotificationEmail(emailData)
|
||||
@@ -348,19 +412,21 @@ func (s *Service) SendTrialEndingEmail(ctx context.Context, tenantID string, day
|
||||
return fmt.Errorf("failed to get tenant: %w", err)
|
||||
}
|
||||
|
||||
// Use a placeholder admin email - in production, would get from tenant owner
|
||||
adminEmail := "admin@" + tenant.Slug + ".bookra.eu"
|
||||
ownerEmail, err := s.repo.GetTenantOwnerEmail(ctx, tenantID)
|
||||
if err != nil || ownerEmail == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
emailData := UsageNotificationData{
|
||||
Type: EmailTypeTrialEnding,
|
||||
TenantName: tenant.Name,
|
||||
TenantSlug: tenant.Slug,
|
||||
BusinessEmail: s.cfg.EmailFrom,
|
||||
AdminEmail: adminEmail,
|
||||
AdminEmail: ownerEmail,
|
||||
Locale: tenant.Locale,
|
||||
PlanCode: tenant.PlanCode,
|
||||
UpgradeURL: "https://bookra.eu/pricing",
|
||||
DashboardURL: "https://bookra.eu/dashboard",
|
||||
UpgradeURL: strings.TrimRight(s.cfg.FrontendURL, "/") + "/dashboard/billing",
|
||||
DashboardURL: strings.TrimRight(s.cfg.FrontendURL, "/") + "/dashboard",
|
||||
}
|
||||
|
||||
msg := RenderUsageNotificationEmail(emailData)
|
||||
|
||||
Reference in new issue
Block a user