mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-22 04:34:27 +00:00
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:
@@ -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 := ¤cy.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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user