mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-03 20:13:00 +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:
@@ -0,0 +1,80 @@
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"bookra/apps/backend/internal/config"
|
||||
"bookra/apps/backend/internal/db"
|
||||
|
||||
"github.com/stripe/stripe-go/v81"
|
||||
"github.com/stripe/stripe-go/v81/invoiceitem"
|
||||
)
|
||||
|
||||
type BillingService struct {
|
||||
cfg config.Config
|
||||
repo db.Repository
|
||||
smsPriceIDs map[string]string // currency -> price ID
|
||||
}
|
||||
|
||||
func NewBillingService(cfg config.Config, repo db.Repository) *BillingService {
|
||||
return &BillingService{
|
||||
cfg: cfg,
|
||||
repo: repo,
|
||||
smsPriceIDs: cfg.StripeSMSPriceMatrix,
|
||||
}
|
||||
}
|
||||
|
||||
// PriceIDForCurrency returns the Stripe price ID for SMS in the given currency
|
||||
func (b *BillingService) PriceIDForCurrency(currency string) string {
|
||||
c := strings.ToLower(strings.TrimSpace(currency))
|
||||
if c == "" {
|
||||
c = "czk"
|
||||
}
|
||||
if id := b.smsPriceIDs[c]; id != "" {
|
||||
return id
|
||||
}
|
||||
// Fallback to CZK
|
||||
return b.smsPriceIDs["czk"]
|
||||
}
|
||||
|
||||
// CreateMonthlyInvoiceItem creates a Stripe InvoiceItem for the total SMS usage of a month.
|
||||
// This adds a line item to the customer's next invoice — charging all messages together.
|
||||
func (b *BillingService) CreateMonthlyInvoiceItem(ctx context.Context, customerID string, currency string, yearMonth string, messageCount int, totalCents int) (string, error) {
|
||||
if customerID == "" {
|
||||
return "", fmt.Errorf("customer id is empty")
|
||||
}
|
||||
|
||||
priceID := b.PriceIDForCurrency(currency)
|
||||
c := strings.ToLower(strings.TrimSpace(currency))
|
||||
if c == "" {
|
||||
c = "czk"
|
||||
}
|
||||
|
||||
// If a Stripe price is configured, use Price + Quantity for a clean invoice line
|
||||
if priceID != "" {
|
||||
item, err := invoiceitem.New(&stripe.InvoiceItemParams{
|
||||
Customer: stripe.String(customerID),
|
||||
Price: stripe.String(priceID),
|
||||
Quantity: stripe.Int64(int64(messageCount)),
|
||||
Description: stripe.String(fmt.Sprintf("SMS Messages (%s) — %d messages", yearMonth, messageCount)),
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create invoice item: %w", err)
|
||||
}
|
||||
return item.ID, nil
|
||||
}
|
||||
|
||||
// Fallback: explicit amount (for dev/testing when no price configured yet)
|
||||
item, err := invoiceitem.New(&stripe.InvoiceItemParams{
|
||||
Customer: stripe.String(customerID),
|
||||
Amount: stripe.Int64(int64(totalCents)),
|
||||
Currency: stripe.String(c),
|
||||
Description: stripe.String(fmt.Sprintf("SMS Messages (%s) — %d messages", yearMonth, messageCount)),
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create invoice item: %w", err)
|
||||
}
|
||||
return item.ID, nil
|
||||
}
|
||||
Reference in New Issue
Block a user