refactor: Extract currency interfaces and GORM models into separate files

- Move Repository, ExchangeRateProvider, BillingService, and AnalyticsService interfaces from types.go to new interface.go
- Move BillingSummary and CurrencyUsageStats structs to interface.go
- Move CurrencyModel, ExchangeRateModel, and TransactionModel to new models.go
- Move GORM hooks (BeforeCreate, BeforeUpdate) and TableName methods to models.go
- Remove redundant comments from exchange_rate_service.go
This commit is contained in:
nutcas3
2026-05-02 00:36:19 +03:00
parent 599b7dbd9f
commit 6b13e0300e
3 changed files with 192 additions and 19 deletions
@@ -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)
@@ -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"`
}
@@ -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
}