Files
Bookra/apps/backend/internal/sms/billing.go
T
Tomas Dvorak 7d3e3448cf
CI / Frontend (push) Successful in 9m50s
CI / Go - apps/auth-service (push) Failing after 4s
CI / Go - apps/backend (push) Successful in 10m18s
CI / Docker publish - auth-service (push) Has been skipped
CI / Docker publish - backend (push) Has been skipped
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.
2026-05-10 11:40:53 +02:00

81 lines
2.4 KiB
Go

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
}