feat: Add currency analytics, billing, and exchange rate services with multi-currency support and transaction processing

- Add AnalyticsServiceImpl with revenue/volume analysis, exchange rate trends, usage stats, and transaction type analytics
- Add GetRevenueByCurrency, GetTransactionVolumeByCurrency, GetExchangeRateTrends, GetCurrencyUsageStats methods
- Add GetMonthlyRevenueTrends, GetTopCurrenciesByRevenue, GetTransactionTypeAnalytics for reporting
- Add BillingServiceImpl with multi-currency billing, payment
This commit is contained in:
nutcas3
2026-05-02 02:50:41 +03:00
parent d3d43e5f35
commit bf5ee28917
6 changed files with 926 additions and 2 deletions
@@ -0,0 +1,244 @@
package services
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
"github.com/sirupsen/logrus"
)
// AnalyticsServiceImpl handles currency analytics operations
type AnalyticsServiceImpl struct {
repository currency.Repository
logger *logrus.Logger
}
// NewAnalyticsService creates a new analytics service
func NewAnalyticsService(repository currency.Repository, logger *logrus.Logger) *AnalyticsServiceImpl {
return &AnalyticsServiceImpl{
repository: repository,
logger: logger,
}
}
// GetRevenueByCurrency calculates revenue breakdown by currency
func (s *AnalyticsServiceImpl) GetRevenueByCurrency(ctx context.Context, filter *currency.TransactionFilter) (map[string]float64, error) {
if filter == nil {
filter = &currency.TransactionFilter{}
}
filter.Status = currency.TransactionStatusCompleted
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get transactions for revenue analysis")
return nil, fmt.Errorf("failed to get transactions: %w", err)
}
revenueByCurrency := make(map[string]float64)
for _, tx := range transactions {
if tx.Type == currency.TransactionTypeSubscription || tx.Type == currency.TransactionTypeUsage || tx.Type == currency.TransactionTypeOverage {
revenueByCurrency[tx.Currency] += tx.Amount
}
}
return revenueByCurrency, nil
}
// GetTransactionVolumeByCurrency calculates transaction volume by currency
func (s *AnalyticsServiceImpl) GetTransactionVolumeByCurrency(ctx context.Context, filter *currency.TransactionFilter) (map[string]int64, error) {
if filter == nil {
filter = &currency.TransactionFilter{}
}
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get transactions for volume analysis")
return nil, fmt.Errorf("failed to get transactions: %w", err)
}
volumeByCurrency := make(map[string]int64)
for _, tx := range transactions {
volumeByCurrency[tx.Currency]++
}
return volumeByCurrency, nil
}
// GetExchangeRateTrends retrieves exchange rate trends for a currency pair
func (s *AnalyticsServiceImpl) GetExchangeRateTrends(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*currency.ExchangeRate, error) {
filter := &currency.ExchangeRateFilter{
FromCurrency: fromCurrency,
ToCurrency: toCurrency,
IsValid: &[]bool{false}[0], // Include historical rates
Limit: days,
SortBy: "valid_from",
SortOrder: "desc",
}
rates, err := s.repository.ListExchangeRates(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get exchange rate trends")
return nil, fmt.Errorf("failed to get exchange rate trends: %w", err)
}
return rates, nil
}
// GetCurrencyUsageStats retrieves currency usage statistics
func (s *AnalyticsServiceImpl) GetCurrencyUsageStats(ctx context.Context) (*currency.CurrencyUsageStats, error) {
// Get total currencies
totalCurrencies, err := s.repository.CountCurrencies(ctx, &currency.CurrencyFilter{})
if err != nil {
return nil, fmt.Errorf("failed to count currencies: %w", err)
}
// Get active currencies
activeCurrencies, err := s.repository.CountCurrencies(ctx, &currency.CurrencyFilter{
IsActive: &[]bool{true}[0],
})
if err != nil {
return nil, fmt.Errorf("failed to count active currencies: %w", err)
}
// Get total transactions
totalTransactions, err := s.repository.CountTransactions(ctx, &currency.TransactionFilter{})
if err != nil {
return nil, fmt.Errorf("failed to count transactions: %w", err)
}
// Get total volume
transactions, err := s.repository.ListTransactions(ctx, &currency.TransactionFilter{
Status: currency.TransactionStatusCompleted,
})
if err != nil {
return nil, fmt.Errorf("failed to get transactions for volume: %w", err)
}
totalVolume := 0.0
currencyDistribution := make(map[string]int64)
for _, tx := range transactions {
totalVolume += tx.BaseAmount
currencyDistribution[tx.Currency]++
}
// Find most used currency
mostUsedCurrency := ""
maxCount := int64(0)
for currency, count := range currencyDistribution {
if count > maxCount {
maxCount = count
mostUsedCurrency = currency
}
}
// Get exchange rate count (using ListExchangeRates for now since CountExchangeRates doesn't exist)
exchangeRates, err := s.repository.ListExchangeRates(ctx, &currency.ExchangeRateFilter{})
if err != nil {
return nil, fmt.Errorf("failed to count exchange rates: %w", err)
}
return &currency.CurrencyUsageStats{
TotalCurrencies: totalCurrencies,
ActiveCurrencies: activeCurrencies,
TotalTransactions: int64(totalTransactions),
TotalVolume: totalVolume,
MostUsedCurrency: mostUsedCurrency,
CurrencyDistribution: currencyDistribution,
ExchangeRateCount: len(exchangeRates),
LastUpdated: time.Now(),
}, nil
}
// GetMonthlyRevenueTrends calculates monthly revenue trends
func (s *AnalyticsServiceImpl) GetMonthlyRevenueTrends(ctx context.Context, months int) (map[string]float64, error) {
endDate := time.Now()
startDate := endDate.AddDate(0, -months, 0)
filter := &currency.TransactionFilter{
Status: currency.TransactionStatusCompleted,
FromDate: &startDate,
ToDate: &endDate,
}
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get transactions for monthly trends")
return nil, fmt.Errorf("failed to get transactions: %w", err)
}
monthlyRevenue := make(map[string]float64)
for _, tx := range transactions {
monthKey := tx.CreatedAt.Format("2006-01")
monthlyRevenue[monthKey] += tx.BaseAmount
}
return monthlyRevenue, nil
}
// GetTopCurrenciesByRevenue returns top currencies by revenue
func (s *AnalyticsServiceImpl) GetTopCurrenciesByRevenue(ctx context.Context, limit int) ([]*currency.CurrencyRevenue, error) {
revenueByCurrency, err := s.GetRevenueByCurrency(ctx, &currency.TransactionFilter{
Status: currency.TransactionStatusCompleted,
})
if err != nil {
return nil, err
}
// Convert to slice and sort
var currencyRevenues []*currency.CurrencyRevenue
for currCode, revenue := range revenueByCurrency {
currencyRevenues = append(currencyRevenues, &currency.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 = &currency.TransactionFilter{}
}
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get transactions for type analytics")
return nil, fmt.Errorf("failed to get transactions: %w", err)
}
typeStats := make(map[string]*currency.TransactionTypeStats)
for _, tx := range transactions {
typeKey := string(tx.Type)
if _, exists := typeStats[typeKey]; !exists {
typeStats[typeKey] = &currency.TransactionTypeStats{
Type: tx.Type,
Count: 0,
Amount: 0.0,
Currency: tx.Currency,
}
}
stats := typeStats[typeKey]
stats.Count++
stats.Amount += tx.BaseAmount
}
return typeStats, nil
}
@@ -0,0 +1,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 := &currency.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 &currency.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 = &currency.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
}
@@ -0,0 +1,197 @@
package services
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
"github.com/sirupsen/logrus"
)
type ExchangeRateService struct {
repository currency.Repository
logger *logrus.Logger
providers []currency.ExchangeRateProvider
baseCurrency string
}
func NewExchangeRateService(repository currency.Repository, logger *logrus.Logger, baseCurrency string) *ExchangeRateService {
return &ExchangeRateService{
repository: repository,
logger: logger,
providers: make([]currency.ExchangeRateProvider, 0),
baseCurrency: baseCurrency,
}
}
func (s *ExchangeRateService) AddProvider(provider currency.ExchangeRateProvider) {
s.providers = append(s.providers, provider)
}
func (s *ExchangeRateService) GetExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*currency.ExchangeRate, error) {
if fromCurrency == toCurrency {
return &currency.ExchangeRate{
FromCurrency: fromCurrency,
ToCurrency: toCurrency,
Rate: 1.0,
Source: "direct",
ValidFrom: time.Now(),
IsActive: true,
}, nil
}
rate, err := s.repository.GetLatestExchangeRate(ctx, fromCurrency, toCurrency)
if err == nil {
return rate, nil
}
for _, provider := range s.providers {
providerRate, err := provider.GetRate(ctx, fromCurrency, toCurrency)
if err == nil {
newRate := &currency.ExchangeRate{
ID: fmt.Sprintf("%s_%s_%d", fromCurrency, toCurrency, time.Now().Unix()),
FromCurrency: fromCurrency,
ToCurrency: toCurrency,
Rate: providerRate,
Source: "provider",
ValidFrom: time.Now(),
IsActive: true,
}
if err := s.repository.CreateExchangeRate(ctx, newRate); err != nil {
s.logger.WithError(err).Error("Failed to save exchange rate")
}
return newRate, nil
}
}
return nil, fmt.Errorf("exchange rate not found: %s to %s", fromCurrency, toCurrency)
}
func (s *ExchangeRateService) ConvertAmount(ctx context.Context, amount float64, fromCurrency, toCurrency string) (*currency.CurrencyConversionResponse, error) {
rate, err := s.GetExchangeRate(ctx, fromCurrency, toCurrency)
if err != nil {
return nil, fmt.Errorf("failed to get exchange rate: %w", err)
}
convertedAmount := amount * rate.Rate
return &currency.CurrencyConversionResponse{
OriginalAmount: amount,
OriginalCurrency: fromCurrency,
ConvertedAmount: convertedAmount,
ConvertedCurrency: toCurrency,
ExchangeRate: rate.Rate,
ConvertedAt: time.Now(),
}, nil
}
func (s *ExchangeRateService) RefreshRates(ctx context.Context) error {
s.logger.Info("Refreshing exchange rates")
for _, provider := range s.providers {
if err := provider.RefreshRates(ctx); err != nil {
s.logger.WithError(err).Error("Failed to refresh rates from provider")
continue
}
}
s.logger.Info("Exchange rates refreshed successfully")
return nil
}
func (s *ExchangeRateService) GetRateHistory(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*currency.ExchangeRate, error) {
filter := &currency.ExchangeRateFilter{
FromCurrency: fromCurrency,
ToCurrency: toCurrency,
IsValid: &[]bool{false}[0], // Include historical rates
Limit: days,
SortBy: "valid_from",
SortOrder: "desc",
}
rates, err := s.repository.ListExchangeRates(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get rate history: %w", err)
}
return rates, nil
}
func (s *ExchangeRateService) UpdateExchangeRate(ctx context.Context, rate *currency.ExchangeRate) error {
if rate.Rate <= 0 {
return fmt.Errorf("invalid exchange rate: must be positive")
}
if rate.FromCurrency == rate.ToCurrency {
return fmt.Errorf("invalid currency pair: from and to currencies cannot be the same")
}
now := time.Now()
rate.ValidFrom = now
rate.IsActive = true
filter := &currency.ExchangeRateFilter{
FromCurrency: rate.FromCurrency,
ToCurrency: rate.ToCurrency,
IsValid: &[]bool{true}[0],
}
oldRates, err := s.repository.ListExchangeRates(ctx, filter)
if err == nil {
for _, oldRate := range oldRates {
oldRate.IsActive = false
if err := s.repository.UpdateExchangeRate(ctx, oldRate); err != nil {
s.logger.WithError(err).Error("Failed to deactivate old exchange rate")
}
}
}
rate.ID = fmt.Sprintf("%s_%s_%d", rate.FromCurrency, rate.ToCurrency, time.Now().Unix())
if err := s.repository.CreateExchangeRate(ctx, rate); err != nil {
return fmt.Errorf("failed to create exchange rate: %w", err)
}
s.logger.WithFields(logrus.Fields{
"from_currency": rate.FromCurrency,
"to_currency": rate.ToCurrency,
"rate": rate.Rate,
"source": rate.Source,
}).Info("Exchange rate updated")
return nil
}
func (s *ExchangeRateService) GetSupportedCurrencies(ctx context.Context) ([]*currency.Currency, error) {
filter := &currency.CurrencyFilter{
IsActive: &[]bool{true}[0],
}
currencies, err := s.repository.ListCurrencies(ctx, filter)
if err != nil {
return nil, fmt.Errorf("failed to get supported currencies: %w", err)
}
return currencies, nil
}
func (s *ExchangeRateService) ValidateCurrencyPair(ctx context.Context, fromCurrency, toCurrency string) error {
_, err := s.repository.GetCurrency(ctx, fromCurrency)
if err != nil {
return fmt.Errorf("unsupported from currency: %s", fromCurrency)
}
_, err = s.repository.GetCurrency(ctx, toCurrency)
if err != nil {
return fmt.Errorf("unsupported to currency: %s", toCurrency)
}
_, err = s.GetExchangeRate(ctx, fromCurrency, toCurrency)
if err != nil {
return fmt.Errorf("no exchange rate available: %s to %s", fromCurrency, toCurrency)
}
return nil
}
@@ -0,0 +1,173 @@
package services
import (
"context"
"fmt"
"time"
"github.com/sirupsen/logrus"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
)
// RatePlanCurrencyIntegrator integrates currency system with rate plans
type RatePlanCurrencyIntegrator struct {
billingService currency.BillingService
exchangeService *ExchangeRateService
ratePlanService rateplan.Service
logger *logrus.Logger
baseCurrency string
}
// NewRatePlanCurrencyIntegrator creates a new rate plan currency integrator
func NewRatePlanCurrencyIntegrator(
billingService currency.BillingService,
exchangeService *ExchangeRateService,
ratePlanService rateplan.Service,
logger *logrus.Logger,
baseCurrency string,
) *RatePlanCurrencyIntegrator {
return &RatePlanCurrencyIntegrator{
billingService: billingService,
exchangeService: exchangeService,
ratePlanService: ratePlanService,
logger: logger,
baseCurrency: baseCurrency,
}
}
// SubscribeToPlanWithCurrency subscribes to a rate plan with currency conversion
func (rpci *RatePlanCurrencyIntegrator) SubscribeToPlanWithCurrency(ctx context.Context, profileID string, planID string, 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 := &currency.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 := &currency.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
}
@@ -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
}
@@ -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 {