feat: Remove MVNO handlers, service, and repository implementations from carrier-connector

- Delete handler.go with StartOnboarding, GetMVNO, ListMVNOs, and RegisterRoutes methods
- Delete handler_management.go with GetMVNO, ListMVNOs, UpdateMVNOStatus, and GetMVNOStats endpoints
- Delete onboarding_service.go with StartOnboarding, executeOnboarding, executeStep workflow orchestration
- Delete repository.go with GormRepository CRUD operations, filtering, and statistics methods
- Update provisioner.go to use map[string]any instead of map[string]interface{}
This commit is contained in:
nutcas3
2026-05-03 09:51:28 +03:00
parent 2344ef0710
commit 88fd53a37d
6 changed files with 6 additions and 593 deletions
@@ -1,109 +0,0 @@
package mvno
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// Handler handles HTTP requests for MVNO operations
type Handler struct {
service *OnboardingService
repo Repository
logger *logrus.Logger
}
// NewHandler creates a new HTTP handler
func NewHandler(service *OnboardingService, repo Repository, logger *logrus.Logger) *Handler {
return &Handler{
service: service,
repo: repo,
logger: logger,
}
}
// StartOnboarding handles POST /mvno/onboarding
func (h *Handler) StartOnboarding(c *gin.Context) {
var req OnboardingRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.WithError(err).Error("Invalid onboarding request")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
mvno, err := h.service.StartOnboarding(c.Request.Context(), &req)
if err != nil {
h.logger.WithError(err).Error("Failed to start onboarding")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := h.repo.CreateMVNO(c.Request.Context(), mvno); err != nil {
h.logger.WithError(err).Error("Failed to save MVNO")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save MVNO"})
return
}
h.logger.WithField("mvno_id", mvno.ID).Info("Onboarding started")
c.JSON(http.StatusCreated, gin.H{
"mvno_id": mvno.ID,
"business_id": mvno.BusinessID,
"status": mvno.Status,
"plan": mvno.Plan,
})
}
// GetMVNO handles GET /mvno/{id}
func (h *Handler) GetMVNO(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "MVNO ID is required"})
return
}
mvno, err := h.repo.GetMVNO(c.Request.Context(), id)
if err != nil {
h.logger.WithError(err).WithField("mvno_id", id).Error("Failed to get MVNO")
c.JSON(http.StatusNotFound, gin.H{"error": "MVNO not found"})
return
}
c.JSON(http.StatusOK, mvno)
}
// ListMVNOs handles GET /mvno
func (h *Handler) ListMVNOs(c *gin.Context) {
filter := &MVNOFilter{}
if status := c.Query("status"); status != "" {
filter.Status = MVNOStatus(status)
}
if plan := c.Query("plan"); plan != "" {
filter.Plan = MVNOPlan(plan)
}
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil {
filter.Limit = l
}
}
mvnos, err := h.repo.ListMVNOs(c.Request.Context(), filter)
if err != nil {
h.logger.WithError(err).Error("Failed to list MVNOs")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list MVNOs"})
return
}
c.JSON(http.StatusOK, gin.H{"mvnos": mvnos, "count": len(mvnos)})
}
// RegisterRoutes registers all MVNO routes
func (h *Handler) RegisterRoutes(router *gin.RouterGroup) {
mvno := router.Group("/mvno")
{
mvno.POST("/onboarding", h.StartOnboarding)
mvno.GET("", h.ListMVNOs)
mvno.GET("/:id", h.GetMVNO)
}
}
@@ -1,118 +0,0 @@
package mvno
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// ManagementHandler handles MVNO management HTTP requests
type ManagementHandler struct {
repo Repository
logger *logrus.Logger
}
// NewManagementHandler creates a new management handler
func NewManagementHandler(repo Repository, logger *logrus.Logger) *ManagementHandler {
return &ManagementHandler{
repo: repo,
logger: logger,
}
}
// GetMVNO handles GET /mvno/{id}
func (h *ManagementHandler) GetMVNO(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "MVNO ID is required"})
return
}
mvno, err := h.repo.GetMVNO(c.Request.Context(), id)
if err != nil {
h.logger.WithError(err).WithField("mvno_id", id).Error("Failed to get MVNO")
c.JSON(http.StatusNotFound, gin.H{"error": "MVNO not found"})
return
}
c.JSON(http.StatusOK, mvno)
}
// ListMVNOs handles GET /mvno
func (h *ManagementHandler) ListMVNOs(c *gin.Context) {
filter := &MVNOFilter{}
// Parse query parameters
if status := c.Query("status"); status != "" {
filter.Status = MVNOStatus(status)
}
if plan := c.Query("plan"); plan != "" {
filter.Plan = MVNOPlan(plan)
}
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil {
filter.Limit = l
}
}
mvnos, err := h.repo.ListMVNOs(c.Request.Context(), filter)
if err != nil {
h.logger.WithError(err).Error("Failed to list MVNOs")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list MVNOs"})
return
}
c.JSON(http.StatusOK, gin.H{
"mvnos": mvnos,
"count": len(mvnos),
})
}
// UpdateMVNOStatus handles PUT /mvno/{id}/status
func (h *ManagementHandler) UpdateMVNOStatus(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "MVNO ID is required"})
return
}
var req struct {
Status MVNOStatus `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.repo.UpdateMVNOStatus(c.Request.Context(), id, req.Status); err != nil {
h.logger.WithError(err).WithField("mvno_id", id).Error("Failed to update MVNO status")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update status"})
return
}
h.logger.WithFields(logrus.Fields{
"mvno_id": id,
"new_status": req.Status,
}).Info("MVNO status updated")
c.JSON(http.StatusOK, gin.H{
"mvno_id": id,
"status": req.Status,
})
}
// GetMVNOStats handles GET /mvno/stats
func (h *ManagementHandler) GetMVNOStats(c *gin.Context) {
stats, err := h.repo.GetMVNOStats(c.Request.Context())
if err != nil {
h.logger.WithError(err).Error("Failed to get MVNO stats")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get statistics"})
return
}
c.JSON(http.StatusOK, gin.H{
"stats": stats,
})
}
@@ -1,151 +0,0 @@
package mvno
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/id"
"github.com/sirupsen/logrus"
)
// OnboardingService handles MVNO onboarding process
type OnboardingService struct {
logger *logrus.Logger
validator *OnboardingValidator
provisioner *MVNOProvisioner
monitor *OnboardingMonitor
}
// NewOnboardingService creates a new onboarding service
func NewOnboardingService(logger *logrus.Logger) *OnboardingService {
return &OnboardingService{
logger: logger,
validator: NewOnboardingValidator(logger),
provisioner: NewMVNOProvisioner(logger),
monitor: NewOnboardingMonitor(logger),
}
}
// StartOnboarding initiates the MVNO onboarding process
func (s *OnboardingService) StartOnboarding(ctx context.Context, req *OnboardingRequest) (*MVNO, error) {
// Validate the onboarding request
if err := s.validator.ValidateRequest(req); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Create MVNO record
mvno := &MVNO{
ID: id.GeneratePrefixed("mvno"),
BusinessID: req.BusinessID,
Name: req.BusinessName,
Status: StatusPending,
Plan: req.Plan,
Config: MVNOConfig{
MaxSubscribers: s.getMaxSubscribersForPlan(req.Plan),
AllowedCountries: req.TargetCountries,
CustomBranding: req.Plan != PlanStarter,
APIAccess: req.Plan != PlanStarter,
AdvancedAnalytics: req.Plan == PlanScale || req.Plan == PlanEnterprise,
},
CreatedAt: time.Now(),
}
// Start onboarding progress tracking
progress := &OnboardingProgress{
MVNOID: mvno.ID,
Steps: s.getOnboardingSteps(),
Progress: 0.0,
StartedAt: time.Now(),
}
s.logger.WithFields(logrus.Fields{
"mvno_id": mvno.ID,
"business_id": req.BusinessID,
"plan": req.Plan,
}).Info("Starting MVNO onboarding")
// Execute onboarding steps asynchronously
go s.executeOnboarding(ctx, mvno, progress)
return mvno, nil
}
// executeOnboarding runs all onboarding steps
func (s *OnboardingService) executeOnboarding(ctx context.Context, mvno *MVNO, progress *OnboardingProgress) {
for i, step := range progress.Steps {
select {
case <-ctx.Done():
s.logger.WithField("mvno_id", mvno.ID).Error("Onboarding cancelled")
return
default:
}
step.Status = "running"
s.monitor.UpdateProgress(mvno.ID, progress)
if err := s.executeStep(ctx, mvno, &step); err != nil {
step.Status = "failed"
step.Error = err.Error()
s.logger.WithError(err).WithField("step", step.Name).Error("Step failed")
break
}
step.Status = "completed"
step.CompletedAt = time.Now()
progress.Progress = float64(i+1) / float64(len(progress.Steps)) * 100
s.monitor.UpdateProgress(mvno.ID, progress)
}
if progress.Progress == 100.0 {
mvno.Status = StatusActive
progress.CompletedAt = time.Now()
s.logger.WithField("mvno_id", mvno.ID).Info("Onboarding completed")
}
}
// executeStep executes a single onboarding step
func (s *OnboardingService) executeStep(ctx context.Context, mvno *MVNO, step *OnboardingStep) error {
switch step.Name {
case "validation":
return s.validator.ValidateMVNO(ctx, mvno)
case "provisioning":
return s.provisioner.ProvisionResources(ctx, mvno)
case "carrier_setup":
return s.provisioner.SetupCarriers(ctx, mvno)
case "billing_setup":
return s.provisioner.SetupBilling(ctx, mvno)
case "api_access":
return s.provisioner.SetupAPIAccess(ctx, mvno)
default:
return fmt.Errorf("unknown step: %s", step.Name)
}
}
// getOnboardingSteps returns the standard onboarding workflow
func (s *OnboardingService) getOnboardingSteps() []OnboardingStep {
return []OnboardingStep{
{Name: "validation", Status: "pending"},
{Name: "provisioning", Status: "pending"},
{Name: "carrier_setup", Status: "pending"},
{Name: "billing_setup", Status: "pending"},
{Name: "api_access", Status: "pending"},
}
}
// getMaxSubscribersForPlan returns subscriber limits per plan
func (s *OnboardingService) getMaxSubscribersForPlan(plan MVNOPlan) int {
switch plan {
case PlanStarter:
return 1000
case PlanGrowth:
return 10000
case PlanScale:
return 100000
case PlanEnterprise:
return -1 // Unlimited
default:
return 1000
}
}
@@ -78,7 +78,7 @@ func (p *MVNOProvisioner) SetupAPIAccess(ctx context.Context, mvno *MVNO) error
_ = id.GeneratePrefixed("sec") // Generate secret key but don't use until storage is implemented
permissions := p.getAPIPermissions(mvno.Plan)
p.logger.WithFields(map[string]interface{}{
p.logger.WithFields(map[string]any{
"mvno_id": mvno.ID,
"api_key": apiKey[:8] + "...",
"permissions": len(permissions),
@@ -102,7 +102,7 @@ func (p *MVNOProvisioner) provisionDatabaseSchema(ctx context.Context, mvno *MVN
// provisionStorageResources provisions storage resources
func (p *MVNOProvisioner) provisionStorageResources(ctx context.Context, mvno *MVNO) error {
storageSize := p.getStorageAllocation(mvno.Plan)
p.logger.WithFields(map[string]interface{}{
p.logger.WithFields(map[string]any{
"mvno_id": mvno.ID,
"storage_gb": storageSize,
}).Info("Storage resources provisioned")
@@ -126,7 +126,7 @@ func (p *MVNOProvisioner) selectCarriers(countries []string) ([]string, error) {
// configureCarrier configures individual carrier
func (p *MVNOProvisioner) configureCarrier(ctx context.Context, mvno *MVNO, carrierID string) error {
p.logger.WithFields(map[string]interface{}{
p.logger.WithFields(map[string]any{
"mvno_id": mvno.ID,
"carrier_id": carrierID,
}).Info("Carrier configured")
@@ -135,7 +135,7 @@ func (p *MVNOProvisioner) configureCarrier(ctx context.Context, mvno *MVNO, carr
// configureRatePlans configures rate plans
func (p *MVNOProvisioner) configureRatePlans(ctx context.Context, mvno *MVNO, billingID string) error {
p.logger.WithFields(map[string]interface{}{
p.logger.WithFields(map[string]any{
"mvno_id": mvno.ID,
"billing_id": billingID,
"plan": mvno.Plan,
@@ -145,7 +145,7 @@ func (p *MVNOProvisioner) configureRatePlans(ctx context.Context, mvno *MVNO, bi
// setupPaymentProcessing setup payment processing
func (p *MVNOProvisioner) setupPaymentProcessing(ctx context.Context, mvno *MVNO, billingID string) error {
p.logger.WithFields(map[string]interface{}{
p.logger.WithFields(map[string]any{
"mvno_id": mvno.ID,
"billing_id": billingID,
}).Info("Payment processing setup")
@@ -1,198 +0,0 @@
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
}
@@ -5,7 +5,6 @@ import (
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handlers"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/sirupsen/logrus"
)
@@ -13,27 +12,17 @@ import (
// SelectionService provides high-level carrier selection operations
type SelectionService struct {
manager *smdp.SMDPManager
handler *handlers.SelectionHandler
logger *logrus.Logger
}
// NewSelectionService creates a new selection service
func NewSelectionService(manager *smdp.SMDPManager) *SelectionService {
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
func NewSelectionService(manager *smdp.SMDPManager, logger *logrus.Logger) *SelectionService {
return &SelectionService{
manager: manager,
handler: handlers.NewSelectionHandler(manager),
logger: logger,
}
}
// GetHandler returns the selection handler for API registration
func (s *SelectionService) GetHandler() *handlers.SelectionHandler {
return s.handler
}
// IntelligentCarrierSelection performs intelligent carrier selection with comprehensive criteria
func (s *SelectionService) IntelligentCarrierSelection(ctx context.Context, request *IntelligentSelectionRequest) (*IntelligentSelectionResponse, error) {
s.logger.WithFields(logrus.Fields{