This commit is contained in:
2026-01-25 21:59:00 +08:00
parent 7fd537bef3
commit 4cad3f0250
118 changed files with 30473 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
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)
}