converter/app/convert_currency.go

123 lines
3.8 KiB
Go
Raw Normal View History

2025-09-06 09:04:18 +03:00
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)
}