diff --git a/apps/carrier-connector/internal/currency/exchange_rate_service.go b/apps/carrier-connector/internal/currency/exchange_rate_service.go index 43b9b67..d862928 100644 --- a/apps/carrier-connector/internal/currency/exchange_rate_service.go +++ b/apps/carrier-connector/internal/currency/exchange_rate_service.go @@ -8,7 +8,6 @@ import ( "github.com/sirupsen/logrus" ) -// ExchangeRateService manages currency exchange rates type ExchangeRateService struct { repository Repository logger *logrus.Logger @@ -16,7 +15,6 @@ type ExchangeRateService struct { baseCurrency string } -// NewExchangeRateService creates a new exchange rate service func NewExchangeRateService(repository Repository, logger *logrus.Logger, baseCurrency string) *ExchangeRateService { return &ExchangeRateService{ repository: repository, @@ -26,12 +24,10 @@ func NewExchangeRateService(repository Repository, logger *logrus.Logger, baseCu } } -// AddProvider adds an exchange rate provider func (s *ExchangeRateService) AddProvider(provider ExchangeRateProvider) { s.providers = append(s.providers, provider) } -// GetExchangeRate gets the current exchange rate between two currencies func (s *ExchangeRateService) GetExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*ExchangeRate, error) { if fromCurrency == toCurrency { return &ExchangeRate{ @@ -44,17 +40,14 @@ func (s *ExchangeRateService) GetExchangeRate(ctx context.Context, fromCurrency, }, nil } - // Try to get from repository first rate, err := s.repository.GetLatestExchangeRate(ctx, fromCurrency, toCurrency) if err == nil { return rate, nil } - // If not found, try to get from providers for _, provider := range s.providers { providerRate, err := provider.GetRate(ctx, fromCurrency, toCurrency) if err == nil { - // Save to repository newRate := &ExchangeRate{ ID: fmt.Sprintf("%s_%s_%d", fromCurrency, toCurrency, time.Now().Unix()), FromCurrency: fromCurrency, @@ -76,7 +69,6 @@ func (s *ExchangeRateService) GetExchangeRate(ctx context.Context, fromCurrency, return nil, fmt.Errorf("exchange rate not found: %s to %s", fromCurrency, toCurrency) } -// ConvertAmount converts an amount from one currency to another func (s *ExchangeRateService) ConvertAmount(ctx context.Context, amount float64, fromCurrency, toCurrency string) (*CurrencyConversionResponse, error) { rate, err := s.GetExchangeRate(ctx, fromCurrency, toCurrency) if err != nil { @@ -95,7 +87,6 @@ func (s *ExchangeRateService) ConvertAmount(ctx context.Context, amount float64, }, nil } -// RefreshRates refreshes exchange rates from all providers func (s *ExchangeRateService) RefreshRates(ctx context.Context) error { s.logger.Info("Refreshing exchange rates") @@ -110,7 +101,6 @@ func (s *ExchangeRateService) RefreshRates(ctx context.Context) error { return nil } -// GetRateHistory gets historical exchange rates func (s *ExchangeRateService) GetRateHistory(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*ExchangeRate, error) { filter := &ExchangeRateFilter{ FromCurrency: fromCurrency, @@ -129,9 +119,7 @@ func (s *ExchangeRateService) GetRateHistory(ctx context.Context, fromCurrency, return rates, nil } -// UpdateExchangeRate updates an exchange rate func (s *ExchangeRateService) UpdateExchangeRate(ctx context.Context, rate *ExchangeRate) error { - // Validate rate if rate.Rate <= 0 { return fmt.Errorf("invalid exchange rate: must be positive") } @@ -140,12 +128,10 @@ func (s *ExchangeRateService) UpdateExchangeRate(ctx context.Context, rate *Exch return fmt.Errorf("invalid currency pair: from and to currencies cannot be the same") } - // Set validity now := time.Now() rate.ValidFrom = now rate.IsActive = true - // Deactivate old rates filter := &ExchangeRateFilter{ FromCurrency: rate.FromCurrency, ToCurrency: rate.ToCurrency, @@ -162,7 +148,6 @@ func (s *ExchangeRateService) UpdateExchangeRate(ctx context.Context, rate *Exch } } - // Create new 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) @@ -178,7 +163,6 @@ func (s *ExchangeRateService) UpdateExchangeRate(ctx context.Context, rate *Exch return nil } -// GetSupportedCurrencies gets all supported currencies func (s *ExchangeRateService) GetSupportedCurrencies(ctx context.Context) ([]*Currency, error) { filter := &CurrencyFilter{ IsActive: &[]bool{true}[0], @@ -192,9 +176,7 @@ func (s *ExchangeRateService) GetSupportedCurrencies(ctx context.Context) ([]*Cu return currencies, nil } -// ValidateCurrencyPair validates if a currency pair is supported func (s *ExchangeRateService) ValidateCurrencyPair(ctx context.Context, fromCurrency, toCurrency string) error { - // Check if currencies exist _, err := s.repository.GetCurrency(ctx, fromCurrency) if err != nil { return fmt.Errorf("unsupported from currency: %s", fromCurrency) @@ -205,7 +187,6 @@ func (s *ExchangeRateService) ValidateCurrencyPair(ctx context.Context, fromCurr return fmt.Errorf("unsupported to currency: %s", toCurrency) } - // Check if exchange rate exists or can be obtained _, err = s.GetExchangeRate(ctx, fromCurrency, toCurrency) if err != nil { return fmt.Errorf("no exchange rate available: %s to %s", fromCurrency, toCurrency) diff --git a/apps/carrier-connector/internal/currency/interface.go b/apps/carrier-connector/internal/currency/interface.go new file mode 100644 index 0000000..b5ba430 --- /dev/null +++ b/apps/carrier-connector/internal/currency/interface.go @@ -0,0 +1,81 @@ +package currency + +import ( + "context" + "time" +) + +// Repository defines the interface for currency data operations +type Repository interface { + // Currency operations + CreateCurrency(ctx context.Context, currency *Currency) error + GetCurrency(ctx context.Context, code string) (*Currency, error) + UpdateCurrency(ctx context.Context, currency *Currency) error + DeleteCurrency(ctx context.Context, code string) error + ListCurrencies(ctx context.Context, filter *CurrencyFilter) ([]*Currency, error) + CountCurrencies(ctx context.Context, filter *CurrencyFilter) (int, error) + + // Exchange rate operations + CreateExchangeRate(ctx context.Context, rate *ExchangeRate) error + GetExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*ExchangeRate, error) + UpdateExchangeRate(ctx context.Context, rate *ExchangeRate) error + DeleteExchangeRate(ctx context.Context, id string) error + ListExchangeRates(ctx context.Context, filter *ExchangeRateFilter) ([]*ExchangeRate, error) + GetLatestExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*ExchangeRate, error) + + // Transaction operations + CreateTransaction(ctx context.Context, transaction *Transaction) error + GetTransaction(ctx context.Context, id string) (*Transaction, error) + UpdateTransaction(ctx context.Context, transaction *Transaction) error + DeleteTransaction(ctx context.Context, id string) error + ListTransactions(ctx context.Context, filter *TransactionFilter) ([]*Transaction, error) + CountTransactions(ctx context.Context, filter *TransactionFilter) (int, error) +} + +// ExchangeRateProvider defines the interface for external exchange rate providers +type ExchangeRateProvider interface { + GetRates(ctx context.Context) (map[string]float64, error) + GetRate(ctx context.Context, fromCurrency, toCurrency string) (float64, error) + RefreshRates(ctx context.Context) error +} + +// BillingService defines the interface for multi-currency billing operations +type BillingService interface { + ProcessBilling(ctx context.Context, req *BillingRequest) (*BillingResponse, error) + ConvertAmount(ctx context.Context, req *CurrencyConversionRequest) (*CurrencyConversionResponse, error) + GetBillingHistory(ctx context.Context, profileID string, filter *TransactionFilter) ([]*Transaction, error) + CalculateTotalBilling(ctx context.Context, profileID string, fromDate, toDate time.Time) (*BillingSummary, error) +} + +// AnalyticsService defines the interface for currency analytics +type AnalyticsService interface { + GetRevenueByCurrency(ctx context.Context, filter *TransactionFilter) (map[string]float64, error) + GetTransactionVolumeByCurrency(ctx context.Context, filter *TransactionFilter) (map[string]int64, error) + GetExchangeRateTrends(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*ExchangeRate, error) + GetCurrencyUsageStats(ctx context.Context) (*CurrencyUsageStats, error) +} + +// BillingSummary represents a billing summary for a profile +type BillingSummary struct { + ProfileID string `json:"profile_id"` + TotalAmount float64 `json:"total_amount"` + Currency string `json:"currency"` + BaseTotalAmount float64 `json:"base_total_amount"` + BaseCurrency string `json:"base_currency"` + TransactionCount int `json:"transaction_count"` + FromDate time.Time `json:"from_date"` + ToDate time.Time `json:"to_date"` + Breakdown map[string]interface{} `json:"breakdown"` +} + +// CurrencyUsageStats represents statistics about currency usage +type CurrencyUsageStats struct { + TotalCurrencies int `json:"total_currencies"` + ActiveCurrencies int `json:"active_currencies"` + TotalTransactions int64 `json:"total_transactions"` + TotalVolume float64 `json:"total_volume"` + MostUsedCurrency string `json:"most_used_currency"` + CurrencyDistribution map[string]int64 `json:"currency_distribution"` + ExchangeRateCount int `json:"exchange_rate_count"` + LastUpdated time.Time `json:"last_updated"` +} diff --git a/apps/carrier-connector/internal/currency/models.go b/apps/carrier-connector/internal/currency/models.go new file mode 100644 index 0000000..709804c --- /dev/null +++ b/apps/carrier-connector/internal/currency/models.go @@ -0,0 +1,111 @@ +package currency + +import ( + "time" + + "gorm.io/gorm" +) + +// CurrencyModel represents the database model for currencies +type CurrencyModel struct { + Code string `gorm:"primaryKey;column:code" json:"code"` + Name string `gorm:"column:name" json:"name"` + Symbol string `gorm:"column:symbol" json:"symbol"` + DecimalPlaces int `gorm:"column:decimal_places" json:"decimal_places"` + IsActive bool `gorm:"column:is_active" json:"is_active"` + SupportedRegions string `gorm:"column:supported_regions;type:text" json:"supported_regions"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` +} + +// TableName returns the table name for the currency model +func (CurrencyModel) TableName() string { + return "currencies" +} + +// ExchangeRateModel represents the database model for exchange rates +type ExchangeRateModel struct { + ID string `gorm:"primaryKey;column:id" json:"id"` + FromCurrency string `gorm:"column:from_currency;index" json:"from_currency"` + ToCurrency string `gorm:"column:to_currency;index" json:"to_currency"` + Rate float64 `gorm:"column:rate" json:"rate"` + Source string `gorm:"column:source" json:"source"` + ValidFrom time.Time `gorm:"column:valid_from;index" json:"valid_from"` + ValidTo *time.Time `gorm:"column:valid_to;index" json:"valid_to"` + IsActive bool `gorm:"column:is_active" json:"is_active"` + CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` +} + +// TableName returns the table name for the exchange rate model +func (ExchangeRateModel) TableName() string { + return "exchange_rates" +} + +// TransactionModel represents the database model for transactions +type TransactionModel struct { + ID string `gorm:"primaryKey;column:id" json:"id"` + ProfileID string `gorm:"column:profile_id;index" json:"profile_id"` + SubscriptionID string `gorm:"column:subscription_id;index" json:"subscription_id"` + Type string `gorm:"column:type;index" json:"type"` + Amount float64 `gorm:"column:amount" json:"amount"` + Currency string `gorm:"column:currency;index" json:"currency"` + BaseAmount float64 `gorm:"column:base_amount" json:"base_amount"` + BaseCurrency string `gorm:"column:base_currency" json:"base_currency"` + ExchangeRate float64 `gorm:"column:exchange_rate" json:"exchange_rate"` + Description string `gorm:"column:description" json:"description"` + Status string `gorm:"column:status;index" json:"status"` + Metadata string `gorm:"column:metadata;type:text" json:"metadata"` + CreatedAt time.Time `gorm:"column:created_at;index" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` +} + +// TableName returns the table name for the transaction model +func (TransactionModel) TableName() string { + return "transactions" +} + +// BeforeCreate hook for CurrencyModel +func (c *CurrencyModel) BeforeCreate(tx *gorm.DB) error { + if c.CreatedAt.IsZero() { + c.CreatedAt = time.Now() + } + c.UpdatedAt = time.Now() + return nil +} + +// BeforeUpdate hook for CurrencyModel +func (c *CurrencyModel) BeforeUpdate(tx *gorm.DB) error { + c.UpdatedAt = time.Now() + return nil +} + +// BeforeCreate hook for ExchangeRateModel +func (e *ExchangeRateModel) BeforeCreate(tx *gorm.DB) error { + if e.CreatedAt.IsZero() { + e.CreatedAt = time.Now() + } + e.UpdatedAt = time.Now() + return nil +} + +// BeforeUpdate hook for ExchangeRateModel +func (e *ExchangeRateModel) BeforeUpdate(tx *gorm.DB) error { + e.UpdatedAt = time.Now() + return nil +} + +// BeforeCreate hook for TransactionModel +func (t *TransactionModel) BeforeCreate(tx *gorm.DB) error { + if t.CreatedAt.IsZero() { + t.CreatedAt = time.Now() + } + t.UpdatedAt = time.Now() + return nil +} + +// BeforeUpdate hook for TransactionModel +func (t *TransactionModel) BeforeUpdate(tx *gorm.DB) error { + t.UpdatedAt = time.Now() + return nil +}