From 77ca25a4537566054b416b092c8c56c6c9cd42f0 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Sat, 2 May 2026 19:03:01 +0300 Subject: [PATCH] feat: Add MVNO HTTP handlers with onboarding, management, and statistics endpoints - Add Handler struct with StartOnboarding, GetMVNO, and ListMVNOs methods in handler.go - Add ManagementHandler struct with GetMVNO, ListMVNOs, UpdateMVNOStatus, and GetMVNOStats methods in handler_management.go - Add RegisterRoutes method for /mvno/onboarding, /mvno, and /mvno/:id route registration - Add query parameter parsing for status, plan, and limit filters in ListMVNOs - Add structured logging with mvno_id, status, and error --- .../internal/mvno/handler.go | 109 ++++++++++++++++ .../internal/mvno/handler_management.go | 118 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 apps/carrier-connector/internal/mvno/handler.go create mode 100644 apps/carrier-connector/internal/mvno/handler_management.go diff --git a/apps/carrier-connector/internal/mvno/handler.go b/apps/carrier-connector/internal/mvno/handler.go new file mode 100644 index 0000000..fc7d803 --- /dev/null +++ b/apps/carrier-connector/internal/mvno/handler.go @@ -0,0 +1,109 @@ +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) + } +} diff --git a/apps/carrier-connector/internal/mvno/handler_management.go b/apps/carrier-connector/internal/mvno/handler_management.go new file mode 100644 index 0000000..753ba15 --- /dev/null +++ b/apps/carrier-connector/internal/mvno/handler_management.go @@ -0,0 +1,118 @@ +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, + }) +}