Compare commits
6 commits
03f8f3a920
...
1038bad7d5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1038bad7d5 | ||
|
|
f9555e47c7 | ||
|
|
452a09fff7 | ||
|
|
46b0fc241c | ||
|
|
480447e9a6 | ||
|
|
3c44f185b0 |
|
|
@ -1,9 +1,11 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"converter/domain"
|
"converter/domain"
|
||||||
"converter/infrastructure/coinmarketcap"
|
"converter/infrastructure/coinmarketcap"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
@ -17,7 +19,7 @@ const (
|
||||||
// Note: The returned RateDTO may contain a reversed rate (e.g., if requesting USD->BTC
|
// 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
|
// but only BTC->USD is available), indicated by FromCode/ToCode being swapped from the request
|
||||||
type RateProvider interface {
|
type RateProvider interface {
|
||||||
GetRate(fromCode, toCode string) (coinmarketcap.RateDTO, error)
|
GetRate(ctx context.Context, fromCode, toCode string) (coinmarketcap.RateDTO, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertCurrencyUseCase handles currency conversion operations
|
// ConvertCurrencyUseCase handles currency conversion operations
|
||||||
|
|
@ -35,7 +37,11 @@ func NewConvertCurrencyUseCase(rateProvider RateProvider, converter *domain.Curr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute performs currency conversion
|
// Execute performs currency conversion
|
||||||
func (uc *ConvertCurrencyUseCase) Execute(amount, fromCode, toCode string) (domain.Money, error) {
|
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
|
// Create currencies with uniform precision
|
||||||
fromCurrency, err := domain.NewCurrency(fromCode, fromCode, CurrencyPrecision)
|
fromCurrency, err := domain.NewCurrency(fromCode, fromCode, CurrencyPrecision)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -49,7 +55,7 @@ func (uc *ConvertCurrencyUseCase) Execute(amount, fromCode, toCode string) (doma
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get exchange rate DTO from provider
|
// Get exchange rate DTO from provider
|
||||||
rateDTO, err := uc.rateProvider.GetRate(fromCode, toCode)
|
rateDTO, err := uc.rateProvider.GetRate(ctx, fromCode, toCode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Money{}, err
|
return domain.Money{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"converter/app/mocks"
|
"converter/app/mocks"
|
||||||
"converter/domain"
|
"converter/domain"
|
||||||
"converter/infrastructure/coinmarketcap"
|
"converter/infrastructure/coinmarketcap"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConvertCurrencyUseCase_Execute(t *testing.T) {
|
func TestConvertCurrencyUseCase_Execute(t *testing.T) {
|
||||||
|
|
@ -36,7 +38,7 @@ func TestConvertCurrencyUseCase_Execute(t *testing.T) {
|
||||||
Price: 50000.0, // BTC price in USD
|
Price: 50000.0, // BTC price in USD
|
||||||
Source: "coinmarketcap",
|
Source: "coinmarketcap",
|
||||||
}
|
}
|
||||||
m.On("GetRate", "BTC", "USD").Return(rateDTO, nil)
|
m.On("GetRate", mock.Anything, "BTC", "USD").Return(rateDTO, nil)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -56,7 +58,7 @@ func TestConvertCurrencyUseCase_Execute(t *testing.T) {
|
||||||
Price: 50000.0, // BTC price in USD (will be inverted)
|
Price: 50000.0, // BTC price in USD (will be inverted)
|
||||||
Source: "coinmarketcap",
|
Source: "coinmarketcap",
|
||||||
}
|
}
|
||||||
m.On("GetRate", "USD", "BTC").Return(rateDTO, nil)
|
m.On("GetRate", mock.Anything, "USD", "BTC").Return(rateDTO, nil)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -89,7 +91,7 @@ func TestConvertCurrencyUseCase_Execute(t *testing.T) {
|
||||||
wantCode: "",
|
wantCode: "",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
setupMock: func(m *mocks.RateProvider) {
|
setupMock: func(m *mocks.RateProvider) {
|
||||||
m.On("GetRate", "XYZ", "ABC").Return(coinmarketcap.RateDTO{}, domain.ErrRateNotFound)
|
m.On("GetRate", mock.Anything, "XYZ", "ABC").Return(coinmarketcap.RateDTO{}, domain.ErrRateNotFound)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -100,7 +102,7 @@ func TestConvertCurrencyUseCase_Execute(t *testing.T) {
|
||||||
wantCode: "",
|
wantCode: "",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
setupMock: func(m *mocks.RateProvider) {
|
setupMock: func(m *mocks.RateProvider) {
|
||||||
m.On("GetRate", "USD", "INVALID").Return(coinmarketcap.RateDTO{}, domain.ErrUnsupportedCurrency)
|
m.On("GetRate", mock.Anything, "USD", "INVALID").Return(coinmarketcap.RateDTO{}, domain.ErrUnsupportedCurrency)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -127,7 +129,7 @@ func TestConvertCurrencyUseCase_Execute(t *testing.T) {
|
||||||
tt.setupMock(mockProvider)
|
tt.setupMock(mockProvider)
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
result, err := useCase.Execute(tt.amount, tt.fromCode, tt.toCode)
|
result, err := useCase.Execute(context.Background(), tt.amount, tt.fromCode, tt.toCode)
|
||||||
|
|
||||||
// Assert error expectations
|
// Assert error expectations
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
context "context"
|
||||||
coinmarketcap "converter/infrastructure/coinmarketcap"
|
coinmarketcap "converter/infrastructure/coinmarketcap"
|
||||||
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
@ -13,9 +14,9 @@ type RateProvider struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRate provides a mock function with given fields: fromCode, toCode
|
// GetRate provides a mock function with given fields: ctx, fromCode, toCode
|
||||||
func (_m *RateProvider) GetRate(fromCode string, toCode string) (coinmarketcap.RateDTO, error) {
|
func (_m *RateProvider) GetRate(ctx context.Context, fromCode string, toCode string) (coinmarketcap.RateDTO, error) {
|
||||||
ret := _m.Called(fromCode, toCode)
|
ret := _m.Called(ctx, fromCode, toCode)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for GetRate")
|
panic("no return value specified for GetRate")
|
||||||
|
|
@ -23,17 +24,17 @@ func (_m *RateProvider) GetRate(fromCode string, toCode string) (coinmarketcap.R
|
||||||
|
|
||||||
var r0 coinmarketcap.RateDTO
|
var r0 coinmarketcap.RateDTO
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(0).(func(string, string) (coinmarketcap.RateDTO, error)); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, string, string) (coinmarketcap.RateDTO, error)); ok {
|
||||||
return rf(fromCode, toCode)
|
return rf(ctx, fromCode, toCode)
|
||||||
}
|
}
|
||||||
if rf, ok := ret.Get(0).(func(string, string) coinmarketcap.RateDTO); ok {
|
if rf, ok := ret.Get(0).(func(context.Context, string, string) coinmarketcap.RateDTO); ok {
|
||||||
r0 = rf(fromCode, toCode)
|
r0 = rf(ctx, fromCode, toCode)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Get(0).(coinmarketcap.RateDTO)
|
r0 = ret.Get(0).(coinmarketcap.RateDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||||
r1 = rf(fromCode, toCode)
|
r1 = rf(ctx, fromCode, toCode)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"converter/app"
|
"converter/app"
|
||||||
"converter/domain"
|
"converter/domain"
|
||||||
"converter/infrastructure/coinmarketcap"
|
"converter/infrastructure/coinmarketcap"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -40,8 +44,22 @@ func main() {
|
||||||
converter := domain.NewCurrencyConverter()
|
converter := domain.NewCurrencyConverter()
|
||||||
useCase := app.NewConvertCurrencyUseCase(client, converter)
|
useCase := app.NewConvertCurrencyUseCase(client, converter)
|
||||||
|
|
||||||
|
// Create context with timeout and signal handling
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Set up graceful shutdown on interrupt signals
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-sigChan
|
||||||
|
fmt.Fprintf(os.Stderr, "\nReceived interrupt signal, shutting down...\n")
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
// Execute conversion
|
// Execute conversion
|
||||||
result, err := useCase.Execute(amount, fromCurrency, toCurrency)
|
result, err := useCase.Execute(ctx, amount, fromCurrency, toCurrency)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package coinmarketcap
|
package coinmarketcap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
|
@ -40,16 +41,16 @@ func NewClient(apiKey string) *Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRate fetches exchange rate from CoinMarketCap API
|
// GetRate fetches exchange rate from CoinMarketCap API
|
||||||
func (c *Client) GetRate(fromCode, toCode string) (RateDTO, error) {
|
func (c *Client) GetRate(ctx context.Context, fromCode, toCode string) (RateDTO, error) {
|
||||||
// Try direct conversion first
|
// Try direct conversion first
|
||||||
rateDTO, err := c.tryGetRate(fromCode, toCode)
|
rateDTO, err := c.tryGetRate(ctx, fromCode, toCode)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return rateDTO, nil
|
return rateDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If empty result but no other error, try reverse
|
// If empty result but no other error, try reverse
|
||||||
if err == ErrUnknownSymbol {
|
if err == ErrUnknownSymbol {
|
||||||
reverseDTO, reverseErr := c.tryGetRate(toCode, fromCode)
|
reverseDTO, reverseErr := c.tryGetRate(ctx, toCode, fromCode)
|
||||||
if reverseErr == nil {
|
if reverseErr == nil {
|
||||||
return reverseDTO, nil
|
return reverseDTO, nil
|
||||||
}
|
}
|
||||||
|
|
@ -69,11 +70,12 @@ func (c *Client) GetRate(fromCode, toCode string) (RateDTO, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryGetRate attempts to get rate in specified direction
|
// tryGetRate attempts to get rate in specified direction
|
||||||
func (c *Client) tryGetRate(fromCode, toCode string) (RateDTO, error) {
|
func (c *Client) tryGetRate(ctx context.Context, fromCode, toCode string) (RateDTO, error) {
|
||||||
var response APIResponse
|
var response APIResponse
|
||||||
|
|
||||||
// Make API request
|
// Make API request
|
||||||
resp, err := c.httpClient.R().
|
resp, err := c.httpClient.R().
|
||||||
|
SetContext(ctx).
|
||||||
SetQueryParam("symbol", fromCode).
|
SetQueryParam("symbol", fromCode).
|
||||||
SetQueryParam("convert", toCode).
|
SetQueryParam("convert", toCode).
|
||||||
SetResult(&response).
|
SetResult(&response).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue