410 lines
12 KiB
Go
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)
|
|
}
|
|
}
|