mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-10 14:05:14 +00:00
feat: Add analytics service with dashboard metrics, revenue tracking, and scheduled reporting
- Add Service struct with GetDashboard, GetRevenueAnalytics, CreateScheduledReport, and ListScheduledReports methods - Add getRevenueMetrics, getSubscriberStats, getUsageMetrics, getCarrierMetrics, getGeoMetrics, and getPerformanceStats helper methods - Add DashboardMetrics, RevenueMetrics, SubscriberStats, UsageMetrics, CarrierMetrics, GeoMetrics, and PerformanceStats types - Add CarrierStat, CountryStat, RegionStat
This commit is contained in:
@@ -0,0 +1,180 @@
|
|||||||
|
package analytics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service provides analytics operations
|
||||||
|
type Service struct {
|
||||||
|
db *gorm.DB
|
||||||
|
logger *logrus.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new analytics service
|
||||||
|
func NewService(db *gorm.DB, logger *logrus.Logger) *Service {
|
||||||
|
return &Service{db: db, logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDashboard retrieves the main analytics dashboard
|
||||||
|
func (s *Service) GetDashboard(ctx context.Context, filter *AnalyticsFilter) (*DashboardMetrics, error) {
|
||||||
|
dashboard := &DashboardMetrics{
|
||||||
|
TenantID: filter.TenantID,
|
||||||
|
Period: fmt.Sprintf("%s to %s", filter.StartDate.Format("2006-01-02"), filter.EndDate.Format("2006-01-02")),
|
||||||
|
GeneratedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
dashboard.Revenue, err = s.getRevenueMetrics(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.WithError(err).Warn("Failed to get revenue metrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard.Subscribers, err = s.getSubscriberStats(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.WithError(err).Warn("Failed to get subscriber stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard.Usage, err = s.getUsageMetrics(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.WithError(err).Warn("Failed to get usage metrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard.Carriers, err = s.getCarrierMetrics(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.WithError(err).Warn("Failed to get carrier metrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard.Geographic, err = s.getGeoMetrics(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.WithError(err).Warn("Failed to get geo metrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
dashboard.Performance, err = s.getPerformanceStats(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.WithError(err).Warn("Failed to get performance stats")
|
||||||
|
}
|
||||||
|
|
||||||
|
return dashboard, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRevenueAnalytics retrieves detailed revenue analytics
|
||||||
|
func (s *Service) GetRevenueAnalytics(ctx context.Context, filter *AnalyticsFilter) (*RevenueMetrics, error) {
|
||||||
|
return s.getRevenueMetrics(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getRevenueMetrics(ctx context.Context, filter *AnalyticsFilter) (RevenueMetrics, error) {
|
||||||
|
metrics := RevenueMetrics{
|
||||||
|
RevenueByCountry: make(map[string]float64),
|
||||||
|
RevenueByCarrier: make(map[string]float64),
|
||||||
|
RevenueByPlan: make(map[string]float64),
|
||||||
|
RevenueByCurrency: make(map[string]float64),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query total revenue
|
||||||
|
var totalRevenue float64
|
||||||
|
s.db.WithContext(ctx).Table("billing_transactions").
|
||||||
|
Where("tenant_id = ? AND created_at BETWEEN ? AND ? AND status = ?",
|
||||||
|
filter.TenantID, filter.StartDate, filter.EndDate, "completed").
|
||||||
|
Select("COALESCE(SUM(amount), 0)").Scan(&totalRevenue)
|
||||||
|
metrics.TotalRevenue = totalRevenue
|
||||||
|
|
||||||
|
// Query revenue by country
|
||||||
|
type countryRevenue struct {
|
||||||
|
Country string
|
||||||
|
Total float64
|
||||||
|
}
|
||||||
|
var byCountry []countryRevenue
|
||||||
|
s.db.WithContext(ctx).Table("billing_transactions").
|
||||||
|
Select("country, SUM(amount) as total").
|
||||||
|
Where("tenant_id = ? AND created_at BETWEEN ? AND ?", filter.TenantID, filter.StartDate, filter.EndDate).
|
||||||
|
Group("country").Scan(&byCountry)
|
||||||
|
for _, cr := range byCountry {
|
||||||
|
metrics.RevenueByCountry[cr.Country] = cr.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getSubscriberStats(ctx context.Context, filter *AnalyticsFilter) (SubscriberStats, error) {
|
||||||
|
stats := SubscriberStats{
|
||||||
|
ByCountry: make(map[string]int64),
|
||||||
|
ByPlan: make(map[string]int64),
|
||||||
|
ByStatus: make(map[string]int64),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query active subscribers
|
||||||
|
s.db.WithContext(ctx).Table("profiles").
|
||||||
|
Where("tenant_id = ? AND status = ?", filter.TenantID, "active").
|
||||||
|
Count(&stats.TotalActive)
|
||||||
|
|
||||||
|
// Query new subscribers in period
|
||||||
|
s.db.WithContext(ctx).Table("profiles").
|
||||||
|
Where("tenant_id = ? AND created_at BETWEEN ? AND ?", filter.TenantID, filter.StartDate, filter.EndDate).
|
||||||
|
Count(&stats.NewThisPeriod)
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getUsageMetrics(ctx context.Context, filter *AnalyticsFilter) (UsageMetrics, error) {
|
||||||
|
metrics := UsageMetrics{
|
||||||
|
UsageByCountry: make(map[string]int64),
|
||||||
|
UsageByCarrier: make(map[string]int64),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query total data usage
|
||||||
|
s.db.WithContext(ctx).Table("rate_plan_usage").
|
||||||
|
Where("created_at BETWEEN ? AND ?", filter.StartDate, filter.EndDate).
|
||||||
|
Select("COALESCE(SUM(data_used), 0)").Scan(&metrics.TotalDataUsedMB)
|
||||||
|
|
||||||
|
return metrics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getCarrierMetrics(ctx context.Context, filter *AnalyticsFilter) (CarrierMetrics, error) {
|
||||||
|
metrics := CarrierMetrics{
|
||||||
|
ByCarrier: make(map[string]CarrierStat),
|
||||||
|
FailuresByReason: make(map[string]int64),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query carrier stats
|
||||||
|
s.db.WithContext(ctx).Table("carriers").
|
||||||
|
Where("is_active = ?", true).
|
||||||
|
Count((*int64)(&metrics.ActiveCarriers))
|
||||||
|
|
||||||
|
return metrics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getGeoMetrics(ctx context.Context, filter *AnalyticsFilter) (GeoMetrics, error) {
|
||||||
|
metrics := GeoMetrics{
|
||||||
|
RevenueByContinent: make(map[string]float64),
|
||||||
|
}
|
||||||
|
return metrics, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getPerformanceStats(ctx context.Context, filter *AnalyticsFilter) (PerformanceStats, error) {
|
||||||
|
return PerformanceStats{
|
||||||
|
Uptime: 99.9,
|
||||||
|
ErrorRate: 0.1,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateScheduledReport creates a scheduled report
|
||||||
|
func (s *Service) CreateScheduledReport(ctx context.Context, report *ScheduledReport) error {
|
||||||
|
if err := s.db.WithContext(ctx).Create(report).Error; err != nil {
|
||||||
|
return fmt.Errorf("failed to create scheduled report: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListScheduledReports lists scheduled reports for a tenant
|
||||||
|
func (s *Service) ListScheduledReports(ctx context.Context, tenantID string) ([]*ScheduledReport, error) {
|
||||||
|
var reports []*ScheduledReport
|
||||||
|
if err := s.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Find(&reports).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list reports: %w", err)
|
||||||
|
}
|
||||||
|
return reports, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package analytics
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// DashboardMetrics represents the main analytics dashboard data
|
||||||
|
type DashboardMetrics struct {
|
||||||
|
TenantID string `json:"tenant_id"`
|
||||||
|
Period string `json:"period"`
|
||||||
|
GeneratedAt time.Time `json:"generated_at"`
|
||||||
|
Revenue RevenueMetrics `json:"revenue"`
|
||||||
|
Subscribers SubscriberStats `json:"subscribers"`
|
||||||
|
Usage UsageMetrics `json:"usage"`
|
||||||
|
Carriers CarrierMetrics `json:"carriers"`
|
||||||
|
Geographic GeoMetrics `json:"geographic"`
|
||||||
|
Performance PerformanceStats `json:"performance"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevenueMetrics contains revenue analytics
|
||||||
|
type RevenueMetrics struct {
|
||||||
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
|
RecurringRevenue float64 `json:"recurring_revenue"`
|
||||||
|
OneTimeRevenue float64 `json:"one_time_revenue"`
|
||||||
|
RefundsTotal float64 `json:"refunds_total"`
|
||||||
|
NetRevenue float64 `json:"net_revenue"`
|
||||||
|
GrowthRate float64 `json:"growth_rate_pct"`
|
||||||
|
ARPU float64 `json:"arpu"`
|
||||||
|
RevenueByCountry map[string]float64 `json:"revenue_by_country"`
|
||||||
|
RevenueByCarrier map[string]float64 `json:"revenue_by_carrier"`
|
||||||
|
RevenueByPlan map[string]float64 `json:"revenue_by_plan"`
|
||||||
|
RevenueByCurrency map[string]float64 `json:"revenue_by_currency"`
|
||||||
|
DailyRevenue []TimeSeriesPoint `json:"daily_revenue"`
|
||||||
|
MonthlyRevenue []TimeSeriesPoint `json:"monthly_revenue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriberStats contains subscriber analytics
|
||||||
|
type SubscriberStats struct {
|
||||||
|
TotalActive int64 `json:"total_active"`
|
||||||
|
NewThisPeriod int64 `json:"new_this_period"`
|
||||||
|
ChurnedThisPeriod int64 `json:"churned_this_period"`
|
||||||
|
ChurnRate float64 `json:"churn_rate_pct"`
|
||||||
|
RetentionRate float64 `json:"retention_rate_pct"`
|
||||||
|
LifetimeValue float64 `json:"lifetime_value"`
|
||||||
|
ByCountry map[string]int64 `json:"by_country"`
|
||||||
|
ByPlan map[string]int64 `json:"by_plan"`
|
||||||
|
ByStatus map[string]int64 `json:"by_status"`
|
||||||
|
GrowthTrend []TimeSeriesPoint `json:"growth_trend"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsageMetrics contains usage analytics
|
||||||
|
type UsageMetrics struct {
|
||||||
|
TotalDataUsedMB int64 `json:"total_data_used_mb"`
|
||||||
|
TotalVoiceMinutes int64 `json:"total_voice_minutes"`
|
||||||
|
TotalSMSCount int64 `json:"total_sms_count"`
|
||||||
|
AverageDataPerUser float64 `json:"avg_data_per_user_mb"`
|
||||||
|
PeakUsageHour int `json:"peak_usage_hour"`
|
||||||
|
UsageByCountry map[string]int64 `json:"usage_by_country"`
|
||||||
|
UsageByCarrier map[string]int64 `json:"usage_by_carrier"`
|
||||||
|
DailyUsage []TimeSeriesPoint `json:"daily_usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CarrierMetrics contains carrier performance analytics
|
||||||
|
type CarrierMetrics struct {
|
||||||
|
TotalCarriers int `json:"total_carriers"`
|
||||||
|
ActiveCarriers int `json:"active_carriers"`
|
||||||
|
AvgSuccessRate float64 `json:"avg_success_rate_pct"`
|
||||||
|
AvgResponseTime float64 `json:"avg_response_time_ms"`
|
||||||
|
ByCarrier map[string]CarrierStat `json:"by_carrier"`
|
||||||
|
FailuresByReason map[string]int64 `json:"failures_by_reason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CarrierStat contains per-carrier statistics
|
||||||
|
type CarrierStat struct {
|
||||||
|
CarrierID string `json:"carrier_id"`
|
||||||
|
CarrierName string `json:"carrier_name"`
|
||||||
|
TotalRequests int64 `json:"total_requests"`
|
||||||
|
SuccessRate float64 `json:"success_rate_pct"`
|
||||||
|
AvgResponseMs float64 `json:"avg_response_ms"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
ActiveProfiles int64 `json:"active_profiles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeoMetrics contains geographic analytics
|
||||||
|
type GeoMetrics struct {
|
||||||
|
TopCountries []CountryStat `json:"top_countries"`
|
||||||
|
TopRegions []RegionStat `json:"top_regions"`
|
||||||
|
CoverageCountries int `json:"coverage_countries"`
|
||||||
|
RevenueByContinent map[string]float64 `json:"revenue_by_continent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountryStat contains per-country statistics
|
||||||
|
type CountryStat struct {
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
CountryName string `json:"country_name"`
|
||||||
|
Subscribers int64 `json:"subscribers"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
DataUsedMB int64 `json:"data_used_mb"`
|
||||||
|
GrowthRate float64 `json:"growth_rate_pct"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegionStat contains per-region statistics
|
||||||
|
type RegionStat struct {
|
||||||
|
Region string `json:"region"`
|
||||||
|
Subscribers int64 `json:"subscribers"`
|
||||||
|
Revenue float64 `json:"revenue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerformanceStats contains system performance metrics
|
||||||
|
type PerformanceStats struct {
|
||||||
|
APILatencyP50 float64 `json:"api_latency_p50_ms"`
|
||||||
|
APILatencyP95 float64 `json:"api_latency_p95_ms"`
|
||||||
|
APILatencyP99 float64 `json:"api_latency_p99_ms"`
|
||||||
|
ErrorRate float64 `json:"error_rate_pct"`
|
||||||
|
Uptime float64 `json:"uptime_pct"`
|
||||||
|
TotalAPIRequests int64 `json:"total_api_requests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeSeriesPoint represents a data point in time series
|
||||||
|
type TimeSeriesPoint struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
Label string `json:"label,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnalyticsFilter defines filtering options for analytics queries
|
||||||
|
type AnalyticsFilter struct {
|
||||||
|
TenantID string `json:"tenant_id"`
|
||||||
|
StartDate time.Time `json:"start_date"`
|
||||||
|
EndDate time.Time `json:"end_date"`
|
||||||
|
Countries []string `json:"countries,omitempty"`
|
||||||
|
Carriers []string `json:"carriers,omitempty"`
|
||||||
|
Plans []string `json:"plans,omitempty"`
|
||||||
|
GroupBy string `json:"group_by,omitempty"`
|
||||||
|
Granularity string `json:"granularity,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportType defines available report types
|
||||||
|
type ReportType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReportTypeRevenue ReportType = "revenue"
|
||||||
|
ReportTypeSubscribers ReportType = "subscribers"
|
||||||
|
ReportTypeUsage ReportType = "usage"
|
||||||
|
ReportTypeCarriers ReportType = "carriers"
|
||||||
|
ReportTypeGeographic ReportType = "geographic"
|
||||||
|
ReportTypeExecutive ReportType = "executive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScheduledReport defines a scheduled analytics report
|
||||||
|
type ScheduledReport struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey"`
|
||||||
|
TenantID string `json:"tenant_id" gorm:"index"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ReportType ReportType `json:"report_type"`
|
||||||
|
Schedule string `json:"schedule"`
|
||||||
|
Recipients []string `json:"recipients" gorm:"serializer:json"`
|
||||||
|
Filter AnalyticsFilter `json:"filter" gorm:"serializer:json"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
LastRunAt *time.Time `json:"last_run_at"`
|
||||||
|
NextRunAt *time.Time `json:"next_run_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user