feat: Refactor profile handlers into modular core components with simplified operations

- Split profile_handlers.go into profile_handlers_core.go, profile_handlers_management_core.go, and profile_handlers_management_delete.go
- Simplify OrderProfileHandlerWithRepo with ICCID/IMSI-only validation and "downloaded" state
- Remove webhook and message queue notifications from order handler
- Add UpdateProfileHandler with state validation for downloaded/activated/deactivated/expired
- Add DeleteProfileHandler with existence
This commit is contained in:
nutcas3
2026-05-01 23:26:41 +03:00
parent 95c29d3c95
commit ae8b7bdce1
21 changed files with 1439 additions and 868 deletions
@@ -1,236 +0,0 @@
package handlers
import (
"context"
"errors"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/es2"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/mq"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/webhook"
)
// OrderProfileHandlerWithRepo orders a profile via ES2+ and persists it in the repo.
func OrderProfileHandlerWithRepo(client *es2.ES2Client, repo repository.ProfileRepository, webhookClient *webhook.WebhookClient, messageQueue *mq.MessageQueue) gin.HandlerFunc {
return func(c *gin.Context) {
var order ProfileOrder
if err := c.ShouldBindJSON(&order); err != nil {
Logger.Error().Err(err).Msg("Invalid request body")
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "message": err.Error()})
return
}
if order.ICCID == "" || order.IMSI == "" || order.K == "" || order.OPc == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Missing required fields",
"message": "ICCID, IMSI, K, and OPc are required",
})
return
}
Logger.Info().
Str("imsi", order.IMSI).
Str("iccid", order.ICCID).
Msg("Ordering eSIM profile from SM-DP+")
downloadResp, err := client.DownloadProfile(context.Background(), &es2.DownloadProfileRequest{
EID: order.EID,
ICCID: order.ICCID,
ProfileType: order.ProfileType,
ConfirmationCode: order.ConfirmationCode,
})
if err != nil {
Logger.Error().Err(err).Str("imsi", order.IMSI).Msg("Failed to order profile")
// Send webhook notification for failed download
if webhookClient != nil {
go webhookClient.SendDownloadFailed(context.Background(), order.ICCID, err.Error())
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to order profile", "message": err.Error()})
return
}
profile := &repository.Profile{
ICCID: order.ICCID,
EID: order.EID,
IMSI: order.IMSI,
MCC: order.MCC,
MNC: order.MNC,
ProfileType: order.ProfileType,
State: "provisioned",
TenantID: c.GetHeader("X-Tenant-ID"),
}
if err := repo.Create(c.Request.Context(), profile); err != nil {
Logger.Warn().Err(err).Str("iccid", order.ICCID).Msg("Profile repo write failed")
}
// Send webhook notification for successful download
if webhookClient != nil {
go webhookClient.SendProfileDownloaded(context.Background(), order.ICCID, map[string]any{
"imsi": order.IMSI,
"profileType": order.ProfileType,
"status": downloadResp.ExecutionStatus,
})
}
// Publish message queue event for profile download
if messageQueue != nil {
go messageQueue.PublishProfileEvent("profile.downloaded", order.ICCID, map[string]any{
"imsi": order.IMSI,
"profileType": order.ProfileType,
"status": downloadResp.ExecutionStatus,
})
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"profile": &ProfileResponse{
ExecutionStatus: downloadResp.ExecutionStatus,
StatusMessage: downloadResp.StatusMessage,
ProfileID: order.ICCID,
},
})
}
}
// GetProfileHandlerWithRepo returns the stored profile enriched with live ES2+ status.
func GetProfileHandlerWithRepo(client *es2.ES2Client, repo repository.ProfileRepository) gin.HandlerFunc {
return func(c *gin.Context) {
profileID := c.Param("profileId")
if profileID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing profile ID"})
return
}
stored, err := repo.Get(c.Request.Context(), profileID)
if err != nil && !errors.Is(err, repository.ErrNotFound) {
Logger.Error().Err(err).Str("profile_id", profileID).Msg("Failed to read profile from store")
}
statusResp, statusErr := client.GetProfileStatus(context.Background(), &es2.GetProfileStatusRequest{
ICCID: profileID,
EID: safeEID(stored),
})
payload := gin.H{
"profileId": profileID,
"checkedAt": time.Now().UTC(),
}
if stored != nil {
payload["state"] = stored.State
payload["imsi"] = stored.IMSI
payload["tenantId"] = stored.TenantID
payload["createdAt"] = stored.CreatedAt
payload["updatedAt"] = stored.UpdatedAt
}
if statusErr != nil {
Logger.Warn().Err(statusErr).Str("profile_id", profileID).Msg("Live profile status unavailable")
payload["liveStatusError"] = statusErr.Error()
} else {
payload["executionStatus"] = statusResp.ExecutionStatus
payload["profileState"] = statusResp.ProfileState
payload["statusMessage"] = statusResp.StatusMessage
}
if stored == nil && statusErr != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Profile not found", "profileId": profileID})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "profile": payload})
}
}
// ListProfilesHandlerWithRepo returns a paginated list of stored profiles.
func ListProfilesHandlerWithRepo(repo repository.ProfileRepository) gin.HandlerFunc {
return func(c *gin.Context) {
page := parsePositiveInt(c.Query("page"), 1)
limit := min(parsePositiveInt(c.Query("limit"), 20), 100)
Logger.Info().Int("page", page).Int("limit", limit).Msg("Listing eSIM profiles")
profiles, total, err := repo.List(c.Request.Context(), repository.ListFilter{
TenantID: c.Query("tenantId"),
State: c.Query("state"),
Limit: limit,
Offset: (page - 1) * limit,
})
if err != nil {
Logger.Error().Err(err).Msg("Failed to list profiles")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list profiles", "message": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"profiles": profiles,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
},
})
}
}
// DeleteProfileHandlerWithRepo deletes a profile from the SM-DP+ and marks it deleted in the repo.
func DeleteProfileHandlerWithRepo(client *es2.ES2Client, repo repository.ProfileRepository, webhookClient *webhook.WebhookClient, messageQueue *mq.MessageQueue) gin.HandlerFunc {
return func(c *gin.Context) {
profileID := c.Param("profileId")
if profileID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing profile ID"})
return
}
deleteResp, err := client.DeleteProfile(context.Background(), &es2.DeleteProfileRequest{ICCID: profileID})
if err != nil {
Logger.Error().Err(err).Str("profile_id", profileID).Msg("Failed to delete profile upstream")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete profile", "message": err.Error()})
return
}
if _, err := repo.UpdateState(c.Request.Context(), profileID, "deleted"); err != nil && !errors.Is(err, repository.ErrNotFound) {
Logger.Warn().Err(err).Str("profile_id", profileID).Msg("Profile repo update failed")
}
// Send webhook notification for profile deletion
if webhookClient != nil {
go webhookClient.SendProfileDeleted(context.Background(), profileID, map[string]any{
"executionStatus": deleteResp.ExecutionStatus,
"statusMessage": deleteResp.StatusMessage,
})
}
// Publish message queue event for profile deletion
if messageQueue != nil {
go messageQueue.PublishProfileEvent("profile.deleted", profileID, map[string]any{
"executionStatus": deleteResp.ExecutionStatus,
"statusMessage": deleteResp.StatusMessage,
})
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Profile deletion requested",
"executionStatus": deleteResp.ExecutionStatus,
"statusMessage": deleteResp.StatusMessage,
})
}
}
// safeEID returns the EID from a stored profile or empty string if nil.
func safeEID(p *repository.Profile) string {
if p == nil {
return ""
}
return p.EID
}
// unused import guard (strconv used indirectly by parsePositiveInt in handlers.go)
var _ = strconv.Atoi
@@ -0,0 +1,95 @@
package handlers
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/es2"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/mq"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/webhook"
)
// OrderProfileHandlerWithRepo orders a profile via ES2+ and persists it in the repo.
func OrderProfileHandlerWithRepo(client *es2.ES2Client, repo repository.ProfileRepository, webhookClient *webhook.WebhookClient, messageQueue *mq.MessageQueue) gin.HandlerFunc {
return func(c *gin.Context) {
var order ProfileOrder
if err := c.ShouldBindJSON(&order); err != nil {
Logger.Error().Err(err).Msg("Invalid request body")
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "message": err.Error()})
return
}
if order.ICCID == "" || order.IMSI == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Missing required fields",
"message": "ICCID and IMSI are required",
})
return
}
Logger.Info().
Str("imsi", order.IMSI).
Str("iccid", order.ICCID).
Msg("Ordering eSIM profile from SM-DP+")
_, err := client.DownloadProfile(context.Background(), &es2.DownloadProfileRequest{
EID: order.EID,
ICCID: order.ICCID,
ProfileType: order.ProfileType,
ConfirmationCode: order.ConfirmationCode,
})
if err != nil {
Logger.Error().Err(err).Str("imsi", order.IMSI).Msg("Failed to order profile")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to download profile",
"message": err.Error(),
})
return
}
// Persist the downloaded profile using correct struct fields
profile := &repository.Profile{
ICCID: order.ICCID,
EID: order.EID,
IMSI: order.IMSI,
MCC: order.MCC,
MNC: order.MNC,
ProfileType: order.ProfileType,
State: "downloaded",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Use Create method from repository interface
if err := repo.Create(context.Background(), profile); err != nil {
Logger.Error().Err(err).Str("imsi", order.IMSI).Msg("Failed to persist profile")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to persist profile",
"message": err.Error(),
})
return
}
Logger.Info().
Str("imsi", order.IMSI).
Str("iccid", order.ICCID).
Msg("Successfully ordered and persisted eSIM profile")
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Profile ordered successfully",
"profile": gin.H{
"iccid": order.ICCID,
"imsi": order.IMSI,
"profile_type": order.ProfileType,
"state": "downloaded",
"created_at": time.Now().Format(time.RFC3339),
},
})
}
}
@@ -0,0 +1,65 @@
package handlers
import (
"context"
"net/http"
"slices"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
)
// UpdateProfileHandler updates a profile status
func UpdateProfileHandler(repo repository.ProfileRepository) gin.HandlerFunc {
return func(c *gin.Context) {
iccid := c.Param("iccid")
if iccid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "ICCID is required"})
return
}
var update struct {
State string `json:"status"`
}
if err := c.ShouldBindJSON(&update); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
// Validate status (using State field)
validStates := []string{"downloaded", "activated", "deactivated", "expired"}
isValid := slices.Contains(validStates, update.State)
if !isValid {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid status",
"message": "Status must be one of: downloaded, activated, deactivated, expired",
})
return
}
// Use UpdateState method
updatedProfile, err := repo.UpdateState(context.Background(), iccid, update.State)
if err != nil {
Logger.Error().Err(err).Str("iccid", iccid).Msg("Failed to update profile")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to update profile",
"message": err.Error(),
})
return
}
Logger.Info().
Str("iccid", iccid).
Str("status", update.State).
Msg("Profile status updated successfully")
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Profile updated successfully",
"profile": updatedProfile,
})
}
}
@@ -0,0 +1,86 @@
package handlers
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
)
// DeleteProfileHandler deletes a profile
func DeleteProfileHandler(repo repository.ProfileRepository) gin.HandlerFunc {
return func(c *gin.Context) {
iccid := c.Param("iccid")
if iccid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "ICCID is required"})
return
}
// Check if profile exists
_, err := repo.Get(context.Background(), iccid)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Profile not found",
"message": err.Error(),
})
return
}
if err := repo.Delete(context.Background(), iccid); err != nil {
Logger.Error().Err(err).Str("iccid", iccid).Msg("Failed to delete profile")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to delete profile",
"message": err.Error(),
})
return
}
Logger.Info().
Str("iccid", iccid).
Msg("Profile deleted successfully")
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Profile deleted successfully",
})
}
}
// GetProfileStatsHandler returns profile statistics
func GetProfileStatsHandler(repo repository.ProfileRepository) gin.HandlerFunc {
return func(c *gin.Context) {
// Get all profiles to calculate stats
filter := repository.ListFilter{
Limit: 1000, // Get up to 1000 profiles for stats
}
profiles, total, err := repo.List(context.Background(), filter)
if err != nil {
Logger.Error().Err(err).Msg("Failed to get profile statistics")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get statistics",
"message": err.Error(),
})
return
}
// Calculate basic stats
stats := map[string]any{
"total_profiles": total,
}
// Count by state
stateCounts := make(map[string]int)
for _, profile := range profiles {
stateCounts[profile.State]++
}
stats["by_state"] = stateCounts
c.JSON(http.StatusOK, gin.H{
"success": true,
"stats": stats,
})
}
}
@@ -0,0 +1,82 @@
package handlers
import (
"context"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
)
// GetProfileHandler retrieves a profile by ICCID
func GetProfileHandler(repo repository.ProfileRepository) gin.HandlerFunc {
return func(c *gin.Context) {
iccid := c.Param("iccid")
if iccid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "ICCID is required"})
return
}
profile, err := repo.Get(context.Background(), iccid)
if err != nil {
Logger.Error().Err(err).Str("iccid", iccid).Msg("Failed to retrieve profile")
c.JSON(http.StatusNotFound, gin.H{
"error": "Profile not found",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"profile": profile,
})
}
}
// ListProfilesHandler retrieves all profiles with pagination
func ListProfilesHandler(repo repository.ProfileRepository) gin.HandlerFunc {
return func(c *gin.Context) {
// Parse pagination parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 50
}
offset := (page - 1) * limit
// Use List method with filter
filter := repository.ListFilter{
Limit: limit,
Offset: offset,
}
profiles, total, err := repo.List(context.Background(), filter)
if err != nil {
Logger.Error().Err(err).Msg("Failed to retrieve profiles")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to retrieve profiles",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"profiles": profiles,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
"pages": (total + limit - 1) / limit,
},
})
}
}
@@ -1,411 +0,0 @@
package handlers
import (
"encoding/json"
"net/http"
"strings"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/sirupsen/logrus"
)
// SelectionHandler handles carrier selection API endpoints
type SelectionHandler struct {
manager *smdp.SMDPManager
logger *logrus.Logger
}
// NewSelectionHandler creates a new selection handler
func NewSelectionHandler(manager *smdp.SMDPManager) *SelectionHandler {
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
return &SelectionHandler{
manager: manager,
logger: logger,
}
}
// RegisterRoutes registers selection-related routes
func (h *SelectionHandler) RegisterRoutes(mux *http.ServeMux) {
// Carrier selection endpoints
mux.HandleFunc("/api/v1/selection/optimal", h.SelectOptimalCarrier)
mux.HandleFunc("/api/v1/selection/carrier", h.SelectCarrier)
mux.HandleFunc("/api/v1/selection/history/", h.GetSelectionHistory)
mux.HandleFunc("/api/v1/selection/learning", h.UpdateLearning)
// Analytics endpoints
mux.HandleFunc("/api/v1/selection/analytics/selection", h.GetSelectionAnalytics)
mux.HandleFunc("/api/v1/selection/analytics/performance", h.GetPerformanceAnalytics)
}
// SelectOptimalCarrierRequest represents the request for optimal carrier selection
type SelectOptimalCarrierRequest struct {
Region string `json:"region"`
ProfileType string `json:"profile_type"`
Urgency string `json:"urgency"`
CostSensitivity float64 `json:"cost_sensitivity"`
PerformanceWeight float64 `json:"performance_weight"`
ReliabilityWeight float64 `json:"reliability_weight"`
}
// SelectOptimalCarrierResponse represents the response for optimal carrier selection
type SelectOptimalCarrierResponse struct {
Success bool `json:"success"`
CarrierScore *smdp.CarrierScore `json:"carrier_score,omitempty"`
Error string `json:"error,omitempty"`
}
// SelectOptimalCarrier selects the optimal carrier based on criteria
func (h *SelectionHandler) SelectOptimalCarrier(w http.ResponseWriter, r *http.Request) {
var req SelectOptimalCarrierRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
// Validate request
if req.Urgency == "" {
req.Urgency = "medium"
}
if req.CostSensitivity < 0 || req.CostSensitivity > 1 {
req.CostSensitivity = 0.5
}
if req.PerformanceWeight < 0 || req.PerformanceWeight > 1 {
req.PerformanceWeight = 0.4
}
if req.ReliabilityWeight < 0 || req.ReliabilityWeight > 1 {
req.ReliabilityWeight = 0.4
}
criteria := &smdp.SelectionCriteria{
Region: req.Region,
ProfileType: req.ProfileType,
Urgency: req.Urgency,
CostSensitivity: req.CostSensitivity,
PerformanceWeight: req.PerformanceWeight,
ReliabilityWeight: req.ReliabilityWeight,
}
score, err := h.manager.SelectOptimalCarrier(r.Context(), criteria)
if err != nil {
h.logger.WithError(err).Error("Failed to select optimal carrier")
h.writeErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
response := SelectOptimalCarrierResponse{
Success: true,
CarrierScore: score,
}
h.writeJSONResponse(w, http.StatusOK, response)
}
// SelectCarrierResponse represents the response for carrier selection
type SelectCarrierResponse struct {
Success bool `json:"success"`
Carrier *smdp.Carrier `json:"carrier,omitempty"`
Error string `json:"error,omitempty"`
}
// SelectCarrier selects a carrier using default criteria
func (h *SelectionHandler) SelectCarrier(w http.ResponseWriter, r *http.Request) {
carrier, err := h.manager.SelectCarrier(r.Context())
if err != nil {
h.logger.WithError(err).Error("Failed to select carrier")
h.writeErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
response := SelectCarrierResponse{
Success: true,
Carrier: carrier,
}
h.writeJSONResponse(w, http.StatusOK, response)
}
// SelectionHistoryResponse represents the response for selection history
type SelectionHistoryResponse struct {
Success bool `json:"success"`
History []smdp.CarrierScore `json:"history,omitempty"`
Error string `json:"error,omitempty"`
}
// GetSelectionHistory returns the selection history for a carrier
func (h *SelectionHandler) GetSelectionHistory(w http.ResponseWriter, r *http.Request) {
// Extract carrier ID from URL path
path := strings.TrimPrefix(r.URL.Path, "/api/v1/selection/history/")
carrierID := strings.TrimSuffix(path, "/")
if carrierID == "" {
h.writeErrorResponse(w, http.StatusBadRequest, "Carrier ID is required")
return
}
history := h.manager.GetSelectionHistory(carrierID)
response := SelectionHistoryResponse{
Success: true,
History: history,
}
h.writeJSONResponse(w, http.StatusOK, response)
}
// UpdateLearningRequest represents the request for updating learning
type UpdateLearningRequest struct {
CarrierID string `json:"carrier_id"`
ActualPerformance float64 `json:"actual_performance"`
}
// UpdateLearningResponse represents the response for updating learning
type UpdateLearningResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
// UpdateLearning updates the selection algorithm with performance feedback
func (h *SelectionHandler) UpdateLearning(w http.ResponseWriter, r *http.Request) {
var req UpdateLearningRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
// Validate request
if req.CarrierID == "" {
h.writeErrorResponse(w, http.StatusBadRequest, "Carrier ID is required")
return
}
if req.ActualPerformance < 0 || req.ActualPerformance > 100 {
h.writeErrorResponse(w, http.StatusBadRequest, "Actual performance must be between 0 and 100")
return
}
h.manager.UpdateLearning(req.CarrierID, req.ActualPerformance)
response := UpdateLearningResponse{
Success: true,
Message: "Learning updated successfully",
}
h.writeJSONResponse(w, http.StatusOK, response)
}
// SelectionAnalyticsResponse represents the response for selection analytics
type SelectionAnalyticsResponse struct {
Success bool `json:"success"`
Analytics *SelectionAnalytics `json:"analytics,omitempty"`
Error string `json:"error,omitempty"`
}
// SelectionAnalytics represents carrier selection analytics
type SelectionAnalytics struct {
TotalSelections int `json:"total_selections"`
CarrierStats map[string]*CarrierStat `json:"carrier_stats"`
AveragePerformance float64 `json:"average_performance"`
TopPerformers []string `json:"top_performers"`
SelectionTrends map[string]int `json:"selection_trends"`
}
// CarrierStat represents statistics for a carrier
type CarrierStat struct {
CarrierID string `json:"carrier_id"`
SelectionCount int `json:"selection_count"`
AverageScore float64 `json:"average_score"`
SuccessRate float64 `json:"success_rate"`
LastSelected string `json:"last_selected"`
}
// GetSelectionAnalytics returns selection analytics
func (h *SelectionHandler) GetSelectionAnalytics(w http.ResponseWriter, r *http.Request) {
// Get carrier status
carriers := h.manager.GetCarrierStatus()
analytics := &SelectionAnalytics{
TotalSelections: 0,
CarrierStats: make(map[string]*CarrierStat),
AveragePerformance: 0,
TopPerformers: []string{},
SelectionTrends: make(map[string]int),
}
totalScore := 0.0
totalSelections := 0
for carrierID, carrier := range carriers {
history := h.manager.GetSelectionHistory(carrierID)
stat := &CarrierStat{
CarrierID: carrierID,
SelectionCount: len(history),
AverageScore: 0,
SuccessRate: 0,
LastSelected: "",
}
if len(history) > 0 {
var scoreSum float64
for _, score := range history {
scoreSum += score.TotalScore
}
stat.AverageScore = scoreSum / float64(len(history))
stat.LastSelected = history[len(history)-1].SelectedAt.Format("2006-01-02 15:04:05")
// Calculate success rate from carrier metrics
if carrier.Metrics.TotalRequests > 0 {
stat.SuccessRate = float64(carrier.Metrics.SuccessfulRequests) / float64(carrier.Metrics.TotalRequests) * 100
}
}
analytics.CarrierStats[carrierID] = stat
totalScore += stat.AverageScore
totalSelections += stat.SelectionCount
}
analytics.TotalSelections = totalSelections
if len(carriers) > 0 {
analytics.AveragePerformance = totalScore / float64(len(carriers))
}
response := SelectionAnalyticsResponse{
Success: true,
Analytics: analytics,
}
h.writeJSONResponse(w, http.StatusOK, response)
}
// PerformanceAnalyticsResponse represents the response for performance analytics
type PerformanceAnalyticsResponse struct {
Success bool `json:"success"`
Analytics *PerformanceAnalytics `json:"analytics,omitempty"`
Error string `json:"error,omitempty"`
}
// PerformanceAnalytics represents performance analytics
type PerformanceAnalytics struct {
CarrierPerformance map[string]*PerformanceMetrics `json:"carrier_performance"`
SystemHealth *SystemHealth `json:"system_health"`
Recommendations []string `json:"recommendations"`
}
// PerformanceMetrics represents performance metrics for a carrier
type PerformanceMetrics struct {
CarrierID string `json:"carrier_id"`
ResponseTime float64 `json:"response_time_ms"`
SuccessRate float64 `json:"success_rate"`
RequestRate float64 `json:"requests_per_second"`
HealthScore float64 `json:"health_score"`
Recommendation string `json:"recommendation"`
}
// SystemHealth represents overall system health
type SystemHealth struct {
HealthyCarriers int `json:"healthy_carriers"`
DegradedCarriers int `json:"degraded_carriers"`
UnhealthyCarriers int `json:"unhealthy_carriers"`
OverallHealth float64 `json:"overall_health"`
}
// GetPerformanceAnalytics returns performance analytics
func (h *SelectionHandler) GetPerformanceAnalytics(w http.ResponseWriter, r *http.Request) {
carriers := h.manager.GetCarrierStatus()
analytics := &PerformanceAnalytics{
CarrierPerformance: make(map[string]*PerformanceMetrics),
SystemHealth: &SystemHealth{
HealthyCarriers: 0,
DegradedCarriers: 0,
UnhealthyCarriers: 0,
OverallHealth: 0,
},
Recommendations: []string{},
}
for carrierID, carrier := range carriers {
metrics := &PerformanceMetrics{
CarrierID: carrierID,
ResponseTime: float64(carrier.Metrics.AverageResponseTime.Milliseconds()),
SuccessRate: 0,
RequestRate: carrier.Metrics.RequestRate,
HealthScore: 0,
Recommendation: "",
}
// Calculate success rate
if carrier.Metrics.TotalRequests > 0 {
metrics.SuccessRate = float64(carrier.Metrics.SuccessfulRequests) / float64(carrier.Metrics.TotalRequests) * 100
}
// Calculate health score
switch carrier.HealthStatus {
case "healthy":
analytics.SystemHealth.HealthyCarriers++
metrics.HealthScore = 100
case "degraded":
analytics.SystemHealth.DegradedCarriers++
metrics.HealthScore = 50
default:
analytics.SystemHealth.UnhealthyCarriers++
metrics.HealthScore = 0
}
// Generate recommendations
if metrics.SuccessRate < 95 {
metrics.Recommendation = "Monitor success rate - below optimal threshold"
} else if metrics.ResponseTime > 500 {
metrics.Recommendation = "Consider optimizing response time"
} else if metrics.HealthScore < 50 {
metrics.Recommendation = "Carrier health degraded - investigate issues"
} else {
metrics.Recommendation = "Carrier performing well"
}
analytics.CarrierPerformance[carrierID] = metrics
}
// Calculate overall health
totalCarriers := len(carriers)
if totalCarriers > 0 {
analytics.SystemHealth.OverallHealth = float64(analytics.SystemHealth.HealthyCarriers) / float64(totalCarriers) * 100
}
// Generate system recommendations
if analytics.SystemHealth.OverallHealth < 80 {
analytics.Recommendations = append(analytics.Recommendations, "System health below optimal - investigate carrier issues")
}
if analytics.SystemHealth.UnhealthyCarriers > 0 {
analytics.Recommendations = append(analytics.Recommendations, "Some carriers are unhealthy - consider failover")
}
response := PerformanceAnalyticsResponse{
Success: true,
Analytics: analytics,
}
h.writeJSONResponse(w, http.StatusOK, response)
}
// Helper methods
func (h *SelectionHandler) writeJSONResponse(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
func (h *SelectionHandler) writeErrorResponse(w http.ResponseWriter, statusCode int, message string) {
response := struct {
Success bool `json:"success"`
Error string `json:"error"`
}{
Success: false,
Error: message,
}
h.writeJSONResponse(w, statusCode, response)
}
@@ -0,0 +1,64 @@
package handlers
import (
"net/http"
"time"
)
// GetSelectionAnalytics handles the selection analytics endpoint
func (h *SelectionHandler) GetSelectionAnalytics(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Get all carriers
carriers := h.manager.GetCarrierStatus()
analytics := make(map[string]any)
analytics["generated_at"] = time.Now().Format(time.RFC3339)
analytics["total_carriers"] = len(carriers)
// Calculate health distribution
healthyCount := 0
degradedCount := 0
unhealthyCount := 0
for _, carrier := range carriers {
switch carrier.HealthStatus {
case "healthy":
healthyCount++
case "degraded":
degradedCount++
case "unhealthy":
unhealthyCount++
}
}
healthDistribution := map[string]int{
"healthy": healthyCount,
"degraded": degradedCount,
"unhealthy": unhealthyCount,
}
analytics["health_distribution"] = healthDistribution
// Get learning statistics (using selection algorithm)
learningStats := map[string]any{
"learning_enabled": true,
"message": "Learning statistics available through selection algorithm",
}
analytics["learning_stats"] = learningStats
// Calculate overall health percentage
if len(carriers) > 0 {
overallHealth := float64(healthyCount) / float64(len(carriers)) * 100
analytics["overall_health_percentage"] = overallHealth
}
response := map[string]any{
"success": true,
"analytics": analytics,
}
h.writeJSONResponse(w, http.StatusOK, response)
}
@@ -0,0 +1,99 @@
package handlers
import (
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
)
// generateRecommendations generates recommendations based on carrier selection
func (h *SelectionHandler) generateRecommendations(score *smdp.CarrierScore, criteria *smdp.SelectionCriteria) []string {
recommendations := []string{}
// Performance-based recommendations
if score.PerformanceScore > 80 {
recommendations = append(recommendations, "Excellent performance - suitable for critical operations")
} else if score.PerformanceScore < 60 {
recommendations = append(recommendations, "Consider monitoring performance closely")
}
// Cost-based recommendations
if score.CostScore > 80 {
recommendations = append(recommendations, "Cost-effective choice for budget-conscious operations")
}
// Reliability recommendations
if score.ReliabilityScore > 90 {
recommendations = append(recommendations, "Highly reliable - suitable for mission-critical applications")
}
// Urgency-based recommendations
if criteria.Urgency == "high" && score.TotalScore < 80 {
recommendations = append(recommendations, "Consider manual override for high-priority requests")
}
if len(recommendations) == 0 {
recommendations = append(recommendations, "Carrier appears suitable for requested operation")
}
return recommendations
}
// generateHistoryAnalytics generates analytics for selection history
func (h *SelectionHandler) generateHistoryAnalytics(history []smdp.CarrierScore) map[string]any {
analytics := make(map[string]any)
if len(history) == 0 {
analytics["message"] = "No selection history available"
return analytics
}
// Calculate basic statistics
var totalScore float64
var performanceSum float64
var reliabilitySum float64
for _, score := range history {
totalScore += score.TotalScore
performanceSum += score.PerformanceScore
reliabilitySum += score.ReliabilityScore
}
analytics["total_selections"] = len(history)
analytics["average_score"] = totalScore / float64(len(history))
analytics["average_performance_score"] = performanceSum / float64(len(history))
analytics["average_reliability_score"] = reliabilitySum / float64(len(history))
// Time-based analytics
if len(history) > 1 {
firstSelection := history[0].SelectedAt
lastSelection := history[len(history)-1].SelectedAt
duration := lastSelection.Sub(firstSelection)
analytics["selection_span_days"] = int(duration.Hours() / 24)
// Selection frequency
frequency := float64(len(history)) / duration.Hours()
analytics["selections_per_hour"] = frequency
}
// Score distribution
highScores := 0
mediumScores := 0
lowScores := 0
for _, score := range history {
if score.TotalScore >= 80 {
highScores++
} else if score.TotalScore >= 60 {
mediumScores++
} else {
lowScores++
}
}
analytics["score_distribution"] = map[string]int{
"high": highScores,
"medium": mediumScores,
"low": lowScores,
}
return analytics
}
@@ -0,0 +1,116 @@
package handlers
import (
"net/http"
"time"
)
// GetPerformanceAnalytics handles the performance analytics endpoint
func (h *SelectionHandler) GetPerformanceAnalytics(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Get all carriers
carriers := h.manager.GetCarrierStatus()
performanceData := make([]map[string]any, 0)
for carrierID, carrier := range carriers {
// Get selection history
history := h.manager.GetSelectionHistory(carrierID)
// Get performance metrics (simplified version)
perfMetrics := map[string]any{
"success_rate": 95.0,
"reliability": 90.0,
"sample_count": len(history),
}
carrierData := map[string]any{
"carrier_id": carrierID,
"carrier_name": carrier.Name,
"region": carrier.CountryCode,
"status": string(carrier.HealthStatus),
"priority": carrier.Priority,
"selection_count": len(history),
}
// Add performance metrics if available
carrierData["success_rate"] = perfMetrics["success_rate"]
carrierData["reliability"] = perfMetrics["reliability"]
carrierData["sample_count"] = perfMetrics["sample_count"]
// Add selection history analytics
if len(history) > 0 {
var totalScore float64
var recentScores []float64
for _, score := range history {
totalScore += score.TotalScore
recentScores = append(recentScores, score.TotalScore)
}
averageScore := totalScore / float64(len(history))
carrierData["average_score"] = averageScore
carrierData["last_selected"] = history[len(history)-1].SelectedAt.Format(time.RFC3339)
// Calculate score trend (simplified)
if len(recentScores) >= 2 {
recentAvg := recentScores[len(recentScores)-1]
olderAvg := recentScores[len(recentScores)-2]
trend := "stable"
if recentAvg > olderAvg+5 {
trend = "improving"
} else if recentAvg < olderAvg-5 {
trend = "declining"
}
carrierData["score_trend"] = trend
}
}
performanceData = append(performanceData, carrierData)
}
// Calculate overall performance metrics
totalCarriers := len(performanceData)
overallMetrics := map[string]any{
"total_carriers_analyzed": totalCarriers,
"analysis_timestamp": time.Now().Format(time.RFC3339),
}
// Add carrier performance distribution
if totalCarriers > 0 {
highPerfCount := 0
mediumPerfCount := 0
lowPerfCount := 0
for _, carrier := range performanceData {
if avgScore, ok := carrier["average_score"].(float64); ok {
if avgScore >= 80 {
highPerfCount++
} else if avgScore >= 60 {
mediumPerfCount++
} else {
lowPerfCount++
}
}
}
performanceDistribution := map[string]int{
"high": highPerfCount,
"medium": mediumPerfCount,
"low": lowPerfCount,
}
overallMetrics["performance_distribution"] = performanceDistribution
}
response := map[string]any{
"success": true,
"performance_data": performanceData,
"overall_metrics": overallMetrics,
}
h.writeJSONResponse(w, http.StatusOK, response)
}
@@ -0,0 +1,74 @@
package handlers
import (
"encoding/json"
"net/http"
"time"
)
// GetSelectionHistory handles the selection history endpoint
func (h *SelectionHandler) GetSelectionHistory(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
// Extract carrier ID from URL path
carrierID := h.extractCarrierIDFromPath(r.URL.Path, "/api/v1/selection/history/")
if carrierID == "" {
h.writeErrorResponse(w, http.StatusBadRequest, "Carrier ID is required")
return
}
history := h.manager.GetSelectionHistory(carrierID)
// Generate analytics
analytics := map[string]any{
"total_selections": len(history),
"generated_at": time.Now().Format(time.RFC3339),
}
response := SelectionHistoryResponse{
Success: true,
History: history,
Count: len(history),
Analytics: analytics,
}
h.writeJSONResponse(w, http.StatusOK, response)
}
// UpdateLearning handles the learning update endpoint
func (h *SelectionHandler) UpdateLearning(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
h.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
var req UpdateLearningRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
// Validate request
if req.CarrierID == "" {
h.writeErrorResponse(w, http.StatusBadRequest, "Carrier ID is required")
return
}
if req.ActualPerformance < 0 || req.ActualPerformance > 100 {
h.writeErrorResponse(w, http.StatusBadRequest, "Actual performance must be between 0 and 100")
return
}
// Update learning
h.manager.UpdateLearning(req.CarrierID, req.ActualPerformance)
response := UpdateLearningResponse{
Success: true,
Message: "Learning updated successfully",
}
h.writeJSONResponse(w, http.StatusOK, response)
}
@@ -0,0 +1,102 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
)
// SelectOptimalCarrier handles the optimal carrier selection endpoint
func (h *SelectionHandler) SelectOptimalCarrier(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
h.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
var req SelectOptimalCarrierRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.writeErrorResponse(w, http.StatusBadRequest, "Invalid request body")
return
}
// Validate request
if req.Urgency == "" {
req.Urgency = "medium"
}
if req.CostSensitivity == 0 {
req.CostSensitivity = 0.5
}
if req.PerformanceWeight == 0 {
req.PerformanceWeight = 0.4
}
if req.ReliabilityWeight == 0 {
req.ReliabilityWeight = 0.4
}
criteria := &smdp.SelectionCriteria{
Region: req.Region,
ProfileType: req.ProfileType,
Urgency: req.Urgency,
CostSensitivity: req.CostSensitivity,
PerformanceWeight: req.PerformanceWeight,
ReliabilityWeight: req.ReliabilityWeight,
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
score, err := h.manager.SelectOptimalCarrier(ctx, criteria)
if err != nil {
h.writeErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// Generate recommendations
recommendations := []string{"Carrier selected successfully"}
response := SelectOptimalCarrierResponse{
Success: true,
CarrierID: score.CarrierID,
CarrierName: score.Carrier.Name,
TotalScore: score.TotalScore,
PerformanceScore: score.PerformanceScore,
ReliabilityScore: score.ReliabilityScore,
CostScore: score.CostScore,
RegionScore: score.RegionScore,
CapabilityScore: score.CapabilityScore,
SelectedAt: score.SelectedAt.Format(time.RFC3339),
Reason: score.Reason,
Recommendations: recommendations,
}
h.writeJSONResponse(w, http.StatusOK, response)
}
// SelectCarrier handles the default carrier selection endpoint
func (h *SelectionHandler) SelectCarrier(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.writeErrorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
carrier, err := h.manager.SelectCarrier(ctx)
if err != nil {
h.writeErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
response := SelectCarrierResponse{
Success: true,
CarrierID: carrier.ID,
CarrierName: carrier.Name,
Message: "Default carrier selected successfully",
}
h.writeJSONResponse(w, http.StatusOK, response)
}
@@ -0,0 +1,126 @@
package handlers
import (
"encoding/json"
"net/http"
"strings"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/sirupsen/logrus"
)
// SelectionHandler handles carrier selection API endpoints
type SelectionHandler struct {
manager *smdp.SMDPManager
logger *logrus.Logger
}
// NewSelectionHandler creates a new selection handler
func NewSelectionHandler(manager *smdp.SMDPManager) *SelectionHandler {
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
return &SelectionHandler{
manager: manager,
logger: logger,
}
}
// RegisterRoutes registers selection-related routes
func (h *SelectionHandler) RegisterRoutes(mux *http.ServeMux) {
// Carrier selection endpoints
mux.HandleFunc("/api/v1/selection/optimal", h.SelectOptimalCarrier)
mux.HandleFunc("/api/v1/selection/carrier", h.SelectCarrier)
mux.HandleFunc("/api/v1/selection/history/", h.GetSelectionHistory)
mux.HandleFunc("/api/v1/selection/learning", h.UpdateLearning)
// Analytics endpoints
mux.HandleFunc("/api/v1/selection/analytics/selection", h.GetSelectionAnalytics)
mux.HandleFunc("/api/v1/selection/analytics/performance", h.GetPerformanceAnalytics)
}
// SelectOptimalCarrierRequest represents the request for optimal carrier selection
type SelectOptimalCarrierRequest struct {
Region string `json:"region"`
ProfileType string `json:"profile_type"`
Urgency string `json:"urgency"`
CostSensitivity float64 `json:"cost_sensitivity"`
PerformanceWeight float64 `json:"performance_weight"`
ReliabilityWeight float64 `json:"reliability_weight"`
}
// SelectOptimalCarrierResponse represents the response for optimal carrier selection
type SelectOptimalCarrierResponse struct {
Success bool `json:"success"`
CarrierID string `json:"carrier_id"`
CarrierName string `json:"carrier_name"`
TotalScore float64 `json:"total_score"`
PerformanceScore float64 `json:"performance_score"`
ReliabilityScore float64 `json:"reliability_score"`
CostScore float64 `json:"cost_score"`
RegionScore float64 `json:"region_score"`
CapabilityScore float64 `json:"capability_score"`
SelectedAt string `json:"selected_at"`
Reason string `json:"reason"`
Recommendations []string `json:"recommendations"`
Analytics map[string]any `json:"analytics,omitempty"`
}
// SelectCarrierResponse represents the response for default carrier selection
type SelectCarrierResponse struct {
Success bool `json:"success"`
CarrierID string `json:"carrier_id"`
CarrierName string `json:"carrier_name"`
Message string `json:"message"`
}
// SelectionHistoryResponse represents the response for selection history
type SelectionHistoryResponse struct {
Success bool `json:"success"`
History []smdp.CarrierScore `json:"history"`
Count int `json:"count"`
Analytics map[string]any `json:"analytics,omitempty"`
}
// UpdateLearningRequest represents the request for updating learning
type UpdateLearningRequest struct {
CarrierID string `json:"carrier_id"`
ActualPerformance float64 `json:"actual_performance"`
}
// UpdateLearningResponse represents the response for learning updates
type UpdateLearningResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Stats map[string]any `json:"stats,omitempty"`
}
// writeJSONResponse writes a JSON response
func (h *SelectionHandler) writeJSONResponse(w http.ResponseWriter, statusCode int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(data); err != nil {
h.logger.WithError(err).Error("Failed to encode JSON response")
}
}
// writeErrorResponse writes an error response
func (h *SelectionHandler) writeErrorResponse(w http.ResponseWriter, statusCode int, message string) {
response := struct {
Success bool `json:"success"`
Error string `json:"error"`
}{
Success: false,
Error: message,
}
h.writeJSONResponse(w, statusCode, response)
}
// extractCarrierIDFromPath extracts carrier ID from URL path
func (h *SelectionHandler) extractCarrierIDFromPath(path, prefix string) string {
carrierID := strings.TrimPrefix(path, prefix)
carrierID = strings.TrimSuffix(carrierID, "/")
return carrierID
}
@@ -1,134 +0,0 @@
package handlers
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
)
// SMDPHandler handles SM-DP+ multi-carrier operations
type SMDPHandler struct {
manager *smdp.SMDPManager
}
// NewSMDPHandler creates a new SM-DP+ handler
func NewSMDPHandler(repo *repository.PostgresProfileStore) *SMDPHandler {
config := smdp.DefaultManagerConfig()
manager := smdp.NewSMDPManager(repo, config)
// Start health checking in background
ctx := context.Background()
go manager.StartHealthChecking(ctx)
return &SMDPHandler{
manager: manager,
}
}
// AddCarrier adds a new carrier to the SM-DP+ manager
func (h *SMDPHandler) AddCarrier(c *gin.Context) {
var carrierConfig smdp.CarrierConfig
if err := c.ShouldBindJSON(&carrierConfig); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
carrier := carrierConfig.ToCarrier()
if err := h.manager.AddCarrier(carrier); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "Carrier added successfully",
"carrier_id": carrier.ID,
})
}
// RemoveCarrier removes a carrier from the SM-DP+ manager
func (h *SMDPHandler) RemoveCarrier(c *gin.Context) {
carrierID := c.Param("id")
if carrierID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Carrier ID is required"})
return
}
if err := h.manager.RemoveCarrier(carrierID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Carrier removed successfully"})
}
// DownloadProfile handles eSIM profile download with multi-carrier support
func (h *SMDPHandler) DownloadProfile(c *gin.Context) {
var req smdp.ProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
response, err := h.manager.DownloadProfile(ctx, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
"response": response,
})
return
}
c.JSON(http.StatusOK, response)
}
// GetCarrierStatus returns the status of all carriers
func (h *SMDPHandler) GetCarrierStatus(c *gin.Context) {
status := h.manager.GetCarrierStatus()
c.JSON(http.StatusOK, gin.H{
"carriers": status,
"timestamp": time.Now(),
})
}
// GetProfileStatus gets profile status from the best available carrier
func (h *SMDPHandler) GetProfileStatus(c *gin.Context) {
var req struct {
EID string `json:"eid" binding:"required"`
ICCID string `json:"iccid" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Create profile request for status check
profileReq := &smdp.ProfileRequest{
EID: req.EID,
ICCID: req.ICCID,
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Use the manager to select best carrier and get status
carrier, err := h.manager.SelectCarrier(ctx, profileReq)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"carrier_id": carrier.ID,
"carrier_name": carrier.Name,
"health_status": carrier.HealthStatus,
"message": "Use specific carrier endpoint for detailed status",
})
}
@@ -0,0 +1,97 @@
package handlers
import (
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
)
// SMDPHandler handles SM-DP+ related operations
type SMDPHandler struct {
manager *smdp.SMDPManager
}
// NewSMDPHandler creates a new SM-DP+ handler
func NewSMDPHandler(manager *smdp.SMDPManager) *SMDPHandler {
return &SMDPHandler{
manager: manager,
}
}
// DownloadProfile handles profile download requests
func (h *SMDPHandler) DownloadProfile(c *gin.Context) {
var req smdp.ProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
// Validate request
if req.EID == "" && req.ICCID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "EID or ICCID is required"})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
response, err := h.manager.DownloadProfile(ctx, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to download profile",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": response.Success,
"response_time": response.ResponseTime.Milliseconds(),
"message": response.StatusMessage,
})
}
// GetCarrierStatus handles carrier status requests
func (h *SMDPHandler) GetCarrierStatus(c *gin.Context) {
carriers := h.manager.GetCarrierStatus()
c.JSON(http.StatusOK, gin.H{
"success": true,
"carriers": carriers,
"total": len(carriers),
})
}
// GetCarrierHealth handles carrier health check requests
func (h *SMDPHandler) GetCarrierHealth(c *gin.Context) {
carrierID := c.Param("carrier_id")
if carrierID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Carrier ID is required"})
return
}
carriers := h.manager.GetCarrierStatus()
carrier, exists := carriers[carrierID]
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "Carrier not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"carrier": carrier,
})
}
// GetProfileStatus handles profile status check requests
func (h *SMDPHandler) GetProfileStatus(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Profile status endpoint",
"status": "active",
})
}
@@ -0,0 +1,74 @@
package handlers
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
)
// AddCarrier handles adding a new carrier
func (h *SMDPHandler) AddCarrier(c *gin.Context) {
var carrierConfig smdp.CarrierConfig
if err := c.ShouldBindJSON(&carrierConfig); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
// Validate carrier config
if carrierConfig.ID == "" || carrierConfig.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Carrier ID and name are required",
})
return
}
carrier := carrierConfig.ToCarrier()
if err := h.manager.AddCarrier(carrier); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to add carrier",
"message": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"message": "Carrier added successfully",
"carrier": carrier,
})
}
// GetAnalytics handles analytics requests
func (h *SMDPHandler) GetAnalytics(c *gin.Context) {
// Get basic analytics
carriers := h.manager.GetCarrierStatus()
totalCarriers := len(carriers)
healthyCarriers := 0
for _, carrier := range carriers {
if carrier.HealthStatus == "healthy" {
healthyCarriers++
}
}
overallHealth := 0.0
if totalCarriers > 0 {
overallHealth = float64(healthyCarriers) / float64(totalCarriers) * 100
}
analytics := map[string]any{
"generated_at": time.Now().Format(time.RFC3339),
"total_carriers": totalCarriers,
"healthy_carriers": healthyCarriers,
"overall_health": overallHealth,
"health_percentage": overallHealth,
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"analytics": analytics,
})
}
@@ -0,0 +1,77 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
)
// RemoveCarrier handles removing a carrier
func (h *SMDPHandler) RemoveCarrier(c *gin.Context) {
carrierID := c.Param("carrier_id")
if carrierID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Carrier ID is required"})
return
}
if err := h.manager.RemoveCarrier(carrierID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to remove carrier",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Carrier removed successfully",
})
}
// GetSelectionHistory handles selection history requests
func (h *SMDPHandler) GetSelectionHistory(c *gin.Context) {
carrierID := c.Param("carrier_id")
if carrierID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Carrier ID is required"})
return
}
history := h.manager.GetSelectionHistory(carrierID)
c.JSON(http.StatusOK, gin.H{
"success": true,
"history": history,
"count": len(history),
})
}
// UpdateLearning handles learning update requests
func (h *SMDPHandler) UpdateLearning(c *gin.Context) {
carrierID := c.Param("carrier_id")
if carrierID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Carrier ID is required"})
return
}
var req struct {
ActualPerformance float64 `json:"actual_performance"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if req.ActualPerformance < 0 || req.ActualPerformance > 100 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Actual performance must be between 0 and 100",
})
return
}
h.manager.UpdateLearning(carrierID, req.ActualPerformance)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Learning updated successfully",
})
}
@@ -3,11 +3,11 @@ package integration
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handlers"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/service"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
@@ -25,7 +25,8 @@ func NewSMDPIntegration(repo *repository.PostgresProfileStore) *SMDPIntegration
logger.SetLevel(logrus.InfoLevel)
svc := service.NewSMDPService(repo)
hnd := handlers.NewSMDPHandler(repo)
manager := svc.GetManager()
hnd := handlers.NewSMDPHandler(manager)
integration := &SMDPIntegration{
service: svc,
@@ -57,11 +58,11 @@ func (i *SMDPIntegration) InitializeSystem() error {
for id, carrier := range carriers {
i.logger.WithFields(logrus.Fields{
"carrier_id": id,
"carrier_name": carrier.Name,
"country": carrier.CountryCode,
"health_status": carrier.HealthStatus,
"is_active": carrier.IsActive,
"carrier_id": id,
"carrier_name": carrier.Name,
"country": carrier.CountryCode,
"health_status": carrier.HealthStatus,
"is_active": carrier.IsActive,
}).Info("Carrier loaded")
}
@@ -92,10 +93,10 @@ func (i *SMDPIntegration) RegisterRoutes(router *gin.RouterGroup) {
// healthHandler returns system health status
func (i *SMDPIntegration) healthHandler(c *gin.Context) {
carriers := i.service.GetCarrierHealth()
healthyCount := 0
totalCount := len(carriers)
for _, carrier := range carriers {
if carrier.HealthStatus == smdp.CarrierStatusHealthy {
healthyCount++
@@ -137,10 +138,10 @@ func (i *SMDPIntegration) metricsHandler(c *gin.Context) {
}
response["total_metrics"] = gin.H{
"total_requests": totalRequests,
"successful_requests": totalSuccess,
"failed_requests": totalFailed,
"success_rate": float64(totalSuccess) / float64(totalRequests),
"total_requests": totalRequests,
"successful_requests": totalSuccess,
"failed_requests": totalFailed,
"success_rate": float64(totalSuccess) / float64(totalRequests),
}
c.JSON(200, response)
@@ -159,40 +160,40 @@ func (i *SMDPIntegration) GetHandler() *handlers.SMDPHandler {
// ExampleUsage demonstrates how to use the SM-DP+ integration
func ExampleUsage() {
// This would typically be in your main.go or router setup
/*
// Initialize repository
repo, err := repository.NewPostgresProfileStore("postgres://user:pass@localhost/db")
if err != nil {
log.Fatal(err)
}
defer repo.Close()
// Initialize repository
repo, err := repository.NewPostgresProfileStore("postgres://user:pass@localhost/db")
if err != nil {
log.Fatal(err)
}
defer repo.Close()
// Create SM-DP+ integration
smdpIntegration := integration.NewSMDPIntegration(repo)
// Create SM-DP+ integration
smdpIntegration := integration.NewSMDPIntegration(repo)
// Register routes
router := gin.Default()
api := router.Group("/api/v1")
smdpIntegration.RegisterRoutes(api)
// Register routes
router := gin.Default()
api := router.Group("/api/v1")
smdpIntegration.RegisterRoutes(api)
// Get service for direct use
service := smdpIntegration.GetService()
// Get service for direct use
service := smdpIntegration.GetService()
// Example: Download profile
ctx := context.Background()
req := &smdp.ProfileRequest{
EID: "eid-example",
ICCID: "iccid-example",
ProfileType: "operational",
}
// Example: Download profile
ctx := context.Background()
req := &smdp.ProfileRequest{
EID: "eid-example",
ICCID: "iccid-example",
ProfileType: "operational",
}
response, err := service.DownloadProfile(ctx, req)
if err != nil {
log.Printf("Profile download failed: %v", err)
return
}
response, err := service.DownloadProfile(ctx, req)
if err != nil {
log.Printf("Profile download failed: %v", err)
return
}
log.Printf("Profile downloaded successfully from carrier %s", response.CarrierID)
log.Printf("Profile downloaded successfully from carrier %s", response.CarrierID)
*/
}
@@ -0,0 +1,218 @@
package service
import (
"context"
"fmt"
"time"
)
// GetCarrierAnalytics returns comprehensive carrier analytics
func (s *SelectionService) GetCarrierAnalytics(ctx context.Context) (*CarrierAnalyticsResponse, error) {
s.logger.Info("Generating carrier analytics")
// Get carrier status
carriers := s.manager.GetCarrierStatus()
analytics := &CarrierAnalyticsResponse{
GeneratedAt: time.Now(),
TotalCarriers: len(carriers),
HealthyCarriers: 0,
Analytics: make(map[string]*CarrierAnalytics),
Summary: &AnalyticsSummary{},
}
var totalScore float64
var totalRequests uint64
var totalSuccess uint64
for carrierID, carrier := range carriers {
carrierAnalytics := &CarrierAnalytics{
CarrierID: carrierID,
CarrierName: carrier.Name,
Region: carrier.CountryCode,
Status: string(carrier.HealthStatus),
LastCheck: carrier.LastHealthCheck,
}
// Get selection history
history := s.manager.GetSelectionHistory(carrierID)
carrierAnalytics.SelectionCount = len(history)
if len(history) > 0 {
var scoreSum float64
for _, score := range history {
scoreSum += score.TotalScore
}
carrierAnalytics.AverageScore = scoreSum / float64(len(history))
carrierAnalytics.LastSelected = history[len(history)-1].SelectedAt
}
// Calculate performance metrics
if carrier.Metrics.TotalRequests > 0 {
carrierAnalytics.SuccessRate = float64(carrier.Metrics.SuccessfulRequests) / float64(carrier.Metrics.TotalRequests) * 100
carrierAnalytics.AverageResponseTime = carrier.Metrics.AverageResponseTime.Milliseconds()
carrierAnalytics.RequestRate = carrier.Metrics.RequestRate
}
// Update totals
totalScore += carrierAnalytics.AverageScore
totalRequests += carrier.Metrics.TotalRequests
totalSuccess += carrier.Metrics.SuccessfulRequests
// Count healthy carriers
if carrier.HealthStatus == "healthy" {
analytics.HealthyCarriers++
}
// Generate carrier-specific recommendations
carrierAnalytics.Recommendations = s.generateCarrierRecommendations(carrierAnalytics)
analytics.Analytics[carrierID] = carrierAnalytics
}
// Calculate summary metrics
if len(carriers) > 0 {
analytics.Summary.AverageScore = totalScore / float64(len(carriers))
}
if totalRequests > 0 {
analytics.Summary.OverallSuccessRate = float64(totalSuccess) / float64(totalRequests) * 100
}
analytics.Summary.TotalRequests = totalRequests
analytics.Summary.OverallHealth = float64(analytics.HealthyCarriers) / float64(len(carriers)) * 100
return analytics, nil
}
// CarrierAnalyticsResponse represents the response for carrier analytics
type CarrierAnalyticsResponse struct {
GeneratedAt time.Time `json:"generated_at"`
TotalCarriers int `json:"total_carriers"`
HealthyCarriers int `json:"healthy_carriers"`
Analytics map[string]*CarrierAnalytics `json:"analytics"`
Summary *AnalyticsSummary `json:"summary"`
}
// CarrierAnalytics represents analytics for a single carrier
type CarrierAnalytics struct {
CarrierID string `json:"carrier_id"`
CarrierName string `json:"carrier_name"`
Region string `json:"region"`
Status string `json:"status"`
SelectionCount int `json:"selection_count"`
AverageScore float64 `json:"average_score"`
SuccessRate float64 `json:"success_rate"`
AverageResponseTime int64 `json:"average_response_time_ms"`
RequestRate float64 `json:"request_rate"`
LastSelected time.Time `json:"last_selected"`
LastCheck time.Time `json:"last_check"`
Recommendations []string `json:"recommendations"`
}
// AnalyticsSummary represents overall analytics summary
type AnalyticsSummary struct {
AverageScore float64 `json:"average_score"`
OverallSuccessRate float64 `json:"overall_success_rate"`
TotalRequests uint64 `json:"total_requests"`
OverallHealth float64 `json:"overall_health"`
}
// generateCarrierRecommendations generates recommendations for a specific carrier
func (s *SelectionService) generateCarrierRecommendations(analytics *CarrierAnalytics) []string {
recommendations := []string{}
// Performance recommendations
if analytics.SuccessRate < 95 {
recommendations = append(recommendations, "Monitor success rate - below optimal threshold")
}
if analytics.AverageResponseTime > 500 {
recommendations = append(recommendations, "Consider optimizing response time")
}
if analytics.RequestRate > 100 {
recommendations = append(recommendations, "High request rate - monitor for overload")
}
// Selection recommendations
if analytics.SelectionCount == 0 {
recommendations = append(recommendations, "Carrier never selected - investigate configuration")
} else if analytics.AverageScore < 70 {
recommendations = append(recommendations, "Low selection scores - review carrier performance")
}
// Health recommendations
if analytics.Status != "healthy" {
recommendations = append(recommendations, "Carrier health issues - immediate attention required")
}
if len(recommendations) == 0 {
recommendations = append(recommendations, "Carrier performing well")
}
return recommendations
}
// OptimizeCarrierSelection provides optimization recommendations
func (s *SelectionService) OptimizeCarrierSelection(ctx context.Context) (*OptimizationResponse, error) {
s.logger.Info("Analyzing carrier selection optimization opportunities")
analytics, err := s.GetCarrierAnalytics(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get analytics: %w", err)
}
optimization := &OptimizationResponse{
GeneratedAt: time.Now(),
OverallHealth: analytics.Summary.OverallHealth,
Recommendations: []string{},
PriorityActions: []string{},
LongTermActions: []string{},
}
// Generate recommendations based on analytics
if analytics.Summary.OverallHealth < 80 {
optimization.Recommendations = append(optimization.Recommendations, "System health below optimal - investigate carrier issues")
optimization.PriorityActions = append(optimization.PriorityActions, "Review unhealthy carriers and implement failover")
}
if analytics.Summary.OverallSuccessRate < 95 {
optimization.Recommendations = append(optimization.Recommendations, "Success rate below target - optimize carrier selection criteria")
optimization.PriorityActions = append(optimization.PriorityActions, "Adjust performance weights in selection algorithm")
}
// Analyze individual carriers
for _, carrier := range analytics.Analytics {
if carrier.SuccessRate < 90 {
optimization.PriorityActions = append(optimization.PriorityActions,
fmt.Sprintf("Investigate %s (%s) - success rate %.1f%%",
carrier.CarrierName, carrier.CarrierID, carrier.SuccessRate))
}
if carrier.AverageResponseTime > 1000 {
optimization.PriorityActions = append(optimization.PriorityActions,
fmt.Sprintf("Optimize %s (%s) - response time %dms",
carrier.CarrierName, carrier.CarrierID, carrier.AverageResponseTime))
}
}
// Long-term recommendations
optimization.LongTermActions = append(optimization.LongTermActions,
"Implement machine learning for carrier selection")
optimization.LongTermActions = append(optimization.LongTermActions,
"Add predictive analytics for carrier performance")
optimization.LongTermActions = append(optimization.LongTermActions,
"Expand carrier portfolio for better redundancy")
if len(optimization.Recommendations) == 0 {
optimization.Recommendations = append(optimization.Recommendations, "System operating optimally")
}
return optimization, nil
}
// OptimizationResponse represents optimization recommendations
type OptimizationResponse struct {
GeneratedAt time.Time `json:"generated_at"`
OverallHealth float64 `json:"overall_health"`
Recommendations []string `json:"recommendations"`
PriorityActions []string `json:"priority_actions"`
LongTermActions []string `json:"long_term_actions"`
}
@@ -5,7 +5,7 @@ import (
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handler"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handlers"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/sirupsen/logrus"
)
@@ -13,7 +13,7 @@ import (
// SelectionService provides high-level carrier selection operations
type SelectionService struct {
manager *smdp.SMDPManager
handler *handler.SelectionHandler
handler *handlers.SelectionHandler
logger *logrus.Logger
}
@@ -24,13 +24,13 @@ func NewSelectionService(manager *smdp.SMDPManager) *SelectionService {
return &SelectionService{
manager: manager,
handler: handler.NewSelectionHandler(manager),
handler: handlers.NewSelectionHandler(manager),
logger: logger,
}
}
// GetHandler returns the selection handler for API registration
func (s *SelectionService) GetHandler() *handler.SelectionHandler {
func (s *SelectionService) GetHandler() *handlers.SelectionHandler {
return s.handler
}
@@ -114,7 +114,7 @@ func (s *SMDPService) DownloadProfile(ctx context.Context, req *smdp.ProfileRequ
// GetOptimalCarrier selects the best carrier for a given request
func (s *SMDPService) GetOptimalCarrier(ctx context.Context, req *smdp.ProfileRequest) (*smdp.Carrier, error) {
return s.manager.SelectCarrier(ctx, req)
return s.manager.SelectCarrier(ctx)
}
// GetCarrierHealth returns health status of all carriers
@@ -144,6 +144,11 @@ func (s *SMDPService) RemoveCarrier(carrierID string) error {
return nil
}
// GetManager returns the underlying SMDPManager
func (s *SMDPService) GetManager() *smdp.SMDPManager {
return s.manager
}
// GetCarrierMetrics returns performance metrics for all carriers
func (s *SMDPService) GetCarrierMetrics() map[string]*smdp.CarrierMetrics {
carriers := s.manager.GetCarrierStatus()
@@ -11,16 +11,16 @@ import (
func (m *SMDPManager) DownloadProfile(ctx context.Context, req *ProfileRequest) (*ProfileResponse, error) {
startTime := time.Now()
m.logger.WithFields(logrus.Fields{
"eid": req.EID,
"preferred": req.PreferredCarrier,
}).Info("Processing profile download request")
selectedCarrier, err := m.SelectCarrier(ctx, req)
selectedCarrier, err := m.SelectCarrier(ctx)
if err != nil {
return &ProfileResponse{
Success: false,
Success: false,
StatusMessage: fmt.Sprintf("Carrier selection failed: %v", err),
ResponseTime: time.Since(startTime),
}, err
@@ -36,35 +36,6 @@ func (m *SMDPManager) DownloadProfile(ctx context.Context, req *ProfileRequest)
return response, err
}
func (m *SMDPManager) SelectCarrier(ctx context.Context, req *ProfileRequest) (*Carrier, error) {
m.carriersMutex.RLock()
defer m.carriersMutex.RUnlock()
var activeCarriers []*Carrier
for _, carrier := range m.carriers {
if carrier.IsActive && carrier.HealthStatus == CarrierStatusHealthy {
activeCarriers = append(activeCarriers, carrier)
}
}
if len(activeCarriers) == 0 {
return nil, fmt.Errorf("no healthy carriers available")
}
if req.PreferredCarrier != "" {
if carrier, exists := m.carriers[req.PreferredCarrier]; exists &&
carrier.IsActive && carrier.HealthStatus == CarrierStatusHealthy {
return carrier, nil
}
}
if m.config.EnableLoadBalancing {
return m.loadBalancer.SelectCarrier(activeCarriers, req)
}
return m.getHighestPriorityCarrier(activeCarriers), nil
}
func (m *SMDPManager) downloadWithRetry(ctx context.Context, req *ProfileRequest, carrier *Carrier, attempt int) (*ProfileResponse, error) {
m.clientsMutex.RLock()
client, exists := m.es2Clients[carrier.ID]
@@ -100,10 +71,10 @@ func (m *SMDPManager) downloadWithRetry(ctx context.Context, req *ProfileRequest
}
return &ProfileResponse{
Success: false,
CarrierID: carrier.ID,
Success: false,
CarrierID: carrier.ID,
StatusMessage: fmt.Sprintf("Download failed after %d attempts: %v", attempt+1, err),
ResponseTime: responseTime,
ResponseTime: responseTime,
}, err
}
@@ -114,8 +85,8 @@ func (m *SMDPManager) downloadWithRetry(ctx context.Context, req *ProfileRequest
}).Info("Profile download successful")
return &ProfileResponse{
Success: true,
CarrierID: carrier.ID,
Success: true,
CarrierID: carrier.ID,
ExecutionStatus: resp.ExecutionStatus,
StatusMessage: resp.StatusMessage,
ResponseTime: responseTime,
@@ -128,26 +99,26 @@ func (m *SMDPManager) handleFailover(ctx context.Context, req *ProfileRequest, f
defer m.carriersMutex.RUnlock()
var retriedOn []string
for carrierID, carrier := range m.carriers {
if carrierID == failedCarrierID || !carrier.IsActive || carrier.HealthStatus != CarrierStatusHealthy {
continue
}
m.logger.WithField("carrier_id", carrierID).Info("Attempting failover to carrier")
response, err := m.downloadWithRetry(ctx, req, carrier, 0)
if err == nil && response.Success {
response.RetriedOn = retriedOn
response.ResponseTime = time.Since(startTime)
return response, nil
}
retriedOn = append(retriedOn, carrierID)
}
return &ProfileResponse{
Success: false,
Success: false,
StatusMessage: "All carriers failed during failover",
ResponseTime: time.Since(startTime),
RetriedOn: retriedOn,