package service import ( "errors" "fmt" "time" "accounting-app/internal/models" "accounting-app/internal/repository" ) // Common exchange rate service errors var ( ErrInvalidRate = errors.New("exchange rate must be positive") ErrInvalidEffectiveDate = errors.New("effective date cannot be in the future") ) // ExchangeRateService handles business logic for exchange rates type ExchangeRateService struct { repo *repository.ExchangeRateRepository } // NewExchangeRateService creates a new ExchangeRateService instance func NewExchangeRateService(repo *repository.ExchangeRateRepository) *ExchangeRateService { return &ExchangeRateService{repo: repo} } // CreateExchangeRate creates a new exchange rate func (s *ExchangeRateService) CreateExchangeRate(rate *models.ExchangeRate) error { // Validate rate value if rate.Rate <= 0 { return ErrInvalidRate } // Validate effective date (should not be in the future) if rate.EffectiveDate.After(time.Now()) { return ErrInvalidEffectiveDate } // Validate currencies are different if rate.FromCurrency == rate.ToCurrency { return repository.ErrSameCurrency } return s.repo.Create(rate) } // GetExchangeRateByID retrieves an exchange rate by its ID func (s *ExchangeRateService) GetExchangeRateByID(id uint) (*models.ExchangeRate, error) { return s.repo.GetByID(id) } // GetAllExchangeRates retrieves all exchange rates func (s *ExchangeRateService) GetAllExchangeRates() ([]models.ExchangeRate, error) { return s.repo.GetAll() } // UpdateExchangeRate updates an existing exchange rate func (s *ExchangeRateService) UpdateExchangeRate(rate *models.ExchangeRate) error { // Validate rate value if rate.Rate <= 0 { return ErrInvalidRate } // Validate effective date (should not be in the future) if rate.EffectiveDate.After(time.Now()) { return ErrInvalidEffectiveDate } // Validate currencies are different if rate.FromCurrency == rate.ToCurrency { return repository.ErrSameCurrency } return s.repo.Update(rate) } // DeleteExchangeRate deletes an exchange rate by its ID func (s *ExchangeRateService) DeleteExchangeRate(id uint) error { return s.repo.Delete(id) } // GetExchangeRateByCurrencyPair retrieves the most recent exchange rate for a currency pair func (s *ExchangeRateService) GetExchangeRateByCurrencyPair(fromCurrency, toCurrency models.Currency) (*models.ExchangeRate, error) { return s.repo.GetByCurrencyPair(fromCurrency, toCurrency) } // GetExchangeRateByCurrencyPairAndDate retrieves the exchange rate for a currency pair on a specific date func (s *ExchangeRateService) GetExchangeRateByCurrencyPairAndDate(fromCurrency, toCurrency models.Currency, date time.Time) (*models.ExchangeRate, error) { return s.repo.GetByCurrencyPairAndDate(fromCurrency, toCurrency, date) } // GetLatestExchangeRates retrieves the most recent exchange rate for each currency pair func (s *ExchangeRateService) GetLatestExchangeRates() ([]models.ExchangeRate, error) { return s.repo.GetLatestRates() } // ConvertCurrency converts an amount from one currency to another using the most recent exchange rate func (s *ExchangeRateService) ConvertCurrency(amount float64, fromCurrency, toCurrency models.Currency) (float64, error) { // If currencies are the same, return the original amount if fromCurrency == toCurrency { return amount, nil } // Get the exchange rate rate, err := s.repo.GetByCurrencyPair(fromCurrency, toCurrency) if err != nil { // If direct rate not found, try inverse rate if errors.Is(err, repository.ErrExchangeRateNotFound) { inverseRate, inverseErr := s.repo.GetByCurrencyPair(toCurrency, fromCurrency) if inverseErr != nil { return 0, fmt.Errorf("no exchange rate found for %s to %s: %w", fromCurrency, toCurrency, err) } // Use inverse rate: 1 / rate if inverseRate.Rate == 0 { return 0, errors.New("invalid inverse exchange rate (zero)") } return amount / inverseRate.Rate, nil } return 0, err } return amount * rate.Rate, nil } // ConvertCurrencyOnDate converts an amount from one currency to another using the exchange rate on a specific date func (s *ExchangeRateService) ConvertCurrencyOnDate(amount float64, fromCurrency, toCurrency models.Currency, date time.Time) (float64, error) { // If currencies are the same, return the original amount if fromCurrency == toCurrency { return amount, nil } // Get the exchange rate for the specific date rate, err := s.repo.GetByCurrencyPairAndDate(fromCurrency, toCurrency, date) if err != nil { // If direct rate not found, try inverse rate if errors.Is(err, repository.ErrExchangeRateNotFound) { inverseRate, inverseErr := s.repo.GetByCurrencyPairAndDate(toCurrency, fromCurrency, date) if inverseErr != nil { return 0, fmt.Errorf("no exchange rate found for %s to %s on %s: %w", fromCurrency, toCurrency, date.Format("2006-01-02"), err) } // Use inverse rate: 1 / rate if inverseRate.Rate == 0 { return 0, errors.New("invalid inverse exchange rate (zero)") } return amount / inverseRate.Rate, nil } return 0, err } return amount * rate.Rate, nil } // GetExchangeRateByCurrency retrieves all exchange rates involving a specific currency func (s *ExchangeRateService) GetExchangeRateByCurrency(currency models.Currency) ([]models.ExchangeRate, error) { return s.repo.GetByCurrency(currency) } // SetExchangeRate creates or updates an exchange rate for a currency pair // This is a convenience method for users to set rates without worrying about create vs update func (s *ExchangeRateService) SetExchangeRate(fromCurrency, toCurrency models.Currency, rate float64, effectiveDate time.Time) error { // Validate rate value if rate <= 0 { return ErrInvalidRate } // Validate effective date if effectiveDate.After(time.Now()) { return ErrInvalidEffectiveDate } // Validate currencies are different if fromCurrency == toCurrency { return repository.ErrSameCurrency } // Create new exchange rate entry exchangeRate := &models.ExchangeRate{ FromCurrency: fromCurrency, ToCurrency: toCurrency, Rate: rate, EffectiveDate: effectiveDate, } return s.repo.Create(exchangeRate) }