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:
@@ -1,14 +1,14 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (r *PGRepository) ListBookingsByTenantBetween(ctx context.Context, tenantID string, from time.Time, to time.Time) ([]BookingRecord, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, service_id, class_session_id, staff_id, location_id,
|
||||
customer_name, customer_email, starts_at, ends_at, status, reference
|
||||
customer_name, customer_email, customer_phone, starts_at, ends_at, status, reference
|
||||
FROM bookings
|
||||
WHERE tenant_id = $1 AND starts_at < $3 AND ends_at > $2
|
||||
ORDER BY starts_at ASC
|
||||
@@ -30,6 +30,7 @@ func (r *PGRepository) ListBookingsByTenantBetween(ctx context.Context, tenantID
|
||||
&record.LocationID,
|
||||
&record.CustomerName,
|
||||
&record.CustomerEmail,
|
||||
&record.CustomerPhone,
|
||||
&record.StartsAt,
|
||||
&record.EndsAt,
|
||||
&record.Status,
|
||||
@@ -47,10 +48,10 @@ func (r *PGRepository) CreateBooking(ctx context.Context, params CreateBookingPa
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
INSERT INTO bookings (
|
||||
tenant_id, service_id, class_session_id, staff_id, location_id,
|
||||
booking_mode, customer_name, customer_email, starts_at, ends_at,
|
||||
booking_mode, customer_name, customer_email, customer_phone, starts_at, ends_at,
|
||||
status, reference, notes
|
||||
)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
|
||||
RETURNING id, reference, status
|
||||
`,
|
||||
params.TenantID,
|
||||
@@ -61,6 +62,7 @@ func (r *PGRepository) CreateBooking(ctx context.Context, params CreateBookingPa
|
||||
params.BookingMode,
|
||||
params.CustomerName,
|
||||
params.CustomerEmail,
|
||||
params.CustomerPhone,
|
||||
params.StartsAt,
|
||||
params.EndsAt,
|
||||
params.Status,
|
||||
|
||||
@@ -86,6 +86,18 @@ type Repository interface {
|
||||
// Working Hours
|
||||
ListWorkingHoursByTenant(ctx context.Context, tenantID string) ([]WorkingHoursRecord, error)
|
||||
UpdateWorkingHours(ctx context.Context, tenantID string, dayOfWeek int, params UpdateWorkingHoursParams) error
|
||||
|
||||
// SMS
|
||||
GetTenantSMSSettings(ctx context.Context, tenantID string) (TenantSMSSettingsRecord, error)
|
||||
UpsertTenantSMSSettings(ctx context.Context, params TenantSMSSettingsRecord) error
|
||||
CreateSMSUsageLog(ctx context.Context, params SMSUsageLogRecord) (string, error)
|
||||
GetSMSUsageThisMonth(ctx context.Context, tenantID string) (SMSUsageSummary, error)
|
||||
GetSMSUsageForMonth(ctx context.Context, tenantID string, yearMonth string) (SMSMonthlyReportRecord, error)
|
||||
ListSMSUsageLogs(ctx context.Context, tenantID string, limit int) ([]SMSUsageLogRecord, error)
|
||||
ListSMSMonthlyReports(ctx context.Context, tenantID string, limit int) ([]SMSMonthlyReportRecord, error)
|
||||
UpsertSMSMonthlyReport(ctx context.Context, params SMSMonthlyReportRecord) error
|
||||
MarkSMSReportInvoiceSent(ctx context.Context, tenantID string, yearMonth string) error
|
||||
ListTenantsWithSMSUsage(ctx context.Context, yearMonth string) ([]TenantRecord, error)
|
||||
}
|
||||
|
||||
type TenantRecord struct {
|
||||
@@ -124,12 +136,12 @@ type MagicLinkRecord struct {
|
||||
}
|
||||
|
||||
type PlatformStats struct {
|
||||
TotalTenants int64 `json:"totalTenants"`
|
||||
TotalUsers int64 `json:"totalUsers"`
|
||||
TotalTenants int64 `json:"totalTenants"`
|
||||
TotalUsers int64 `json:"totalUsers"`
|
||||
ActiveSubscriptions int64 `json:"activeSubscriptions"`
|
||||
TrialSubscriptions int64 `json:"trialSubscriptions"`
|
||||
BookingsThisMonth int64 `json:"bookingsThisMonth"`
|
||||
RevenueThisMonth int64 `json:"revenueThisMonthCents"`
|
||||
BookingsThisMonth int64 `json:"bookingsThisMonth"`
|
||||
RevenueThisMonth int64 `json:"revenueThisMonthCents"`
|
||||
}
|
||||
|
||||
type AdminAuditLogParams struct {
|
||||
@@ -229,6 +241,7 @@ type BookingRecord struct {
|
||||
LocationID *string
|
||||
CustomerName string
|
||||
CustomerEmail string
|
||||
CustomerPhone string
|
||||
StartsAt time.Time
|
||||
EndsAt time.Time
|
||||
Status string
|
||||
@@ -244,6 +257,7 @@ type CreateBookingParams struct {
|
||||
BookingMode string
|
||||
CustomerName string
|
||||
CustomerEmail string
|
||||
CustomerPhone string
|
||||
StartsAt time.Time
|
||||
EndsAt time.Time
|
||||
Status string
|
||||
@@ -414,6 +428,44 @@ type UpdateWorkingHoursParams struct {
|
||||
IsOpen *bool
|
||||
}
|
||||
|
||||
// SMS Records
|
||||
type TenantSMSSettingsRecord struct {
|
||||
TenantID string
|
||||
Enabled bool
|
||||
SenderName string
|
||||
MonthlyLimit int
|
||||
StripeSubscriptionItemID string
|
||||
}
|
||||
|
||||
type SMSUsageLogRecord struct {
|
||||
ID string
|
||||
TenantID string
|
||||
RecipientPhone string
|
||||
MessageBody string
|
||||
ExternalMessageID string
|
||||
ExternalRequestID string
|
||||
Status string
|
||||
CostCents int
|
||||
SentAt time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type SMSUsageSummary struct {
|
||||
MessageCount int
|
||||
TotalCostCents int
|
||||
}
|
||||
|
||||
type SMSMonthlyReportRecord struct {
|
||||
ID string
|
||||
TenantID string
|
||||
YearMonth string
|
||||
MessageCount int
|
||||
TotalCostCents int
|
||||
StripeInvoiceID string
|
||||
InvoiceSentAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type PGRepository struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
@@ -428,46 +480,14 @@ func NewRepository(pools *Pools, demoMode bool) Repository {
|
||||
return NewMemoryRepository()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ============================================
|
||||
// LOCATION / ZONE METHODS - PG REPOSITORY (STUBS)
|
||||
// ============================================
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ============================================
|
||||
// BLOCKED DAYS METHODS - PG REPOSITORY (STUBS)
|
||||
// ============================================
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ============================================
|
||||
// CUSTOMER METHODS - PG REPOSITORY (STUBS)
|
||||
// ============================================
|
||||
@@ -572,11 +592,11 @@ func (r *PGRepository) GetBookingByReference(ctx context.Context, reference stri
|
||||
var rec BookingRecord
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, service_id, class_session_id, staff_id, location_id,
|
||||
customer_name, customer_email, starts_at, ends_at, status, reference
|
||||
customer_name, customer_email, customer_phone, starts_at, ends_at, status, reference
|
||||
FROM bookings
|
||||
WHERE reference = $1
|
||||
`, reference).Scan(&rec.ID, &rec.TenantID, &rec.ServiceID, &rec.ClassSessionID, &rec.StaffID, &rec.LocationID,
|
||||
&rec.CustomerName, &rec.CustomerEmail, &rec.StartsAt, &rec.EndsAt, &rec.Status, &rec.Reference)
|
||||
&rec.CustomerName, &rec.CustomerEmail, &rec.CustomerPhone, &rec.StartsAt, &rec.EndsAt, &rec.Status, &rec.Reference)
|
||||
return rec, err
|
||||
}
|
||||
|
||||
@@ -598,8 +618,6 @@ func (r *PGRepository) RescheduleBooking(ctx context.Context, bookingID string,
|
||||
// WORKING HOURS METHODS - PG REPOSITORY (STUBS)
|
||||
// ============================================
|
||||
|
||||
|
||||
|
||||
type MemoryRepository struct {
|
||||
tenant TenantRecord
|
||||
membership TenantMembershipRecord
|
||||
@@ -617,6 +635,9 @@ type MemoryRepository struct {
|
||||
blockedDays []BlockedDayRecord
|
||||
customers []CustomerRecord
|
||||
workingHours []WorkingHoursRecord
|
||||
smsSettings TenantSMSSettingsRecord
|
||||
smsLogs []SMSUsageLogRecord
|
||||
smsReports []SMSMonthlyReportRecord
|
||||
}
|
||||
|
||||
func NewMemoryRepository() *MemoryRepository {
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// ============================================
|
||||
// SMS SETTINGS - PG REPOSITORY
|
||||
// ============================================
|
||||
|
||||
func (r *PGRepository) GetTenantSMSSettings(ctx context.Context, tenantID string) (TenantSMSSettingsRecord, error) {
|
||||
var rec TenantSMSSettingsRecord
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT tenant_id, enabled, COALESCE(sender_name, ''), COALESCE(monthly_limit, 0), COALESCE(stripe_subscription_item_id, '')
|
||||
FROM tenant_sms_settings
|
||||
WHERE tenant_id = $1
|
||||
`, tenantID).Scan(&rec.TenantID, &rec.Enabled, &rec.SenderName, &rec.MonthlyLimit, &rec.StripeSubscriptionItemID)
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return TenantSMSSettingsRecord{TenantID: tenantID}, nil
|
||||
}
|
||||
return TenantSMSSettingsRecord{}, err
|
||||
}
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
func (r *PGRepository) UpsertTenantSMSSettings(ctx context.Context, params TenantSMSSettingsRecord) error {
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
INSERT INTO tenant_sms_settings (tenant_id, enabled, sender_name, monthly_limit, stripe_subscription_item_id, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, now())
|
||||
ON CONFLICT (tenant_id) DO UPDATE SET
|
||||
enabled = EXCLUDED.enabled,
|
||||
sender_name = EXCLUDED.sender_name,
|
||||
monthly_limit = EXCLUDED.monthly_limit,
|
||||
stripe_subscription_item_id = EXCLUDED.stripe_subscription_item_id,
|
||||
updated_at = now()
|
||||
`, params.TenantID, params.Enabled, params.SenderName, params.MonthlyLimit, params.StripeSubscriptionItemID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SMS USAGE LOGS - PG REPOSITORY
|
||||
// ============================================
|
||||
|
||||
func (r *PGRepository) CreateSMSUsageLog(ctx context.Context, params SMSUsageLogRecord) (string, error) {
|
||||
var id string
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
INSERT INTO sms_usage_logs (tenant_id, recipient_phone, message_body, external_message_id, external_request_id, status, cost_cents, sent_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id
|
||||
`, params.TenantID, params.RecipientPhone, params.MessageBody, params.ExternalMessageID, params.ExternalRequestID, params.Status, params.CostCents, params.SentAt).Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func (r *PGRepository) GetSMSUsageThisMonth(ctx context.Context, tenantID string) (SMSUsageSummary, error) {
|
||||
var summary SMSUsageSummary
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(COUNT(*), 0), COALESCE(SUM(cost_cents), 0)
|
||||
FROM sms_usage_logs
|
||||
WHERE tenant_id = $1 AND date_trunc('month', created_at) = date_trunc('month', now())
|
||||
`, tenantID).Scan(&summary.MessageCount, &summary.TotalCostCents)
|
||||
return summary, err
|
||||
}
|
||||
|
||||
func (r *PGRepository) GetSMSUsageForMonth(ctx context.Context, tenantID string, yearMonth string) (SMSMonthlyReportRecord, error) {
|
||||
var rec SMSMonthlyReportRecord
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(COUNT(*), 0), COALESCE(SUM(cost_cents), 0)
|
||||
FROM sms_usage_logs
|
||||
WHERE tenant_id = $1 AND to_char(created_at, 'YYYY-MM') = $2
|
||||
`, tenantID, yearMonth).Scan(&rec.MessageCount, &rec.TotalCostCents)
|
||||
if err != nil {
|
||||
return SMSMonthlyReportRecord{}, err
|
||||
}
|
||||
rec.TenantID = tenantID
|
||||
rec.YearMonth = yearMonth
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
func (r *PGRepository) ListSMSUsageLogs(ctx context.Context, tenantID string, limit int) ([]SMSUsageLogRecord, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, recipient_phone, message_body, external_message_id, external_request_id, status, cost_cents, created_at
|
||||
FROM sms_usage_logs
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`, tenantID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []SMSUsageLogRecord
|
||||
for rows.Next() {
|
||||
var rec SMSUsageLogRecord
|
||||
if err := rows.Scan(&rec.ID, &rec.TenantID, &rec.RecipientPhone, &rec.MessageBody, &rec.ExternalMessageID, &rec.ExternalRequestID, &rec.Status, &rec.CostCents, &rec.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
func (r *PGRepository) ListSMSMonthlyReports(ctx context.Context, tenantID string, limit int) ([]SMSMonthlyReportRecord, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, year_month, message_count, total_cost_cents, stripe_invoice_id, invoice_sent_at, created_at
|
||||
FROM sms_monthly_reports
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY year_month DESC
|
||||
LIMIT $2
|
||||
`, tenantID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []SMSMonthlyReportRecord
|
||||
for rows.Next() {
|
||||
var rec SMSMonthlyReportRecord
|
||||
if err := rows.Scan(&rec.ID, &rec.TenantID, &rec.YearMonth, &rec.MessageCount, &rec.TotalCostCents, &rec.StripeInvoiceID, &rec.InvoiceSentAt, &rec.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
func (r *PGRepository) UpsertSMSMonthlyReport(ctx context.Context, params SMSMonthlyReportRecord) error {
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
INSERT INTO sms_monthly_reports (tenant_id, year_month, message_count, total_cost_cents, stripe_invoice_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (tenant_id, year_month) DO UPDATE SET
|
||||
message_count = EXCLUDED.message_count,
|
||||
total_cost_cents = EXCLUDED.total_cost_cents,
|
||||
stripe_invoice_id = EXCLUDED.stripe_invoice_id,
|
||||
created_at = now()
|
||||
`, params.TenantID, params.YearMonth, params.MessageCount, params.TotalCostCents, params.StripeInvoiceID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *PGRepository) MarkSMSReportInvoiceSent(ctx context.Context, tenantID string, yearMonth string) error {
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
UPDATE sms_monthly_reports
|
||||
SET invoice_sent_at = now()
|
||||
WHERE tenant_id = $1 AND year_month = $2
|
||||
`, tenantID, yearMonth)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *PGRepository) ListTenantsWithSMSUsage(ctx context.Context, yearMonth string) ([]TenantRecord, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT DISTINCT t.id, t.slug, t.name, t.preset, t.locale, t.timezone, t.plan_code, t.subscription_status,
|
||||
t.billing_provider, t.billing_customer_id, t.billing_subscription_id
|
||||
FROM tenants t
|
||||
JOIN tenant_sms_settings s ON s.tenant_id = t.id AND s.enabled = true
|
||||
JOIN sms_usage_logs l ON l.tenant_id = t.id AND to_char(l.created_at, 'YYYY-MM') = $1
|
||||
`, yearMonth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []TenantRecord
|
||||
for rows.Next() {
|
||||
var rec TenantRecord
|
||||
if err := rows.Scan(&rec.ID, &rec.Slug, &rec.Name, &rec.Preset, &rec.Locale, &rec.Timezone, &rec.PlanCode, &rec.SubscriptionStatus,
|
||||
&rec.BillingProvider, &rec.BillingCustomerID, &rec.BillingSubscription); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SMS SETTINGS - MEMORY REPOSITORY
|
||||
// ============================================
|
||||
|
||||
func (r *MemoryRepository) GetTenantSMSSettings(_ context.Context, tenantID string) (TenantSMSSettingsRecord, error) {
|
||||
if tenantID != r.tenant.ID {
|
||||
return TenantSMSSettingsRecord{}, pgx.ErrNoRows
|
||||
}
|
||||
return r.smsSettings, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) UpsertTenantSMSSettings(_ context.Context, params TenantSMSSettingsRecord) error {
|
||||
r.smsSettings = params
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) CreateSMSUsageLog(_ context.Context, params SMSUsageLogRecord) (string, error) {
|
||||
params.ID = fmt.Sprintf("sms-%d", len(r.smsLogs))
|
||||
params.CreatedAt = time.Now().UTC()
|
||||
r.smsLogs = append([]SMSUsageLogRecord{params}, r.smsLogs...)
|
||||
return params.ID, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) GetSMSUsageThisMonth(_ context.Context, tenantID string) (SMSUsageSummary, error) {
|
||||
if tenantID != r.tenant.ID {
|
||||
return SMSUsageSummary{}, nil
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
var count, cost int
|
||||
for _, log := range r.smsLogs {
|
||||
if log.TenantID == tenantID && log.CreatedAt.Year() == now.Year() && log.CreatedAt.Month() == now.Month() {
|
||||
count++
|
||||
cost += log.CostCents
|
||||
}
|
||||
}
|
||||
return SMSUsageSummary{MessageCount: count, TotalCostCents: cost}, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) GetSMSUsageForMonth(_ context.Context, tenantID string, yearMonth string) (SMSMonthlyReportRecord, error) {
|
||||
if tenantID != r.tenant.ID {
|
||||
return SMSMonthlyReportRecord{}, nil
|
||||
}
|
||||
var count, cost int
|
||||
for _, log := range r.smsLogs {
|
||||
if log.TenantID == tenantID && log.CreatedAt.Format("2006-01") == yearMonth {
|
||||
count++
|
||||
cost += log.CostCents
|
||||
}
|
||||
}
|
||||
return SMSMonthlyReportRecord{TenantID: tenantID, YearMonth: yearMonth, MessageCount: count, TotalCostCents: cost}, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) ListSMSUsageLogs(_ context.Context, tenantID string, limit int) ([]SMSUsageLogRecord, error) {
|
||||
if tenantID != r.tenant.ID {
|
||||
return nil, nil
|
||||
}
|
||||
if limit > len(r.smsLogs) {
|
||||
limit = len(r.smsLogs)
|
||||
}
|
||||
return r.smsLogs[:limit], nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) ListSMSMonthlyReports(_ context.Context, tenantID string, limit int) ([]SMSMonthlyReportRecord, error) {
|
||||
if tenantID != r.tenant.ID {
|
||||
return nil, nil
|
||||
}
|
||||
if limit > len(r.smsReports) {
|
||||
limit = len(r.smsReports)
|
||||
}
|
||||
return r.smsReports[:limit], nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) UpsertSMSMonthlyReport(_ context.Context, params SMSMonthlyReportRecord) error {
|
||||
for i, rep := range r.smsReports {
|
||||
if rep.TenantID == params.TenantID && rep.YearMonth == params.YearMonth {
|
||||
r.smsReports[i] = params
|
||||
return nil
|
||||
}
|
||||
}
|
||||
r.smsReports = append([]SMSMonthlyReportRecord{params}, r.smsReports...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) MarkSMSReportInvoiceSent(_ context.Context, tenantID string, yearMonth string) error {
|
||||
now := time.Now().UTC()
|
||||
for i, rep := range r.smsReports {
|
||||
if rep.TenantID == tenantID && rep.YearMonth == yearMonth {
|
||||
r.smsReports[i].InvoiceSentAt = &now
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) ListTenantsWithSMSUsage(_ context.Context, yearMonth string) ([]TenantRecord, error) {
|
||||
for _, log := range r.smsLogs {
|
||||
if log.TenantID == r.tenant.ID && log.CreatedAt.Format("2006-01") == yearMonth && r.smsSettings.Enabled {
|
||||
return []TenantRecord{r.tenant}, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user