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