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:
Nutcase
2026-05-02 01:28:15 +03:00
committed by GitHub
parent 488c1767e0
commit 7614476e4f
29 changed files with 2730 additions and 84 deletions
@@ -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 := &currency.TransactionFilter{}
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil {
filter.Limit = limit
}
}
if offsetStr := c.Query("offset"); offsetStr != "" {
if offset, err := strconv.Atoi(offsetStr); err == nil {
filter.Offset = offset
}
}
if fromDateStr := c.Query("from_date"); fromDateStr != "" {
if fromDate, err := time.Parse("2006-01-02", fromDateStr); err == nil {
filter.FromDate = &fromDate
}
}
if toDateStr := c.Query("to_date"); toDateStr != "" {
if toDate, err := time.Parse("2006-01-02", toDateStr); err == nil {
filter.ToDate = &toDate
}
}
transactions, err := h.billingService.GetBillingHistory(c.Request.Context(), profileID, filter)
if err != nil {
h.logger.WithError(err).Error("Failed to get billing history")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"transactions": transactions,
"count": len(transactions),
})
}
// GetBillingSummary handles billing summary requests
func (h *CurrencyHandler) GetBillingSummary(c *gin.Context) {
profileID := c.Param("profile_id")
if profileID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "profile_id parameter is required"})
return
}
// Parse date parameters
fromDateStr := c.Query("from_date")
toDateStr := c.Query("to_date")
var fromDate, toDate time.Time
var err error
if fromDateStr != "" {
fromDate, err = time.Parse("2006-01-02", fromDateStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid from_date format, use YYYY-MM-DD"})
return
}
} else {
fromDate = time.Now().AddDate(0, -1, 0) // Default to 1 month ago
}
if toDateStr != "" {
toDate, err = time.Parse("2006-01-02", toDateStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid to_date format, use YYYY-MM-DD"})
return
}
} else {
toDate = time.Now() // Default to now
}
summary, err := h.billingService.CalculateTotalBilling(c.Request.Context(), profileID, fromDate, toDate)
if err != nil {
h.logger.WithError(err).Error("Failed to calculate billing summary")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, summary)
}
// ProcessRefund handles refund requests
func (h *CurrencyHandler) ProcessRefund(c *gin.Context) {
transactionID := c.Param("transaction_id")
if transactionID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "transaction_id parameter is required"})
return
}
var req struct {
Amount float64 `json:"amount" binding:"required,min=0"`
Reason string `json:"reason" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.WithError(err).Error("Invalid refund request")
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// This would need to be added to the BillingService interface
// For now, we'll return an error
c.JSON(http.StatusNotImplemented, gin.H{"error": "Refund processing not yet implemented"})
}
// GetBillingAnalytics handles billing analytics requests
func (h *CurrencyHandler) GetBillingAnalytics(c *gin.Context) {
// This would need to be added to the BillingService interface
// For now, we'll return an error
c.JSON(http.StatusNotImplemented, gin.H{"error": "Billing analytics not yet implemented"})
}
// GetSupportedCurrencies handles supported currencies requests
func (h *CurrencyHandler) GetSupportedCurrencies(c *gin.Context) {
currencies, err := h.exchangeService.GetSupportedCurrencies(c.Request.Context())
if err != nil {
h.logger.WithError(err).Error("Failed to get supported currencies")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"currencies": currencies,
"count": len(currencies),
})
}
// RefreshExchangeRates handles exchange rate refresh requests
func (h *CurrencyHandler) RefreshExchangeRates(c *gin.Context) {
err := h.exchangeService.RefreshRates(c.Request.Context())
if err != nil {
h.logger.WithError(err).Error("Failed to refresh exchange rates")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Exchange rates refreshed successfully"})
}
// GetExchangeRateHistory handles exchange rate history requests
func (h *CurrencyHandler) GetExchangeRateHistory(c *gin.Context) {
fromCurrency := c.Param("from")
toCurrency := c.Param("to")
daysStr := c.Query("days")
if fromCurrency == "" || toCurrency == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "from and to currency parameters are required"})
return
}
days := 30 // Default to 30 days
if daysStr != "" {
if parsedDays, err := strconv.Atoi(daysStr); err == nil && parsedDays > 0 {
days = parsedDays
}
}
rates, err := h.exchangeService.GetRateHistory(c.Request.Context(), fromCurrency, toCurrency, days)
if err != nil {
h.logger.WithError(err).Error("Failed to get exchange rate history")
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"rates": rates,
"count": len(rates),
"from": fromCurrency,
"to": toCurrency,
"days": days,
})
}
@@ -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(&currency.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(&currency.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(&currency.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(&currency.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 &currency.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 &currency.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 &currency.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 &currency.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 &currency.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 &currency.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,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()
@@ -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
@@ -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(&currency.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(&currency.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(&currency.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(&currency.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 = &currency.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 = &currency.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 := &currency.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, &currency.CurrencyFilter{})
if err != nil {
return nil, fmt.Errorf("failed to count currencies: %w", err)
}
// Get active currencies
activeCurrencies, err := s.repository.CountCurrencies(ctx, &currency.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, &currency.TransactionFilter{})
if err != nil {
return nil, fmt.Errorf("failed to count transactions: %w", err)
}
// Get total volume
transactions, err := s.repository.ListTransactions(ctx, &currency.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, &currency.ExchangeRateFilter{})
if err != nil {
return nil, fmt.Errorf("failed to count exchange rates: %w", err)
}
return &currency.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 := &currency.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, &currency.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, &currency.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 = &currency.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] = &currency.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 := &currency.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 &currency.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 = &currency.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 &currency.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 := &currency.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 &currency.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 := &currency.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 := &currency.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 := &currency.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 := &currency.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 := &currency.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,4 +1,4 @@
package service
package services
import (
"context"
@@ -1,4 +1,4 @@
package service
package services
import (
"context"
@@ -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,
@@ -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")
@@ -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
}
}
@@ -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,4 +1,4 @@
package service
package services
import (
"context"