mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 02:52:53 +00:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user