package handler import ( "errors" "strconv" "time" "accounting-app/pkg/api" "accounting-app/internal/models" "accounting-app/internal/repository" "accounting-app/internal/service" "github.com/gin-gonic/gin" ) // ExchangeRateHandler handles HTTP requests for exchange rate operations type ExchangeRateHandler struct { exchangeRateService *service.ExchangeRateService yunAPIClient *service.YunAPIClient } // NewExchangeRateHandler creates a new ExchangeRateHandler instance func NewExchangeRateHandler(exchangeRateService *service.ExchangeRateService) *ExchangeRateHandler { return &ExchangeRateHandler{ exchangeRateService: exchangeRateService, } } // NewExchangeRateHandlerWithClient creates a new ExchangeRateHandler with YunAPI client func NewExchangeRateHandlerWithClient(exchangeRateService *service.ExchangeRateService, yunAPIClient *service.YunAPIClient) *ExchangeRateHandler { return &ExchangeRateHandler{ exchangeRateService: exchangeRateService, yunAPIClient: yunAPIClient, } } // ExchangeRateInput represents the input for creating/updating an exchange rate type ExchangeRateInput struct { FromCurrency models.Currency `json:"from_currency" binding:"required"` ToCurrency models.Currency `json:"to_currency" binding:"required"` Rate float64 `json:"rate" binding:"required,gt=0"` EffectiveDate string `json:"effective_date" binding:"required"` // Format: YYYY-MM-DD } // ConvertCurrencyInput represents the input for currency conversion type ConvertCurrencyInput struct { Amount float64 `json:"amount" binding:"required,gt=0"` FromCurrency models.Currency `json:"from_currency" binding:"required"` ToCurrency models.Currency `json:"to_currency" binding:"required"` Date string `json:"date,omitempty"` // Optional, format: YYYY-MM-DD } // CreateExchangeRate handles POST /api/v1/exchange-rates // Creates a new exchange rate with the provided data func (h *ExchangeRateHandler) CreateExchangeRate(c *gin.Context) { var input ExchangeRateInput if err := c.ShouldBindJSON(&input); err != nil { api.ValidationError(c, "Invalid request body: "+err.Error()) return } // Parse effective date effectiveDate, err := time.Parse("2006-01-02", input.EffectiveDate) if err != nil { api.BadRequest(c, "Invalid effective date format. Use YYYY-MM-DD") return } // Create exchange rate model exchangeRate := &models.ExchangeRate{ FromCurrency: input.FromCurrency, ToCurrency: input.ToCurrency, Rate: input.Rate, EffectiveDate: effectiveDate, } // Create exchange rate err = h.exchangeRateService.CreateExchangeRate(exchangeRate) if err != nil { if errors.Is(err, service.ErrInvalidRate) { api.BadRequest(c, "Exchange rate must be positive") return } if errors.Is(err, service.ErrInvalidEffectiveDate) { api.BadRequest(c, "Effective date cannot be in the future") return } if errors.Is(err, repository.ErrSameCurrency) { api.BadRequest(c, "From and to currency cannot be the same") return } api.InternalError(c, "Failed to create exchange rate: "+err.Error()) return } api.Created(c, exchangeRate) } // GetExchangeRates handles GET /api/v1/exchange-rates // Returns a list of all exchange rates func (h *ExchangeRateHandler) GetExchangeRates(c *gin.Context) { // Check if we should get latest rates only latestOnly := c.Query("latest") == "true" var rates []models.ExchangeRate var err error if latestOnly { rates, err = h.exchangeRateService.GetLatestExchangeRates() } else { rates, err = h.exchangeRateService.GetAllExchangeRates() } if err != nil { api.InternalError(c, "Failed to get exchange rates: "+err.Error()) return } api.Success(c, rates) } // GetExchangeRate handles GET /api/v1/exchange-rates/:id // Returns a single exchange rate by ID func (h *ExchangeRateHandler) GetExchangeRate(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid exchange rate ID") return } rate, err := h.exchangeRateService.GetExchangeRateByID(uint(id)) if err != nil { if errors.Is(err, repository.ErrExchangeRateNotFound) { api.NotFound(c, "Exchange rate not found") return } api.InternalError(c, "Failed to get exchange rate: "+err.Error()) return } api.Success(c, rate) } // UpdateExchangeRate handles PUT /api/v1/exchange-rates/:id // Updates an existing exchange rate with the provided data func (h *ExchangeRateHandler) UpdateExchangeRate(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid exchange rate ID") return } var input ExchangeRateInput if err := c.ShouldBindJSON(&input); err != nil { api.ValidationError(c, "Invalid request body: "+err.Error()) return } // Parse effective date effectiveDate, err := time.Parse("2006-01-02", input.EffectiveDate) if err != nil { api.BadRequest(c, "Invalid effective date format. Use YYYY-MM-DD") return } // Create exchange rate model with ID exchangeRate := &models.ExchangeRate{ ID: uint(id), FromCurrency: input.FromCurrency, ToCurrency: input.ToCurrency, Rate: input.Rate, EffectiveDate: effectiveDate, } // Update exchange rate err = h.exchangeRateService.UpdateExchangeRate(exchangeRate) if err != nil { if errors.Is(err, repository.ErrExchangeRateNotFound) { api.NotFound(c, "Exchange rate not found") return } if errors.Is(err, service.ErrInvalidRate) { api.BadRequest(c, "Exchange rate must be positive") return } if errors.Is(err, service.ErrInvalidEffectiveDate) { api.BadRequest(c, "Effective date cannot be in the future") return } if errors.Is(err, repository.ErrSameCurrency) { api.BadRequest(c, "From and to currency cannot be the same") return } api.InternalError(c, "Failed to update exchange rate: "+err.Error()) return } api.Success(c, exchangeRate) } // DeleteExchangeRate handles DELETE /api/v1/exchange-rates/:id // Deletes an exchange rate by ID func (h *ExchangeRateHandler) DeleteExchangeRate(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid exchange rate ID") return } err = h.exchangeRateService.DeleteExchangeRate(uint(id)) if err != nil { if errors.Is(err, repository.ErrExchangeRateNotFound) { api.NotFound(c, "Exchange rate not found") return } api.InternalError(c, "Failed to delete exchange rate: "+err.Error()) return } api.NoContent(c) } // GetExchangeRateByCurrencyPair handles GET /api/v1/exchange-rates/pair // Returns the most recent exchange rate for a currency pair // Query params: from_currency, to_currency, date (optional) func (h *ExchangeRateHandler) GetExchangeRateByCurrencyPair(c *gin.Context) { fromCurrency := models.Currency(c.Query("from_currency")) toCurrency := models.Currency(c.Query("to_currency")) dateStr := c.Query("date") if fromCurrency == "" || toCurrency == "" { api.BadRequest(c, "Both from_currency and to_currency are required") return } var rate *models.ExchangeRate var err error if dateStr != "" { // Get rate for specific date date, parseErr := time.Parse("2006-01-02", dateStr) if parseErr != nil { api.BadRequest(c, "Invalid date format. Use YYYY-MM-DD") return } rate, err = h.exchangeRateService.GetExchangeRateByCurrencyPairAndDate(fromCurrency, toCurrency, date) } else { // Get most recent rate rate, err = h.exchangeRateService.GetExchangeRateByCurrencyPair(fromCurrency, toCurrency) } if err != nil { if errors.Is(err, repository.ErrExchangeRateNotFound) { api.NotFound(c, "Exchange rate not found for the specified currency pair") return } if errors.Is(err, repository.ErrSameCurrency) { api.BadRequest(c, "From and to currency cannot be the same") return } api.InternalError(c, "Failed to get exchange rate: "+err.Error()) return } api.Success(c, rate) } // ConvertCurrency handles POST /api/v1/exchange-rates/convert // Converts an amount from one currency to another func (h *ExchangeRateHandler) ConvertCurrency(c *gin.Context) { var input ConvertCurrencyInput if err := c.ShouldBindJSON(&input); err != nil { api.ValidationError(c, "Invalid request body: "+err.Error()) return } var convertedAmount float64 var err error if input.Date != "" { // Convert using rate on specific date date, parseErr := time.Parse("2006-01-02", input.Date) if parseErr != nil { api.BadRequest(c, "Invalid date format. Use YYYY-MM-DD") return } convertedAmount, err = h.exchangeRateService.ConvertCurrencyOnDate(input.Amount, input.FromCurrency, input.ToCurrency, date) } else { // Convert using most recent rate convertedAmount, err = h.exchangeRateService.ConvertCurrency(input.Amount, input.FromCurrency, input.ToCurrency) } if err != nil { if errors.Is(err, repository.ErrExchangeRateNotFound) { api.NotFound(c, "Exchange rate not found for the specified currency pair") return } api.InternalError(c, "Failed to convert currency: "+err.Error()) return } api.Success(c, gin.H{ "original_amount": input.Amount, "from_currency": input.FromCurrency, "to_currency": input.ToCurrency, "converted_amount": convertedAmount, "date": input.Date, }) } // GetExchangeRatesByCurrency handles GET /api/v1/exchange-rates/currency/:currency // Returns all exchange rates involving a specific currency func (h *ExchangeRateHandler) GetExchangeRatesByCurrency(c *gin.Context) { currency := models.Currency(c.Param("currency")) if currency == "" { api.BadRequest(c, "Currency is required") return } rates, err := h.exchangeRateService.GetExchangeRateByCurrency(currency) if err != nil { api.InternalError(c, "Failed to get exchange rates: "+err.Error()) return } api.Success(c, rates) } // SetExchangeRate handles POST /api/v1/exchange-rates/set // Convenience endpoint to set an exchange rate (creates new entry) func (h *ExchangeRateHandler) SetExchangeRate(c *gin.Context) { var input ExchangeRateInput if err := c.ShouldBindJSON(&input); err != nil { api.ValidationError(c, "Invalid request body: "+err.Error()) return } // Parse effective date effectiveDate, err := time.Parse("2006-01-02", input.EffectiveDate) if err != nil { api.BadRequest(c, "Invalid effective date format. Use YYYY-MM-DD") return } // Set exchange rate err = h.exchangeRateService.SetExchangeRate(input.FromCurrency, input.ToCurrency, input.Rate, effectiveDate) if err != nil { if errors.Is(err, service.ErrInvalidRate) { api.BadRequest(c, "Exchange rate must be positive") return } if errors.Is(err, service.ErrInvalidEffectiveDate) { api.BadRequest(c, "Effective date cannot be in the future") return } if errors.Is(err, repository.ErrSameCurrency) { api.BadRequest(c, "From and to currency cannot be the same") return } api.InternalError(c, "Failed to set exchange rate: "+err.Error()) return } api.Success(c, gin.H{ "message": "Exchange rate set successfully", }) } // RefreshExchangeRates handles POST /api/v1/exchange-rates/refresh // Manually triggers a refresh of exchange rates from YunAPI func (h *ExchangeRateHandler) RefreshExchangeRates(c *gin.Context) { if h.yunAPIClient == nil { api.InternalError(c, "Exchange rate refresh is not configured") return } savedCount, err := h.yunAPIClient.ForceRefresh() if err != nil { if errors.Is(err, service.ErrAPIRequestFailed) { api.BadGateway(c, "Failed to fetch exchange rates from external API: "+err.Error()) return } api.InternalError(c, "Failed to refresh exchange rates: "+err.Error()) return } api.Success(c, gin.H{ "message": "Exchange rates refreshed successfully", "rates_saved": savedCount, }) } // RegisterRoutes registers all exchange rate routes to the given router group func (h *ExchangeRateHandler) RegisterRoutes(rg *gin.RouterGroup) { exchangeRates := rg.Group("/exchange-rates") { exchangeRates.POST("", h.CreateExchangeRate) exchangeRates.GET("", h.GetExchangeRates) exchangeRates.GET("/pair", h.GetExchangeRateByCurrencyPair) exchangeRates.POST("/convert", h.ConvertCurrency) exchangeRates.POST("/set", h.SetExchangeRate) exchangeRates.POST("/refresh", h.RefreshExchangeRates) exchangeRates.GET("/currency/:currency", h.GetExchangeRatesByCurrency) exchangeRates.GET("/:id", h.GetExchangeRate) exchangeRates.PUT("/:id", h.UpdateExchangeRate) exchangeRates.DELETE("/:id", h.DeleteExchangeRate) } }