Implement application layer
This commit is contained in:
parent
9a9a6b338b
commit
da4775fa2f
122
app/convert_currency.go
Normal file
122
app/convert_currency.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue