feat: Implement revenue analytics calculation, resource performance tracking, and round-robin load balancing

- Add revenue calculation in GetRevenueAnalytics with active subscription base price aggregation
- Add date range filtering for revenue analytics with StartDate and EndDate support
- Add per-plan revenue, subscription count, and average revenue calculations
- Add error handling with partial result fallback for revenue queries
- Replace mock resource performance with event-based performance
This commit is contained in:
nutcas3
2026-05-02 17:56:34 +03:00
parent 4241c38389
commit 5fddd48994
5 changed files with 111 additions and 26 deletions
@@ -131,7 +131,6 @@ func (r *GormRepository) GetUsageAnalytics(ctx context.Context, filter *UsageAna
// GetRevenueAnalytics retrieves revenue analytics
func (r *GormRepository) GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyticsFilter) (*RevenueAnalytics, error) {
// Simplified implementation - in production, you'd have actual revenue tracking
analytics := &RevenueAnalytics{
TotalRevenue: 0,
RevenueByPlan: make(map[string]float64),
@@ -141,8 +140,41 @@ func (r *GormRepository) GetRevenueAnalytics(ctx context.Context, filter *Revenu
TimelineData: []TimelineDataPoint{},
}
// This would typically join with billing/payment tables
// For now, returning empty analytics
// Calculate revenue by plan from active subscriptions and their base prices
type planRevenue struct {
PlanID string `gorm:"column:rate_plan_id"`
Revenue float64 `gorm:"column:revenue"`
SubCount int64 `gorm:"column:sub_count"`
}
var planRevenues []planRevenue
query := r.db.WithContext(ctx).
Table("rate_plan_subscriptions").
Select("rate_plan_subscriptions.rate_plan_id, SUM(rate_plans.base_price) as revenue, COUNT(rate_plan_subscriptions.id) as sub_count").
Joins("JOIN rate_plans ON rate_plans.id = rate_plan_subscriptions.rate_plan_id").
Where("rate_plan_subscriptions.status = ?", "active").
Group("rate_plan_subscriptions.rate_plan_id")
if filter != nil && !filter.StartDate.IsZero() {
query = query.Where("rate_plan_subscriptions.created_at >= ?", filter.StartDate)
}
if filter != nil && !filter.EndDate.IsZero() {
query = query.Where("rate_plan_subscriptions.created_at <= ?", filter.EndDate)
}
if err := query.Find(&planRevenues).Error; err != nil {
// Non-fatal: return partial results with a log
r.logger.WithError(err).Error("Failed to calculate revenue by plan")
} else {
for _, pr := range planRevenues {
analytics.RevenueByPlan[pr.PlanID] = pr.Revenue
analytics.TotalRevenue += pr.Revenue
if pr.SubCount > 0 {
analytics.AverageRevenue[pr.PlanID] = pr.Revenue / float64(pr.SubCount)
}
}
}
return analytics, nil
}
@@ -10,8 +10,6 @@ import (
// GetUsageStats retrieves usage statistics for a tenant
func (r *GormTenantRepository) GetUsageStats(ctx context.Context, tenantID string) (*tenant.TenantUsageStats, error) {
// This is a complex query that would typically involve joins and aggregations
// For now, return a basic implementation
stats := &tenant.TenantUsageStats{
TenantID: tenantID,
ResourceBreakdown: make(map[string]int64),
@@ -135,7 +133,6 @@ func (r *GormTenantRepository) CreateEvent(ctx context.Context, event *tenant.Te
return err
}
// Create a simple event record (in a real implementation, this would be a proper table)
eventRecord := struct {
ID string `gorm:"primaryKey"`
TenantID string `gorm:"index"`
@@ -161,8 +161,10 @@ func (rpci *RatePlanCurrencyIntegrator) CalculatePlanCostInCurrency(ctx context.
}
func (rpci *RatePlanCurrencyIntegrator) calculateOverageCost(ctx context.Context, plan *rateplan.RatePlan, usage *rateplan.RatePlanUsage) (float64, error) {
// TODO: Use context for timeout/cancellation in overage calculations
_ = ctx // Suppress unused parameter warning until implementation is complete
// Check for context cancellation before calculation
if err := ctx.Err(); err != nil {
return 0.0, fmt.Errorf("overage calculation cancelled: %w", err)
}
overageCost := 0.0
if plan.DataAllowance != nil && usage.DataUsed > plan.DataAllowance.Amount {
@@ -161,24 +161,77 @@ func (s *TenantServiceImpl) parseAPIRequestEvents(events []*tenant.TenantEvent)
}
func (s *TenantServiceImpl) buildResourcePerformance(ctx context.Context, tenantID, timeRange string) map[string]*tenant.ResourcePerformance {
// TODO: Use context, tenantID, and timeRange for actual performance data retrieval
// For now, return mock performance data - should be replaced with real analytics
_ = ctx // Suppress unused parameter warning until implementation is complete
_ = tenantID // Suppress unused parameter warning until implementation is complete
_ = timeRange // Suppress unused parameter warning until implementation is complete
resourcePerformance := make(map[string]*tenant.ResourcePerformance)
resourceTypes := []string{"users", "profiles", "carriers", "api_calls", "storage"}
// Determine event limit based on time range
eventLimit := 100
switch timeRange {
case "1h":
eventLimit = 50
case "24h":
eventLimit = 200
case "7d":
eventLimit = 500
case "30d":
eventLimit = 1000
}
for _, resourceType := range resourceTypes {
performance := &tenant.ResourcePerformance{
ResourceType: resourceType,
ResponseTime: 150.5, // Mock response time in ms
Throughput: 1000.0, // Mock requests per second
ErrorRate: 2.1, // Mock error rate percentage
// Retrieve actual tenant events for performance calculation
events, err := s.repository.ListEvents(ctx, tenantID, eventLimit)
if err != nil {
s.logger.WithError(err).Error("Failed to retrieve tenant events for performance analytics")
return resourcePerformance
}
// Aggregate performance metrics per resource type from events
type perfAccumulator struct {
totalResponseTime float64
totalRequests int
errorCount int
}
accumulators := make(map[string]*perfAccumulator)
for _, event := range events {
resourceType, _ := event.EventData["resource_type"].(string)
if resourceType == "" {
resourceType = string(event.EventType)
}
resourcePerformance[resourceType] = performance
acc, exists := accumulators[resourceType]
if !exists {
acc = &perfAccumulator{}
accumulators[resourceType] = acc
}
acc.totalRequests++
if respTime, ok := event.EventData["response_time"].(float64); ok {
acc.totalResponseTime += respTime
}
if event.EventType == "error" || event.EventType == tenant.TenantEventQuotaExceeded {
acc.errorCount++
}
}
// Convert accumulators to ResourcePerformance
for resourceType, acc := range accumulators {
avgResponseTime := 0.0
if acc.totalRequests > 0 {
avgResponseTime = acc.totalResponseTime / float64(acc.totalRequests)
}
errorRate := 0.0
if acc.totalRequests > 0 {
errorRate = (float64(acc.errorCount) / float64(acc.totalRequests)) * 100
}
resourcePerformance[resourceType] = &tenant.ResourcePerformance{
ResourceType: resourceType,
ResponseTime: avgResponseTime,
Throughput: float64(acc.totalRequests),
ErrorRate: errorRate,
}
}
return resourcePerformance
@@ -8,8 +8,9 @@ import (
// LoadBalancer implements different load balancing strategies for carrier selection
type LoadBalancer struct {
strategy LoadBalancingStrategy
rand *rand.Rand
strategy LoadBalancingStrategy
rand *rand.Rand
rrCounter uint64 // round-robin request counter
}
// LoadBalancingStrategy defines the load balancing algorithm
@@ -55,9 +56,9 @@ func (lb *LoadBalancer) SelectCarrier(carriers []*Carrier, req *ProfileRequest)
// roundRobinSelect implements round-robin carrier selection
func (lb *LoadBalancer) roundRobinSelect(carriers []*Carrier) *Carrier {
// Simple round-robin based on request count
// In a real implementation, you'd maintain state across requests
return carriers[0] // Simplified for demo
idx := lb.rrCounter % uint64(len(carriers))
lb.rrCounter++
return carriers[idx]
}
// weightedRoundRobinSelect selects carrier based on priority and health