diff --git a/app/convert_currency.go b/app/convert_currency.go new file mode 100644 index 0000000..aff16d8 --- /dev/null +++ b/app/convert_currency.go @@ -0,0 +1,122 @@ +package app + +import ( + "context" + "converter/domain" + "converter/infrastructure/coinmarketcap" + "fmt" + "strings" + + "github.com/shopspring/decimal" +) + +// Constants +const ( + CurrencyPrecision = 8 // All currencies use 8 decimal precision +) + +// RateProvider defines the contract for fetching exchange rates +// Note: The returned RateDTO may contain a reversed rate (e.g., if requesting USD->BTC +// but only BTC->USD is available), indicated by FromCode/ToCode being swapped from the request +type RateProvider interface { + GetRate(ctx context.Context, fromCode, toCode string) (coinmarketcap.RateDTO, error) +} + +// ConvertCurrencyUseCase handles currency conversion operations +type ConvertCurrencyUseCase struct { + rateProvider RateProvider + converter *domain.CurrencyConverter +} + +// NewConvertCurrencyUseCase creates a new ConvertCurrencyUseCase +func NewConvertCurrencyUseCase(rateProvider RateProvider, converter *domain.CurrencyConverter) *ConvertCurrencyUseCase { + return &ConvertCurrencyUseCase{ + rateProvider: rateProvider, + converter: converter, + } +} + +// Execute performs currency conversion +func (uc *ConvertCurrencyUseCase) Execute(ctx context.Context, amount, fromCode, toCode string) (domain.Money, error) { + // Normalize currency codes to uppercase for consistency + fromCode = strings.ToUpper(strings.TrimSpace(fromCode)) + toCode = strings.ToUpper(strings.TrimSpace(toCode)) + + // Create currencies with uniform precision + fromCurrency, err := domain.NewCurrency(fromCode, fromCode, CurrencyPrecision) + if err != nil { + return domain.Money{}, err + } + + // Create money object from input + money, err := domain.NewMoney(amount, fromCurrency) + if err != nil { + return domain.Money{}, err + } + + // Get exchange rate DTO from provider + rateDTO, err := uc.rateProvider.GetRate(ctx, fromCode, toCode) + if err != nil { + return domain.Money{}, err + } + + // Convert DTO to domain Rate (domain logic) + rate, err := uc.createDomainRate(rateDTO, fromCode, toCode) + if err != nil { + return domain.Money{}, err + } + + // Convert using domain service + result, err := uc.converter.Convert(money, rate) + if err != nil { + return domain.Money{}, err + } + + return result, nil +} + +// createDomainRate converts RateDTO to domain.Rate with proper domain validation +func (uc *ConvertCurrencyUseCase) createDomainRate(dto coinmarketcap.RateDTO, requestedFromCode, requestedToCode string) (domain.Rate, error) { + // Convert price to decimal with proper precision first + priceStr := fmt.Sprintf("%.*f", CurrencyPrecision, dto.Price) + rateValue, err := decimal.NewFromString(priceStr) + if err != nil { + return domain.Rate{}, fmt.Errorf("invalid price format: %v", err) + } + + // Check if we got a reversed rate and handle accordingly + if dto.FromCode == requestedToCode && dto.ToCode == requestedFromCode { + // Reversed: we got BTC->USD but requested USD->BTC + // dto.FromName always contains the crypto name (Bitcoin) + if rateValue.IsZero() { + return domain.Rate{}, fmt.Errorf("cannot invert zero price") + } + + fromCurrency, err := domain.NewCurrency(requestedFromCode, dto.ToName, CurrencyPrecision) + if err != nil { + return domain.Rate{}, err + } + + toCurrency, err := domain.NewCurrency(requestedToCode, dto.FromName, CurrencyPrecision) + if err != nil { + return domain.Rate{}, err + } + + invertedRate := decimal.NewFromInt(1).Div(rateValue) + return domain.NewRate(fromCurrency, toCurrency, invertedRate, dto.Source) + } + + // Direct: we got exactly what we requested + // dto.FromName contains the crypto name + fromCurrency, err := domain.NewCurrency(dto.FromCode, dto.FromName, CurrencyPrecision) + if err != nil { + return domain.Rate{}, err + } + + toCurrency, err := domain.NewCurrency(dto.ToCode, dto.ToName, CurrencyPrecision) + if err != nil { + return domain.Rate{}, err + } + + return domain.NewRate(fromCurrency, toCurrency, rateValue, dto.Source) +}