diff --git a/apps/carrier-connector/internal/rateplan/repository_rateplan.go b/apps/carrier-connector/internal/rateplan/repository_rateplan.go new file mode 100644 index 0000000..bc3e1d8 --- /dev/null +++ b/apps/carrier-connector/internal/rateplan/repository_rateplan.go @@ -0,0 +1,180 @@ +package rateplan + +import ( + "context" + "fmt" + "time" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// GormRepository implements the Repository interface using GORM +type GormRepository struct { + db *gorm.DB + logger *logrus.Logger +} + +// NewGormRepository creates a new GORM repository +func NewGormRepository(db *gorm.DB, logger *logrus.Logger) *GormRepository { + return &GormRepository{ + db: db, + logger: logger, + } +} +// CreateUsage creates a new usage record +func (r *GormRepository) CreateUsage(ctx context.Context, usage *RatePlanUsage) error { + usage.LastUpdated = time.Now() + + if err := r.db.WithContext(ctx).Create(usage).Error; err != nil { + r.logger.WithError(err).Error("Failed to create usage record") + return fmt.Errorf("failed to create usage record: %w", err) + } + + return nil +} + +// GetUsage retrieves a usage record by ID +func (r *GormRepository) GetUsage(ctx context.Context, id string) (*RatePlanUsage, error) { + var usage RatePlanUsage + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&usage).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("usage record not found: %s", id) + } + r.logger.WithError(err).Error("Failed to get usage record") + return nil, fmt.Errorf("failed to get usage record: %w", err) + } + + return &usage, nil +} + +// UpdateUsage updates an existing usage record +func (r *GormRepository) UpdateUsage(ctx context.Context, usage *RatePlanUsage) error { + usage.LastUpdated = time.Now() + + result := r.db.WithContext(ctx).Where("id = ?", usage.ID).Updates(usage) + if result.Error != nil { + r.logger.WithError(result.Error).Error("Failed to update usage record") + return fmt.Errorf("failed to update usage record: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("usage record not found: %s", usage.ID) + } + + return nil +} + +// GetCurrentUsage retrieves the current usage for a profile +func (r *GormRepository) GetCurrentUsage(ctx context.Context, profileID string) (*RatePlanUsage, error) { + var usage RatePlanUsage + err := r.db.WithContext(ctx). + Where("profile_id = ?", profileID). + Order("cycle_start DESC"). + First(&usage).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil // No usage record found + } + r.logger.WithError(err).Error("Failed to get current usage") + return nil, fmt.Errorf("failed to get current usage: %w", err) + } + + return &usage, nil +} + +// ListUsageHistory retrieves usage history for a profile +func (r *GormRepository) ListUsageHistory(ctx context.Context, profileID string, limit int) ([]*RatePlanUsage, error) { + query := r.db.WithContext(ctx).Where("profile_id = ?", profileID).Order("cycle_start DESC") + + if limit > 0 { + query = query.Limit(limit) + } + + var usageHistory []*RatePlanUsage + if err := query.Find(&usageHistory).Error; err != nil { + r.logger.WithError(err).Error("Failed to list usage history") + return nil, fmt.Errorf("failed to list usage history: %w", err) + } + + return usageHistory, nil +} + +// GetUsageAnalytics retrieves usage analytics +func (r *GormRepository) GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsFilter) (*UsageAnalytics, error) { + analytics := &UsageAnalytics{ + TotalDataUsed: 0, + TotalVoiceUsed: 0, + TotalSMSUsed: 0, + ActiveUsers: 0, + AverageUsage: make(map[string]float64), + UsageByPlan: make(map[string]int64), + UsageByRegion: make(map[string]int64), + TimelineData: []TimelineDataPoint{}, + } + + // Get active users count + var activeUsersCount int64 + if err := r.db.WithContext(ctx). + Model(&RatePlanSubscription{}). + Where("status = ?", SubscriptionStatusActive). + Count(&activeUsersCount).Error; err != nil { + r.logger.WithError(err).Error("Failed to get active users count") + return nil, fmt.Errorf("failed to get active users count: %w", err) + } + + // Get total usage + var totalDataUsed, totalVoiceUsed, totalSMSUsed int64 + if err := r.db.WithContext(ctx). + Model(&RatePlanUsage{}). + Select("COALESCE(SUM(data_used), 0), COALESCE(SUM(voice_used), 0), COALESCE(SUM(sms_used), 0)"). + Where("cycle_start >= ? AND cycle_end <= ?", filter.StartDate, filter.EndDate). + Row().Scan(&totalDataUsed, &totalVoiceUsed, &totalSMSUsed); err != nil { + r.logger.WithError(err).Error("Failed to get total usage") + return nil, fmt.Errorf("failed to get total usage: %w", err) + } + + analytics.ActiveUsers = int(activeUsersCount) + analytics.TotalDataUsed = totalDataUsed + analytics.TotalVoiceUsed = totalVoiceUsed + analytics.TotalSMSUsed = totalSMSUsed + + return analytics, nil +} + +// 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), + RevenueByCarrier: make(map[string]float64), + RevenueByRegion: make(map[string]float64), + AverageRevenue: make(map[string]float64), + TimelineData: []TimelineDataPoint{}, + } + + // This would typically join with billing/payment tables + // For now, returning empty analytics + return analytics, nil +} + +// GetPopularPlans retrieves the most popular rate plans +func (r *GormRepository) GetPopularPlans(ctx context.Context, limit int) ([]*RatePlan, error) { + var plans []*RatePlan + if err := r.db.WithContext(ctx). + Model(&RatePlan{}). + Select("rate_plans.*, COUNT(rate_plan_subscriptions.id) as subscription_count"). + Joins("LEFT JOIN rate_plan_subscriptions ON rate_plans.id = rate_plan_subscriptions.rate_plan_id"). + Where("rate_plans.is_active = ? AND rate_plans.status = ?", true, PlanStatusActive). + Group("rate_plans.id"). + Order("subscription_count DESC"). + Limit(limit). + Find(&plans).Error; err != nil { + r.logger.WithError(err).Error("Failed to get popular plans") + return nil, fmt.Errorf("failed to get popular plans: %w", err) + } + + return plans, nil +} diff --git a/apps/carrier-connector/internal/rateplan/repository_rateplan_crud.go b/apps/carrier-connector/internal/rateplan/repository_rateplan_crud.go new file mode 100644 index 0000000..d3809f1 --- /dev/null +++ b/apps/carrier-connector/internal/rateplan/repository_rateplan_crud.go @@ -0,0 +1,167 @@ +package rateplan + +import ( + "context" + "fmt" + "time" + + "gorm.io/gorm" +) + +// CreateRatePlan creates a new rate plan +func (r *GormRepository) CreateRatePlan(ctx context.Context, plan *RatePlan) error { + now := time.Now() + plan.CreatedAt = now + plan.UpdatedAt = now + + if err := r.db.WithContext(ctx).Create(plan).Error; err != nil { + r.logger.WithError(err).Error("Failed to create rate plan") + return fmt.Errorf("failed to create rate plan: %w", err) + } + + return nil +} + +func (r *GormRepository) GetRatePlan(ctx context.Context, id string) (*RatePlan, error) { + var plan RatePlan + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("rate plan not found: %s", id) + } + r.logger.WithError(err).Error("Failed to get rate plan") + return nil, fmt.Errorf("failed to get rate plan: %w", err) + } + + return &plan, nil +} + +func (r *GormRepository) UpdateRatePlan(ctx context.Context, plan *RatePlan) error { + plan.UpdatedAt = time.Now() + + result := r.db.WithContext(ctx).Where("id = ?", plan.ID).Updates(plan) + if result.Error != nil { + r.logger.WithError(result.Error).Error("Failed to update rate plan") + return fmt.Errorf("failed to update rate plan: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("rate plan not found: %s", plan.ID) + } + + return nil +} + +func (r *GormRepository) DeleteRatePlan(ctx context.Context, id string) error { + result := r.db.WithContext(ctx).Where("id = ?", id).Delete(&RatePlan{}) + if result.Error != nil { + r.logger.WithError(result.Error).Error("Failed to delete rate plan") + return fmt.Errorf("failed to delete rate plan: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("rate plan not found: %s", id) + } + + return nil +} + +func (r *GormRepository) ListRatePlans(ctx context.Context, filter *RatePlanFilter) ([]*RatePlan, error) { + query := r.db.WithContext(ctx).Model(&RatePlan{}) + + if filter.CarrierID != "" { + query = query.Where("carrier_id = ?", filter.CarrierID) + } + if filter.Region != "" { + query = query.Where("region = ?", filter.Region) + } + if filter.PlanType != "" { + query = query.Where("plan_type = ?", filter.PlanType) + } + if filter.Status != "" { + query = query.Where("status = ?", filter.Status) + } + if filter.MinPrice > 0 { + query = query.Where("base_price >= ?", filter.MinPrice) + } + if filter.MaxPrice > 0 { + query = query.Where("base_price <= ?", filter.MaxPrice) + } + if filter.IsActive != nil { + query = query.Where("is_active = ?", *filter.IsActive) + } + if filter.ValidFrom != nil { + query = query.Where("valid_from >= ?", *filter.ValidFrom) + } + if filter.ValidTo != nil { + query = query.Where("valid_to <= ?", *filter.ValidTo) + } + + // Apply ordering + if filter.SortBy != "" { + order := "ASC" + if filter.SortOrder == "desc" { + order = "DESC" + } + query = query.Order(fmt.Sprintf("%s %s", filter.SortBy, order)) + } else { + query = query.Order("priority DESC, created_at DESC") + } + + // Apply pagination + if filter.Limit > 0 { + query = query.Limit(filter.Limit) + if filter.Offset > 0 { + query = query.Offset(filter.Offset) + } + } + + var plans []*RatePlan + if err := query.Find(&plans).Error; err != nil { + r.logger.WithError(err).Error("Failed to list rate plans") + return nil, fmt.Errorf("failed to list rate plans: %w", err) + } + + return plans, nil +} + +// CountRatePlans counts rate plans with filtering +func (r *GormRepository) CountRatePlans(ctx context.Context, filter *RatePlanFilter) (int, error) { + query := r.db.WithContext(ctx).Model(&RatePlan{}) + + // Apply filters + if filter.CarrierID != "" { + query = query.Where("carrier_id = ?", filter.CarrierID) + } + if filter.Region != "" { + query = query.Where("region = ?", filter.Region) + } + if filter.PlanType != "" { + query = query.Where("plan_type = ?", filter.PlanType) + } + if filter.Status != "" { + query = query.Where("status = ?", filter.Status) + } + if filter.MinPrice > 0 { + query = query.Where("base_price >= ?", filter.MinPrice) + } + if filter.MaxPrice > 0 { + query = query.Where("base_price <= ?", filter.MaxPrice) + } + if filter.IsActive != nil { + query = query.Where("is_active = ?", *filter.IsActive) + } + if filter.ValidFrom != nil { + query = query.Where("valid_from >= ?", *filter.ValidFrom) + } + if filter.ValidTo != nil { + query = query.Where("valid_to <= ?", *filter.ValidTo) + } + + var count int64 + if err := query.Count(&count).Error; err != nil { + r.logger.WithError(err).Error("Failed to count rate plans") + return 0, fmt.Errorf("failed to count rate plans: %w", err) + } + + return int(count), nil +} diff --git a/apps/carrier-connector/internal/rateplan/repository_rateplan_subscription.go b/apps/carrier-connector/internal/rateplan/repository_rateplan_subscription.go new file mode 100644 index 0000000..d8dca04 --- /dev/null +++ b/apps/carrier-connector/internal/rateplan/repository_rateplan_subscription.go @@ -0,0 +1,123 @@ +package rateplan + +import ( + "context" + "fmt" + "time" + + "gorm.io/gorm" +) +// CreateSubscription creates a new rate plan subscription +func (r *GormRepository) CreateSubscription(ctx context.Context, subscription *RatePlanSubscription) error { + now := time.Now() + subscription.CreatedAt = now + subscription.UpdatedAt = now + + if err := r.db.WithContext(ctx).Create(subscription).Error; err != nil { + r.logger.WithError(err).Error("Failed to create subscription") + return fmt.Errorf("failed to create subscription: %w", err) + } + + return nil +} + +// GetSubscription retrieves a subscription by ID +func (r *GormRepository) GetSubscription(ctx context.Context, id string) (*RatePlanSubscription, error) { + var subscription RatePlanSubscription + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&subscription).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("subscription not found: %s", id) + } + r.logger.WithError(err).Error("Failed to get subscription") + return nil, fmt.Errorf("failed to get subscription: %w", err) + } + + return &subscription, nil +} + +// GetActiveSubscription retrieves the active subscription for a profile +func (r *GormRepository) GetActiveSubscription(ctx context.Context, profileID string) (*RatePlanSubscription, error) { + var subscription RatePlanSubscription + err := r.db.WithContext(ctx). + Where("profile_id = ? AND status = ?", profileID, SubscriptionStatusActive). + Order("started_at DESC"). + First(&subscription).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil // No active subscription found + } + r.logger.WithError(err).Error("Failed to get active subscription") + return nil, fmt.Errorf("failed to get active subscription: %w", err) + } + + return &subscription, nil +} + +// UpdateSubscription updates an existing subscription +func (r *GormRepository) UpdateSubscription(ctx context.Context, subscription *RatePlanSubscription) error { + subscription.UpdatedAt = time.Now() + + result := r.db.WithContext(ctx).Where("id = ?", subscription.ID).Updates(subscription) + if result.Error != nil { + r.logger.WithError(result.Error).Error("Failed to update subscription") + return fmt.Errorf("failed to update subscription: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("subscription not found: %s", subscription.ID) + } + + return nil +} + +// DeleteSubscription deletes a subscription +func (r *GormRepository) DeleteSubscription(ctx context.Context, id string) error { + result := r.db.WithContext(ctx).Where("id = ?", id).Delete(&RatePlanSubscription{}) + if result.Error != nil { + r.logger.WithError(result.Error).Error("Failed to delete subscription") + return fmt.Errorf("failed to delete subscription: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("subscription not found: %s", id) + } + + return nil +} +// ListSubscriptions retrieves subscriptions for a profile +func (r *GormRepository) ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error) { + query := r.db.WithContext(ctx).Where("profile_id = ?", profileID) + + // Apply filters + if filter.Status != "" { + query = query.Where("status = ?", filter.Status) + } + if filter.RatePlanID != "" { + query = query.Where("rate_plan_id = ?", filter.RatePlanID) + } + if filter.StartedAfter != nil { + query = query.Where("started_at >= ?", *filter.StartedAfter) + } + if filter.StartedBefore != nil { + query = query.Where("started_at <= ?", *filter.StartedBefore) + } + + query = query.Order("started_at DESC") + + // Apply pagination + if filter.Limit > 0 { + query = query.Limit(filter.Limit) + if filter.Offset > 0 { + query = query.Offset(filter.Offset) + } + } + + var subscriptions []*RatePlanSubscription + if err := query.Find(&subscriptions).Error; err != nil { + r.logger.WithError(err).Error("Failed to list subscriptions") + return nil, fmt.Errorf("failed to list subscriptions: %w", err) + } + + return subscriptions, nil +}