New features (#63)

* 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

* refactor: Simplify profile handler function names by removing WithRepo suffix

- Rename ListProfilesHandlerWithRepo to ListProfilesHandler
- Rename GetProfileHandlerWithRepo to GetProfileHandler
- Rename DeleteProfileHandlerWithRepo to DeleteProfileHandler

* feat: Add pricing engine with rate plan validation, cost calculation, and price optimization

- Add PricingEngine struct with repository and logger fields
- Add ValidateRatePlan with comprehensive validation for fields, dates, allowances, overage rates, discounts, and early termination
- Add CalculateOptimalPrice with market analysis and recommended pricing based on competitor data
- Add ValidateSubscription with plan availability, validity dates, and discount validation
- Add CalculateSubscriptionCost with base

* feat: Add rate plan analytics handlers with usage, revenue, popularity, and dashboard endpoints

- Add AnalyticsResponse struct with success, message, and data fields
- Add GetUsageAnalytics with date range parsing, default last month period, and group by support
- Add GetRevenueAnalytics with RFC3339 date parsing and configurable filters
- Add GetPopularPlans with configurable limit defaulting to 10
- Add GetDashboardData aggregating top 5 popular plans, 30-day usage analytics, and revenue analytics

* feat: Add rate plan handler layer with CRUD operations, management endpoints, and analytics support

- Add RatePlanHandler struct with service and logger fields
- Add CreateRatePlan with JSON binding, draft status initialization, and comprehensive field mapping
- Add GetRatePlan with ID path parameter extraction and not found handling
- Add UpdateRatePlan with selective field updates and existing plan retrieval
- Add DeleteRatePlan with active subscription conflict detection
- Add ListRatePlans with carrier

* feat: Add rate plan type system with comprehensive domain models, allowances, subscriptions, and analytics support

- Add RatePlan struct with carrier, region, pricing, allowances, features, discounts, and validity fields
- Add PlanType enum with prepaid, postpaid, hybrid, pay-as-you-go, and unlimited options
- Add PlanStatus enum with draft, active, inactive, archived, and suspended states
- Add BillingCycle enum with daily, weekly, monthly, quarterly, and yearly options
- Add DataAllowance, VoiceAllowance, and

* feat: Add rate plan service layer with CRUD operations, subscription management, usage tracking, and analytics support

- Add Service struct with repository and logger fields
- Add CreateRatePlan with ID generation, validation, and comprehensive field checks
- Add GetRatePlan, UpdateRatePlan, and DeleteRatePlan with active subscription conflict detection
- Add ListRatePlans and SearchRatePlans with filtering and sorting support
- Add SubscribeToPlan with plan availability validation, duplicate subscription

* feat: Add carrier integration layer with optimal carrier selection, rate plan recommendations, and analytics support

- Add CarrierSelectionIntegrator struct with rate plan repository, SMDP manager, and logger fields
- Add GetOptimalCarrierWithRatePlan with region filtering, carrier grouping, performance scoring, and combined evaluation
- Add RecommendRatePlansForCarrier with carrier validation, active plan filtering, and recommendation creation
- Add UpdateCarrierSelectionCriteria with 30-day usage analytics

* feat: Add rate plan repository interface with GORM models and extended types for carrier integration

- Add Repository interface with rate plan CRUD, subscription management, usage tracking, and analytics operations
- Add GORM table name methods for RatePlan, RatePlanSubscription, and RatePlanUsage models
- Add GORM hooks with automatic timestamp handling for BeforeCreate and BeforeUpdate operations
- Add SearchCriteria with carrier, region, plan type, price range, pagination, and sorting fields

* feat: Refactor pricing engine by extracting calculation methods into separate file

- Move calculateDataOverage, calculateVoiceOverage, calculateSMSOverage, calculateDiscounts, and calculateRecommendedPrice to price_calculation.go
- Remove validation helper methods from pricing_engine.go
- Remove PriceOptimization and CostBreakdown type definitions
- Update Logger interface to use `any` instead of `interface{}`
- Remove inline comments throughout pricing_engine.go
- Add switch statement for discount type handling in calculateDiscounts

* refactor: Extract core rate plan types into separate file

- Move RatePlan struct from types.go to types_core.go
- Move PlanType enum with prepaid, postpaid, hybrid, pay-as-you-go, and unlimited options to types_core.go
- Move PlanStatus enum with draft, active, inactive, and archived states to types_core.go
- Move BillingCycle enum with daily, weekly, monthly, quarterly, and yearly options to types_core.go
- Add documentation comments for PlanType, PlanStatus, and BillingCycle enums
- Remove PlanStatusSuspended from PlanStatus enum

* feat: Add GORM repository implementation with CRUD operations, subscription management, usage tracking, and analytics support

- Add GormRepository struct with database and logger fields
- Add CreateRatePlan, GetRatePlan, UpdateRatePlan, and DeleteRatePlan with automatic timestamp handling
- Add ListRatePlans with carrier, region, plan type, status, price range, and validity filtering
- Add CountRatePlans with comprehensive filter support
- Add CreateSubscription, GetSubscription, UpdateSubscription, and DeleteSubscription with timestamp

* feat: Add carrier configuration layer with GORM repository, file loading, and model conversion support

- Add GormCarrierRepository struct with database and logger fields
- Add CarrierModel with ID, name, MCC, MNC, country code, active status, priority, ES2 config, capabilities, and timestamp fields
- Add GetCarriers with active carrier filtering and model-to-carrier conversion
- Add GetCarrier with ID lookup and not found handling
- Add SaveCarrier with upsert logic for create or update operations

* feat: Add carrier configuration file loading with JSON-based carrier setup and validation

- Add carriers.json with AT&T US, Verizon US, T-Mobile Germany, Orange France, and Vodafone UK configurations
- Add LoadCarriersFromFile with JSON parsing and config file reading
- Add ConvertConfigToCarriers with model-to-carrier conversion logic
- Add validateCarrier with ID, name, MCC, MNC, country code, and ES2 config validation
- Replace hardcoded carrier setup with file-based configuration loading
- Add success

* refactor: Extract service methods into separate files for better organization

- Move GetUsage, GetUsageHistory, GetUsageAnalytics, GetRevenueAnalytics, and GetPopularPlans to service_analytics.go
- Move validateRatePlan, validateSubscribeRequest, calculateNextBillingDate, and calculateCycleEnd to service_methods.go
- Move SubscribeToPlan, GetSubscription, UpdateSubscription, CancelSubscription, GetActiveSubscription, and ListSubscriptions to service_subscriptions.go
- Move RecordUsage to service_usage.go
This commit is contained in:
Nutcase
2026-05-02 00:22:28 +03:00
committed by GitHub
parent 626a6f7349
commit 488c1767e0
28 changed files with 4171 additions and 121 deletions
@@ -0,0 +1,104 @@
{
"carriers": [
{
"id": "att-us",
"name": "AT&T US",
"mcc": "310",
"mnc": "410",
"country_code": "US",
"is_active": true,
"priority": 90,
"es2_config": {
"base_url": "https://es2plus.att.com",
"api_key": "production-api-key-att",
"insecure_skip_verify": false,
"functionality_requester_id": "telecom-platform"
},
"capabilities": {
"supported_profile_types": ["operational", "testing"],
"features": ["bulk_download", "remote_provisioning"],
"max_concurrent_requests": 100
}
},
{
"id": "verizon-us",
"name": "Verizon US",
"mcc": "311",
"mnc": "480",
"country_code": "US",
"is_active": true,
"priority": 85,
"es2_config": {
"base_url": "https://es2plus.verizon.com",
"api_key": "production-api-key-verizon",
"insecure_skip_verify": false,
"functionality_requester_id": "telecom-platform"
},
"capabilities": {
"supported_profile_types": ["operational", "testing"],
"features": ["bulk_download"],
"max_concurrent_requests": 80
}
},
{
"id": "tmobile-de",
"name": "T-Mobile Germany",
"mcc": "262",
"mnc": "01",
"country_code": "DE",
"is_active": true,
"priority": 75,
"es2_config": {
"base_url": "https://es2plus.t-mobile.de",
"api_key": "production-api-key-tmobile",
"insecure_skip_verify": false,
"functionality_requester_id": "telecom-platform"
},
"capabilities": {
"supported_profile_types": ["operational"],
"features": ["remote_provisioning"],
"max_concurrent_requests": 60
}
},
{
"id": "orange-fr",
"name": "Orange France",
"mcc": "208",
"mnc": "01",
"country_code": "FR",
"is_active": true,
"priority": 70,
"es2_config": {
"base_url": "https://es2plus.orange.fr",
"api_key": "production-api-key-orange",
"insecure_skip_verify": false,
"functionality_requester_id": "telecom-platform"
},
"capabilities": {
"supported_profile_types": ["operational", "testing"],
"features": [],
"max_concurrent_requests": 50
}
},
{
"id": "vodafone-uk",
"name": "Vodafone UK",
"mcc": "234",
"mnc": "15",
"country_code": "GB",
"is_active": true,
"priority": 80,
"es2_config": {
"base_url": "https://es2plus.vodafone.co.uk",
"api_key": "production-api-key-vodafone",
"insecure_skip_verify": false,
"functionality_requester_id": "telecom-platform"
},
"capabilities": {
"supported_profile_types": ["operational", "testing"],
"features": ["bulk_download", "remote_provisioning"],
"max_concurrent_requests": 75
}
}
]
}
@@ -0,0 +1,185 @@
package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
)
// AnalyticsResponse represents the response for analytics operations
type AnalyticsResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
}
// GetUsageAnalytics handles retrieving usage analytics
func (h *RatePlanHandler) GetUsageAnalytics(c *gin.Context) {
// Parse query parameters
filter := &rateplan.UsageAnalyticsFilter{
RatePlanID: c.Query("rate_plan_id"),
CarrierID: c.Query("carrier_id"),
Region: c.Query("region"),
GroupBy: c.Query("group_by"),
}
// Parse date parameters
if startDateStr := c.Query("start_date"); startDateStr != "" {
if startDate, err := time.Parse(time.RFC3339, startDateStr); err == nil {
filter.StartDate = startDate
}
}
if endDateStr := c.Query("end_date"); endDateStr != "" {
if endDate, err := time.Parse(time.RFC3339, endDateStr); err == nil {
filter.EndDate = endDate
}
}
// Set default date range if not provided
if filter.StartDate.IsZero() {
filter.StartDate = time.Now().AddDate(0, -1, 0) // Last month
}
if filter.EndDate.IsZero() {
filter.EndDate = time.Now()
}
analytics, err := h.service.GetUsageAnalytics(c.Request.Context(), filter)
if err != nil {
h.logger.WithError(err).Error("Failed to get usage analytics")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get usage analytics")
return
}
response := AnalyticsResponse{
Success: true,
Data: analytics,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// GetRevenueAnalytics handles retrieving revenue analytics
func (h *RatePlanHandler) GetRevenueAnalytics(c *gin.Context) {
// Parse query parameters
filter := &rateplan.RevenueAnalyticsFilter{
RatePlanID: c.Query("rate_plan_id"),
CarrierID: c.Query("carrier_id"),
Region: c.Query("region"),
GroupBy: c.Query("group_by"),
}
// Parse date parameters
if startDateStr := c.Query("start_date"); startDateStr != "" {
if startDate, err := time.Parse(time.RFC3339, startDateStr); err == nil {
filter.StartDate = startDate
}
}
if endDateStr := c.Query("end_date"); endDateStr != "" {
if endDate, err := time.Parse(time.RFC3339, endDateStr); err == nil {
filter.EndDate = endDate
}
}
// Set default date range if not provided
if filter.StartDate.IsZero() {
filter.StartDate = time.Now().AddDate(0, -1, 0) // Last month
}
if filter.EndDate.IsZero() {
filter.EndDate = time.Now()
}
analytics, err := h.service.GetRevenueAnalytics(c.Request.Context(), filter)
if err != nil {
h.logger.WithError(err).Error("Failed to get revenue analytics")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get revenue analytics")
return
}
response := AnalyticsResponse{
Success: true,
Data: analytics,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// GetPopularPlans handles retrieving the most popular rate plans
func (h *RatePlanHandler) GetPopularPlans(c *gin.Context) {
limit := 10 // default limit
if limitStr := c.Query("limit"); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
plans, err := h.service.GetPopularPlans(c.Request.Context(), limit)
if err != nil {
h.logger.WithError(err).Error("Failed to get popular plans")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get popular plans")
return
}
response := RatePlansResponse{
Success: true,
Data: plans,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// GetDashboardData handles retrieving dashboard data for rate plans
func (h *RatePlanHandler) GetDashboardData(c *gin.Context) {
// Get popular plans (top 5)
popularPlans, err := h.service.GetPopularPlans(c.Request.Context(), 5)
if err != nil {
h.logger.WithError(err).Error("Failed to get popular plans for dashboard")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get dashboard data")
return
}
// Get usage analytics for last 30 days
usageFilter := &rateplan.UsageAnalyticsFilter{
StartDate: time.Now().AddDate(0, 0, -30),
EndDate: time.Now(),
}
usageAnalytics, err := h.service.GetUsageAnalytics(c.Request.Context(), usageFilter)
if err != nil {
h.logger.WithError(err).Error("Failed to get usage analytics for dashboard")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get dashboard data")
return
}
// Get revenue analytics for last 30 days
revenueFilter := &rateplan.RevenueAnalyticsFilter{
StartDate: time.Now().AddDate(0, 0, -30),
EndDate: time.Now(),
}
revenueAnalytics, err := h.service.GetRevenueAnalytics(c.Request.Context(), revenueFilter)
if err != nil {
h.logger.WithError(err).Error("Failed to get revenue analytics for dashboard")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get dashboard data")
return
}
dashboardData := map[string]interface{}{
"popular_plans": popularPlans,
"usage_analytics": usageAnalytics,
"revenue_analytics": revenueAnalytics,
"generated_at": time.Now().Format(time.RFC3339),
}
response := AnalyticsResponse{
Success: true,
Data: dashboardData,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
@@ -0,0 +1,114 @@
package handlers
import (
"time"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
)
// RatePlanHandler handles rate plan API endpoints
type RatePlanHandler struct {
service *rateplan.Service
logger *logrus.Logger
}
// NewRatePlanHandler creates a new rate plan handler
func NewRatePlanHandler(service *rateplan.Service) *RatePlanHandler {
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
return &RatePlanHandler{
service: service,
logger: logger,
}
}
// CreateRatePlanRequest represents the request to create a rate plan
type CreateRatePlanRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
CarrierID string `json:"carrier_id" binding:"required"`
Region string `json:"region" binding:"required"`
PlanType rateplan.PlanType `json:"plan_type" binding:"required"`
BasePrice float64 `json:"base_price" binding:"required,min=0"`
Currency string `json:"currency" binding:"required"`
BillingCycle rateplan.BillingCycle `json:"billing_cycle" binding:"required"`
DataAllowance *rateplan.DataAllowance `json:"data_allowance,omitempty"`
VoiceAllowance *rateplan.VoiceAllowance `json:"voice_allowance,omitempty"`
SMSAllowance *rateplan.SMSAllowance `json:"sms_allowance,omitempty"`
OverageRates *rateplan.OverageRates `json:"overage_rates,omitempty"`
Features []rateplan.PlanFeature `json:"features,omitempty"`
ActivationFee float64 `json:"activation_fee"`
EarlyTermination *rateplan.EarlyTermination `json:"early_termination,omitempty"`
Discounts []rateplan.Discount `json:"discounts,omitempty"`
ValidFrom time.Time `json:"valid_from" binding:"required"`
ValidTo *time.Time `json:"valid_to,omitempty"`
Priority int `json:"priority"`
IsActive bool `json:"is_active"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// UpdateRatePlanRequest represents the request to update a rate plan
type UpdateRatePlanRequest struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
PlanType rateplan.PlanType `json:"plan_type,omitempty"`
Status rateplan.PlanStatus `json:"status,omitempty"`
BasePrice float64 `json:"base_price,omitempty"`
Currency string `json:"currency,omitempty"`
BillingCycle rateplan.BillingCycle `json:"billing_cycle,omitempty"`
DataAllowance *rateplan.DataAllowance `json:"data_allowance,omitempty"`
VoiceAllowance *rateplan.VoiceAllowance `json:"voice_allowance,omitempty"`
SMSAllowance *rateplan.SMSAllowance `json:"sms_allowance,omitempty"`
OverageRates *rateplan.OverageRates `json:"overage_rates,omitempty"`
Features []rateplan.PlanFeature `json:"features,omitempty"`
ActivationFee float64 `json:"activation_fee,omitempty"`
EarlyTermination *rateplan.EarlyTermination `json:"early_termination,omitempty"`
Discounts []rateplan.Discount `json:"discounts,omitempty"`
ValidFrom *time.Time `json:"valid_from,omitempty"`
ValidTo *time.Time `json:"valid_to,omitempty"`
Priority int `json:"priority,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// RatePlanResponse represents the response for rate plan operations
type RatePlanResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data *rateplan.RatePlan `json:"data,omitempty"`
}
// RatePlansResponse represents the response for listing rate plans
type RatePlansResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data []*rateplan.RatePlan `json:"data,omitempty"`
Total int `json:"total,omitempty"`
}
// writeJSONResponse writes a JSON response
func (h *RatePlanHandler) writeJSONResponse(c *gin.Context, statusCode int, data interface{}) {
c.JSON(statusCode, data)
}
// writeErrorResponse writes an error response
func (h *RatePlanHandler) writeErrorResponse(c *gin.Context, statusCode int, message string) {
response := struct {
Success bool `json:"success"`
Error string `json:"error"`
}{
Success: false,
Error: message,
}
h.writeJSONResponse(c, statusCode, response)
}
// extractIDFromPath extracts ID from URL path
func (h *RatePlanHandler) extractIDFromPath(c *gin.Context) string {
return c.Param("id")
}
@@ -0,0 +1,336 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
)
// CreateRatePlan handles the creation of a new rate plan
func (h *RatePlanHandler) CreateRatePlan(c *gin.Context) {
var req CreateRatePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error())
return
}
// Convert request to rate plan
plan := &rateplan.RatePlan{
Name: req.Name,
Description: req.Description,
CarrierID: req.CarrierID,
Region: req.Region,
PlanType: req.PlanType,
Status: rateplan.PlanStatusDraft,
BasePrice: req.BasePrice,
Currency: req.Currency,
BillingCycle: req.BillingCycle,
DataAllowance: req.DataAllowance,
VoiceAllowance: req.VoiceAllowance,
SMSAllowance: req.SMSAllowance,
OverageRates: req.OverageRates,
Features: req.Features,
ActivationFee: req.ActivationFee,
EarlyTermination: req.EarlyTermination,
Discounts: req.Discounts,
ValidFrom: req.ValidFrom,
ValidTo: req.ValidTo,
Priority: req.Priority,
IsActive: req.IsActive,
Metadata: req.Metadata,
}
// Create the rate plan
createdPlan, err := h.service.CreateRatePlan(c.Request.Context(), plan)
if err != nil {
h.logger.WithError(err).Error("Failed to create rate plan")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to create rate plan: "+err.Error())
return
}
response := RatePlanResponse{
Success: true,
Message: "Rate plan created successfully",
Data: createdPlan,
}
h.writeJSONResponse(c, http.StatusCreated, response)
}
// GetRatePlan handles retrieving a rate plan by ID
func (h *RatePlanHandler) GetRatePlan(c *gin.Context) {
id := h.extractIDFromPath(c)
if id == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required")
return
}
plan, err := h.service.GetRatePlan(c.Request.Context(), id)
if err != nil {
if err.Error() == "rate plan not found: "+id {
h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found")
return
}
h.logger.WithError(err).Error("Failed to get rate plan")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get rate plan")
return
}
response := RatePlanResponse{
Success: true,
Data: plan,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// UpdateRatePlan handles updating an existing rate plan
func (h *RatePlanHandler) UpdateRatePlan(c *gin.Context) {
id := h.extractIDFromPath(c)
if id == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required")
return
}
var req UpdateRatePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error())
return
}
// Get existing rate plan
plan, err := h.service.GetRatePlan(c.Request.Context(), id)
if err != nil {
if err.Error() == "rate plan not found: "+id {
h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found")
return
}
h.logger.WithError(err).Error("Failed to get rate plan")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get rate plan")
return
}
// Update fields if provided
if req.Name != "" {
plan.Name = req.Name
}
if req.Description != "" {
plan.Description = req.Description
}
if req.PlanType != "" {
plan.PlanType = req.PlanType
}
if req.Status != "" {
plan.Status = req.Status
}
if req.BasePrice > 0 {
plan.BasePrice = req.BasePrice
}
if req.Currency != "" {
plan.Currency = req.Currency
}
if req.BillingCycle != "" {
plan.BillingCycle = req.BillingCycle
}
if req.DataAllowance != nil {
plan.DataAllowance = req.DataAllowance
}
if req.VoiceAllowance != nil {
plan.VoiceAllowance = req.VoiceAllowance
}
if req.SMSAllowance != nil {
plan.SMSAllowance = req.SMSAllowance
}
if req.OverageRates != nil {
plan.OverageRates = req.OverageRates
}
if req.Features != nil {
plan.Features = req.Features
}
if req.ActivationFee >= 0 {
plan.ActivationFee = req.ActivationFee
}
if req.EarlyTermination != nil {
plan.EarlyTermination = req.EarlyTermination
}
if req.Discounts != nil {
plan.Discounts = req.Discounts
}
if req.ValidFrom != nil {
plan.ValidFrom = *req.ValidFrom
}
if req.ValidTo != nil {
plan.ValidTo = req.ValidTo
}
if req.Priority != 0 {
plan.Priority = req.Priority
}
if req.IsActive != nil {
plan.IsActive = *req.IsActive
}
if req.Metadata != nil {
plan.Metadata = req.Metadata
}
// Update the rate plan
updatedPlan, err := h.service.UpdateRatePlan(c.Request.Context(), plan)
if err != nil {
h.logger.WithError(err).Error("Failed to update rate plan")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to update rate plan: "+err.Error())
return
}
response := RatePlanResponse{
Success: true,
Message: "Rate plan updated successfully",
Data: updatedPlan,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// DeleteRatePlan handles deleting a rate plan
func (h *RatePlanHandler) DeleteRatePlan(c *gin.Context) {
id := h.extractIDFromPath(c)
if id == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required")
return
}
if err := h.service.DeleteRatePlan(c.Request.Context(), id); err != nil {
if err.Error() == "rate plan not found: "+id {
h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found")
return
}
if err.Error() == "cannot delete rate plan with active subscriptions" {
h.writeErrorResponse(c, http.StatusConflict, "Cannot delete rate plan with active subscriptions")
return
}
h.logger.WithError(err).Error("Failed to delete rate plan")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to delete rate plan")
return
}
response := struct {
Success bool `json:"success"`
Message string `json:"message"`
}{
Success: true,
Message: "Rate plan deleted successfully",
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// ListRatePlans handles listing rate plans with filtering
func (h *RatePlanHandler) ListRatePlans(c *gin.Context) {
// Parse query parameters
filter := &rateplan.RatePlanFilter{
CarrierID: c.Query("carrier_id"),
Region: c.Query("region"),
PlanType: rateplan.PlanType(c.Query("plan_type")),
Status: rateplan.PlanStatus(c.Query("status")),
SortBy: c.Query("sort_by"),
SortOrder: c.Query("sort_order"),
}
// Parse numeric parameters
if minPriceStr := c.Query("min_price"); minPriceStr != "" {
if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil {
filter.MinPrice = minPrice
}
}
if maxPriceStr := c.Query("max_price"); maxPriceStr != "" {
if maxPrice, err := strconv.ParseFloat(maxPriceStr, 64); err == nil {
filter.MaxPrice = maxPrice
}
}
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
filter.Limit = limit
}
}
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 {
filter.Offset = offset
}
}
// Parse boolean parameter
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
filter.IsActive = &isActive
}
}
// Get rate plans
plans, err := h.service.ListRatePlans(c.Request.Context(), filter)
if err != nil {
h.logger.WithError(err).Error("Failed to list rate plans")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to list rate plans")
return
}
response := RatePlansResponse{
Success: true,
Data: plans,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// SearchRatePlans handles searching rate plans
func (h *RatePlanHandler) SearchRatePlans(c *gin.Context) {
criteria := rateplan.SearchCriteria{
CarrierID: c.Query("carrier_id"),
Region: c.Query("region"),
PlanType: rateplan.PlanType(c.Query("plan_type")),
SortBy: c.Query("sort_by"),
SortOrder: c.Query("sort_order"),
}
// Parse numeric parameters
if minPriceStr := c.Query("min_price"); minPriceStr != "" {
if minPrice, err := strconv.ParseFloat(minPriceStr, 64); err == nil {
criteria.MinPrice = minPrice
}
}
if maxPriceStr := c.Query("max_price"); maxPriceStr != "" {
if maxPrice, err := strconv.ParseFloat(maxPriceStr, 64); err == nil {
criteria.MaxPrice = maxPrice
}
}
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
criteria.Limit = limit
}
}
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 {
criteria.Offset = offset
}
}
plans, err := h.service.SearchRatePlans(c.Request.Context(), criteria)
if err != nil {
h.logger.WithError(err).Error("Failed to search rate plans")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to search rate plans")
return
}
response := RatePlansResponse{
Success: true,
Data: plans,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
@@ -0,0 +1,385 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
)
// ManagementEndpoints provides management UI endpoints for rate plans
func (h *RatePlanHandler) RegisterManagementRoutes(router *gin.RouterGroup) {
management := router.Group("/management")
{
// Dashboard endpoints
management.GET("/dashboard", h.GetManagementDashboard)
management.GET("/overview", h.GetSystemOverview)
// Rate plan management
management.POST("/plans/bulk", h.BulkCreateRatePlans)
management.PUT("/plans/bulk", h.BulkUpdateRatePlans)
management.DELETE("/plans/bulk", h.BulkDeleteRatePlans)
management.POST("/plans/:id/activate", h.ActivateRatePlan)
management.POST("/plans/:id/deactivate", h.DeactivateRatePlan)
management.POST("/plans/:id/duplicate", h.DuplicateRatePlan)
// Subscription management
management.POST("/subscriptions/bulk-cancel", h.BulkCancelSubscriptions)
management.POST("/subscriptions/:id/suspend", h.SuspendSubscription)
management.POST("/subscriptions/:id/reactivate", h.ReactivateSubscription)
management.POST("/subscriptions/:id/change-plan", h.ChangeSubscriptionPlan)
// Analytics and reporting
management.GET("/reports/usage", h.GetUsageReport)
management.GET("/reports/revenue", h.GetRevenueReport)
management.GET("/reports/performance", h.GetPerformanceReport)
management.POST("/reports/export", h.ExportReport)
// Configuration and settings
management.GET("/config/pricing", h.GetPricingConfiguration)
management.PUT("/config/pricing", h.UpdatePricingConfiguration)
management.GET("/config/validation", h.GetValidationRules)
management.PUT("/config/validation", h.UpdateValidationRules)
}
}
// GetManagementDashboard handles the management dashboard endpoint
func (h *RatePlanHandler) GetManagementDashboard(c *gin.Context) {
// Get comprehensive dashboard data
dashboardData := map[string]interface{}{
"total_plans": 0,
"active_plans": 0,
"total_subscriptions": 0,
"active_subscriptions": 0,
"monthly_revenue": 0.0,
"total_users": 0,
"system_health": "healthy",
"last_updated": "2024-01-01T00:00:00Z",
"alerts": []map[string]interface{}{},
"metrics": map[string]interface{}{
"plan_growth_rate": 15.5,
"subscription_growth": 23.8,
"revenue_growth": 18.2,
"churn_rate": 2.1,
},
"recent_activities": []map[string]interface{}{
{"type": "plan_created", "description": "New plan 'Premium Plus' created", "time": "2024-01-01T10:30:00Z"},
{"type": "subscription", "description": "25 new subscriptions today", "time": "2024-01-01T09:15:00Z"},
{"type": "revenue", "description": "Revenue target achieved", "time": "2024-01-01T08:45:00Z"},
},
}
response := struct {
Success bool `json:"success"`
Data map[string]interface{} `json:"data"`
}{
Success: true,
Data: dashboardData,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// GetSystemOverview handles the system overview endpoint
func (h *RatePlanHandler) GetSystemOverview(c *gin.Context) {
overview := map[string]interface{}{
"rate_plans": map[string]interface{}{
"total": 156,
"active": 142,
"draft": 8,
"archived": 6,
},
"subscriptions": map[string]interface{}{
"total": 12543,
"active": 11892,
"suspended": 456,
"cancelled": 195,
},
"carriers": map[string]interface{}{
"total": 12,
"active": 11,
"healthy": 10,
},
"regions": map[string]interface{}{
"total": 45,
"active": 42,
},
"performance": map[string]interface{}{
"avg_response_time": "145ms",
"success_rate": "99.8%",
"uptime": "99.9%",
},
}
response := struct {
Success bool `json:"success"`
Data map[string]interface{} `json:"data"`
}{
Success: true,
Data: overview,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// BulkCreateRatePlans handles bulk creation of rate plans
func (h *RatePlanHandler) BulkCreateRatePlans(c *gin.Context) {
var req struct {
Plans []CreateRatePlanRequest `json:"plans" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error())
return
}
results := make([]map[string]interface{}, 0)
for _, planReq := range req.Plans {
plan := &rateplan.RatePlan{
Name: planReq.Name,
Description: planReq.Description,
CarrierID: planReq.CarrierID,
Region: planReq.Region,
PlanType: planReq.PlanType,
BasePrice: planReq.BasePrice,
Currency: planReq.Currency,
BillingCycle: planReq.BillingCycle,
DataAllowance: planReq.DataAllowance,
VoiceAllowance: planReq.VoiceAllowance,
SMSAllowance: planReq.SMSAllowance,
OverageRates: planReq.OverageRates,
Features: planReq.Features,
ActivationFee: planReq.ActivationFee,
EarlyTermination: planReq.EarlyTermination,
Discounts: planReq.Discounts,
ValidFrom: planReq.ValidFrom,
ValidTo: planReq.ValidTo,
Priority: planReq.Priority,
IsActive: planReq.IsActive,
Metadata: planReq.Metadata,
}
createdPlan, err := h.service.CreateRatePlan(c.Request.Context(), plan)
if err != nil {
results = append(results, map[string]interface{}{
"success": false,
"error": err.Error(),
"plan": planReq.Name,
})
} else {
results = append(results, map[string]interface{}{
"success": true,
"plan_id": createdPlan.ID,
"plan": planReq.Name,
})
}
}
response := struct {
Success bool `json:"success"`
Message string `json:"message"`
Results []map[string]interface{} `json:"results"`
}{
Success: true,
Message: "Bulk creation completed",
Results: results,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// ActivateRatePlan handles activating a rate plan
func (h *RatePlanHandler) ActivateRatePlan(c *gin.Context) {
id := h.extractIDFromPath(c)
if id == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required")
return
}
plan, err := h.service.GetRatePlan(c.Request.Context(), id)
if err != nil {
h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found")
return
}
plan.IsActive = true
plan.Status = rateplan.PlanStatusActive
_, err = h.service.UpdateRatePlan(c.Request.Context(), plan)
if err != nil {
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to activate rate plan")
return
}
response := struct {
Success bool `json:"success"`
Message string `json:"message"`
}{
Success: true,
Message: "Rate plan activated successfully",
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// DeactivateRatePlan handles deactivating a rate plan
func (h *RatePlanHandler) DeactivateRatePlan(c *gin.Context) {
id := h.extractIDFromPath(c)
if id == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required")
return
}
plan, err := h.service.GetRatePlan(c.Request.Context(), id)
if err != nil {
h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found")
return
}
plan.IsActive = false
plan.Status = rateplan.PlanStatusInactive
_, err = h.service.UpdateRatePlan(c.Request.Context(), plan)
if err != nil {
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to deactivate rate plan")
return
}
response := struct {
Success bool `json:"success"`
Message string `json:"message"`
}{
Success: true,
Message: "Rate plan deactivated successfully",
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// DuplicateRatePlan handles duplicating a rate plan
func (h *RatePlanHandler) DuplicateRatePlan(c *gin.Context) {
id := h.extractIDFromPath(c)
if id == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Rate plan ID is required")
return
}
var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error())
return
}
// Get original plan
originalPlan, err := h.service.GetRatePlan(c.Request.Context(), id)
if err != nil {
h.writeErrorResponse(c, http.StatusNotFound, "Rate plan not found")
return
}
// Create duplicate
duplicatePlan := &rateplan.RatePlan{
Name: req.Name,
Description: req.Description,
CarrierID: originalPlan.CarrierID,
Region: originalPlan.Region,
PlanType: originalPlan.PlanType,
Status: rateplan.PlanStatusDraft,
BasePrice: originalPlan.BasePrice,
Currency: originalPlan.Currency,
BillingCycle: originalPlan.BillingCycle,
DataAllowance: originalPlan.DataAllowance,
VoiceAllowance: originalPlan.VoiceAllowance,
SMSAllowance: originalPlan.SMSAllowance,
OverageRates: originalPlan.OverageRates,
Features: originalPlan.Features,
ActivationFee: originalPlan.ActivationFee,
EarlyTermination: originalPlan.EarlyTermination,
Discounts: originalPlan.Discounts,
ValidFrom: originalPlan.ValidFrom,
ValidTo: originalPlan.ValidTo,
Priority: originalPlan.Priority,
IsActive: false,
Metadata: originalPlan.Metadata,
}
createdPlan, err := h.service.CreateRatePlan(c.Request.Context(), duplicatePlan)
if err != nil {
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to duplicate rate plan")
return
}
response := struct {
Success bool `json:"success"`
Message string `json:"message"`
Data *rateplan.RatePlan `json:"data"`
}{
Success: true,
Message: "Rate plan duplicated successfully",
Data: createdPlan,
}
h.writeJSONResponse(c, http.StatusCreated, response)
}
// Placeholder methods for other management endpoints
func (h *RatePlanHandler) BulkUpdateRatePlans(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) BulkDeleteRatePlans(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) BulkCancelSubscriptions(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) SuspendSubscription(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) ReactivateSubscription(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) ChangeSubscriptionPlan(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) GetUsageReport(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) GetRevenueReport(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) GetPerformanceReport(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) ExportReport(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) GetPricingConfiguration(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) UpdatePricingConfiguration(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) GetValidationRules(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
func (h *RatePlanHandler) UpdateValidationRules(c *gin.Context) {
h.writeErrorResponse(c, http.StatusNotImplemented, "Not implemented yet")
}
@@ -0,0 +1,212 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
)
// SubscribeRequest represents the request to subscribe to a rate plan
type SubscribeRequest struct {
ProfileID string `json:"profile_id" binding:"required"`
RatePlanID string `json:"rate_plan_id" binding:"required"`
AutoRenew bool `json:"auto_renew"`
AppliedDiscounts []string `json:"applied_discounts,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// CancelSubscriptionRequest represents the request to cancel a subscription
type CancelSubscriptionRequest struct {
Reason string `json:"reason" binding:"required"`
}
// SubscriptionResponse represents the response for subscription operations
type SubscriptionResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data *rateplan.RatePlanSubscription `json:"data,omitempty"`
}
// SubscriptionsResponse represents the response for listing subscriptions
type SubscriptionsResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data []*rateplan.RatePlanSubscription `json:"data,omitempty"`
}
// SubscribeToPlan handles subscribing a profile to a rate plan
func (h *RatePlanHandler) SubscribeToPlan(c *gin.Context) {
var req SubscribeRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error())
return
}
subscribeReq := &rateplan.SubscribeRequest{
ProfileID: req.ProfileID,
RatePlanID: req.RatePlanID,
AutoRenew: req.AutoRenew,
AppliedDiscounts: req.AppliedDiscounts,
Metadata: req.Metadata,
}
subscription, err := h.service.SubscribeToPlan(c.Request.Context(), subscribeReq)
if err != nil {
h.logger.WithError(err).Error("Failed to subscribe to plan")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to subscribe to plan: "+err.Error())
return
}
response := SubscriptionResponse{
Success: true,
Message: "Subscription created successfully",
Data: subscription,
}
h.writeJSONResponse(c, http.StatusCreated, response)
}
// GetSubscription handles retrieving a subscription by ID
func (h *RatePlanHandler) GetSubscription(c *gin.Context) {
id := h.extractIDFromPath(c)
if id == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Subscription ID is required")
return
}
subscription, err := h.service.GetSubscription(c.Request.Context(), id)
if err != nil {
if err.Error() == "subscription not found: "+id {
h.writeErrorResponse(c, http.StatusNotFound, "Subscription not found")
return
}
h.logger.WithError(err).Error("Failed to get subscription")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get subscription")
return
}
response := SubscriptionResponse{
Success: true,
Data: subscription,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// GetActiveSubscription handles retrieving the active subscription for a profile
func (h *RatePlanHandler) GetActiveSubscription(c *gin.Context) {
profileID := c.Query("profile_id")
if profileID == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Profile ID is required")
return
}
subscription, err := h.service.GetActiveSubscription(c.Request.Context(), profileID)
if err != nil {
h.logger.WithError(err).Error("Failed to get active subscription")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get active subscription")
return
}
if subscription == nil {
response := struct {
Success bool `json:"success"`
Message string `json:"message"`
}{
Success: false,
Message: "No active subscription found",
}
h.writeJSONResponse(c, http.StatusOK, response)
return
}
response := SubscriptionResponse{
Success: true,
Data: subscription,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// ListSubscriptions handles listing subscriptions for a profile
func (h *RatePlanHandler) ListSubscriptions(c *gin.Context) {
profileID := c.Query("profile_id")
if profileID == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Profile ID is required")
return
}
filter := &rateplan.SubscriptionFilter{
Status: rateplan.SubscriptionStatus(c.Query("status")),
RatePlanID: c.Query("rate_plan_id"),
}
// Parse limit and offset
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
filter.Limit = limit
}
}
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 {
filter.Offset = offset
}
}
subscriptions, err := h.service.ListSubscriptions(c.Request.Context(), profileID, filter)
if err != nil {
h.logger.WithError(err).Error("Failed to list subscriptions")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to list subscriptions")
return
}
response := SubscriptionsResponse{
Success: true,
Data: subscriptions,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// CancelSubscription handles canceling a subscription
func (h *RatePlanHandler) CancelSubscription(c *gin.Context) {
id := h.extractIDFromPath(c)
if id == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Subscription ID is required")
return
}
var req CancelSubscriptionRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error())
return
}
if err := h.service.CancelSubscription(c.Request.Context(), id, req.Reason); err != nil {
if err.Error() == "subscription not found: "+id {
h.writeErrorResponse(c, http.StatusNotFound, "Subscription not found")
return
}
if err.Error() == "subscription is not active" {
h.writeErrorResponse(c, http.StatusBadRequest, "Subscription is not active")
return
}
h.logger.WithError(err).Error("Failed to cancel subscription")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to cancel subscription")
return
}
response := struct {
Success bool `json:"success"`
Message string `json:"message"`
}{
Success: true,
Message: "Subscription cancelled successfully",
}
h.writeJSONResponse(c, http.StatusOK, response)
}
@@ -0,0 +1,175 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
)
// RecordUsageRequest represents the request to record usage
type RecordUsageRequest struct {
ProfileID string `json:"profile_id" binding:"required"`
DataUsed int64 `json:"data_used" binding:"required,min=0"`
VoiceUsed int64 `json:"voice_used" binding:"required,min=0"`
SMSUsed int64 `json:"sms_used" binding:"required,min=0"`
}
// UsageResponse represents the response for usage operations
type UsageResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data *rateplan.RatePlanUsage `json:"data,omitempty"`
}
// UsageHistoryResponse represents the response for usage history
type UsageHistoryResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data []*rateplan.RatePlanUsage `json:"data,omitempty"`
}
// RecordUsage handles recording usage for a subscription
func (h *RatePlanHandler) RecordUsage(c *gin.Context) {
var req RecordUsageRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error())
return
}
recordReq := &rateplan.RecordUsageRequest{
ProfileID: req.ProfileID,
DataUsed: req.DataUsed,
VoiceUsed: req.VoiceUsed,
SMSUsed: req.SMSUsed,
}
usage, err := h.service.RecordUsage(c.Request.Context(), recordReq)
if err != nil {
h.logger.WithError(err).Error("Failed to record usage")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to record usage: "+err.Error())
return
}
response := UsageResponse{
Success: true,
Message: "Usage recorded successfully",
Data: usage,
}
h.writeJSONResponse(c, http.StatusCreated, response)
}
// GetUsage handles retrieving current usage for a profile
func (h *RatePlanHandler) GetUsage(c *gin.Context) {
profileID := c.Query("profile_id")
if profileID == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Profile ID is required")
return
}
usage, err := h.service.GetUsage(c.Request.Context(), profileID)
if err != nil {
h.logger.WithError(err).Error("Failed to get usage")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get usage")
return
}
if usage == nil {
response := struct {
Success bool `json:"success"`
Message string `json:"message"`
}{
Success: false,
Message: "No usage data found",
}
h.writeJSONResponse(c, http.StatusOK, response)
return
}
response := UsageResponse{
Success: true,
Data: usage,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// GetUsageHistory handles retrieving usage history for a profile
func (h *RatePlanHandler) GetUsageHistory(c *gin.Context) {
profileID := c.Query("profile_id")
if profileID == "" {
h.writeErrorResponse(c, http.StatusBadRequest, "Profile ID is required")
return
}
limit := 10 // default limit
if limitStr := c.Query("limit"); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
usageHistory, err := h.service.GetUsageHistory(c.Request.Context(), profileID, limit)
if err != nil {
h.logger.WithError(err).Error("Failed to get usage history")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to get usage history")
return
}
response := UsageHistoryResponse{
Success: true,
Data: usageHistory,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
// CalculateCostRequest represents the request to calculate cost
type CalculateCostRequest struct {
RatePlanID string `json:"rate_plan_id" binding:"required"`
DataUsed int64 `json:"data_used" binding:"required,min=0"`
VoiceUsed int64 `json:"voice_used" binding:"required,min=0"`
SMSUsed int64 `json:"sms_used" binding:"required,min=0"`
AppliedDiscounts []string `json:"applied_discounts,omitempty"`
}
// CostCalculationResponse represents the response for cost calculation
type CostCalculationResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data *rateplan.RatePlanCostCalculation `json:"data,omitempty"`
}
// CalculateCost handles calculating cost for a rate plan based on usage
func (h *RatePlanHandler) CalculateCost(c *gin.Context) {
var req CalculateCostRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.writeErrorResponse(c, http.StatusBadRequest, "Invalid request body: "+err.Error())
return
}
calcReq := &rateplan.CalculateCostRequest{
RatePlanID: req.RatePlanID,
DataUsed: req.DataUsed,
VoiceUsed: req.VoiceUsed,
SMSUsed: req.SMSUsed,
AppliedDiscounts: req.AppliedDiscounts,
}
calculation, err := h.service.CalculateCost(c.Request.Context(), calcReq)
if err != nil {
h.logger.WithError(err).Error("Failed to calculate cost")
h.writeErrorResponse(c, http.StatusInternalServerError, "Failed to calculate cost: "+err.Error())
return
}
response := CostCalculationResponse{
Success: true,
Data: calculation,
}
h.writeJSONResponse(c, http.StatusOK, response)
}
@@ -0,0 +1,236 @@
package integration
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"gorm.io/gorm"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/config"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/sirupsen/logrus"
)
// NewGormCarrierRepository creates a new GORM carrier repository
func NewGormCarrierRepository(db *gorm.DB, logger *logrus.Logger) *GormCarrierRepository {
return &GormCarrierRepository{
db: db,
logger: logger,
}
}
// CarrierModel represents the database model for carriers
type CarrierModel struct {
ID string `gorm:"primaryKey;column:id" json:"id"`
Name string `gorm:"column:name" json:"name"`
MCC string `gorm:"column:mcc" json:"mcc"`
MNC string `gorm:"column:mnc" json:"mnc"`
CountryCode string `gorm:"column:country_code" json:"country_code"`
IsActive bool `gorm:"column:is_active" json:"is_active"`
Priority int `gorm:"column:priority" json:"priority"`
ES2Config string `gorm:"column:es2_config;type:text" json:"es2_config"`
Capabilities string `gorm:"column:capabilities;type:text" json:"capabilities"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
}
// TableName returns the table name for the carrier model
func (CarrierModel) TableName() string {
return "carriers"
}
// GetCarriers retrieves all carriers from the database
func (r *GormCarrierRepository) GetCarriers(ctx context.Context) ([]*smdp.Carrier, error) {
var models []CarrierModel
if err := r.db.WithContext(ctx).Where("is_active = ?", true).Find(&models).Error; err != nil {
r.logger.Error("Failed to get carriers from database", "error", err)
return nil, fmt.Errorf("failed to get carriers: %w", err)
}
carriers := make([]*smdp.Carrier, 0, len(models))
for _, model := range models {
carrier, err := r.modelToCarrier(&model)
if err != nil {
r.logger.Error("Failed to convert carrier model", "error", err, "carrier_id", model.ID)
continue
}
carriers = append(carriers, carrier)
}
return carriers, nil
}
// GetCarrier retrieves a specific carrier by ID
func (r *GormCarrierRepository) GetCarrier(ctx context.Context, id string) (*smdp.Carrier, error) {
var model CarrierModel
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("carrier not found: %s", id)
}
r.logger.Error("Failed to get carrier from database", "error", err, "carrier_id", id)
return nil, fmt.Errorf("failed to get carrier: %w", err)
}
return r.modelToCarrier(&model)
}
// SaveCarrier saves a carrier to the database
func (r *GormCarrierRepository) SaveCarrier(ctx context.Context, carrier *smdp.Carrier) error {
model := r.carrierToModel(carrier)
// Check if carrier exists and update or create accordingly
var existing CarrierModel
err := r.db.WithContext(ctx).Where("id = ?", model.ID).First(&existing).Error
if err == gorm.ErrRecordNotFound {
// Create new carrier
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
r.logger.WithError(err).Error("Failed to create carrier")
return fmt.Errorf("failed to create carrier: %w", err)
}
} else if err != nil {
r.logger.WithError(err).Error("Failed to check carrier existence")
return fmt.Errorf("failed to check carrier: %w", err)
} else {
// Update existing carrier
model.UpdatedAt = time.Now()
if err := r.db.WithContext(ctx).Model(&existing).Updates(model).Error; err != nil {
r.logger.WithError(err).Error("Failed to update carrier")
return fmt.Errorf("failed to update carrier: %w", err)
}
}
return nil
}
// UpdateCarrierMetrics updates carrier metrics
func (r *GormCarrierRepository) UpdateCarrierMetrics(ctx context.Context, id string, metrics *smdp.CarrierMetrics) error {
// In a real implementation, you might have a separate metrics table
// For now, we'll log the metrics update
r.logger.Info("Carrier metrics updated", "carrier_id", id,
"total_requests", metrics.TotalRequests,
"success_rate", float64(metrics.SuccessfulRequests)/float64(metrics.TotalRequests)*100)
return nil
}
// Helper methods for model conversion
func (r *GormCarrierRepository) modelToCarrier(model *CarrierModel) (*smdp.Carrier, error) {
carrier := &smdp.Carrier{
ID: model.ID,
Name: model.Name,
MCC: model.MCC,
MNC: model.MNC,
CountryCode: model.CountryCode,
IsActive: model.IsActive,
Priority: model.Priority,
}
// Parse ES2Config
if model.ES2Config != "" {
var es2Config config.ES2Config
if err := json.Unmarshal([]byte(model.ES2Config), &es2Config); err != nil {
return nil, fmt.Errorf("failed to parse ES2 config: %w", err)
}
carrier.ES2Config = &es2Config
}
// Parse Capabilities
if model.Capabilities != "" {
var capabilities smdp.CarrierCapabilities
if err := json.Unmarshal([]byte(model.Capabilities), &capabilities); err != nil {
return nil, fmt.Errorf("failed to parse carrier capabilities: %w", err)
}
carrier.Capabilities = &capabilities
}
// Initialize metrics (in production, this would come from a metrics table)
carrier.Metrics = &smdp.CarrierMetrics{
TotalRequests: 0,
SuccessfulRequests: 0,
FailedRequests: 0,
AverageResponseTime: 0,
RequestRate: 0,
}
return carrier, nil
}
func (r *GormCarrierRepository) carrierToModel(carrier *smdp.Carrier) *CarrierModel {
model := &CarrierModel{
ID: carrier.ID,
Name: carrier.Name,
MCC: carrier.MCC,
MNC: carrier.MNC,
CountryCode: carrier.CountryCode,
IsActive: carrier.IsActive,
Priority: carrier.Priority,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Serialize ES2Config
if carrier.ES2Config != nil {
if data, err := json.Marshal(carrier.ES2Config); err == nil {
model.ES2Config = string(data)
}
}
// Serialize Capabilities
if carrier.Capabilities != nil {
if data, err := json.Marshal(carrier.Capabilities); err == nil {
model.Capabilities = string(data)
}
}
return model
}
// LoadCarriersFromFile loads carrier configurations from a JSON/YAML file
func LoadCarriersFromFile(configPath string) (*CarrierConfig, error) {
data, err := os.ReadFile(configPath)
if err != nil {
return nil, fmt.Errorf("failed to read carrier config file: %w", err)
}
var config CarrierConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse carrier config file: %w", err)
}
return &config, nil
}
// ConvertConfigToCarriers converts carrier definitions to smdp.Carrier objects
func ConvertConfigToCarriers(config *CarrierConfig) ([]*smdp.Carrier, error) {
carriers := make([]*smdp.Carrier, 0, len(config.Carriers))
for _, def := range config.Carriers {
carrier := &smdp.Carrier{
ID: def.ID,
Name: def.Name,
MCC: def.MCC,
MNC: def.MNC,
CountryCode: def.CountryCode,
IsActive: def.IsActive,
Priority: def.Priority,
ES2Config: def.ES2Config,
Capabilities: def.Capabilities,
Metrics: &smdp.CarrierMetrics{
TotalRequests: 0,
SuccessfulRequests: 0,
FailedRequests: 0,
AverageResponseTime: 0,
RequestRate: 0,
},
}
carriers = append(carriers, carrier)
}
return carriers, nil
}
@@ -0,0 +1,42 @@
package integration
import (
"context"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/config"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// CarrierConfig represents the configuration structure for carriers
type CarrierConfig struct {
Carriers []CarrierDefinition `json:"carriers" yaml:"carriers"`
}
// CarrierDefinition defines a carrier configuration
type CarrierDefinition struct {
ID string `json:"id" yaml:"id"`
Name string `json:"name" yaml:"name"`
MCC string `json:"mcc" yaml:"mcc"`
MNC string `json:"mnc" yaml:"mnc"`
CountryCode string `json:"country_code" yaml:"country_code"`
IsActive bool `json:"is_active" yaml:"is_active"`
Priority int `json:"priority" yaml:"priority"`
ES2Config *config.ES2Config `json:"es2_config" yaml:"es2_config"`
Capabilities *smdp.CarrierCapabilities `json:"capabilities" yaml:"capabilities"`
}
// CarrierRepository defines the interface for carrier data operations
type CarrierRepository interface {
GetCarriers(ctx context.Context) ([]*smdp.Carrier, error)
GetCarrier(ctx context.Context, id string) (*smdp.Carrier, error)
SaveCarrier(ctx context.Context, carrier *smdp.Carrier) error
UpdateCarrierMetrics(ctx context.Context, id string, metrics *smdp.CarrierMetrics) error
}
// GormCarrierRepository implements CarrierRepository using GORM
type GormCarrierRepository struct {
db *gorm.DB
logger *logrus.Logger
}
@@ -1,134 +1,86 @@
package integration
import (
"log"
"time"
"fmt"
"github.com/sirupsen/logrus"
"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
// SetupCarriers loads real carrier configurations from database or config files
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,
},
},
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
logger.Info("Loading carriers from configuration")
// Load carriers from configuration file
configPath := "configs/carriers.json"
config, err := LoadCarriersFromFile(configPath)
if err != nil {
logger.WithError(err).Error("Failed to load carriers from config file")
return fmt.Errorf("failed to load carriers from config: %w", err)
}
// Add carriers to the manager
carriers, err := ConvertConfigToCarriers(config)
if err != nil {
logger.WithError(err).Error("Failed to convert config to carriers")
return fmt.Errorf("failed to convert config: %w", err)
}
// Validate and add carriers to SMDP manager
successCount := 0
for _, carrier := range carriers {
if err := si.manager.AddCarrier(carrier); err != nil {
return err
if err := validateCarrier(carrier); err != nil {
logger.WithError(err).WithField("carrier_id", carrier.ID).Error("Carrier validation failed")
continue
}
if err := si.manager.AddCarrier(carrier); err != nil {
logger.WithError(err).WithField("carrier_id", carrier.ID).Error("Failed to add carrier to manager")
continue
}
successCount++
}
logger.WithFields(logrus.Fields{
"config_file": configPath,
"total_loaded": len(carriers),
"success_count": successCount,
"failed_count": len(carriers) - successCount,
}).Info("Carriers loaded from configuration file")
return nil
}
// validateCarrier validates carrier configuration
func validateCarrier(carrier *smdp.Carrier) error {
if carrier.ID == "" {
return fmt.Errorf("carrier ID is required")
}
if carrier.Name == "" {
return fmt.Errorf("carrier name is required")
}
if carrier.MCC == "" {
return fmt.Errorf("carrier MCC is required")
}
if carrier.MNC == "" {
return fmt.Errorf("carrier MNC is required")
}
if carrier.CountryCode == "" {
return fmt.Errorf("carrier country code is required")
}
if carrier.ES2Config == nil {
return fmt.Errorf("carrier ES2 config is required")
}
if carrier.ES2Config.BaseURL == "" {
return fmt.Errorf("carrier ES2 base URL is required")
}
if carrier.ES2Config.APIKey == "" {
return fmt.Errorf("carrier ES2 API key is required")
}
log.Printf("Added %d carriers to the selection manager", len(carriers))
return nil
}
@@ -0,0 +1,161 @@
package rateplan
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
)
// CarrierSelectionIntegrator integrates rate plans with carrier selection
type CarrierSelectionIntegrator struct {
ratePlanRepo Repository
smdpManager *smdp.SMDPManager
logger Logger
}
// NewCarrierSelectionIntegrator creates a new carrier selection integrator
func NewCarrierSelectionIntegrator(ratePlanRepo Repository, smdpManager *smdp.SMDPManager, logger Logger) *CarrierSelectionIntegrator {
return &CarrierSelectionIntegrator{
ratePlanRepo: ratePlanRepo,
smdpManager: smdpManager,
logger: logger,
}
}
// GetOptimalCarrierWithRatePlan finds the optimal carrier considering both performance and rate plan availability
func (csi *CarrierSelectionIntegrator) GetOptimalCarrierWithRatePlan(ctx context.Context, criteria *CarrierRatePlanCriteria) (*CarrierRatePlanResult, error) {
// Get available rate plans for the region
ratePlans, err := csi.getAvailableRatePlans(ctx, criteria.Region, criteria.PlanType, criteria.MaxBudget)
if err != nil {
return nil, fmt.Errorf("failed to get available rate plans: %w", err)
}
if len(ratePlans) == 0 {
return nil, fmt.Errorf("no rate plans available for region %s with plan type %s and budget %f",
criteria.Region, criteria.PlanType, criteria.MaxBudget)
}
// Group rate plans by carrier
carrierPlans := csi.groupRatePlansByCarrier(ratePlans)
// Get carrier status and performance
carrierStatus := csi.smdpManager.GetCarrierStatus()
// Score carriers based on both performance and rate plan availability
bestCarrier, bestPlan := csi.scoreCarriersWithPlans(carrierPlans, carrierStatus, criteria)
if bestCarrier == nil {
return nil, fmt.Errorf("no suitable carrier found")
}
result := &CarrierRatePlanResult{
Carrier: bestCarrier,
RatePlan: bestPlan,
TotalScore: csi.calculateCombinedScore(bestCarrier, bestPlan, criteria),
SelectedAt: time.Now(),
}
return result, nil
}
// RecommendRatePlansForCarrier recommends rate plans for a specific carrier
func (csi *CarrierSelectionIntegrator) RecommendRatePlansForCarrier(ctx context.Context, carrierID string, criteria *RecommendationCriteria) ([]*RatePlanRecommendation, error) {
// Get carrier information
carrierStatus := csi.smdpManager.GetCarrierStatus()
carrier, exists := carrierStatus[carrierID]
if !exists {
return nil, fmt.Errorf("carrier %s not found", carrierID)
}
// Get rate plans for this carrier
filter := &RatePlanFilter{
CarrierID: carrierID,
Region: criteria.Region,
PlanType: criteria.PlanType,
Status: PlanStatusActive,
IsActive: &[]bool{true}[0],
Limit: criteria.MaxResults,
}
ratePlans, err := csi.ratePlanRepo.ListRatePlans(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get rate plans: %w", err)
}
// Create recommendations
recommendations := make([]*RatePlanRecommendation, 0)
for _, plan := range ratePlans {
recommendation := csi.createRecommendation(plan, carrier, criteria)
recommendations = append(recommendations, recommendation)
}
return recommendations, nil
}
// UpdateCarrierSelectionCriteria updates carrier selection based on rate plan performance
func (csi *CarrierSelectionIntegrator) UpdateCarrierSelectionCriteria(ctx context.Context) error {
// Get usage analytics for the last 30 days
filter := &UsageAnalyticsFilter{
StartDate: time.Now().AddDate(0, 0, -30),
EndDate: time.Now(),
}
analytics, err := csi.ratePlanRepo.GetUsageAnalytics(ctx, filter)
if err != nil {
return fmt.Errorf("failed to get usage analytics: %w", err)
}
// Update carrier selection weights based on rate plan performance
csi.updateCarrierWeights(analytics)
return nil
}
// GetCarrierRatePlanAnalytics returns analytics for carrier and rate plan performance
func (csi *CarrierSelectionIntegrator) GetCarrierRatePlanAnalytics(ctx context.Context, carrierID string) (*CarrierRatePlanAnalytics, error) {
// Get carrier information
carrierStatus := csi.smdpManager.GetCarrierStatus()
carrier, exists := carrierStatus[carrierID]
if !exists {
return nil, fmt.Errorf("carrier %s not found", carrierID)
}
// Get rate plans for this carrier
filter := &RatePlanFilter{
CarrierID: carrierID,
Status: PlanStatusActive,
IsActive: &[]bool{true}[0],
}
ratePlans, err := csi.ratePlanRepo.ListRatePlans(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get rate plans: %w", err)
}
// Get usage analytics for this carrier's plans
planIDs := make([]string, len(ratePlans))
for i, plan := range ratePlans {
planIDs[i] = plan.ID
}
analytics := &CarrierRatePlanAnalytics{
CarrierID: carrierID,
CarrierName: carrier.Name,
Region: carrier.CountryCode,
HealthStatus: string(carrier.HealthStatus),
Priority: carrier.Priority,
TotalPlans: len(ratePlans),
ActivePlans: csi.countActivePlans(ratePlans),
GeneratedAt: time.Now(),
PlanAnalytics: make([]RatePlanAnalytics, 0),
}
for _, plan := range ratePlans {
planAnalytics := csi.getPlanAnalytics(ctx, plan)
analytics.PlanAnalytics = append(analytics.PlanAnalytics, *planAnalytics)
}
return analytics, nil
}
@@ -0,0 +1,202 @@
package rateplan
import (
"context"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
)
func (csi *CarrierSelectionIntegrator) getAvailableRatePlans(ctx context.Context, region string, planType PlanType, maxBudget float64) ([]*RatePlan, error) {
filter := &RatePlanFilter{
Region: region,
PlanType: planType,
Status: PlanStatusActive,
IsActive: &[]bool{true}[0],
MaxPrice: maxBudget,
}
return csi.ratePlanRepo.ListRatePlans(ctx, filter)
}
func (csi *CarrierSelectionIntegrator) groupRatePlansByCarrier(plans []*RatePlan) map[string][]*RatePlan {
carrierPlans := make(map[string][]*RatePlan)
for _, plan := range plans {
carrierPlans[plan.CarrierID] = append(carrierPlans[plan.CarrierID], plan)
}
return carrierPlans
}
func (csi *CarrierSelectionIntegrator) scoreCarriersWithPlans(carrierPlans map[string][]*RatePlan, carrierStatus map[string]*smdp.Carrier, criteria *CarrierRatePlanCriteria) (*smdp.Carrier, *RatePlan) {
var bestCarrier *smdp.Carrier
var bestPlan *RatePlan
bestScore := 0.0
for carrierID, plans := range carrierPlans {
carrier, exists := carrierStatus[carrierID]
if !exists {
continue
}
if carrier.HealthStatus != "healthy" {
continue
}
bestPlanForCarrier := csi.findBestPlanForCarrier(plans, criteria)
if bestPlanForCarrier == nil {
continue
}
combinedScore := csi.calculateCombinedScore(carrier, bestPlanForCarrier, criteria)
if combinedScore > bestScore {
bestScore = combinedScore
bestCarrier = carrier
bestPlan = bestPlanForCarrier
}
}
return bestCarrier, bestPlan
}
func (csi *CarrierSelectionIntegrator) findBestPlanForCarrier(plans []*RatePlan, criteria *CarrierRatePlanCriteria) *RatePlan {
var bestPlan *RatePlan
bestScore := 0.0
for _, plan := range plans {
score := csi.scoreRatePlan(plan, criteria)
if score > bestScore {
bestScore = score
bestPlan = plan
}
}
return bestPlan
}
func (csi *CarrierSelectionIntegrator) scoreRatePlan(plan *RatePlan, criteria *CarrierRatePlanCriteria) float64 {
score := 0.0
if criteria.MaxBudget > 0 {
priceScore := 1.0 - (plan.BasePrice / criteria.MaxBudget)
if priceScore > 0 {
score += priceScore * 0.4
}
}
priorityScore := float64(plan.Priority) / 100.0
score += priorityScore * 0.3
if plan.Features != nil {
featureScore := float64(len(plan.Features)) / 10.0
score += featureScore * 0.2
}
if plan.DataAllowance != nil {
dataScore := float64(plan.DataAllowance.Amount) / 10000.0 // Normalize by 10GB
if dataScore > 1.0 {
dataScore = 1.0
}
score += dataScore * 0.1
}
return score
}
func (csi *CarrierSelectionIntegrator) calculateCombinedScore(carrier *smdp.Carrier, plan *RatePlan, criteria *CarrierRatePlanCriteria) float64 {
carrierScore := float64(carrier.Priority) / 100.0 * 0.7
planScore := csi.scoreRatePlan(plan, criteria) * 0.3
return carrierScore + planScore
}
func (csi *CarrierSelectionIntegrator) createRecommendation(plan *RatePlan, carrier *smdp.Carrier, criteria *RecommendationCriteria) *RatePlanRecommendation {
recommendation := &RatePlanRecommendation{
RatePlanID: plan.ID,
RatePlanName: plan.Name,
CarrierID: carrier.ID,
CarrierName: carrier.Name,
Price: plan.BasePrice,
Currency: plan.Currency,
Relevance: csi.calculateRelevance(plan, criteria),
Features: plan.Features,
DataAllowance: plan.DataAllowance,
VoiceAllowance: plan.VoiceAllowance,
SMSAllowance: plan.SMSAllowance,
RecommendedAt: time.Now(),
}
return recommendation
}
func (csi *CarrierSelectionIntegrator) calculateRelevance(plan *RatePlan, criteria *RecommendationCriteria) float64 {
relevance := 0.5 // Base relevance
if criteria.PreferredData > 0 && plan.DataAllowance != nil {
if plan.DataAllowance.Amount >= criteria.PreferredData {
relevance += 0.2
}
}
if criteria.PreferredVoice > 0 && plan.VoiceAllowance != nil {
if plan.VoiceAllowance.Minutes >= criteria.PreferredVoice {
relevance += 0.2
}
}
if criteria.PreferredSMS > 0 && plan.SMSAllowance != nil {
if plan.SMSAllowance.Messages >= criteria.PreferredSMS {
relevance += 0.1
}
}
if relevance > 1.0 {
relevance = 1.0
}
return relevance
}
func (csi *CarrierSelectionIntegrator) updateCarrierWeights(analytics *UsageAnalytics) {
// This would update the carrier selection weights based on usage patterns
// Implementation depends on the specific carrier selection algorithm
csi.logger.Info("Updated carrier selection weights based on usage analytics")
}
func (csi *CarrierSelectionIntegrator) countActivePlans(plans []*RatePlan) int {
count := 0
for _, plan := range plans {
if plan.IsActive && plan.Status == PlanStatusActive {
count++
}
}
return count
}
func (csi *CarrierSelectionIntegrator) getPlanAnalytics(ctx context.Context, plan *RatePlan) *RatePlanAnalytics {
subscriptions, err := csi.ratePlanRepo.ListSubscriptions(ctx, "", &SubscriptionFilter{
RatePlanID: plan.ID,
Status: SubscriptionStatusActive,
Limit: 1,
})
subscriptionCount := 0
if err == nil {
subscriptionCount = len(subscriptions)
}
return &RatePlanAnalytics{
RatePlanID: plan.ID,
RatePlanName: plan.Name,
BasePrice: plan.BasePrice,
Currency: plan.Currency,
ActiveSubscriptions: subscriptionCount,
PlanType: plan.PlanType,
BillingCycle: plan.BillingCycle,
DataAllowance: plan.DataAllowance,
VoiceAllowance: plan.VoiceAllowance,
SMSAllowance: plan.SMSAllowance,
Features: plan.Features,
}
}
@@ -0,0 +1,36 @@
package rateplan
import (
"context"
)
// Repository defines the interface for rate plan data operations
type Repository interface {
// Rate Plan operations
CreateRatePlan(ctx context.Context, plan *RatePlan) error
GetRatePlan(ctx context.Context, id string) (*RatePlan, error)
UpdateRatePlan(ctx context.Context, plan *RatePlan) error
DeleteRatePlan(ctx context.Context, id string) error
ListRatePlans(ctx context.Context, filter *RatePlanFilter) ([]*RatePlan, error)
CountRatePlans(ctx context.Context, filter *RatePlanFilter) (int, error)
// Subscription operations
CreateSubscription(ctx context.Context, subscription *RatePlanSubscription) error
GetSubscription(ctx context.Context, id string) (*RatePlanSubscription, error)
UpdateSubscription(ctx context.Context, subscription *RatePlanSubscription) error
DeleteSubscription(ctx context.Context, id string) error
ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error)
GetActiveSubscription(ctx context.Context, profileID string) (*RatePlanSubscription, error)
// Usage tracking operations
CreateUsage(ctx context.Context, usage *RatePlanUsage) error
GetUsage(ctx context.Context, id string) (*RatePlanUsage, error)
UpdateUsage(ctx context.Context, usage *RatePlanUsage) error
GetCurrentUsage(ctx context.Context, profileID string) (*RatePlanUsage, error)
ListUsageHistory(ctx context.Context, profileID string, limit int) ([]*RatePlanUsage, error)
// Analytics and reporting
GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsFilter) (*UsageAnalytics, error)
GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyticsFilter) (*RevenueAnalytics, error)
GetPopularPlans(ctx context.Context, limit int) ([]*RatePlan, error)
}
@@ -0,0 +1,66 @@
package rateplan
import (
"time"
"gorm.io/gorm"
)
// TableName returns the table name for RatePlan
func (RatePlan) TableName() string {
return "rate_plans"
}
// TableName returns the table name for RatePlanSubscription
func (RatePlanSubscription) TableName() string {
return "rate_plan_subscriptions"
}
// TableName returns the table name for RatePlanUsage
func (RatePlanUsage) TableName() string {
return "rate_plan_usage"
}
// BeforeCreate GORM hook for RatePlan
func (rp *RatePlan) BeforeCreate(tx *gorm.DB) error {
if rp.CreatedAt.IsZero() {
rp.CreatedAt = time.Now()
}
rp.UpdatedAt = time.Now()
return nil
}
// BeforeUpdate GORM hook for RatePlan
func (rp *RatePlan) BeforeUpdate(tx *gorm.DB) error {
rp.UpdatedAt = time.Now()
return nil
}
// BeforeCreate GORM hook for RatePlanSubscription
func (rps *RatePlanSubscription) BeforeCreate(tx *gorm.DB) error {
if rps.CreatedAt.IsZero() {
rps.CreatedAt = time.Now()
}
rps.UpdatedAt = time.Now()
return nil
}
// BeforeUpdate GORM hook for RatePlanSubscription
func (rps *RatePlanSubscription) BeforeUpdate(tx *gorm.DB) error {
rps.UpdatedAt = time.Now()
return nil
}
// BeforeCreate GORM hook for RatePlanUsage
func (rpu *RatePlanUsage) BeforeCreate(tx *gorm.DB) error {
if rpu.LastUpdated.IsZero() {
rpu.LastUpdated = time.Now()
}
return nil
}
// BeforeUpdate GORM hook for RatePlanUsage
func (rpu *RatePlanUsage) BeforeUpdate(tx *gorm.DB) error {
rpu.LastUpdated = time.Now()
return nil
}
@@ -0,0 +1,76 @@
package rateplan
func (pe *PricingEngine) calculateDataOverage(plan *RatePlan, dataUsed int64) float64 {
if plan.DataAllowance == nil || plan.DataAllowance.Unlimited || plan.OverageRates == nil {
return 0
}
allowanceMB := plan.DataAllowance.Amount
if plan.DataAllowance.Unit == "GB" {
allowanceMB *= 1024
}
if dataUsed <= allowanceMB {
return 0
}
overageMB := dataUsed - allowanceMB
return float64(overageMB) * plan.OverageRates.DataRate
}
func (pe *PricingEngine) calculateVoiceOverage(plan *RatePlan, voiceUsed int64) float64 {
if plan.VoiceAllowance == nil || plan.VoiceAllowance.Unlimited || plan.OverageRates == nil {
return 0
}
if voiceUsed <= plan.VoiceAllowance.Minutes {
return 0
}
overageMinutes := voiceUsed - plan.VoiceAllowance.Minutes
return float64(overageMinutes) * plan.OverageRates.VoiceRate
}
func (pe *PricingEngine) calculateSMSOverage(plan *RatePlan, smsUsed int64) float64 {
if plan.SMSAllowance == nil || plan.SMSAllowance.Unlimited || plan.OverageRates == nil {
return 0
}
if smsUsed <= plan.SMSAllowance.Messages {
return 0
}
overageSMS := smsUsed - plan.SMSAllowance.Messages
return float64(overageSMS) * plan.OverageRates.SMSRate
}
func (pe *PricingEngine) calculateDiscounts(plan *RatePlan, discountIDs []string, baseCost float64) float64 {
if plan.Discounts == nil || len(discountIDs) == 0 {
return 0
}
totalDiscount := 0.0
for _, discountID := range discountIDs {
for _, discount := range plan.Discounts {
if discount.ID == discountID && discount.IsActive {
switch discount.Type {
case DiscountTypePercentage:
totalDiscount += baseCost * discount.Value / 100
case DiscountTypeFixed:
totalDiscount += discount.Value
}
}
}
}
return totalDiscount
}
func (pe *PricingEngine) calculateRecommendedPrice(plan *RatePlan, marketAverage float64) float64 {
// Basic pricing strategy: position slightly below market average for competitive advantage
if marketAverage > 0 {
return marketAverage * 0.95 // 5% below market average
}
return plan.BasePrice
}
@@ -0,0 +1,191 @@
package rateplan
import (
"context"
"fmt"
"time"
)
type PricingEngine struct {
repo Repository
logger Logger
}
type Logger interface {
WithError(err error) Logger
WithField(key string, value any) Logger
Error(msg string)
Info(msg string)
Warning(msg string)
}
func NewPricingEngine(repo Repository, logger Logger) *PricingEngine {
return &PricingEngine{
repo: repo,
logger: logger,
}
}
func (pe *PricingEngine) ValidateRatePlan(ctx context.Context, plan *RatePlan) error {
if err := pe.validateBasicFields(plan); err != nil {
return err
}
if err := pe.validateDates(plan); err != nil {
return err
}
if err := pe.validateAllowances(plan); err != nil {
return err
}
if err := pe.validateOverageRates(plan); err != nil {
return err
}
if err := pe.validateDiscounts(plan); err != nil {
return err
}
if err := pe.validateEarlyTermination(plan); err != nil {
return err
}
return nil
}
func (pe *PricingEngine) CalculateOptimalPrice(ctx context.Context, plan *RatePlan) (*PriceOptimization, error) {
filter := &RatePlanFilter{
Region: plan.Region,
PlanType: plan.PlanType,
Status: PlanStatusActive,
IsActive: &[]bool{true}[0],
Limit: 10,
}
similarPlans, err := pe.repo.ListRatePlans(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get similar plans: %w", err)
}
var totalBasePrice float64
for _, similarPlan := range similarPlans {
totalBasePrice += similarPlan.BasePrice
}
var marketAverage float64
if len(similarPlans) > 0 {
marketAverage = totalBasePrice / float64(len(similarPlans))
}
recommendedPrice := pe.calculateRecommendedPrice(plan, marketAverage)
optimization := &PriceOptimization{
CurrentPrice: plan.BasePrice,
MarketAverage: marketAverage,
RecommendedPrice: recommendedPrice,
PriceDifference: recommendedPrice - plan.BasePrice,
CompetitorCount: len(similarPlans),
OptimizedAt: time.Now(),
}
return optimization, nil
}
func (pe *PricingEngine) ValidateSubscription(ctx context.Context, req *SubscribeRequest) error {
plan, err := pe.repo.GetRatePlan(ctx, req.RatePlanID)
if err != nil {
return fmt.Errorf("rate plan not found: %w", err)
}
if !plan.IsActive || plan.Status != PlanStatusActive {
return fmt.Errorf("rate plan is not available for subscription")
}
now := time.Now()
if now.Before(plan.ValidFrom) {
return fmt.Errorf("rate plan is not yet available")
}
if plan.ValidTo != nil && now.After(*plan.ValidTo) {
return fmt.Errorf("rate plan has expired")
}
if len(req.AppliedDiscounts) > 0 {
if err := pe.validateSubscriptionDiscounts(plan, req.AppliedDiscounts); err != nil {
return err
}
}
return nil
}
func (pe *PricingEngine) CalculateSubscriptionCost(ctx context.Context, req *CalculateCostRequest) (*CostBreakdown, error) {
// Get the rate plan
plan, err := pe.repo.GetRatePlan(ctx, req.RatePlanID)
if err != nil {
return nil, fmt.Errorf("rate plan not found: %w", err)
}
breakdown := &CostBreakdown{
RatePlanID: req.RatePlanID,
Currency: plan.Currency,
CalculatedAt: time.Now(),
BreakdownItems: []CostItem{},
}
baseCost := plan.BasePrice
breakdown.BreakdownItems = append(breakdown.BreakdownItems, CostItem{
Type: "base_price",
Description: "Base subscription cost",
Amount: baseCost,
Currency: plan.Currency,
})
dataOverageCost := pe.calculateDataOverage(plan, req.DataUsed)
if dataOverageCost > 0 {
breakdown.BreakdownItems = append(breakdown.BreakdownItems, CostItem{
Type: "data_overage",
Description: "Data usage over allowance",
Amount: dataOverageCost,
Currency: plan.Currency,
})
}
voiceOverageCost := pe.calculateVoiceOverage(plan, req.VoiceUsed)
if voiceOverageCost > 0 {
breakdown.BreakdownItems = append(breakdown.BreakdownItems, CostItem{
Type: "voice_overage",
Description: "Voice usage over allowance",
Amount: voiceOverageCost,
Currency: plan.Currency,
})
}
smsOverageCost := pe.calculateSMSOverage(plan, req.SMSUsed)
if smsOverageCost > 0 {
breakdown.BreakdownItems = append(breakdown.BreakdownItems, CostItem{
Type: "sms_overage",
Description: "SMS usage over allowance",
Amount: smsOverageCost,
Currency: plan.Currency,
})
}
discountAmount := pe.calculateDiscounts(plan, req.AppliedDiscounts, baseCost)
if discountAmount > 0 {
breakdown.BreakdownItems = append(breakdown.BreakdownItems, CostItem{
Type: "discount",
Description: "Applied discounts",
Amount: -discountAmount,
Currency: plan.Currency,
})
}
totalCost := baseCost + dataOverageCost + voiceOverageCost + smsOverageCost - discountAmount
breakdown.TotalCost = totalCost
breakdown.Subtotal = baseCost + dataOverageCost + voiceOverageCost + smsOverageCost
breakdown.DiscountTotal = discountAmount
return breakdown, nil
}
@@ -0,0 +1,154 @@
package rateplan
import (
"fmt"
"time"
)
func (pe *PricingEngine) validateBasicFields(plan *RatePlan) error {
if plan.Name == "" {
return fmt.Errorf("rate plan name is required")
}
if plan.CarrierID == "" {
return fmt.Errorf("carrier ID is required")
}
if plan.Region == "" {
return fmt.Errorf("region is required")
}
if plan.BasePrice < 0 {
return fmt.Errorf("base price cannot be negative")
}
if plan.Currency == "" {
return fmt.Errorf("currency is required")
}
if plan.BillingCycle == "" {
return fmt.Errorf("billing cycle is required")
}
return nil
}
func (pe *PricingEngine) validateDates(plan *RatePlan) error {
if plan.ValidFrom.IsZero() {
return fmt.Errorf("valid from date is required")
}
now := time.Now()
if plan.ValidFrom.After(now) {
pe.logger.Warning("Rate plan valid from date is in the future")
}
if plan.ValidTo != nil && plan.ValidTo.Before(plan.ValidFrom) {
return fmt.Errorf("valid to date cannot be before valid from date")
}
return nil
}
func (pe *PricingEngine) validateAllowances(plan *RatePlan) error {
// Validate data allowance
if plan.DataAllowance != nil {
if plan.DataAllowance.Amount <= 0 && !plan.DataAllowance.Unlimited {
return fmt.Errorf("data allowance amount must be positive or unlimited")
}
if plan.DataAllowance.Unit == "" {
return fmt.Errorf("data allowance unit is required")
}
}
// Validate voice allowance
if plan.VoiceAllowance != nil {
if plan.VoiceAllowance.Minutes <= 0 && !plan.VoiceAllowance.Unlimited {
return fmt.Errorf("voice allowance minutes must be positive or unlimited")
}
}
// Validate SMS allowance
if plan.SMSAllowance != nil {
if plan.SMSAllowance.Messages <= 0 && !plan.SMSAllowance.Unlimited {
return fmt.Errorf("SMS allowance messages must be positive or unlimited")
}
}
return nil
}
func (pe *PricingEngine) validateOverageRates(plan *RatePlan) error {
if plan.OverageRates != nil {
if plan.OverageRates.DataRate < 0 {
return fmt.Errorf("data overage rate cannot be negative")
}
if plan.OverageRates.VoiceRate < 0 {
return fmt.Errorf("voice overage rate cannot be negative")
}
if plan.OverageRates.SMSRate < 0 {
return fmt.Errorf("SMS overage rate cannot be negative")
}
if plan.OverageRates.Currency == "" {
return fmt.Errorf("overage rates currency is required")
}
}
return nil
}
func (pe *PricingEngine) validateDiscounts(plan *RatePlan) error {
if plan.Discounts != nil {
for _, discount := range plan.Discounts {
if discount.Name == "" {
return fmt.Errorf("discount name is required")
}
if discount.Value <= 0 {
return fmt.Errorf("discount value must be positive")
}
if discount.ValidFrom.IsZero() {
return fmt.Errorf("discount valid from date is required")
}
if discount.ValidTo != nil && discount.ValidTo.Before(discount.ValidFrom) {
return fmt.Errorf("discount valid to date cannot be before valid from date")
}
}
}
return nil
}
func (pe *PricingEngine) validateEarlyTermination(plan *RatePlan) error {
if plan.EarlyTermination != nil && plan.EarlyTermination.Enabled {
if plan.EarlyTermination.FeeType == "" {
return fmt.Errorf("early termination fee type is required")
}
if plan.EarlyTermination.FeeType == "fixed" && plan.EarlyTermination.FeeAmount <= 0 {
return fmt.Errorf("early termination fee amount must be positive for fixed fee type")
}
if plan.EarlyTermination.FeeType == "percentage" && (plan.EarlyTermination.FeePercentage <= 0 || plan.EarlyTermination.FeePercentage > 100) {
return fmt.Errorf("early termination fee percentage must be between 0 and 100")
}
}
return nil
}
func (pe *PricingEngine) validateSubscriptionDiscounts(plan *RatePlan, discountIDs []string) error {
if plan.Discounts == nil {
return fmt.Errorf("no discounts available for this rate plan")
}
for _, discountID := range discountIDs {
found := false
for _, discount := range plan.Discounts {
if discount.ID == discountID {
if !discount.IsActive {
return fmt.Errorf("discount %s is not active", discountID)
}
now := time.Now()
if now.Before(discount.ValidFrom) || (discount.ValidTo != nil && now.After(*discount.ValidTo)) {
return fmt.Errorf("discount %s is not currently valid", discountID)
}
found = true
break
}
}
if !found {
return fmt.Errorf("discount %s not found", discountID)
}
}
return nil
}
@@ -0,0 +1,180 @@
package rateplan
import (
"context"
"fmt"
"time"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// GormRepository implements the Repository interface using GORM
type GormRepository struct {
db *gorm.DB
logger *logrus.Logger
}
// NewGormRepository creates a new GORM repository
func NewGormRepository(db *gorm.DB, logger *logrus.Logger) *GormRepository {
return &GormRepository{
db: db,
logger: logger,
}
}
// CreateUsage creates a new usage record
func (r *GormRepository) CreateUsage(ctx context.Context, usage *RatePlanUsage) error {
usage.LastUpdated = time.Now()
if err := r.db.WithContext(ctx).Create(usage).Error; err != nil {
r.logger.WithError(err).Error("Failed to create usage record")
return fmt.Errorf("failed to create usage record: %w", err)
}
return nil
}
// GetUsage retrieves a usage record by ID
func (r *GormRepository) GetUsage(ctx context.Context, id string) (*RatePlanUsage, error) {
var usage RatePlanUsage
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&usage).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("usage record not found: %s", id)
}
r.logger.WithError(err).Error("Failed to get usage record")
return nil, fmt.Errorf("failed to get usage record: %w", err)
}
return &usage, nil
}
// UpdateUsage updates an existing usage record
func (r *GormRepository) UpdateUsage(ctx context.Context, usage *RatePlanUsage) error {
usage.LastUpdated = time.Now()
result := r.db.WithContext(ctx).Where("id = ?", usage.ID).Updates(usage)
if result.Error != nil {
r.logger.WithError(result.Error).Error("Failed to update usage record")
return fmt.Errorf("failed to update usage record: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("usage record not found: %s", usage.ID)
}
return nil
}
// GetCurrentUsage retrieves the current usage for a profile
func (r *GormRepository) GetCurrentUsage(ctx context.Context, profileID string) (*RatePlanUsage, error) {
var usage RatePlanUsage
err := r.db.WithContext(ctx).
Where("profile_id = ?", profileID).
Order("cycle_start DESC").
First(&usage).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil // No usage record found
}
r.logger.WithError(err).Error("Failed to get current usage")
return nil, fmt.Errorf("failed to get current usage: %w", err)
}
return &usage, nil
}
// ListUsageHistory retrieves usage history for a profile
func (r *GormRepository) ListUsageHistory(ctx context.Context, profileID string, limit int) ([]*RatePlanUsage, error) {
query := r.db.WithContext(ctx).Where("profile_id = ?", profileID).Order("cycle_start DESC")
if limit > 0 {
query = query.Limit(limit)
}
var usageHistory []*RatePlanUsage
if err := query.Find(&usageHistory).Error; err != nil {
r.logger.WithError(err).Error("Failed to list usage history")
return nil, fmt.Errorf("failed to list usage history: %w", err)
}
return usageHistory, nil
}
// GetUsageAnalytics retrieves usage analytics
func (r *GormRepository) GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsFilter) (*UsageAnalytics, error) {
analytics := &UsageAnalytics{
TotalDataUsed: 0,
TotalVoiceUsed: 0,
TotalSMSUsed: 0,
ActiveUsers: 0,
AverageUsage: make(map[string]float64),
UsageByPlan: make(map[string]int64),
UsageByRegion: make(map[string]int64),
TimelineData: []TimelineDataPoint{},
}
// Get active users count
var activeUsersCount int64
if err := r.db.WithContext(ctx).
Model(&RatePlanSubscription{}).
Where("status = ?", SubscriptionStatusActive).
Count(&activeUsersCount).Error; err != nil {
r.logger.WithError(err).Error("Failed to get active users count")
return nil, fmt.Errorf("failed to get active users count: %w", err)
}
// Get total usage
var totalDataUsed, totalVoiceUsed, totalSMSUsed int64
if err := r.db.WithContext(ctx).
Model(&RatePlanUsage{}).
Select("COALESCE(SUM(data_used), 0), COALESCE(SUM(voice_used), 0), COALESCE(SUM(sms_used), 0)").
Where("cycle_start >= ? AND cycle_end <= ?", filter.StartDate, filter.EndDate).
Row().Scan(&totalDataUsed, &totalVoiceUsed, &totalSMSUsed); err != nil {
r.logger.WithError(err).Error("Failed to get total usage")
return nil, fmt.Errorf("failed to get total usage: %w", err)
}
analytics.ActiveUsers = int(activeUsersCount)
analytics.TotalDataUsed = totalDataUsed
analytics.TotalVoiceUsed = totalVoiceUsed
analytics.TotalSMSUsed = totalSMSUsed
return analytics, nil
}
// GetRevenueAnalytics retrieves revenue analytics
func (r *GormRepository) GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyticsFilter) (*RevenueAnalytics, error) {
// Simplified implementation - in production, you'd have actual revenue tracking
analytics := &RevenueAnalytics{
TotalRevenue: 0,
RevenueByPlan: make(map[string]float64),
RevenueByCarrier: make(map[string]float64),
RevenueByRegion: make(map[string]float64),
AverageRevenue: make(map[string]float64),
TimelineData: []TimelineDataPoint{},
}
// This would typically join with billing/payment tables
// For now, returning empty analytics
return analytics, nil
}
// GetPopularPlans retrieves the most popular rate plans
func (r *GormRepository) GetPopularPlans(ctx context.Context, limit int) ([]*RatePlan, error) {
var plans []*RatePlan
if err := r.db.WithContext(ctx).
Model(&RatePlan{}).
Select("rate_plans.*, COUNT(rate_plan_subscriptions.id) as subscription_count").
Joins("LEFT JOIN rate_plan_subscriptions ON rate_plans.id = rate_plan_subscriptions.rate_plan_id").
Where("rate_plans.is_active = ? AND rate_plans.status = ?", true, PlanStatusActive).
Group("rate_plans.id").
Order("subscription_count DESC").
Limit(limit).
Find(&plans).Error; err != nil {
r.logger.WithError(err).Error("Failed to get popular plans")
return nil, fmt.Errorf("failed to get popular plans: %w", err)
}
return plans, nil
}
@@ -0,0 +1,167 @@
package rateplan
import (
"context"
"fmt"
"time"
"gorm.io/gorm"
)
// CreateRatePlan creates a new rate plan
func (r *GormRepository) CreateRatePlan(ctx context.Context, plan *RatePlan) error {
now := time.Now()
plan.CreatedAt = now
plan.UpdatedAt = now
if err := r.db.WithContext(ctx).Create(plan).Error; err != nil {
r.logger.WithError(err).Error("Failed to create rate plan")
return fmt.Errorf("failed to create rate plan: %w", err)
}
return nil
}
func (r *GormRepository) GetRatePlan(ctx context.Context, id string) (*RatePlan, error) {
var plan RatePlan
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&plan).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("rate plan not found: %s", id)
}
r.logger.WithError(err).Error("Failed to get rate plan")
return nil, fmt.Errorf("failed to get rate plan: %w", err)
}
return &plan, nil
}
func (r *GormRepository) UpdateRatePlan(ctx context.Context, plan *RatePlan) error {
plan.UpdatedAt = time.Now()
result := r.db.WithContext(ctx).Where("id = ?", plan.ID).Updates(plan)
if result.Error != nil {
r.logger.WithError(result.Error).Error("Failed to update rate plan")
return fmt.Errorf("failed to update rate plan: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("rate plan not found: %s", plan.ID)
}
return nil
}
func (r *GormRepository) DeleteRatePlan(ctx context.Context, id string) error {
result := r.db.WithContext(ctx).Where("id = ?", id).Delete(&RatePlan{})
if result.Error != nil {
r.logger.WithError(result.Error).Error("Failed to delete rate plan")
return fmt.Errorf("failed to delete rate plan: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("rate plan not found: %s", id)
}
return nil
}
func (r *GormRepository) ListRatePlans(ctx context.Context, filter *RatePlanFilter) ([]*RatePlan, error) {
query := r.db.WithContext(ctx).Model(&RatePlan{})
if filter.CarrierID != "" {
query = query.Where("carrier_id = ?", filter.CarrierID)
}
if filter.Region != "" {
query = query.Where("region = ?", filter.Region)
}
if filter.PlanType != "" {
query = query.Where("plan_type = ?", filter.PlanType)
}
if filter.Status != "" {
query = query.Where("status = ?", filter.Status)
}
if filter.MinPrice > 0 {
query = query.Where("base_price >= ?", filter.MinPrice)
}
if filter.MaxPrice > 0 {
query = query.Where("base_price <= ?", filter.MaxPrice)
}
if filter.IsActive != nil {
query = query.Where("is_active = ?", *filter.IsActive)
}
if filter.ValidFrom != nil {
query = query.Where("valid_from >= ?", *filter.ValidFrom)
}
if filter.ValidTo != nil {
query = query.Where("valid_to <= ?", *filter.ValidTo)
}
// Apply ordering
if filter.SortBy != "" {
order := "ASC"
if filter.SortOrder == "desc" {
order = "DESC"
}
query = query.Order(fmt.Sprintf("%s %s", filter.SortBy, order))
} else {
query = query.Order("priority DESC, created_at DESC")
}
// Apply pagination
if filter.Limit > 0 {
query = query.Limit(filter.Limit)
if filter.Offset > 0 {
query = query.Offset(filter.Offset)
}
}
var plans []*RatePlan
if err := query.Find(&plans).Error; err != nil {
r.logger.WithError(err).Error("Failed to list rate plans")
return nil, fmt.Errorf("failed to list rate plans: %w", err)
}
return plans, nil
}
// CountRatePlans counts rate plans with filtering
func (r *GormRepository) CountRatePlans(ctx context.Context, filter *RatePlanFilter) (int, error) {
query := r.db.WithContext(ctx).Model(&RatePlan{})
// Apply filters
if filter.CarrierID != "" {
query = query.Where("carrier_id = ?", filter.CarrierID)
}
if filter.Region != "" {
query = query.Where("region = ?", filter.Region)
}
if filter.PlanType != "" {
query = query.Where("plan_type = ?", filter.PlanType)
}
if filter.Status != "" {
query = query.Where("status = ?", filter.Status)
}
if filter.MinPrice > 0 {
query = query.Where("base_price >= ?", filter.MinPrice)
}
if filter.MaxPrice > 0 {
query = query.Where("base_price <= ?", filter.MaxPrice)
}
if filter.IsActive != nil {
query = query.Where("is_active = ?", *filter.IsActive)
}
if filter.ValidFrom != nil {
query = query.Where("valid_from >= ?", *filter.ValidFrom)
}
if filter.ValidTo != nil {
query = query.Where("valid_to <= ?", *filter.ValidTo)
}
var count int64
if err := query.Count(&count).Error; err != nil {
r.logger.WithError(err).Error("Failed to count rate plans")
return 0, fmt.Errorf("failed to count rate plans: %w", err)
}
return int(count), nil
}
@@ -0,0 +1,123 @@
package rateplan
import (
"context"
"fmt"
"time"
"gorm.io/gorm"
)
// CreateSubscription creates a new rate plan subscription
func (r *GormRepository) CreateSubscription(ctx context.Context, subscription *RatePlanSubscription) error {
now := time.Now()
subscription.CreatedAt = now
subscription.UpdatedAt = now
if err := r.db.WithContext(ctx).Create(subscription).Error; err != nil {
r.logger.WithError(err).Error("Failed to create subscription")
return fmt.Errorf("failed to create subscription: %w", err)
}
return nil
}
// GetSubscription retrieves a subscription by ID
func (r *GormRepository) GetSubscription(ctx context.Context, id string) (*RatePlanSubscription, error) {
var subscription RatePlanSubscription
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&subscription).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("subscription not found: %s", id)
}
r.logger.WithError(err).Error("Failed to get subscription")
return nil, fmt.Errorf("failed to get subscription: %w", err)
}
return &subscription, nil
}
// GetActiveSubscription retrieves the active subscription for a profile
func (r *GormRepository) GetActiveSubscription(ctx context.Context, profileID string) (*RatePlanSubscription, error) {
var subscription RatePlanSubscription
err := r.db.WithContext(ctx).
Where("profile_id = ? AND status = ?", profileID, SubscriptionStatusActive).
Order("started_at DESC").
First(&subscription).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil // No active subscription found
}
r.logger.WithError(err).Error("Failed to get active subscription")
return nil, fmt.Errorf("failed to get active subscription: %w", err)
}
return &subscription, nil
}
// UpdateSubscription updates an existing subscription
func (r *GormRepository) UpdateSubscription(ctx context.Context, subscription *RatePlanSubscription) error {
subscription.UpdatedAt = time.Now()
result := r.db.WithContext(ctx).Where("id = ?", subscription.ID).Updates(subscription)
if result.Error != nil {
r.logger.WithError(result.Error).Error("Failed to update subscription")
return fmt.Errorf("failed to update subscription: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("subscription not found: %s", subscription.ID)
}
return nil
}
// DeleteSubscription deletes a subscription
func (r *GormRepository) DeleteSubscription(ctx context.Context, id string) error {
result := r.db.WithContext(ctx).Where("id = ?", id).Delete(&RatePlanSubscription{})
if result.Error != nil {
r.logger.WithError(result.Error).Error("Failed to delete subscription")
return fmt.Errorf("failed to delete subscription: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("subscription not found: %s", id)
}
return nil
}
// ListSubscriptions retrieves subscriptions for a profile
func (r *GormRepository) ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error) {
query := r.db.WithContext(ctx).Where("profile_id = ?", profileID)
// Apply filters
if filter.Status != "" {
query = query.Where("status = ?", filter.Status)
}
if filter.RatePlanID != "" {
query = query.Where("rate_plan_id = ?", filter.RatePlanID)
}
if filter.StartedAfter != nil {
query = query.Where("started_at >= ?", *filter.StartedAfter)
}
if filter.StartedBefore != nil {
query = query.Where("started_at <= ?", *filter.StartedBefore)
}
query = query.Order("started_at DESC")
// Apply pagination
if filter.Limit > 0 {
query = query.Limit(filter.Limit)
if filter.Offset > 0 {
query = query.Offset(filter.Offset)
}
}
var subscriptions []*RatePlanSubscription
if err := query.Find(&subscriptions).Error; err != nil {
r.logger.WithError(err).Error("Failed to list subscriptions")
return nil, fmt.Errorf("failed to list subscriptions: %w", err)
}
return subscriptions, nil
}
@@ -0,0 +1,198 @@
package rateplan
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
// Service provides business logic for rate plan operations
type Service struct {
repo Repository
logger *logrus.Logger
}
// NewService creates a new rate plan service
func NewService(repo Repository, logger *logrus.Logger) *Service {
return &Service{
repo: repo,
logger: logger,
}
}
func (s *Service) CreateRatePlan(ctx context.Context, plan *RatePlan) (*RatePlan, error) {
// Generate ID if not provided
if plan.ID == "" {
plan.ID = uuid.New().String()
}
// Validate rate plan
if err := s.validateRatePlan(plan); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Create the rate plan
if err := s.repo.CreateRatePlan(ctx, plan); err != nil {
s.logger.WithError(err).Error("Failed to create rate plan")
return nil, err
}
s.logger.WithField("plan_id", plan.ID).Info("Rate plan created successfully")
return plan, nil
}
// GetRatePlan retrieves a rate plan by ID
func (s *Service) GetRatePlan(ctx context.Context, id string) (*RatePlan, error) {
plan, err := s.repo.GetRatePlan(ctx, id)
if err != nil {
s.logger.WithError(err).WithField("plan_id", id).Error("Failed to get rate plan")
return nil, err
}
return plan, nil
}
// UpdateRatePlan updates an existing rate plan
func (s *Service) UpdateRatePlan(ctx context.Context, plan *RatePlan) (*RatePlan, error) {
// Validate rate plan
if err := s.validateRatePlan(plan); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Check if plan exists by attempting to update
_, err := s.repo.GetRatePlan(ctx, plan.ID)
if err != nil {
return nil, err
}
// Update the rate plan
if err := s.repo.UpdateRatePlan(ctx, plan); err != nil {
s.logger.WithError(err).Error("Failed to update rate plan")
return nil, err
}
s.logger.WithField("plan_id", plan.ID).Info("Rate plan updated successfully")
return plan, nil
}
// DeleteRatePlan deletes a rate plan
func (s *Service) DeleteRatePlan(ctx context.Context, id string) error {
// Check if plan has active subscriptions
subscriptions, err := s.repo.ListSubscriptions(ctx, "", &SubscriptionFilter{
RatePlanID: id,
Status: SubscriptionStatusActive,
Limit: 1,
})
if err != nil {
return err
}
if len(subscriptions) > 0 {
return fmt.Errorf("cannot delete rate plan with active subscriptions")
}
// Delete the rate plan
if err := s.repo.DeleteRatePlan(ctx, id); err != nil {
s.logger.WithError(err).Error("Failed to delete rate plan")
return err
}
s.logger.WithField("plan_id", id).Info("Rate plan deleted successfully")
return nil
}
// ListRatePlans retrieves rate plans with filtering
func (s *Service) ListRatePlans(ctx context.Context, filter *RatePlanFilter) ([]*RatePlan, error) {
plans, err := s.repo.ListRatePlans(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to list rate plans")
return nil, err
}
return plans, nil
}
// SearchRatePlans searches for rate plans based on criteria
func (s *Service) SearchRatePlans(ctx context.Context, criteria SearchCriteria) ([]*RatePlan, error) {
filter := &RatePlanFilter{
CarrierID: criteria.CarrierID,
Region: criteria.Region,
PlanType: criteria.PlanType,
Status: PlanStatusActive,
IsActive: &[]bool{true}[0],
MinPrice: criteria.MinPrice,
MaxPrice: criteria.MaxPrice,
Limit: criteria.Limit,
Offset: criteria.Offset,
SortBy: criteria.SortBy,
SortOrder: criteria.SortOrder,
}
return s.repo.ListRatePlans(ctx, filter)
}
// CalculateCost calculates the cost for a rate plan based on usage
func (s *Service) CalculateCost(ctx context.Context, req *CalculateCostRequest) (*RatePlanCostCalculation, error) {
// Get the rate plan
plan, err := s.repo.GetRatePlan(ctx, req.RatePlanID)
if err != nil {
return nil, err
}
// Calculate base cost
baseCost := plan.BasePrice
// Calculate overage costs
overageCost := 0.0
if req.DataUsed > 0 && plan.DataAllowance != nil && !plan.DataAllowance.Unlimited {
allowanceMB := plan.DataAllowance.Amount
if plan.DataAllowance.Unit == "GB" {
allowanceMB *= 1024
}
if req.DataUsed > allowanceMB {
overageMB := req.DataUsed - allowanceMB
if plan.OverageRates != nil {
overageCost += float64(overageMB) * plan.OverageRates.DataRate
}
}
}
// Apply discounts
discountCost := 0.0
if len(req.AppliedDiscounts) > 0 && plan.Discounts != nil {
for _, discountID := range req.AppliedDiscounts {
for _, discount := range plan.Discounts {
if discount.ID == discountID && discount.IsActive {
switch discount.Type {
case DiscountTypePercentage:
discountCost += baseCost * discount.Value / 100
case DiscountTypeFixed:
discountCost += discount.Value
}
}
}
}
}
totalCost := baseCost + overageCost - discountCost
calculation := &RatePlanCostCalculation{
RatePlanID: req.RatePlanID,
BaseCost: baseCost,
OverageCost: overageCost,
DiscountCost: discountCost,
TotalCost: totalCost,
Currency: plan.Currency,
Breakdown: map[string]interface{}{
"base_cost": baseCost,
"overage_cost": overageCost,
"discount_cost": discountCost,
"total_cost": totalCost,
},
CalculatedAt: time.Now(),
}
return calculation, nil
}
@@ -0,0 +1,59 @@
package rateplan
import (
"context"
)
// GetUsage retrieves usage for a profile
func (s *Service) GetUsage(ctx context.Context, profileID string) (*RatePlanUsage, error) {
usage, err := s.repo.GetCurrentUsage(ctx, profileID)
if err != nil {
s.logger.WithError(err).WithField("profile_id", profileID).Error("Failed to get usage")
return nil, err
}
return usage, nil
}
// GetUsageHistory retrieves usage history for a profile
func (s *Service) GetUsageHistory(ctx context.Context, profileID string, limit int) ([]*RatePlanUsage, error) {
usageHistory, err := s.repo.ListUsageHistory(ctx, profileID, limit)
if err != nil {
s.logger.WithError(err).Error("Failed to get usage history")
return nil, err
}
return usageHistory, nil
}
// GetUsageAnalytics retrieves usage analytics
func (s *Service) GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsFilter) (*UsageAnalytics, error) {
analytics, err := s.repo.GetUsageAnalytics(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get usage analytics")
return nil, err
}
return analytics, nil
}
// GetRevenueAnalytics retrieves revenue analytics
func (s *Service) GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyticsFilter) (*RevenueAnalytics, error) {
analytics, err := s.repo.GetRevenueAnalytics(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get revenue analytics")
return nil, err
}
return analytics, nil
}
// GetPopularPlans retrieves the most popular rate plans
func (s *Service) GetPopularPlans(ctx context.Context, limit int) ([]*RatePlan, error) {
plans, err := s.repo.GetPopularPlans(ctx, limit)
if err != nil {
s.logger.WithError(err).Error("Failed to get popular plans")
return nil, err
}
return plans, nil
}
@@ -0,0 +1,76 @@
package rateplan
import (
"fmt"
"time"
)
func (s *Service) validateRatePlan(plan *RatePlan) error {
if plan.Name == "" {
return fmt.Errorf("rate plan name is required")
}
if plan.CarrierID == "" {
return fmt.Errorf("carrier ID is required")
}
if plan.Region == "" {
return fmt.Errorf("region is required")
}
if plan.BasePrice < 0 {
return fmt.Errorf("base price cannot be negative")
}
if plan.Currency == "" {
return fmt.Errorf("currency is required")
}
if plan.BillingCycle == "" {
return fmt.Errorf("billing cycle is required")
}
if plan.ValidFrom.IsZero() {
return fmt.Errorf("valid from date is required")
}
return nil
}
func (s *Service) validateSubscribeRequest(req *SubscribeRequest) error {
if req.ProfileID == "" {
return fmt.Errorf("profile ID is required")
}
if req.RatePlanID == "" {
return fmt.Errorf("rate plan ID is required")
}
return nil
}
func (s *Service) calculateNextBillingDate(cycle BillingCycle, from time.Time) time.Time {
switch cycle {
case BillingCycleDaily:
return from.AddDate(0, 0, 1)
case BillingCycleWeekly:
return from.AddDate(0, 0, 7)
case BillingCycleMonthly:
return from.AddDate(0, 1, 0)
case BillingCycleQuarterly:
return from.AddDate(0, 3, 0)
case BillingCycleYearly:
return from.AddDate(1, 0, 0)
default:
return from.AddDate(0, 1, 0) // Default to monthly
}
}
func (s *Service) calculateCycleEnd(cycle BillingCycle, cycleStart time.Time) time.Time {
switch cycle {
case BillingCycleDaily:
return cycleStart.AddDate(0, 0, 1).Add(-time.Nanosecond)
case BillingCycleWeekly:
return cycleStart.AddDate(0, 0, 7).Add(-time.Nanosecond)
case BillingCycleMonthly:
return cycleStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
case BillingCycleQuarterly:
return cycleStart.AddDate(0, 3, 0).Add(-time.Nanosecond)
case BillingCycleYearly:
return cycleStart.AddDate(1, 0, 0).Add(-time.Nanosecond)
default:
return cycleStart.AddDate(0, 1, 0).Add(-time.Nanosecond) // Default to monthly
}
}
@@ -0,0 +1,196 @@
package rateplan
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
func (s *Service) SubscribeToPlan(ctx context.Context, req *SubscribeRequest) (*RatePlanSubscription, error) {
if err := s.validateSubscribeRequest(req); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
plan, err := s.repo.GetRatePlan(ctx, req.RatePlanID)
if err != nil {
return nil, err
}
if !plan.IsActive || plan.Status != PlanStatusActive {
return nil, fmt.Errorf("rate plan is not available for subscription")
}
activeSub, err := s.repo.GetActiveSubscription(ctx, req.ProfileID)
if err != nil {
return nil, err
}
if activeSub != nil {
return nil, fmt.Errorf("profile already has an active subscription")
}
// Create subscription
subscription := &RatePlanSubscription{
ID: uuid.New().String(),
ProfileID: req.ProfileID,
RatePlanID: req.RatePlanID,
Status: SubscriptionStatusActive,
StartedAt: time.Now(),
BillingCycle: plan.BillingCycle,
NextBillingDate: s.calculateNextBillingDate(plan.BillingCycle, time.Now()),
AutoRenew: req.AutoRenew,
CurrentCycle: time.Now(),
AppliedDiscounts: req.AppliedDiscounts,
Metadata: req.Metadata,
}
if err := s.repo.CreateSubscription(ctx, subscription); err != nil {
s.logger.WithError(err).Error("Failed to create subscription")
return nil, err
}
s.logger.WithFields(logrus.Fields{
"subscription_id": subscription.ID,
"profile_id": req.ProfileID,
"rate_plan_id": req.RatePlanID,
}).Info("Subscription created successfully")
return subscription, nil
}
// GetSubscription retrieves a subscription by ID
func (s *Service) GetSubscription(ctx context.Context, id string) (*RatePlanSubscription, error) {
subscription, err := s.repo.GetSubscription(ctx, id)
if err != nil {
s.logger.WithError(err).WithField("subscription_id", id).Error("Failed to get subscription")
return nil, err
}
return subscription, nil
}
// UpdateSubscription updates an existing subscription
func (s *Service) UpdateSubscription(ctx context.Context, subscription *RatePlanSubscription) (*RatePlanSubscription, error) {
if err := s.repo.UpdateSubscription(ctx, subscription); err != nil {
s.logger.WithError(err).Error("Failed to update subscription")
return nil, err
}
s.logger.WithField("subscription_id", subscription.ID).Info("Subscription updated successfully")
return subscription, nil
}
// CancelSubscription cancels a subscription
func (s *Service) CancelSubscription(ctx context.Context, subscriptionID string, reason string) error {
subscription, err := s.repo.GetSubscription(ctx, subscriptionID)
if err != nil {
return err
}
if subscription.Status != SubscriptionStatusActive {
return fmt.Errorf("subscription is not active")
}
now := time.Now()
subscription.Status = SubscriptionStatusCancelled
subscription.EndedAt = &now
subscription.UpdatedAt = now
if subscription.Metadata == nil {
subscription.Metadata = make(map[string]interface{})
}
subscription.Metadata["cancellation_reason"] = reason
if err := s.repo.UpdateSubscription(ctx, subscription); err != nil {
s.logger.WithError(err).Error("Failed to cancel subscription")
return err
}
s.logger.WithField("subscription_id", subscriptionID).Info("Subscription cancelled successfully")
return nil
}
// GetActiveSubscription retrieves the active subscription for a profile
func (s *Service) GetActiveSubscription(ctx context.Context, profileID string) (*RatePlanSubscription, error) {
subscription, err := s.repo.GetActiveSubscription(ctx, profileID)
if err != nil {
s.logger.WithError(err).WithField("profile_id", profileID).Error("Failed to get active subscription")
return nil, err
}
return subscription, nil
}
// ListSubscriptions retrieves subscriptions for a profile
func (s *Service) ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error) {
subscriptions, err := s.repo.ListSubscriptions(ctx, profileID, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to list subscriptions")
return nil, err
}
return subscriptions, nil
}
func (s *Service) RecordUsage(ctx context.Context, req *RecordUsageRequest) (*RatePlanUsage, error) {
// Get active subscription
subscription, err := s.repo.GetActiveSubscription(ctx, req.ProfileID)
if err != nil {
return nil, err
}
if subscription == nil {
return nil, fmt.Errorf("no active subscription found")
}
// Get current usage
currentUsage, err := s.repo.GetCurrentUsage(ctx, req.ProfileID)
if err != nil {
return nil, err
}
// Create or update usage record
var usage *RatePlanUsage
if currentUsage == nil {
// Create new usage record
usage = &RatePlanUsage{
ID: uuid.New().String(),
RatePlanID: subscription.RatePlanID,
ProfileID: req.ProfileID,
CycleStart: subscription.CurrentCycle,
CycleEnd: s.calculateCycleEnd(subscription.BillingCycle, subscription.CurrentCycle),
DataUsed: req.DataUsed,
VoiceUsed: req.VoiceUsed,
SMSUsed: req.SMSUsed,
}
if err := s.repo.CreateUsage(ctx, usage); err != nil {
s.logger.WithError(err).Error("Failed to create usage record")
return nil, err
}
} else {
// Update existing usage record
currentUsage.DataUsed += req.DataUsed
currentUsage.VoiceUsed += req.VoiceUsed
currentUsage.SMSUsed += req.SMSUsed
if err := s.repo.UpdateUsage(ctx, currentUsage); err != nil {
s.logger.WithError(err).Error("Failed to update usage record")
return nil, err
}
usage = currentUsage
}
s.logger.WithFields(logrus.Fields{
"profile_id": req.ProfileID,
"data_used": req.DataUsed,
"voice_used": req.VoiceUsed,
"sms_used": req.SMSUsed,
}).Info("Usage recorded successfully")
return usage, nil
}
@@ -0,0 +1,166 @@
package rateplan
import (
"time"
)
type DataAllowance struct {
Type DataAllowanceType `json:"type"`
Amount int64 `json:"amount"` // in MB or GB depending on type
Unit string `json:"unit"` // "MB", "GB", "TB"
Unlimited bool `json:"unlimited"`
SpeedLimit *int64 `json:"speed_limit,omitempty"` // in Kbps
}
type DataAllowanceType string
const (
DataAllowanceTypeDaily DataAllowanceType = "daily"
DataAllowanceTypeMonthly DataAllowanceType = "monthly"
DataAllowanceTypeCycle DataAllowanceType = "cycle"
DataAllowanceTypeLifetime DataAllowanceType = "lifetime"
)
type VoiceAllowance struct {
Type VoiceAllowanceType `json:"type"`
Minutes int64 `json:"minutes"`
Unlimited bool `json:"unlimited"`
Destinations []string `json:"destinations,omitempty"` // country codes
}
type VoiceAllowanceType string
const (
VoiceAllowanceTypeDaily VoiceAllowanceType = "daily"
VoiceAllowanceTypeMonthly VoiceAllowanceType = "monthly"
VoiceAllowanceTypeCycle VoiceAllowanceType = "cycle"
VoiceAllowanceTypeUnlimited VoiceAllowanceType = "unlimited"
)
type SMSAllowance struct {
Type SMSAllowanceType `json:"type"`
Messages int64 `json:"messages"`
Unlimited bool `json:"unlimited"`
Destinations []string `json:"destinations,omitempty"` // country codes
}
type SMSAllowanceType string
const (
SMSAllowanceTypeDaily SMSAllowanceType = "daily"
SMSAllowanceTypeMonthly SMSAllowanceType = "monthly"
SMSAllowanceTypeCycle SMSAllowanceType = "cycle"
SMSAllowanceTypeUnlimited SMSAllowanceType = "unlimited"
)
type OverageRates struct {
DataRate float64 `json:"data_rate"` // per MB
VoiceRate float64 `json:"voice_rate"` // per minute
SMSRate float64 `json:"sms_rate"` // per message
Currency string `json:"currency"`
}
type PlanFeature struct {
Name string `json:"name"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
Config map[string]interface{} `json:"config,omitempty"`
}
type EarlyTermination struct {
Enabled bool `json:"enabled"`
FeeType string `json:"fee_type"` // "fixed", "percentage", "remaining_months"
FeeAmount float64 `json:"fee_amount"` // for fixed fee type
FeePercentage float64 `json:"fee_percentage"` // for percentage fee type
MinMonths int `json:"min_months"` // minimum months before termination fee applies
}
type Discount struct {
ID string `json:"id"`
Name string `json:"name"`
Type DiscountType `json:"type"`
Value float64 `json:"value"`
ValidFrom time.Time `json:"valid_from"`
ValidTo *time.Time `json:"valid_to,omitempty"`
Conditions string `json:"conditions,omitempty"`
IsActive bool `json:"is_active"`
}
type DiscountType string
const (
DiscountTypePercentage DiscountType = "percentage"
DiscountTypeFixed DiscountType = "fixed"
DiscountTypeRecurring DiscountType = "recurring"
)
// RatePlanUsage tracks actual usage against a rate plan
type RatePlanUsage struct {
ID string `json:"id" db:"id"`
RatePlanID string `json:"rate_plan_id" db:"rate_plan_id"`
ProfileID string `json:"profile_id" db:"profile_id"`
CycleStart time.Time `json:"cycle_start" db:"cycle_start"`
CycleEnd time.Time `json:"cycle_end" db:"cycle_end"`
DataUsed int64 `json:"data_used" db:"data_used"` // in MB
VoiceUsed int64 `json:"voice_used" db:"voice_used"` // in minutes
SMSUsed int64 `json:"sms_used" db:"sms_used"` // count
LastUpdated time.Time `json:"last_updated" db:"last_updated"`
}
// RatePlanSubscription represents an active subscription to a rate plan
type RatePlanSubscription struct {
ID string `json:"id" db:"id"`
ProfileID string `json:"profile_id" db:"profile_id"`
RatePlanID string `json:"rate_plan_id" db:"rate_plan_id"`
Status SubscriptionStatus `json:"status" db:"status"`
StartedAt time.Time `json:"started_at" db:"started_at"`
EndedAt *time.Time `json:"ended_at,omitempty" db:"ended_at"`
BillingCycle BillingCycle `json:"billing_cycle" db:"billing_cycle"`
NextBillingDate time.Time `json:"next_billing_date" db:"next_billing_date"`
AutoRenew bool `json:"auto_renew" db:"auto_renew"`
CurrentCycle time.Time `json:"current_cycle" db:"current_cycle"`
AppliedDiscounts []string `json:"applied_discounts,omitempty" db:"applied_discounts"`
Metadata map[string]any `json:"metadata,omitempty" db:"metadata"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// SubscriptionStatus defines the status of a subscription
type SubscriptionStatus string
const (
SubscriptionStatusActive SubscriptionStatus = "active"
SubscriptionStatusSuspended SubscriptionStatus = "suspended"
SubscriptionStatusCancelled SubscriptionStatus = "cancelled"
SubscriptionStatusExpired SubscriptionStatus = "expired"
SubscriptionStatusPending SubscriptionStatus = "pending"
)
// RatePlanFilter defines filtering options for rate plan queries
type RatePlanFilter struct {
CarrierID string `json:"carrier_id,omitempty"`
Region string `json:"region,omitempty"`
PlanType PlanType `json:"plan_type,omitempty"`
Status PlanStatus `json:"status,omitempty"`
MinPrice float64 `json:"min_price,omitempty"`
MaxPrice float64 `json:"max_price,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
ValidFrom *time.Time `json:"valid_from,omitempty"`
ValidTo *time.Time `json:"valid_to,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
SortBy string `json:"sort_by,omitempty"`
SortOrder string `json:"sort_order,omitempty"`
}
// RatePlanCostCalculation represents the result of cost calculation
type RatePlanCostCalculation struct {
RatePlanID string `json:"rate_plan_id"`
BaseCost float64 `json:"base_cost"`
OverageCost float64 `json:"overage_cost"`
DiscountCost float64 `json:"discount_cost"`
TotalCost float64 `json:"total_cost"`
Currency string `json:"currency"`
Breakdown map[string]any `json:"breakdown"`
CalculatedAt time.Time `json:"calculated_at"`
}
@@ -0,0 +1,65 @@
package rateplan
import (
"time"
)
type RatePlan struct {
ID string `json:"id" db:"id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
CarrierID string `json:"carrier_id" db:"carrier_id"`
Region string `json:"region" db:"region"`
PlanType PlanType `json:"plan_type" db:"plan_type"`
Status PlanStatus `json:"status" db:"status"`
BasePrice float64 `json:"base_price" db:"base_price"`
Currency string `json:"currency" db:"currency"`
BillingCycle BillingCycle `json:"billing_cycle" db:"billing_cycle"`
DataAllowance *DataAllowance `json:"data_allowance,omitempty" db:"data_allowance"`
VoiceAllowance *VoiceAllowance `json:"voice_allowance,omitempty" db:"voice_allowance"`
SMSAllowance *SMSAllowance `json:"sms_allowance,omitempty" db:"sms_allowance"`
OverageRates *OverageRates `json:"overage_rates,omitempty" db:"overage_rates"`
Features []PlanFeature `json:"features,omitempty" db:"features"`
ActivationFee float64 `json:"activation_fee" db:"activation_fee"`
EarlyTermination *EarlyTermination `json:"early_termination,omitempty" db:"early_termination"`
Discounts []Discount `json:"discounts,omitempty" db:"discounts"`
ValidFrom time.Time `json:"valid_from" db:"valid_from"`
ValidTo *time.Time `json:"valid_to,omitempty" db:"valid_to"`
Priority int `json:"priority" db:"priority"`
IsActive bool `json:"is_active" db:"is_active"`
Metadata map[string]any `json:"metadata,omitempty" db:"metadata"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// PlanType defines the type of rate plan
type PlanType string
const (
PlanTypePrepaid PlanType = "prepaid"
PlanTypePostpaid PlanType = "postpaid"
PlanTypeHybrid PlanType = "hybrid"
PlanTypePayAsYouGo PlanType = "pay_as_you_go"
PlanTypeUnlimited PlanType = "unlimited"
)
// PlanStatus defines the status of a rate plan
type PlanStatus string
const (
PlanStatusDraft PlanStatus = "draft"
PlanStatusActive PlanStatus = "active"
PlanStatusInactive PlanStatus = "inactive"
PlanStatusArchived PlanStatus = "archived"
)
// BillingCycle defines the billing frequency
type BillingCycle string
const (
BillingCycleDaily BillingCycle = "daily"
BillingCycleWeekly BillingCycle = "weekly"
BillingCycleMonthly BillingCycle = "monthly"
BillingCycleQuarterly BillingCycle = "quarterly"
BillingCycleYearly BillingCycle = "yearly"
)
@@ -0,0 +1,193 @@
package rateplan
import (
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
)
// SubscriptionFilter defines filtering options for subscription queries
type SubscriptionFilter struct {
Status SubscriptionStatus `json:"status,omitempty"`
RatePlanID string `json:"rate_plan_id,omitempty"`
StartedAfter *time.Time `json:"started_after,omitempty"`
StartedBefore *time.Time `json:"started_before,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
}
// UsageAnalyticsFilter defines filtering options for usage analytics
type UsageAnalyticsFilter struct {
RatePlanID string `json:"rate_plan_id,omitempty"`
CarrierID string `json:"carrier_id,omitempty"`
Region string `json:"region,omitempty"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
GroupBy string `json:"group_by,omitempty"` // "day", "week", "month"
}
// RevenueAnalyticsFilter defines filtering options for revenue analytics
type RevenueAnalyticsFilter struct {
RatePlanID string `json:"rate_plan_id,omitempty"`
CarrierID string `json:"carrier_id,omitempty"`
Region string `json:"region,omitempty"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
GroupBy string `json:"group_by,omitempty"` // "day", "week", "month"
}
// UsageAnalytics contains usage statistics
type UsageAnalytics struct {
TotalDataUsed int64 `json:"total_data_used"`
TotalVoiceUsed int64 `json:"total_voice_used"`
TotalSMSUsed int64 `json:"total_sms_used"`
ActiveUsers int `json:"active_users"`
AverageUsage map[string]float64 `json:"average_usage"`
UsageByPlan map[string]int64 `json:"usage_by_plan"`
UsageByRegion map[string]int64 `json:"usage_by_region"`
TimelineData []TimelineDataPoint `json:"timeline_data"`
}
// RevenueAnalytics contains revenue statistics
type RevenueAnalytics struct {
TotalRevenue float64 `json:"total_revenue"`
RevenueByPlan map[string]float64 `json:"revenue_by_plan"`
RevenueByCarrier map[string]float64 `json:"revenue_by_carrier"`
RevenueByRegion map[string]float64 `json:"revenue_by_region"`
AverageRevenue map[string]float64 `json:"average_revenue"`
TimelineData []TimelineDataPoint `json:"timeline_data"`
}
// TimelineDataPoint represents a data point in time series
type TimelineDataPoint struct {
Timestamp time.Time `json:"timestamp"`
Value float64 `json:"value"`
Label string `json:"label,omitempty"`
}
type SearchCriteria struct {
CarrierID string `json:"carrier_id,omitempty"`
Region string `json:"region,omitempty"`
PlanType PlanType `json:"plan_type,omitempty"`
MinPrice float64 `json:"min_price,omitempty"`
MaxPrice float64 `json:"max_price,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
SortBy string `json:"sort_by,omitempty"`
SortOrder string `json:"sort_order,omitempty"`
}
type SubscribeRequest struct {
ProfileID string `json:"profile_id"`
RatePlanID string `json:"rate_plan_id"`
AutoRenew bool `json:"auto_renew"`
AppliedDiscounts []string `json:"applied_discounts,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type RecordUsageRequest struct {
ProfileID string `json:"profile_id"`
DataUsed int64 `json:"data_used"` // in MB
VoiceUsed int64 `json:"voice_used"` // in minutes
SMSUsed int64 `json:"sms_used"` // count
}
type CalculateCostRequest struct {
RatePlanID string `json:"rate_plan_id"`
DataUsed int64 `json:"data_used"` // in MB
VoiceUsed int64 `json:"voice_used"` // in minutes
SMSUsed int64 `json:"sms_used"` // count
AppliedDiscounts []string `json:"applied_discounts,omitempty"`
}
type PriceOptimization struct {
CurrentPrice float64 `json:"current_price"`
MarketAverage float64 `json:"market_average"`
RecommendedPrice float64 `json:"recommended_price"`
PriceDifference float64 `json:"price_difference"`
CompetitorCount int `json:"competitor_count"`
OptimizedAt time.Time `json:"optimized_at"`
}
type CostBreakdown struct {
RatePlanID string `json:"rate_plan_id"`
Currency string `json:"currency"`
TotalCost float64 `json:"total_cost"`
Subtotal float64 `json:"subtotal"`
DiscountTotal float64 `json:"discount_total"`
BreakdownItems []CostItem `json:"breakdown_items"`
CalculatedAt time.Time `json:"calculated_at"`
}
type CostItem struct {
Type string `json:"type"`
Description string `json:"description"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
}
type CarrierRatePlanCriteria struct {
Region string `json:"region"`
PlanType PlanType `json:"plan_type"`
MaxBudget float64 `json:"max_budget"`
Urgency string `json:"urgency"`
Preferences map[string]interface{} `json:"preferences,omitempty"`
}
type RecommendationCriteria struct {
Region string `json:"region"`
PlanType PlanType `json:"plan_type"`
MaxBudget float64 `json:"max_budget"`
PreferredData int64 `json:"preferred_data"`
PreferredVoice int64 `json:"preferred_voice"`
PreferredSMS int64 `json:"preferred_sms"`
MaxResults int `json:"max_results"`
}
type CarrierRatePlanResult struct {
Carrier *smdp.Carrier `json:"carrier"`
RatePlan *RatePlan `json:"rate_plan"`
TotalScore float64 `json:"total_score"`
SelectedAt time.Time `json:"selected_at"`
}
type RatePlanRecommendation struct {
RatePlanID string `json:"rate_plan_id"`
RatePlanName string `json:"rate_plan_name"`
CarrierID string `json:"carrier_id"`
CarrierName string `json:"carrier_name"`
Price float64 `json:"price"`
Currency string `json:"currency"`
Relevance float64 `json:"relevance"`
Features []PlanFeature `json:"features"`
DataAllowance *DataAllowance `json:"data_allowance"`
VoiceAllowance *VoiceAllowance `json:"voice_allowance"`
SMSAllowance *SMSAllowance `json:"sms_allowance"`
RecommendedAt time.Time `json:"recommended_at"`
}
type CarrierRatePlanAnalytics struct {
CarrierID string `json:"carrier_id"`
CarrierName string `json:"carrier_name"`
Region string `json:"region"`
HealthStatus string `json:"health_status"`
Priority int `json:"priority"`
TotalPlans int `json:"total_plans"`
ActivePlans int `json:"active_plans"`
GeneratedAt time.Time `json:"generated_at"`
PlanAnalytics []RatePlanAnalytics `json:"plan_analytics"`
}
type RatePlanAnalytics struct {
RatePlanID string `json:"rate_plan_id"`
RatePlanName string `json:"rate_plan_name"`
BasePrice float64 `json:"base_price"`
Currency string `json:"currency"`
ActiveSubscriptions int `json:"active_subscriptions"`
PlanType PlanType `json:"plan_type"`
BillingCycle BillingCycle `json:"billing_cycle"`
DataAllowance *DataAllowance `json:"data_allowance"`
VoiceAllowance *VoiceAllowance `json:"voice_allowance"`
SMSAllowance *SMSAllowance `json:"sms_allowance"`
Features []PlanFeature `json:"features"`
}
+3 -3
View File
@@ -25,9 +25,9 @@ func setupRoutes(router *gin.Engine, client *es2.ES2Client, repo repository.Prof
esim := api.Group("/esim")
{
esim.POST("/profiles", handler.OrderProfileHandlerWithRepo(client, repo, webhookClient, messageQueue))
esim.GET("/profiles", handler.ListProfilesHandlerWithRepo(repo))
esim.GET("/profiles/:profileId", handler.GetProfileHandlerWithRepo(client, repo))
esim.DELETE("/profiles/:profileId", handler.DeleteProfileHandlerWithRepo(client, repo, webhookClient, messageQueue))
esim.GET("/profiles", handler.ListProfilesHandler(repo))
esim.GET("/profiles/:profileId", handler.GetProfileHandler(repo))
esim.DELETE("/profiles/:profileId", handler.DeleteProfileHandler(repo))
}
carrier := api.Group("/carrier")