diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc459b4 --- /dev/null +++ b/README.md @@ -0,0 +1,203 @@ +# Currency Converter CLI + +A command-line utility for converting currencies using CoinMarketCap API, built with Go following Clean Architecture principles. + +## Quick Start + +### Prerequisites +- Go 1.19 or higher +- CoinMarketCap API key (get from https://coinmarketcap.com/api/) + +### Installation & Setup +1. Clone the repository +2. Set your API key: + ```bash + export CMC_API_KEY=your_api_key_here + ``` + +### Usage + +#### Using Taskfile (Recommended) +```bash +# Install task runner (if not already installed) +go install github.com/go-task/task/v3/cmd/task@latest + +# Build and run with parameters +task run -- 123.45 USD BTC +task run -- 1 BTC USD +task run -- 100 EUR BTC + +# Other available tasks +task build # Build the application +task test # Run tests +task test-verbose # Run tests with verbose output +task clean # Clean build artifacts +task all # Full build pipeline +``` + +#### Using Go directly +```bash +# Build the application +go build -o bin/converter ./cmd/converter + +# Run conversions +./bin/converter 123.45 USD BTC +./bin/converter 1 BTC USD +./bin/converter 100 EUR BTC +``` + +#### Using Go commands directly +```bash +# Run tests +go test ./... + +# Build manually +mkdir -p bin && go build -o bin/converter ./cmd/converter + +# Run with API key +CMC_API_KEY=your_key ./bin/converter 50 USD BTC +``` + +## Project Structure + +``` +converter/ +├── cmd/converter/ # CLI application entry point +│ └── main.go # Main function, argument parsing, DI setup +├── app/ # Application layer (Use Cases) +│ ├── convert_currency.go # Core use case implementation +│ ├── convert_currency_test.go # Use case tests with mocks +│ └── mocks/ # Generated mocks (mockery) +│ └── RateProvider.go # Mock for RateProvider interface +├── domain/ # Domain layer (Business Logic) +│ ├── entities.go # Value objects (Currency, Money, Rate) +│ ├── entities_test.go # Domain entity tests +│ ├── services.go # Domain service (CurrencyConverter) +│ └── services_test.go # Domain service tests +├── infrastructure/ # Infrastructure layer (External APIs) +│ └── coinmarketcap/ # CoinMarketCap API client +│ ├── client.go # HTTP client with retry/fallback logic +│ └── models.go # API response DTOs +├── Taskfile.yml # Task runner configuration +├── go.mod # Go module definition +├── go.sum # Go module checksums +├── TASK.md # Original task requirements (Russian) +├── CLAUDE.md # Project configuration for Claude Code +└── README.md # This file +``` + +### Architecture Overview + +This project implements **Clean Architecture** with clear separation of concerns: + +- **Domain Layer**: Pure business logic with no external dependencies + - Value objects (Currency, Money, Rate) with validation + - Domain services (CurrencyConverter) for business operations + - Domain errors and business rules + +- **Application Layer**: Use cases that orchestrate domain logic + - ConvertCurrencyUseCase: Main conversion workflow + - RateProvider interface: Defines contract for rate fetching + - DTO to domain object mapping with reversal detection + +- **Infrastructure Layer**: External integrations and technical details + - CoinMarketCap API client with automatic retry logic + - HTTP error handling and response mapping + - Rate reversal logic (USD→BTC becomes BTC→USD inverted) + +- **CLI Layer**: User interface and dependency injection + - Command-line parsing and validation + - Environment variable configuration + - Error formatting for end users + +## Testing + +The project includes comprehensive test coverage using **testify** and **mockery**: + +```bash +# Run all tests +task test +# or +go test ./... + +# Run tests with verbose output +task test-verbose +# or +go test -v ./... + +# Run tests with coverage +task test-coverage +# or +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out +``` + +### Test Structure +- **Unit Tests**: Domain entities and services with table-driven tests +- **Use Case Tests**: Application layer with generated mocks using testify/mockery +- **Mock Generation**: Use `task generate-mocks` to regenerate mocks + +## Architecture Decisions + +### Why decimal.Decimal for Money Handling + +We chose `shopspring/decimal` over floating-point numbers and other alternatives for the following critical reasons: + +#### Financial Accuracy Requirements +- Floating point arithmetic cannot represent decimal numbers exactly +- Example: `0.1 + 0.2 ≠ 0.3` in floating-point arithmetic +- Currency conversion errors compound with multiple operations +- CoinMarketCap API returns exchange rates with high precision (8+ decimal places) + +#### Real-world Impact +```go +// Problematic with float64 +Bad: float64(123.45) * float64(0.000016) = 0.001975200000000001 + +// Accurate with decimal.Decimal +Good: decimal(123.45) * decimal(0.000016) = 0.001975200000000000 +``` + +#### Industry Standards +- Banking and financial systems never use floating-point for monetary calculations +- Major payment APIs (Stripe, PayPal) use string representations to avoid precision loss +- `shopspring/decimal` is the Go standard for financial applications (used by Kubernetes billing, trading systems) + +#### Alternative Analysis +- **big.Rat**: Overkill for this use case, overly complex API for basic currency operations +- **Integer scaling**: Requires manual precision management, error-prone across currencies with different decimal places (USD=2, BTC=8, ETH=18) +- **float64**: Unacceptable precision loss for financial data + +#### Performance vs Precision Trade-off +- Using integer scaling would significantly increase development complexity with manual precision management across different currency types +- decimal.Decimal adds only microseconds vs potentially costly conversion errors +- Ensures user trust by avoiding strange rounding artifacts +- Better maintainability with clear intent and JSON-friendly serialization + +**Conclusion**: The cost of precision errors in financial software far exceeds the minimal performance overhead of using decimal.Decimal. + +### Currency Handling Strategy + +Our implementation uses CoinMarketCap's `/v1/cryptocurrency/quotes/latest` endpoint with intelligent fallback logic: + +- **Direct request**: Try the requested conversion (e.g., `USD → BTC`) +- **Fallback on empty data**: If no data returned, try reverse direction (`BTC → USD`) and invert the rate +- **Error handling**: Distinguish between unknown symbols, invalid parameters, and network issues + +This approach handles both crypto-to-fiat and fiat-to-crypto conversions using only the cryptocurrency endpoint: +- **BTC → USD**: Direct API call works +- **USD → BTC**: API call fails (USD not in crypto DB), retry as BTC → USD, then invert rate + +The automatic fallback with rate inversion eliminates the need for separate fiat currency handling while maintaining mathematical precision using decimal arithmetic. + +### Currency Precision Strategy + +To keep the implementation simple, we use a uniform precision rule: + +- **All currencies**: 8 decimal places (covers cryptocurrency precision requirements) + +This approach handles high-precision cryptocurrencies properly while being acceptable for fiat currencies (even though they typically use 2 decimal places). In a production environment, we would maintain per-currency precision metadata, but 8 decimals covers all practical use cases without overcomplicating the initial implementation. + +## Development + +This project follows Clean Architecture principles and SOLID design patterns as specified in the requirements.