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:
@@ -132,6 +132,7 @@ type CreateBookingRequest struct {
|
||||
LocationID *string `json:"locationId,omitempty"`
|
||||
CustomerName string `json:"customerName"`
|
||||
CustomerEmail string `json:"customerEmail"`
|
||||
CustomerPhone string `json:"customerPhone,omitempty"`
|
||||
Notes string `json:"notes"`
|
||||
StartsAt string `json:"startsAt"`
|
||||
EndsAt string `json:"endsAt"`
|
||||
@@ -146,31 +147,32 @@ type CreateBookingResponse struct {
|
||||
type PlanEntitlements struct {
|
||||
MaxLocations int `json:"maxLocations"`
|
||||
MaxStaff int `json:"maxStaff"`
|
||||
MaxBookingsMonth int `json:"maxBookingsMonth"` // -1 = unlimited
|
||||
MaxBookingsMonth int `json:"maxBookingsMonth"` // -1 = unlimited
|
||||
EmailReminders bool `json:"emailReminders"`
|
||||
AdvancedReporting bool `json:"advancedReporting"`
|
||||
WidgetEmbedding bool `json:"widgetEmbedding"`
|
||||
UmamiTracking bool `json:"umamiTracking"`
|
||||
APIAccess bool `json:"apiAccess"`
|
||||
DedicatedManager bool `json:"dedicatedManager"`
|
||||
SMSAvailable bool `json:"smsAvailable"`
|
||||
}
|
||||
|
||||
type PlanPricing struct {
|
||||
MonthlyAmountCents int `json:"monthlyAmountCents"`
|
||||
YearlyAmountCents int `json:"yearlyAmountCents"`
|
||||
MonthlyFormatted string `json:"monthlyFormatted"`
|
||||
YearlyFormatted string `json:"yearlyFormatted"`
|
||||
YearlySavings string `json:"yearlySavings"`
|
||||
YearlySavingsPercent int `json:"yearlySavingsPercent"`
|
||||
MonthlyAmountCents int `json:"monthlyAmountCents"`
|
||||
YearlyAmountCents int `json:"yearlyAmountCents"`
|
||||
MonthlyFormatted string `json:"monthlyFormatted"`
|
||||
YearlyFormatted string `json:"yearlyFormatted"`
|
||||
YearlySavings string `json:"yearlySavings"`
|
||||
YearlySavingsPercent int `json:"yearlySavingsPercent"`
|
||||
}
|
||||
|
||||
type PlanDisplayPrice struct {
|
||||
Currency string `json:"currency"`
|
||||
AmountCents int `json:"amountCents"`
|
||||
Formatted string `json:"formatted"`
|
||||
YearlyAmountCents int `json:"yearlyAmountCents,omitempty"`
|
||||
YearlyFormatted string `json:"yearlyFormatted,omitempty"`
|
||||
YearlySavings string `json:"yearlySavings,omitempty"`
|
||||
Currency string `json:"currency"`
|
||||
AmountCents int `json:"amountCents"`
|
||||
Formatted string `json:"formatted"`
|
||||
YearlyAmountCents int `json:"yearlyAmountCents,omitempty"`
|
||||
YearlyFormatted string `json:"yearlyFormatted,omitempty"`
|
||||
YearlySavings string `json:"yearlySavings,omitempty"`
|
||||
YearlySavingsPercent int `json:"yearlySavingsPercent,omitempty"`
|
||||
}
|
||||
|
||||
@@ -205,11 +207,11 @@ type CheckoutSessionRequest struct {
|
||||
|
||||
type CheckoutLaunchResponse struct {
|
||||
// Stripe checkout
|
||||
CheckoutURL string `json:"checkoutUrl,omitempty"`
|
||||
CheckoutURL string `json:"checkoutUrl,omitempty"`
|
||||
// Paddle checkout
|
||||
PriceID string `json:"priceId,omitempty"`
|
||||
CustomerID string `json:"customerId,omitempty"`
|
||||
CustomerEmail string `json:"customerEmail,omitempty"`
|
||||
PriceID string `json:"priceId,omitempty"`
|
||||
CustomerID string `json:"customerId,omitempty"`
|
||||
CustomerEmail string `json:"customerEmail,omitempty"`
|
||||
// Common
|
||||
SuccessRedirectURL string `json:"successRedirectUrl,omitempty"`
|
||||
CancelRedirectURL string `json:"cancelRedirectUrl,omitempty"`
|
||||
@@ -261,19 +263,19 @@ type UpdateLocationRequest struct {
|
||||
// ============================================
|
||||
|
||||
type BlockedDay struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId"`
|
||||
Date time.Time `json:"date"`
|
||||
Reason string `json:"reason"`
|
||||
Type string `json:"type"` // full, partial
|
||||
StaffID *string `json:"staffId,omitempty"`
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId"`
|
||||
Date time.Time `json:"date"`
|
||||
Reason string `json:"reason"`
|
||||
Type string `json:"type"` // full, partial
|
||||
StaffID *string `json:"staffId,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type CreateBlockedDayRequest struct {
|
||||
Date string `json:"date" binding:"required"` // RFC3339
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
Type string `json:"type" binding:"required"` // full, partial
|
||||
Date string `json:"date" binding:"required"` // RFC3339
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
Type string `json:"type" binding:"required"` // full, partial
|
||||
StaffID *string `json:"staffId,omitempty"`
|
||||
}
|
||||
|
||||
@@ -287,32 +289,32 @@ type UpdateBlockedDayRequest struct {
|
||||
// ============================================
|
||||
|
||||
type Customer struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Status string `json:"status"` // active, inactive, vip
|
||||
BookingsCount int `json:"bookingsCount"`
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Status string `json:"status"` // active, inactive, vip
|
||||
BookingsCount int `json:"bookingsCount"`
|
||||
LastBookingAt *time.Time `json:"lastBookingAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type CreateCustomerRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Status string `json:"status,omitempty"` // defaults to active
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Status string `json:"status,omitempty"` // defaults to active
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateCustomerRequest struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================
|
||||
@@ -333,9 +335,9 @@ type CustomerBookingView struct {
|
||||
}
|
||||
|
||||
type RescheduleBookingRequest struct {
|
||||
NewStartsAt string `json:"newStartsAt" binding:"required"` // RFC3339
|
||||
NewEndsAt string `json:"newEndsAt" binding:"required"` // RFC3339
|
||||
Reason string `json:"reason,omitempty"`
|
||||
NewStartsAt string `json:"newStartsAt" binding:"required"` // RFC3339
|
||||
NewEndsAt string `json:"newEndsAt" binding:"required"` // RFC3339
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type CancelBookingRequest struct {
|
||||
@@ -347,35 +349,35 @@ type CancelBookingRequest struct {
|
||||
// ============================================
|
||||
|
||||
type AdminDashboardStats struct {
|
||||
TotalTenants int64 `json:"totalTenants"`
|
||||
TotalUsers int64 `json:"totalUsers"`
|
||||
ActiveSubscriptions int64 `json:"activeSubscriptions"`
|
||||
TrialSubscriptions int64 `json:"trialSubscriptions"`
|
||||
BookingsThisMonth int64 `json:"bookingsThisMonth"`
|
||||
TotalTenants int64 `json:"totalTenants"`
|
||||
TotalUsers int64 `json:"totalUsers"`
|
||||
ActiveSubscriptions int64 `json:"activeSubscriptions"`
|
||||
TrialSubscriptions int64 `json:"trialSubscriptions"`
|
||||
BookingsThisMonth int64 `json:"bookingsThisMonth"`
|
||||
RevenueThisMonthCents int64 `json:"revenueThisMonthCents"`
|
||||
}
|
||||
|
||||
type AdminTenantList struct {
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Tenants []AdminTenant `json:"tenants"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Tenants []AdminTenant `json:"tenants"`
|
||||
}
|
||||
|
||||
type AdminTenant struct {
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
PlanCode string `json:"planCode"`
|
||||
ID string `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
PlanCode string `json:"planCode"`
|
||||
SubscriptionStatus string `json:"subscriptionStatus"`
|
||||
BillingProvider string `json:"billingProvider"`
|
||||
BillingProvider string `json:"billingProvider"`
|
||||
}
|
||||
|
||||
type AdminUserList struct {
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Users []AdminUser `json:"users"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Users []AdminUser `json:"users"`
|
||||
}
|
||||
|
||||
type AdminUser struct {
|
||||
@@ -421,7 +423,7 @@ type UpdateWorkingHoursRequest struct {
|
||||
type EmailTemplate struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId"`
|
||||
Type string `json:"type"` // booking_confirmation, reminder, cancellation, etc.
|
||||
Type string `json:"type"` // booking_confirmation, reminder, cancellation, etc.
|
||||
Subject string `json:"subject"`
|
||||
BodyHTML string `json:"bodyHtml"`
|
||||
BodyText string `json:"bodyText"`
|
||||
@@ -436,14 +438,69 @@ type SendEmailRequest struct {
|
||||
}
|
||||
|
||||
type EmailNotification struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId"`
|
||||
BookingID string `json:"bookingId,omitempty"`
|
||||
Channel string `json:"channel"` // email, sms
|
||||
Type string `json:"type"` // confirmation, reminder, cancellation
|
||||
Recipient string `json:"recipient"`
|
||||
Status string `json:"status"` // pending, sent, failed
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenantId"`
|
||||
BookingID string `json:"bookingId,omitempty"`
|
||||
Channel string `json:"channel"` // email, sms
|
||||
Type string `json:"type"` // confirmation, reminder, cancellation
|
||||
Recipient string `json:"recipient"`
|
||||
Status string `json:"status"` // pending, sent, failed
|
||||
SentAt *time.Time `json:"sentAt,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SMS MODELS
|
||||
// ============================================
|
||||
|
||||
type SMSSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
SenderName string `json:"senderName,omitempty"`
|
||||
MonthlyLimit int `json:"monthlyLimit,omitempty"`
|
||||
MessagesSent int `json:"messagesSent"`
|
||||
TotalCostCents int `json:"totalCostCents"`
|
||||
Available bool `json:"available"`
|
||||
}
|
||||
|
||||
type UpdateSMSSettingsRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
SenderName string `json:"senderName,omitempty"`
|
||||
MonthlyLimit int `json:"monthlyLimit,omitempty"`
|
||||
}
|
||||
|
||||
type SendSMSRequest struct {
|
||||
To string `json:"to" binding:"required"`
|
||||
Body string `json:"body" binding:"required,max=1000"`
|
||||
}
|
||||
|
||||
type SendSMSResponse struct {
|
||||
LogID string `json:"logId"`
|
||||
MessageID string `json:"messageId,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CostCents int `json:"costCents"`
|
||||
}
|
||||
|
||||
type SMSUsageLog struct {
|
||||
ID string `json:"id"`
|
||||
RecipientPhone string `json:"recipientPhone"`
|
||||
MessageBody string `json:"messageBody,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CostCents int `json:"costCents"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type SMSUsageReport struct {
|
||||
YearMonth string `json:"yearMonth"`
|
||||
MessageCount int `json:"messageCount"`
|
||||
TotalCostCents int `json:"totalCostCents"`
|
||||
StripeInvoiceID string `json:"stripeInvoiceId,omitempty"`
|
||||
InvoiceSentAt *time.Time `json:"invoiceSentAt,omitempty"`
|
||||
}
|
||||
|
||||
type SMSInvoiceBatchResponse struct {
|
||||
YearMonth string `json:"yearMonth"`
|
||||
ProcessedCount int `json:"processedCount"`
|
||||
FailedCount int `json:"failedCount"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user