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
This commit is contained in:
nutcas3
2026-05-01 23:38:30 +03:00
parent 536cc7f83a
commit b13d9132fd
5 changed files with 1222 additions and 0 deletions
@@ -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")
}
@@ -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)
}
@@ -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")
}
@@ -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)
}
@@ -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)
}