mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-27 07:02:15 +00:00
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:
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user