feat: Add multi-currency billing service with transaction processing, currency conversion, and history tracking

- Add BillingServiceImpl struct with repository, exchange service, logger, and base currency fields
- Add NewBillingService constructor with dependency injection
- Add ProcessBilling with request validation, currency conversion, transaction creation, and payment processing
- Add ConvertAmount wrapper delegating to exchange service
- Add GetBillingHistory with profile ID filtering and transaction retries
This commit is contained in:
nutcas3
2026-05-02 00:34:09 +03:00
parent 1e66806030
commit 70f64719e3
@@ -0,0 +1,148 @@
package currency
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
// BillingServiceImpl handles multi-currency billing operations
type BillingServiceImpl struct {
repository Repository
exchangeService *ExchangeRateService
logger *logrus.Logger
baseCurrency string
}
// NewBillingService creates a new billing service
func NewBillingService(repository Repository, exchangeService *ExchangeRateService, logger *logrus.Logger, baseCurrency string) *BillingServiceImpl {
return &BillingServiceImpl{
repository: repository,
exchangeService: exchangeService,
logger: logger,
baseCurrency: baseCurrency,
}
}
// ProcessBilling processes a billing request in multi-currency context
func (s *BillingServiceImpl) ProcessBilling(ctx context.Context, req *BillingRequest) (*BillingResponse, error) {
// Validate request
if err := s.validateBillingRequest(req); err != nil {
return nil, fmt.Errorf("invalid billing request: %w", err)
}
// Convert to base currency if needed
baseAmount := req.Amount
exchangeRate := 1.0
if req.Currency != s.baseCurrency {
conversion, err := s.exchangeService.ConvertAmount(ctx, req.Amount, req.Currency, s.baseCurrency)
if err != nil {
s.logger.WithError(err).Error("Failed to convert currency for billing")
return nil, fmt.Errorf("currency conversion failed: %w", err)
}
baseAmount = conversion.ConvertedAmount
exchangeRate = conversion.ExchangeRate
}
// Create transaction
transaction := &Transaction{
ID: uuid.New().String(),
ProfileID: req.ProfileID,
SubscriptionID: req.SubscriptionID,
Type: TransactionTypeSubscription,
Amount: req.Amount,
Currency: req.Currency,
BaseAmount: baseAmount,
BaseCurrency: s.baseCurrency,
ExchangeRate: exchangeRate,
Description: req.Description,
Status: TransactionStatusPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save transaction
if err := s.repository.CreateTransaction(ctx, transaction); err != nil {
s.logger.WithError(err).Error("Failed to create billing transaction")
return nil, fmt.Errorf("failed to create transaction: %w", err)
}
// Process payment (in real implementation, this would integrate with payment gateway)
transaction.Status = TransactionStatusCompleted
if err := s.repository.UpdateTransaction(ctx, transaction); err != nil {
s.logger.WithError(err).Error("Failed to update transaction status")
return nil, fmt.Errorf("failed to update transaction: %w", err)
}
s.logger.WithFields(logrus.Fields{
"transaction_id": transaction.ID,
"profile_id": req.ProfileID,
"amount": req.Amount,
"currency": req.Currency,
"base_amount": baseAmount,
"base_currency": s.baseCurrency,
}).Info("Billing processed successfully")
return &BillingResponse{
TransactionID: transaction.ID,
Amount: transaction.Amount,
Currency: transaction.Currency,
BaseAmount: transaction.BaseAmount,
BaseCurrency: transaction.BaseCurrency,
ExchangeRate: transaction.ExchangeRate,
Status: string(transaction.Status),
ProcessedAt: time.Now(),
}, nil
}
// ConvertAmount converts an amount between currencies
func (s *BillingServiceImpl) ConvertAmount(ctx context.Context, req *CurrencyConversionRequest) (*CurrencyConversionResponse, error) {
return s.exchangeService.ConvertAmount(ctx, req.Amount, req.FromCurrency, req.ToCurrency)
}
// GetBillingHistory retrieves billing history for a profile
func (s *BillingServiceImpl) GetBillingHistory(ctx context.Context, profileID string, filter *TransactionFilter) ([]*Transaction, error) {
if filter == nil {
filter = &TransactionFilter{}
}
filter.ProfileID = profileID
transactions, err := s.repository.ListTransactions(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to get billing history")
return nil, fmt.Errorf("failed to get billing history: %w", err)
}
return transactions, nil
}
// validateBillingRequest validates a billing request
func (s *BillingServiceImpl) validateBillingRequest(req *BillingRequest) error {
if req.ProfileID == "" {
return fmt.Errorf("profile ID is required")
}
if req.SubscriptionID == "" {
return fmt.Errorf("subscription ID is required")
}
if req.Amount <= 0 {
return fmt.Errorf("amount must be positive")
}
if req.Currency == "" {
return fmt.Errorf("currency is required")
}
if req.BillingDate.IsZero() {
req.BillingDate = time.Now()
}
// Validate currency
if err := s.exchangeService.ValidateCurrencyPair(context.Background(), req.Currency, s.baseCurrency); err != nil {
return fmt.Errorf("invalid currency: %w", err)
}
return nil
}