# 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.