diff --git a/apps/carrier-connector/internal/services/analytics_service.go b/apps/carrier-connector/internal/services/analytics_service.go new file mode 100644 index 0000000..6535854 --- /dev/null +++ b/apps/carrier-connector/internal/services/analytics_service.go @@ -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 currCode, revenue := range revenueByCurrency { + currencyRevenues = append(currencyRevenues, ¤cy.CurrencyRevenue{ + Currency: currCode, + 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 +} diff --git a/apps/carrier-connector/internal/services/billing_core.go b/apps/carrier-connector/internal/services/billing_core.go new file mode 100644 index 0000000..311b747 --- /dev/null +++ b/apps/carrier-connector/internal/services/billing_core.go @@ -0,0 +1,149 @@ +package services + +import ( + "context" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency" + "github.com/sirupsen/logrus" +) + +// BillingServiceImpl handles multi-currency billing operations +type BillingServiceImpl struct { + repository currency.Repository + exchangeService currency.ExchangeRateService + logger *logrus.Logger + baseCurrency string +} + +// NewBillingService creates a new billing service +func NewBillingService(repository currency.Repository, exchangeService currency.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 *currency.BillingRequest) (*currency.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 := ¤cy.Transaction{ + ID: uuid.New().String(), + ProfileID: req.ProfileID, + SubscriptionID: req.SubscriptionID, + Type: currency.TransactionTypeSubscription, + Amount: req.Amount, + Currency: req.Currency, + BaseAmount: baseAmount, + BaseCurrency: s.baseCurrency, + ExchangeRate: exchangeRate, + Description: req.Description, + Status: currency.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 = currency.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 ¤cy.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 *currency.CurrencyConversionRequest) (*currency.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 *currency.TransactionFilter) ([]*currency.Transaction, error) { + if filter == nil { + filter = ¤cy.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 *currency.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 +} diff --git a/apps/carrier-connector/internal/services/exchange_rate_service.go b/apps/carrier-connector/internal/services/exchange_rate_service.go new file mode 100644 index 0000000..e521725 --- /dev/null +++ b/apps/carrier-connector/internal/services/exchange_rate_service.go @@ -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 +} diff --git a/apps/carrier-connector/internal/services/rateplan_core.go b/apps/carrier-connector/internal/services/rateplan_core.go new file mode 100644 index 0000000..a98d2f4 --- /dev/null +++ b/apps/carrier-connector/internal/services/rateplan_core.go @@ -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, targetCurrency 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 targetCurrency != plan.Currency { + conversion, err := rpci.exchangeService.ConvertAmount(ctx, plan.BasePrice, plan.Currency, targetCurrency) + 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": targetCurrency, + "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: targetCurrency, + 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": targetCurrency, + "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, targetCurrency 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 targetCurrency != plan.Currency { + conversion, err := rpci.exchangeService.ConvertAmount(ctx, baseCost, plan.Currency, targetCurrency) + 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: targetCurrency, + 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 +} diff --git a/apps/carrier-connector/internal/services/rateplan_methods.go b/apps/carrier-connector/internal/services/rateplan_methods.go new file mode 100644 index 0000000..c05dc9f --- /dev/null +++ b/apps/carrier-connector/internal/services/rateplan_methods.go @@ -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 +} diff --git a/apps/carrier-connector/internal/services/service_methods.go b/apps/carrier-connector/internal/services/service_methods.go index 9fb58f5..bac5bdc 100644 --- a/apps/carrier-connector/internal/services/service_methods.go +++ b/apps/carrier-connector/internal/services/service_methods.go @@ -3,8 +3,7 @@ package services import ( "fmt" "time" - - "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository" + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository" ) func (s *Service) validateRatePlan(plan *repository.RatePlan) error {