mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-11 06:25:13 +00:00
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:
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user