mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
397 lines
13 KiB
Go
397 lines
13 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/nutcas3/telecom-platform/apps/api-server/internal/config"
|
|
"github.com/nutcas3/telecom-platform/apps/api-server/internal/database"
|
|
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
|
|
)
|
|
|
|
// ChargingService handles charging and billing operations
|
|
type ChargingService struct {
|
|
db *database.Database
|
|
engine *ChargingEngineClient
|
|
}
|
|
|
|
// NewChargingService creates a new charging service
|
|
func NewChargingService(db *database.Database, cfg *config.Config) *ChargingService {
|
|
return &ChargingService{
|
|
db: db,
|
|
engine: NewChargingEngineClient(&cfg.ChargingEngine),
|
|
}
|
|
}
|
|
|
|
// GetSystemStats gets system statistics
|
|
func (cs *ChargingService) GetSystemStats(ctx context.Context) (*models.SystemStats, error) {
|
|
var stats models.SystemStats
|
|
|
|
// Count active sessions
|
|
var activeSessions int64
|
|
if err := cs.db.DB.WithContext(ctx).Model(&models.Session{}).
|
|
Where("status IN ?", []string{"active", "connected", "established"}).
|
|
Count(&activeSessions).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count active sessions: %w", err)
|
|
}
|
|
stats.ActiveSessions = int(activeSessions)
|
|
|
|
// Count total accounts
|
|
var totalAccounts int64
|
|
if err := cs.db.DB.WithContext(ctx).Model(&models.SubscriberAccount{}).
|
|
Count(&totalAccounts).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count total accounts: %w", err)
|
|
}
|
|
stats.TotalAccounts = int(totalAccounts)
|
|
|
|
// Count blocked users
|
|
var blockedUsers int64
|
|
if err := cs.db.DB.WithContext(ctx).Model(&models.Subscriber{}).
|
|
Where("status IN ?", []string{"suspended", "deactivated", "blocked"}).
|
|
Count(&blockedUsers).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count blocked users: %w", err)
|
|
}
|
|
stats.BlockedUsers = int(blockedUsers)
|
|
|
|
// Count low balance alerts
|
|
var lowBalanceAlerts int64
|
|
if err := cs.db.DB.WithContext(ctx).Model(&models.Alert{}).
|
|
Where("type = ? AND resolved = ?", models.AlertTypeLowBalance, false).
|
|
Count(&lowBalanceAlerts).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to count low balance alerts: %w", err)
|
|
}
|
|
stats.LowBalanceAlerts = int(lowBalanceAlerts)
|
|
|
|
// Calculate uptime (simplified - in real implementation would track service start time)
|
|
stats.Uptime = time.Since(time.Now().AddDate(0, 0, -7)).Seconds() // Assume 7 days uptime
|
|
|
|
return &stats, nil
|
|
}
|
|
|
|
// GetHealthStatus gets health status
|
|
func (cs *ChargingService) GetHealthStatus(ctx context.Context) (*models.HealthStatus, error) {
|
|
status := &models.HealthStatus{
|
|
RedisConnected: true, // In real implementation, would check Redis connection
|
|
ActiveSync: true, // In real implementation, would check sync status
|
|
LastSync: time.Now(),
|
|
MemoryUsage: 50000000, // In real implementation, would get actual memory usage
|
|
}
|
|
|
|
// Check database connectivity
|
|
if err := cs.db.DB.WithContext(ctx).Exec("SELECT 1").Error; err != nil {
|
|
status.ActiveSync = false
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// GetUsageStats gets usage statistics for a subscriber
|
|
func (cs *ChargingService) GetUsageStats(ctx context.Context, imsi string, period string) (*UsageStats, error) {
|
|
var dataUsage, voiceUsage, smsUsage, totalCost float64
|
|
|
|
// Calculate date range based on period
|
|
var startTime time.Time
|
|
switch period {
|
|
case "daily":
|
|
startTime = time.Now().AddDate(0, 0, -1)
|
|
case "weekly":
|
|
startTime = time.Now().AddDate(0, 0, -7)
|
|
case "monthly":
|
|
startTime = time.Now().AddDate(0, -1, 0)
|
|
default:
|
|
startTime = time.Now().AddDate(0, -1, 0) // Default to monthly
|
|
}
|
|
|
|
// Get usage statistics from database
|
|
if err := cs.db.DB.WithContext(ctx).Model(&models.UsageEvent{}).
|
|
Where("imsi = ? AND created_at >= ?", imsi, startTime).
|
|
Select("SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as data_usage, SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as voice_usage, SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as sms_usage, SUM(cost) as total_cost",
|
|
models.UsageTypeData, models.UsageTypeVoice, models.UsageTypeSMS).
|
|
Row().Scan(&dataUsage, &voiceUsage, &smsUsage, &totalCost); err != nil {
|
|
return nil, fmt.Errorf("failed to get usage stats: %w", err)
|
|
}
|
|
|
|
// Calculate trend (compare with previous period)
|
|
var prevDataUsage, prevVoiceUsage, prevSmsUsage float64
|
|
prevStartTime := startTime.AddDate(0, 0, -int(time.Since(startTime).Hours()/24))
|
|
|
|
if err := cs.db.DB.WithContext(ctx).Model(&models.UsageEvent{}).
|
|
Where("imsi = ? AND created_at >= ? AND created_at < ?", imsi, prevStartTime, startTime).
|
|
Select("SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as data_usage, SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as voice_usage, SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as sms_usage",
|
|
models.UsageTypeData, models.UsageTypeVoice, models.UsageTypeSMS).
|
|
Row().Scan(&prevDataUsage, &prevVoiceUsage, &prevSmsUsage); err != nil {
|
|
// If no previous data, set trend to zero
|
|
prevDataUsage, prevVoiceUsage, prevSmsUsage = 0, 0, 0
|
|
}
|
|
|
|
// Calculate trend percentage
|
|
totalCurrentUsage := dataUsage + voiceUsage + smsUsage
|
|
totalPrevUsage := prevDataUsage + prevVoiceUsage + prevSmsUsage
|
|
var trendPercentage float64
|
|
if totalPrevUsage > 0 {
|
|
trendPercentage = ((totalCurrentUsage - totalPrevUsage) / totalPrevUsage) * 100
|
|
}
|
|
|
|
direction := "STABLE"
|
|
if trendPercentage > 5 {
|
|
direction = "UP"
|
|
} else if trendPercentage < -5 {
|
|
direction = "DOWN"
|
|
}
|
|
|
|
stats := &UsageStats{
|
|
DataUsage: dataUsage,
|
|
VoiceUsage: voiceUsage,
|
|
SmsUsage: smsUsage,
|
|
Cost: totalCost,
|
|
Period: period,
|
|
Trend: &UsageTrend{
|
|
Direction: direction,
|
|
Percentage: trendPercentage,
|
|
PeriodOverPeriod: trendPercentage,
|
|
},
|
|
}
|
|
return stats, nil
|
|
}
|
|
|
|
// GetRealTimeUsage gets real-time usage for a subscriber
|
|
func (cs *ChargingService) GetRealTimeUsage(ctx context.Context, imsi string) (*RealTimeUsage, error) {
|
|
// Get current active session
|
|
var session models.Session
|
|
var currentSession *CurrentSession
|
|
|
|
if err := cs.db.DB.WithContext(ctx).
|
|
Where("imsi = ? AND status IN ?", imsi, []string{"active", "connected", "established"}).
|
|
First(&session).Error; err == nil {
|
|
// Get usage for current session
|
|
var dataUsed, voiceUsed, smsUsed, sessionCost float64
|
|
if err := cs.db.DB.WithContext(ctx).Model(&models.UsageEvent{}).
|
|
Where("session_id = ?", session.SessionID).
|
|
Select("SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as data_usage, SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as voice_usage, SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as sms_usage, SUM(cost) as total_cost",
|
|
models.UsageTypeData, models.UsageTypeVoice, models.UsageTypeSMS).
|
|
Row().Scan(&dataUsed, &voiceUsed, &smsUsed, &sessionCost); err != nil {
|
|
dataUsed, voiceUsed, smsUsed, sessionCost = 0, 0, 0, 0
|
|
}
|
|
|
|
currentSession = &CurrentSession{
|
|
SessionID: session.SessionID,
|
|
StartTime: session.CreatedAt,
|
|
DataUsed: dataUsed,
|
|
VoiceUsed: voiceUsed,
|
|
SmsUsed: smsUsed,
|
|
Cost: sessionCost,
|
|
}
|
|
}
|
|
|
|
// Get today's usage
|
|
today := time.Now().Truncate(24 * time.Hour)
|
|
var dataUsed, voiceUsed, smsUsed, todayCost float64
|
|
|
|
if err := cs.db.DB.WithContext(ctx).Model(&models.UsageEvent{}).
|
|
Where("imsi = ? AND created_at >= ?", imsi, today).
|
|
Select("SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as data_usage, SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as voice_usage, SUM(CASE WHEN usage_type = ? THEN volume ELSE 0 END) as sms_usage, SUM(cost) as total_cost",
|
|
models.UsageTypeData, models.UsageTypeVoice, models.UsageTypeSMS).
|
|
Row().Scan(&dataUsed, &voiceUsed, &smsUsed, &todayCost); err != nil {
|
|
dataUsed, voiceUsed, smsUsed, todayCost = 0, 0, 0, 0
|
|
}
|
|
|
|
usage := &RealTimeUsage{
|
|
CurrentSession: currentSession,
|
|
TodayUsage: &TodayUsage{
|
|
DataUsed: dataUsed,
|
|
VoiceUsed: voiceUsed,
|
|
SmsUsed: smsUsed,
|
|
Cost: todayCost,
|
|
},
|
|
}
|
|
return usage, nil
|
|
}
|
|
|
|
// ListUsageEvents lists usage events
|
|
func (cs *ChargingService) ListUsageEvents(ctx context.Context, limit int, offset int, where string) ([]*models.UsageEvent, int64, error) {
|
|
query := cs.db.DB.WithContext(ctx).Model(&models.UsageEvent{})
|
|
|
|
// Apply where clause if provided
|
|
if where != "" {
|
|
query = query.Where(where)
|
|
}
|
|
|
|
// Get total count
|
|
var total int64
|
|
if err := query.Count(&total).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("failed to count usage events: %w", err)
|
|
}
|
|
|
|
// Get usage events with pagination
|
|
var events []*models.UsageEvent
|
|
if err := query.Preload("Subscriber").Limit(limit).Offset(offset).Order("timestamp DESC").Find(&events).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("failed to list usage events: %w", err)
|
|
}
|
|
|
|
return events, total, nil
|
|
}
|
|
|
|
// SearchUsageEvents searches usage events
|
|
func (cs *ChargingService) SearchUsageEvents(ctx context.Context, query string, limit int) ([]*models.UsageEvent, error) {
|
|
var events []*models.UsageEvent
|
|
|
|
// Search usage events by IMSI, session ID, or usage type
|
|
searchPattern := "%" + query + "%"
|
|
if err := cs.db.DB.WithContext(ctx).
|
|
Where("imsi ILIKE ? OR session_id ILIKE ? OR usage_type ILIKE ?",
|
|
searchPattern, searchPattern, searchPattern).
|
|
Preload("Subscriber").
|
|
Limit(limit).
|
|
Order("timestamp DESC").
|
|
Find(&events).Error; err != nil {
|
|
return nil, fmt.Errorf("failed to search usage events: %w", err)
|
|
}
|
|
|
|
return events, nil
|
|
}
|
|
|
|
// SubscribeToUsageUpdates subscribes to usage updates
|
|
func (cs *ChargingService) SubscribeToUsageUpdates(ctx context.Context, imsi string) (<-chan *models.UsageEvent, error) {
|
|
ch := make(chan *models.UsageEvent, 20) // Buffered channel for high-frequency updates
|
|
|
|
go func() {
|
|
defer close(ch)
|
|
|
|
// Poll for new usage events every 1 second (high frequency for real-time usage)
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
var lastEventID uint = 0
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
// Query for new usage events since last check
|
|
var events []models.UsageEvent
|
|
if err := cs.db.DB.WithContext(ctx).
|
|
Where("imsi = ? AND id > ?", imsi, lastEventID).
|
|
Order("id ASC").
|
|
Limit(20).
|
|
Find(&events).Error; err != nil {
|
|
// Log error but continue polling
|
|
continue
|
|
}
|
|
|
|
// Send new events to channel
|
|
for _, event := range events {
|
|
select {
|
|
case ch <- &event:
|
|
lastEventID = event.ID
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return ch, nil
|
|
}
|
|
|
|
// SubscribeToSystemStatsUpdates subscribes to system stats updates
|
|
func (cs *ChargingService) SubscribeToSystemStatsUpdates(ctx context.Context) (<-chan *models.SystemStats, error) {
|
|
ch := make(chan *models.SystemStats, 5) // Buffered channel to prevent blocking
|
|
|
|
go func() {
|
|
defer close(ch)
|
|
|
|
// Poll for system stats updates every 5 seconds (lower frequency for system stats)
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
var lastStats models.SystemStats
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
// Get current system stats
|
|
stats, err := cs.GetSystemStats(ctx)
|
|
if err != nil {
|
|
// Log error but continue polling
|
|
continue
|
|
}
|
|
|
|
// Only send if stats have changed significantly
|
|
if lastStats.ActiveSessions != stats.ActiveSessions ||
|
|
lastStats.TotalAccounts != stats.TotalAccounts ||
|
|
lastStats.BlockedUsers != stats.BlockedUsers ||
|
|
lastStats.LowBalanceAlerts != stats.LowBalanceAlerts {
|
|
|
|
select {
|
|
case ch <- stats:
|
|
lastStats = *stats
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
return ch, nil
|
|
}
|
|
|
|
// TriggerMaintenance triggers system maintenance
|
|
func (cs *ChargingService) TriggerMaintenance(ctx context.Context) (bool, error) {
|
|
// In a real implementation, this would trigger maintenance tasks like:
|
|
// - Database cleanup
|
|
// - Cache refresh
|
|
// - Log rotation
|
|
// - Health checks
|
|
|
|
// For now, simulate maintenance by updating a maintenance timestamp
|
|
if err := cs.db.DB.WithContext(ctx).Exec("UPDATE system_config SET last_maintenance = ? WHERE id = 1", time.Now()).Error; err != nil {
|
|
// If table doesn't exist, just return success
|
|
return true, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// Helper types for charging service
|
|
type UsageStats struct {
|
|
DataUsage float64 `json:"data_usage"`
|
|
VoiceUsage float64 `json:"voice_usage"`
|
|
SmsUsage float64 `json:"sms_usage"`
|
|
Cost float64 `json:"cost"`
|
|
Period string `json:"period"`
|
|
Trend *UsageTrend `json:"trend"`
|
|
}
|
|
|
|
type UsageTrend struct {
|
|
Direction string `json:"direction"`
|
|
Percentage float64 `json:"percentage"`
|
|
PeriodOverPeriod float64 `json:"period_over_period"`
|
|
}
|
|
|
|
type RealTimeUsage struct {
|
|
CurrentSession *CurrentSession `json:"current_session"`
|
|
TodayUsage *TodayUsage `json:"today_usage"`
|
|
}
|
|
|
|
type CurrentSession struct {
|
|
SessionID string `json:"session_id"`
|
|
StartTime time.Time `json:"start_time"`
|
|
DataUsed float64 `json:"data_used"`
|
|
VoiceUsed float64 `json:"voice_used"`
|
|
SmsUsed float64 `json:"sms_used"`
|
|
Cost float64 `json:"cost"`
|
|
}
|
|
|
|
type TodayUsage struct {
|
|
DataUsed float64 `json:"data_used"`
|
|
VoiceUsed float64 `json:"voice_used"`
|
|
SmsUsed float64 `json:"sms_used"`
|
|
Cost float64 `json:"cost"`
|
|
}
|