feat: Add currency handler layer with conversion, billing, exchange rates, and analytics endpoints

- Add CurrencyHandler struct with billing service, exchange service, and logger fields
- Add ConvertCurrency endpoint with JSON binding and amount conversion support
- Add GetExchangeRate endpoint with currency pair path parameters
- Add ProcessBilling endpoint with billing request validation and transaction processing
- Add GetBillingHistory with profile ID filtering, date range, limit, and offset support
This commit is contained in:
nutcas3
2026-05-02 00:38:29 +03:00
parent 077e381995
commit c93dd63474
2 changed files with 416 additions and 0 deletions
@@ -0,0 +1,149 @@
package currency
import (
"time"
)
// Currency represents a currency with its properties
type Currency struct {
Code string `json:"code" db:"code"` // ISO 4217 currency code (USD, EUR, etc.)
Name string `json:"name" db:"name"` // Full currency name
Symbol string `json:"symbol" db:"symbol"` // Currency symbol ($, €, etc.)
DecimalPlaces int `json:"decimal_places" db:"decimal_places"` // Number of decimal places
IsActive bool `json:"is_active" db:"is_active"` // Whether currency is active
SupportedRegions []string `json:"supported_regions" db:"supported_regions"` // Supported regions/countries
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// ExchangeRate represents an exchange rate between two currencies
type ExchangeRate struct {
ID string `json:"id" db:"id"`
FromCurrency string `json:"from_currency" db:"from_currency"`
ToCurrency string `json:"to_currency" db:"to_currency"`
Rate float64 `json:"rate" db:"rate"` // Exchange rate (1 FromCurrency = Rate ToCurrency)
Source string `json:"source" db:"source"` // Data source (ECB, FED, etc.)
ValidFrom time.Time `json:"valid_from" db:"valid_from"` // When rate becomes valid
ValidTo *time.Time `json:"valid_to,omitempty" db:"valid_to"` // When rate expires (optional)
IsActive bool `json:"is_active" db:"is_active"` // Whether rate is currently active
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Transaction represents a financial transaction in multi-currency context
type Transaction struct {
ID string `json:"id" db:"id"`
ProfileID string `json:"profile_id" db:"profile_id"`
SubscriptionID string `json:"subscription_id" db:"subscription_id"`
Type TransactionType `json:"type" db:"type"`
Amount float64 `json:"amount" db:"amount"`
Currency string `json:"currency" db:"currency"`
BaseAmount float64 `json:"base_amount" db:"base_amount"` // Amount in base currency (USD)
BaseCurrency string `json:"base_currency" db:"base_currency"` // Base currency for reporting
ExchangeRate float64 `json:"exchange_rate" db:"exchange_rate"` // Rate used for conversion
Description string `json:"description" db:"description"`
Status TransactionStatus `json:"status" db:"status"`
Metadata map[string]interface{} `json:"metadata,omitempty" db:"metadata"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// TransactionType defines the type of transaction
type TransactionType string
const (
TransactionTypeSubscription TransactionType = "subscription"
TransactionTypeUsage TransactionType = "usage"
TransactionTypeOverage TransactionType = "overage"
TransactionTypeRefund TransactionType = "refund"
TransactionTypeAdjustment TransactionType = "adjustment"
TransactionTypeDiscount TransactionType = "discount"
)
// TransactionStatus defines the status of a transaction
type TransactionStatus string
const (
TransactionStatusPending TransactionStatus = "pending"
TransactionStatusCompleted TransactionStatus = "completed"
TransactionStatusFailed TransactionStatus = "failed"
TransactionStatusCancelled TransactionStatus = "cancelled"
)
// CurrencyFilter defines filtering options for currency queries
type CurrencyFilter struct {
IsActive *bool `json:"is_active,omitempty"`
Region string `json:"region,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
SortBy string `json:"sort_by,omitempty"`
SortOrder string `json:"sort_order,omitempty"`
}
// ExchangeRateFilter defines filtering options for exchange rate queries
type ExchangeRateFilter struct {
FromCurrency string `json:"from_currency,omitempty"`
ToCurrency string `json:"to_currency,omitempty"`
Source string `json:"source,omitempty"`
IsValid *bool `json:"is_valid,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
SortBy string `json:"sort_by,omitempty"`
SortOrder string `json:"sort_order,omitempty"`
}
// TransactionFilter defines filtering options for transaction queries
type TransactionFilter struct {
ProfileID string `json:"profile_id,omitempty"`
SubscriptionID string `json:"subscription_id,omitempty"`
Type TransactionType `json:"type,omitempty"`
Status TransactionStatus `json:"status,omitempty"`
Currency string `json:"currency,omitempty"`
FromDate *time.Time `json:"from_date,omitempty"`
ToDate *time.Time `json:"to_date,omitempty"`
MinAmount float64 `json:"min_amount,omitempty"`
MaxAmount float64 `json:"max_amount,omitempty"`
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
SortBy string `json:"sort_by,omitempty"`
SortOrder string `json:"sort_order,omitempty"`
}
// CurrencyConversionRequest represents a request to convert an amount between currencies
type CurrencyConversionRequest struct {
Amount float64 `json:"amount" binding:"required,min=0"`
FromCurrency string `json:"from_currency" binding:"required"`
ToCurrency string `json:"to_currency" binding:"required"`
}
// CurrencyConversionResponse represents the response from currency conversion
type CurrencyConversionResponse struct {
OriginalAmount float64 `json:"original_amount"`
OriginalCurrency string `json:"original_currency"`
ConvertedAmount float64 `json:"converted_amount"`
ConvertedCurrency string `json:"converted_currency"`
ExchangeRate float64 `json:"exchange_rate"`
ConvertedAt time.Time `json:"converted_at"`
}
// BillingRequest represents a billing request in multi-currency context
type BillingRequest struct {
ProfileID string `json:"profile_id" binding:"required"`
SubscriptionID string `json:"subscription_id" binding:"required"`
Amount float64 `json:"amount" binding:"required,min=0"`
Currency string `json:"currency" binding:"required"`
Description string `json:"description"`
BillingDate time.Time `json:"billing_date"`
}
// BillingResponse represents the response from billing operation
type BillingResponse struct {
TransactionID string `json:"transaction_id"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
BaseAmount float64 `json:"base_amount"`
BaseCurrency string `json:"base_currency"`
ExchangeRate float64 `json:"exchange_rate"`
Status string `json:"status"`
ProcessedAt time.Time `json:"processed_at"`
}
@@ -0,0 +1,267 @@
package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
)
// CurrencyHandler handles currency-related HTTP requests
type CurrencyHandler struct {
billingService currency.BillingService
exchangeService *currency.ExchangeRateService
logger *logrus.Logger
}
// NewCurrencyHandler creates a new currency handler
func NewCurrencyHandler(billingService currency.BillingService, exchangeService *currency.ExchangeRateService, logger *logrus.Logger) *CurrencyHandler {
return &CurrencyHandler{
billingService: billingService,
exchangeService: exchangeService,
logger: logger,
}
}
// ConvertCurrency handles currency conversion requests
func (h *CurrencyHandler) ConvertCurrency(c *gin.Context) {
var req currency.CurrencyConversionRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.WithError(err).Error("Invalid currency conversion request")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
response, err := h.billingService.ConvertAmount(c.Request.Context(), &req)
if err != nil {
h.logger.WithError(err).Error("Currency conversion failed")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, response)
}
// GetExchangeRate handles exchange rate requests
func (h *CurrencyHandler) GetExchangeRate(c *gin.Context) {
fromCurrency := c.Param("from")
toCurrency := c.Param("to")
if fromCurrency == "" || toCurrency == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "from and to currency parameters are required"})
return
}
rate, err := h.exchangeService.GetExchangeRate(c.Request.Context(), fromCurrency, toCurrency)
if err != nil {
h.logger.WithError(err).Error("Failed to get exchange rate")
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, rate)
}
// ProcessBilling handles billing requests
func (h *CurrencyHandler) ProcessBilling(c *gin.Context) {
var req currency.BillingRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.WithError(err).Error("Invalid billing request")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
response, err := h.billingService.ProcessBilling(c.Request.Context(), &req)
if err != nil {
h.logger.WithError(err).Error("Billing processing failed")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, response)
}
// GetBillingHistory handles billing history requests
func (h *CurrencyHandler) GetBillingHistory(c *gin.Context) {
profileID := c.Param("profile_id")
if profileID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "profile_id parameter is required"})
return
}
// Parse query parameters
filter := &currency.TransactionFilter{}
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil {
filter.Limit = limit
}
}
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil {
filter.Offset = offset
}
}
if fromDateStr := c.Query("from_date"); fromDateStr != "" {
if fromDate, err := time.Parse("2006-01-02", fromDateStr); err == nil {
filter.FromDate = &fromDate
}
}
if toDateStr := c.Query("to_date"); toDateStr != "" {
if toDate, err := time.Parse("2006-01-02", toDateStr); err == nil {
filter.ToDate = &toDate
}
}
transactions, err := h.billingService.GetBillingHistory(c.Request.Context(), profileID, filter)
if err != nil {
h.logger.WithError(err).Error("Failed to get billing history")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"transactions": transactions,
"count": len(transactions),
})
}
// GetBillingSummary handles billing summary requests
func (h *CurrencyHandler) GetBillingSummary(c *gin.Context) {
profileID := c.Param("profile_id")
if profileID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "profile_id parameter is required"})
return
}
// Parse date parameters
fromDateStr := c.Query("from_date")
toDateStr := c.Query("to_date")
var fromDate, toDate time.Time
var err error
if fromDateStr != "" {
fromDate, err = time.Parse("2006-01-02", fromDateStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid from_date format, use YYYY-MM-DD"})
return
}
} else {
fromDate = time.Now().AddDate(0, -1, 0) // Default to 1 month ago
}
if toDateStr != "" {
toDate, err = time.Parse("2006-01-02", toDateStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid to_date format, use YYYY-MM-DD"})
return
}
} else {
toDate = time.Now() // Default to now
}
summary, err := h.billingService.CalculateTotalBilling(c.Request.Context(), profileID, fromDate, toDate)
if err != nil {
h.logger.WithError(err).Error("Failed to calculate billing summary")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, summary)
}
// ProcessRefund handles refund requests
func (h *CurrencyHandler) ProcessRefund(c *gin.Context) {
transactionID := c.Param("transaction_id")
if transactionID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "transaction_id parameter is required"})
return
}
var req struct {
Amount float64 `json:"amount" binding:"required,min=0"`
Reason string `json:"reason" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.WithError(err).Error("Invalid refund request")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// This would need to be added to the BillingService interface
// For now, we'll return an error
c.JSON(http.StatusNotImplemented, gin.H{"error": "Refund processing not yet implemented"})
}
// GetBillingAnalytics handles billing analytics requests
func (h *CurrencyHandler) GetBillingAnalytics(c *gin.Context) {
// This would need to be added to the BillingService interface
// For now, we'll return an error
c.JSON(http.StatusNotImplemented, gin.H{"error": "Billing analytics not yet implemented"})
}
// GetSupportedCurrencies handles supported currencies requests
func (h *CurrencyHandler) GetSupportedCurrencies(c *gin.Context) {
currencies, err := h.exchangeService.GetSupportedCurrencies(c.Request.Context())
if err != nil {
h.logger.WithError(err).Error("Failed to get supported currencies")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"currencies": currencies,
"count": len(currencies),
})
}
// RefreshExchangeRates handles exchange rate refresh requests
func (h *CurrencyHandler) RefreshExchangeRates(c *gin.Context) {
err := h.exchangeService.RefreshRates(c.Request.Context())
if err != nil {
h.logger.WithError(err).Error("Failed to refresh exchange rates")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Exchange rates refreshed successfully"})
}
// GetExchangeRateHistory handles exchange rate history requests
func (h *CurrencyHandler) GetExchangeRateHistory(c *gin.Context) {
fromCurrency := c.Param("from")
toCurrency := c.Param("to")
daysStr := c.Query("days")
if fromCurrency == "" || toCurrency == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "from and to currency parameters are required"})
return
}
days := 30 // Default to 30 days
if daysStr != "" {
if parsedDays, err := strconv.Atoi(daysStr); err == nil && parsedDays > 0 {
days = parsedDays
}
}
rates, err := h.exchangeService.GetRateHistory(c.Request.Context(), fromCurrency, toCurrency, days)
if err != nil {
h.logger.WithError(err).Error("Failed to get exchange rate history")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"rates": rates,
"count": len(rates),
"from": fromCurrency,
"to": toCurrency,
"days": days,
})
}