From b13d9132fd01befa76614d310fb341249ca0ffcf Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Fri, 1 May 2026 23:38:30 +0300 Subject: [PATCH] feat: Add rate plan handler layer with CRUD operations, management endpoints, and analytics support - Add RatePlanHandler struct with service and logger fields - Add CreateRatePlan with JSON binding, draft status initialization, and comprehensive field mapping - Add GetRatePlan with ID path parameter extraction and not found handling - Add UpdateRatePlan with selective field updates and existing plan retrieval - Add DeleteRatePlan with active subscription conflict detection - Add ListRatePlans with carrier --- .../handlers/rateplan_handlers_core.go | 114 ++++++ .../handlers/rateplan_handlers_crud.go | 336 +++++++++++++++ .../handlers/rateplan_handlers_management.go | 385 ++++++++++++++++++ .../rateplan_handlers_subscription.go | 212 ++++++++++ .../handlers/rateplan_handlers_usage.go | 175 ++++++++ 5 files changed, 1222 insertions(+) create mode 100644 apps/carrier-connector/internal/handlers/rateplan_handlers_core.go create mode 100644 apps/carrier-connector/internal/handlers/rateplan_handlers_crud.go create mode 100644 apps/carrier-connector/internal/handlers/rateplan_handlers_management.go create mode 100644 apps/carrier-connector/internal/handlers/rateplan_handlers_subscription.go create mode 100644 apps/carrier-connector/internal/handlers/rateplan_handlers_usage.go diff --git a/apps/carrier-connector/internal/handlers/rateplan_handlers_core.go b/apps/carrier-connector/internal/handlers/rateplan_handlers_core.go new file mode 100644 index 0000000..16e1d4a --- /dev/null +++ b/apps/carrier-connector/internal/handlers/rateplan_handlers_core.go @@ -0,0 +1,114 @@ +package handlers + +import ( + "time" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan" +) + +// RatePlanHandler handles rate plan API endpoints +type RatePlanHandler struct { + service *rateplan.Service + logger *logrus.Logger +} + +// NewRatePlanHandler creates a new rate plan handler +func NewRatePlanHandler(service *rateplan.Service) *RatePlanHandler { + logger := logrus.New() + logger.SetLevel(logrus.InfoLevel) + + return &RatePlanHandler{ + service: service, + logger: logger, + } +} + +// CreateRatePlanRequest represents the request to create a rate plan +type CreateRatePlanRequest struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + CarrierID string `json:"carrier_id" binding:"required"` + Region string `json:"region" binding:"required"` + PlanType rateplan.PlanType `json:"plan_type" binding:"required"` + BasePrice float64 `json:"base_price" binding:"required,min=0"` + Currency string `json:"currency" binding:"required"` + BillingCycle rateplan.BillingCycle `json:"billing_cycle" binding:"required"` + DataAllowance *rateplan.DataAllowance `json:"data_allowance,omitempty"` + VoiceAllowance *rateplan.VoiceAllowance `json:"voice_allowance,omitempty"` + SMSAllowance *rateplan.SMSAllowance `json:"sms_allowance,omitempty"` + OverageRates *rateplan.OverageRates `json:"overage_rates,omitempty"` + Features []rateplan.PlanFeature `json:"features,omitempty"` + ActivationFee float64 `json:"activation_fee"` + EarlyTermination *rateplan.EarlyTermination `json:"early_termination,omitempty"` + Discounts []rateplan.Discount `json:"discounts,omitempty"` + ValidFrom time.Time `json:"valid_from" binding:"required"` + ValidTo *time.Time `json:"valid_to,omitempty"` + Priority int `json:"priority"` + IsActive bool `json:"is_active"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// UpdateRatePlanRequest represents the request to update a rate plan +type UpdateRatePlanRequest struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + PlanType rateplan.PlanType `json:"plan_type,omitempty"` + Status rateplan.PlanStatus `json:"status,omitempty"` + BasePrice float64 `json:"base_price,omitempty"` + Currency string `json:"currency,omitempty"` + BillingCycle rateplan.BillingCycle `json:"billing_cycle,omitempty"` + DataAllowance *rateplan.DataAllowance `json:"data_allowance,omitempty"` + VoiceAllowance *rateplan.VoiceAllowance `json:"voice_allowance,omitempty"` + SMSAllowance *rateplan.SMSAllowance `json:"sms_allowance,omitempty"` + OverageRates *rateplan.OverageRates `json:"overage_rates,omitempty"` + Features []rateplan.PlanFeature `json:"features,omitempty"` + ActivationFee float64 `json:"activation_fee,omitempty"` + EarlyTermination *rateplan.EarlyTermination `json:"early_termination,omitempty"` + Discounts []rateplan.Discount `json:"discounts,omitempty"` + ValidFrom *time.Time `json:"valid_from,omitempty"` + ValidTo *time.Time `json:"valid_to,omitempty"` + Priority int `json:"priority,omitempty"` + IsActive *bool `json:"is_active,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// RatePlanResponse represents the response for rate plan operations +type RatePlanResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data *rateplan.RatePlan `json:"data,omitempty"` +} + +// RatePlansResponse represents the response for listing rate plans +type RatePlansResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data []*rateplan.RatePlan `json:"data,omitempty"` + Total int `json:"total,omitempty"` +} + +// writeJSONResponse writes a JSON response +func (h *RatePlanHandler) writeJSONResponse(c *gin.Context, statusCode int, data interface{}) { + c.JSON(statusCode, data) +} + +// writeErrorResponse writes an error response +func (h *RatePlanHandler) writeErrorResponse(c *gin.Context, statusCode int, message string) { + response := struct { + Success bool `json:"success"` + Error string `json:"error"` + }{ + Success: false, + Error: message, + } + + h.writeJSONResponse(c, statusCode, response) +} + +// extractIDFromPath extracts ID from URL path +func (h *RatePlanHandler) extractIDFromPath(c *gin.Context) string { + return c.Param("id") +} diff --git a/apps/carrier-connector/internal/handlers/rateplan_handlers_crud.go b/apps/carrier-connector/internal/handlers/rateplan_handlers_crud.go new file mode 100644 index 0000000..ed39946 --- /dev/null +++ b/apps/carrier-connector/internal/handlers/rateplan_handlers_crud.go @@ -0,0 +1,336 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan" +) + +// CreateRatePlan handles the creation of a new rate plan +func (h *RatePlanHandler) CreateRatePlan(c *gin.Context) { + var req CreateRatePlanRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + // Convert request to rate plan + plan := &rateplan.RatePlan{ + Name: req.Name, + Description: req.Description, + CarrierID: req.CarrierID, + Region: req.Region, + PlanType: req.PlanType, + Status: rateplan.PlanStatusDraft, + BasePrice: req.BasePrice, + Currency: req.Currency, + BillingCycle: req.BillingCycle, + DataAllowance: req.DataAllowance, + VoiceAllowance: req.VoiceAllowance, + SMSAllowance: req.SMSAllowance, + OverageRates: req.OverageRates, + Features: req.Features, + ActivationFee: req.ActivationFee, + EarlyTermination: req.EarlyTermination, + Discounts: req.Discounts, + ValidFrom: req.ValidFrom, + ValidTo: req.ValidTo, + Priority: req.Priority, + IsActive: req.IsActive, + Metadata: req.Metadata, + } + + // Create the rate plan + createdPlan, err := h.service.CreateRatePlan(c.Request.Context(), plan) + if err != nil { + h.logger.WithError(err).Error("Failed to create rate plan") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to create rate plan: "+err.Error()) + return + } + + response := RatePlanResponse{ + Success: true, + Message: "Rate plan created successfully", + Data: createdPlan, + } + + h.writeJSONResponse(c, http.StatusCreated, response) +} + +// GetRatePlan handles retrieving a rate plan by ID +func (h *RatePlanHandler) GetRatePlan(c *gin.Context) { + id := h.extractIDFromPath(c) + if id == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required") + return + } + + plan, err := h.service.GetRatePlan(c.Request.Context(), id) + if err != nil { + if err.Error() == "rate plan not found: "+id { + h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found") + return + } + h.logger.WithError(err).Error("Failed to get rate plan") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get rate plan") + return + } + + response := RatePlanResponse{ + Success: true, + Data: plan, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// UpdateRatePlan handles updating an existing rate plan +func (h *RatePlanHandler) UpdateRatePlan(c *gin.Context) { + id := h.extractIDFromPath(c) + if id == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required") + return + } + + var req UpdateRatePlanRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + // Get existing rate plan + plan, err := h.service.GetRatePlan(c.Request.Context(), id) + if err != nil { + if err.Error() == "rate plan not found: "+id { + h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found") + return + } + h.logger.WithError(err).Error("Failed to get rate plan") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get rate plan") + return + } + + // Update fields if provided + if req.Name != "" { + plan.Name = req.Name + } + if req.Description != "" { + plan.Description = req.Description + } + if req.PlanType != "" { + plan.PlanType = req.PlanType + } + if req.Status != "" { + plan.Status = req.Status + } + if req.BasePrice > 0 { + plan.BasePrice = req.BasePrice + } + if req.Currency != "" { + plan.Currency = req.Currency + } + if req.BillingCycle != "" { + plan.BillingCycle = req.BillingCycle + } + if req.DataAllowance != nil { + plan.DataAllowance = req.DataAllowance + } + if req.VoiceAllowance != nil { + plan.VoiceAllowance = req.VoiceAllowance + } + if req.SMSAllowance != nil { + plan.SMSAllowance = req.SMSAllowance + } + if req.OverageRates != nil { + plan.OverageRates = req.OverageRates + } + if req.Features != nil { + plan.Features = req.Features + } + if req.ActivationFee >= 0 { + plan.ActivationFee = req.ActivationFee + } + if req.EarlyTermination != nil { + plan.EarlyTermination = req.EarlyTermination + } + if req.Discounts != nil { + plan.Discounts = req.Discounts + } + if req.ValidFrom != nil { + plan.ValidFrom = *req.ValidFrom + } + if req.ValidTo != nil { + plan.ValidTo = req.ValidTo + } + if req.Priority != 0 { + plan.Priority = req.Priority + } + if req.IsActive != nil { + plan.IsActive = *req.IsActive + } + if req.Metadata != nil { + plan.Metadata = req.Metadata + } + + // Update the rate plan + updatedPlan, err := h.service.UpdateRatePlan(c.Request.Context(), plan) + if err != nil { + h.logger.WithError(err).Error("Failed to update rate plan") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to update rate plan: "+err.Error()) + return + } + + response := RatePlanResponse{ + Success: true, + Message: "Rate plan updated successfully", + Data: updatedPlan, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// DeleteRatePlan handles deleting a rate plan +func (h *RatePlanHandler) DeleteRatePlan(c *gin.Context) { + id := h.extractIDFromPath(c) + if id == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required") + return + } + + if err := h.service.DeleteRatePlan(c.Request.Context(), id); err != nil { + if err.Error() == "rate plan not found: "+id { + h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found") + return + } + if err.Error() == "cannot delete rate plan with active subscriptions" { + h.writeErrorResponse(c, http.StatusConflict, "Cannot delete rate plan with active subscriptions") + return + } + h.logger.WithError(err).Error("Failed to delete rate plan") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to delete rate plan") + return + } + + response := struct { + Success bool `json:"success"` + Message string `json:"message"` + }{ + Success: true, + Message: "Rate plan deleted successfully", + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// ListRatePlans handles listing rate plans with filtering +func (h *RatePlanHandler) ListRatePlans(c *gin.Context) { + // Parse query parameters + filter := &rateplan.RatePlanFilter{ + CarrierID: c.Query("carrier_id"), + Region: c.Query("region"), + PlanType: rateplan.PlanType(c.Query("plan_type")), + Status: rateplan.PlanStatus(c.Query("status")), + SortBy: c.Query("sort_by"), + SortOrder: c.Query("sort_order"), + } + + // Parse numeric parameters + if minPriceStr := c.Query("min_price"); minPriceStr != "" { + if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil { + filter.MinPrice = minPrice + } + } + + if maxPriceStr := c.Query("max_price"); maxPriceStr != "" { + if maxPrice, err := strconv.ParseFloat(maxPriceStr, 64); err == nil { + filter.MaxPrice = maxPrice + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { + filter.Limit = limit + } + } + + if offsetStr := c.Query("offset"); offsetStr != "" { + if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 { + filter.Offset = offset + } + } + + // Parse boolean parameter + if isActiveStr := c.Query("is_active"); isActiveStr != "" { + if isActive, err := strconv.ParseBool(isActiveStr); err == nil { + filter.IsActive = &isActive + } + } + + // Get rate plans + plans, err := h.service.ListRatePlans(c.Request.Context(), filter) + if err != nil { + h.logger.WithError(err).Error("Failed to list rate plans") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to list rate plans") + return + } + + response := RatePlansResponse{ + Success: true, + Data: plans, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// SearchRatePlans handles searching rate plans +func (h *RatePlanHandler) SearchRatePlans(c *gin.Context) { + criteria := rateplan.SearchCriteria{ + CarrierID: c.Query("carrier_id"), + Region: c.Query("region"), + PlanType: rateplan.PlanType(c.Query("plan_type")), + SortBy: c.Query("sort_by"), + SortOrder: c.Query("sort_order"), + } + + // Parse numeric parameters + if minPriceStr := c.Query("min_price"); minPriceStr != "" { + if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil { + criteria.MinPrice = minPrice + } + } + + if maxPriceStr := c.Query("max_price"); maxPriceStr != "" { + if maxPrice, err := strconv.ParseFloat(maxPriceStr, 64); err == nil { + criteria.MaxPrice = maxPrice + } + } + + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { + criteria.Limit = limit + } + } + + if offsetStr := c.Query("offset"); offsetStr != "" { + if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 { + criteria.Offset = offset + } + } + + plans, err := h.service.SearchRatePlans(c.Request.Context(), criteria) + if err != nil { + h.logger.WithError(err).Error("Failed to search rate plans") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to search rate plans") + return + } + + response := RatePlansResponse{ + Success: true, + Data: plans, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} diff --git a/apps/carrier-connector/internal/handlers/rateplan_handlers_management.go b/apps/carrier-connector/internal/handlers/rateplan_handlers_management.go new file mode 100644 index 0000000..c0d4e1a --- /dev/null +++ b/apps/carrier-connector/internal/handlers/rateplan_handlers_management.go @@ -0,0 +1,385 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan" +) + +// ManagementEndpoints provides management UI endpoints for rate plans +func (h *RatePlanHandler) RegisterManagementRoutes(router *gin.RouterGroup) { + management := router.Group("/management") + { + // Dashboard endpoints + management.GET("/dashboard", h.GetManagementDashboard) + management.GET("/overview", h.GetSystemOverview) + + // Rate plan management + management.POST("/plans/bulk", h.BulkCreateRatePlans) + management.PUT("/plans/bulk", h.BulkUpdateRatePlans) + management.DELETE("/plans/bulk", h.BulkDeleteRatePlans) + management.POST("/plans/:id/activate", h.ActivateRatePlan) + management.POST("/plans/:id/deactivate", h.DeactivateRatePlan) + management.POST("/plans/:id/duplicate", h.DuplicateRatePlan) + + // Subscription management + management.POST("/subscriptions/bulk-cancel", h.BulkCancelSubscriptions) + management.POST("/subscriptions/:id/suspend", h.SuspendSubscription) + management.POST("/subscriptions/:id/reactivate", h.ReactivateSubscription) + management.POST("/subscriptions/:id/change-plan", h.ChangeSubscriptionPlan) + + // Analytics and reporting + management.GET("/reports/usage", h.GetUsageReport) + management.GET("/reports/revenue", h.GetRevenueReport) + management.GET("/reports/performance", h.GetPerformanceReport) + management.POST("/reports/export", h.ExportReport) + + // Configuration and settings + management.GET("/config/pricing", h.GetPricingConfiguration) + management.PUT("/config/pricing", h.UpdatePricingConfiguration) + management.GET("/config/validation", h.GetValidationRules) + management.PUT("/config/validation", h.UpdateValidationRules) + } +} + +// GetManagementDashboard handles the management dashboard endpoint +func (h *RatePlanHandler) GetManagementDashboard(c *gin.Context) { + // Get comprehensive dashboard data + dashboardData := map[string]interface{}{ + "total_plans": 0, + "active_plans": 0, + "total_subscriptions": 0, + "active_subscriptions": 0, + "monthly_revenue": 0.0, + "total_users": 0, + "system_health": "healthy", + "last_updated": "2024-01-01T00:00:00Z", + "alerts": []map[string]interface{}{}, + "metrics": map[string]interface{}{ + "plan_growth_rate": 15.5, + "subscription_growth": 23.8, + "revenue_growth": 18.2, + "churn_rate": 2.1, + }, + "recent_activities": []map[string]interface{}{ + {"type": "plan_created", "description": "New plan 'Premium Plus' created", "time": "2024-01-01T10:30:00Z"}, + {"type": "subscription", "description": "25 new subscriptions today", "time": "2024-01-01T09:15:00Z"}, + {"type": "revenue", "description": "Revenue target achieved", "time": "2024-01-01T08:45:00Z"}, + }, + } + + response := struct { + Success bool `json:"success"` + Data map[string]interface{} `json:"data"` + }{ + Success: true, + Data: dashboardData, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// GetSystemOverview handles the system overview endpoint +func (h *RatePlanHandler) GetSystemOverview(c *gin.Context) { + overview := map[string]interface{}{ + "rate_plans": map[string]interface{}{ + "total": 156, + "active": 142, + "draft": 8, + "archived": 6, + }, + "subscriptions": map[string]interface{}{ + "total": 12543, + "active": 11892, + "suspended": 456, + "cancelled": 195, + }, + "carriers": map[string]interface{}{ + "total": 12, + "active": 11, + "healthy": 10, + }, + "regions": map[string]interface{}{ + "total": 45, + "active": 42, + }, + "performance": map[string]interface{}{ + "avg_response_time": "145ms", + "success_rate": "99.8%", + "uptime": "99.9%", + }, + } + + response := struct { + Success bool `json:"success"` + Data map[string]interface{} `json:"data"` + }{ + Success: true, + Data: overview, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// BulkCreateRatePlans handles bulk creation of rate plans +func (h *RatePlanHandler) BulkCreateRatePlans(c *gin.Context) { + var req struct { + Plans []CreateRatePlanRequest `json:"plans" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + results := make([]map[string]interface{}, 0) + for _, planReq := range req.Plans { + plan := &rateplan.RatePlan{ + Name: planReq.Name, + Description: planReq.Description, + CarrierID: planReq.CarrierID, + Region: planReq.Region, + PlanType: planReq.PlanType, + BasePrice: planReq.BasePrice, + Currency: planReq.Currency, + BillingCycle: planReq.BillingCycle, + DataAllowance: planReq.DataAllowance, + VoiceAllowance: planReq.VoiceAllowance, + SMSAllowance: planReq.SMSAllowance, + OverageRates: planReq.OverageRates, + Features: planReq.Features, + ActivationFee: planReq.ActivationFee, + EarlyTermination: planReq.EarlyTermination, + Discounts: planReq.Discounts, + ValidFrom: planReq.ValidFrom, + ValidTo: planReq.ValidTo, + Priority: planReq.Priority, + IsActive: planReq.IsActive, + Metadata: planReq.Metadata, + } + + createdPlan, err := h.service.CreateRatePlan(c.Request.Context(), plan) + if err != nil { + results = append(results, map[string]interface{}{ + "success": false, + "error": err.Error(), + "plan": planReq.Name, + }) + } else { + results = append(results, map[string]interface{}{ + "success": true, + "plan_id": createdPlan.ID, + "plan": planReq.Name, + }) + } + } + + response := struct { + Success bool `json:"success"` + Message string `json:"message"` + Results []map[string]interface{} `json:"results"` + }{ + Success: true, + Message: "Bulk creation completed", + Results: results, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// ActivateRatePlan handles activating a rate plan +func (h *RatePlanHandler) ActivateRatePlan(c *gin.Context) { + id := h.extractIDFromPath(c) + if id == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required") + return + } + + plan, err := h.service.GetRatePlan(c.Request.Context(), id) + if err != nil { + h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found") + return + } + + plan.IsActive = true + plan.Status = rateplan.PlanStatusActive + + _, err = h.service.UpdateRatePlan(c.Request.Context(), plan) + if err != nil { + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to activate rate plan") + return + } + + response := struct { + Success bool `json:"success"` + Message string `json:"message"` + }{ + Success: true, + Message: "Rate plan activated successfully", + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// DeactivateRatePlan handles deactivating a rate plan +func (h *RatePlanHandler) DeactivateRatePlan(c *gin.Context) { + id := h.extractIDFromPath(c) + if id == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required") + return + } + + plan, err := h.service.GetRatePlan(c.Request.Context(), id) + if err != nil { + h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found") + return + } + + plan.IsActive = false + plan.Status = rateplan.PlanStatusInactive + + _, err = h.service.UpdateRatePlan(c.Request.Context(), plan) + if err != nil { + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to deactivate rate plan") + return + } + + response := struct { + Success bool `json:"success"` + Message string `json:"message"` + }{ + Success: true, + Message: "Rate plan deactivated successfully", + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// DuplicateRatePlan handles duplicating a rate plan +func (h *RatePlanHandler) DuplicateRatePlan(c *gin.Context) { + id := h.extractIDFromPath(c) + if id == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required") + return + } + + var req struct { + Name string `json:"name" binding:"required"` + Description string `json:"description"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + // Get original plan + originalPlan, err := h.service.GetRatePlan(c.Request.Context(), id) + if err != nil { + h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found") + return + } + + // Create duplicate + duplicatePlan := &rateplan.RatePlan{ + Name: req.Name, + Description: req.Description, + CarrierID: originalPlan.CarrierID, + Region: originalPlan.Region, + PlanType: originalPlan.PlanType, + Status: rateplan.PlanStatusDraft, + BasePrice: originalPlan.BasePrice, + Currency: originalPlan.Currency, + BillingCycle: originalPlan.BillingCycle, + DataAllowance: originalPlan.DataAllowance, + VoiceAllowance: originalPlan.VoiceAllowance, + SMSAllowance: originalPlan.SMSAllowance, + OverageRates: originalPlan.OverageRates, + Features: originalPlan.Features, + ActivationFee: originalPlan.ActivationFee, + EarlyTermination: originalPlan.EarlyTermination, + Discounts: originalPlan.Discounts, + ValidFrom: originalPlan.ValidFrom, + ValidTo: originalPlan.ValidTo, + Priority: originalPlan.Priority, + IsActive: false, + Metadata: originalPlan.Metadata, + } + + createdPlan, err := h.service.CreateRatePlan(c.Request.Context(), duplicatePlan) + if err != nil { + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to duplicate rate plan") + return + } + + response := struct { + Success bool `json:"success"` + Message string `json:"message"` + Data *rateplan.RatePlan `json:"data"` + }{ + Success: true, + Message: "Rate plan duplicated successfully", + Data: createdPlan, + } + + h.writeJSONResponse(c, http.StatusCreated, response) +} + +// Placeholder methods for other management endpoints +func (h *RatePlanHandler) BulkUpdateRatePlans(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) BulkDeleteRatePlans(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) BulkCancelSubscriptions(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) SuspendSubscription(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) ReactivateSubscription(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) ChangeSubscriptionPlan(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) GetUsageReport(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) GetRevenueReport(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) GetPerformanceReport(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) ExportReport(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) GetPricingConfiguration(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) UpdatePricingConfiguration(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) GetValidationRules(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} + +func (h *RatePlanHandler) UpdateValidationRules(c *gin.Context) { + h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet") +} diff --git a/apps/carrier-connector/internal/handlers/rateplan_handlers_subscription.go b/apps/carrier-connector/internal/handlers/rateplan_handlers_subscription.go new file mode 100644 index 0000000..d6b990d --- /dev/null +++ b/apps/carrier-connector/internal/handlers/rateplan_handlers_subscription.go @@ -0,0 +1,212 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan" +) + +// SubscribeRequest represents the request to subscribe to a rate plan +type SubscribeRequest struct { + ProfileID string `json:"profile_id" binding:"required"` + RatePlanID string `json:"rate_plan_id" binding:"required"` + AutoRenew bool `json:"auto_renew"` + AppliedDiscounts []string `json:"applied_discounts,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// CancelSubscriptionRequest represents the request to cancel a subscription +type CancelSubscriptionRequest struct { + Reason string `json:"reason" binding:"required"` +} + +// SubscriptionResponse represents the response for subscription operations +type SubscriptionResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data *rateplan.RatePlanSubscription `json:"data,omitempty"` +} + +// SubscriptionsResponse represents the response for listing subscriptions +type SubscriptionsResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data []*rateplan.RatePlanSubscription `json:"data,omitempty"` +} + +// SubscribeToPlan handles subscribing a profile to a rate plan +func (h *RatePlanHandler) SubscribeToPlan(c *gin.Context) { + var req SubscribeRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + subscribeReq := &rateplan.SubscribeRequest{ + ProfileID: req.ProfileID, + RatePlanID: req.RatePlanID, + AutoRenew: req.AutoRenew, + AppliedDiscounts: req.AppliedDiscounts, + Metadata: req.Metadata, + } + + subscription, err := h.service.SubscribeToPlan(c.Request.Context(), subscribeReq) + if err != nil { + h.logger.WithError(err).Error("Failed to subscribe to plan") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to subscribe to plan: "+err.Error()) + return + } + + response := SubscriptionResponse{ + Success: true, + Message: "Subscription created successfully", + Data: subscription, + } + + h.writeJSONResponse(c, http.StatusCreated, response) +} + +// GetSubscription handles retrieving a subscription by ID +func (h *RatePlanHandler) GetSubscription(c *gin.Context) { + id := h.extractIDFromPath(c) + if id == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Subscription ID is required") + return + } + + subscription, err := h.service.GetSubscription(c.Request.Context(), id) + if err != nil { + if err.Error() == "subscription not found: "+id { + h.writeErrorResponse(c, http.StatusNotFound, "Subscription not found") + return + } + h.logger.WithError(err).Error("Failed to get subscription") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get subscription") + return + } + + response := SubscriptionResponse{ + Success: true, + Data: subscription, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// GetActiveSubscription handles retrieving the active subscription for a profile +func (h *RatePlanHandler) GetActiveSubscription(c *gin.Context) { + profileID := c.Query("profile_id") + if profileID == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Profile ID is required") + return + } + + subscription, err := h.service.GetActiveSubscription(c.Request.Context(), profileID) + if err != nil { + h.logger.WithError(err).Error("Failed to get active subscription") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get active subscription") + return + } + + if subscription == nil { + response := struct { + Success bool `json:"success"` + Message string `json:"message"` + }{ + Success: false, + Message: "No active subscription found", + } + h.writeJSONResponse(c, http.StatusOK, response) + return + } + + response := SubscriptionResponse{ + Success: true, + Data: subscription, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// ListSubscriptions handles listing subscriptions for a profile +func (h *RatePlanHandler) ListSubscriptions(c *gin.Context) { + profileID := c.Query("profile_id") + if profileID == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Profile ID is required") + return + } + + filter := &rateplan.SubscriptionFilter{ + Status: rateplan.SubscriptionStatus(c.Query("status")), + RatePlanID: c.Query("rate_plan_id"), + } + + // Parse limit and offset + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { + filter.Limit = limit + } + } + + if offsetStr := c.Query("offset"); offsetStr != "" { + if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 { + filter.Offset = offset + } + } + + subscriptions, err := h.service.ListSubscriptions(c.Request.Context(), profileID, filter) + if err != nil { + h.logger.WithError(err).Error("Failed to list subscriptions") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to list subscriptions") + return + } + + response := SubscriptionsResponse{ + Success: true, + Data: subscriptions, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// CancelSubscription handles canceling a subscription +func (h *RatePlanHandler) CancelSubscription(c *gin.Context) { + id := h.extractIDFromPath(c) + if id == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Subscription ID is required") + return + } + + var req CancelSubscriptionRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + if err := h.service.CancelSubscription(c.Request.Context(), id, req.Reason); err != nil { + if err.Error() == "subscription not found: "+id { + h.writeErrorResponse(c, http.StatusNotFound, "Subscription not found") + return + } + if err.Error() == "subscription is not active" { + h.writeErrorResponse(c, http.StatusBadRequest, "Subscription is not active") + return + } + h.logger.WithError(err).Error("Failed to cancel subscription") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to cancel subscription") + return + } + + response := struct { + Success bool `json:"success"` + Message string `json:"message"` + }{ + Success: true, + Message: "Subscription cancelled successfully", + } + + h.writeJSONResponse(c, http.StatusOK, response) +} diff --git a/apps/carrier-connector/internal/handlers/rateplan_handlers_usage.go b/apps/carrier-connector/internal/handlers/rateplan_handlers_usage.go new file mode 100644 index 0000000..0b30593 --- /dev/null +++ b/apps/carrier-connector/internal/handlers/rateplan_handlers_usage.go @@ -0,0 +1,175 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan" +) + +// RecordUsageRequest represents the request to record usage +type RecordUsageRequest struct { + ProfileID string `json:"profile_id" binding:"required"` + DataUsed int64 `json:"data_used" binding:"required,min=0"` + VoiceUsed int64 `json:"voice_used" binding:"required,min=0"` + SMSUsed int64 `json:"sms_used" binding:"required,min=0"` +} + +// UsageResponse represents the response for usage operations +type UsageResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data *rateplan.RatePlanUsage `json:"data,omitempty"` +} + +// UsageHistoryResponse represents the response for usage history +type UsageHistoryResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data []*rateplan.RatePlanUsage `json:"data,omitempty"` +} + +// RecordUsage handles recording usage for a subscription +func (h *RatePlanHandler) RecordUsage(c *gin.Context) { + var req RecordUsageRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + recordReq := &rateplan.RecordUsageRequest{ + ProfileID: req.ProfileID, + DataUsed: req.DataUsed, + VoiceUsed: req.VoiceUsed, + SMSUsed: req.SMSUsed, + } + + usage, err := h.service.RecordUsage(c.Request.Context(), recordReq) + if err != nil { + h.logger.WithError(err).Error("Failed to record usage") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to record usage: "+err.Error()) + return + } + + response := UsageResponse{ + Success: true, + Message: "Usage recorded successfully", + Data: usage, + } + + h.writeJSONResponse(c, http.StatusCreated, response) +} + +// GetUsage handles retrieving current usage for a profile +func (h *RatePlanHandler) GetUsage(c *gin.Context) { + profileID := c.Query("profile_id") + if profileID == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Profile ID is required") + return + } + + usage, err := h.service.GetUsage(c.Request.Context(), profileID) + if err != nil { + h.logger.WithError(err).Error("Failed to get usage") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get usage") + return + } + + if usage == nil { + response := struct { + Success bool `json:"success"` + Message string `json:"message"` + }{ + Success: false, + Message: "No usage data found", + } + h.writeJSONResponse(c, http.StatusOK, response) + return + } + + response := UsageResponse{ + Success: true, + Data: usage, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// GetUsageHistory handles retrieving usage history for a profile +func (h *RatePlanHandler) GetUsageHistory(c *gin.Context) { + profileID := c.Query("profile_id") + if profileID == "" { + h.writeErrorResponse(c, http.StatusBadRequest, "Profile ID is required") + return + } + + limit := 10 // default limit + if limitStr := c.Query("limit"); limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { + limit = parsedLimit + } + } + + usageHistory, err := h.service.GetUsageHistory(c.Request.Context(), profileID, limit) + if err != nil { + h.logger.WithError(err).Error("Failed to get usage history") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get usage history") + return + } + + response := UsageHistoryResponse{ + Success: true, + Data: usageHistory, + } + + h.writeJSONResponse(c, http.StatusOK, response) +} + +// CalculateCostRequest represents the request to calculate cost +type CalculateCostRequest struct { + RatePlanID string `json:"rate_plan_id" binding:"required"` + DataUsed int64 `json:"data_used" binding:"required,min=0"` + VoiceUsed int64 `json:"voice_used" binding:"required,min=0"` + SMSUsed int64 `json:"sms_used" binding:"required,min=0"` + AppliedDiscounts []string `json:"applied_discounts,omitempty"` +} + +// CostCalculationResponse represents the response for cost calculation +type CostCalculationResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data *rateplan.RatePlanCostCalculation `json:"data,omitempty"` +} + +// CalculateCost handles calculating cost for a rate plan based on usage +func (h *RatePlanHandler) CalculateCost(c *gin.Context) { + var req CalculateCostRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error()) + return + } + + calcReq := &rateplan.CalculateCostRequest{ + RatePlanID: req.RatePlanID, + DataUsed: req.DataUsed, + VoiceUsed: req.VoiceUsed, + SMSUsed: req.SMSUsed, + AppliedDiscounts: req.AppliedDiscounts, + } + + calculation, err := h.service.CalculateCost(c.Request.Context(), calcReq) + if err != nil { + h.logger.WithError(err).Error("Failed to calculate cost") + h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to calculate cost: "+err.Error()) + return + } + + response := CostCalculationResponse{ + Success: true, + Data: calculation, + } + + h.writeJSONResponse(c, http.StatusOK, response) +}