mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-13 23:45:13 +00:00
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:
@@ -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 = ¤cy.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 = ¤cy.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 := ¤cy.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, ¤cy.CurrencyFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count currencies: %w", err)
|
||||
}
|
||||
|
||||
// Get active currencies
|
||||
activeCurrencies, err := s.repository.CountCurrencies(ctx, ¤cy.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, ¤cy.TransactionFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count transactions: %w", err)
|
||||
}
|
||||
|
||||
// Get total volume
|
||||
transactions, err := s.repository.ListTransactions(ctx, ¤cy.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, ¤cy.ExchangeRateFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count exchange rates: %w", err)
|
||||
}
|
||||
|
||||
return ¤cy.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 := ¤cy.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, ¤cy.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, ¤cy.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 = ¤cy.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] = ¤cy.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 ¤cy.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 := ¤cy.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 ¤cy.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 := ¤cy.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 := ¤cy.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 := ¤cy.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 := ¤cy.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 := ¤cy.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
|
||||
}
|
||||
Reference in New Issue
Block a user