From 3b10a90851b6831e8ed76f1428c838a2a1337764 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Sat, 2 May 2026 19:04:14 +0300 Subject: [PATCH] feat: Add MVNO repository with CRUD operations, filtering, and statistics aggregation - Add Repository interface with CreateMVNO, GetMVNO, UpdateMVNO, ListMVNOs, DeleteMVNO, and UpdateMVNOStatus methods - Add GormRepository struct with db and logger dependencies for GORM-based persistence - Add MVNOFilter struct with status, plan, business_id, limit, offset, and date range filtering - Add GetMVNO and GetMVNOByBusinessID methods with record not found error handling - Add ListMVNOs with dynamic query --- .../internal/mvno/repository.go | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 apps/carrier-connector/internal/mvno/repository.go diff --git a/apps/carrier-connector/internal/mvno/repository.go b/apps/carrier-connector/internal/mvno/repository.go new file mode 100644 index 0000000..6c59999 --- /dev/null +++ b/apps/carrier-connector/internal/mvno/repository.go @@ -0,0 +1,198 @@ +package mvno + +import ( + "context" + "fmt" + "time" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +// Repository defines the interface for MVNO data operations +type Repository interface { + CreateMVNO(ctx context.Context, mvno *MVNO) error + GetMVNO(ctx context.Context, id string) (*MVNO, error) + GetMVNOByBusinessID(ctx context.Context, businessID string) (*MVNO, error) + UpdateMVNO(ctx context.Context, mvno *MVNO) error + ListMVNOs(ctx context.Context, filter *MVNOFilter) ([]*MVNO, error) + DeleteMVNO(ctx context.Context, id string) error + UpdateMVNOStatus(ctx context.Context, id string, status MVNOStatus) error + GetMVNOStats(ctx context.Context) (map[string]interface{}, error) +} + +// MVNOFilter defines filtering options for MVNO queries +type MVNOFilter struct { + Status MVNOStatus `json:"status,omitempty"` + Plan MVNOPlan `json:"plan,omitempty"` + BusinessID string `json:"business_id,omitempty"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` + CreatedAfter *time.Time `json:"created_after,omitempty"` + CreatedBefore *time.Time `json:"created_before,omitempty"` +} + +// GormRepository implements Repository 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, + } +} + +// CreateMVNO creates a new MVNO record +func (r *GormRepository) CreateMVNO(ctx context.Context, mvno *MVNO) error { + if err := r.db.WithContext(ctx).Create(mvno).Error; err != nil { + return fmt.Errorf("failed to create MVNO: %w", err) + } + + r.logger.Info("MVNO created", "id", mvno.ID, "business_id", mvno.BusinessID) + return nil +} + +// GetMVNO retrieves an MVNO by ID +func (r *GormRepository) GetMVNO(ctx context.Context, id string) (*MVNO, error) { + var mvno MVNO + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&mvno).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("MVNO not found: %s", id) + } + return nil, fmt.Errorf("failed to get MVNO: %w", err) + } + return &mvno, nil +} + +// GetMVNOByBusinessID retrieves an MVNO by business ID +func (r *GormRepository) GetMVNOByBusinessID(ctx context.Context, businessID string) (*MVNO, error) { + var mvno MVNO + if err := r.db.WithContext(ctx).Where("business_id = ?", businessID).First(&mvno).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("MVNO not found for business ID: %s", businessID) + } + return nil, fmt.Errorf("failed to get MVNO by business ID: %w", err) + } + return &mvno, nil +} + +// UpdateMVNO updates an existing MVNO +func (r *GormRepository) UpdateMVNO(ctx context.Context, mvno *MVNO) error { + if err := r.db.WithContext(ctx).Save(mvno).Error; err != nil { + return fmt.Errorf("failed to update MVNO: %w", err) + } + + r.logger.Info("MVNO updated", "id", mvno.ID, "status", mvno.Status) + return nil +} + +// ListMVNOs lists MVNOs with optional filtering +func (r *GormRepository) ListMVNOs(ctx context.Context, filter *MVNOFilter) ([]*MVNO, error) { + query := r.db.WithContext(ctx).Model(&MVNO{}) + + // Apply filters + if filter != nil { + if filter.Status != "" { + query = query.Where("status = ?", filter.Status) + } + if filter.Plan != "" { + query = query.Where("plan = ?", filter.Plan) + } + if filter.BusinessID != "" { + query = query.Where("business_id = ?", filter.BusinessID) + } + if filter.CreatedAfter != nil { + query = query.Where("created_at >= ?", *filter.CreatedAfter) + } + if filter.CreatedBefore != nil { + query = query.Where("created_at <= ?", *filter.CreatedBefore) + } + if filter.Limit > 0 { + query = query.Limit(filter.Limit) + } + if filter.Offset > 0 { + query = query.Offset(filter.Offset) + } + } + + var mvnos []*MVNO + if err := query.Order("created_at DESC").Find(&mvnos).Error; err != nil { + return nil, fmt.Errorf("failed to list MVNOs: %w", err) + } + + return mvnos, nil +} + +// DeleteMVNO soft deletes an MVNO +func (r *GormRepository) DeleteMVNO(ctx context.Context, id string) error { + if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&MVNO{}).Error; err != nil { + return fmt.Errorf("failed to delete MVNO: %w", err) + } + + r.logger.Info("MVNO deleted", "id", id) + return nil +} + +// GetMVNOStats returns statistics about MVNOs +func (r *GormRepository) GetMVNOStats(ctx context.Context) (map[string]any, error) { + stats := make(map[string]any) + + // Total count + var totalCount int64 + if err := r.db.WithContext(ctx).Model(&MVNO{}).Count(&totalCount).Error; err != nil { + return nil, fmt.Errorf("failed to count total MVNOs: %w", err) + } + stats["total"] = totalCount + + // Count by status + var statusCounts []struct { + Status MVNOStatus `gorm:"column:status"` + Count int64 `gorm:"column:count"` + } + if err := r.db.WithContext(ctx).Model(&MVNO{}). + Select("status, COUNT(*) as count"). + Group("status"). + Scan(&statusCounts).Error; err != nil { + return nil, fmt.Errorf("failed to count by status: %w", err) + } + + statusMap := make(map[string]int64) + for _, sc := range statusCounts { + statusMap[string(sc.Status)] = sc.Count + } + stats["by_status"] = statusMap + + // Count by plan + var planCounts []struct { + Plan MVNOPlan `gorm:"column:plan"` + Count int64 `gorm:"column:count"` + } + if err := r.db.WithContext(ctx).Model(&MVNO{}). + Select("plan, COUNT(*) as count"). + Group("plan"). + Scan(&planCounts).Error; err != nil { + return nil, fmt.Errorf("failed to count by plan: %w", err) + } + + planMap := make(map[string]int64) + for _, pc := range planCounts { + planMap[string(pc.Plan)] = pc.Count + } + stats["by_plan"] = planMap + + return stats, nil +} + +// UpdateMVNOStatus updates only the status of an MVNO +func (r *GormRepository) UpdateMVNOStatus(ctx context.Context, id string, status MVNOStatus) error { + if err := r.db.WithContext(ctx).Model(&MVNO{}).Where("id = ?", id).Update("status", status).Error; err != nil { + return fmt.Errorf("failed to update MVNO status: %w", err) + } + + r.logger.Info("MVNO status updated", "id", id, "new_status", status) + return nil +}