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 }