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) }