Files
Novault-backend/internal/handler/exchange_rate_handler.go
2026-01-25 21:59:00 +08:00

410 lines
12 KiB
Go

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)
}
}