New features (#62)

* refactor: Rename handler package to handlers in carrier-connector

- Rename internal/handler directory to internal/handlers
- Update handler import path in main.go and routes.go

* feat: Add SM-DP+ handler with multi-carrier management and profile operations

- Add SMDPHandler struct with manager field and NewSMDPHandler constructor
- Add AddCarrier endpoint with JSON binding and carrier configuration support
- Add RemoveCarrier endpoint with carrier ID parameter validation
- Add DownloadProfile endpoint with 30-second timeout and context cancellation
- Add GetCarrierStatus endpoint returning all carriers with timestamp
- Add GetProfileStatus endpoint with EID/ICCID validation

* feat: Add SM-DP+ integration layer with multi-carrier system initialization and route registration

- Add SMDPIntegration struct with service, handler, repository, and logger fields
- Add NewSMDPIntegration constructor with default carrier initialization
- Add InitializeSystem method with carrier health logging and status tracking
- Add RegisterRoutes method with profile, carrier, health, and metrics endpoints
- Add healthHandler with carrier health status aggregation and degraded state detection
- Add metricsHandler

* feat: Add SM-DP+ service layer with multi-carrier management and profile download operations

- Add SMDPService struct with manager, repository, and logger fields
- Add NewSMDPService constructor with default config and background health checking
- Add InitializeDefaultCarriers with AT&T, Verizon, and T-Mobile DE configurations
- Add DownloadProfile method with context support and request logging
- Add GetOptimalCarrier wrapping SelectCarrier for best carrier selection
- Add GetCarrierHealth returning

* feat: Add SM-DP+ manager with multi-carrier load balancing, health checking, and profile operations

- Add DefaultManagerConfig with 30s health checks, 3 retries, and circuit breaker configuration
- Add CarrierConfig struct with ES2 settings, MCC/MNC, priority, and feature support
- Add ToCarrier method converting CarrierConfig to Carrier with metrics and capabilities
- Add HealthChecker with periodic carrier health monitoring and status evaluation
- Add LoadBalancer with weighted round-robin, least

* feat: Add intelligent carrier selection service with scoring, recommendations, and analytics

- Add SelectionService struct with manager, handler, and logger fields
- Add NewSelectionService constructor with info-level logging
- Add IntelligentCarrierSelection method with comprehensive criteria evaluation
- Add IntelligentSelectionRequest with region, profile type, urgency, and cost sensitivity
- Add UserPreferences and BusinessContext structs for selection customization
- Add IntelligentSelectionResponse with selected

* feat: Add carrier selection handler with optimal selection, analytics, and performance monitoring endpoints

- Add SelectionHandler struct with manager and logger fields
- Add NewSelectionHandler constructor with info-level logging
- Add SelectOptimalCarrier endpoint with criteria validation and scoring
- Add SelectCarrier endpoint using default selection criteria
- Add GetSelectionHistory endpoint with carrier ID path parameter
- Add UpdateLearning endpoint with performance feedback validation
- Add GetSelectionAnalytics endpoint with carrier stats

* feat: Add carrier selection integration layer with multi-carrier setup, HTTP server, and demo capabilities

- Add SelectionIntegration struct with manager, selection service, handler, and SMDP service fields
- Add NewSelectionIntegration constructor with default manager config and 30s health checks
- Add SetupCarriers with AT&T, Verizon, T-Mobile DE, and Orange FR configurations
- Add StartServer method with health endpoint and 30s read/write timeouts
- Add StartHealthChecking wrapper for background

* 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

* refactor: Extract carrier setup logic into separate file and remove unnecessary comments

- Move SetupCarriers method from selection_integration.go to new setup_carriers.go file
- Remove redundant comments from SelectionIntegration struct and methods
- Clean up inline comments in RunDemo method while preserving functionality

* feat: Add intelligent carrier selection algorithm with machine learning optimization and performance tracking

- Add SelectionAlgorithm with scoring, weighting, and ML-based carrier selection
- Add SelectionCriteria struct with region, profile type, urgency, and cost/performance weights
- Add CarrierScore with performance, reliability, cost, region, and capability scores
- Add calculateWeightedScore combining all scores with ML-optimized weights
- Add getWeights with urgency-based adjustments

* feat: Integrate SelectionAlgorithm into SMDPManager with optimal carrier selection and learning capabilities

- Add selector field to SMDPManager struct with SelectionAlgorithm type
- Initialize selector in NewSMDPManager constructor
- Add SelectOptimalCarrier method filtering healthy carriers and delegating to selection algorithm
- Add SelectCarrier wrapper with default criteria (operational profile, medium urgency, balanced weights)
- Add GetSelectionHistory method returning carrier selection history
- Add UpdateLearning
This commit is contained in:
Nutcase
2026-05-01 23:29:35 +03:00
committed by GitHub
parent d61aa93bd5
commit 626a6f7349
32 changed files with 3733 additions and 462 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,
},
})
}
}
@@ -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",
})
}
@@ -0,0 +1,191 @@
package integration
import (
"context"
"log"
"net/http"
"time"
"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"
)
type SelectionIntegration struct {
manager *smdp.SMDPManager
selectionService *service.SelectionService
selectionHandler *handlers.SelectionHandler
smdpService *service.SMDPService
server *http.Server
}
func NewSelectionIntegration(repo *repository.PostgresProfileStore) *SelectionIntegration {
// Create SMDP manager with default configuration
config := &smdp.ManagerConfig{
HealthCheckInterval: 30 * time.Second,
EnableFailover: true,
EnableLoadBalancing: true,
MaxRetries: 3,
RetryDelay: 1 * time.Second,
}
manager := smdp.NewSMDPManager(repo, config)
// Create selection service
selectionService := service.NewSelectionService(manager)
// Create SMDP service
smdpService := service.NewSMDPService(repo)
return &SelectionIntegration{
manager: manager,
selectionService: selectionService,
selectionHandler: selectionService.GetHandler(),
smdpService: smdpService,
}
}
// StartServer starts the HTTP server with all endpoints
func (si *SelectionIntegration) StartServer(port string) error {
// Create HTTP multiplexer
mux := http.NewServeMux()
// Register selection routes
si.selectionHandler.RegisterRoutes(mux)
// Add health check endpoint
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "healthy", "service": "selection-integration"}`))
})
// Create and configure server
si.server = &http.Server{
Addr: ":" + port,
Handler: mux,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Printf("Starting selection integration server on port %s", port)
return si.server.ListenAndServe()
}
// StartHealthChecking starts the background health checking process
func (si *SelectionIntegration) StartHealthChecking(ctx context.Context) {
si.manager.StartHealthChecking(ctx)
}
// GetManager returns the SMDP manager for testing
func (si *SelectionIntegration) GetManager() *smdp.SMDPManager {
return si.manager
}
// GetSelectionService returns the selection service for testing
func (si *SelectionIntegration) GetSelectionService() *service.SelectionService {
return si.selectionService
}
// Shutdown gracefully shuts down the server
func (si *SelectionIntegration) Shutdown(ctx context.Context) error {
if si.server != nil {
return si.server.Shutdown(ctx)
}
return nil
}
func (si *SelectionIntegration) RunDemo(ctx context.Context) error {
log.Println("Starting carrier selection demonstration...")
if err := si.SetupCarriers(); err != nil {
return err
}
si.StartHealthChecking(ctx)
time.Sleep(2 * time.Second)
log.Println("Demonstrating intelligent carrier selection...")
scenarios := []struct {
name string
criteria *smdp.SelectionCriteria
}{
{
name: "High Priority US Request",
criteria: &smdp.SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "high",
CostSensitivity: 0.2,
PerformanceWeight: 0.6,
ReliabilityWeight: 0.6,
},
},
{
name: "Cost-Optimized European Request",
criteria: &smdp.SelectionCriteria{
Region: "DE",
ProfileType: "operational",
Urgency: "low",
CostSensitivity: 0.8,
PerformanceWeight: 0.2,
ReliabilityWeight: 0.3,
},
},
{
name: "Balanced Global Request",
criteria: &smdp.SelectionCriteria{
Region: "",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
},
},
}
for _, scenario := range scenarios {
log.Printf("Testing scenario: %s", scenario.name)
score, err := si.manager.SelectOptimalCarrier(ctx, scenario.criteria)
if err != nil {
log.Printf("Error in scenario %s: %v", scenario.name, err)
continue
}
log.Printf("Selected carrier: %s (%s)", score.Carrier.Name, score.CarrierID)
log.Printf("Total score: %.2f", score.TotalScore)
log.Printf("Performance: %.2f, Reliability: %.2f, Cost: %.2f, Region: %.2f, Capability: %.2f",
score.PerformanceScore, score.ReliabilityScore, score.CostScore, score.RegionScore, score.CapabilityScore)
log.Printf("Reason: %s", score.Reason)
log.Println("---")
}
log.Println("Generating carrier analytics...")
analytics, err := si.selectionService.GetCarrierAnalytics(ctx)
if err != nil {
log.Printf("Error generating analytics: %v", err)
} else {
log.Printf("Analytics generated for %d carriers", analytics.TotalCarriers)
log.Printf("Overall health: %.1f%%", analytics.Summary.OverallHealth)
log.Printf("Overall success rate: %.1f%%", analytics.Summary.OverallSuccessRate)
}
log.Println("Generating optimization recommendations...")
optimization, err := si.selectionService.OptimizeCarrierSelection(ctx)
if err != nil {
log.Printf("Error generating optimization: %v", err)
} else {
log.Printf("System health: %.1f%%", optimization.OverallHealth)
log.Printf("Recommendations: %d", len(optimization.Recommendations))
log.Printf("Priority actions: %d", len(optimization.PriorityActions))
}
log.Println("Carrier selection demonstration completed successfully!")
return nil
}
@@ -0,0 +1,134 @@
package integration
import (
"log"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/config"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
)
// SetupCarriers configures default carriers for demonstration
func (si *SelectionIntegration) SetupCarriers() error {
// Configure sample carriers with different characteristics
carriers := []*smdp.Carrier{
{
ID: "att-us",
Name: "AT&T US",
MCC: "310",
MNC: "410",
CountryCode: "US",
IsActive: true,
Priority: 90,
ES2Config: &config.ES2Config{
BaseURL: "https://es2plus.att.com",
APIKey: "demo-key-att",
InsecureSkipVerify: false,
FunctionalityRequesterID: "telecom-platform",
},
Capabilities: &smdp.CarrierCapabilities{
SupportedProfileTypes: []string{"operational", "testing"},
Features: []string{"bulk_download", "remote_provisioning"},
MaxConcurrentRequests: 100,
},
Metrics: &smdp.CarrierMetrics{
TotalRequests: 1000,
SuccessfulRequests: 980,
FailedRequests: 20,
AverageResponseTime: 150 * time.Millisecond,
RequestRate: 10.5,
},
},
{
ID: "verizon-us",
Name: "Verizon US",
MCC: "311",
MNC: "480",
CountryCode: "US",
IsActive: true,
Priority: 85,
ES2Config: &config.ES2Config{
BaseURL: "https://es2plus.verizon.com",
APIKey: "demo-key-verizon",
InsecureSkipVerify: false,
FunctionalityRequesterID: "telecom-platform",
},
Capabilities: &smdp.CarrierCapabilities{
SupportedProfileTypes: []string{"operational", "testing"},
Features: []string{"bulk_download"},
MaxConcurrentRequests: 80,
},
Metrics: &smdp.CarrierMetrics{
TotalRequests: 800,
SuccessfulRequests: 790,
FailedRequests: 10,
AverageResponseTime: 120 * time.Millisecond,
RequestRate: 8.2,
},
},
{
ID: "tmobile-de",
Name: "T-Mobile Germany",
MCC: "262",
MNC: "01",
CountryCode: "DE",
IsActive: true,
Priority: 75,
ES2Config: &config.ES2Config{
BaseURL: "https://es2plus.t-mobile.de",
APIKey: "demo-key-tmobile",
InsecureSkipVerify: false,
FunctionalityRequesterID: "telecom-platform",
},
Capabilities: &smdp.CarrierCapabilities{
SupportedProfileTypes: []string{"operational"},
Features: []string{"remote_provisioning"},
MaxConcurrentRequests: 60,
},
Metrics: &smdp.CarrierMetrics{
TotalRequests: 600,
SuccessfulRequests: 570,
FailedRequests: 30,
AverageResponseTime: 200 * time.Millisecond,
RequestRate: 6.8,
},
},
{
ID: "orange-fr",
Name: "Orange France",
MCC: "208",
MNC: "01",
CountryCode: "FR",
IsActive: true,
Priority: 70,
ES2Config: &config.ES2Config{
BaseURL: "https://es2plus.orange.fr",
APIKey: "demo-key-orange",
InsecureSkipVerify: false,
FunctionalityRequesterID: "telecom-platform",
},
Capabilities: &smdp.CarrierCapabilities{
SupportedProfileTypes: []string{"operational", "testing"},
Features: []string{},
MaxConcurrentRequests: 50,
},
Metrics: &smdp.CarrierMetrics{
TotalRequests: 400,
SuccessfulRequests: 380,
FailedRequests: 20,
AverageResponseTime: 180 * time.Millisecond,
RequestRate: 4.5,
},
},
}
// Add carriers to the manager
for _, carrier := range carriers {
if err := si.manager.AddCarrier(carrier); err != nil {
return err
}
}
log.Printf("Added %d carriers to the selection manager", len(carriers))
return nil
}
@@ -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"`
}
@@ -0,0 +1,169 @@
package service
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handlers"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/sirupsen/logrus"
)
// SelectionService provides high-level carrier selection operations
type SelectionService struct {
manager *smdp.SMDPManager
handler *handlers.SelectionHandler
logger *logrus.Logger
}
// NewSelectionService creates a new selection service
func NewSelectionService(manager *smdp.SMDPManager) *SelectionService {
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
return &SelectionService{
manager: manager,
handler: handlers.NewSelectionHandler(manager),
logger: logger,
}
}
// GetHandler returns the selection handler for API registration
func (s *SelectionService) GetHandler() *handlers.SelectionHandler {
return s.handler
}
// IntelligentCarrierSelection performs intelligent carrier selection with comprehensive criteria
func (s *SelectionService) IntelligentCarrierSelection(ctx context.Context, request *IntelligentSelectionRequest) (*IntelligentSelectionResponse, error) {
s.logger.WithFields(logrus.Fields{
"region": request.Region,
"profile_type": request.ProfileType,
"urgency": request.Urgency,
"cost_sensitivity": request.CostSensitivity,
}).Info("Performing intelligent carrier selection")
criteria := &smdp.SelectionCriteria{
Region: request.Region,
ProfileType: request.ProfileType,
Urgency: request.Urgency,
CostSensitivity: request.CostSensitivity,
PerformanceWeight: request.PerformanceWeight,
ReliabilityWeight: request.ReliabilityWeight,
}
// Select optimal carrier
score, err := s.manager.SelectOptimalCarrier(ctx, criteria)
if err != nil {
return nil, fmt.Errorf("failed to select optimal carrier: %w", err)
}
// Record the selection for analytics
s.recordSelection(score)
// Generate recommendations
recommendations := s.generateRecommendations(score, request)
response := &IntelligentSelectionResponse{
Success: true,
SelectedCarrier: score.Carrier,
SelectionScore: score,
Recommendations: recommendations,
SelectionTime: time.Now(),
}
s.logger.WithFields(logrus.Fields{
"selected_carrier": score.CarrierID,
"total_score": score.TotalScore,
"reason": score.Reason,
}).Info("Intelligent carrier selection completed")
return response, nil
}
// IntelligentSelectionRequest represents a comprehensive carrier selection request
type IntelligentSelectionRequest 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"`
UserPreferences *UserPreferences `json:"user_preferences,omitempty"`
BusinessContext *BusinessContext `json:"business_context,omitempty"`
}
// UserPreferences represents user-specific preferences
type UserPreferences struct {
PreferredCarriers []string `json:"preferred_carriers"`
ExcludedCarriers []string `json:"excluded_carriers"`
MaxResponseTime int `json:"max_response_time_ms"`
MinSuccessRate float64 `json:"min_success_rate"`
}
// BusinessContext represents business-specific context
type BusinessContext struct {
CustomerTier string `json:"customer_tier"`
ServiceLevel string `json:"service_level"`
BillingModel string `json:"billing_model"`
GeographicScope string `json:"geographic_scope"`
}
// IntelligentSelectionResponse represents the response from intelligent carrier selection
type IntelligentSelectionResponse struct {
Success bool `json:"success"`
SelectedCarrier *smdp.Carrier `json:"selected_carrier"`
SelectionScore *smdp.CarrierScore `json:"selection_score"`
Recommendations []string `json:"recommendations"`
SelectionTime time.Time `json:"selection_time"`
Alternatives []*smdp.CarrierScore `json:"alternatives,omitempty"`
}
// recordSelection records a carrier selection for analytics
func (s *SelectionService) recordSelection(score *smdp.CarrierScore) {
// This would typically be stored in a database for analytics
s.logger.WithFields(logrus.Fields{
"carrier_id": score.CarrierID,
"score": score.TotalScore,
"reason": score.Reason,
"timestamp": score.SelectedAt,
}).Info("Carrier selection recorded")
}
// generateRecommendations generates recommendations based on selection results
func (s *SelectionService) generateRecommendations(score *smdp.CarrierScore, request *IntelligentSelectionRequest) []string {
recommendations := []string{}
// Performance-based recommendations
if score.PerformanceScore < 70 {
recommendations = append(recommendations, "Consider monitoring carrier performance closely")
} else if score.PerformanceScore > 90 {
recommendations = append(recommendations, "Excellent performance - consider prioritizing this carrier")
}
// Cost-based recommendations
if score.CostScore < 50 && request.CostSensitivity > 0.7 {
recommendations = append(recommendations, "Higher cost detected - consider cost optimization strategies")
}
// Reliability-based recommendations
if score.ReliabilityScore < 60 {
recommendations = append(recommendations, "Reliability concerns - ensure failover mechanisms are in place")
}
// Regional recommendations
if score.RegionScore < 50 {
recommendations = append(recommendations, "Regional compatibility issues - consider regional carrier partnerships")
}
// Urgency-based recommendations
if request.Urgency == "high" && score.TotalScore < 80 {
recommendations = append(recommendations, "High urgency request - consider manual carrier override if needed")
}
if len(recommendations) == 0 {
recommendations = append(recommendations, "Carrier selection appears optimal")
}
return recommendations
}
@@ -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()
@@ -13,15 +13,16 @@ import (
// SMDPManager manages multiple SM-DP+ carriers
type SMDPManager struct {
carriers map[string]*Carrier
carriersMutex sync.RWMutex
es2Clients map[string]*es2.ES2Client
clientsMutex sync.RWMutex
repository *repository.PostgresProfileStore
healthChecker *HealthChecker
loadBalancer *LoadBalancer
config *ManagerConfig
logger *logrus.Logger
carriers map[string]*Carrier
carriersMutex sync.RWMutex
es2Clients map[string]*es2.ES2Client
clientsMutex sync.RWMutex
repository *repository.PostgresProfileStore
healthChecker *HealthChecker
loadBalancer *LoadBalancer
selector *SelectionAlgorithm
config *ManagerConfig
logger *logrus.Logger
}
// NewSMDPManager creates a new SM-DP+ Manager
@@ -37,6 +38,7 @@ func NewSMDPManager(repo *repository.PostgresProfileStore, config *ManagerConfig
logger: logger,
healthChecker: NewHealthChecker(config.HealthCheckInterval),
loadBalancer: NewLoadBalancer(),
selector: NewSelectionAlgorithm(),
}
}
@@ -109,6 +111,51 @@ func (m *SMDPManager) validateCarrier(carrier *Carrier) error {
return nil
}
// SelectOptimalCarrier selects the best carrier based on criteria
func (m *SMDPManager) SelectOptimalCarrier(ctx context.Context, criteria *SelectionCriteria) (*CarrierScore, error) {
m.carriersMutex.RLock()
defer m.carriersMutex.RUnlock()
// Get all active and healthy carriers
healthyCarriers := make([]*Carrier, 0)
for _, carrier := range m.carriers {
if carrier.IsActive && carrier.HealthStatus == CarrierStatusHealthy {
healthyCarriers = append(healthyCarriers, carrier)
}
}
return m.selector.SelectOptimalCarrier(ctx, healthyCarriers, criteria)
}
// SelectCarrier selects a carrier using the optimal algorithm with default criteria
func (m *SMDPManager) SelectCarrier(ctx context.Context) (*Carrier, error) {
criteria := &SelectionCriteria{
Region: "",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
score, err := m.SelectOptimalCarrier(ctx, criteria)
if err != nil {
return nil, err
}
return score.Carrier, nil
}
// GetSelectionHistory returns the selection history for a carrier
func (m *SMDPManager) GetSelectionHistory(carrierID string) []CarrierScore {
return m.selector.GetSelectionHistory(carrierID)
}
// UpdateLearning updates the selection algorithm with performance feedback
func (m *SMDPManager) UpdateLearning(carrierID string, actualPerformance float64) {
m.selector.UpdateLearning(carrierID, actualPerformance)
}
// updateCarrierHealth updates the health status of a carrier
func (m *SMDPManager) updateCarrierHealth(carrierID string, status CarrierHealthStatus) {
m.carriersMutex.Lock()
@@ -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,
@@ -0,0 +1,183 @@
package smdp
import (
"math"
"time"
"github.com/sirupsen/logrus"
)
// SelectionCriteria defines parameters for carrier selection
type SelectionCriteria struct {
Region string `json:"region"`
ProfileType string `json:"profile_type"`
Urgency string `json:"urgency"` // "low", "medium", "high"
CostSensitivity float64 `json:"cost_sensitivity"` // 0.0-1.0
PerformanceWeight float64 `json:"performance_weight"` // 0.0-1.0
ReliabilityWeight float64 `json:"reliability_weight"` // 0.0-1.0
}
// CarrierScore represents the scored evaluation of a carrier
type CarrierScore struct {
CarrierID string `json:"carrier_id"`
Carrier *Carrier `json:"carrier"`
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 time.Time `json:"selected_at"`
Reason string `json:"reason"`
}
// SelectionAlgorithm implements intelligent carrier selection
type SelectionAlgorithm struct {
logger *logrus.Logger
history map[string][]CarrierScore // Selection history per carrier
maxHistory int
learningRate float64
mlModel *LearningModel // Machine learning model
}
// NewSelectionAlgorithm creates a new selection algorithm instance
func NewSelectionAlgorithm() *SelectionAlgorithm {
return &SelectionAlgorithm{
logger: logrus.New(),
history: make(map[string][]CarrierScore),
maxHistory: 100,
learningRate: 0.1,
mlModel: NewLearningModel(),
}
}
// calculateWeightedScore combines all scores with weights
func (sa *SelectionAlgorithm) calculateWeightedScore(score *CarrierScore, criteria *SelectionCriteria) float64 {
weights := sa.getWeights(criteria)
total := score.PerformanceScore*weights.performance +
score.ReliabilityScore*weights.reliability +
score.CostScore*weights.cost +
score.RegionScore*weights.region +
score.CapabilityScore*weights.capability
return math.Min(100.0, total)
}
// getWeights returns selection weights based on criteria and ML optimization
func (sa *SelectionAlgorithm) getWeights(criteria *SelectionCriteria) struct {
performance float64
reliability float64
cost float64
region float64
capability float64
} {
// Get ML-optimized weights as base
mlWeights := sa.mlModel.GetOptimizedWeights()
weights := struct {
performance float64
reliability float64
cost float64
region float64
capability float64
}{
performance: mlWeights.Performance,
reliability: mlWeights.Reliability,
cost: mlWeights.Cost,
region: mlWeights.Region,
capability: mlWeights.Capability,
}
// Adjust based on urgency (override ML weights for specific business rules)
switch criteria.Urgency {
case "high":
weights.performance = math.Max(weights.performance, 0.4)
weights.reliability = math.Max(weights.reliability, 0.4)
weights.cost = math.Min(weights.cost, 0.1)
weights.region = math.Min(weights.region, 0.05)
weights.capability = math.Min(weights.capability, 0.05)
case "low":
weights.cost = math.Max(weights.cost, 0.4)
weights.performance = math.Min(weights.performance, 0.2)
weights.reliability = math.Min(weights.reliability, 0.2)
}
// Apply user-defined weights (business preferences override ML)
if criteria.PerformanceWeight > 0 {
weights.performance = criteria.PerformanceWeight
}
if criteria.ReliabilityWeight > 0 {
weights.reliability = criteria.ReliabilityWeight
}
if criteria.CostSensitivity > 0 {
weights.cost = criteria.CostSensitivity
}
// Normalize weights
total := weights.performance + weights.reliability + weights.cost + weights.region + weights.capability
if total > 0 {
weights.performance /= total
weights.reliability /= total
weights.cost /= total
weights.region /= total
weights.capability /= total
}
return weights
}
// recordSelection records the selection for learning
func (sa *SelectionAlgorithm) recordSelection(score *CarrierScore) {
history := sa.history[score.CarrierID]
history = append(history, *score)
// Limit history size
if len(history) > sa.maxHistory {
history = history[1:]
}
sa.history[score.CarrierID] = history
}
// GetSelectionHistory returns selection history for a carrier
func (sa *SelectionAlgorithm) GetSelectionHistory(carrierID string) []CarrierScore {
return sa.history[carrierID]
}
// UpdateLearning updates the algorithm based on feedback using ML model
func (sa *SelectionAlgorithm) UpdateLearning(carrierID string, actualPerformance float64) {
history := sa.history[carrierID]
if len(history) == 0 {
return
}
// Get the most recent selection to get expected score
lastSelection := history[len(history)-1]
expectedScore := lastSelection.TotalScore
// Update ML model with performance feedback
sa.mlModel.UpdateLearning(carrierID, actualPerformance, expectedScore)
// Legacy learning rate adjustment for backward compatibility
if actualPerformance < 50.0 {
sa.learningRate *= 0.95 // Reduce learning rate for poor performance
} else {
sa.learningRate *= 1.05 // Increase learning rate for good performance
}
}
// GetLearningStats returns statistics about the ML learning model
func (sa *SelectionAlgorithm) GetLearningStats() map[string]any {
return sa.mlModel.GetLearningStats()
}
// PredictPerformance predicts expected performance for a carrier
func (sa *SelectionAlgorithm) PredictPerformance(carrierID string, criteria *SelectionCriteria) float64 {
return sa.mlModel.PredictPerformance(carrierID, criteria)
}
// GetCarrierPerformance returns detailed performance metrics for a carrier
func (sa *SelectionAlgorithm) GetCarrierPerformance(carrierID string) *PerformanceMetrics {
return sa.mlModel.GetCarrierPerformance(carrierID)
}
@@ -0,0 +1,331 @@
package smdp
import (
"math"
"time"
)
// LearningModel represents the machine learning model for carrier selection
type LearningModel struct {
// Performance tracking
carrierPerformance map[string]*PerformanceMetrics
learningRate float64
adaptationThreshold float64
// Weight optimization
baseWeights WeightVector
optimizedWeights WeightVector
// Historical data
performanceHistory map[string][]float64
selectionHistory map[string][]SelectionRecord
// Model parameters
momentum float64
regularization float64
decayRate float64
}
// PerformanceMetrics tracks carrier performance over time
type PerformanceMetrics struct {
SuccessRate float64
AverageResponseTime float64
Reliability float64
CostEfficiency float64
LastUpdated time.Time
SampleCount int
}
// WeightVector represents the selection weights
type WeightVector struct {
Performance float64
Reliability float64
Cost float64
Region float64
Capability float64
}
// SelectionRecord tracks individual selection outcomes
type SelectionRecord struct {
CarrierID string
SelectedAt time.Time
ExpectedScore float64
ActualScore float64
Performance float64
ResponseTime time.Duration
Success bool
SelectionReason string
}
// NewLearningModel creates a new machine learning model
func NewLearningModel() *LearningModel {
return &LearningModel{
carrierPerformance: make(map[string]*PerformanceMetrics),
performanceHistory: make(map[string][]float64),
selectionHistory: make(map[string][]SelectionRecord),
learningRate: 0.01,
adaptationThreshold: 0.05,
momentum: 0.9,
regularization: 0.001,
decayRate: 0.995,
baseWeights: WeightVector{
Performance: 0.3,
Reliability: 0.3,
Cost: 0.2,
Region: 0.1,
Capability: 0.1,
},
optimizedWeights: WeightVector{
Performance: 0.3,
Reliability: 0.3,
Cost: 0.2,
Region: 0.1,
Capability: 0.1,
},
}
}
// UpdateLearning updates the model with performance feedback using ML techniques
func (lm *LearningModel) UpdateLearning(carrierID string, actualPerformance float64, expectedScore float64) {
// Record the performance outcome
record := SelectionRecord{
CarrierID: carrierID,
SelectedAt: time.Now(),
ExpectedScore: expectedScore,
ActualScore: actualPerformance,
Performance: actualPerformance,
Success: actualPerformance >= 70.0, // 70% threshold for success
}
// Add to selection history
history := lm.selectionHistory[carrierID]
history = append(history, record)
// Keep only last 100 records per carrier
if len(history) > 100 {
history = history[1:]
}
lm.selectionHistory[carrierID] = history
// Update carrier performance metrics
lm.updatePerformanceMetrics(carrierID, record)
// Calculate performance prediction error
predictionError := actualPerformance - expectedScore
// If error is significant, trigger weight optimization
if math.Abs(predictionError) > lm.adaptationThreshold {
lm.optimizeWeights(carrierID, predictionError)
}
// Apply learning rate decay
lm.learningRate *= lm.decayRate
if lm.learningRate < 0.001 {
lm.learningRate = 0.001 // Minimum learning rate
}
}
// updatePerformanceMetrics updates the performance metrics for a carrier
func (lm *LearningModel) updatePerformanceMetrics(carrierID string, record SelectionRecord) {
metrics, exists := lm.carrierPerformance[carrierID]
if !exists {
metrics = &PerformanceMetrics{
LastUpdated: time.Now(),
}
lm.carrierPerformance[carrierID] = metrics
}
// Update metrics with exponential moving average
alpha := 0.1 // Smoothing factor
if metrics.SampleCount == 0 {
// First sample
metrics.SuccessRate = 1.0
if !record.Success {
metrics.SuccessRate = 0.0
}
metrics.AverageResponseTime = float64(record.ResponseTime.Milliseconds())
metrics.Reliability = record.Performance / 100.0
} else {
// Exponential moving average update
if record.Success {
metrics.SuccessRate = alpha*1.0 + (1-alpha)*metrics.SuccessRate
} else {
metrics.SuccessRate = alpha*0.0 + (1-alpha)*metrics.SuccessRate
}
responseTimeMs := float64(record.ResponseTime.Milliseconds())
metrics.AverageResponseTime = alpha*responseTimeMs + (1-alpha)*metrics.AverageResponseTime
metrics.Reliability = alpha*(record.Performance/100.0) + (1-alpha)*metrics.Reliability
}
metrics.CostEfficiency = 1.0 - (float64(record.ResponseTime.Milliseconds()) / 1000.0) // Simple cost proxy
metrics.LastUpdated = time.Now()
metrics.SampleCount++
}
// optimizeWeights uses gradient descent to optimize selection weights
func (lm *LearningModel) optimizeWeights(carrierID string, error float64) {
// Calculate gradients based on performance error
gradients := lm.calculateGradients(carrierID, error)
// Apply gradient descent with momentum
learningRate := lm.learningRate
// Update optimized weights
lm.optimizedWeights.Performance -= learningRate * gradients.Performance
lm.optimizedWeights.Reliability -= learningRate * gradients.Reliability
lm.optimizedWeights.Cost -= learningRate * gradients.Cost
lm.optimizedWeights.Region -= learningRate * gradients.Region
lm.optimizedWeights.Capability -= learningRate * gradients.Capability
// Apply regularization to prevent overfitting
lm.applyRegularization()
// Ensure weights sum to 1 and are non-negative
lm.normalizeWeights()
}
// calculateGradients calculates the gradient for each weight based on error
func (lm *LearningModel) calculateGradients(carrierID string, error float64) WeightVector {
metrics := lm.carrierPerformance[carrierID]
if metrics == nil {
return WeightVector{}
}
// Calculate gradients based on how each factor contributed to the error
gradients := WeightVector{}
// Performance gradient: higher error suggests need to adjust performance weight
gradients.Performance = error * (metrics.Reliability - 0.5) * 2.0
// Reliability gradient: adjust based on success rate
gradients.Reliability = error * (metrics.SuccessRate - 0.5) * 2.0
// Cost gradient: adjust based on cost efficiency
gradients.Cost = error * (metrics.CostEfficiency - 0.5) * 2.0
// Region and capability gradients (simplified)
gradients.Region = error * 0.1
gradients.Capability = error * 0.1
return gradients
}
// applyRegularization applies L2 regularization to prevent overfitting
func (lm *LearningModel) applyRegularization() {
regFactor := lm.regularization
lm.optimizedWeights.Performance *= (1.0 - regFactor)
lm.optimizedWeights.Reliability *= (1.0 - regFactor)
lm.optimizedWeights.Cost *= (1.0 - regFactor)
lm.optimizedWeights.Region *= (1.0 - regFactor)
lm.optimizedWeights.Capability *= (1.0 - regFactor)
}
// normalizeWeights ensures weights sum to 1 and are non-negative
func (lm *LearningModel) normalizeWeights() {
// Ensure non-negative
if lm.optimizedWeights.Performance < 0 {
lm.optimizedWeights.Performance = 0
}
if lm.optimizedWeights.Reliability < 0 {
lm.optimizedWeights.Reliability = 0
}
if lm.optimizedWeights.Cost < 0 {
lm.optimizedWeights.Cost = 0
}
if lm.optimizedWeights.Region < 0 {
lm.optimizedWeights.Region = 0
}
if lm.optimizedWeights.Capability < 0 {
lm.optimizedWeights.Capability = 0
}
// Normalize to sum to 1
total := lm.optimizedWeights.Performance + lm.optimizedWeights.Reliability +
lm.optimizedWeights.Cost + lm.optimizedWeights.Region + lm.optimizedWeights.Capability
if total > 0 {
lm.optimizedWeights.Performance /= total
lm.optimizedWeights.Reliability /= total
lm.optimizedWeights.Cost /= total
lm.optimizedWeights.Region /= total
lm.optimizedWeights.Capability /= total
} else {
// Fallback to base weights if all weights become zero
lm.optimizedWeights = lm.baseWeights
}
}
// GetOptimizedWeights returns the current optimized weights for selection
func (lm *LearningModel) GetOptimizedWeights() WeightVector {
return lm.optimizedWeights
}
// GetCarrierPerformance returns performance metrics for a carrier
func (lm *LearningModel) GetCarrierPerformance(carrierID string) *PerformanceMetrics {
return lm.carrierPerformance[carrierID]
}
// PredictPerformance predicts the expected performance for a carrier
func (lm *LearningModel) PredictPerformance(carrierID string, criteria *SelectionCriteria) float64 {
metrics := lm.carrierPerformance[carrierID]
if metrics == nil {
return 75.0 // Default prediction for new carriers
}
// Use weighted combination of performance metrics
weights := lm.optimizedWeights
prediction := metrics.SuccessRate*weights.Reliability*100 +
(1.0-metrics.AverageResponseTime/1000.0)*weights.Performance*100 +
metrics.CostEfficiency*weights.Cost*100 +
0.5*weights.Region*100 + // Simplified region prediction
0.5*weights.Capability*100 // Simplified capability prediction
return math.Min(100.0, math.Max(0.0, prediction))
}
// GetLearningStats returns statistics about the learning model
func (lm *LearningModel) GetLearningStats() map[string]any {
stats := make(map[string]any)
stats["learning_rate"] = lm.learningRate
stats["total_carriers_tracked"] = len(lm.carrierPerformance)
stats["optimized_weights"] = lm.optimizedWeights
stats["base_weights"] = lm.baseWeights
// Calculate overall performance improvement
totalImprovement := 0.0
sampleCount := 0
for _, history := range lm.selectionHistory {
if len(history) > 10 { // Only consider carriers with sufficient data
firstHalf := history[:len(history)/2]
secondHalf := history[len(history)/2:]
firstAvg := 0.0
for _, record := range firstHalf {
firstAvg += record.ActualScore
}
firstAvg /= float64(len(firstHalf))
secondAvg := 0.0
for _, record := range secondHalf {
secondAvg += record.ActualScore
}
secondAvg /= float64(len(secondHalf))
improvement := secondAvg - firstAvg
totalImprovement += improvement
sampleCount++
}
}
if sampleCount > 0 {
stats["average_performance_improvement"] = totalImprovement / float64(sampleCount)
}
return stats
}
@@ -0,0 +1,94 @@
package smdp
import (
"context"
"fmt"
"sort"
"time"
"github.com/sirupsen/logrus"
)
// SelectOptimalCarrier selects the best carrier based on multiple criteria
func (sa *SelectionAlgorithm) SelectOptimalCarrier(
ctx context.Context,
carriers []*Carrier,
criteria *SelectionCriteria,
) (*CarrierScore, error) {
if len(carriers) == 0 {
return nil, fmt.Errorf("no carriers available for selection")
}
sa.logger.WithFields(logrus.Fields{
"region": criteria.Region,
"profile_type": criteria.ProfileType,
"urgency": criteria.Urgency,
"carrier_count": len(carriers),
}).Info("Starting optimal carrier selection")
// Score all carriers
scores := make([]*CarrierScore, 0, len(carriers))
for _, carrier := range carriers {
if !carrier.IsActive || carrier.HealthStatus != CarrierStatusHealthy {
continue
}
score := sa.scoreCarrier(carrier, criteria)
scores = append(scores, score)
}
if len(scores) == 0 {
return nil, fmt.Errorf("no healthy carriers available")
}
// Sort by total score (descending)
sort.Slice(scores, func(i, j int) bool {
return scores[i].TotalScore > scores[j].TotalScore
})
// Select the best carrier
bestScore := scores[0]
sa.recordSelection(bestScore)
sa.logger.WithFields(logrus.Fields{
"selected_carrier": bestScore.CarrierID,
"total_score": bestScore.TotalScore,
"performance": bestScore.PerformanceScore,
"reliability": bestScore.ReliabilityScore,
"cost": bestScore.CostScore,
}).Info("Optimal carrier selected")
return bestScore, nil
}
// scoreCarrier calculates a comprehensive score for a carrier
func (sa *SelectionAlgorithm) scoreCarrier(carrier *Carrier, criteria *SelectionCriteria) *CarrierScore {
score := &CarrierScore{
Carrier: carrier,
CarrierID: carrier.ID,
SelectedAt: time.Now(),
}
// Performance score (0-100)
score.PerformanceScore = sa.calculatePerformanceScore(carrier)
// Reliability score (0-100)
score.ReliabilityScore = sa.calculateReliabilityScore(carrier)
// Cost score (0-100, higher is better/cheaper)
score.CostScore = sa.calculateCostScore(carrier, criteria)
// Region compatibility score (0-100)
score.RegionScore = sa.calculateRegionScore(carrier, criteria.Region)
// Capability score (0-100)
score.CapabilityScore = sa.calculateCapabilityScore(carrier, criteria.ProfileType)
// Calculate weighted total score
score.TotalScore = sa.calculateWeightedScore(score, criteria)
// Generate selection reason
score.Reason = sa.generateReason(score, criteria)
return score
}
@@ -0,0 +1,168 @@
package smdp
import (
"fmt"
"math"
"slices"
)
// calculatePerformanceScore evaluates carrier performance metrics
func (sa *SelectionAlgorithm) calculatePerformanceScore(carrier *Carrier) float64 {
metrics := carrier.Metrics
if metrics.TotalRequests == 0 {
return 50.0 // Neutral score for new carriers
}
// Success rate (40% weight)
successRate := float64(metrics.SuccessfulRequests) / float64(metrics.TotalRequests)
successScore := successRate * 40.0
// Response time score (30% weight) - lower is better
responseScore := 30.0
if metrics.AverageResponseTime > 0 {
// Normalize: 1s = 0 points, 100ms = 30 points
responseMs := metrics.AverageResponseTime.Milliseconds()
if responseMs <= 100 {
responseScore = 30.0
} else if responseMs >= 1000 {
responseScore = 0.0
} else {
responseScore = 30.0 * (1.0 - float64(responseMs-100)/900.0)
}
}
// Request rate score (30% weight) - moderate rate is optimal
requestScore := 15.0 // Base score
if metrics.RequestRate > 0 && metrics.RequestRate <= 100 {
requestScore = 30.0
} else if metrics.RequestRate > 100 {
// Penalize very high rates (potential overload)
requestScore = 30.0 * math.Max(0, 1.0-metrics.RequestRate/1000.0)
}
return successScore + responseScore + requestScore
}
// calculateReliabilityScore evaluates carrier reliability
func (sa *SelectionAlgorithm) calculateReliabilityScore(carrier *Carrier) float64 {
// Health status (40% weight)
healthScore := 0.0
switch carrier.HealthStatus {
case CarrierStatusHealthy:
healthScore = 40.0
case CarrierStatusDegraded:
healthScore = 20.0
case CarrierStatusUnhealthy:
healthScore = 0.0
default:
healthScore = 10.0
}
// Uptime based on recent performance (30% weight)
uptimeScore := 30.0
if carrier.Metrics.TotalRequests > 0 {
successRate := float64(carrier.Metrics.SuccessfulRequests) / float64(carrier.Metrics.TotalRequests)
uptimeScore = successRate * 30.0
}
// Priority score (30% weight) - higher priority = more reliable
priorityScore := float64(carrier.Priority) / 100.0 * 30.0
if priorityScore > 30.0 {
priorityScore = 30.0
}
return healthScore + uptimeScore + priorityScore
}
// calculateCostScore evaluates cost effectiveness
func (sa *SelectionAlgorithm) calculateCostScore(carrier *Carrier, criteria *SelectionCriteria) float64 {
// This would integrate with actual pricing data
// For now, use priority as a proxy (higher priority = potentially more expensive)
costScore := 100.0 - float64(carrier.Priority)
if costScore < 0 {
costScore = 0
}
// Apply cost sensitivity
if criteria.CostSensitivity > 0.5 {
costScore *= 1.5 // Boost cost score for cost-sensitive requests
}
return costScore
}
// calculateRegionScore evaluates regional compatibility
func (sa *SelectionAlgorithm) calculateRegionScore(carrier *Carrier, region string) float64 {
if region == "" {
return 50.0 // Neutral score if no region specified
}
// Check if carrier supports the region
if carrier.CountryCode == region {
return 100.0
}
// Check regional compatibility through MCC
// This would require a region-to-MCC mapping
// For now, return neutral score
return 50.0
}
// calculateCapabilityScore evaluates carrier capabilities
func (sa *SelectionAlgorithm) calculateCapabilityScore(carrier *Carrier, profileType string) float64 {
capabilities := carrier.Capabilities
score := 50.0 // Base score
// Check if profile type is supported
if slices.Contains(capabilities.SupportedProfileTypes, profileType) {
score += 30.0
}
// Check for advanced features
hasBulkDownload := false
hasRemoteProvisioning := false
for _, feature := range capabilities.Features {
if feature == "bulk_download" {
hasBulkDownload = true
}
if feature == "remote_provisioning" {
hasRemoteProvisioning = true
}
}
if hasBulkDownload {
score += 10.0
}
if hasRemoteProvisioning {
score += 10.0
}
return math.Min(100.0, score)
}
// generateReason creates a human-readable selection reason
func (sa *SelectionAlgorithm) generateReason(score *CarrierScore, criteria *SelectionCriteria) string {
reasons := []string{}
if score.PerformanceScore > 80 {
reasons = append(reasons, "excellent performance")
}
if score.ReliabilityScore > 80 {
reasons = append(reasons, "high reliability")
}
if score.CostScore > 80 {
reasons = append(reasons, "cost-effective")
}
if score.RegionScore > 80 {
reasons = append(reasons, "region-compatible")
}
if score.CapabilityScore > 80 {
reasons = append(reasons, "full capability support")
}
if len(reasons) == 0 {
return "best available option"
}
return fmt.Sprintf("selected for %s", reasons[0])
}
@@ -0,0 +1,166 @@
package smdp
import (
"context"
"testing"
)
// TestEdgeCases tests edge cases and boundary conditions
func TestEdgeCases(t *testing.T) {
selector := NewSelectionAlgorithm()
// Test empty carrier list
t.Run("EmptyCarrierList", func(t *testing.T) {
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
_, err := selector.SelectOptimalCarrier(context.Background(), []*Carrier{}, criteria)
if err == nil {
t.Error("Expected error for empty carrier list")
}
})
// Test all unhealthy carriers
t.Run("AllUnhealthyCarriers", func(t *testing.T) {
unhealthyCarriers := []*Carrier{
createUnhealthyCarrier("broken1", "Broken 1", "US"),
createUnhealthyCarrier("broken2", "Broken 2", "US"),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
_, err := selector.SelectOptimalCarrier(context.Background(), unhealthyCarriers, criteria)
if err == nil {
t.Error("Expected error when all carriers are unhealthy")
}
})
// Test inactive carriers
t.Run("InactiveCarriers", func(t *testing.T) {
inactiveCarriers := []*Carrier{
createInactiveCarrier("inactive1", "Inactive 1", "US"),
createInactiveCarrier("inactive2", "Inactive 2", "US"),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
_, err := selector.SelectOptimalCarrier(context.Background(), inactiveCarriers, criteria)
if err == nil {
t.Error("Expected error when all carriers are inactive")
}
})
}
// TestMultipleSelections tests behavior with multiple selections
func TestMultipleSelections(t *testing.T) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("carrier1", "Carrier 1", "US", 90, 95, 100, 5.0),
createTestCarrier("carrier2", "Carrier 2", "US", 85, 98, 120, 4.0),
createTestCarrier("carrier3", "Carrier 3", "US", 80, 97, 110, 6.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Perform multiple selections
selections := make(map[string]int)
for i := range 20 {
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Selection %d failed: %v", i, err)
}
selections[score.CarrierID]++
}
// Check that we have some distribution
totalSelections := 0
for _, count := range selections {
totalSelections += count
}
if totalSelections != 20 {
t.Errorf("Expected 20 total selections, got %d", totalSelections)
}
t.Logf("Selection distribution: %+v", selections)
}
// TestLearningFeedback tests the learning feedback mechanism
func TestLearningFeedback(t *testing.T) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("learning-carrier", "Learning Carrier", "US", 80, 90, 150, 5.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Initial selection
_, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Initial selection failed: %v", err)
}
initialLearningRate := selector.learningRate
// Update with poor performance
selector.UpdateLearning("learning-carrier", 30.0)
learningRateAfterPoor := selector.learningRate
// Update with good performance
selector.UpdateLearning("learning-carrier", 90.0)
learningRateAfterGood := selector.learningRate
// Verify learning rate changes
if learningRateAfterPoor >= initialLearningRate {
t.Error("Learning rate should decrease after poor performance")
}
if learningRateAfterGood <= learningRateAfterPoor {
t.Error("Learning rate should increase after good performance")
}
t.Logf("Learning rate progression: %.4f -> %.4f -> %.4f",
initialLearningRate, learningRateAfterPoor, learningRateAfterGood)
}
// Helper function to create inactive carrier
func createInactiveCarrier(id, name, country string) *Carrier {
carrier := createTestCarrier(id, name, country, 50, 50, 500, 1.0)
carrier.IsActive = false
return carrier
}
@@ -0,0 +1,95 @@
package smdp
import (
"context"
"testing"
)
// BenchmarkSelectionAlgorithm benchmarks the selection algorithm performance
func BenchmarkSelectionAlgorithm(b *testing.B) {
selector := NewSelectionAlgorithm()
carriers := make([]*Carrier, 100)
for i := range 100 {
carriers[i] = createTestCarrier(
"carrier-"+string(rune(i)),
"Carrier "+string(rune(i)),
"US",
50+i%50,
80+i%20,
100+int64(i*10),
1.0+float64(i)*0.1,
)
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
b.Fatalf("Selection failed: %v", err)
}
}
}
// BenchmarkScoring benchmarks individual scoring functions
func BenchmarkScoring(b *testing.B) {
selector := NewSelectionAlgorithm()
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
criteria := &SelectionCriteria{CostSensitivity: 0.5}
b.Run("PerformanceScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculatePerformanceScore(carrier)
}
})
b.Run("ReliabilityScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculateReliabilityScore(carrier)
}
})
b.Run("CostScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculateCostScore(carrier, criteria)
}
})
b.Run("RegionScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculateRegionScore(carrier, "US")
}
})
b.Run("CapabilityScore", func(b *testing.B) {
for i := 0; i < b.N; i++ {
selector.calculateCapabilityScore(carrier, "operational")
}
})
}
// BenchmarkWeightCalculation benchmarks weight calculation
func BenchmarkWeightCalculation(b *testing.B) {
selector := NewSelectionAlgorithm()
criteria := &SelectionCriteria{
Urgency: "medium",
PerformanceWeight: 0.4,
ReliabilityWeight: 0.3,
CostSensitivity: 0.3,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
selector.getWeights(criteria)
}
}
@@ -0,0 +1,204 @@
package smdp
import (
"context"
"testing"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/config"
)
// TestSelectionAlgorithm tests the intelligent carrier selection algorithm
func TestSelectionAlgorithm(t *testing.T) {
// Create selection algorithm
selector := NewSelectionAlgorithm()
// Create test carriers with different characteristics
carriers := []*Carrier{
createTestCarrier("att-us", "AT&T US", "US", 90, 98, 150, 10.5),
createTestCarrier("verizon-us", "Verizon US", "US", 85, 99, 120, 8.2),
createTestCarrier("tmobile-de", "T-Mobile DE", "DE", 75, 95, 200, 6.8),
createTestCarrier("orange-fr", "Orange FR", "FR", 70, 95, 180, 4.5),
}
// Test case 1: High priority US request
t.Run("HighPriorityUSRequest", func(t *testing.T) {
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "high",
CostSensitivity: 0.2,
PerformanceWeight: 0.6,
ReliabilityWeight: 0.6,
}
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier: %v", err)
}
// Should select a US carrier with high performance
if score.CarrierID != "att-us" && score.CarrierID != "verizon-us" {
t.Errorf("Expected US carrier, got %s", score.CarrierID)
}
if score.TotalScore < 80 {
t.Errorf("Expected high score for high priority request, got %.2f", score.TotalScore)
}
t.Logf("Selected carrier: %s with score %.2f", score.CarrierID, score.TotalScore)
})
// Test case 2: Cost-optimized European request
t.Run("CostOptimizedEURequest", func(t *testing.T) {
criteria := &SelectionCriteria{
Region: "DE",
ProfileType: "operational",
Urgency: "low",
CostSensitivity: 0.8,
PerformanceWeight: 0.2,
ReliabilityWeight: 0.3,
}
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier: %v", err)
}
// Should prefer European carrier for better region compatibility
if score.CarrierID != "tmobile-de" && score.CarrierID != "orange-fr" {
t.Logf("Note: Selected non-EU carrier %s for DE region", score.CarrierID)
}
t.Logf("Selected carrier: %s with score %.2f", score.CarrierID, score.TotalScore)
})
// Test case 3: Balanced global request
t.Run("BalancedGlobalRequest", func(t *testing.T) {
criteria := &SelectionCriteria{
Region: "",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier: %v", err)
}
// Should select the best overall performer
if score.TotalScore < 70 {
t.Errorf("Expected reasonable score for balanced request, got %.2f", score.TotalScore)
}
t.Logf("Selected carrier: %s with score %.2f", score.CarrierID, score.TotalScore)
})
// Test case 4: No healthy carriers available
t.Run("NoHealthyCarriers", func(t *testing.T) {
unhealthyCarriers := []*Carrier{
createUnhealthyCarrier("broken-carrier", "Broken Carrier", "US"),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
_, err := selector.SelectOptimalCarrier(context.Background(), unhealthyCarriers, criteria)
if err == nil {
t.Error("Expected error when no healthy carriers available")
}
t.Logf("Correctly returned error: %v", err)
})
}
// TestSelectionLearning tests the learning capabilities
func TestSelectionLearning(t *testing.T) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("test-carrier", "Test Carrier", "US", 80, 90, 150, 5.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Perform selection
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier: %v", err)
}
// Check selection history
history := selector.GetSelectionHistory("test-carrier")
if len(history) != 1 {
t.Errorf("Expected 1 selection in history, got %d", len(history))
}
// Update learning with performance feedback
selector.UpdateLearning("test-carrier", 85.0)
// Perform another selection to see if learning affects results
score2, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier on second attempt: %v", err)
}
t.Logf("First selection score: %.2f, Second selection score: %.2f",
score.TotalScore, score2.TotalScore)
}
// Helper functions
func createTestCarrier(id, name, country string, priority, successRate int, responseTime int64, requestRate float64) *Carrier {
return &Carrier{
ID: id,
Name: name,
CountryCode: country,
MCC: "310",
MNC: "410",
IsActive: true,
Priority: priority,
HealthStatus: CarrierStatusHealthy,
LastHealthCheck: time.Now(),
ES2Config: &config.ES2Config{
BaseURL: "https://example.com",
APIKey: "test-key",
InsecureSkipVerify: false,
FunctionalityRequesterID: "telecom-platform",
},
Capabilities: &CarrierCapabilities{
SupportedProfileTypes: []string{"operational", "testing"},
Features: []string{"bulk_download"},
MaxConcurrentRequests: 100,
},
Metrics: &CarrierMetrics{
TotalRequests: uint64(1000 * requestRate),
SuccessfulRequests: uint64(float64(1000*requestRate) * float64(successRate) / 100),
FailedRequests: uint64(float64(1000*requestRate) * float64(100-successRate) / 100),
AverageResponseTime: time.Duration(responseTime) * time.Millisecond,
RequestRate: requestRate,
},
}
}
func createUnhealthyCarrier(id, name, country string) *Carrier {
carrier := createTestCarrier(id, name, country, 50, 50, 500, 1.0)
carrier.HealthStatus = CarrierStatusUnhealthy
return carrier
}
@@ -0,0 +1,308 @@
package smdp
import (
"context"
"testing"
)
// TestMachineLearningModel tests the ML learning capabilities
func TestMachineLearningModel(t *testing.T) {
mlModel := NewLearningModel()
// Test initial state
t.Run("InitialState", func(t *testing.T) {
weights := mlModel.GetOptimizedWeights()
// Should start with base weights
if weights.Performance < 0.29 || weights.Performance > 0.31 {
t.Errorf("Expected performance weight around 0.3, got %.3f", weights.Performance)
}
stats := mlModel.GetLearningStats()
if stats["learning_rate"].(float64) <= 0 {
t.Error("Learning rate should be positive")
}
})
// Test learning updates
t.Run("LearningUpdates", func(t *testing.T) {
carrierID := "test-ml-carrier"
// Simulate multiple selections with feedback
for i := range 10 {
expectedScore := float64(70 + i*2) // Improving expected scores
actualScore := float64(65 + i*3) // Actual performance
mlModel.UpdateLearning(carrierID, actualScore, expectedScore)
}
// Check that weights have been optimized
optimizedWeights := mlModel.GetOptimizedWeights()
originalWeights := WeightVector{Performance: 0.3, Reliability: 0.3, Cost: 0.2, Region: 0.1, Capability: 0.1}
// Weights should have changed from original
if optimizedWeights.Performance == originalWeights.Performance &&
optimizedWeights.Reliability == originalWeights.Reliability {
t.Error("Expected weights to change after learning")
}
// Weights should still sum to 1
total := optimizedWeights.Performance + optimizedWeights.Reliability +
optimizedWeights.Cost + optimizedWeights.Region + optimizedWeights.Capability
if total < 0.99 || total > 1.01 {
t.Errorf("Weights should sum to 1.0, got %.3f", total)
}
})
// Test performance metrics tracking
t.Run("PerformanceMetrics", func(t *testing.T) {
carrierID := "metrics-carrier"
// Add some performance data
mlModel.UpdateLearning(carrierID, 85.0, 80.0)
mlModel.UpdateLearning(carrierID, 90.0, 85.0)
mlModel.UpdateLearning(carrierID, 78.0, 82.0)
metrics := mlModel.GetCarrierPerformance(carrierID)
if metrics == nil {
t.Fatal("Expected performance metrics for carrier")
}
if metrics.SampleCount != 3 {
t.Errorf("Expected 3 samples, got %d", metrics.SampleCount)
}
if metrics.SuccessRate < 0 || metrics.SuccessRate > 1 {
t.Errorf("Success rate should be between 0 and 1, got %.3f", metrics.SuccessRate)
}
})
// Test performance prediction
t.Run("PerformancePrediction", func(t *testing.T) {
carrierID := "prediction-carrier"
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Add training data
for i := range 5 {
mlModel.UpdateLearning(carrierID, 80.0+float64(i*2), 75.0+float64(i))
}
prediction := mlModel.PredictPerformance(carrierID, criteria)
if prediction < 0 || prediction > 100 {
t.Errorf("Prediction should be between 0 and 100, got %.2f", prediction)
}
// Prediction for unknown carrier should return default
unknownPrediction := mlModel.PredictPerformance("unknown-carrier", criteria)
if unknownPrediction != 75.0 {
t.Errorf("Expected default prediction for unknown carrier, got %.2f", unknownPrediction)
}
})
}
// TestMLIntegration tests the integration of ML with SelectionAlgorithm
func TestMLIntegration(t *testing.T) {
selector := NewSelectionAlgorithm()
// Create test carriers
carriers := []*Carrier{
createTestCarrier("ml-test-1", "ML Test 1", "US", 80, 85, 150, 5.0),
createTestCarrier("ml-test-2", "ML Test 2", "US", 75, 90, 120, 4.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Test initial selection
t.Run("InitialSelection", func(t *testing.T) {
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Selection failed: %v", err)
}
// Provide learning feedback
selector.UpdateLearning(score.CarrierID, 85.0)
// Check learning stats
stats := selector.GetLearningStats()
if stats["total_carriers_tracked"].(int) == 0 {
t.Error("Expected carriers to be tracked in ML model")
}
})
// Test learning over multiple iterations
t.Run("IterativeLearning", func(t *testing.T) {
weightsBefore := selector.mlModel.GetOptimizedWeights()
// Perform multiple selections with feedback
for i := range 5 {
score, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Selection %d failed: %v", i, err)
}
// Simulate performance feedback (varying performance)
actualPerformance := 70.0 + float64(i*3)
selector.UpdateLearning(score.CarrierID, actualPerformance)
}
weightsAfter := selector.mlModel.GetOptimizedWeights()
// Weights should have been optimized
if weightsAfter.Performance == weightsBefore.Performance &&
weightsAfter.Reliability == weightsBefore.Reliability {
t.Error("Expected weights to change after learning iterations")
}
// Check performance prediction
prediction := selector.PredictPerformance("ml-test-1", criteria)
if prediction < 0 || prediction > 100 {
t.Errorf("Prediction should be between 0 and 100, got %.2f", prediction)
}
})
// Test carrier performance metrics
t.Run("CarrierPerformanceMetrics", func(t *testing.T) {
// Get performance metrics for a carrier
metrics := selector.GetCarrierPerformance("ml-test-1")
if metrics == nil {
t.Error("Expected performance metrics for carrier")
}
if metrics.SampleCount == 0 {
t.Error("Expected sample count > 0 after learning")
}
})
}
// TestMLWeightOptimization tests the weight optimization logic
func TestMLWeightOptimization(t *testing.T) {
mlModel := NewLearningModel()
t.Run("GradientDescent", func(t *testing.T) {
carrierID := "gradient-test"
// Simulate consistent poor performance to trigger weight optimization
for range 10 {
expectedScore := 80.0
actualScore := 50.0 // Consistently poor performance
mlModel.UpdateLearning(carrierID, actualScore, expectedScore)
}
weights := mlModel.GetOptimizedWeights()
// After consistent poor performance, weights should be adjusted
// This is a simplified test - in practice, the optimization is more complex
total := weights.Performance + weights.Reliability + weights.Cost + weights.Region + weights.Capability
if total < 0.99 || total > 1.01 {
t.Errorf("Weights should still sum to 1.0 after optimization, got %.3f", total)
}
// All weights should be non-negative
if weights.Performance < 0 || weights.Reliability < 0 || weights.Cost < 0 ||
weights.Region < 0 || weights.Capability < 0 {
t.Error("All weights should be non-negative")
}
})
t.Run("Regularization", func(t *testing.T) {
// Test that regularization prevents extreme weight values
carrierID := "regularization-test"
// Simulate extreme performance variations
for i := range 20 {
expectedScore := 80.0
actualScore := 20.0 + float64(i*3) // Wide range of performance
mlModel.UpdateLearning(carrierID, actualScore, expectedScore)
}
weights := mlModel.GetOptimizedWeights()
// Weights should be reasonable (not too extreme)
for _, weight := range []float64{
weights.Performance, weights.Reliability, weights.Cost,
weights.Region, weights.Capability} {
if weight < 0.05 || weight > 0.7 {
t.Errorf("Weight %.3f seems too extreme (should be between 0.05 and 0.7)", weight)
}
}
})
}
// TestMLLearningRate tests the learning rate adaptation
func TestMLLearningRate(t *testing.T) {
mlModel := NewLearningModel()
initialLearningRate := mlModel.GetLearningStats()["learning_rate"].(float64)
// Add some learning data
for range 10 {
mlModel.UpdateLearning("rate-test", 75.0, 70.0)
}
finalLearningRate := mlModel.GetLearningStats()["learning_rate"].(float64)
// Learning rate should have decayed
if finalLearningRate >= initialLearningRate {
t.Error("Learning rate should decay over time")
}
// Learning rate should not go below minimum
if finalLearningRate < 0.001 {
t.Error("Learning rate should not go below minimum threshold")
}
}
// BenchmarkMLModel benchmarks the ML model performance
func BenchmarkMLModel(b *testing.B) {
mlModel := NewLearningModel()
carrierID := "benchmark-carrier"
b.ResetTimer()
for i := 0; i < b.N; i++ {
mlModel.UpdateLearning(carrierID, 75.0, 70.0)
}
}
// BenchmarkSelectionWithML benchmarks selection with ML integration
func BenchmarkSelectionWithML(b *testing.B) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("benchmark-1", "Benchmark 1", "US", 80, 85, 150, 5.0),
createTestCarrier("benchmark-2", "Benchmark 2", "US", 75, 90, 120, 4.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
b.Fatalf("Selection failed: %v", err)
}
selector.UpdateLearning("benchmark-1", 75.0)
}
}
@@ -0,0 +1,199 @@
package smdp
import (
"context"
"testing"
)
// TestScoringComponents tests individual scoring components
func TestScoringComponents(t *testing.T) {
selector := NewSelectionAlgorithm()
// Test performance scoring
t.Run("PerformanceScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
score := selector.calculatePerformanceScore(carrier)
if score < 0 || score > 100 {
t.Errorf("Performance score should be between 0 and 100, got %.2f", score)
}
t.Logf("Performance score: %.2f", score)
})
// Test reliability scoring
t.Run("ReliabilityScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
score := selector.calculateReliabilityScore(carrier)
if score < 0 || score > 100 {
t.Errorf("Reliability score should be between 0 and 100, got %.2f", score)
}
t.Logf("Reliability score: %.2f", score)
})
// Test cost scoring
t.Run("CostScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
criteria := &SelectionCriteria{CostSensitivity: 0.5}
score := selector.calculateCostScore(carrier, criteria)
if score < 0 || score > 100 {
t.Errorf("Cost score should be between 0 and 100, got %.2f", score)
}
t.Logf("Cost score: %.2f", score)
})
// Test region scoring
t.Run("RegionScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
// Test matching region
score1 := selector.calculateRegionScore(carrier, "US")
if score1 != 100.0 {
t.Errorf("Expected perfect score for matching region, got %.2f", score1)
}
// Test non-matching region
score2 := selector.calculateRegionScore(carrier, "DE")
if score2 != 50.0 {
t.Errorf("Expected neutral score for non-matching region, got %.2f", score2)
}
// Test empty region
score3 := selector.calculateRegionScore(carrier, "")
if score3 != 50.0 {
t.Errorf("Expected neutral score for empty region, got %.2f", score3)
}
t.Logf("Region scores - Match: %.2f, Non-match: %.2f, Empty: %.2f", score1, score2, score3)
})
// Test capability scoring
t.Run("CapabilityScoring", func(t *testing.T) {
carrier := createTestCarrier("test", "Test", "US", 80, 95, 150, 5.0)
// Test supported profile type
score1 := selector.calculateCapabilityScore(carrier, "operational")
if score1 < 80.0 {
t.Errorf("Expected high score for supported profile type, got %.2f", score1)
}
// Test unsupported profile type
score2 := selector.calculateCapabilityScore(carrier, "unsupported")
if score2 < 50.0 {
t.Errorf("Expected base score for unsupported profile type, got %.2f", score2)
}
t.Logf("Capability scores - Supported: %.2f, Unsupported: %.2f", score1, score2)
})
}
// TestWeightCalculation tests the weight calculation logic
func TestWeightCalculation(t *testing.T) {
selector := NewSelectionAlgorithm()
// Test default weights
t.Run("DefaultWeights", func(t *testing.T) {
criteria := &SelectionCriteria{
Urgency: "medium",
}
weights := selector.getWeights(criteria)
total := weights.performance + weights.reliability + weights.cost + weights.region + weights.capability
if total < 0.99 || total > 1.01 { // Allow for floating point precision
t.Errorf("Weights should sum to 1.0, got %.2f", total)
}
t.Logf("Default weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f",
weights.performance, weights.reliability, weights.cost, weights.region, weights.capability)
})
// Test high urgency weights
t.Run("HighUrgencyWeights", func(t *testing.T) {
criteria := &SelectionCriteria{
Urgency: "high",
}
weights := selector.getWeights(criteria)
if weights.performance < 0.39 || weights.reliability < 0.39 {
t.Errorf("High urgency should prioritize performance and reliability")
}
t.Logf("High urgency weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f",
weights.performance, weights.reliability, weights.cost, weights.region, weights.capability)
})
// Test low urgency weights
t.Run("LowUrgencyWeights", func(t *testing.T) {
criteria := &SelectionCriteria{
Urgency: "low",
}
weights := selector.getWeights(criteria)
if weights.cost < 0.39 {
t.Errorf("Low urgency should prioritize cost")
}
t.Logf("Low urgency weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f",
weights.performance, weights.reliability, weights.cost, weights.region, weights.capability)
})
// Test custom weights
t.Run("CustomWeights", func(t *testing.T) {
criteria := &SelectionCriteria{
Urgency: "medium",
PerformanceWeight: 0.7,
ReliabilityWeight: 0.2,
CostSensitivity: 0.1,
}
weights := selector.getWeights(criteria)
total := weights.performance + weights.reliability + weights.cost + weights.region + weights.capability
if total < 0.99 || total > 1.01 {
t.Errorf("Custom weights should sum to 1.0, got %.2f", total)
}
t.Logf("Custom weights - P: %.2f, R: %.2f, C: %.2f, Reg: %.2f, Cap: %.2f",
weights.performance, weights.reliability, weights.cost, weights.region, weights.capability)
})
}
// TestSelectionHistory tests the selection history functionality
func TestSelectionHistory(t *testing.T) {
selector := NewSelectionAlgorithm()
carriers := []*Carrier{
createTestCarrier("test-carrier", "Test Carrier", "US", 80, 90, 150, 5.0),
}
criteria := &SelectionCriteria{
Region: "US",
ProfileType: "operational",
Urgency: "medium",
CostSensitivity: 0.5,
PerformanceWeight: 0.4,
ReliabilityWeight: 0.4,
}
// Perform multiple selections
for i := range 5 {
_, err := selector.SelectOptimalCarrier(context.Background(), carriers, criteria)
if err != nil {
t.Fatalf("Failed to select optimal carrier on iteration %d: %v", i, err)
}
}
// Check selection history
history := selector.GetSelectionHistory("test-carrier")
if len(history) != 5 {
t.Errorf("Expected 5 selections in history, got %d", len(history))
}
// Test learning feedback
selector.UpdateLearning("test-carrier", 85.0)
t.Logf("Selection history contains %d entries", len(history))
}