refactor: Rename service package to services and move repository implementations to repository package

- Rename internal/service to internal/services in carrier-connector
- Update service imports in selection_integration.go and smdp_integration.go
- Move currency repository files from internal/currency to internal/repository
- Move rate plan repository files from internal/rateplan to internal/repository
- Update package declarations and imports for currency domain types
- Add currency package imports
This commit is contained in:
nutcas3
2026-05-02 01:21:18 +03:00
parent c93dd63474
commit 295649bcea
21 changed files with 163 additions and 177 deletions
@@ -0,0 +1,244 @@
package services
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
"github.com/sirupsen/logrus"
)
// AnalyticsServiceImpl handles currency analytics operations
type AnalyticsServiceImpl struct {
repository currency.Repository
logger *logrus.Logger
}
// NewAnalyticsService creates a new analytics service
func NewAnalyticsService(repository currency.Repository, logger *logrus.Logger) *AnalyticsServiceImpl {
return &AnalyticsServiceImpl{
repository: repository,
logger: logger,
}
}
// GetRevenueByCurrency calculates revenue breakdown by currency
func (s *AnalyticsServiceImpl) GetRevenueByCurrency(ctx context.Context, filter *currency.TransactionFilter) (map[string]float64, error) {
if filter == nil {
filter = &currency.TransactionFilter{}
}
filter.Status = currency.TransactionStatusCompleted
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get transactions for revenue analysis")
return nil, fmt.Errorf("failed to get transactions: %w", err)
}
revenueByCurrency := make(map[string]float64)
for _, tx := range transactions {
if tx.Type == currency.TransactionTypeSubscription || tx.Type == currency.TransactionTypeUsage || tx.Type == currency.TransactionTypeOverage {
revenueByCurrency[tx.Currency] += tx.Amount
}
}
return revenueByCurrency, nil
}
// GetTransactionVolumeByCurrency calculates transaction volume by currency
func (s *AnalyticsServiceImpl) GetTransactionVolumeByCurrency(ctx context.Context, filter *currency.TransactionFilter) (map[string]int64, error) {
if filter == nil {
filter = &currency.TransactionFilter{}
}
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get transactions for volume analysis")
return nil, fmt.Errorf("failed to get transactions: %w", err)
}
volumeByCurrency := make(map[string]int64)
for _, tx := range transactions {
volumeByCurrency[tx.Currency]++
}
return volumeByCurrency, nil
}
// GetExchangeRateTrends retrieves exchange rate trends for a currency pair
func (s *AnalyticsServiceImpl) GetExchangeRateTrends(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*currency.ExchangeRate, error) {
filter := &currency.ExchangeRateFilter{
FromCurrency: fromCurrency,
ToCurrency: toCurrency,
IsValid: &[]bool{false}[0], // Include historical rates
Limit: days,
SortBy: "valid_from",
SortOrder: "desc",
}
rates, err := s.repository.ListExchangeRates(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get exchange rate trends")
return nil, fmt.Errorf("failed to get exchange rate trends: %w", err)
}
return rates, nil
}
// GetCurrencyUsageStats retrieves currency usage statistics
func (s *AnalyticsServiceImpl) GetCurrencyUsageStats(ctx context.Context) (*currency.CurrencyUsageStats, error) {
// Get total currencies
totalCurrencies, err := s.repository.CountCurrencies(ctx, &currency.CurrencyFilter{})
if err != nil {
return nil, fmt.Errorf("failed to count currencies: %w", err)
}
// Get active currencies
activeCurrencies, err := s.repository.CountCurrencies(ctx, &currency.CurrencyFilter{
IsActive: &[]bool{true}[0],
})
if err != nil {
return nil, fmt.Errorf("failed to count active currencies: %w", err)
}
// Get total transactions
totalTransactions, err := s.repository.CountTransactions(ctx, &currency.TransactionFilter{})
if err != nil {
return nil, fmt.Errorf("failed to count transactions: %w", err)
}
// Get total volume
transactions, err := s.repository.ListTransactions(ctx, &currency.TransactionFilter{
Status: currency.TransactionStatusCompleted,
})
if err != nil {
return nil, fmt.Errorf("failed to get transactions for volume: %w", err)
}
totalVolume := 0.0
currencyDistribution := make(map[string]int64)
for _, tx := range transactions {
totalVolume += tx.BaseAmount
currencyDistribution[tx.Currency]++
}
// Find most used currency
mostUsedCurrency := ""
maxCount := int64(0)
for currency, count := range currencyDistribution {
if count > maxCount {
maxCount = count
mostUsedCurrency = currency
}
}
// Get exchange rate count (using ListExchangeRates for now since CountExchangeRates doesn't exist)
exchangeRates, err := s.repository.ListExchangeRates(ctx, &currency.ExchangeRateFilter{})
if err != nil {
return nil, fmt.Errorf("failed to count exchange rates: %w", err)
}
return &currency.CurrencyUsageStats{
TotalCurrencies: totalCurrencies,
ActiveCurrencies: activeCurrencies,
TotalTransactions: int64(totalTransactions),
TotalVolume: totalVolume,
MostUsedCurrency: mostUsedCurrency,
CurrencyDistribution: currencyDistribution,
ExchangeRateCount: len(exchangeRates),
LastUpdated: time.Now(),
}, nil
}
// GetMonthlyRevenueTrends calculates monthly revenue trends
func (s *AnalyticsServiceImpl) GetMonthlyRevenueTrends(ctx context.Context, months int) (map[string]float64, error) {
endDate := time.Now()
startDate := endDate.AddDate(0, -months, 0)
filter := &currency.TransactionFilter{
Status: currency.TransactionStatusCompleted,
FromDate: &startDate,
ToDate: &endDate,
}
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get transactions for monthly trends")
return nil, fmt.Errorf("failed to get transactions: %w", err)
}
monthlyRevenue := make(map[string]float64)
for _, tx := range transactions {
monthKey := tx.CreatedAt.Format("2006-01")
monthlyRevenue[monthKey] += tx.BaseAmount
}
return monthlyRevenue, nil
}
// GetTopCurrenciesByRevenue returns top currencies by revenue
func (s *AnalyticsServiceImpl) GetTopCurrenciesByRevenue(ctx context.Context, limit int) ([]*currency.CurrencyRevenue, error) {
revenueByCurrency, err := s.GetRevenueByCurrency(ctx, &currency.TransactionFilter{
Status: currency.TransactionStatusCompleted,
})
if err != nil {
return nil, err
}
// Convert to slice and sort
var currencyRevenues []*currency.CurrencyRevenue
for currency, revenue := range revenueByCurrency {
currencyRevenues = append(currencyRevenues, &currency.CurrencyRevenue{
Currency: currency,
Revenue: revenue,
})
}
// Simple sort (in production, use proper sorting)
if len(currencyRevenues) > limit {
currencyRevenues = currencyRevenues[:limit]
}
return currencyRevenues, nil
}
// GetTransactionTypeAnalytics returns analytics by transaction type
func (s *AnalyticsServiceImpl) GetTransactionTypeAnalytics(ctx context.Context, filter *currency.TransactionFilter) (map[string]*currency.TransactionTypeStats, error) {
if filter == nil {
filter = &currency.TransactionFilter{}
}
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get transactions for type analytics")
return nil, fmt.Errorf("failed to get transactions: %w", err)
}
typeStats := make(map[string]*currency.TransactionTypeStats)
for _, tx := range transactions {
typeKey := string(tx.Type)
if _, exists := typeStats[typeKey]; !exists {
typeStats[typeKey] = &currency.TransactionTypeStats{
Type: tx.Type,
Count: 0,
Amount: 0.0,
Currency: tx.Currency,
}
}
stats := typeStats[typeKey]
stats.Count++
stats.Amount += tx.BaseAmount
}
return typeStats, nil
}
@@ -0,0 +1,148 @@
package services
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
// BillingServiceImpl handles multi-currency billing operations
type BillingServiceImpl struct {
repository Repository
exchangeService *ExchangeRateService
logger *logrus.Logger
baseCurrency string
}
// NewBillingService creates a new billing service
func NewBillingService(repository Repository, exchangeService *ExchangeRateService, logger *logrus.Logger, baseCurrency string) *BillingServiceImpl {
return &BillingServiceImpl{
repository: repository,
exchangeService: exchangeService,
logger: logger,
baseCurrency: baseCurrency,
}
}
// ProcessBilling processes a billing request in multi-currency context
func (s *BillingServiceImpl) ProcessBilling(ctx context.Context, req *BillingRequest) (*BillingResponse, error) {
// Validate request
if err := s.validateBillingRequest(req); err != nil {
return nil, fmt.Errorf("invalid billing request: %w", err)
}
// Convert to base currency if needed
baseAmount := req.Amount
exchangeRate := 1.0
if req.Currency != s.baseCurrency {
conversion, err := s.exchangeService.ConvertAmount(ctx, req.Amount, req.Currency, s.baseCurrency)
if err != nil {
s.logger.WithError(err).Error("Failed to convert currency for billing")
return nil, fmt.Errorf("currency conversion failed: %w", err)
}
baseAmount = conversion.ConvertedAmount
exchangeRate = conversion.ExchangeRate
}
// Create transaction
transaction := &Transaction{
ID: uuid.New().String(),
ProfileID: req.ProfileID,
SubscriptionID: req.SubscriptionID,
Type: TransactionTypeSubscription,
Amount: req.Amount,
Currency: req.Currency,
BaseAmount: baseAmount,
BaseCurrency: s.baseCurrency,
ExchangeRate: exchangeRate,
Description: req.Description,
Status: TransactionStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save transaction
if err := s.repository.CreateTransaction(ctx, transaction); err != nil {
s.logger.WithError(err).Error("Failed to create billing transaction")
return nil, fmt.Errorf("failed to create transaction: %w", err)
}
// Process payment (in real implementation, this would integrate with payment gateway)
transaction.Status = TransactionStatusCompleted
if err := s.repository.UpdateTransaction(ctx, transaction); err != nil {
s.logger.WithError(err).Error("Failed to update transaction status")
return nil, fmt.Errorf("failed to update transaction: %w", err)
}
s.logger.WithFields(logrus.Fields{
"transaction_id": transaction.ID,
"profile_id": req.ProfileID,
"amount": req.Amount,
"currency": req.Currency,
"base_amount": baseAmount,
"base_currency": s.baseCurrency,
}).Info("Billing processed successfully")
return &BillingResponse{
TransactionID: transaction.ID,
Amount: transaction.Amount,
Currency: transaction.Currency,
BaseAmount: transaction.BaseAmount,
BaseCurrency: transaction.BaseCurrency,
ExchangeRate: transaction.ExchangeRate,
Status: string(transaction.Status),
ProcessedAt: time.Now(),
}, nil
}
// ConvertAmount converts an amount between currencies
func (s *BillingServiceImpl) ConvertAmount(ctx context.Context, req *CurrencyConversionRequest) (*CurrencyConversionResponse, error) {
return s.exchangeService.ConvertAmount(ctx, req.Amount, req.FromCurrency, req.ToCurrency)
}
// GetBillingHistory retrieves billing history for a profile
func (s *BillingServiceImpl) GetBillingHistory(ctx context.Context, profileID string, filter *TransactionFilter) ([]*Transaction, error) {
if filter == nil {
filter = &TransactionFilter{}
}
filter.ProfileID = profileID
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get billing history")
return nil, fmt.Errorf("failed to get billing history: %w", err)
}
return transactions, nil
}
// validateBillingRequest validates a billing request
func (s *BillingServiceImpl) validateBillingRequest(req *BillingRequest) error {
if req.ProfileID == "" {
return fmt.Errorf("profile ID is required")
}
if req.SubscriptionID == "" {
return fmt.Errorf("subscription ID is required")
}
if req.Amount <= 0 {
return fmt.Errorf("amount must be positive")
}
if req.Currency == "" {
return fmt.Errorf("currency is required")
}
if req.BillingDate.IsZero() {
req.BillingDate = time.Now()
}
// Validate currency
if err := s.exchangeService.ValidateCurrencyPair(context.Background(), req.Currency, s.baseCurrency); err != nil {
return fmt.Errorf("invalid currency: %w", err)
}
return nil
}
@@ -0,0 +1,197 @@
package services
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
"github.com/sirupsen/logrus"
)
type ExchangeRateService struct {
repository currency.Repository
logger *logrus.Logger
providers []currency.ExchangeRateProvider
baseCurrency string
}
func NewExchangeRateService(repository currency.Repository, logger *logrus.Logger, baseCurrency string) *ExchangeRateService {
return &ExchangeRateService{
repository: repository,
logger: logger,
providers: make([]currency.ExchangeRateProvider, 0),
baseCurrency: baseCurrency,
}
}
func (s *ExchangeRateService) AddProvider(provider currency.ExchangeRateProvider) {
s.providers = append(s.providers, provider)
}
func (s *ExchangeRateService) GetExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*currency.ExchangeRate, error) {
if fromCurrency == toCurrency {
return &currency.ExchangeRate{
FromCurrency: fromCurrency,
ToCurrency: toCurrency,
Rate: 1.0,
Source: "direct",
ValidFrom: time.Now(),
IsActive: true,
}, nil
}
rate, err := s.repository.GetLatestExchangeRate(ctx, fromCurrency, toCurrency)
if err == nil {
return rate, nil
}
for _, provider := range s.providers {
providerRate, err := provider.GetRate(ctx, fromCurrency, toCurrency)
if err == nil {
newRate := &currency.ExchangeRate{
ID: fmt.Sprintf("%s_%s_%d", fromCurrency, toCurrency, time.Now().Unix()),
FromCurrency: fromCurrency,
ToCurrency: toCurrency,
Rate: providerRate,
Source: "provider",
ValidFrom: time.Now(),
IsActive: true,
}
if err := s.repository.CreateExchangeRate(ctx, newRate); err != nil {
s.logger.WithError(err).Error("Failed to save exchange rate")
}
return newRate, nil
}
}
return nil, fmt.Errorf("exchange rate not found: %s to %s", fromCurrency, toCurrency)
}
func (s *ExchangeRateService) ConvertAmount(ctx context.Context, amount float64, fromCurrency, toCurrency string) (*currency.CurrencyConversionResponse, error) {
rate, err := s.GetExchangeRate(ctx, fromCurrency, toCurrency)
if err != nil {
return nil, fmt.Errorf("failed to get exchange rate: %w", err)
}
convertedAmount := amount * rate.Rate
return &currency.CurrencyConversionResponse{
OriginalAmount: amount,
OriginalCurrency: fromCurrency,
ConvertedAmount: convertedAmount,
ConvertedCurrency: toCurrency,
ExchangeRate: rate.Rate,
ConvertedAt: time.Now(),
}, nil
}
func (s *ExchangeRateService) RefreshRates(ctx context.Context) error {
s.logger.Info("Refreshing exchange rates")
for _, provider := range s.providers {
if err := provider.RefreshRates(ctx); err != nil {
s.logger.WithError(err).Error("Failed to refresh rates from provider")
continue
}
}
s.logger.Info("Exchange rates refreshed successfully")
return nil
}
func (s *ExchangeRateService) GetRateHistory(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*currency.ExchangeRate, error) {
filter := &currency.ExchangeRateFilter{
FromCurrency: fromCurrency,
ToCurrency: toCurrency,
IsValid: &[]bool{false}[0], // Include historical rates
Limit: days,
SortBy: "valid_from",
SortOrder: "desc",
}
rates, err := s.repository.ListExchangeRates(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get rate history: %w", err)
}
return rates, nil
}
func (s *ExchangeRateService) UpdateExchangeRate(ctx context.Context, rate *currency.ExchangeRate) error {
if rate.Rate <= 0 {
return fmt.Errorf("invalid exchange rate: must be positive")
}
if rate.FromCurrency == rate.ToCurrency {
return fmt.Errorf("invalid currency pair: from and to currencies cannot be the same")
}
now := time.Now()
rate.ValidFrom = now
rate.IsActive = true
filter := &currency.ExchangeRateFilter{
FromCurrency: rate.FromCurrency,
ToCurrency: rate.ToCurrency,
IsValid: &[]bool{true}[0],
}
oldRates, err := s.repository.ListExchangeRates(ctx, filter)
if err == nil {
for _, oldRate := range oldRates {
oldRate.IsActive = false
if err := s.repository.UpdateExchangeRate(ctx, oldRate); err != nil {
s.logger.WithError(err).Error("Failed to deactivate old exchange rate")
}
}
}
rate.ID = fmt.Sprintf("%s_%s_%d", rate.FromCurrency, rate.ToCurrency, time.Now().Unix())
if err := s.repository.CreateExchangeRate(ctx, rate); err != nil {
return fmt.Errorf("failed to create exchange rate: %w", err)
}
s.logger.WithFields(logrus.Fields{
"from_currency": rate.FromCurrency,
"to_currency": rate.ToCurrency,
"rate": rate.Rate,
"source": rate.Source,
}).Info("Exchange rate updated")
return nil
}
func (s *ExchangeRateService) GetSupportedCurrencies(ctx context.Context) ([]*currency.Currency, error) {
filter := &currency.CurrencyFilter{
IsActive: &[]bool{true}[0],
}
currencies, err := s.repository.ListCurrencies(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get supported currencies: %w", err)
}
return currencies, nil
}
func (s *ExchangeRateService) ValidateCurrencyPair(ctx context.Context, fromCurrency, toCurrency string) error {
_, err := s.repository.GetCurrency(ctx, fromCurrency)
if err != nil {
return fmt.Errorf("unsupported from currency: %s", fromCurrency)
}
_, err = s.repository.GetCurrency(ctx, toCurrency)
if err != nil {
return fmt.Errorf("unsupported to currency: %s", toCurrency)
}
_, err = s.GetExchangeRate(ctx, fromCurrency, toCurrency)
if err != nil {
return fmt.Errorf("no exchange rate available: %s to %s", fromCurrency, toCurrency)
}
return nil
}
@@ -0,0 +1,173 @@
package services
import (
"context"
"fmt"
"time"
"github.com/sirupsen/logrus"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
)
// RatePlanCurrencyIntegrator integrates currency system with rate plans
type RatePlanCurrencyIntegrator struct {
billingService currency.BillingService
exchangeService *ExchangeRateService
ratePlanService rateplan.Service
logger *logrus.Logger
baseCurrency string
}
// NewRatePlanCurrencyIntegrator creates a new rate plan currency integrator
func NewRatePlanCurrencyIntegrator(
billingService currency.BillingService,
exchangeService *ExchangeRateService,
ratePlanService rateplan.Service,
logger *logrus.Logger,
baseCurrency string,
) *RatePlanCurrencyIntegrator {
return &RatePlanCurrencyIntegrator{
billingService: billingService,
exchangeService: exchangeService,
ratePlanService: ratePlanService,
logger: logger,
baseCurrency: baseCurrency,
}
}
// SubscribeToPlanWithCurrency subscribes to a rate plan with currency conversion
func (rpci *RatePlanCurrencyIntegrator) SubscribeToPlanWithCurrency(ctx context.Context, profileID string, planID string, currency string) (*rateplan.RatePlanSubscription, error) {
// Get the rate plan
plan, err := rpci.ratePlanService.GetRatePlan(ctx, planID)
if err != nil {
return nil, fmt.Errorf("failed to get rate plan: %w", err)
}
// Convert price to requested currency if needed
subscriptionPrice := plan.BasePrice
exchangeRate := 1.0
if currency != plan.Currency {
conversion, err := rpci.exchangeService.ConvertAmount(ctx, plan.BasePrice, plan.Currency, currency)
if err != nil {
rpci.logger.WithError(err).Error("Failed to convert rate plan price")
return nil, fmt.Errorf("currency conversion failed: %w", err)
}
subscriptionPrice = conversion.ConvertedAmount
exchangeRate = conversion.ExchangeRate
}
// Create subscription with currency information
subscription := &rateplan.RatePlanSubscription{
ProfileID: profileID,
RatePlanID: planID,
Status: rateplan.SubscriptionStatusActive,
StartedAt: time.Now(),
Metadata: map[string]interface{}{
"original_currency": plan.Currency,
"subscription_currency": currency,
"original_price": plan.BasePrice,
"subscription_price": subscriptionPrice,
"exchange_rate": exchangeRate,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Create subscription request
subscribeReq := &rateplan.SubscribeRequest{
ProfileID: profileID,
RatePlanID: planID,
AutoRenew: true,
Metadata: subscription.Metadata,
}
createdSubscription, err := rpci.ratePlanService.SubscribeToPlan(ctx, subscribeReq)
if err != nil {
return nil, fmt.Errorf("failed to create subscription: %w", err)
}
// Process initial billing
billingReq := &currency.BillingRequest{
ProfileID: profileID,
SubscriptionID: createdSubscription.ID,
Amount: subscriptionPrice,
Currency: currency,
Description: fmt.Sprintf("Initial subscription to %s", plan.Name),
BillingDate: time.Now(),
}
_, err = rpci.billingService.ProcessBilling(ctx, billingReq)
if err != nil {
rpci.logger.WithError(err).Error("Failed to process initial billing")
// Don't fail the subscription if billing fails, but log it
}
rpci.logger.WithFields(logrus.Fields{
"profile_id": profileID,
"plan_id": planID,
"currency": currency,
"subscription_id": createdSubscription.ID,
}).Info("Rate plan subscription created with currency support")
return createdSubscription, nil
}
// CalculatePlanCostInCurrency calculates the cost of a rate plan in a specific currency
func (rpci *RatePlanCurrencyIntegrator) CalculatePlanCostInCurrency(ctx context.Context, planID string, currency string, usageData *rateplan.RatePlanUsage) (*currency.BillingSummary, error) {
// Get the rate plan
plan, err := rpci.ratePlanService.GetRatePlan(ctx, planID)
if err != nil {
return nil, fmt.Errorf("failed to get rate plan: %w", err)
}
// Calculate base cost
baseCost := plan.BasePrice
// Add overage costs if usage data is provided
if usageData != nil {
overageCost, err := rpci.calculateOverageCost(ctx, plan, usageData)
if err != nil {
rpci.logger.WithError(err).Warn("Failed to calculate overage cost")
} else {
baseCost += overageCost
}
}
// Convert to requested currency
convertedCost := baseCost
exchangeRate := 1.0
if currency != plan.Currency {
conversion, err := rpci.exchangeService.ConvertAmount(ctx, baseCost, plan.Currency, currency)
if err != nil {
return nil, fmt.Errorf("currency conversion failed: %w", err)
}
convertedCost = conversion.ConvertedAmount
exchangeRate = conversion.ExchangeRate
}
// Create billing summary
summary := &currency.BillingSummary{
ProfileID: usageData.ProfileID,
TotalAmount: convertedCost,
Currency: currency,
BaseTotalAmount: baseCost,
BaseCurrency: plan.Currency,
TransactionCount: 1,
FromDate: time.Now().AddDate(0, -1, 0),
ToDate: time.Now(),
Breakdown: map[string]interface{}{
"plan_id": planID,
"plan_name": plan.Name,
"base_cost": plan.BasePrice,
"overage_cost": baseCost - plan.BasePrice,
"exchange_rate": exchangeRate,
"original_currency": plan.Currency,
},
}
return summary, nil
}
@@ -0,0 +1,162 @@
package services
import (
"context"
"fmt"
"time"
"github.com/sirupsen/logrus"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
)
// GetPlansInCurrency gets rate plans with prices converted to a specific currency
func (rpci *RatePlanCurrencyIntegrator) GetPlansInCurrency(ctx context.Context, filter *rateplan.RatePlanFilter, targetCurrency string) ([]*rateplan.RatePlan, error) {
// Get plans using original filter
plans, err := rpci.ratePlanService.ListRatePlans(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get rate plans: %w", err)
}
// Convert prices to target currency
for _, plan := range plans {
if plan.Currency != targetCurrency {
conversion, err := rpci.exchangeService.ConvertAmount(ctx, plan.BasePrice, plan.Currency, targetCurrency)
if err != nil {
rpci.logger.WithError(err).WithFields(logrus.Fields{
"plan_id": plan.ID,
"from_currency": plan.Currency,
"to_currency": targetCurrency,
}).Warn("Failed to convert plan price")
continue
}
// Store original price and update with converted price
if plan.Metadata == nil {
plan.Metadata = make(map[string]interface{})
}
plan.Metadata["original_price"] = plan.BasePrice
plan.Metadata["original_currency"] = plan.Currency
plan.Metadata["exchange_rate"] = conversion.ExchangeRate
plan.BasePrice = conversion.ConvertedAmount
plan.Currency = targetCurrency
}
}
return plans, nil
}
// UpdatePlanCurrency updates a rate plan's currency and converts prices
func (rpci *RatePlanCurrencyIntegrator) UpdatePlanCurrency(ctx context.Context, planID string, newCurrency string) error {
// Get the current plan
plan, err := rpci.ratePlanService.GetRatePlan(ctx, planID)
if err != nil {
return fmt.Errorf("failed to get rate plan: %w", err)
}
// If currency is the same, no conversion needed
if plan.Currency == newCurrency {
return nil
}
// Convert all monetary values to new currency
convertedPrice, err := rpci.exchangeService.ConvertAmount(ctx, plan.BasePrice, plan.Currency, newCurrency)
if err != nil {
return fmt.Errorf("failed to convert base price: %w", err)
}
// Update plan with new currency and converted prices
plan.BasePrice = convertedPrice.ConvertedAmount
plan.Currency = newCurrency
// Store conversion information in metadata
if plan.Metadata == nil {
plan.Metadata = make(map[string]interface{})
}
plan.Metadata["currency_conversion"] = map[string]interface{}{
"from_currency": plan.Metadata["original_currency"],
"to_currency": newCurrency,
"exchange_rate": convertedPrice.ExchangeRate,
"converted_at": time.Now(),
}
// Update the plan
updatedPlan, err := rpci.ratePlanService.UpdateRatePlan(ctx, plan)
if err != nil {
return fmt.Errorf("failed to update rate plan: %w", err)
}
// Use the updated plan for logging
plan = updatedPlan
rpci.logger.WithFields(logrus.Fields{
"plan_id": planID,
"from_currency": plan.Metadata["original_currency"],
"to_currency": newCurrency,
"exchange_rate": convertedPrice.ExchangeRate,
}).Info("Rate plan currency updated")
return nil
}
// calculateOverageCost calculates overage costs for usage
func (rpci *RatePlanCurrencyIntegrator) calculateOverageCost(ctx context.Context, plan *rateplan.RatePlan, usage *rateplan.RatePlanUsage) (float64, error) {
overageCost := 0.0
// Calculate data overage
if plan.DataAllowance != nil && usage.DataUsed > plan.DataAllowance.Amount {
dataOverage := usage.DataUsed - plan.DataAllowance.Amount
if plan.OverageRates != nil {
overageCost += float64(dataOverage) * plan.OverageRates.DataRate
}
}
// Calculate voice overage
if plan.VoiceAllowance != nil && usage.VoiceUsed > plan.VoiceAllowance.Minutes {
voiceOverage := usage.VoiceUsed - plan.VoiceAllowance.Minutes
if plan.OverageRates != nil {
overageCost += float64(voiceOverage) * plan.OverageRates.VoiceRate
}
}
// Calculate SMS overage
if plan.SMSAllowance != nil && usage.SMSUsed > plan.SMSAllowance.Messages {
smsOverage := usage.SMSUsed - plan.SMSAllowance.Messages
if plan.OverageRates != nil {
overageCost += float64(smsOverage) * plan.OverageRates.SMSRate
}
}
return overageCost, nil
}
// GetCurrencyUsageForPlan gets currency usage statistics for a specific rate plan
func (rpci *RatePlanCurrencyIntegrator) GetCurrencyUsageForPlan(ctx context.Context, planID string) (map[string]int64, error) {
// Get all subscriptions for this plan
filter := &rateplan.SubscriptionFilter{
RatePlanID: planID,
Status: rateplan.SubscriptionStatusActive,
}
subscriptions, err := rpci.ratePlanService.ListSubscriptions(ctx, "", filter)
if err != nil {
return nil, fmt.Errorf("failed to get subscriptions: %w", err)
}
// Count currencies
currencyUsage := make(map[string]int64)
for _, subscription := range subscriptions {
currency := rpci.baseCurrency // Default to base currency
if subscription.Metadata != nil {
if subCurrency, exists := subscription.Metadata["subscription_currency"]; exists {
if subCurrencyStr, ok := subCurrency.(string); ok {
currency = subCurrencyStr
}
}
}
currencyUsage[currency]++
}
return currencyUsage, nil
}
@@ -0,0 +1,218 @@
package services
import (
"context"
"fmt"
"time"
)
// GetCarrierAnalytics returns comprehensive carrier analytics
func (s *SelectionService) GetCarrierAnalytics(ctx context.Context) (*CarrierAnalyticsResponse, error) {
s.logger.Info("Generating carrier analytics")
// Get carrier status
carriers := s.manager.GetCarrierStatus()
analytics := &CarrierAnalyticsResponse{
GeneratedAt: time.Now(),
TotalCarriers: len(carriers),
HealthyCarriers: 0,
Analytics: make(map[string]*CarrierAnalytics),
Summary: &AnalyticsSummary{},
}
var totalScore float64
var totalRequests uint64
var totalSuccess uint64
for carrierID, carrier := range carriers {
carrierAnalytics := &CarrierAnalytics{
CarrierID: carrierID,
CarrierName: carrier.Name,
Region: carrier.CountryCode,
Status: string(carrier.HealthStatus),
LastCheck: carrier.LastHealthCheck,
}
// Get selection history
history := s.manager.GetSelectionHistory(carrierID)
carrierAnalytics.SelectionCount = len(history)
if len(history) > 0 {
var scoreSum float64
for _, score := range history {
scoreSum += score.TotalScore
}
carrierAnalytics.AverageScore = scoreSum / float64(len(history))
carrierAnalytics.LastSelected = history[len(history)-1].SelectedAt
}
// Calculate performance metrics
if carrier.Metrics.TotalRequests > 0 {
carrierAnalytics.SuccessRate = float64(carrier.Metrics.SuccessfulRequests) / float64(carrier.Metrics.TotalRequests) * 100
carrierAnalytics.AverageResponseTime = carrier.Metrics.AverageResponseTime.Milliseconds()
carrierAnalytics.RequestRate = carrier.Metrics.RequestRate
}
// Update totals
totalScore += carrierAnalytics.AverageScore
totalRequests += carrier.Metrics.TotalRequests
totalSuccess += carrier.Metrics.SuccessfulRequests
// Count healthy carriers
if carrier.HealthStatus == "healthy" {
analytics.HealthyCarriers++
}
// Generate carrier-specific recommendations
carrierAnalytics.Recommendations = s.generateCarrierRecommendations(carrierAnalytics)
analytics.Analytics[carrierID] = carrierAnalytics
}
// Calculate summary metrics
if len(carriers) > 0 {
analytics.Summary.AverageScore = totalScore / float64(len(carriers))
}
if totalRequests > 0 {
analytics.Summary.OverallSuccessRate = float64(totalSuccess) / float64(totalRequests) * 100
}
analytics.Summary.TotalRequests = totalRequests
analytics.Summary.OverallHealth = float64(analytics.HealthyCarriers) / float64(len(carriers)) * 100
return analytics, nil
}
// CarrierAnalyticsResponse represents the response for carrier analytics
type CarrierAnalyticsResponse struct {
GeneratedAt time.Time `json:"generated_at"`
TotalCarriers int `json:"total_carriers"`
HealthyCarriers int `json:"healthy_carriers"`
Analytics map[string]*CarrierAnalytics `json:"analytics"`
Summary *AnalyticsSummary `json:"summary"`
}
// CarrierAnalytics represents analytics for a single carrier
type CarrierAnalytics struct {
CarrierID string `json:"carrier_id"`
CarrierName string `json:"carrier_name"`
Region string `json:"region"`
Status string `json:"status"`
SelectionCount int `json:"selection_count"`
AverageScore float64 `json:"average_score"`
SuccessRate float64 `json:"success_rate"`
AverageResponseTime int64 `json:"average_response_time_ms"`
RequestRate float64 `json:"request_rate"`
LastSelected time.Time `json:"last_selected"`
LastCheck time.Time `json:"last_check"`
Recommendations []string `json:"recommendations"`
}
// AnalyticsSummary represents overall analytics summary
type AnalyticsSummary struct {
AverageScore float64 `json:"average_score"`
OverallSuccessRate float64 `json:"overall_success_rate"`
TotalRequests uint64 `json:"total_requests"`
OverallHealth float64 `json:"overall_health"`
}
// generateCarrierRecommendations generates recommendations for a specific carrier
func (s *SelectionService) generateCarrierRecommendations(analytics *CarrierAnalytics) []string {
recommendations := []string{}
// Performance recommendations
if analytics.SuccessRate < 95 {
recommendations = append(recommendations, "Monitor success rate - below optimal threshold")
}
if analytics.AverageResponseTime > 500 {
recommendations = append(recommendations, "Consider optimizing response time")
}
if analytics.RequestRate > 100 {
recommendations = append(recommendations, "High request rate - monitor for overload")
}
// Selection recommendations
if analytics.SelectionCount == 0 {
recommendations = append(recommendations, "Carrier never selected - investigate configuration")
} else if analytics.AverageScore < 70 {
recommendations = append(recommendations, "Low selection scores - review carrier performance")
}
// Health recommendations
if analytics.Status != "healthy" {
recommendations = append(recommendations, "Carrier health issues - immediate attention required")
}
if len(recommendations) == 0 {
recommendations = append(recommendations, "Carrier performing well")
}
return recommendations
}
// OptimizeCarrierSelection provides optimization recommendations
func (s *SelectionService) OptimizeCarrierSelection(ctx context.Context) (*OptimizationResponse, error) {
s.logger.Info("Analyzing carrier selection optimization opportunities")
analytics, err := s.GetCarrierAnalytics(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get analytics: %w", err)
}
optimization := &OptimizationResponse{
GeneratedAt: time.Now(),
OverallHealth: analytics.Summary.OverallHealth,
Recommendations: []string{},
PriorityActions: []string{},
LongTermActions: []string{},
}
// Generate recommendations based on analytics
if analytics.Summary.OverallHealth < 80 {
optimization.Recommendations = append(optimization.Recommendations, "System health below optimal - investigate carrier issues")
optimization.PriorityActions = append(optimization.PriorityActions, "Review unhealthy carriers and implement failover")
}
if analytics.Summary.OverallSuccessRate < 95 {
optimization.Recommendations = append(optimization.Recommendations, "Success rate below target - optimize carrier selection criteria")
optimization.PriorityActions = append(optimization.PriorityActions, "Adjust performance weights in selection algorithm")
}
// Analyze individual carriers
for _, carrier := range analytics.Analytics {
if carrier.SuccessRate < 90 {
optimization.PriorityActions = append(optimization.PriorityActions,
fmt.Sprintf("Investigate %s (%s) - success rate %.1f%%",
carrier.CarrierName, carrier.CarrierID, carrier.SuccessRate))
}
if carrier.AverageResponseTime > 1000 {
optimization.PriorityActions = append(optimization.PriorityActions,
fmt.Sprintf("Optimize %s (%s) - response time %dms",
carrier.CarrierName, carrier.CarrierID, carrier.AverageResponseTime))
}
}
// Long-term recommendations
optimization.LongTermActions = append(optimization.LongTermActions,
"Implement machine learning for carrier selection")
optimization.LongTermActions = append(optimization.LongTermActions,
"Add predictive analytics for carrier performance")
optimization.LongTermActions = append(optimization.LongTermActions,
"Expand carrier portfolio for better redundancy")
if len(optimization.Recommendations) == 0 {
optimization.Recommendations = append(optimization.Recommendations, "System operating optimally")
}
return optimization, nil
}
// OptimizationResponse represents optimization recommendations
type OptimizationResponse struct {
GeneratedAt time.Time `json:"generated_at"`
OverallHealth float64 `json:"overall_health"`
Recommendations []string `json:"recommendations"`
PriorityActions []string `json:"priority_actions"`
LongTermActions []string `json:"long_term_actions"`
}
@@ -0,0 +1,169 @@
package services
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handlers"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/sirupsen/logrus"
)
// SelectionService provides high-level carrier selection operations
type SelectionService struct {
manager *smdp.SMDPManager
handler *handlers.SelectionHandler
logger *logrus.Logger
}
// NewSelectionService creates a new selection service
func NewSelectionService(manager *smdp.SMDPManager) *SelectionService {
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
return &SelectionService{
manager: manager,
handler: handlers.NewSelectionHandler(manager),
logger: logger,
}
}
// GetHandler returns the selection handler for API registration
func (s *SelectionService) GetHandler() *handlers.SelectionHandler {
return s.handler
}
// IntelligentCarrierSelection performs intelligent carrier selection with comprehensive criteria
func (s *SelectionService) IntelligentCarrierSelection(ctx context.Context, request *IntelligentSelectionRequest) (*IntelligentSelectionResponse, error) {
s.logger.WithFields(logrus.Fields{
"region": request.Region,
"profile_type": request.ProfileType,
"urgency": request.Urgency,
"cost_sensitivity": request.CostSensitivity,
}).Info("Performing intelligent carrier selection")
criteria := &smdp.SelectionCriteria{
Region: request.Region,
ProfileType: request.ProfileType,
Urgency: request.Urgency,
CostSensitivity: request.CostSensitivity,
PerformanceWeight: request.PerformanceWeight,
ReliabilityWeight: request.ReliabilityWeight,
}
// Select optimal carrier
score, err := s.manager.SelectOptimalCarrier(ctx, criteria)
if err != nil {
return nil, fmt.Errorf("failed to select optimal carrier: %w", err)
}
// Record the selection for analytics
s.recordSelection(score)
// Generate recommendations
recommendations := s.generateRecommendations(score, request)
response := &IntelligentSelectionResponse{
Success: true,
SelectedCarrier: score.Carrier,
SelectionScore: score,
Recommendations: recommendations,
SelectionTime: time.Now(),
}
s.logger.WithFields(logrus.Fields{
"selected_carrier": score.CarrierID,
"total_score": score.TotalScore,
"reason": score.Reason,
}).Info("Intelligent carrier selection completed")
return response, nil
}
// IntelligentSelectionRequest represents a comprehensive carrier selection request
type IntelligentSelectionRequest struct {
Region string `json:"region"`
ProfileType string `json:"profile_type"`
Urgency string `json:"urgency"`
CostSensitivity float64 `json:"cost_sensitivity"`
PerformanceWeight float64 `json:"performance_weight"`
ReliabilityWeight float64 `json:"reliability_weight"`
UserPreferences *UserPreferences `json:"user_preferences,omitempty"`
BusinessContext *BusinessContext `json:"business_context,omitempty"`
}
// UserPreferences represents user-specific preferences
type UserPreferences struct {
PreferredCarriers []string `json:"preferred_carriers"`
ExcludedCarriers []string `json:"excluded_carriers"`
MaxResponseTime int `json:"max_response_time_ms"`
MinSuccessRate float64 `json:"min_success_rate"`
}
// BusinessContext represents business-specific context
type BusinessContext struct {
CustomerTier string `json:"customer_tier"`
ServiceLevel string `json:"service_level"`
BillingModel string `json:"billing_model"`
GeographicScope string `json:"geographic_scope"`
}
// IntelligentSelectionResponse represents the response from intelligent carrier selection
type IntelligentSelectionResponse struct {
Success bool `json:"success"`
SelectedCarrier *smdp.Carrier `json:"selected_carrier"`
SelectionScore *smdp.CarrierScore `json:"selection_score"`
Recommendations []string `json:"recommendations"`
SelectionTime time.Time `json:"selection_time"`
Alternatives []*smdp.CarrierScore `json:"alternatives,omitempty"`
}
// recordSelection records a carrier selection for analytics
func (s *SelectionService) recordSelection(score *smdp.CarrierScore) {
// This would typically be stored in a database for analytics
s.logger.WithFields(logrus.Fields{
"carrier_id": score.CarrierID,
"score": score.TotalScore,
"reason": score.Reason,
"timestamp": score.SelectedAt,
}).Info("Carrier selection recorded")
}
// generateRecommendations generates recommendations based on selection results
func (s *SelectionService) generateRecommendations(score *smdp.CarrierScore, request *IntelligentSelectionRequest) []string {
recommendations := []string{}
// Performance-based recommendations
if score.PerformanceScore < 70 {
recommendations = append(recommendations, "Consider monitoring carrier performance closely")
} else if score.PerformanceScore > 90 {
recommendations = append(recommendations, "Excellent performance - consider prioritizing this carrier")
}
// Cost-based recommendations
if score.CostScore < 50 && request.CostSensitivity > 0.7 {
recommendations = append(recommendations, "Higher cost detected - consider cost optimization strategies")
}
// Reliability-based recommendations
if score.ReliabilityScore < 60 {
recommendations = append(recommendations, "Reliability concerns - ensure failover mechanisms are in place")
}
// Regional recommendations
if score.RegionScore < 50 {
recommendations = append(recommendations, "Regional compatibility issues - consider regional carrier partnerships")
}
// Urgency-based recommendations
if request.Urgency == "high" && score.TotalScore < 80 {
recommendations = append(recommendations, "High urgency request - consider manual carrier override if needed")
}
if len(recommendations) == 0 {
recommendations = append(recommendations, "Carrier selection appears optimal")
}
return recommendations
}
@@ -0,0 +1,198 @@
package services
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 services
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 services
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 services
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,162 @@
package services
import (
"context"
"fmt"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
"github.com/sirupsen/logrus"
)
// SMDPService provides high-level SM-DP+ operations with multi-carrier support
type SMDPService struct {
manager *smdp.SMDPManager
repository *repository.PostgresProfileStore
logger *logrus.Logger
}
// NewSMDPService creates a new SM-DP+ service
func NewSMDPService(repo *repository.PostgresProfileStore) *SMDPService {
config := smdp.DefaultManagerConfig()
manager := smdp.NewSMDPManager(repo, config)
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
// Start health checking
ctx := context.Background()
go manager.StartHealthChecking(ctx)
return &SMDPService{
manager: manager,
repository: repo,
logger: logger,
}
}
// InitializeDefaultCarriers adds common carriers to the manager
func (s *SMDPService) InitializeDefaultCarriers() error {
defaultCarriers := []*smdp.CarrierConfig{
{
ID: "carrier-att-us",
Name: "AT&T US",
CountryCode: "US",
MCC: "310",
MNC: "410",
ES2BaseURL: "https://smdp.att.com",
ES2APIKey: "",
ES2InsecureSkip: false,
Priority: 100,
IsActive: true,
MaxConcurrentReqs: 100,
SupportedProfileTypes: []string{"operational", "test"},
SupportedMCCs: []string{"310", "311", "312", "313", "314", "315", "316"},
Features: []string{"bulk_download", "remote_provisioning"},
},
{
ID: "carrier-verizon-us",
Name: "Verizon US",
CountryCode: "US",
MCC: "311",
MNC: "480",
ES2BaseURL: "https://smdp.verizon.com",
ES2APIKey: "",
ES2InsecureSkip: false,
Priority: 90,
IsActive: true,
MaxConcurrentReqs: 80,
SupportedProfileTypes: []string{"operational", "test"},
SupportedMCCs: []string{"311", "312"},
Features: []string{"bulk_download"},
},
{
ID: "carrier-tmobile-de",
Name: "T-Mobile DE",
CountryCode: "DE",
MCC: "262",
MNC: "01",
ES2BaseURL: "https://smdp.t-mobile.de",
ES2APIKey: "",
ES2InsecureSkip: false,
Priority: 85,
IsActive: true,
MaxConcurrentReqs: 60,
SupportedProfileTypes: []string{"operational"},
SupportedMCCs: []string{"262"},
Features: []string{"bulk_download", "remote_provisioning"},
},
}
for _, carrierConfig := range defaultCarriers {
carrier := carrierConfig.ToCarrier()
if err := s.manager.AddCarrier(carrier); err != nil {
s.logger.WithError(err).WithField("carrier_id", carrier.ID).
Warn("Failed to add default carrier")
continue
}
s.logger.WithField("carrier_id", carrier.ID).Info("Added default carrier")
}
return nil
}
// DownloadProfile handles profile download with intelligent carrier selection
func (s *SMDPService) DownloadProfile(ctx context.Context, req *smdp.ProfileRequest) (*smdp.ProfileResponse, error) {
s.logger.WithFields(logrus.Fields{
"eid": req.EID,
"iccid": req.ICCID,
"preferred": req.PreferredCarrier,
}).Info("Processing profile download through SM-DP+ service")
return s.manager.DownloadProfile(ctx, req)
}
// GetOptimalCarrier selects the best carrier for a given request
func (s *SMDPService) GetOptimalCarrier(ctx context.Context, req *smdp.ProfileRequest) (*smdp.Carrier, error) {
return s.manager.SelectCarrier(ctx)
}
// GetCarrierHealth returns health status of all carriers
func (s *SMDPService) GetCarrierHealth() map[string]*smdp.Carrier {
return s.manager.GetCarrierStatus()
}
// AddCarrier dynamically adds a new carrier
func (s *SMDPService) AddCarrier(carrierConfig *smdp.CarrierConfig) error {
carrier := carrierConfig.ToCarrier()
if err := s.manager.AddCarrier(carrier); err != nil {
return fmt.Errorf("failed to add carrier %s: %w", carrier.ID, err)
}
s.logger.WithField("carrier_id", carrier.ID).Info("Successfully added new carrier")
return nil
}
// RemoveCarrier removes a carrier from the system
func (s *SMDPService) RemoveCarrier(carrierID string) error {
if err := s.manager.RemoveCarrier(carrierID); err != nil {
return fmt.Errorf("failed to remove carrier %s: %w", carrierID, err)
}
s.logger.WithField("carrier_id", carrierID).Info("Successfully removed carrier")
return nil
}
// GetManager returns the underlying SMDPManager
func (s *SMDPService) GetManager() *smdp.SMDPManager {
return s.manager
}
// GetCarrierMetrics returns performance metrics for all carriers
func (s *SMDPService) GetCarrierMetrics() map[string]*smdp.CarrierMetrics {
carriers := s.manager.GetCarrierStatus()
metrics := make(map[string]*smdp.CarrierMetrics)
for id, carrier := range carriers {
metrics[id] = carrier.Metrics
}
return metrics
}