mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-26 14:42:15 +00:00
Multi-Currency Support for International Billing (#64)
* refactor: Rename handler package to handlers in carrier-connector
- Rename internal/handler directory to internal/handlers
- Update handler import path in main.go and routes.go
* feat: Add SM-DP+ handler with multi-carrier management and profile operations
- Add SMDPHandler struct with manager field and NewSMDPHandler constructor
- Add AddCarrier endpoint with JSON binding and carrier configuration support
- Add RemoveCarrier endpoint with carrier ID parameter validation
- Add DownloadProfile endpoint with 30-second timeout and context cancellation
- Add GetCarrierStatus endpoint returning all carriers with timestamp
- Add GetProfileStatus endpoint with EID/ICCID validation
* feat: Add SM-DP+ integration layer with multi-carrier system initialization and route registration
- Add SMDPIntegration struct with service, handler, repository, and logger fields
- Add NewSMDPIntegration constructor with default carrier initialization
- Add InitializeSystem method with carrier health logging and status tracking
- Add RegisterRoutes method with profile, carrier, health, and metrics endpoints
- Add healthHandler with carrier health status aggregation and degraded state detection
- Add metricsHandler
* feat: Add SM-DP+ service layer with multi-carrier management and profile download operations
- Add SMDPService struct with manager, repository, and logger fields
- Add NewSMDPService constructor with default config and background health checking
- Add InitializeDefaultCarriers with AT&T, Verizon, and T-Mobile DE configurations
- Add DownloadProfile method with context support and request logging
- Add GetOptimalCarrier wrapping SelectCarrier for best carrier selection
- Add GetCarrierHealth returning
* feat: Add SM-DP+ manager with multi-carrier load balancing, health checking, and profile operations
- Add DefaultManagerConfig with 30s health checks, 3 retries, and circuit breaker configuration
- Add CarrierConfig struct with ES2 settings, MCC/MNC, priority, and feature support
- Add ToCarrier method converting CarrierConfig to Carrier with metrics and capabilities
- Add HealthChecker with periodic carrier health monitoring and status evaluation
- Add LoadBalancer with weighted round-robin, least
* feat: Add intelligent carrier selection service with scoring, recommendations, and analytics
- Add SelectionService struct with manager, handler, and logger fields
- Add NewSelectionService constructor with info-level logging
- Add IntelligentCarrierSelection method with comprehensive criteria evaluation
- Add IntelligentSelectionRequest with region, profile type, urgency, and cost sensitivity
- Add UserPreferences and BusinessContext structs for selection customization
- Add IntelligentSelectionResponse with selected
* feat: Add carrier selection handler with optimal selection, analytics, and performance monitoring endpoints
- Add SelectionHandler struct with manager and logger fields
- Add NewSelectionHandler constructor with info-level logging
- Add SelectOptimalCarrier endpoint with criteria validation and scoring
- Add SelectCarrier endpoint using default selection criteria
- Add GetSelectionHistory endpoint with carrier ID path parameter
- Add UpdateLearning endpoint with performance feedback validation
- Add GetSelectionAnalytics endpoint with carrier stats
* feat: Add carrier selection integration layer with multi-carrier setup, HTTP server, and demo capabilities
- Add SelectionIntegration struct with manager, selection service, handler, and SMDP service fields
- Add NewSelectionIntegration constructor with default manager config and 30s health checks
- Add SetupCarriers with AT&T, Verizon, T-Mobile DE, and Orange FR configurations
- Add StartServer method with health endpoint and 30s read/write timeouts
- Add StartHealthChecking wrapper for background
* feat: Refactor profile handlers into modular core components with simplified operations
- Split profile_handlers.go into profile_handlers_core.go, profile_handlers_management_core.go, and profile_handlers_management_delete.go
- Simplify OrderProfileHandlerWithRepo with ICCID/IMSI-only validation and "downloaded" state
- Remove webhook and message queue notifications from order handler
- Add UpdateProfileHandler with state validation for downloaded/activated/deactivated/expired
- Add DeleteProfileHandler with existence
* refactor: Extract carrier setup logic into separate file and remove unnecessary comments
- Move SetupCarriers method from selection_integration.go to new setup_carriers.go file
- Remove redundant comments from SelectionIntegration struct and methods
- Clean up inline comments in RunDemo method while preserving functionality
* feat: Add intelligent carrier selection algorithm with machine learning optimization and performance tracking
- Add SelectionAlgorithm with scoring, weighting, and ML-based carrier selection
- Add SelectionCriteria struct with region, profile type, urgency, and cost/performance weights
- Add CarrierScore with performance, reliability, cost, region, and capability scores
- Add calculateWeightedScore combining all scores with ML-optimized weights
- Add getWeights with urgency-based adjustments
* feat: Integrate SelectionAlgorithm into SMDPManager with optimal carrier selection and learning capabilities
- Add selector field to SMDPManager struct with SelectionAlgorithm type
- Initialize selector in NewSMDPManager constructor
- Add SelectOptimalCarrier method filtering healthy carriers and delegating to selection algorithm
- Add SelectCarrier wrapper with default criteria (operational profile, medium urgency, balanced weights)
- Add GetSelectionHistory method returning carrier selection history
- Add UpdateLearning
* refactor: Simplify profile handler function names by removing WithRepo suffix
- Rename ListProfilesHandlerWithRepo to ListProfilesHandler
- Rename GetProfileHandlerWithRepo to GetProfileHandler
- Rename DeleteProfileHandlerWithRepo to DeleteProfileHandler
* feat: Add pricing engine with rate plan validation, cost calculation, and price optimization
- Add PricingEngine struct with repository and logger fields
- Add ValidateRatePlan with comprehensive validation for fields, dates, allowances, overage rates, discounts, and early termination
- Add CalculateOptimalPrice with market analysis and recommended pricing based on competitor data
- Add ValidateSubscription with plan availability, validity dates, and discount validation
- Add CalculateSubscriptionCost with base
* feat: Add rate plan analytics handlers with usage, revenue, popularity, and dashboard endpoints
- Add AnalyticsResponse struct with success, message, and data fields
- Add GetUsageAnalytics with date range parsing, default last month period, and group by support
- Add GetRevenueAnalytics with RFC3339 date parsing and configurable filters
- Add GetPopularPlans with configurable limit defaulting to 10
- Add GetDashboardData aggregating top 5 popular plans, 30-day usage analytics, and revenue analytics
* feat: Add rate plan handler layer with CRUD operations, management endpoints, and analytics support
- Add RatePlanHandler struct with service and logger fields
- Add CreateRatePlan with JSON binding, draft status initialization, and comprehensive field mapping
- Add GetRatePlan with ID path parameter extraction and not found handling
- Add UpdateRatePlan with selective field updates and existing plan retrieval
- Add DeleteRatePlan with active subscription conflict detection
- Add ListRatePlans with carrier
* feat: Add rate plan type system with comprehensive domain models, allowances, subscriptions, and analytics support
- Add RatePlan struct with carrier, region, pricing, allowances, features, discounts, and validity fields
- Add PlanType enum with prepaid, postpaid, hybrid, pay-as-you-go, and unlimited options
- Add PlanStatus enum with draft, active, inactive, archived, and suspended states
- Add BillingCycle enum with daily, weekly, monthly, quarterly, and yearly options
- Add DataAllowance, VoiceAllowance, and
* feat: Add rate plan service layer with CRUD operations, subscription management, usage tracking, and analytics support
- Add Service struct with repository and logger fields
- Add CreateRatePlan with ID generation, validation, and comprehensive field checks
- Add GetRatePlan, UpdateRatePlan, and DeleteRatePlan with active subscription conflict detection
- Add ListRatePlans and SearchRatePlans with filtering and sorting support
- Add SubscribeToPlan with plan availability validation, duplicate subscription
* feat: Add carrier integration layer with optimal carrier selection, rate plan recommendations, and analytics support
- Add CarrierSelectionIntegrator struct with rate plan repository, SMDP manager, and logger fields
- Add GetOptimalCarrierWithRatePlan with region filtering, carrier grouping, performance scoring, and combined evaluation
- Add RecommendRatePlansForCarrier with carrier validation, active plan filtering, and recommendation creation
- Add UpdateCarrierSelectionCriteria with 30-day usage analytics
* feat: Add rate plan repository interface with GORM models and extended types for carrier integration
- Add Repository interface with rate plan CRUD, subscription management, usage tracking, and analytics operations
- Add GORM table name methods for RatePlan, RatePlanSubscription, and RatePlanUsage models
- Add GORM hooks with automatic timestamp handling for BeforeCreate and BeforeUpdate operations
- Add SearchCriteria with carrier, region, plan type, price range, pagination, and sorting fields
* feat: Refactor pricing engine by extracting calculation methods into separate file
- Move calculateDataOverage, calculateVoiceOverage, calculateSMSOverage, calculateDiscounts, and calculateRecommendedPrice to price_calculation.go
- Remove validation helper methods from pricing_engine.go
- Remove PriceOptimization and CostBreakdown type definitions
- Update Logger interface to use `any` instead of `interface{}`
- Remove inline comments throughout pricing_engine.go
- Add switch statement for discount type handling in calculateDiscounts
* refactor: Extract core rate plan types into separate file
- Move RatePlan struct from types.go to types_core.go
- Move PlanType enum with prepaid, postpaid, hybrid, pay-as-you-go, and unlimited options to types_core.go
- Move PlanStatus enum with draft, active, inactive, and archived states to types_core.go
- Move BillingCycle enum with daily, weekly, monthly, quarterly, and yearly options to types_core.go
- Add documentation comments for PlanType, PlanStatus, and BillingCycle enums
- Remove PlanStatusSuspended from PlanStatus enum
* feat: Add GORM repository implementation with CRUD operations, subscription management, usage tracking, and analytics support
- Add GormRepository struct with database and logger fields
- Add CreateRatePlan, GetRatePlan, UpdateRatePlan, and DeleteRatePlan with automatic timestamp handling
- Add ListRatePlans with carrier, region, plan type, status, price range, and validity filtering
- Add CountRatePlans with comprehensive filter support
- Add CreateSubscription, GetSubscription, UpdateSubscription, and DeleteSubscription with timestamp
* feat: Add carrier configuration layer with GORM repository, file loading, and model conversion support
- Add GormCarrierRepository struct with database and logger fields
- Add CarrierModel with ID, name, MCC, MNC, country code, active status, priority, ES2 config, capabilities, and timestamp fields
- Add GetCarriers with active carrier filtering and model-to-carrier conversion
- Add GetCarrier with ID lookup and not found handling
- Add SaveCarrier with upsert logic for create or update operations
* feat: Add carrier configuration file loading with JSON-based carrier setup and validation
- Add carriers.json with AT&T US, Verizon US, T-Mobile Germany, Orange France, and Vodafone UK configurations
- Add LoadCarriersFromFile with JSON parsing and config file reading
- Add ConvertConfigToCarriers with model-to-carrier conversion logic
- Add validateCarrier with ID, name, MCC, MNC, country code, and ES2 config validation
- Replace hardcoded carrier setup with file-based configuration loading
- Add success
* refactor: Extract service methods into separate files for better organization
- Move GetUsage, GetUsageHistory, GetUsageAnalytics, GetRevenueAnalytics, and GetPopularPlans to service_analytics.go
- Move validateRatePlan, validateSubscribeRequest, calculateNextBillingDate, and calculateCycleEnd to service_methods.go
- Move SubscribeToPlan, GetSubscription, UpdateSubscription, CancelSubscription, GetActiveSubscription, and ListSubscriptions to service_subscriptions.go
- Move RecordUsage to service_usage.go
* feat: Add currency configuration file with 20 major currencies and regional support
- Add currencies.json with USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, INR, BRL, MXN, SEK, NOK, DKK, SGD, HKD, NZD, ZAR, KRW, and TRY configurations
- Add currency metadata with code, name, symbol, decimal places, active status, and supported regions
- Add regional mappings for currency support across multiple countries and territories
* feat: Add currency analytics service with revenue tracking, transaction analysis, and usage statistics
- Add AnalyticsServiceImpl struct with repository and logger fields
- Add GetRevenueByCurrency with completed transaction filtering and revenue aggregation by currency
- Add GetTransactionVolumeByCurrency with transaction count grouping
- Add GetExchangeRateTrends with historical rate retrieval and date-based filtering
- Add GetCurrencyUsageStats with total/active currency counts, transaction volume
* 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
* feat: Add exchange rate service with multi-provider support, currency conversion, and historical rate tracking
- Add ExchangeRateService struct with repository, logger, providers, and base currency fields
- Add NewExchangeRateService constructor and AddProvider for provider registration
- Add GetExchangeRate with repository caching, provider fallback, and same-currency handling
- Add ConvertAmount wrapper with exchange rate retrieval and amount calculation
- Add RefreshRates with multi-provider rate
* refactor: Extract currency interfaces and GORM models into separate files
- Move Repository, ExchangeRateProvider, BillingService, and AnalyticsService interfaces from types.go to new interface.go
- Move BillingSummary and CurrencyUsageStats structs to interface.go
- Move CurrencyModel, ExchangeRateModel, and TransactionModel to new models.go
- Move GORM hooks (BeforeCreate, BeforeUpdate) and TableName methods to models.go
- Remove redundant comments from exchange_rate_service.go
* feat: Add rate plan currency integrator with subscription creation, cost calculation, and multi-currency billing support
- Add RatePlanCurrencyIntegrator struct with billing service, exchange service, rate plan service, logger, and base currency fields
- Add NewRatePlanCurrencyIntegrator constructor with dependency injection
- Add SubscribeToPlanWithCurrency with price conversion, subscription creation, and initial billing processing
- Add CalculatePlanCostInCurrency with base cost calculation
* feat: Add currency repository with GORM implementation, rate plan integration, and transaction management
- Add GormRepository with CRUD operations for currencies, exchange rates, and transactions
- Add repository helper methods for model-to-domain conversions with JSON metadata handling
- Add GetHistoricalRates with date range filtering and chronological ordering
- Add ListExchangeRates with currency pair, active status, and date range filtering
- Add CreateTransaction, GetTransaction, UpdateTransaction, and List
* 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
* refactor: Rename service package to services and move repository implementations to repository package
- Rename internal/service to internal/services in carrier-connector
- Update service imports in selection_integration.go and smdp_integration.go
- Move currency repository files from internal/currency to internal/repository
- Move rate plan repository files from internal/rateplan to internal/repository
- Update package declarations and imports for currency domain types
- Add currency package imports
* refactor: Add ExchangeRateService interface and format currency types with consistent alignment
- Add ExchangeRateService interface with GetExchangeRate, ConvertAmount, GetRateHistory, UpdateExchangeRate, GetSupportedCurrencies, ValidateCurrencyPair, and RefreshRates methods
- Add CurrencyRevenue struct with currency and revenue fields
- Add TransactionTypeStats struct with type, count, amount, and currency fields
- Align struct field tags consistently across Currency, ExchangeRate, Transaction, and request/response types
* refactor: Update service layer to use repository package types and align imports
- Update Service struct to use repository.Repository interface
- Update all method signatures to use repository package types (RatePlan, RatePlanSubscription, RatePlanUsage, etc.)
- Update filter and request types to use repository package (RatePlanFilter, SubscriptionFilter, CalculateCostRequest, etc.)
- Update status constants to use repository package (SubscriptionStatusActive, PlanStatusActive, DiscountTypePercentage, DiscountTypeFixed)
* refactor: Update handler constructors to use interface types instead of concrete implementations
- Update CurrencyHandler.exchangeService field from *currency.ExchangeRateService to currency.ExchangeRateService interface
- Update NewCurrencyHandler parameter from *currency.ExchangeRateService to currency.ExchangeRateService interface
- Update RatePlanHandler.service field from *rateplan.Service to rateplan.Service interface
- Update NewRatePlanHandler parameter from *rateplan.Service to rateplan.Service interface
* refactor: Rename currency parameter from 'currency' to 'curr' in currencyToModel helper method and add Service interface to rate plan package
- Rename currency parameter to curr in GormRepository.currencyToModel to avoid shadowing currency package name
- Add Service interface with rate plan, subscription, usage, and analytics operations
- Add CreateRatePlan, GetRatePlan, UpdateRatePlan, DeleteRatePlan, ListRatePlans, and SearchRatePlans methods
- Add SubscribeToPlan, GetSubscription, UpdateSubscription, CancelSubscription, GetActiveSubscription, and ListSubscriptions methods
- Add Rec
* feat: Add repository types package with rate plan, subscription, usage, and analytics domain models
- Add Repository interface with rate plan CRUD, subscription management, usage tracking, and analytics operations
- Add RatePlan struct with pricing, allowances, overage rates, discounts, and metadata fields
- Add RatePlanSubscription struct with billing cycle, auto-renewal, and applied discounts support
- Add RatePlanUsage struct with cycle tracking and data/voice/SMS usage metrics
- Add filter types for rate plans, subscriptions, usage analytics, and revenue analytics
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"currencies": [
|
||||
{
|
||||
"code": "USD",
|
||||
"name": "United States Dollar",
|
||||
"symbol": "$",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["US", "PR", "VI", "GU", "UM"]
|
||||
},
|
||||
{
|
||||
"code": "EUR",
|
||||
"name": "Euro",
|
||||
"symbol": "€",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["AT", "BE", "CY", "DE", "EE", "ES", "FI", "FR", "GR", "IE", "IT", "LU", "LV", "LT", "MT", "NL", "PT", "SK", "SI"]
|
||||
},
|
||||
{
|
||||
"code": "GBP",
|
||||
"name": "British Pound Sterling",
|
||||
"symbol": "£",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["GB", "GG", "IM", "JE"]
|
||||
},
|
||||
{
|
||||
"code": "JPY",
|
||||
"name": "Japanese Yen",
|
||||
"symbol": "¥",
|
||||
"decimal_places": 0,
|
||||
"is_active": true,
|
||||
"supported_regions": ["JP"]
|
||||
},
|
||||
{
|
||||
"code": "CAD",
|
||||
"name": "Canadian Dollar",
|
||||
"symbol": "C$",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["CA"]
|
||||
},
|
||||
{
|
||||
"code": "AUD",
|
||||
"name": "Australian Dollar",
|
||||
"symbol": "A$",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["AU", "CX", "CC", "HM", "KI", "NR", "TV", "NF"]
|
||||
},
|
||||
{
|
||||
"code": "CHF",
|
||||
"name": "Swiss Franc",
|
||||
"symbol": "CHF",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["CH", "LI"]
|
||||
},
|
||||
{
|
||||
"code": "CNY",
|
||||
"name": "Chinese Yuan",
|
||||
"symbol": "¥",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["CN"]
|
||||
},
|
||||
{
|
||||
"code": "INR",
|
||||
"name": "Indian Rupee",
|
||||
"symbol": "₹",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["IN"]
|
||||
},
|
||||
{
|
||||
"code": "BRL",
|
||||
"name": "Brazilian Real",
|
||||
"symbol": "R$",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["BR"]
|
||||
},
|
||||
{
|
||||
"code": "MXN",
|
||||
"name": "Mexican Peso",
|
||||
"symbol": "$",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["MX"]
|
||||
},
|
||||
{
|
||||
"code": "SEK",
|
||||
"name": "Swedish Krona",
|
||||
"symbol": "kr",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["SE"]
|
||||
},
|
||||
{
|
||||
"code": "NOK",
|
||||
"name": "Norwegian Krone",
|
||||
"symbol": "kr",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["NO", "SJ", "BV"]
|
||||
},
|
||||
{
|
||||
"code": "DKK",
|
||||
"name": "Danish Krone",
|
||||
"symbol": "kr",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["DK", "FO", "GL"]
|
||||
},
|
||||
{
|
||||
"code": "SGD",
|
||||
"name": "Singapore Dollar",
|
||||
"symbol": "S$",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["SG"]
|
||||
},
|
||||
{
|
||||
"code": "HKD",
|
||||
"name": "Hong Kong Dollar",
|
||||
"symbol": "HK$",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["HK"]
|
||||
},
|
||||
{
|
||||
"code": "NZD",
|
||||
"name": "New Zealand Dollar",
|
||||
"symbol": "NZ$",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["NZ", "CK", "NU", "TK", "PN"]
|
||||
},
|
||||
{
|
||||
"code": "ZAR",
|
||||
"name": "South African Rand",
|
||||
"symbol": "R",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["ZA", "LS", "SZ"]
|
||||
},
|
||||
{
|
||||
"code": "KRW",
|
||||
"name": "South Korean Won",
|
||||
"symbol": "₩",
|
||||
"decimal_places": 0,
|
||||
"is_active": true,
|
||||
"supported_regions": ["KR"]
|
||||
},
|
||||
{
|
||||
"code": "TRY",
|
||||
"name": "Turkish Lira",
|
||||
"symbol": "₺",
|
||||
"decimal_places": 2,
|
||||
"is_active": true,
|
||||
"supported_regions": ["TR"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package currency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Repository defines the interface for currency data operations
|
||||
type Repository interface {
|
||||
// Currency operations
|
||||
CreateCurrency(ctx context.Context, currency *Currency) error
|
||||
GetCurrency(ctx context.Context, code string) (*Currency, error)
|
||||
UpdateCurrency(ctx context.Context, currency *Currency) error
|
||||
DeleteCurrency(ctx context.Context, code string) error
|
||||
ListCurrencies(ctx context.Context, filter *CurrencyFilter) ([]*Currency, error)
|
||||
CountCurrencies(ctx context.Context, filter *CurrencyFilter) (int, error)
|
||||
|
||||
// Exchange rate operations
|
||||
CreateExchangeRate(ctx context.Context, rate *ExchangeRate) error
|
||||
GetExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*ExchangeRate, error)
|
||||
UpdateExchangeRate(ctx context.Context, rate *ExchangeRate) error
|
||||
DeleteExchangeRate(ctx context.Context, id string) error
|
||||
ListExchangeRates(ctx context.Context, filter *ExchangeRateFilter) ([]*ExchangeRate, error)
|
||||
GetLatestExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*ExchangeRate, error)
|
||||
|
||||
// Transaction operations
|
||||
CreateTransaction(ctx context.Context, transaction *Transaction) error
|
||||
GetTransaction(ctx context.Context, id string) (*Transaction, error)
|
||||
UpdateTransaction(ctx context.Context, transaction *Transaction) error
|
||||
DeleteTransaction(ctx context.Context, id string) error
|
||||
ListTransactions(ctx context.Context, filter *TransactionFilter) ([]*Transaction, error)
|
||||
CountTransactions(ctx context.Context, filter *TransactionFilter) (int, error)
|
||||
}
|
||||
|
||||
// ExchangeRateProvider defines the interface for external exchange rate providers
|
||||
type ExchangeRateProvider interface {
|
||||
GetRates(ctx context.Context) (map[string]float64, error)
|
||||
GetRate(ctx context.Context, fromCurrency, toCurrency string) (float64, error)
|
||||
RefreshRates(ctx context.Context) error
|
||||
}
|
||||
|
||||
// ExchangeRateService defines the interface for exchange rate operations
|
||||
type ExchangeRateService interface {
|
||||
GetExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*ExchangeRate, error)
|
||||
ConvertAmount(ctx context.Context, amount float64, fromCurrency, toCurrency string) (*CurrencyConversionResponse, error)
|
||||
GetRateHistory(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*ExchangeRate, error)
|
||||
UpdateExchangeRate(ctx context.Context, rate *ExchangeRate) error
|
||||
GetSupportedCurrencies(ctx context.Context) ([]*Currency, error)
|
||||
ValidateCurrencyPair(ctx context.Context, fromCurrency, toCurrency string) error
|
||||
RefreshRates(ctx context.Context) error
|
||||
}
|
||||
|
||||
// BillingService defines the interface for multi-currency billing operations
|
||||
type BillingService interface {
|
||||
ProcessBilling(ctx context.Context, req *BillingRequest) (*BillingResponse, error)
|
||||
ConvertAmount(ctx context.Context, req *CurrencyConversionRequest) (*CurrencyConversionResponse, error)
|
||||
GetBillingHistory(ctx context.Context, profileID string, filter *TransactionFilter) ([]*Transaction, error)
|
||||
CalculateTotalBilling(ctx context.Context, profileID string, fromDate, toDate time.Time) (*BillingSummary, error)
|
||||
}
|
||||
|
||||
// AnalyticsService defines the interface for currency analytics
|
||||
type AnalyticsService interface {
|
||||
GetRevenueByCurrency(ctx context.Context, filter *TransactionFilter) (map[string]float64, error)
|
||||
GetTransactionVolumeByCurrency(ctx context.Context, filter *TransactionFilter) (map[string]int64, error)
|
||||
GetExchangeRateTrends(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*ExchangeRate, error)
|
||||
GetCurrencyUsageStats(ctx context.Context) (*CurrencyUsageStats, error)
|
||||
}
|
||||
|
||||
// BillingSummary represents a billing summary for a profile
|
||||
type BillingSummary struct {
|
||||
ProfileID string `json:"profile_id"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
Currency string `json:"currency"`
|
||||
BaseTotalAmount float64 `json:"base_total_amount"`
|
||||
BaseCurrency string `json:"base_currency"`
|
||||
TransactionCount int `json:"transaction_count"`
|
||||
FromDate time.Time `json:"from_date"`
|
||||
ToDate time.Time `json:"to_date"`
|
||||
Breakdown map[string]interface{} `json:"breakdown"`
|
||||
}
|
||||
|
||||
// CurrencyUsageStats represents statistics about currency usage
|
||||
type CurrencyUsageStats struct {
|
||||
TotalCurrencies int `json:"total_currencies"`
|
||||
ActiveCurrencies int `json:"active_currencies"`
|
||||
TotalTransactions int64 `json:"total_transactions"`
|
||||
TotalVolume float64 `json:"total_volume"`
|
||||
MostUsedCurrency string `json:"most_used_currency"`
|
||||
CurrencyDistribution map[string]int64 `json:"currency_distribution"`
|
||||
ExchangeRateCount int `json:"exchange_rate_count"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package currency
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CurrencyModel represents the database model for currencies
|
||||
type CurrencyModel struct {
|
||||
Code string `gorm:"primaryKey;column:code" json:"code"`
|
||||
Name string `gorm:"column:name" json:"name"`
|
||||
Symbol string `gorm:"column:symbol" json:"symbol"`
|
||||
DecimalPlaces int `gorm:"column:decimal_places" json:"decimal_places"`
|
||||
IsActive bool `gorm:"column:is_active" json:"is_active"`
|
||||
SupportedRegions string `gorm:"column:supported_regions;type:text" json:"supported_regions"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for the currency model
|
||||
func (CurrencyModel) TableName() string {
|
||||
return "currencies"
|
||||
}
|
||||
|
||||
// ExchangeRateModel represents the database model for exchange rates
|
||||
type ExchangeRateModel struct {
|
||||
ID string `gorm:"primaryKey;column:id" json:"id"`
|
||||
FromCurrency string `gorm:"column:from_currency;index" json:"from_currency"`
|
||||
ToCurrency string `gorm:"column:to_currency;index" json:"to_currency"`
|
||||
Rate float64 `gorm:"column:rate" json:"rate"`
|
||||
Source string `gorm:"column:source" json:"source"`
|
||||
ValidFrom time.Time `gorm:"column:valid_from;index" json:"valid_from"`
|
||||
ValidTo *time.Time `gorm:"column:valid_to;index" json:"valid_to"`
|
||||
IsActive bool `gorm:"column:is_active" json:"is_active"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for the exchange rate model
|
||||
func (ExchangeRateModel) TableName() string {
|
||||
return "exchange_rates"
|
||||
}
|
||||
|
||||
// TransactionModel represents the database model for transactions
|
||||
type TransactionModel struct {
|
||||
ID string `gorm:"primaryKey;column:id" json:"id"`
|
||||
ProfileID string `gorm:"column:profile_id;index" json:"profile_id"`
|
||||
SubscriptionID string `gorm:"column:subscription_id;index" json:"subscription_id"`
|
||||
Type string `gorm:"column:type;index" json:"type"`
|
||||
Amount float64 `gorm:"column:amount" json:"amount"`
|
||||
Currency string `gorm:"column:currency;index" json:"currency"`
|
||||
BaseAmount float64 `gorm:"column:base_amount" json:"base_amount"`
|
||||
BaseCurrency string `gorm:"column:base_currency" json:"base_currency"`
|
||||
ExchangeRate float64 `gorm:"column:exchange_rate" json:"exchange_rate"`
|
||||
Description string `gorm:"column:description" json:"description"`
|
||||
Status string `gorm:"column:status;index" json:"status"`
|
||||
Metadata string `gorm:"column:metadata;type:text" json:"metadata"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;index" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for the transaction model
|
||||
func (TransactionModel) TableName() string {
|
||||
return "transactions"
|
||||
}
|
||||
|
||||
// BeforeCreate hook for CurrencyModel
|
||||
func (c *CurrencyModel) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.CreatedAt.IsZero() {
|
||||
c.CreatedAt = time.Now()
|
||||
}
|
||||
c.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook for CurrencyModel
|
||||
func (c *CurrencyModel) BeforeUpdate(tx *gorm.DB) error {
|
||||
c.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeCreate hook for ExchangeRateModel
|
||||
func (e *ExchangeRateModel) BeforeCreate(tx *gorm.DB) error {
|
||||
if e.CreatedAt.IsZero() {
|
||||
e.CreatedAt = time.Now()
|
||||
}
|
||||
e.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook for ExchangeRateModel
|
||||
func (e *ExchangeRateModel) BeforeUpdate(tx *gorm.DB) error {
|
||||
e.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeCreate hook for TransactionModel
|
||||
func (t *TransactionModel) BeforeCreate(tx *gorm.DB) error {
|
||||
if t.CreatedAt.IsZero() {
|
||||
t.CreatedAt = time.Now()
|
||||
}
|
||||
t.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook for TransactionModel
|
||||
func (t *TransactionModel) BeforeUpdate(tx *gorm.DB) error {
|
||||
t.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
// CurrencyRevenue represents revenue for a specific currency
|
||||
type CurrencyRevenue struct {
|
||||
Currency string `json:"currency"`
|
||||
Revenue float64 `json:"revenue"`
|
||||
}
|
||||
|
||||
// TransactionTypeStats represents statistics for a transaction type
|
||||
type TransactionTypeStats struct {
|
||||
Type TransactionType `json:"type"`
|
||||
Count int `json:"count"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
|
||||
// RatePlanHandler handles rate plan API endpoints
|
||||
type RatePlanHandler struct {
|
||||
service *rateplan.Service
|
||||
service rateplan.Service
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewRatePlanHandler creates a new rate plan handler
|
||||
func NewRatePlanHandler(service *rateplan.Service) *RatePlanHandler {
|
||||
func NewRatePlanHandler(service rateplan.Service) *RatePlanHandler {
|
||||
logger := logrus.New()
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@ import (
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handlers"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/service"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/services"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
|
||||
)
|
||||
|
||||
type SelectionIntegration struct {
|
||||
manager *smdp.SMDPManager
|
||||
selectionService *service.SelectionService
|
||||
selectionService *services.SelectionService
|
||||
selectionHandler *handlers.SelectionHandler
|
||||
smdpService *service.SMDPService
|
||||
smdpService *services.SMDPService
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ func NewSelectionIntegration(repo *repository.PostgresProfileStore) *SelectionIn
|
||||
manager := smdp.NewSMDPManager(repo, config)
|
||||
|
||||
// Create selection service
|
||||
selectionService := service.NewSelectionService(manager)
|
||||
selectionService := services.NewSelectionService(manager)
|
||||
|
||||
// Create SMDP service
|
||||
smdpService := service.NewSMDPService(repo)
|
||||
smdpService := services.NewSMDPService(repo)
|
||||
|
||||
return &SelectionIntegration{
|
||||
manager: manager,
|
||||
@@ -85,7 +85,7 @@ func (si *SelectionIntegration) GetManager() *smdp.SMDPManager {
|
||||
}
|
||||
|
||||
// GetSelectionService returns the selection service for testing
|
||||
func (si *SelectionIntegration) GetSelectionService() *service.SelectionService {
|
||||
func (si *SelectionIntegration) GetSelectionService() *services.SelectionService {
|
||||
return si.selectionService
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handlers"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/service"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/services"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/smdp"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SMDPIntegration integrates the multi-carrier SM-DP+ system with the existing carrier connector
|
||||
type SMDPIntegration struct {
|
||||
service *service.SMDPService
|
||||
service *services.SMDPService
|
||||
handler *handlers.SMDPHandler
|
||||
repository *repository.PostgresProfileStore
|
||||
logger *logrus.Logger
|
||||
@@ -24,7 +24,7 @@ func NewSMDPIntegration(repo *repository.PostgresProfileStore) *SMDPIntegration
|
||||
logger := logrus.New()
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
|
||||
svc := service.NewSMDPService(repo)
|
||||
svc := services.NewSMDPService(repo)
|
||||
manager := svc.GetManager()
|
||||
hnd := handlers.NewSMDPHandler(manager)
|
||||
|
||||
@@ -148,7 +148,7 @@ func (i *SMDPIntegration) metricsHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetService returns the SM-DP+ service for direct access
|
||||
func (i *SMDPIntegration) GetService() *service.SMDPService {
|
||||
func (i *SMDPIntegration) GetService() *services.SMDPService {
|
||||
return i.service
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,36 @@ import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Service defines the interface for rate plan business operations
|
||||
type Service interface {
|
||||
// Rate Plan operations
|
||||
CreateRatePlan(ctx context.Context, plan *RatePlan) (*RatePlan, error)
|
||||
GetRatePlan(ctx context.Context, id string) (*RatePlan, error)
|
||||
UpdateRatePlan(ctx context.Context, plan *RatePlan) (*RatePlan, error)
|
||||
DeleteRatePlan(ctx context.Context, id string) error
|
||||
ListRatePlans(ctx context.Context, filter *RatePlanFilter) ([]*RatePlan, error)
|
||||
SearchRatePlans(ctx context.Context, criteria SearchCriteria) ([]*RatePlan, error)
|
||||
|
||||
// Subscription operations
|
||||
SubscribeToPlan(ctx context.Context, req *SubscribeRequest) (*RatePlanSubscription, error)
|
||||
GetSubscription(ctx context.Context, id string) (*RatePlanSubscription, error)
|
||||
UpdateSubscription(ctx context.Context, subscription *RatePlanSubscription) (*RatePlanSubscription, error)
|
||||
CancelSubscription(ctx context.Context, subscriptionID string, reason string) error
|
||||
GetActiveSubscription(ctx context.Context, profileID string) (*RatePlanSubscription, error)
|
||||
ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error)
|
||||
|
||||
// Usage operations
|
||||
RecordUsage(ctx context.Context, req *RecordUsageRequest) (*RatePlanUsage, error)
|
||||
GetUsage(ctx context.Context, id string) (*RatePlanUsage, error)
|
||||
GetUsageHistory(ctx context.Context, profileID string, limit int) ([]*RatePlanUsage, error)
|
||||
CalculateCost(ctx context.Context, req *CalculateCostRequest) (*RatePlanCostCalculation, error)
|
||||
|
||||
// Analytics operations
|
||||
GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsFilter) (*UsageAnalytics, error)
|
||||
GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyticsFilter) (*RevenueAnalytics, error)
|
||||
GetPopularPlans(ctx context.Context, limit int) ([]*RatePlan, error)
|
||||
}
|
||||
|
||||
// Repository defines the interface for rate plan data operations
|
||||
type Repository interface {
|
||||
// Rate Plan operations
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormRepository implements the currency repository using GORM
|
||||
type GormRepository struct {
|
||||
db *gorm.DB
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewGormRepository creates a new GORM currency repository
|
||||
func NewGormRepository(db *gorm.DB, logger *logrus.Logger) *GormRepository {
|
||||
return &GormRepository{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCurrency creates a new currency
|
||||
func (r *GormRepository) CreateCurrency(ctx context.Context, currency *currency.Currency) error {
|
||||
model := r.currencyToModel(currency)
|
||||
|
||||
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to create currency")
|
||||
return fmt.Errorf("failed to create currency: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrency retrieves a currency by code
|
||||
func (r *GormRepository) GetCurrency(ctx context.Context, code string) (*currency.Currency, error) {
|
||||
var model currency.CurrencyModel
|
||||
if err := r.db.WithContext(ctx).Where("code = ?", code).First(&model).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("currency not found: %s", code)
|
||||
}
|
||||
r.logger.WithError(err).Error("Failed to get currency")
|
||||
return nil, fmt.Errorf("failed to get currency: %w", err)
|
||||
}
|
||||
|
||||
return r.modelToCurrency(&model)
|
||||
}
|
||||
|
||||
// UpdateCurrency updates an existing currency
|
||||
func (r *GormRepository) UpdateCurrency(ctx context.Context, currency *currency.Currency) error {
|
||||
model := r.currencyToModel(currency)
|
||||
|
||||
if err := r.db.WithContext(ctx).Save(model).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to update currency")
|
||||
return fmt.Errorf("failed to update currency: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCurrency deletes a currency
|
||||
func (r *GormRepository) DeleteCurrency(ctx context.Context, code string) error {
|
||||
if err := r.db.WithContext(ctx).Delete(¤cy.CurrencyModel{}, "code = ?", code).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to delete currency")
|
||||
return fmt.Errorf("failed to delete currency: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCurrencies retrieves currencies based on filter
|
||||
func (r *GormRepository) ListCurrencies(ctx context.Context, filter *currency.CurrencyFilter) ([]*currency.Currency, error) {
|
||||
query := r.db.WithContext(ctx).Model(¤cy.CurrencyModel{})
|
||||
|
||||
// Apply filters
|
||||
if filter.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *filter.IsActive)
|
||||
}
|
||||
if filter.Region != "" {
|
||||
query = query.Where("supported_regions LIKE ?", "%"+filter.Region+"%")
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
if filter.SortBy != "" {
|
||||
order := filter.SortBy
|
||||
if filter.SortOrder == "desc" {
|
||||
order += " DESC"
|
||||
}
|
||||
query = query.Order(order)
|
||||
} else {
|
||||
query = query.Order("code ASC")
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
if filter.Limit > 0 {
|
||||
query = query.Limit(filter.Limit)
|
||||
}
|
||||
if filter.Offset > 0 {
|
||||
query = query.Offset(filter.Offset)
|
||||
}
|
||||
|
||||
var models []currency.CurrencyModel
|
||||
if err := query.Find(&models).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to list currencies")
|
||||
return nil, fmt.Errorf("failed to list currencies: %w", err)
|
||||
}
|
||||
|
||||
currencies := make([]*currency.Currency, 0, len(models))
|
||||
for _, model := range models {
|
||||
curr, err := r.modelToCurrency(&model)
|
||||
if err != nil {
|
||||
r.logger.WithError(err).Error("Failed to convert currency model")
|
||||
continue
|
||||
}
|
||||
currencies = append(currencies, curr)
|
||||
}
|
||||
|
||||
return currencies, nil
|
||||
}
|
||||
|
||||
// CountCurrencies counts currencies based on filter
|
||||
func (r *GormRepository) CountCurrencies(ctx context.Context, filter *currency.CurrencyFilter) (int, error) {
|
||||
query := r.db.WithContext(ctx).Model(¤cy.CurrencyModel{})
|
||||
|
||||
// Apply filters
|
||||
if filter.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *filter.IsActive)
|
||||
}
|
||||
if filter.Region != "" {
|
||||
query = query.Where("supported_regions LIKE ?", "%"+filter.Region+"%")
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to count currencies")
|
||||
return 0, fmt.Errorf("failed to count currencies: %w", err)
|
||||
}
|
||||
|
||||
return int(count), nil
|
||||
}
|
||||
|
||||
// CreateExchangeRate creates a new exchange rate
|
||||
func (r *GormRepository) CreateExchangeRate(ctx context.Context, rate *currency.ExchangeRate) error {
|
||||
model := r.exchangeRateToModel(rate)
|
||||
|
||||
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to create exchange rate")
|
||||
return fmt.Errorf("failed to create exchange rate: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExchangeRate retrieves an exchange rate by ID
|
||||
func (r *GormRepository) GetExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*currency.ExchangeRate, error) {
|
||||
var model currency.ExchangeRateModel
|
||||
if err := r.db.WithContext(ctx).Where("from_currency = ? AND to_currency = ?", fromCurrency, toCurrency).First(&model).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("exchange rate not found: %s to %s", fromCurrency, toCurrency)
|
||||
}
|
||||
r.logger.WithError(err).Error("Failed to get exchange rate")
|
||||
return nil, fmt.Errorf("failed to get exchange rate: %w", err)
|
||||
}
|
||||
|
||||
return r.modelToExchangeRate(&model)
|
||||
}
|
||||
|
||||
// UpdateExchangeRate updates an existing exchange rate
|
||||
func (r *GormRepository) UpdateExchangeRate(ctx context.Context, rate *currency.ExchangeRate) error {
|
||||
model := r.exchangeRateToModel(rate)
|
||||
|
||||
if err := r.db.WithContext(ctx).Save(model).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to update exchange rate")
|
||||
return fmt.Errorf("failed to update exchange rate: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteExchangeRate deletes an exchange rate
|
||||
func (r *GormRepository) DeleteExchangeRate(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).Delete(¤cy.ExchangeRateModel{}, "id = ?", id).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to delete exchange rate")
|
||||
return fmt.Errorf("failed to delete exchange rate: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
|
||||
)
|
||||
|
||||
// currencyToModel converts currency domain model to database model
|
||||
func (r *GormRepository) currencyToModel(curr *currency.Currency) *currency.CurrencyModel {
|
||||
return ¤cy.CurrencyModel{
|
||||
Code: curr.Code,
|
||||
Name: curr.Name,
|
||||
Symbol: curr.Symbol,
|
||||
DecimalPlaces: curr.DecimalPlaces,
|
||||
IsActive: curr.IsActive,
|
||||
SupportedRegions: strings.Join(curr.SupportedRegions, ","),
|
||||
CreatedAt: curr.CreatedAt,
|
||||
UpdatedAt: curr.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// modelToCurrency converts database model to currency domain model
|
||||
func (r *GormRepository) modelToCurrency(model *currency.CurrencyModel) (*currency.Currency, error) {
|
||||
var regions []string
|
||||
if model.SupportedRegions != "" {
|
||||
regions = strings.Split(model.SupportedRegions, ",")
|
||||
}
|
||||
|
||||
return ¤cy.Currency{
|
||||
Code: model.Code,
|
||||
Name: model.Name,
|
||||
Symbol: model.Symbol,
|
||||
DecimalPlaces: model.DecimalPlaces,
|
||||
IsActive: model.IsActive,
|
||||
SupportedRegions: regions,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// exchangeRateToModel converts exchange rate domain model to database model
|
||||
func (r *GormRepository) exchangeRateToModel(rate *currency.ExchangeRate) *currency.ExchangeRateModel {
|
||||
return ¤cy.ExchangeRateModel{
|
||||
ID: rate.ID,
|
||||
FromCurrency: rate.FromCurrency,
|
||||
ToCurrency: rate.ToCurrency,
|
||||
Rate: rate.Rate,
|
||||
Source: rate.Source,
|
||||
ValidFrom: rate.ValidFrom,
|
||||
ValidTo: rate.ValidTo,
|
||||
IsActive: rate.IsActive,
|
||||
CreatedAt: rate.CreatedAt,
|
||||
UpdatedAt: rate.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// modelToExchangeRate converts database model to exchange rate domain model
|
||||
func (r *GormRepository) modelToExchangeRate(model *currency.ExchangeRateModel) (*currency.ExchangeRate, error) {
|
||||
return ¤cy.ExchangeRate{
|
||||
ID: model.ID,
|
||||
FromCurrency: model.FromCurrency,
|
||||
ToCurrency: model.ToCurrency,
|
||||
Rate: model.Rate,
|
||||
Source: model.Source,
|
||||
ValidFrom: model.ValidFrom,
|
||||
ValidTo: model.ValidTo,
|
||||
IsActive: model.IsActive,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// transactionToModel converts transaction domain model to database model
|
||||
func (r *GormRepository) transactionToModel(transaction *currency.Transaction) *currency.TransactionModel {
|
||||
metadata := ""
|
||||
if transaction.Metadata != nil {
|
||||
if data, err := json.Marshal(transaction.Metadata); err == nil {
|
||||
metadata = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
return ¤cy.TransactionModel{
|
||||
ID: transaction.ID,
|
||||
ProfileID: transaction.ProfileID,
|
||||
SubscriptionID: transaction.SubscriptionID,
|
||||
Type: string(transaction.Type),
|
||||
Amount: transaction.Amount,
|
||||
Currency: transaction.Currency,
|
||||
BaseAmount: transaction.BaseAmount,
|
||||
BaseCurrency: transaction.BaseCurrency,
|
||||
ExchangeRate: transaction.ExchangeRate,
|
||||
Description: transaction.Description,
|
||||
Status: string(transaction.Status),
|
||||
Metadata: metadata,
|
||||
CreatedAt: transaction.CreatedAt,
|
||||
UpdatedAt: transaction.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// modelToTransaction converts database model to transaction domain model
|
||||
func (r *GormRepository) modelToTransaction(model *currency.TransactionModel) (*currency.Transaction, error) {
|
||||
var metadata map[string]interface{}
|
||||
if model.Metadata != "" {
|
||||
if err := json.Unmarshal([]byte(model.Metadata), &metadata); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse metadata: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ¤cy.Transaction{
|
||||
ID: model.ID,
|
||||
ProfileID: model.ProfileID,
|
||||
SubscriptionID: model.SubscriptionID,
|
||||
Type: currency.TransactionType(model.Type),
|
||||
Amount: model.Amount,
|
||||
Currency: model.Currency,
|
||||
BaseAmount: model.BaseAmount,
|
||||
BaseCurrency: model.BaseCurrency,
|
||||
ExchangeRate: model.ExchangeRate,
|
||||
Description: model.Description,
|
||||
Status: currency.TransactionStatus(model.Status),
|
||||
Metadata: metadata,
|
||||
CreatedAt: model.CreatedAt,
|
||||
UpdatedAt: model.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
+1
-15
@@ -1,27 +1,13 @@
|
||||
package rateplan
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GormRepository implements the Repository interface using GORM
|
||||
type GormRepository struct {
|
||||
db *gorm.DB
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewGormRepository creates a new GORM repository
|
||||
func NewGormRepository(db *gorm.DB, logger *logrus.Logger) *GormRepository {
|
||||
return &GormRepository{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
// CreateUsage creates a new usage record
|
||||
func (r *GormRepository) CreateUsage(ctx context.Context, usage *RatePlanUsage) error {
|
||||
usage.LastUpdated = time.Now()
|
||||
+3
-1
@@ -1,4 +1,4 @@
|
||||
package rateplan
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
|
||||
)
|
||||
|
||||
// CreateRatePlan creates a new rate plan
|
||||
+3
-1
@@ -1,4 +1,4 @@
|
||||
package rateplan
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CreateSubscription creates a new rate plan subscription
|
||||
func (r *GormRepository) CreateSubscription(ctx context.Context, subscription *RatePlanSubscription) error {
|
||||
now := time.Now()
|
||||
@@ -85,6 +86,7 @@ func (r *GormRepository) DeleteSubscription(ctx context.Context, id string) erro
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSubscriptions retrieves subscriptions for a profile
|
||||
func (r *GormRepository) ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error) {
|
||||
query := r.db.WithContext(ctx).Where("profile_id = ?", profileID)
|
||||
@@ -0,0 +1,92 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
|
||||
)
|
||||
|
||||
// ListExchangeRates retrieves exchange rates based on filter
|
||||
func (r *GormRepository) ListExchangeRates(ctx context.Context, filter *currency.ExchangeRateFilter) ([]*currency.ExchangeRate, error) {
|
||||
query := r.db.WithContext(ctx).Model(¤cy.ExchangeRateModel{})
|
||||
|
||||
// Apply filters
|
||||
if filter.FromCurrency != "" {
|
||||
query = query.Where("from_currency = ?", filter.FromCurrency)
|
||||
}
|
||||
if filter.ToCurrency != "" {
|
||||
query = query.Where("to_currency = ?", filter.ToCurrency)
|
||||
}
|
||||
if filter.Source != "" {
|
||||
query = query.Where("source = ?", filter.Source)
|
||||
}
|
||||
if filter.IsValid != nil {
|
||||
now := time.Now()
|
||||
if *filter.IsValid {
|
||||
query = query.Where("valid_from <= ? AND (valid_to IS NULL OR valid_to >= ?)", now, now)
|
||||
} else {
|
||||
query = query.Where("valid_to < ?", now)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
if filter.SortBy != "" {
|
||||
order := filter.SortBy
|
||||
if filter.SortOrder == "desc" {
|
||||
order += " DESC"
|
||||
}
|
||||
query = query.Order(order)
|
||||
} else {
|
||||
query = query.Order("from_currency, to_currency, valid_from DESC")
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
if filter.Limit > 0 {
|
||||
query = query.Limit(filter.Limit)
|
||||
}
|
||||
if filter.Offset > 0 {
|
||||
query = query.Offset(filter.Offset)
|
||||
}
|
||||
|
||||
var models []currency.ExchangeRateModel
|
||||
if err := query.Find(&models).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to list exchange rates")
|
||||
return nil, fmt.Errorf("failed to list exchange rates: %w", err)
|
||||
}
|
||||
|
||||
rates := make([]*currency.ExchangeRate, 0, len(models))
|
||||
for _, model := range models {
|
||||
rate, err := r.modelToExchangeRate(&model)
|
||||
if err != nil {
|
||||
r.logger.WithError(err).Error("Failed to convert exchange rate model")
|
||||
continue
|
||||
}
|
||||
rates = append(rates, rate)
|
||||
}
|
||||
|
||||
return rates, nil
|
||||
}
|
||||
|
||||
// GetLatestExchangeRate gets the latest valid exchange rate
|
||||
func (r *GormRepository) GetLatestExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*currency.ExchangeRate, error) {
|
||||
now := time.Now()
|
||||
var model currency.ExchangeRateModel
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("from_currency = ? AND to_currency = ? AND is_active = ?", fromCurrency, toCurrency, true).
|
||||
Where("valid_from <= ? AND (valid_to IS NULL OR valid_to >= ?)", now, now).
|
||||
Order("valid_from DESC").
|
||||
First(&model).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("no valid exchange rate found: %s to %s", fromCurrency, toCurrency)
|
||||
}
|
||||
r.logger.WithError(err).Error("Failed to get latest exchange rate")
|
||||
return nil, fmt.Errorf("failed to get latest exchange rate: %w", err)
|
||||
}
|
||||
|
||||
return r.modelToExchangeRate(&model)
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
|
||||
)
|
||||
|
||||
// CreateTransaction creates a new transaction
|
||||
func (r *GormRepository) CreateTransaction(ctx context.Context, transaction *currency.Transaction) error {
|
||||
model := r.transactionToModel(transaction)
|
||||
|
||||
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to create transaction")
|
||||
return fmt.Errorf("failed to create transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTransaction retrieves a transaction by ID
|
||||
func (r *GormRepository) GetTransaction(ctx context.Context, id string) (*currency.Transaction, error) {
|
||||
var model currency.TransactionModel
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("transaction not found: %s", id)
|
||||
}
|
||||
r.logger.WithError(err).Error("Failed to get transaction")
|
||||
return nil, fmt.Errorf("failed to get transaction: %w", err)
|
||||
}
|
||||
|
||||
return r.modelToTransaction(&model)
|
||||
}
|
||||
|
||||
// UpdateTransaction updates an existing transaction
|
||||
func (r *GormRepository) UpdateTransaction(ctx context.Context, transaction *currency.Transaction) error {
|
||||
model := r.transactionToModel(transaction)
|
||||
|
||||
if err := r.db.WithContext(ctx).Save(model).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to update transaction")
|
||||
return fmt.Errorf("failed to update transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTransaction deletes a transaction
|
||||
func (r *GormRepository) DeleteTransaction(ctx context.Context, id string) error {
|
||||
if err := r.db.WithContext(ctx).Delete(¤cy.TransactionModel{}, "id = ?", id).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to delete transaction")
|
||||
return fmt.Errorf("failed to delete transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListTransactions retrieves transactions based on filter
|
||||
func (r *GormRepository) ListTransactions(ctx context.Context, filter *currency.TransactionFilter) ([]*currency.Transaction, error) {
|
||||
query := r.db.WithContext(ctx).Model(¤cy.TransactionModel{})
|
||||
|
||||
// Apply filters
|
||||
if filter.ProfileID != "" {
|
||||
query = query.Where("profile_id = ?", filter.ProfileID)
|
||||
}
|
||||
if filter.SubscriptionID != "" {
|
||||
query = query.Where("subscription_id = ?", filter.SubscriptionID)
|
||||
}
|
||||
if filter.Type != "" {
|
||||
query = query.Where("type = ?", filter.Type)
|
||||
}
|
||||
if filter.Status != "" {
|
||||
query = query.Where("status = ?", filter.Status)
|
||||
}
|
||||
if filter.Currency != "" {
|
||||
query = query.Where("currency = ?", filter.Currency)
|
||||
}
|
||||
if filter.FromDate != nil {
|
||||
query = query.Where("created_at >= ?", *filter.FromDate)
|
||||
}
|
||||
if filter.ToDate != nil {
|
||||
query = query.Where("created_at <= ?", *filter.ToDate)
|
||||
}
|
||||
if filter.MinAmount > 0 {
|
||||
query = query.Where("amount >= ?", filter.MinAmount)
|
||||
}
|
||||
if filter.MaxAmount > 0 {
|
||||
query = query.Where("amount <= ?", filter.MaxAmount)
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
if filter.SortBy != "" {
|
||||
order := filter.SortBy
|
||||
if filter.SortOrder == "desc" {
|
||||
order += " DESC"
|
||||
}
|
||||
query = query.Order(order)
|
||||
} else {
|
||||
query = query.Order("created_at DESC")
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
if filter.Limit > 0 {
|
||||
query = query.Limit(filter.Limit)
|
||||
}
|
||||
if filter.Offset > 0 {
|
||||
query = query.Offset(filter.Offset)
|
||||
}
|
||||
|
||||
var models []currency.TransactionModel
|
||||
if err := query.Find(&models).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to list transactions")
|
||||
return nil, fmt.Errorf("failed to list transactions: %w", err)
|
||||
}
|
||||
|
||||
transactions := make([]*currency.Transaction, 0, len(models))
|
||||
for _, model := range models {
|
||||
tx, err := r.modelToTransaction(&model)
|
||||
if err != nil {
|
||||
r.logger.WithError(err).Error("Failed to convert transaction model")
|
||||
continue
|
||||
}
|
||||
transactions = append(transactions, tx)
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
// CountTransactions counts transactions based on filter
|
||||
func (r *GormRepository) CountTransactions(ctx context.Context, filter *currency.TransactionFilter) (int, error) {
|
||||
query := r.db.WithContext(ctx).Model(¤cy.TransactionModel{})
|
||||
|
||||
// Apply filters
|
||||
if filter.ProfileID != "" {
|
||||
query = query.Where("profile_id = ?", filter.ProfileID)
|
||||
}
|
||||
if filter.SubscriptionID != "" {
|
||||
query = query.Where("subscription_id = ?", filter.SubscriptionID)
|
||||
}
|
||||
if filter.Type != "" {
|
||||
query = query.Where("type = ?", filter.Type)
|
||||
}
|
||||
if filter.Status != "" {
|
||||
query = query.Where("status = ?", filter.Status)
|
||||
}
|
||||
if filter.Currency != "" {
|
||||
query = query.Where("currency = ?", filter.Currency)
|
||||
}
|
||||
if filter.FromDate != nil {
|
||||
query = query.Where("created_at >= ?", *filter.FromDate)
|
||||
}
|
||||
if filter.ToDate != nil {
|
||||
query = query.Where("created_at <= ?", *filter.ToDate)
|
||||
}
|
||||
if filter.MinAmount > 0 {
|
||||
query = query.Where("amount >= ?", filter.MinAmount)
|
||||
}
|
||||
if filter.MaxAmount > 0 {
|
||||
query = query.Where("amount <= ?", filter.MaxAmount)
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
r.logger.WithError(err).Error("Failed to count transactions")
|
||||
return 0, fmt.Errorf("failed to count transactions: %w", err)
|
||||
}
|
||||
|
||||
return int(count), nil
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Repository defines the interface for rate plan data operations
|
||||
type Repository interface {
|
||||
// Rate Plan operations
|
||||
CreateRatePlan(ctx context.Context, plan *RatePlan) error
|
||||
GetRatePlan(ctx context.Context, id string) (*RatePlan, error)
|
||||
UpdateRatePlan(ctx context.Context, plan *RatePlan) error
|
||||
DeleteRatePlan(ctx context.Context, id string) error
|
||||
ListRatePlans(ctx context.Context, filter *RatePlanFilter) ([]*RatePlan, error)
|
||||
|
||||
// Subscription operations
|
||||
CreateSubscription(ctx context.Context, subscription *RatePlanSubscription) error
|
||||
GetSubscription(ctx context.Context, id string) (*RatePlanSubscription, error)
|
||||
UpdateSubscription(ctx context.Context, subscription *RatePlanSubscription) error
|
||||
GetActiveSubscription(ctx context.Context, profileID string) (*RatePlanSubscription, error)
|
||||
ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error)
|
||||
|
||||
// Usage operations
|
||||
CreateUsage(ctx context.Context, usage *RatePlanUsage) error
|
||||
GetUsage(ctx context.Context, id string) (*RatePlanUsage, error)
|
||||
UpdateUsage(ctx context.Context, usage *RatePlanUsage) error
|
||||
GetCurrentUsage(ctx context.Context, profileID string) (*RatePlanUsage, error)
|
||||
ListUsageHistory(ctx context.Context, profileID string, limit int) ([]*RatePlanUsage, error)
|
||||
|
||||
// Analytics operations
|
||||
GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsFilter) (*UsageAnalytics, error)
|
||||
GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyticsFilter) (*RevenueAnalytics, error)
|
||||
GetPopularPlans(ctx context.Context, limit int) ([]*RatePlan, error)
|
||||
}
|
||||
|
||||
// RatePlanUsage represents usage data for a rate plan
|
||||
type RatePlanUsage struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
RatePlanID string `json:"rate_plan_id" gorm:"index"`
|
||||
ProfileID string `json:"profile_id" gorm:"index"`
|
||||
CycleStart time.Time `json:"cycle_start"`
|
||||
CycleEnd time.Time `json:"cycle_end"`
|
||||
DataUsed int64 `json:"data_used"`
|
||||
VoiceUsed int64 `json:"voice_used"`
|
||||
SMSUsed int64 `json:"sms_used"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for RatePlanUsage
|
||||
func (RatePlanUsage) TableName() string {
|
||||
return "rate_plan_usage"
|
||||
}
|
||||
|
||||
// RatePlanSubscription represents a subscription to a rate plan
|
||||
type RatePlanSubscription struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
ProfileID string `json:"profile_id" gorm:"index"`
|
||||
RatePlanID string `json:"rate_plan_id" gorm:"index"`
|
||||
Status SubscriptionStatus `json:"status" gorm:"index"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
EndedAt *time.Time `json:"ended_at,omitempty"`
|
||||
BillingCycle BillingCycle `json:"billing_cycle"`
|
||||
NextBillingDate time.Time `json:"next_billing_date"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
CurrentCycle time.Time `json:"current_cycle"`
|
||||
AppliedDiscounts []string `json:"applied_discounts,omitempty" gorm:"serializer:json"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty" gorm:"serializer:json"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for RatePlanSubscription
|
||||
func (RatePlanSubscription) TableName() string {
|
||||
return "rate_plan_subscriptions"
|
||||
}
|
||||
|
||||
// SubscriptionStatus represents the status of a subscription
|
||||
type SubscriptionStatus string
|
||||
|
||||
const (
|
||||
SubscriptionStatusActive SubscriptionStatus = "active"
|
||||
SubscriptionStatusCancelled SubscriptionStatus = "cancelled"
|
||||
SubscriptionStatusExpired SubscriptionStatus = "expired"
|
||||
SubscriptionStatusSuspended SubscriptionStatus = "suspended"
|
||||
)
|
||||
|
||||
// BillingCycle represents the billing cycle type
|
||||
type BillingCycle string
|
||||
|
||||
const (
|
||||
BillingCycleDaily BillingCycle = "daily"
|
||||
BillingCycleWeekly BillingCycle = "weekly"
|
||||
BillingCycleMonthly BillingCycle = "monthly"
|
||||
BillingCycleQuarterly BillingCycle = "quarterly"
|
||||
BillingCycleYearly BillingCycle = "yearly"
|
||||
)
|
||||
|
||||
// SubscriptionFilter defines filtering options for subscription queries
|
||||
type SubscriptionFilter struct {
|
||||
Status SubscriptionStatus `json:"status,omitempty"`
|
||||
RatePlanID string `json:"rate_plan_id,omitempty"`
|
||||
StartedAfter *time.Time `json:"started_after,omitempty"`
|
||||
StartedBefore *time.Time `json:"started_before,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
}
|
||||
|
||||
// RatePlan represents a rate plan
|
||||
type RatePlan struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CarrierID string `json:"carrier_id" gorm:"index"`
|
||||
Region string `json:"region" gorm:"index"`
|
||||
PlanType PlanType `json:"plan_type"`
|
||||
BasePrice float64 `json:"base_price"`
|
||||
Currency string `json:"currency"`
|
||||
BillingCycle BillingCycle `json:"billing_cycle"`
|
||||
DataAllowance *DataAllowance `json:"data_allowance,omitempty" gorm:"serializer:json"`
|
||||
VoiceAllowance *VoiceAllowance `json:"voice_allowance,omitempty" gorm:"serializer:json"`
|
||||
SMSAllowance *SMSAllowance `json:"sms_allowance,omitempty" gorm:"serializer:json"`
|
||||
OverageRates *OverageRates `json:"overage_rates,omitempty" gorm:"serializer:json"`
|
||||
Discounts []*Discount `json:"discounts,omitempty" gorm:"serializer:json"`
|
||||
ValidFrom time.Time `json:"valid_from"`
|
||||
ValidTo *time.Time `json:"valid_to,omitempty"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Status PlanStatus `json:"status"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty" gorm:"serializer:json"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for RatePlan
|
||||
func (RatePlan) TableName() string {
|
||||
return "rate_plans"
|
||||
}
|
||||
|
||||
// PlanType represents the type of rate plan
|
||||
type PlanType string
|
||||
|
||||
const (
|
||||
PlanTypeData PlanType = "data"
|
||||
PlanTypeVoice PlanType = "voice"
|
||||
PlanTypeSMS PlanType = "sms"
|
||||
PlanTypeBundle PlanType = "bundle"
|
||||
)
|
||||
|
||||
// PlanStatus represents the status of a rate plan
|
||||
type PlanStatus string
|
||||
|
||||
const (
|
||||
PlanStatusActive PlanStatus = "active"
|
||||
PlanStatusInactive PlanStatus = "inactive"
|
||||
PlanStatusDraft PlanStatus = "draft"
|
||||
)
|
||||
|
||||
// DataAllowance represents data allowance configuration
|
||||
type DataAllowance struct {
|
||||
Amount int64 `json:"amount"`
|
||||
Unit string `json:"unit"`
|
||||
Unlimited bool `json:"unlimited"`
|
||||
}
|
||||
|
||||
// VoiceAllowance represents voice allowance configuration
|
||||
type VoiceAllowance struct {
|
||||
Minutes int64 `json:"minutes"`
|
||||
Unlimited bool `json:"unlimited"`
|
||||
}
|
||||
|
||||
// SMSAllowance represents SMS allowance configuration
|
||||
type SMSAllowance struct {
|
||||
Messages int64 `json:"messages"`
|
||||
Unlimited bool `json:"unlimited"`
|
||||
}
|
||||
|
||||
// OverageRates represents overage rate configuration
|
||||
type OverageRates struct {
|
||||
DataRate float64 `json:"data_rate"`
|
||||
VoiceRate float64 `json:"voice_rate"`
|
||||
SMSRate float64 `json:"sms_rate"`
|
||||
}
|
||||
|
||||
// Discount represents a discount configuration
|
||||
type Discount struct {
|
||||
ID string `json:"id"`
|
||||
Type DiscountType `json:"type"`
|
||||
Value float64 `json:"value"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// DiscountType represents the type of discount
|
||||
type DiscountType string
|
||||
|
||||
const (
|
||||
DiscountTypePercentage DiscountType = "percentage"
|
||||
DiscountTypeFixed DiscountType = "fixed"
|
||||
)
|
||||
|
||||
// RatePlanFilter defines filtering options for rate plan queries
|
||||
type RatePlanFilter struct {
|
||||
CarrierID string `json:"carrier_id,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
PlanType PlanType `json:"plan_type,omitempty"`
|
||||
Status PlanStatus `json:"status,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
MinPrice float64 `json:"min_price,omitempty"`
|
||||
MaxPrice float64 `json:"max_price,omitempty"`
|
||||
ValidFrom *time.Time `json:"valid_from,omitempty"`
|
||||
ValidTo *time.Time `json:"valid_to,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
SortBy string `json:"sort_by,omitempty"`
|
||||
SortOrder string `json:"sort_order,omitempty"`
|
||||
}
|
||||
|
||||
// UsageAnalyticsFilter defines filtering options for usage analytics
|
||||
type UsageAnalyticsFilter struct {
|
||||
RatePlanID string `json:"rate_plan_id,omitempty"`
|
||||
CarrierID string `json:"carrier_id,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
GroupBy string `json:"group_by,omitempty"`
|
||||
}
|
||||
|
||||
// UsageAnalytics contains usage statistics
|
||||
type UsageAnalytics struct {
|
||||
TotalDataUsed int64 `json:"total_data_used"`
|
||||
TotalVoiceUsed int64 `json:"total_voice_used"`
|
||||
TotalSMSUsed int64 `json:"total_sms_used"`
|
||||
ActiveUsers int `json:"active_users"`
|
||||
AverageUsage map[string]float64 `json:"average_usage"`
|
||||
UsageByPlan map[string]int64 `json:"usage_by_plan"`
|
||||
UsageByRegion map[string]int64 `json:"usage_by_region"`
|
||||
TimelineData []TimelineDataPoint `json:"timeline_data"`
|
||||
}
|
||||
|
||||
// RevenueAnalyticsFilter defines filtering options for revenue analytics
|
||||
type RevenueAnalyticsFilter struct {
|
||||
RatePlanID string `json:"rate_plan_id,omitempty"`
|
||||
CarrierID string `json:"carrier_id,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate time.Time `json:"end_date"`
|
||||
GroupBy string `json:"group_by,omitempty"`
|
||||
}
|
||||
|
||||
// RevenueAnalytics contains revenue statistics
|
||||
type RevenueAnalytics struct {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
RevenueByPlan map[string]float64 `json:"revenue_by_plan"`
|
||||
RevenueByCarrier map[string]float64 `json:"revenue_by_carrier"`
|
||||
RevenueByRegion map[string]float64 `json:"revenue_by_region"`
|
||||
AverageRevenue map[string]float64 `json:"average_revenue"`
|
||||
TimelineData []TimelineDataPoint `json:"timeline_data"`
|
||||
}
|
||||
|
||||
// TimelineDataPoint represents a data point in time series
|
||||
type TimelineDataPoint struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Value float64 `json:"value"`
|
||||
Label string `json:"label,omitempty"`
|
||||
}
|
||||
|
||||
// SearchCriteria defines search criteria for rate plans
|
||||
type SearchCriteria struct {
|
||||
CarrierID string `json:"carrier_id,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
PlanType PlanType `json:"plan_type,omitempty"`
|
||||
MinPrice float64 `json:"min_price,omitempty"`
|
||||
MaxPrice float64 `json:"max_price,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
SortBy string `json:"sort_by,omitempty"`
|
||||
SortOrder string `json:"sort_order,omitempty"`
|
||||
}
|
||||
|
||||
// SubscribeRequest represents a request to subscribe to a rate plan
|
||||
type SubscribeRequest struct {
|
||||
ProfileID string `json:"profile_id"`
|
||||
RatePlanID string `json:"rate_plan_id"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
AppliedDiscounts []string `json:"applied_discounts,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// RecordUsageRequest represents a request to record usage
|
||||
type RecordUsageRequest struct {
|
||||
ProfileID string `json:"profile_id"`
|
||||
DataUsed int64 `json:"data_used"`
|
||||
VoiceUsed int64 `json:"voice_used"`
|
||||
SMSUsed int64 `json:"sms_used"`
|
||||
}
|
||||
|
||||
// CalculateCostRequest represents a request to calculate cost
|
||||
type CalculateCostRequest struct {
|
||||
RatePlanID string `json:"rate_plan_id"`
|
||||
DataUsed int64 `json:"data_used"`
|
||||
VoiceUsed int64 `json:"voice_used"`
|
||||
SMSUsed int64 `json:"sms_used"`
|
||||
AppliedDiscounts []string `json:"applied_discounts,omitempty"`
|
||||
}
|
||||
|
||||
// RatePlanCostCalculation represents the result of a cost calculation
|
||||
type RatePlanCostCalculation struct {
|
||||
RatePlanID string `json:"rate_plan_id"`
|
||||
BaseCost float64 `json:"base_cost"`
|
||||
OverageCost float64 `json:"overage_cost"`
|
||||
DiscountCost float64 `json:"discount_cost"`
|
||||
TotalCost float64 `json:"total_cost"`
|
||||
Currency string `json:"currency"`
|
||||
Breakdown map[string]interface{} `json:"breakdown"`
|
||||
CalculatedAt time.Time `json:"calculated_at"`
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// AnalyticsServiceImpl handles currency analytics operations
|
||||
type AnalyticsServiceImpl struct {
|
||||
repository currency.Repository
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewAnalyticsService creates a new analytics service
|
||||
func NewAnalyticsService(repository currency.Repository, logger *logrus.Logger) *AnalyticsServiceImpl {
|
||||
return &AnalyticsServiceImpl{
|
||||
repository: repository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRevenueByCurrency calculates revenue breakdown by currency
|
||||
func (s *AnalyticsServiceImpl) GetRevenueByCurrency(ctx context.Context, filter *currency.TransactionFilter) (map[string]float64, error) {
|
||||
if filter == nil {
|
||||
filter = ¤cy.TransactionFilter{}
|
||||
}
|
||||
|
||||
filter.Status = currency.TransactionStatusCompleted
|
||||
|
||||
transactions, err := s.repository.ListTransactions(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get transactions for revenue analysis")
|
||||
return nil, fmt.Errorf("failed to get transactions: %w", err)
|
||||
}
|
||||
|
||||
revenueByCurrency := make(map[string]float64)
|
||||
|
||||
for _, tx := range transactions {
|
||||
if tx.Type == currency.TransactionTypeSubscription || tx.Type == currency.TransactionTypeUsage || tx.Type == currency.TransactionTypeOverage {
|
||||
revenueByCurrency[tx.Currency] += tx.Amount
|
||||
}
|
||||
}
|
||||
|
||||
return revenueByCurrency, nil
|
||||
}
|
||||
|
||||
// GetTransactionVolumeByCurrency calculates transaction volume by currency
|
||||
func (s *AnalyticsServiceImpl) GetTransactionVolumeByCurrency(ctx context.Context, filter *currency.TransactionFilter) (map[string]int64, error) {
|
||||
if filter == nil {
|
||||
filter = ¤cy.TransactionFilter{}
|
||||
}
|
||||
|
||||
transactions, err := s.repository.ListTransactions(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get transactions for volume analysis")
|
||||
return nil, fmt.Errorf("failed to get transactions: %w", err)
|
||||
}
|
||||
|
||||
volumeByCurrency := make(map[string]int64)
|
||||
|
||||
for _, tx := range transactions {
|
||||
volumeByCurrency[tx.Currency]++
|
||||
}
|
||||
|
||||
return volumeByCurrency, nil
|
||||
}
|
||||
|
||||
// GetExchangeRateTrends retrieves exchange rate trends for a currency pair
|
||||
func (s *AnalyticsServiceImpl) GetExchangeRateTrends(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*currency.ExchangeRate, error) {
|
||||
filter := ¤cy.ExchangeRateFilter{
|
||||
FromCurrency: fromCurrency,
|
||||
ToCurrency: toCurrency,
|
||||
IsValid: &[]bool{false}[0], // Include historical rates
|
||||
Limit: days,
|
||||
SortBy: "valid_from",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
|
||||
rates, err := s.repository.ListExchangeRates(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get exchange rate trends")
|
||||
return nil, fmt.Errorf("failed to get exchange rate trends: %w", err)
|
||||
}
|
||||
|
||||
return rates, nil
|
||||
}
|
||||
|
||||
// GetCurrencyUsageStats retrieves currency usage statistics
|
||||
func (s *AnalyticsServiceImpl) GetCurrencyUsageStats(ctx context.Context) (*currency.CurrencyUsageStats, error) {
|
||||
// Get total currencies
|
||||
totalCurrencies, err := s.repository.CountCurrencies(ctx, ¤cy.CurrencyFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count currencies: %w", err)
|
||||
}
|
||||
|
||||
// Get active currencies
|
||||
activeCurrencies, err := s.repository.CountCurrencies(ctx, ¤cy.CurrencyFilter{
|
||||
IsActive: &[]bool{true}[0],
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count active currencies: %w", err)
|
||||
}
|
||||
|
||||
// Get total transactions
|
||||
totalTransactions, err := s.repository.CountTransactions(ctx, ¤cy.TransactionFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count transactions: %w", err)
|
||||
}
|
||||
|
||||
// Get total volume
|
||||
transactions, err := s.repository.ListTransactions(ctx, ¤cy.TransactionFilter{
|
||||
Status: currency.TransactionStatusCompleted,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get transactions for volume: %w", err)
|
||||
}
|
||||
|
||||
totalVolume := 0.0
|
||||
currencyDistribution := make(map[string]int64)
|
||||
|
||||
for _, tx := range transactions {
|
||||
totalVolume += tx.BaseAmount
|
||||
currencyDistribution[tx.Currency]++
|
||||
}
|
||||
|
||||
// Find most used currency
|
||||
mostUsedCurrency := ""
|
||||
maxCount := int64(0)
|
||||
|
||||
for currency, count := range currencyDistribution {
|
||||
if count > maxCount {
|
||||
maxCount = count
|
||||
mostUsedCurrency = currency
|
||||
}
|
||||
}
|
||||
|
||||
// Get exchange rate count (using ListExchangeRates for now since CountExchangeRates doesn't exist)
|
||||
exchangeRates, err := s.repository.ListExchangeRates(ctx, ¤cy.ExchangeRateFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count exchange rates: %w", err)
|
||||
}
|
||||
|
||||
return ¤cy.CurrencyUsageStats{
|
||||
TotalCurrencies: totalCurrencies,
|
||||
ActiveCurrencies: activeCurrencies,
|
||||
TotalTransactions: int64(totalTransactions),
|
||||
TotalVolume: totalVolume,
|
||||
MostUsedCurrency: mostUsedCurrency,
|
||||
CurrencyDistribution: currencyDistribution,
|
||||
ExchangeRateCount: len(exchangeRates),
|
||||
LastUpdated: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetMonthlyRevenueTrends calculates monthly revenue trends
|
||||
func (s *AnalyticsServiceImpl) GetMonthlyRevenueTrends(ctx context.Context, months int) (map[string]float64, error) {
|
||||
endDate := time.Now()
|
||||
startDate := endDate.AddDate(0, -months, 0)
|
||||
|
||||
filter := ¤cy.TransactionFilter{
|
||||
Status: currency.TransactionStatusCompleted,
|
||||
FromDate: &startDate,
|
||||
ToDate: &endDate,
|
||||
}
|
||||
|
||||
transactions, err := s.repository.ListTransactions(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get transactions for monthly trends")
|
||||
return nil, fmt.Errorf("failed to get transactions: %w", err)
|
||||
}
|
||||
|
||||
monthlyRevenue := make(map[string]float64)
|
||||
|
||||
for _, tx := range transactions {
|
||||
monthKey := tx.CreatedAt.Format("2006-01")
|
||||
monthlyRevenue[monthKey] += tx.BaseAmount
|
||||
}
|
||||
|
||||
return monthlyRevenue, nil
|
||||
}
|
||||
|
||||
// GetTopCurrenciesByRevenue returns top currencies by revenue
|
||||
func (s *AnalyticsServiceImpl) GetTopCurrenciesByRevenue(ctx context.Context, limit int) ([]*currency.CurrencyRevenue, error) {
|
||||
revenueByCurrency, err := s.GetRevenueByCurrency(ctx, ¤cy.TransactionFilter{
|
||||
Status: currency.TransactionStatusCompleted,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to slice and sort
|
||||
var currencyRevenues []*currency.CurrencyRevenue
|
||||
for currCode, revenue := range revenueByCurrency {
|
||||
currencyRevenues = append(currencyRevenues, ¤cy.CurrencyRevenue{
|
||||
Currency: currCode,
|
||||
Revenue: revenue,
|
||||
})
|
||||
}
|
||||
|
||||
// Simple sort (in production, use proper sorting)
|
||||
if len(currencyRevenues) > limit {
|
||||
currencyRevenues = currencyRevenues[:limit]
|
||||
}
|
||||
|
||||
return currencyRevenues, nil
|
||||
}
|
||||
|
||||
// GetTransactionTypeAnalytics returns analytics by transaction type
|
||||
func (s *AnalyticsServiceImpl) GetTransactionTypeAnalytics(ctx context.Context, filter *currency.TransactionFilter) (map[string]*currency.TransactionTypeStats, error) {
|
||||
if filter == nil {
|
||||
filter = ¤cy.TransactionFilter{}
|
||||
}
|
||||
|
||||
transactions, err := s.repository.ListTransactions(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get transactions for type analytics")
|
||||
return nil, fmt.Errorf("failed to get transactions: %w", err)
|
||||
}
|
||||
|
||||
typeStats := make(map[string]*currency.TransactionTypeStats)
|
||||
|
||||
for _, tx := range transactions {
|
||||
typeKey := string(tx.Type)
|
||||
|
||||
if _, exists := typeStats[typeKey]; !exists {
|
||||
typeStats[typeKey] = ¤cy.TransactionTypeStats{
|
||||
Type: tx.Type,
|
||||
Count: 0,
|
||||
Amount: 0.0,
|
||||
Currency: tx.Currency,
|
||||
}
|
||||
}
|
||||
|
||||
stats := typeStats[typeKey]
|
||||
stats.Count++
|
||||
stats.Amount += tx.BaseAmount
|
||||
}
|
||||
|
||||
return typeStats, nil
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// BillingServiceImpl handles multi-currency billing operations
|
||||
type BillingServiceImpl struct {
|
||||
repository currency.Repository
|
||||
exchangeService currency.ExchangeRateService
|
||||
logger *logrus.Logger
|
||||
baseCurrency string
|
||||
}
|
||||
|
||||
// NewBillingService creates a new billing service
|
||||
func NewBillingService(repository currency.Repository, exchangeService currency.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 *currency.BillingRequest) (*currency.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 := ¤cy.Transaction{
|
||||
ID: uuid.New().String(),
|
||||
ProfileID: req.ProfileID,
|
||||
SubscriptionID: req.SubscriptionID,
|
||||
Type: currency.TransactionTypeSubscription,
|
||||
Amount: req.Amount,
|
||||
Currency: req.Currency,
|
||||
BaseAmount: baseAmount,
|
||||
BaseCurrency: s.baseCurrency,
|
||||
ExchangeRate: exchangeRate,
|
||||
Description: req.Description,
|
||||
Status: currency.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 = currency.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 ¤cy.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 *currency.CurrencyConversionRequest) (*currency.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 *currency.TransactionFilter) ([]*currency.Transaction, error) {
|
||||
if filter == nil {
|
||||
filter = ¤cy.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 *currency.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
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ExchangeRateService struct {
|
||||
repository currency.Repository
|
||||
logger *logrus.Logger
|
||||
providers []currency.ExchangeRateProvider
|
||||
baseCurrency string
|
||||
}
|
||||
|
||||
func NewExchangeRateService(repository currency.Repository, logger *logrus.Logger, baseCurrency string) *ExchangeRateService {
|
||||
return &ExchangeRateService{
|
||||
repository: repository,
|
||||
logger: logger,
|
||||
providers: make([]currency.ExchangeRateProvider, 0),
|
||||
baseCurrency: baseCurrency,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) AddProvider(provider currency.ExchangeRateProvider) {
|
||||
s.providers = append(s.providers, provider)
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) GetExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*currency.ExchangeRate, error) {
|
||||
if fromCurrency == toCurrency {
|
||||
return ¤cy.ExchangeRate{
|
||||
FromCurrency: fromCurrency,
|
||||
ToCurrency: toCurrency,
|
||||
Rate: 1.0,
|
||||
Source: "direct",
|
||||
ValidFrom: time.Now(),
|
||||
IsActive: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
rate, err := s.repository.GetLatestExchangeRate(ctx, fromCurrency, toCurrency)
|
||||
if err == nil {
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
for _, provider := range s.providers {
|
||||
providerRate, err := provider.GetRate(ctx, fromCurrency, toCurrency)
|
||||
if err == nil {
|
||||
newRate := ¤cy.ExchangeRate{
|
||||
ID: fmt.Sprintf("%s_%s_%d", fromCurrency, toCurrency, time.Now().Unix()),
|
||||
FromCurrency: fromCurrency,
|
||||
ToCurrency: toCurrency,
|
||||
Rate: providerRate,
|
||||
Source: "provider",
|
||||
ValidFrom: time.Now(),
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.repository.CreateExchangeRate(ctx, newRate); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to save exchange rate")
|
||||
}
|
||||
|
||||
return newRate, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("exchange rate not found: %s to %s", fromCurrency, toCurrency)
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) ConvertAmount(ctx context.Context, amount float64, fromCurrency, toCurrency string) (*currency.CurrencyConversionResponse, error) {
|
||||
rate, err := s.GetExchangeRate(ctx, fromCurrency, toCurrency)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get exchange rate: %w", err)
|
||||
}
|
||||
|
||||
convertedAmount := amount * rate.Rate
|
||||
|
||||
return ¤cy.CurrencyConversionResponse{
|
||||
OriginalAmount: amount,
|
||||
OriginalCurrency: fromCurrency,
|
||||
ConvertedAmount: convertedAmount,
|
||||
ConvertedCurrency: toCurrency,
|
||||
ExchangeRate: rate.Rate,
|
||||
ConvertedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) RefreshRates(ctx context.Context) error {
|
||||
s.logger.Info("Refreshing exchange rates")
|
||||
|
||||
for _, provider := range s.providers {
|
||||
if err := provider.RefreshRates(ctx); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to refresh rates from provider")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Exchange rates refreshed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) GetRateHistory(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*currency.ExchangeRate, error) {
|
||||
filter := ¤cy.ExchangeRateFilter{
|
||||
FromCurrency: fromCurrency,
|
||||
ToCurrency: toCurrency,
|
||||
IsValid: &[]bool{false}[0], // Include historical rates
|
||||
Limit: days,
|
||||
SortBy: "valid_from",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
|
||||
rates, err := s.repository.ListExchangeRates(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rate history: %w", err)
|
||||
}
|
||||
|
||||
return rates, nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) UpdateExchangeRate(ctx context.Context, rate *currency.ExchangeRate) error {
|
||||
if rate.Rate <= 0 {
|
||||
return fmt.Errorf("invalid exchange rate: must be positive")
|
||||
}
|
||||
|
||||
if rate.FromCurrency == rate.ToCurrency {
|
||||
return fmt.Errorf("invalid currency pair: from and to currencies cannot be the same")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
rate.ValidFrom = now
|
||||
rate.IsActive = true
|
||||
|
||||
filter := ¤cy.ExchangeRateFilter{
|
||||
FromCurrency: rate.FromCurrency,
|
||||
ToCurrency: rate.ToCurrency,
|
||||
IsValid: &[]bool{true}[0],
|
||||
}
|
||||
|
||||
oldRates, err := s.repository.ListExchangeRates(ctx, filter)
|
||||
if err == nil {
|
||||
for _, oldRate := range oldRates {
|
||||
oldRate.IsActive = false
|
||||
if err := s.repository.UpdateExchangeRate(ctx, oldRate); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to deactivate old exchange rate")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rate.ID = fmt.Sprintf("%s_%s_%d", rate.FromCurrency, rate.ToCurrency, time.Now().Unix())
|
||||
if err := s.repository.CreateExchangeRate(ctx, rate); err != nil {
|
||||
return fmt.Errorf("failed to create exchange rate: %w", err)
|
||||
}
|
||||
|
||||
s.logger.WithFields(logrus.Fields{
|
||||
"from_currency": rate.FromCurrency,
|
||||
"to_currency": rate.ToCurrency,
|
||||
"rate": rate.Rate,
|
||||
"source": rate.Source,
|
||||
}).Info("Exchange rate updated")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) GetSupportedCurrencies(ctx context.Context) ([]*currency.Currency, error) {
|
||||
filter := ¤cy.CurrencyFilter{
|
||||
IsActive: &[]bool{true}[0],
|
||||
}
|
||||
|
||||
currencies, err := s.repository.ListCurrencies(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get supported currencies: %w", err)
|
||||
}
|
||||
|
||||
return currencies, nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) ValidateCurrencyPair(ctx context.Context, fromCurrency, toCurrency string) error {
|
||||
_, err := s.repository.GetCurrency(ctx, fromCurrency)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unsupported from currency: %s", fromCurrency)
|
||||
}
|
||||
|
||||
_, err = s.repository.GetCurrency(ctx, toCurrency)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unsupported to currency: %s", toCurrency)
|
||||
}
|
||||
|
||||
_, err = s.GetExchangeRate(ctx, fromCurrency, toCurrency)
|
||||
if err != nil {
|
||||
return fmt.Errorf("no exchange rate available: %s to %s", fromCurrency, toCurrency)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
|
||||
)
|
||||
|
||||
// RatePlanCurrencyIntegrator integrates currency system with rate plans
|
||||
type RatePlanCurrencyIntegrator struct {
|
||||
billingService currency.BillingService
|
||||
exchangeService *ExchangeRateService
|
||||
ratePlanService rateplan.Service
|
||||
logger *logrus.Logger
|
||||
baseCurrency string
|
||||
}
|
||||
|
||||
// NewRatePlanCurrencyIntegrator creates a new rate plan currency integrator
|
||||
func NewRatePlanCurrencyIntegrator(
|
||||
billingService currency.BillingService,
|
||||
exchangeService *ExchangeRateService,
|
||||
ratePlanService rateplan.Service,
|
||||
logger *logrus.Logger,
|
||||
baseCurrency string,
|
||||
) *RatePlanCurrencyIntegrator {
|
||||
return &RatePlanCurrencyIntegrator{
|
||||
billingService: billingService,
|
||||
exchangeService: exchangeService,
|
||||
ratePlanService: ratePlanService,
|
||||
logger: logger,
|
||||
baseCurrency: baseCurrency,
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeToPlanWithCurrency subscribes to a rate plan with currency conversion
|
||||
func (rpci *RatePlanCurrencyIntegrator) SubscribeToPlanWithCurrency(ctx context.Context, profileID string, planID string, targetCurrency string) (*rateplan.RatePlanSubscription, error) {
|
||||
// Get the rate plan
|
||||
plan, err := rpci.ratePlanService.GetRatePlan(ctx, planID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rate plan: %w", err)
|
||||
}
|
||||
|
||||
// Convert price to requested currency if needed
|
||||
subscriptionPrice := plan.BasePrice
|
||||
exchangeRate := 1.0
|
||||
|
||||
if targetCurrency != plan.Currency {
|
||||
conversion, err := rpci.exchangeService.ConvertAmount(ctx, plan.BasePrice, plan.Currency, targetCurrency)
|
||||
if err != nil {
|
||||
rpci.logger.WithError(err).Error("Failed to convert rate plan price")
|
||||
return nil, fmt.Errorf("currency conversion failed: %w", err)
|
||||
}
|
||||
subscriptionPrice = conversion.ConvertedAmount
|
||||
exchangeRate = conversion.ExchangeRate
|
||||
}
|
||||
|
||||
// Create subscription with currency information
|
||||
subscription := &rateplan.RatePlanSubscription{
|
||||
ProfileID: profileID,
|
||||
RatePlanID: planID,
|
||||
Status: rateplan.SubscriptionStatusActive,
|
||||
StartedAt: time.Now(),
|
||||
Metadata: map[string]interface{}{
|
||||
"original_currency": plan.Currency,
|
||||
"subscription_currency": targetCurrency,
|
||||
"original_price": plan.BasePrice,
|
||||
"subscription_price": subscriptionPrice,
|
||||
"exchange_rate": exchangeRate,
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Create subscription request
|
||||
subscribeReq := &rateplan.SubscribeRequest{
|
||||
ProfileID: profileID,
|
||||
RatePlanID: planID,
|
||||
AutoRenew: true,
|
||||
Metadata: subscription.Metadata,
|
||||
}
|
||||
|
||||
createdSubscription, err := rpci.ratePlanService.SubscribeToPlan(ctx, subscribeReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create subscription: %w", err)
|
||||
}
|
||||
|
||||
// Process initial billing
|
||||
billingReq := ¤cy.BillingRequest{
|
||||
ProfileID: profileID,
|
||||
SubscriptionID: createdSubscription.ID,
|
||||
Amount: subscriptionPrice,
|
||||
Currency: targetCurrency,
|
||||
Description: fmt.Sprintf("Initial subscription to %s", plan.Name),
|
||||
BillingDate: time.Now(),
|
||||
}
|
||||
|
||||
_, err = rpci.billingService.ProcessBilling(ctx, billingReq)
|
||||
if err != nil {
|
||||
rpci.logger.WithError(err).Error("Failed to process initial billing")
|
||||
// Don't fail the subscription if billing fails, but log it
|
||||
}
|
||||
|
||||
rpci.logger.WithFields(logrus.Fields{
|
||||
"profile_id": profileID,
|
||||
"plan_id": planID,
|
||||
"currency": targetCurrency,
|
||||
"subscription_id": createdSubscription.ID,
|
||||
}).Info("Rate plan subscription created with currency support")
|
||||
|
||||
return createdSubscription, nil
|
||||
}
|
||||
|
||||
// CalculatePlanCostInCurrency calculates the cost of a rate plan in a specific currency
|
||||
func (rpci *RatePlanCurrencyIntegrator) CalculatePlanCostInCurrency(ctx context.Context, planID string, targetCurrency string, usageData *rateplan.RatePlanUsage) (*currency.BillingSummary, error) {
|
||||
// Get the rate plan
|
||||
plan, err := rpci.ratePlanService.GetRatePlan(ctx, planID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rate plan: %w", err)
|
||||
}
|
||||
|
||||
// Calculate base cost
|
||||
baseCost := plan.BasePrice
|
||||
|
||||
// Add overage costs if usage data is provided
|
||||
if usageData != nil {
|
||||
overageCost, err := rpci.calculateOverageCost(ctx, plan, usageData)
|
||||
if err != nil {
|
||||
rpci.logger.WithError(err).Warn("Failed to calculate overage cost")
|
||||
} else {
|
||||
baseCost += overageCost
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to requested currency
|
||||
convertedCost := baseCost
|
||||
exchangeRate := 1.0
|
||||
|
||||
if targetCurrency != plan.Currency {
|
||||
conversion, err := rpci.exchangeService.ConvertAmount(ctx, baseCost, plan.Currency, targetCurrency)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("currency conversion failed: %w", err)
|
||||
}
|
||||
convertedCost = conversion.ConvertedAmount
|
||||
exchangeRate = conversion.ExchangeRate
|
||||
}
|
||||
|
||||
// Create billing summary
|
||||
summary := ¤cy.BillingSummary{
|
||||
ProfileID: usageData.ProfileID,
|
||||
TotalAmount: convertedCost,
|
||||
Currency: targetCurrency,
|
||||
BaseTotalAmount: baseCost,
|
||||
BaseCurrency: plan.Currency,
|
||||
TransactionCount: 1,
|
||||
FromDate: time.Now().AddDate(0, -1, 0),
|
||||
ToDate: time.Now(),
|
||||
Breakdown: map[string]interface{}{
|
||||
"plan_id": planID,
|
||||
"plan_name": plan.Name,
|
||||
"base_cost": plan.BasePrice,
|
||||
"overage_cost": baseCost - plan.BasePrice,
|
||||
"exchange_rate": exchangeRate,
|
||||
"original_currency": plan.Currency,
|
||||
},
|
||||
}
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/rateplan"
|
||||
)
|
||||
|
||||
// GetPlansInCurrency gets rate plans with prices converted to a specific currency
|
||||
func (rpci *RatePlanCurrencyIntegrator) GetPlansInCurrency(ctx context.Context, filter *rateplan.RatePlanFilter, targetCurrency string) ([]*rateplan.RatePlan, error) {
|
||||
// Get plans using original filter
|
||||
plans, err := rpci.ratePlanService.ListRatePlans(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rate plans: %w", err)
|
||||
}
|
||||
|
||||
// Convert prices to target currency
|
||||
for _, plan := range plans {
|
||||
if plan.Currency != targetCurrency {
|
||||
conversion, err := rpci.exchangeService.ConvertAmount(ctx, plan.BasePrice, plan.Currency, targetCurrency)
|
||||
if err != nil {
|
||||
rpci.logger.WithError(err).WithFields(logrus.Fields{
|
||||
"plan_id": plan.ID,
|
||||
"from_currency": plan.Currency,
|
||||
"to_currency": targetCurrency,
|
||||
}).Warn("Failed to convert plan price")
|
||||
continue
|
||||
}
|
||||
|
||||
// Store original price and update with converted price
|
||||
if plan.Metadata == nil {
|
||||
plan.Metadata = make(map[string]interface{})
|
||||
}
|
||||
plan.Metadata["original_price"] = plan.BasePrice
|
||||
plan.Metadata["original_currency"] = plan.Currency
|
||||
plan.Metadata["exchange_rate"] = conversion.ExchangeRate
|
||||
plan.BasePrice = conversion.ConvertedAmount
|
||||
plan.Currency = targetCurrency
|
||||
}
|
||||
}
|
||||
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
// UpdatePlanCurrency updates a rate plan's currency and converts prices
|
||||
func (rpci *RatePlanCurrencyIntegrator) UpdatePlanCurrency(ctx context.Context, planID string, newCurrency string) error {
|
||||
// Get the current plan
|
||||
plan, err := rpci.ratePlanService.GetRatePlan(ctx, planID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get rate plan: %w", err)
|
||||
}
|
||||
|
||||
// If currency is the same, no conversion needed
|
||||
if plan.Currency == newCurrency {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert all monetary values to new currency
|
||||
convertedPrice, err := rpci.exchangeService.ConvertAmount(ctx, plan.BasePrice, plan.Currency, newCurrency)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert base price: %w", err)
|
||||
}
|
||||
|
||||
// Update plan with new currency and converted prices
|
||||
plan.BasePrice = convertedPrice.ConvertedAmount
|
||||
plan.Currency = newCurrency
|
||||
|
||||
// Store conversion information in metadata
|
||||
if plan.Metadata == nil {
|
||||
plan.Metadata = make(map[string]interface{})
|
||||
}
|
||||
plan.Metadata["currency_conversion"] = map[string]interface{}{
|
||||
"from_currency": plan.Metadata["original_currency"],
|
||||
"to_currency": newCurrency,
|
||||
"exchange_rate": convertedPrice.ExchangeRate,
|
||||
"converted_at": time.Now(),
|
||||
}
|
||||
|
||||
// Update the plan
|
||||
updatedPlan, err := rpci.ratePlanService.UpdateRatePlan(ctx, plan)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update rate plan: %w", err)
|
||||
}
|
||||
|
||||
// Use the updated plan for logging
|
||||
plan = updatedPlan
|
||||
|
||||
rpci.logger.WithFields(logrus.Fields{
|
||||
"plan_id": planID,
|
||||
"from_currency": plan.Metadata["original_currency"],
|
||||
"to_currency": newCurrency,
|
||||
"exchange_rate": convertedPrice.ExchangeRate,
|
||||
}).Info("Rate plan currency updated")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateOverageCost calculates overage costs for usage
|
||||
func (rpci *RatePlanCurrencyIntegrator) calculateOverageCost(ctx context.Context, plan *rateplan.RatePlan, usage *rateplan.RatePlanUsage) (float64, error) {
|
||||
overageCost := 0.0
|
||||
|
||||
// Calculate data overage
|
||||
if plan.DataAllowance != nil && usage.DataUsed > plan.DataAllowance.Amount {
|
||||
dataOverage := usage.DataUsed - plan.DataAllowance.Amount
|
||||
if plan.OverageRates != nil {
|
||||
overageCost += float64(dataOverage) * plan.OverageRates.DataRate
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate voice overage
|
||||
if plan.VoiceAllowance != nil && usage.VoiceUsed > plan.VoiceAllowance.Minutes {
|
||||
voiceOverage := usage.VoiceUsed - plan.VoiceAllowance.Minutes
|
||||
if plan.OverageRates != nil {
|
||||
overageCost += float64(voiceOverage) * plan.OverageRates.VoiceRate
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate SMS overage
|
||||
if plan.SMSAllowance != nil && usage.SMSUsed > plan.SMSAllowance.Messages {
|
||||
smsOverage := usage.SMSUsed - plan.SMSAllowance.Messages
|
||||
if plan.OverageRates != nil {
|
||||
overageCost += float64(smsOverage) * plan.OverageRates.SMSRate
|
||||
}
|
||||
}
|
||||
|
||||
return overageCost, nil
|
||||
}
|
||||
|
||||
// GetCurrencyUsageForPlan gets currency usage statistics for a specific rate plan
|
||||
func (rpci *RatePlanCurrencyIntegrator) GetCurrencyUsageForPlan(ctx context.Context, planID string) (map[string]int64, error) {
|
||||
// Get all subscriptions for this plan
|
||||
filter := &rateplan.SubscriptionFilter{
|
||||
RatePlanID: planID,
|
||||
Status: rateplan.SubscriptionStatusActive,
|
||||
}
|
||||
|
||||
subscriptions, err := rpci.ratePlanService.ListSubscriptions(ctx, "", filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get subscriptions: %w", err)
|
||||
}
|
||||
|
||||
// Count currencies
|
||||
currencyUsage := make(map[string]int64)
|
||||
|
||||
for _, subscription := range subscriptions {
|
||||
currency := rpci.baseCurrency // Default to base currency
|
||||
if subscription.Metadata != nil {
|
||||
if subCurrency, exists := subscription.Metadata["subscription_currency"]; exists {
|
||||
if subCurrencyStr, ok := subCurrency.(string); ok {
|
||||
currency = subCurrencyStr
|
||||
}
|
||||
}
|
||||
}
|
||||
currencyUsage[currency]++
|
||||
}
|
||||
|
||||
return currencyUsage, nil
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
+18
-16
@@ -1,4 +1,4 @@
|
||||
package rateplan
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,24 +6,25 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Service provides business logic for rate plan operations
|
||||
type Service struct {
|
||||
repo Repository
|
||||
repo repository.Repository
|
||||
logger *logrus.Logger
|
||||
}
|
||||
|
||||
// NewService creates a new rate plan service
|
||||
func NewService(repo Repository, logger *logrus.Logger) *Service {
|
||||
func NewService(repo repository.Repository, logger *logrus.Logger) *Service {
|
||||
return &Service{
|
||||
repo: repo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) CreateRatePlan(ctx context.Context, plan *RatePlan) (*RatePlan, error) {
|
||||
func (s *Service) CreateRatePlan(ctx context.Context, plan *repository.RatePlan) (*repository.RatePlan, error) {
|
||||
// Generate ID if not provided
|
||||
if plan.ID == "" {
|
||||
plan.ID = uuid.New().String()
|
||||
@@ -45,7 +46,7 @@ func (s *Service) CreateRatePlan(ctx context.Context, plan *RatePlan) (*RatePlan
|
||||
}
|
||||
|
||||
// GetRatePlan retrieves a rate plan by ID
|
||||
func (s *Service) GetRatePlan(ctx context.Context, id string) (*RatePlan, error) {
|
||||
func (s *Service) GetRatePlan(ctx context.Context, id string) (*repository.RatePlan, error) {
|
||||
plan, err := s.repo.GetRatePlan(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).WithField("plan_id", id).Error("Failed to get rate plan")
|
||||
@@ -56,7 +57,7 @@ func (s *Service) GetRatePlan(ctx context.Context, id string) (*RatePlan, error)
|
||||
}
|
||||
|
||||
// UpdateRatePlan updates an existing rate plan
|
||||
func (s *Service) UpdateRatePlan(ctx context.Context, plan *RatePlan) (*RatePlan, error) {
|
||||
func (s *Service) UpdateRatePlan(ctx context.Context, plan *repository.RatePlan) (*repository.RatePlan, error) {
|
||||
// Validate rate plan
|
||||
if err := s.validateRatePlan(plan); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
@@ -81,9 +82,9 @@ func (s *Service) UpdateRatePlan(ctx context.Context, plan *RatePlan) (*RatePlan
|
||||
// DeleteRatePlan deletes a rate plan
|
||||
func (s *Service) DeleteRatePlan(ctx context.Context, id string) error {
|
||||
// Check if plan has active subscriptions
|
||||
subscriptions, err := s.repo.ListSubscriptions(ctx, "", &SubscriptionFilter{
|
||||
subscriptions, err := s.repo.ListSubscriptions(ctx, "", &repository.SubscriptionFilter{
|
||||
RatePlanID: id,
|
||||
Status: SubscriptionStatusActive,
|
||||
Status: repository.SubscriptionStatusActive,
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -105,7 +106,7 @@ func (s *Service) DeleteRatePlan(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
// ListRatePlans retrieves rate plans with filtering
|
||||
func (s *Service) ListRatePlans(ctx context.Context, filter *RatePlanFilter) ([]*RatePlan, error) {
|
||||
func (s *Service) ListRatePlans(ctx context.Context, filter *repository.RatePlanFilter) ([]*repository.RatePlan, error) {
|
||||
plans, err := s.repo.ListRatePlans(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to list rate plans")
|
||||
@@ -116,12 +117,12 @@ func (s *Service) ListRatePlans(ctx context.Context, filter *RatePlanFilter) ([]
|
||||
}
|
||||
|
||||
// SearchRatePlans searches for rate plans based on criteria
|
||||
func (s *Service) SearchRatePlans(ctx context.Context, criteria SearchCriteria) ([]*RatePlan, error) {
|
||||
filter := &RatePlanFilter{
|
||||
func (s *Service) SearchRatePlans(ctx context.Context, criteria repository.SearchCriteria) ([]*repository.RatePlan, error) {
|
||||
filter := &repository.RatePlanFilter{
|
||||
CarrierID: criteria.CarrierID,
|
||||
Region: criteria.Region,
|
||||
PlanType: criteria.PlanType,
|
||||
Status: PlanStatusActive,
|
||||
Status: repository.PlanStatusActive,
|
||||
IsActive: &[]bool{true}[0],
|
||||
MinPrice: criteria.MinPrice,
|
||||
MaxPrice: criteria.MaxPrice,
|
||||
@@ -133,8 +134,9 @@ func (s *Service) SearchRatePlans(ctx context.Context, criteria SearchCriteria)
|
||||
|
||||
return s.repo.ListRatePlans(ctx, filter)
|
||||
}
|
||||
|
||||
// CalculateCost calculates the cost for a rate plan based on usage
|
||||
func (s *Service) CalculateCost(ctx context.Context, req *CalculateCostRequest) (*RatePlanCostCalculation, error) {
|
||||
func (s *Service) CalculateCost(ctx context.Context, req *repository.CalculateCostRequest) (*repository.RatePlanCostCalculation, error) {
|
||||
// Get the rate plan
|
||||
plan, err := s.repo.GetRatePlan(ctx, req.RatePlanID)
|
||||
if err != nil {
|
||||
@@ -166,9 +168,9 @@ func (s *Service) CalculateCost(ctx context.Context, req *CalculateCostRequest)
|
||||
for _, discount := range plan.Discounts {
|
||||
if discount.ID == discountID && discount.IsActive {
|
||||
switch discount.Type {
|
||||
case DiscountTypePercentage:
|
||||
case repository.DiscountTypePercentage:
|
||||
discountCost += baseCost * discount.Value / 100
|
||||
case DiscountTypeFixed:
|
||||
case repository.DiscountTypeFixed:
|
||||
discountCost += discount.Value
|
||||
}
|
||||
}
|
||||
@@ -178,7 +180,7 @@ func (s *Service) CalculateCost(ctx context.Context, req *CalculateCostRequest)
|
||||
|
||||
totalCost := baseCost + overageCost - discountCost
|
||||
|
||||
calculation := &RatePlanCostCalculation{
|
||||
calculation := &repository.RatePlanCostCalculation{
|
||||
RatePlanID: req.RatePlanID,
|
||||
BaseCost: baseCost,
|
||||
OverageCost: overageCost,
|
||||
+9
-6
@@ -1,11 +1,13 @@
|
||||
package rateplan
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
|
||||
)
|
||||
|
||||
// GetUsage retrieves usage for a profile
|
||||
func (s *Service) GetUsage(ctx context.Context, profileID string) (*RatePlanUsage, error) {
|
||||
func (s *Service) GetUsage(ctx context.Context, profileID string) (*repository.RatePlanUsage, error) {
|
||||
usage, err := s.repo.GetCurrentUsage(ctx, profileID)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).WithField("profile_id", profileID).Error("Failed to get usage")
|
||||
@@ -14,8 +16,9 @@ func (s *Service) GetUsage(ctx context.Context, profileID string) (*RatePlanUsag
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
|
||||
// GetUsageHistory retrieves usage history for a profile
|
||||
func (s *Service) GetUsageHistory(ctx context.Context, profileID string, limit int) ([]*RatePlanUsage, error) {
|
||||
func (s *Service) GetUsageHistory(ctx context.Context, profileID string, limit int) ([]*repository.RatePlanUsage, error) {
|
||||
usageHistory, err := s.repo.ListUsageHistory(ctx, profileID, limit)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get usage history")
|
||||
@@ -26,7 +29,7 @@ func (s *Service) GetUsageHistory(ctx context.Context, profileID string, limit i
|
||||
}
|
||||
|
||||
// GetUsageAnalytics retrieves usage analytics
|
||||
func (s *Service) GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsFilter) (*UsageAnalytics, error) {
|
||||
func (s *Service) GetUsageAnalytics(ctx context.Context, filter *repository.UsageAnalyticsFilter) (*repository.UsageAnalytics, error) {
|
||||
analytics, err := s.repo.GetUsageAnalytics(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get usage analytics")
|
||||
@@ -37,7 +40,7 @@ func (s *Service) GetUsageAnalytics(ctx context.Context, filter *UsageAnalyticsF
|
||||
}
|
||||
|
||||
// GetRevenueAnalytics retrieves revenue analytics
|
||||
func (s *Service) GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyticsFilter) (*RevenueAnalytics, error) {
|
||||
func (s *Service) GetRevenueAnalytics(ctx context.Context, filter *repository.RevenueAnalyticsFilter) (*repository.RevenueAnalytics, error) {
|
||||
analytics, err := s.repo.GetRevenueAnalytics(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get revenue analytics")
|
||||
@@ -48,7 +51,7 @@ func (s *Service) GetRevenueAnalytics(ctx context.Context, filter *RevenueAnalyt
|
||||
}
|
||||
|
||||
// GetPopularPlans retrieves the most popular rate plans
|
||||
func (s *Service) GetPopularPlans(ctx context.Context, limit int) ([]*RatePlan, error) {
|
||||
func (s *Service) GetPopularPlans(ctx context.Context, limit int) ([]*repository.RatePlan, error) {
|
||||
plans, err := s.repo.GetPopularPlans(ctx, limit)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get popular plans")
|
||||
+17
-16
@@ -1,11 +1,13 @@
|
||||
package rateplan
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
|
||||
)
|
||||
|
||||
func (s *Service) validateRatePlan(plan *RatePlan) error {
|
||||
func (s *Service) validateRatePlan(plan *repository.RatePlan) error {
|
||||
if plan.Name == "" {
|
||||
return fmt.Errorf("rate plan name is required")
|
||||
}
|
||||
@@ -30,7 +32,7 @@ func (s *Service) validateRatePlan(plan *RatePlan) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) validateSubscribeRequest(req *SubscribeRequest) error {
|
||||
func (s *Service) validateSubscribeRequest(req *repository.SubscribeRequest) error {
|
||||
if req.ProfileID == "" {
|
||||
return fmt.Errorf("profile ID is required")
|
||||
}
|
||||
@@ -40,37 +42,36 @@ func (s *Service) validateSubscribeRequest(req *SubscribeRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) calculateNextBillingDate(cycle BillingCycle, from time.Time) time.Time {
|
||||
func (s *Service) calculateNextBillingDate(cycle repository.BillingCycle, from time.Time) time.Time {
|
||||
switch cycle {
|
||||
case BillingCycleDaily:
|
||||
case repository.BillingCycleDaily:
|
||||
return from.AddDate(0, 0, 1)
|
||||
case BillingCycleWeekly:
|
||||
case repository.BillingCycleWeekly:
|
||||
return from.AddDate(0, 0, 7)
|
||||
case BillingCycleMonthly:
|
||||
case repository.BillingCycleMonthly:
|
||||
return from.AddDate(0, 1, 0)
|
||||
case BillingCycleQuarterly:
|
||||
case repository.BillingCycleQuarterly:
|
||||
return from.AddDate(0, 3, 0)
|
||||
case BillingCycleYearly:
|
||||
case repository.BillingCycleYearly:
|
||||
return from.AddDate(1, 0, 0)
|
||||
default:
|
||||
return from.AddDate(0, 1, 0) // Default to monthly
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) calculateCycleEnd(cycle BillingCycle, cycleStart time.Time) time.Time {
|
||||
func (s *Service) calculateCycleEnd(cycle repository.BillingCycle, cycleStart time.Time) time.Time {
|
||||
switch cycle {
|
||||
case BillingCycleDaily:
|
||||
case repository.BillingCycleDaily:
|
||||
return cycleStart.AddDate(0, 0, 1).Add(-time.Nanosecond)
|
||||
case BillingCycleWeekly:
|
||||
case repository.BillingCycleWeekly:
|
||||
return cycleStart.AddDate(0, 0, 7).Add(-time.Nanosecond)
|
||||
case BillingCycleMonthly:
|
||||
case repository.BillingCycleMonthly:
|
||||
return cycleStart.AddDate(0, 1, 0).Add(-time.Nanosecond)
|
||||
case BillingCycleQuarterly:
|
||||
case repository.BillingCycleQuarterly:
|
||||
return cycleStart.AddDate(0, 3, 0).Add(-time.Nanosecond)
|
||||
case BillingCycleYearly:
|
||||
case repository.BillingCycleYearly:
|
||||
return cycleStart.AddDate(1, 0, 0).Add(-time.Nanosecond)
|
||||
default:
|
||||
return cycleStart.AddDate(0, 1, 0).Add(-time.Nanosecond) // Default to monthly
|
||||
}
|
||||
}
|
||||
|
||||
+15
-14
@@ -1,4 +1,4 @@
|
||||
package rateplan
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s *Service) SubscribeToPlan(ctx context.Context, req *SubscribeRequest) (*RatePlanSubscription, error) {
|
||||
func (s *Service) SubscribeToPlan(ctx context.Context, req *repository.SubscribeRequest) (*repository.RatePlanSubscription, error) {
|
||||
if err := s.validateSubscribeRequest(req); err != nil {
|
||||
return nil, fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
@@ -19,7 +20,7 @@ func (s *Service) SubscribeToPlan(ctx context.Context, req *SubscribeRequest) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !plan.IsActive || plan.Status != PlanStatusActive {
|
||||
if !plan.IsActive || plan.Status != repository.PlanStatusActive {
|
||||
return nil, fmt.Errorf("rate plan is not available for subscription")
|
||||
}
|
||||
|
||||
@@ -33,11 +34,11 @@ func (s *Service) SubscribeToPlan(ctx context.Context, req *SubscribeRequest) (*
|
||||
}
|
||||
|
||||
// Create subscription
|
||||
subscription := &RatePlanSubscription{
|
||||
subscription := &repository.RatePlanSubscription{
|
||||
ID: uuid.New().String(),
|
||||
ProfileID: req.ProfileID,
|
||||
RatePlanID: req.RatePlanID,
|
||||
Status: SubscriptionStatusActive,
|
||||
Status: repository.SubscriptionStatusActive,
|
||||
StartedAt: time.Now(),
|
||||
BillingCycle: plan.BillingCycle,
|
||||
NextBillingDate: s.calculateNextBillingDate(plan.BillingCycle, time.Now()),
|
||||
@@ -62,7 +63,7 @@ func (s *Service) SubscribeToPlan(ctx context.Context, req *SubscribeRequest) (*
|
||||
}
|
||||
|
||||
// GetSubscription retrieves a subscription by ID
|
||||
func (s *Service) GetSubscription(ctx context.Context, id string) (*RatePlanSubscription, error) {
|
||||
func (s *Service) GetSubscription(ctx context.Context, id string) (*repository.RatePlanSubscription, error) {
|
||||
subscription, err := s.repo.GetSubscription(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).WithField("subscription_id", id).Error("Failed to get subscription")
|
||||
@@ -73,7 +74,7 @@ func (s *Service) GetSubscription(ctx context.Context, id string) (*RatePlanSubs
|
||||
}
|
||||
|
||||
// UpdateSubscription updates an existing subscription
|
||||
func (s *Service) UpdateSubscription(ctx context.Context, subscription *RatePlanSubscription) (*RatePlanSubscription, error) {
|
||||
func (s *Service) UpdateSubscription(ctx context.Context, subscription *repository.RatePlanSubscription) (*repository.RatePlanSubscription, error) {
|
||||
if err := s.repo.UpdateSubscription(ctx, subscription); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to update subscription")
|
||||
return nil, err
|
||||
@@ -90,12 +91,12 @@ func (s *Service) CancelSubscription(ctx context.Context, subscriptionID string,
|
||||
return err
|
||||
}
|
||||
|
||||
if subscription.Status != SubscriptionStatusActive {
|
||||
if subscription.Status != repository.SubscriptionStatusActive {
|
||||
return fmt.Errorf("subscription is not active")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
subscription.Status = SubscriptionStatusCancelled
|
||||
subscription.Status = repository.SubscriptionStatusCancelled
|
||||
subscription.EndedAt = &now
|
||||
subscription.UpdatedAt = now
|
||||
|
||||
@@ -114,7 +115,7 @@ func (s *Service) CancelSubscription(ctx context.Context, subscriptionID string,
|
||||
}
|
||||
|
||||
// GetActiveSubscription retrieves the active subscription for a profile
|
||||
func (s *Service) GetActiveSubscription(ctx context.Context, profileID string) (*RatePlanSubscription, error) {
|
||||
func (s *Service) GetActiveSubscription(ctx context.Context, profileID string) (*repository.RatePlanSubscription, error) {
|
||||
subscription, err := s.repo.GetActiveSubscription(ctx, profileID)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).WithField("profile_id", profileID).Error("Failed to get active subscription")
|
||||
@@ -125,7 +126,7 @@ func (s *Service) GetActiveSubscription(ctx context.Context, profileID string) (
|
||||
}
|
||||
|
||||
// ListSubscriptions retrieves subscriptions for a profile
|
||||
func (s *Service) ListSubscriptions(ctx context.Context, profileID string, filter *SubscriptionFilter) ([]*RatePlanSubscription, error) {
|
||||
func (s *Service) ListSubscriptions(ctx context.Context, profileID string, filter *repository.SubscriptionFilter) ([]*repository.RatePlanSubscription, error) {
|
||||
subscriptions, err := s.repo.ListSubscriptions(ctx, profileID, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to list subscriptions")
|
||||
@@ -135,7 +136,7 @@ func (s *Service) ListSubscriptions(ctx context.Context, profileID string, filte
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
func (s *Service) RecordUsage(ctx context.Context, req *RecordUsageRequest) (*RatePlanUsage, error) {
|
||||
func (s *Service) RecordUsage(ctx context.Context, req *repository.RecordUsageRequest) (*repository.RatePlanUsage, error) {
|
||||
// Get active subscription
|
||||
subscription, err := s.repo.GetActiveSubscription(ctx, req.ProfileID)
|
||||
if err != nil {
|
||||
@@ -153,10 +154,10 @@ func (s *Service) RecordUsage(ctx context.Context, req *RecordUsageRequest) (*Ra
|
||||
}
|
||||
|
||||
// Create or update usage record
|
||||
var usage *RatePlanUsage
|
||||
var usage *repository.RatePlanUsage
|
||||
if currentUsage == nil {
|
||||
// Create new usage record
|
||||
usage = &RatePlanUsage{
|
||||
usage = &repository.RatePlanUsage{
|
||||
ID: uuid.New().String(),
|
||||
RatePlanID: subscription.RatePlanID,
|
||||
ProfileID: req.ProfileID,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
Reference in New Issue
Block a user