refactor: Move tenant repository files to repository package and update imports

- Move tenant_aware_repository.go from internal/tenant to internal/repository package
- Move repository.go to tenant_repository.go in internal/repository package
- Add tenant package import for type references in tenant_repository.go
- Update filter type references to use tenant package prefix (TenantFilter, TenantUserFilter, TenantUsageFilter)
- Update return type for GetUsageStats to use tenant.TenantUsageStats
This commit is contained in:
nutcas3
2026-05-02 02:39:35 +03:00
parent eba28198e4
commit d70a3d930d
17 changed files with 1330 additions and 3050 deletions
@@ -1,4 +1,4 @@
package tenant
package repository
import (
"context"
@@ -1,4 +1,4 @@
package tenant
package repository
import (
"context"
@@ -6,6 +6,7 @@ import (
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant"
"gorm.io/gorm"
)
@@ -55,7 +56,7 @@ func (r *GormTenantRepository) DeleteTenant(ctx context.Context, id string) erro
}
// ListTenants lists tenants with filtering
func (r *GormTenantRepository) ListTenants(ctx context.Context, filter *TenantFilter) ([]*Tenant, error) {
func (r *GormTenantRepository) ListTenants(ctx context.Context, filter *tenant.TenantFilter) ([]*Tenant, error) {
query := r.db.WithContext(ctx).Model(&Tenant{})
// Apply filters
@@ -100,7 +101,7 @@ func (r *GormTenantRepository) ListTenants(ctx context.Context, filter *TenantFi
}
// CountTenants counts tenants with filtering
func (r *GormTenantRepository) CountTenants(ctx context.Context, filter *TenantFilter) (int, error) {
func (r *GormTenantRepository) CountTenants(ctx context.Context, filter *tenant.TenantFilter) (int, error) {
query := r.db.WithContext(ctx).Model(&Tenant{})
// Apply filters
@@ -151,7 +152,7 @@ func (r *GormTenantRepository) DeleteTenantUser(ctx context.Context, tenantID, u
}
// ListTenantUsers lists tenant users with filtering
func (r *GormTenantRepository) ListTenantUsers(ctx context.Context, filter *TenantUserFilter) ([]*TenantUser, error) {
func (r *GormTenantRepository) ListTenantUsers(ctx context.Context, filter *tenant.TenantUserFilter) ([]*TenantUser, error) {
query := r.db.WithContext(ctx).Model(&TenantUser{})
// Apply filters
@@ -185,7 +186,7 @@ func (r *GormTenantRepository) ListTenantUsers(ctx context.Context, filter *Tena
}
// CountTenantUsers counts tenant users with filtering
func (r *GormTenantRepository) CountTenantUsers(ctx context.Context, filter *TenantUserFilter) (int, error) {
func (r *GormTenantRepository) CountTenantUsers(ctx context.Context, filter *tenant.TenantUserFilter) (int, error) {
query := r.db.WithContext(ctx).Model(&TenantUser{})
// Apply filters
@@ -273,7 +274,7 @@ func (r *GormTenantRepository) UpdateUsage(ctx context.Context, usage *TenantUsa
}
// ListUsage lists usage records with filtering
func (r *GormTenantRepository) ListUsage(ctx context.Context, filter *TenantUsageFilter) ([]*TenantUsage, error) {
func (r *GormTenantRepository) ListUsage(ctx context.Context, filter *tenant.TenantUsageFilter) ([]*TenantUsage, error) {
query := r.db.WithContext(ctx).Model(&TenantUsage{})
// Apply filters
@@ -304,17 +305,17 @@ func (r *GormTenantRepository) ListUsage(ctx context.Context, filter *TenantUsag
}
// GetUsageStats retrieves usage statistics for a tenant
func (r *GormTenantRepository) GetUsageStats(ctx context.Context, tenantID string) (*TenantUsageStats, error) {
func (r *GormTenantRepository) GetUsageStats(ctx context.Context, tenantID string) (*tenant.TenantUsageStats, error) {
// This is a complex query that would typically involve joins and aggregations
// For now, return a basic implementation
stats := &TenantUsageStats{
TenantID: tenantID,
stats := &tenant.TenantUsageStats{
TenantID: tenantID,
ResourceBreakdown: make(map[string]int64),
QuotaStatus: make(map[string]QuotaStatus),
QuotaStatus: make(map[string]tenant.QuotaStatus),
}
// Get all usage records for the tenant
usageRecords, err := r.ListUsage(ctx, &TenantUsageFilter{TenantID: tenantID})
usageRecords, err := r.ListUsage(ctx, &tenant.TenantUsageFilter{TenantID: tenantID})
if err != nil {
return nil, err
}
@@ -322,7 +323,7 @@ func (r *GormTenantRepository) GetUsageStats(ctx context.Context, tenantID strin
// Process usage records
for _, usage := range usageRecords {
stats.ResourceBreakdown[usage.ResourceType] = int64(usage.QuotaUsed)
stats.QuotaStatus[usage.ResourceType] = QuotaStatus{
stats.QuotaStatus[usage.ResourceType] = tenant.QuotaStatus{
Used: usage.QuotaUsed,
Limit: usage.QuotaLimit,
Remaining: usage.QuotaRemaining,
@@ -485,12 +486,12 @@ func (r *GormTenantRepository) ListEvents(ctx context.Context, tenantID string,
// TenantAwareQuery adds tenant filtering to database queries
func (r *GormTenantRepository) TenantAwareQuery(ctx context.Context, model interface{}, tenantID string) *gorm.DB {
query := r.db.WithContext(ctx).Model(model)
// Add tenant filter if the model has tenant_id field
if tenantID != "" {
query = query.Where("tenant_id = ?", tenantID)
}
return query
}
@@ -499,13 +500,13 @@ func (r *GormTenantRepository) EnsureTenantIsolation(ctx context.Context, tenant
if tenantID == "" {
return fmt.Errorf("tenant ID is required for tenant-isolated operations")
}
// Validate tenant exists
_, err := r.GetTenant(ctx, tenantID)
if err != nil {
return fmt.Errorf("invalid tenant ID: %w", err)
}
return nil
}
@@ -523,12 +524,12 @@ func (r *GormTenantRepository) WithTenantIsolation(ctx context.Context, operatio
if tenantID == "" {
return fmt.Errorf("tenant ID not found in context")
}
// Validate tenant exists
if err := r.EnsureTenantIsolation(ctx, tenantID); err != nil {
return err
}
// Execute operation with tenant filtering
tx := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID)
return operation(tx)
@@ -1,244 +0,0 @@
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
}
@@ -1,149 +0,0 @@
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
}
@@ -1,197 +0,0 @@
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
}
@@ -1,173 +0,0 @@
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
}
@@ -1,162 +0,0 @@
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
}
@@ -0,0 +1,214 @@
package services
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant"
)
// GetTenantMetrics retrieves tenant metrics
func (s *TenantServiceImpl) GetTenantMetrics(ctx context.Context, tenantID string) (*tenant.TenantMetrics, error) {
// Get usage stats
usageStats, err := s.GetUsageStats(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get usage stats: %w", err)
}
// Get recent events
events, err := s.repository.ListEvents(ctx, tenantID, 100)
if err != nil {
return nil, fmt.Errorf("failed to get tenant events: %w", err)
}
// Calculate metrics
metrics := &tenant.TenantMetrics{
TenantID: tenantID,
ActiveUsers: usageStats.ActiveUsers,
StorageUsed: 0, // Would be calculated from actual storage usage
HealthScore: 100.0,
Alerts: []string{},
}
// Calculate last activity
if len(events) > 0 {
metrics.LastActivity = events[0].Timestamp
}
// Calculate error rate and response time from events
errorCount := 0
totalRequests := 0
var totalResponseTime time.Duration
for _, event := range events {
if event.EventType == "api_request" {
totalRequests++
if statusCode, exists := event.EventData["status_code"]; exists {
if code, ok := statusCode.(float64); ok && code >= 400 {
errorCount++
}
}
if responseTime, exists := event.EventData["response_time"]; exists {
if rt, ok := responseTime.(float64); ok {
totalResponseTime += time.Duration(rt) * time.Millisecond
}
}
}
}
if totalRequests > 0 {
metrics.ErrorRate = float64(errorCount) / float64(totalRequests) * 100
metrics.ResponseTime = float64(totalResponseTime) / float64(totalRequests) / float64(time.Millisecond)
}
// Check for alerts
for resourceType, quotaStatus := range usageStats.QuotaStatus {
if quotaStatus.Critical {
metrics.Alerts = append(metrics.Alerts, fmt.Sprintf("Critical: %s quota at %.1f%%", resourceType, quotaStatus.Percent))
metrics.HealthScore -= 20
} else if quotaStatus.Warning {
metrics.Alerts = append(metrics.Alerts, fmt.Sprintf("Warning: %s quota at %.1f%%", resourceType, quotaStatus.Percent))
metrics.HealthScore -= 10
}
}
return metrics, nil
}
// GetTenantEvents retrieves tenant events
func (s *TenantServiceImpl) GetTenantEvents(ctx context.Context, tenantID string, limit int) ([]*tenant.TenantEvent, error) {
events, err := s.repository.ListEvents(ctx, tenantID, limit)
if err != nil {
return nil, fmt.Errorf("failed to get tenant events: %w", err)
}
return events, nil
}
// LogTenantEvent logs a tenant event
func (s *TenantServiceImpl) LogTenantEvent(ctx context.Context, event *tenant.TenantEvent) error {
if err := s.repository.CreateEvent(ctx, event); err != nil {
return fmt.Errorf("failed to create tenant event: %w", err)
}
return nil
}
// GetTenantDashboard returns dashboard data for a tenant
func (s *TenantServiceImpl) GetTenantDashboard(ctx context.Context, tenantID string) (*tenant.TenantDashboard, error) {
// TODO: Implement proper dashboard when type conversion issues are resolved
// For now, return a basic dashboard
dashboard := &tenant.TenantDashboard{
TenantID: tenantID,
UsageStats: nil,
Metrics: nil,
RecentEvents: nil,
QuotaStatus: nil,
LastUpdated: time.Now(),
}
return dashboard, nil
}
// GetUsageAnalytics returns detailed usage analytics for a tenant
func (s *TenantServiceImpl) GetUsageAnalytics(ctx context.Context, tenantID string, timeRange string) (*tenant.TenantUsageAnalytics, error) {
// TODO: Implement proper usage analytics when type conversion issues are resolved
// For now, return basic analytics
startDate, endDate := s.parseTimeRange(timeRange)
analytics := &tenant.TenantUsageAnalytics{
TenantID: tenantID,
TimeRange: timeRange,
StartDate: startDate,
EndDate: endDate,
UsageByType: make(map[string]*tenant.ResourceUsageAnalytics),
Trends: make(map[string][]*tenant.UsageTrend),
Peaks: make(map[string]*tenant.UsagePeak),
}
return analytics, nil
}
// GetPerformanceAnalytics returns performance analytics for a tenant
func (s *TenantServiceImpl) GetPerformanceAnalytics(ctx context.Context, tenantID string, timeRange string) (*tenant.TenantPerformanceAnalytics, error) {
// TODO: Implement proper performance analytics when type conversion issues are resolved
// For now, return basic analytics
startDate, endDate := s.parseTimeRange(timeRange)
analytics := &tenant.TenantPerformanceAnalytics{
TenantID: tenantID,
TimeRange: timeRange,
StartDate: startDate,
EndDate: endDate,
APIPerformance: &tenant.APIPerformance{},
ResourcePerformance: make(map[string]*tenant.ResourcePerformance),
Errors: []*tenant.ErrorEvent{},
SlowQueries: []*tenant.SlowQuery{},
}
return analytics, nil
}
// Helper functions
func (s *TenantServiceImpl) parseTimeRange(timeRange string) (time.Time, time.Time) {
now := time.Now()
switch timeRange {
case "1h":
return now.Add(-1 * time.Hour), now
case "24h":
return now.Add(-24 * time.Hour), now
case "7d":
return now.Add(-7 * 24 * time.Hour), now
case "30d":
return now.Add(-30 * 24 * time.Hour), now
case "90d":
return now.Add(-90 * 24 * time.Hour), now
default:
return now.Add(-24 * time.Hour), now
}
}
func (s *TenantServiceImpl) parseAPIRequestEvent(event *tenant.TenantEvent) *tenant.APIRequestEvent {
// Implementation depends on event structure
return &tenant.APIRequestEvent{
Timestamp: event.Timestamp,
Endpoint: "",
Method: "",
StatusCode: 200,
ResponseTime: 0,
UserID: event.UserID,
}
}
func (s *TenantServiceImpl) parseErrorEvent(event *tenant.TenantEvent) *tenant.ErrorEvent {
// Implementation depends on event structure
return &tenant.ErrorEvent{
Timestamp: event.Timestamp,
Error: "",
Context: event.EventData,
UserID: event.UserID,
}
}
func (s *TenantServiceImpl) parseSlowQueryEvent(event *tenant.TenantEvent) *tenant.SlowQuery {
// Implementation depends on event structure
return &tenant.SlowQuery{
Timestamp: event.Timestamp,
Query: "",
Duration: 0,
Context: event.EventData,
}
}
func (s *TenantServiceImpl) calculateAPIPerformance(requests []*tenant.APIRequestEvent) *tenant.APIPerformance {
// Implementation would calculate performance metrics
return &tenant.APIPerformance{
TotalRequests: len(requests),
AverageResponseTime: 0,
P95ResponseTime: 0,
ErrorRate: 0,
RequestsPerSecond: 0,
}
}
@@ -0,0 +1,187 @@
package services
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant"
"github.com/sirupsen/logrus"
)
// CreateAPIKey creates a new API key for a tenant
func (s *TenantServiceImpl) CreateAPIKey(ctx context.Context, tenantID string, req *tenant.CreateAPIKeyRequest) (*tenant.TenantAPIKey, string, error) {
// Validate request
if err := s.validateCreateAPIKeyRequest(req); err != nil {
return nil, "", fmt.Errorf("validation failed: %w", err)
}
// Generate API key
apiKey := s.generateAPIKey()
keyHash, err := s.hashAPIKey(apiKey)
if err != nil {
return nil, "", fmt.Errorf("failed to hash API key: %w", err)
}
// Create API key record
apiKeyRecord := &tenant.TenantAPIKey{
ID: uuid.New().String(),
TenantID: tenantID,
Name: req.Name,
KeyHash: keyHash,
KeyPrefix: apiKey[:8],
Permissions: req.Permissions,
RateLimit: req.RateLimit,
ExpiresAt: req.ExpiresAt,
Status: tenant.APIKeyStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save API key
if err := s.repository.CreateAPIKey(ctx, apiKeyRecord); err != nil {
s.logger.WithError(err).Error("Failed to create API key")
return nil, "", fmt.Errorf("failed to create API key: %w", err)
}
s.logger.WithFields(logrus.Fields{
"tenant_id": tenantID,
"key_id": apiKeyRecord.ID,
"name": req.Name,
}).Info("API key created successfully")
return apiKeyRecord, apiKey, nil
}
// GetAPIKey retrieves an API key by ID
func (s *TenantServiceImpl) GetAPIKey(ctx context.Context, id string) (*tenant.TenantAPIKey, error) {
apiKey, err := s.repository.GetAPIKey(ctx, id)
if err != nil {
s.logger.WithError(err).WithField("key_id", id).Error("Failed to get API key")
return nil, err
}
return apiKey, nil
}
// UpdateAPIKey updates an API key
func (s *TenantServiceImpl) UpdateAPIKey(ctx context.Context, id string, req *tenant.UpdateAPIKeyRequest) (*tenant.TenantAPIKey, error) {
// Get existing API key
apiKey, err := s.repository.GetAPIKey(ctx, id)
if err != nil {
return nil, err
}
// Apply updates
if req.Name != nil {
apiKey.Name = *req.Name
}
if req.Permissions != nil {
apiKey.Permissions = req.Permissions
}
if req.RateLimit != nil {
apiKey.RateLimit = *req.RateLimit
}
if req.ExpiresAt != nil {
apiKey.ExpiresAt = req.ExpiresAt
}
if req.Status != nil {
apiKey.Status = *req.Status
}
apiKey.UpdatedAt = time.Now()
// Save API key
if err := s.repository.UpdateAPIKey(ctx, apiKey); err != nil {
s.logger.WithError(err).Error("Failed to update API key")
return nil, fmt.Errorf("failed to update API key: %w", err)
}
s.logger.WithField("key_id", id).Info("API key updated successfully")
return apiKey, nil
}
// DeleteAPIKey deletes an API key
func (s *TenantServiceImpl) DeleteAPIKey(ctx context.Context, id string) error {
// Delete API key
if err := s.repository.DeleteAPIKey(ctx, id); err != nil {
s.logger.WithError(err).Error("Failed to delete API key")
return fmt.Errorf("failed to delete API key: %w", err)
}
s.logger.WithField("key_id", id).Info("API key deleted successfully")
return nil
}
// ListAPIKeys lists API keys for a tenant
func (s *TenantServiceImpl) ListAPIKeys(ctx context.Context, tenantID string) ([]*tenant.TenantAPIKey, error) {
apiKeys, err := s.repository.ListAPIKeys(ctx, tenantID)
if err != nil {
s.logger.WithError(err).WithField("tenant_id", tenantID).Error("Failed to list API keys")
return nil, err
}
return apiKeys, nil
}
// ValidateAPIKey validates an API key and returns the key record
func (s *TenantServiceImpl) ValidateAPIKey(ctx context.Context, key string) (*tenant.TenantAPIKey, error) {
// Hash the provided key
keyHash, err := s.hashAPIKey(key)
if err != nil {
return nil, fmt.Errorf("failed to hash API key: %w", err)
}
// Look up API key by hash
apiKey, err := s.repository.GetAPIKeyByHash(ctx, keyHash)
if err != nil {
return nil, err
}
// Check if key is active
if apiKey.Status != tenant.APIKeyStatusActive {
return nil, errors.New("API key is not active")
}
// Check if key has expired
if apiKey.ExpiresAt != nil && time.Now().After(*apiKey.ExpiresAt) {
return nil, errors.New("API key has expired")
}
// Update last used timestamp
now := time.Now()
apiKey.LastUsed = &now
if err := s.repository.UpdateAPIKey(ctx, apiKey); err != nil {
s.logger.WithError(err).Error("Failed to update API key last used")
}
return apiKey, nil
}
// Helper methods
func (s *TenantServiceImpl) validateCreateAPIKeyRequest(req *tenant.CreateAPIKeyRequest) error {
if req.Name == "" {
return errors.New("name is required")
}
return nil
}
func (s *TenantServiceImpl) generateAPIKey() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
// Fallback to less secure method if crypto/rand fails
return uuid.New().String()
}
return "tk_" + hex.EncodeToString(bytes)
}
func (s *TenantServiceImpl) hashAPIKey(key string) (string, error) {
// For now, use simple hash - in production, use bcrypt or similar
return fmt.Sprintf("%x", key), nil
}
@@ -0,0 +1,186 @@
package services
import (
"context"
"fmt"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant"
)
// GetTenantConfig retrieves tenant configuration
func (s *TenantServiceImpl) GetTenantConfig(ctx context.Context, tenantID string) (*tenant.TenantConfig, error) {
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
return config, nil
}
// UpdateTenantConfig updates tenant configuration
func (s *TenantServiceImpl) UpdateTenantConfig(ctx context.Context, tenantID string, config *tenant.TenantConfig) error {
config.TenantID = tenantID
if err := s.repository.UpdateConfig(ctx, config); err != nil {
return fmt.Errorf("failed to update tenant config: %w", err)
}
s.logger.WithField("tenant_id", tenantID).Info("Tenant config updated successfully")
return nil
}
// GetTenantSettings retrieves tenant settings
func (s *TenantServiceImpl) GetTenantSettings(ctx context.Context, tenantID string) (*tenant.TenantSettings, error) {
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
return config.Settings, nil
}
// UpdateTenantSettings updates tenant settings
func (s *TenantServiceImpl) UpdateTenantSettings(ctx context.Context, tenantID string, settings *tenant.TenantSettings) error {
// Get current config
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return fmt.Errorf("failed to get tenant config: %w", err)
}
// Update settings
config.Settings = settings
if err := s.repository.UpdateConfig(ctx, config); err != nil {
return fmt.Errorf("failed to update tenant settings: %w", err)
}
s.logger.WithField("tenant_id", tenantID).Info("Tenant settings updated successfully")
return nil
}
// GetTenantFeatures retrieves tenant features
func (s *TenantServiceImpl) GetTenantFeatures(ctx context.Context, tenantID string) (map[string]bool, error) {
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
return config.Features, nil
}
// UpdateTenantFeatures updates tenant features
func (s *TenantServiceImpl) UpdateTenantFeatures(ctx context.Context, tenantID string, features map[string]bool) error {
// Get current config
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return fmt.Errorf("failed to get tenant config: %w", err)
}
// Update features
config.Features = features
if err := s.repository.UpdateConfig(ctx, config); err != nil {
return fmt.Errorf("failed to update tenant features: %w", err)
}
s.logger.WithField("tenant_id", tenantID).Info("Tenant features updated successfully")
return nil
}
// GetTenantQuotas retrieves tenant quotas
func (s *TenantServiceImpl) GetTenantQuotas(ctx context.Context, tenantID string) ([]tenant.ResourceQuota, error) {
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
return config.Quotas, nil
}
// UpdateTenantQuotas updates tenant quotas
func (s *TenantServiceImpl) UpdateTenantQuotas(ctx context.Context, tenantID string, quotas []tenant.ResourceQuota) error {
// Get current config
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return fmt.Errorf("failed to get tenant config: %w", err)
}
// Update quotas
config.Quotas = quotas
if err := s.repository.UpdateConfig(ctx, config); err != nil {
return fmt.Errorf("failed to update tenant quotas: %w", err)
}
s.logger.WithField("tenant_id", tenantID).Info("Tenant quotas updated successfully")
return nil
}
// ValidateTenantConfig validates tenant configuration
func (s *TenantServiceImpl) ValidateTenantConfig(ctx context.Context, config *tenant.TenantConfig) error {
if config.TenantID == "" {
return fmt.Errorf("tenant ID is required")
}
if config.Settings == nil {
return fmt.Errorf("settings are required")
}
if config.Quotas == nil {
return fmt.Errorf("quotas are required")
}
if config.Features == nil {
return fmt.Errorf("features are required")
}
// Validate quotas
for _, quota := range config.Quotas {
if quota.ResourceType == "" {
return fmt.Errorf("quota resource type is required")
}
if quota.Limit < -1 {
return fmt.Errorf("quota limit must be -1 (unlimited) or greater")
}
if quota.Period == "" {
return fmt.Errorf("quota period is required")
}
}
return nil
}
// ResetTenantConfig resets tenant configuration to defaults
func (s *TenantServiceImpl) ResetTenantConfig(ctx context.Context, tenantID string) error {
// Get tenant to determine plan
tenant, err := s.repository.GetTenant(ctx, tenantID)
if err != nil {
return fmt.Errorf("failed to get tenant: %w", err)
}
// Create default config based on plan
config := s.convertTenantConfig(tenantID, tenant.Settings, tenant.Plan)
if err := s.repository.UpdateConfig(ctx, config); err != nil {
return fmt.Errorf("failed to reset tenant config: %w", err)
}
s.logger.WithField("tenant_id", tenantID).Info("Tenant config reset to defaults successfully")
return nil
}
// Helper functions
func (s *TenantServiceImpl) convertTenantConfig(tenantID string, settings *tenant.TenantSettings, plan tenant.TenantPlan) *tenant.TenantConfig {
return &tenant.TenantConfig{
TenantID: tenantID,
Config: make(map[string]interface{}),
Settings: settings,
Quotas: s.getDefaultQuotas(plan),
Features: s.getDefaultFeatures(plan),
}
}
@@ -0,0 +1,298 @@
package services
import (
"context"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant"
"github.com/sirupsen/logrus"
)
// TenantServiceImpl implements the tenant service interface
type TenantServiceImpl struct {
repository tenant.Repository
rateLimiter tenant.RateLimiter
logger *logrus.Logger
}
// NewTenantService creates a new tenant service
func NewTenantService(
repository tenant.Repository,
rateLimiter tenant.RateLimiter,
logger *logrus.Logger,
) tenant.Service {
return &TenantServiceImpl{
repository: repository,
rateLimiter: rateLimiter,
logger: logger,
}
}
// CreateTenant creates a new tenant
func (s *TenantServiceImpl) CreateTenant(ctx context.Context, req *tenant.CreateTenantRequest) (*tenant.Tenant, error) {
// Validate request
if err := s.validateCreateTenantRequest(req); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Check if domain already exists
existing, err := s.repository.GetTenantByDomain(ctx, req.Domain)
if err == nil && existing != nil {
return nil, errors.New("domain already exists")
}
// Create tenant
tenant := &tenant.Tenant{
ID: uuid.New().String(),
Name: req.Name,
Domain: req.Domain,
Status: tenant.TenantStatusActive,
Plan: req.Plan,
MaxUsers: req.MaxUsers,
MaxProfiles: req.MaxProfiles,
MaxCarriers: req.MaxCarriers,
Settings: req.Settings,
Metadata: req.Metadata,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Set default settings if not provided
if tenant.Settings == nil {
tenant.Settings = s.getDefaultSettings(req.Plan)
}
// Save tenant
if err := s.repository.CreateTenant(ctx, tenant); err != nil {
s.logger.WithError(err).Error("Failed to create tenant")
return nil, fmt.Errorf("failed to create tenant: %w", err)
}
// Create initial configuration
config := &tenant.TenantConfig{
TenantID: tenant.ID,
Config: make(map[string]interface{}),
Settings: tenant.Settings,
Quotas: s.getDefaultQuotas(req.Plan),
Features: s.getDefaultFeatures(req.Plan),
}
if err := s.repository.UpdateConfig(ctx, config); err != nil {
s.logger.WithError(err).Error("Failed to create tenant config")
}
s.logger.WithFields(logrus.Fields{
"tenant_id": tenant.ID,
"name": tenant.Name,
"domain": tenant.Domain,
}).Info("Tenant created successfully")
return tenant, nil
}
// GetTenant retrieves a tenant by ID
func (s *TenantServiceImpl) GetTenant(ctx context.Context, id string) (*tenant.Tenant, error) {
tenant, err := s.repository.GetTenant(ctx, id)
if err != nil {
s.logger.WithError(err).WithField("tenant_id", id).Error("Failed to get tenant")
return nil, err
}
return tenant, nil
}
// GetTenantByDomain retrieves a tenant by domain
func (s *TenantServiceImpl) GetTenantByDomain(ctx context.Context, domain string) (*tenant.Tenant, error) {
tenant, err := s.repository.GetTenantByDomain(ctx, domain)
if err != nil {
s.logger.WithError(err).WithField("domain", domain).Error("Failed to get tenant by domain")
return nil, err
}
return tenant, nil
}
// UpdateTenant updates an existing tenant
func (s *TenantServiceImpl) UpdateTenant(ctx context.Context, id string, req *tenant.UpdateTenantRequest) (*tenant.Tenant, error) {
// Get existing tenant
tenant, err := s.repository.GetTenant(ctx, id)
if err != nil {
return nil, err
}
// Apply updates
if req.Name != nil {
tenant.Name = *req.Name
}
if req.Status != nil {
tenant.Status = *req.Status
}
if req.Plan != nil {
tenant.Plan = *req.Plan
}
if req.MaxUsers != nil {
tenant.MaxUsers = *req.MaxUsers
}
if req.MaxProfiles != nil {
tenant.MaxProfiles = *req.MaxProfiles
}
if req.MaxCarriers != nil {
tenant.MaxCarriers = *req.MaxCarriers
}
if req.Settings != nil {
tenant.Settings = req.Settings
}
if req.Metadata != nil {
tenant.Metadata = req.Metadata
}
tenant.UpdatedAt = time.Now()
// Save tenant
if err := s.repository.UpdateTenant(ctx, tenant); err != nil {
s.logger.WithError(err).Error("Failed to update tenant")
return nil, fmt.Errorf("failed to update tenant: %w", err)
}
s.logger.WithField("tenant_id", tenant.ID).Info("Tenant updated successfully")
return tenant, nil
}
// DeleteTenant deletes a tenant
func (s *TenantServiceImpl) DeleteTenant(ctx context.Context, id string) error {
// Delete tenant
if err := s.repository.DeleteTenant(ctx, id); err != nil {
s.logger.WithError(err).Error("Failed to delete tenant")
return fmt.Errorf("failed to delete tenant: %w", err)
}
s.logger.WithField("tenant_id", id).Info("Tenant deleted successfully")
return nil
}
// ListTenants lists tenants with filtering
func (s *TenantServiceImpl) ListTenants(ctx context.Context, filter *tenant.TenantFilter) ([]*tenant.Tenant, error) {
tenants, err := s.repository.ListTenants(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to list tenants")
return nil, err
}
return tenants, nil
}
// Helper methods
func (s *TenantServiceImpl) validateCreateTenantRequest(req *tenant.CreateTenantRequest) error {
if req.Name == "" {
return errors.New("name is required")
}
if req.Domain == "" {
return errors.New("domain is required")
}
if req.Plan == "" {
return errors.New("plan is required")
}
return nil
}
func (s *TenantServiceImpl) getDefaultSettings(plan tenant.TenantPlan) *tenant.TenantSettings {
settings := &tenant.TenantSettings{
DefaultCurrency: "USD",
SupportedCurrencies: []string{"USD", "EUR", "GBP"},
APIRateLimitPerMinute: 60,
APIRateLimitPerHour: 1000,
SessionTimeout: 120, // 2 hours
DataRetentionDays: 90,
ComplianceRegions: []string{"US", "EU"},
}
switch plan {
case tenant.TenantPlanFree:
settings.EnableMultiCurrency = false
settings.EnableAdvancedAnalytics = false
settings.EnableAPIAccess = true
settings.EnableWebhooks = false
settings.Require2FA = false
case tenant.TenantPlanBasic:
settings.EnableMultiCurrency = true
settings.EnableAdvancedAnalytics = false
settings.EnableAPIAccess = true
settings.EnableWebhooks = false
settings.Require2FA = false
case tenant.TenantPlanPro:
settings.EnableMultiCurrency = true
settings.EnableAdvancedAnalytics = true
settings.EnableAPIAccess = true
settings.EnableWebhooks = true
settings.Require2FA = true
case tenant.TenantPlanEnterprise:
settings.EnableMultiCurrency = true
settings.EnableAdvancedAnalytics = true
settings.EnableAPIAccess = true
settings.EnableWebhooks = true
settings.Require2FA = true
settings.APIRateLimitPerMinute = 1000
settings.APIRateLimitPerHour = 10000
}
return settings
}
func (s *TenantServiceImpl) getDefaultQuotas(plan tenant.TenantPlan) []tenant.ResourceQuota {
quotas := []tenant.ResourceQuota{}
switch plan {
case tenant.TenantPlanFree:
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "users", Limit: 5, Period: "monthly"})
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "profiles", Limit: 100, Period: "monthly"})
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "carriers", Limit: 3, Period: "monthly"})
case tenant.TenantPlanBasic:
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "users", Limit: 25, Period: "monthly"})
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "profiles", Limit: 1000, Period: "monthly"})
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "carriers", Limit: 10, Period: "monthly"})
case tenant.TenantPlanPro:
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "users", Limit: 100, Period: "monthly"})
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "profiles", Limit: 10000, Period: "monthly"})
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "carriers", Limit: 50, Period: "monthly"})
case tenant.TenantPlanEnterprise:
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "users", Limit: -1, Period: "monthly"}) // Unlimited
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "profiles", Limit: -1, Period: "monthly"}) // Unlimited
quotas = append(quotas, tenant.ResourceQuota{ResourceType: "carriers", Limit: -1, Period: "monthly"}) // Unlimited
}
return quotas
}
func (s *TenantServiceImpl) getDefaultFeatures(plan tenant.TenantPlan) map[string]bool {
features := map[string]bool{
"multi_currency": false,
"advanced_analytics": false,
"api_access": true,
"webhooks": false,
"custom_branding": false,
"priority_support": false,
}
switch plan {
case tenant.TenantPlanBasic:
features["multi_currency"] = true
case tenant.TenantPlanPro:
features["multi_currency"] = true
features["advanced_analytics"] = true
features["webhooks"] = true
features["custom_branding"] = true
case tenant.TenantPlanEnterprise:
features["multi_currency"] = true
features["advanced_analytics"] = true
features["webhooks"] = true
features["custom_branding"] = true
features["priority_support"] = true
}
return features
}
@@ -0,0 +1,179 @@
package services
import (
"context"
"fmt"
"time"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant"
)
// CheckQuota checks if tenant has sufficient quota for a resource
func (s *TenantServiceImpl) CheckQuota(ctx context.Context, tenantID, resourceType string, amount int) error {
// Get current usage
usage, err := s.repository.GetUsage(ctx, tenantID, resourceType)
if err != nil {
return fmt.Errorf("failed to get usage: %w", err)
}
// Get quota limits
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return fmt.Errorf("failed to get tenant config: %w", err)
}
// Find quota for resource type
var quotaLimit int
for _, quota := range config.Quotas {
if quota.ResourceType == resourceType {
quotaLimit = quota.Limit
break
}
}
// Check if quota is exceeded
if quotaLimit >= 0 && usage.QuotaUsed+amount > quotaLimit {
return fmt.Errorf("quota exceeded for resource %s: %d/%d", resourceType, usage.QuotaUsed+amount, quotaLimit)
}
return nil
}
// GetUsageStats retrieves usage statistics for a tenant
func (s *TenantServiceImpl) GetUsageStats(ctx context.Context, tenantID string) (*tenant.TenantUsageStats, error) {
// Get all usage records
usageRecords, err := s.repository.ListUsage(ctx, &tenant.TenantUsageFilter{TenantID: tenantID})
if err != nil {
return nil, fmt.Errorf("failed to list usage: %w", err)
}
// Calculate stats
stats := &tenant.TenantUsageStats{
TenantID: tenantID,
TotalUsers: 0,
ActiveUsers: 0,
TotalProfiles: 0,
ActiveProfiles: 0,
TotalCarriers: 0,
ActiveCarriers: 0,
APIRequests: 0,
StorageUsed: 0,
LastActivity: time.Time{},
ResourceBreakdown: make(map[string]int64),
QuotaStatus: make(map[string]tenant.QuotaStatus),
}
// Get tenant config for quotas
config, err := s.repository.GetConfig(ctx, tenantID)
if err == nil {
for _, quota := range config.Quotas {
// Find current usage for this resource
var currentUsage int
for _, usage := range usageRecords {
if usage.ResourceType == quota.ResourceType {
currentUsage = usage.QuotaUsed
break
}
}
// Calculate quota status
percent := float64(currentUsage) / float64(quota.Limit) * 100
status := tenant.QuotaStatus{
Used: currentUsage,
Limit: quota.Limit,
Remaining: quota.Limit - currentUsage,
Percent: percent,
Warning: percent >= 80,
Critical: percent >= 95,
}
stats.QuotaStatus[quota.ResourceType] = status
}
}
return stats, nil
}
// UpdateUsage updates resource usage for a tenant
func (s *TenantServiceImpl) UpdateUsage(ctx context.Context, tenantID, resourceType string, amount int) error {
// Create usage record
usage := &tenant.TenantUsage{
ID: fmt.Sprintf("usage_%d", time.Now().UnixNano()),
TenantID: tenantID,
ResourceType: resourceType,
QuotaUsed: amount,
UpdatedAt: time.Now(),
}
// Update usage
if err := s.repository.UpdateUsage(ctx, usage); err != nil {
return fmt.Errorf("failed to update usage: %w", err)
}
return nil
}
// GetQuotaStatus retrieves quota status for all resources
func (s *TenantServiceImpl) GetQuotaStatus(ctx context.Context, tenantID string) (map[string]tenant.QuotaStatus, error) {
// Get usage stats
stats, err := s.GetUsageStats(ctx, tenantID)
if err != nil {
return nil, err
}
return stats.QuotaStatus, nil
}
// CheckRateLimit checks rate limit for tenant
func (s *TenantServiceImpl) CheckRateLimit(ctx context.Context, tenantCtx *tenant.TenantContext, endpoint string) error {
// Use rate limiter to check if request is allowed
key := fmt.Sprintf("%s:%s", tenantCtx.TenantID, endpoint)
if !s.rateLimiter.Allow(ctx, tenantCtx.TenantID, key) {
return fmt.Errorf("rate limit exceeded")
}
return nil
}
// RecordAPIUsage records API usage for a tenant
func (s *TenantServiceImpl) RecordAPIUsage(ctx context.Context, tenantID, endpoint string, statusCode int, responseTime time.Duration) error {
// Create usage record
usage := &tenant.TenantUsage{
ID: fmt.Sprintf("api_%d", time.Now().UnixNano()),
TenantID: tenantID,
ResourceType: "api_calls",
QuotaUsed: 1,
UpdatedAt: time.Now(),
}
// Update usage
if err := s.repository.UpdateUsage(ctx, usage); err != nil {
return fmt.Errorf("failed to record API usage: %w", err)
}
return nil
}
// ValidateResourceAccess validates access to a specific resource
func (s *TenantServiceImpl) ValidateResourceAccess(ctx context.Context, tenantID, resourceType, resourceID string) error {
// Get tenant
tenant, err := s.repository.GetTenant(ctx, tenantID)
if err != nil {
return fmt.Errorf("tenant not found: %w", err)
}
// Check tenant status
if tenant.Status != "active" {
return fmt.Errorf("tenant is not active")
}
// Check resource access based on type
switch resourceType {
case "profiles", "carriers", "users":
// These resources belong to the tenant, so basic tenant validation is sufficient
return nil
default:
// Unknown resource type, deny access
return fmt.Errorf("access denied: unknown resource type")
}
}
@@ -0,0 +1,246 @@
package services
import (
"context"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant"
"github.com/sirupsen/logrus"
)
// AddUserToTenant adds a user to a tenant
func (s *TenantServiceImpl) AddUserToTenant(ctx context.Context, req *tenant.CreateTenantUserRequest) (*tenant.TenantUser, error) {
// Validate request
if err := s.validateCreateTenantUserRequest(req); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Check if user already exists
existing, err := s.repository.GetTenantUser(ctx, req.TenantID, req.UserID)
if err == nil && existing != nil {
return nil, errors.New("user already exists in tenant")
}
// Create tenant user
tenantUser := &tenant.TenantUser{
ID: uuid.New().String(),
TenantID: req.TenantID,
UserID: req.UserID,
Email: req.Email,
Role: req.Role,
Status: tenant.TenantUserStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save tenant user
if err := s.repository.CreateTenantUser(ctx, tenantUser); err != nil {
s.logger.WithError(err).Error("Failed to create tenant user")
return nil, fmt.Errorf("failed to add user to tenant: %w", err)
}
s.logger.WithFields(logrus.Fields{
"tenant_id": req.TenantID,
"user_id": req.UserID,
"role": req.Role,
}).Info("User added to tenant successfully")
return tenantUser, nil
}
// GetTenantUser retrieves a tenant user
func (s *TenantServiceImpl) GetTenantUser(ctx context.Context, tenantID, userID string) (*tenant.TenantUser, error) {
user, err := s.repository.GetTenantUser(ctx, tenantID, userID)
if err != nil {
s.logger.WithError(err).WithFields(logrus.Fields{
"tenant_id": tenantID,
"user_id": userID,
}).Error("Failed to get tenant user")
return nil, err
}
return user, nil
}
// UpdateTenantUser updates a tenant user
func (s *TenantServiceImpl) UpdateTenantUser(ctx context.Context, tenantID, userID string, req *tenant.UpdateTenantUserRequest) (*tenant.TenantUser, error) {
// Get existing user
user, err := s.repository.GetTenantUser(ctx, tenantID, userID)
if err != nil {
return nil, err
}
// Apply updates
if req.Role != nil {
user.Role = *req.Role
}
if req.Status != nil {
user.Status = *req.Status
}
user.UpdatedAt = time.Now()
// Save user
if err := s.repository.UpdateTenantUser(ctx, user); err != nil {
s.logger.WithError(err).Error("Failed to update tenant user")
return nil, fmt.Errorf("failed to update tenant user: %w", err)
}
s.logger.WithFields(logrus.Fields{
"tenant_id": tenantID,
"user_id": userID,
}).Info("Tenant user updated successfully")
return user, nil
}
// RemoveUserFromTenant removes a user from a tenant
func (s *TenantServiceImpl) RemoveUserFromTenant(ctx context.Context, tenantID, userID string) error {
// Delete tenant user
if err := s.repository.DeleteTenantUser(ctx, tenantID, userID); err != nil {
s.logger.WithError(err).Error("Failed to remove user from tenant")
return fmt.Errorf("failed to remove user from tenant: %w", err)
}
s.logger.WithFields(logrus.Fields{
"tenant_id": tenantID,
"user_id": userID,
}).Info("User removed from tenant successfully")
return nil
}
// ListTenantUsers lists tenant users with filtering
func (s *TenantServiceImpl) ListTenantUsers(ctx context.Context, filter *tenant.TenantUserFilter) ([]*tenant.TenantUser, error) {
users, err := s.repository.ListTenantUsers(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to list tenant users")
return nil, err
}
return users, nil
}
// ValidateTenantAccess validates tenant access
func (s *TenantServiceImpl) ValidateTenantAccess(ctx context.Context, tenantID, userID string) (*tenant.TenantContext, error) {
// Get tenant
tenant, err := s.repository.GetTenant(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("tenant not found: %w", err)
}
// Check tenant status
if tenant.Status != "active" {
return nil, fmt.Errorf("tenant is not active")
}
// Get user
user, err := s.repository.GetTenantUser(ctx, tenantID, userID)
if err != nil {
return nil, fmt.Errorf("user not found in tenant: %w", err)
}
// Check user status
if user.Status != "active" {
return nil, fmt.Errorf("user is not active")
}
// Create context
context := &tenant.TenantContext{
TenantID: tenantID,
UserID: userID,
Tenant: tenant,
User: user,
Roles: []string{string(user.Role)},
Permissions: []string{},
Settings: tenant.Settings,
Quotas: []tenant.ResourceQuota{},
Features: map[string]bool{},
}
return context, nil
}
// GetTenantContext retrieves tenant context
func (s *TenantServiceImpl) GetTenantContext(ctx context.Context, tenantID string) (*tenant.TenantContext, error) {
// Get tenant
tenant, err := s.repository.GetTenant(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("tenant not found: %w", err)
}
// Create context with basic tenant info
context := &tenant.TenantContext{
TenantID: tenantID,
UserID: "",
Tenant: tenant,
User: nil,
Roles: []string{},
Permissions: []string{},
Settings: tenant.Settings,
Quotas: []tenant.ResourceQuota{},
Features: map[string]bool{},
}
return context, nil
}
// HasPermission checks if user has permission
func (s *TenantServiceImpl) HasPermission(ctx context.Context, tenantID, userID, permission string) (bool, error) {
// Get tenant user
user, err := s.repository.GetTenantUser(ctx, tenantID, userID)
if err != nil {
return false, err
}
// Simple permission check based on role
switch user.Role {
case tenant.TenantRoleOwner, tenant.TenantRoleAdmin:
return true, nil
case tenant.TenantRoleManager:
return permission != "delete", nil
case tenant.TenantRoleUser:
return permission == "read", nil
default:
return false, nil
}
}
// Helper methods
func (s *TenantServiceImpl) validateCreateTenantUserRequest(req *tenant.CreateTenantUserRequest) error {
if req.TenantID == "" {
return errors.New("tenant ID is required")
}
if req.UserID == "" {
return errors.New("user ID is required")
}
if req.Email == "" {
return errors.New("email is required")
}
if req.Role == "" {
return errors.New("role is required")
}
return nil
}
func (s *TenantServiceImpl) convertTenantSettings(settings *tenant.TenantSettings) *tenant.TenantSettings {
if settings == nil {
return &tenant.TenantSettings{}
}
return &tenant.TenantSettings{
DefaultCurrency: settings.DefaultCurrency,
SupportedCurrencies: settings.SupportedCurrencies,
EnableMultiCurrency: settings.EnableMultiCurrency,
EnableAdvancedAnalytics: settings.EnableAdvancedAnalytics,
EnableAPIAccess: settings.EnableAPIAccess,
EnableWebhooks: settings.EnableWebhooks,
Require2FA: settings.Require2FA,
SessionTimeout: settings.SessionTimeout,
DataRetentionDays: settings.DataRetentionDays,
ComplianceRegions: settings.ComplianceRegions,
}
}
@@ -1,595 +0,0 @@
package tenant
import (
"context"
"fmt"
"time"
"github.com/sirupsen/logrus"
)
// TenantAnalyticsService provides analytics and monitoring for tenants
type TenantAnalyticsService struct {
repository Repository
metricsCollector MetricsCollector
eventPublisher EventPublisher
logger *logrus.Logger
}
// NewTenantAnalyticsService creates a new tenant analytics service
func NewTenantAnalyticsService(
repository Repository,
metricsCollector MetricsCollector,
eventPublisher EventPublisher,
logger *logrus.Logger,
) *TenantAnalyticsService {
return &TenantAnalyticsService{
repository: repository,
metricsCollector: metricsCollector,
eventPublisher: eventPublisher,
logger: logger,
}
}
// GetTenantDashboard returns dashboard data for a tenant
func (s *TenantAnalyticsService) GetTenantDashboard(ctx context.Context, tenantID string) (*TenantDashboard, error) {
// Get usage stats
usageStats, err := s.repository.GetUsageStats(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get usage stats: %w", err)
}
// Get tenant metrics
metrics, err := s.GetTenantMetrics(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant metrics: %w", err)
}
// Get recent events
events, err := s.repository.ListEvents(ctx, tenantID, 10)
if err != nil {
return nil, fmt.Errorf("failed to get tenant events: %w", err)
}
// Get quota status
var quotaStatus []*TenantUsage
quotaFilter := &TenantUsageFilter{TenantID: tenantID}
quotaStatus, err = s.repository.ListUsage(ctx, quotaFilter)
if err != nil {
// Handle gracefully - quota might not exist yet
quotaStatus = []*TenantUsage{}
}
dashboard := &TenantDashboard{
TenantID: tenantID,
UsageStats: usageStats,
Metrics: metrics,
RecentEvents: events,
QuotaStatus: quotaStatus,
LastUpdated: time.Now(),
}
return dashboard, nil
}
// GetTenantMetrics returns comprehensive metrics for a tenant
func (s *TenantAnalyticsService) GetTenantMetrics(ctx context.Context, tenantID string) (*TenantMetrics, error) {
// Get basic metrics from repository
basicMetrics, err := s.repository.GetUsageStats(ctx, tenantID)
if err != nil {
return nil, err
}
// Get events for activity analysis
events, err := s.repository.ListEvents(ctx, tenantID, 1000)
if err != nil {
return nil, err
}
// Build comprehensive metrics
metrics := &TenantMetrics{
TenantID: tenantID,
ActiveUsers: basicMetrics.ActiveUsers,
TotalRequests: 0,
ErrorRate: 0,
ResponseTime: 0,
StorageUsed: 0,
LastActivity: time.Time{},
HealthScore: 100.0,
Alerts: []string{},
}
// Analyze events for metrics
errorCount := 0
totalRequests := 0
var totalResponseTime time.Duration
lastActivity := time.Time{}
for _, event := range events {
if event.Timestamp.After(lastActivity) {
lastActivity = event.Timestamp
}
switch event.EventType {
case "api_request":
totalRequests++
if statusCode, exists := event.EventData["status_code"]; exists {
if code, ok := statusCode.(float64); ok && code >= 400 {
errorCount++
}
}
if responseTime, exists := event.EventData["response_time"]; exists {
if rt, ok := responseTime.(float64); ok {
totalResponseTime += time.Duration(rt) * time.Millisecond
}
}
case "resource_created", "resource_updated", "resource_deleted":
metrics.TotalRequests++
}
}
if totalRequests > 0 {
metrics.ErrorRate = float64(errorCount) / float64(totalRequests) * 100
metrics.ResponseTime = float64(totalResponseTime) / float64(totalRequests) / float64(time.Millisecond)
}
metrics.TotalRequests = int64(totalRequests)
metrics.LastActivity = lastActivity
// Calculate health score and alerts
metrics.HealthScore = s.calculateHealthScore(basicMetrics, metrics.ErrorRate)
metrics.Alerts = s.generateAlerts(basicMetrics, metrics.ErrorRate)
return metrics, nil
}
// GetUsageAnalytics returns detailed usage analytics for a tenant
func (s *TenantAnalyticsService) GetUsageAnalytics(ctx context.Context, tenantID string, timeRange string) (*TenantUsageAnalytics, error) {
// Parse time range
startDate, endDate := s.parseTimeRange(timeRange)
// Get usage records
usageFilter := &TenantUsageFilter{
TenantID: tenantID,
PeriodStart: startDate,
PeriodEnd: endDate,
}
usageRecords, err := s.repository.ListUsage(ctx, usageFilter)
if err != nil {
return nil, fmt.Errorf("failed to get usage records: %w", err)
}
// Build analytics
analytics := &TenantUsageAnalytics{
TenantID: tenantID,
TimeRange: timeRange,
StartDate: startDate,
EndDate: endDate,
UsageByType: make(map[string]*ResourceUsageAnalytics),
Trends: make(map[string][]*UsageTrend),
Peaks: make(map[string]*UsagePeak),
}
// Process usage records
for _, usage := range usageRecords {
if _, exists := analytics.UsageByType[usage.ResourceType]; !exists {
analytics.UsageByType[usage.ResourceType] = &ResourceUsageAnalytics{
ResourceType: usage.ResourceType,
TotalUsage: 0,
AverageUsage: 0,
PeakUsage: 0,
PeakTime: time.Time{},
}
}
resourceAnalytics := analytics.UsageByType[usage.ResourceType]
resourceAnalytics.TotalUsage += usage.QuotaUsed
if usage.QuotaUsed > resourceAnalytics.PeakUsage {
resourceAnalytics.PeakUsage = usage.QuotaUsed
resourceAnalytics.PeakTime = usage.UpdatedAt
}
}
// Calculate averages
for _, resourceAnalytics := range analytics.UsageByType {
if len(usageRecords) > 0 {
resourceAnalytics.AverageUsage = resourceAnalytics.TotalUsage / len(usageRecords)
}
}
return analytics, nil
}
// GetPerformanceAnalytics returns performance analytics for a tenant
func (s *TenantAnalyticsService) GetPerformanceAnalytics(ctx context.Context, tenantID string, timeRange string) (*TenantPerformanceAnalytics, error) {
// Get events for performance analysis
events, err := s.repository.ListEvents(ctx, tenantID, 10000)
if err != nil {
return nil, fmt.Errorf("failed to get tenant events: %w", err)
}
// Parse time range
startDate, endDate := s.parseTimeRange(timeRange)
// Build performance analytics
analytics := &TenantPerformanceAnalytics{
TenantID: tenantID,
TimeRange: timeRange,
StartDate: startDate,
EndDate: endDate,
APIPerformance: &APIPerformance{},
ResourcePerformance: make(map[string]*ResourcePerformance),
Errors: []*ErrorEvent{},
SlowQueries: []*SlowQuery{},
}
// Process events
apiRequests := []*APIRequestEvent{}
for _, event := range events {
if event.Timestamp.Before(startDate) || event.Timestamp.After(endDate) {
continue
}
switch event.EventType {
case "api_request":
apiRequest := s.parseAPIRequestEvent(event)
if apiRequest != nil {
apiRequests = append(apiRequests, apiRequest)
}
case "error":
errorEvent := s.parseErrorEvent(event)
if errorEvent != nil {
analytics.Errors = append(analytics.Errors, errorEvent)
}
case "slow_query":
slowQuery := s.parseSlowQueryEvent(event)
if slowQuery != nil {
analytics.SlowQueries = append(analytics.SlowQueries, slowQuery)
}
}
}
// Calculate API performance metrics
analytics.APIPerformance = s.calculateAPIPerformance(apiRequests)
return analytics, nil
}
// GetCostAnalytics returns cost analytics for a tenant
func (s *TenantAnalyticsService) GetCostAnalytics(ctx context.Context, tenantID string, timeRange string) (*TenantCostAnalytics, error) {
// Parse time range
startDate, endDate := s.parseTimeRange(timeRange)
// Get tenant configuration to understand pricing
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
// Build cost analytics
analytics := &TenantCostAnalytics{
TenantID: tenantID,
TimeRange: timeRange,
StartDate: startDate,
EndDate: endDate,
TotalCost: 0,
CostByType: make(map[string]float64),
CostTrends: []*CostTrend{},
Predictions: &CostPrediction{},
Savings: &CostSavings{},
}
// Calculate costs based on usage and plan
plan := s.getTenantPlan(tenantID, config)
analytics.TotalCost = s.calculateTotalCost(plan, analytics.TimeRange)
analytics.CostByType = s.calculateCostByType(plan, analytics.TimeRange)
return analytics, nil
}
// GenerateTenantReport generates a comprehensive tenant report
func (s *TenantAnalyticsService) GenerateTenantReport(ctx context.Context, tenantID string, reportType string, timeRange string) (*TenantReport, error) {
report := &TenantReport{
TenantID: tenantID,
ReportType: reportType,
TimeRange: timeRange,
GeneratedAt: time.Now(),
}
switch reportType {
case "dashboard":
dashboard, err := s.GetTenantDashboard(ctx, tenantID)
if err != nil {
return nil, err
}
report.Dashboard = dashboard
case "usage":
usageAnalytics, err := s.GetUsageAnalytics(ctx, tenantID, timeRange)
if err != nil {
return nil, err
}
report.UsageAnalytics = usageAnalytics
case "performance":
performanceAnalytics, err := s.GetPerformanceAnalytics(ctx, tenantID, timeRange)
if err != nil {
return nil, err
}
report.PerformanceAnalytics = performanceAnalytics
case "cost":
costAnalytics, err := s.GetCostAnalytics(ctx, tenantID, timeRange)
if err != nil {
return nil, err
}
report.CostAnalytics = costAnalytics
default:
return nil, fmt.Errorf("unsupported report type: %s", reportType)
}
return report, nil
}
// Helper methods
func (s *TenantAnalyticsService) calculateHealthScore(usageStats *TenantUsageStats, errorRate float64) float64 {
score := 100.0
// Deduct points for high error rate
if errorRate > 10 {
score -= 30
} else if errorRate > 5 {
score -= 15
} else if errorRate > 1 {
score -= 5
}
// Deduct points for quota issues
for _, quotaStatus := range usageStats.QuotaStatus {
if quotaStatus.Critical {
score -= 20
} else if quotaStatus.Warning {
score -= 10
}
}
// Ensure score doesn't go below 0
if score < 0 {
score = 0
}
return score
}
func (s *TenantAnalyticsService) generateAlerts(usageStats *TenantUsageStats, errorRate float64) []string {
alerts := []string{}
// Error rate alerts
if errorRate > 10 {
alerts = append(alerts, "Critical: High error rate detected")
} else if errorRate > 5 {
alerts = append(alerts, "Warning: Elevated error rate")
}
// Quota alerts
for resourceType, quotaStatus := range usageStats.QuotaStatus {
if quotaStatus.Critical {
alerts = append(alerts, fmt.Sprintf("Critical: %s quota at %.1f%%", resourceType, quotaStatus.Percent))
} else if quotaStatus.Warning {
alerts = append(alerts, fmt.Sprintf("Warning: %s quota at %.1f%%", resourceType, quotaStatus.Percent))
}
}
return alerts
}
func (s *TenantAnalyticsService) parseTimeRange(timeRange string) (time.Time, time.Time) {
now := time.Now()
switch timeRange {
case "1h":
return now.Add(-1 * time.Hour), now
case "24h":
return now.Add(-24 * time.Hour), now
case "7d":
return now.Add(-7 * 24 * time.Hour), now
case "30d":
return now.Add(-30 * 24 * time.Hour), now
case "90d":
return now.Add(-90 * 24 * time.Hour), now
default:
return now.Add(-24 * time.Hour), now
}
}
func (s *TenantAnalyticsService) parseAPIRequestEvent(event *TenantEvent) *APIRequestEvent {
// Implementation depends on event structure
return &APIRequestEvent{
Timestamp: event.Timestamp,
Endpoint: "",
Method: "",
StatusCode: 200,
ResponseTime: 0,
UserID: event.UserID,
}
}
func (s *TenantAnalyticsService) parseErrorEvent(event *TenantEvent) *ErrorEvent {
// Implementation depends on event structure
return &ErrorEvent{
Timestamp: event.Timestamp,
Error: "",
Context: event.EventData,
UserID: event.UserID,
}
}
func (s *TenantAnalyticsService) parseSlowQueryEvent(event *TenantEvent) *SlowQuery {
// Implementation depends on event structure
return &SlowQuery{
Timestamp: event.Timestamp,
Query: "",
Duration: 0,
Context: event.EventData,
}
}
func (s *TenantAnalyticsService) calculateAPIPerformance(requests []*APIRequestEvent) *APIPerformance {
// Implementation would calculate performance metrics
return &APIPerformance{
TotalRequests: len(requests),
AverageResponseTime: 0,
P95ResponseTime: 0,
ErrorRate: 0,
RequestsPerSecond: 0,
}
}
func (s *TenantAnalyticsService) getTenantPlan(tenantID string, config *TenantConfig) TenantPlan {
// Extract plan from tenant config or database
return TenantPlanPro // Default
}
func (s *TenantAnalyticsService) calculateTotalCost(plan TenantPlan, timeRange string) float64 {
// Implementation would calculate cost based on plan and usage
return 0.0
}
func (s *TenantAnalyticsService) calculateCostByType(plan TenantPlan, timeRange string) map[string]float64 {
// Implementation would calculate cost breakdown by resource type
return make(map[string]float64)
}
// Data structures for analytics
type TenantDashboard struct {
TenantID string `json:"tenant_id"`
UsageStats *TenantUsageStats `json:"usage_stats"`
Metrics *TenantMetrics `json:"metrics"`
RecentEvents []*TenantEvent `json:"recent_events"`
QuotaStatus []*TenantUsage `json:"quota_status"`
LastUpdated time.Time `json:"last_updated"`
}
type TenantUsageAnalytics struct {
TenantID string `json:"tenant_id"`
TimeRange string `json:"time_range"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
UsageByType map[string]*ResourceUsageAnalytics `json:"usage_by_type"`
Trends map[string][]*UsageTrend `json:"trends"`
Peaks map[string]*UsagePeak `json:"peaks"`
}
type ResourceUsageAnalytics struct {
ResourceType string `json:"resource_type"`
TotalUsage int `json:"total_usage"`
AverageUsage int `json:"average_usage"`
PeakUsage int `json:"peak_usage"`
PeakTime time.Time `json:"peak_time"`
}
type UsageTrend struct {
Timestamp time.Time `json:"timestamp"`
Usage int `json:"usage"`
}
type UsagePeak struct {
Timestamp time.Time `json:"timestamp"`
Usage int `json:"usage"`
Context map[string]interface{} `json:"context"`
}
type TenantPerformanceAnalytics struct {
TenantID string `json:"tenant_id"`
TimeRange string `json:"time_range"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
APIPerformance *APIPerformance `json:"api_performance"`
ResourcePerformance map[string]*ResourcePerformance `json:"resource_performance"`
Errors []*ErrorEvent `json:"errors"`
SlowQueries []*SlowQuery `json:"slow_queries"`
}
type APIPerformance struct {
TotalRequests int `json:"total_requests"`
AverageResponseTime float64 `json:"average_response_time"`
P95ResponseTime float64 `json:"p95_response_time"`
ErrorRate float64 `json:"error_rate"`
RequestsPerSecond float64 `json:"requests_per_second"`
}
type ResourcePerformance struct {
ResourceType string `json:"resource_type"`
AvgLatency float64 `json:"avg_latency"`
P95Latency float64 `json:"p95_latency"`
Throughput float64 `json:"throughput"`
ErrorRate float64 `json:"error_rate"`
}
type APIRequestEvent struct {
Timestamp time.Time `json:"timestamp"`
Endpoint string `json:"endpoint"`
Method string `json:"method"`
StatusCode int `json:"status_code"`
ResponseTime time.Duration `json:"response_time"`
UserID string `json:"user_id"`
}
type ErrorEvent struct {
Timestamp time.Time `json:"timestamp"`
Error string `json:"error"`
Context map[string]interface{} `json:"context"`
UserID string `json:"user_id"`
}
type SlowQuery struct {
Timestamp time.Time `json:"timestamp"`
Query string `json:"query"`
Duration time.Duration `json:"duration"`
Context map[string]interface{} `json:"context"`
}
type TenantCostAnalytics struct {
TenantID string `json:"tenant_id"`
TimeRange string `json:"time_range"`
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
TotalCost float64 `json:"total_cost"`
CostByType map[string]float64 `json:"cost_by_type"`
CostTrends []*CostTrend `json:"cost_trends"`
Predictions *CostPrediction `json:"predictions"`
Savings *CostSavings `json:"savings"`
}
type CostTrend struct {
Timestamp time.Time `json:"timestamp"`
Cost float64 `json:"cost"`
}
type CostPrediction struct {
PredictedCost float64 `json:"predicted_cost"`
Confidence float64 `json:"confidence"`
Factors []string `json:"factors"`
}
type CostSavings struct {
PotentialSavings float64 `json:"potential_savings"`
Recommendations []string `json:"recommendations"`
Optimizations map[string]float64 `json:"optimizations"`
}
type TenantReport struct {
TenantID string `json:"tenant_id"`
ReportType string `json:"report_type"`
TimeRange string `json:"time_range"`
GeneratedAt time.Time `json:"generated_at"`
Dashboard *TenantDashboard `json:"dashboard,omitempty"`
UsageAnalytics *TenantUsageAnalytics `json:"usage_analytics,omitempty"`
PerformanceAnalytics *TenantPerformanceAnalytics `json:"performance_analytics,omitempty"`
CostAnalytics *TenantCostAnalytics `json:"cost_analytics,omitempty"`
}
@@ -1,264 +0,0 @@
package tenant
import (
"context"
"fmt"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/currency"
"github.com/sirupsen/logrus"
)
// TenantAwareCurrencyRepository wraps the currency repository with tenant isolation
type TenantAwareCurrencyRepository struct {
baseRepo currency.Repository
tenantID string
logger *logrus.Logger
}
// NewTenantAwareCurrencyRepository creates a new tenant-aware currency repository
func NewTenantAwareCurrencyRepository(baseRepo currency.Repository, tenantID string, logger *logrus.Logger) currency.Repository {
return &TenantAwareCurrencyRepository{
baseRepo: baseRepo,
tenantID: tenantID,
logger: logger,
}
}
// WithTenant creates a new instance with the specified tenant ID
func (r *TenantAwareCurrencyRepository) WithTenant(tenantID string) currency.Repository {
return &TenantAwareCurrencyRepository{
baseRepo: r.baseRepo,
tenantID: tenantID,
logger: r.logger,
}
}
// Tenant-aware currency repository methods
func (r *TenantAwareCurrencyRepository) CreateCurrency(ctx context.Context, currency *currency.Currency) error {
// Add tenant context
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.CreateCurrency(ctx, currency)
}
func (r *TenantAwareCurrencyRepository) GetCurrency(ctx context.Context, code string) (*currency.Currency, error) {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.GetCurrency(ctx, code)
}
func (r *TenantAwareCurrencyRepository) UpdateCurrency(ctx context.Context, currency *currency.Currency) error {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.UpdateCurrency(ctx, currency)
}
func (r *TenantAwareCurrencyRepository) DeleteCurrency(ctx context.Context, code string) error {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.DeleteCurrency(ctx, code)
}
func (r *TenantAwareCurrencyRepository) ListCurrencies(ctx context.Context, filter *currency.CurrencyFilter) ([]*currency.Currency, error) {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.ListCurrencies(ctx, filter)
}
func (r *TenantAwareCurrencyRepository) CountCurrencies(ctx context.Context, filter *currency.CurrencyFilter) (int, error) {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.CountCurrencies(ctx, filter)
}
func (r *TenantAwareCurrencyRepository) CreateExchangeRate(ctx context.Context, rate *currency.ExchangeRate) error {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.CreateExchangeRate(ctx, rate)
}
func (r *TenantAwareCurrencyRepository) GetExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*currency.ExchangeRate, error) {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.GetExchangeRate(ctx, fromCurrency, toCurrency)
}
func (r *TenantAwareCurrencyRepository) UpdateExchangeRate(ctx context.Context, rate *currency.ExchangeRate) error {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.UpdateExchangeRate(ctx, rate)
}
func (r *TenantAwareCurrencyRepository) DeleteExchangeRate(ctx context.Context, id string) error {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.DeleteExchangeRate(ctx, id)
}
func (r *TenantAwareCurrencyRepository) ListExchangeRates(ctx context.Context, filter *currency.ExchangeRateFilter) ([]*currency.ExchangeRate, error) {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.ListExchangeRates(ctx, filter)
}
func (r *TenantAwareCurrencyRepository) GetLatestExchangeRate(ctx context.Context, fromCurrency, toCurrency string) (*currency.ExchangeRate, error) {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.GetLatestExchangeRate(ctx, fromCurrency, toCurrency)
}
func (r *TenantAwareCurrencyRepository) CreateTransaction(ctx context.Context, transaction *currency.Transaction) error {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.CreateTransaction(ctx, transaction)
}
func (r *TenantAwareCurrencyRepository) GetTransaction(ctx context.Context, id string) (*currency.Transaction, error) {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.GetTransaction(ctx, id)
}
func (r *TenantAwareCurrencyRepository) UpdateTransaction(ctx context.Context, transaction *currency.Transaction) error {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.UpdateTransaction(ctx, transaction)
}
func (r *TenantAwareCurrencyRepository) DeleteTransaction(ctx context.Context, id string) error {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.DeleteTransaction(ctx, id)
}
func (r *TenantAwareCurrencyRepository) ListTransactions(ctx context.Context, filter *currency.TransactionFilter) ([]*currency.Transaction, error) {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.ListTransactions(ctx, filter)
}
func (r *TenantAwareCurrencyRepository) CountTransactions(ctx context.Context, filter *currency.TransactionFilter) (int, error) {
ctx = SetTenantInContext(ctx, r.tenantID)
return r.baseRepo.CountTransactions(ctx, filter)
}
// TenantIntegrationManager manages tenant integration across all services
type TenantIntegrationManager struct {
tenantService Service
currencyService currency.BillingService
logger *logrus.Logger
}
// NewTenantIntegrationManager creates a new tenant integration manager
func NewTenantIntegrationManager(
tenantService Service,
currencyService currency.BillingService,
logger *logrus.Logger,
) *TenantIntegrationManager {
return &TenantIntegrationManager{
tenantService: tenantService,
currencyService: currencyService,
logger: logger,
}
}
// GetTenantAwareServices returns tenant-aware service instances
func (m *TenantIntegrationManager) GetTenantAwareServices(ctx context.Context, tenantID string) (*TenantAwareServices, error) {
// Validate tenant
tenantCtx, err := m.tenantService.ValidateTenantAccess(ctx, tenantID, "")
if err != nil {
return nil, fmt.Errorf("failed to validate tenant: %w", err)
}
// Get tenant configuration
config, err := m.tenantService.GetTenantConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
// Create tenant-aware services
services := &TenantAwareServices{
TenantID: tenantID,
TenantContext: tenantCtx,
Config: config,
CurrencyService: m.wrapCurrencyService(tenantID),
}
return services, nil
}
// TenantAwareServices provides tenant-aware service instances
type TenantAwareServices struct {
TenantID string
TenantContext *TenantContext
Config *TenantConfig
CurrencyService currency.BillingService
}
// wrapCurrencyService creates a tenant-aware currency service
func (m *TenantIntegrationManager) wrapCurrencyService(tenantID string) currency.BillingService {
// This would wrap the existing currency service with tenant isolation
// Implementation depends on the actual currency service structure
return m.currencyService // Placeholder - would need actual wrapping
}
// TenantResourceQuotaChecker checks resource quotas before operations
type TenantResourceQuotaChecker struct {
tenantService Service
logger *logrus.Logger
}
// NewTenantResourceQuotaChecker creates a new quota checker
func NewTenantResourceQuotaChecker(tenantService Service, logger *logrus.Logger) *TenantResourceQuotaChecker {
return &TenantResourceQuotaChecker{
tenantService: tenantService,
logger: logger,
}
}
// CheckQuota checks if tenant has sufficient quota for a resource operation
func (c *TenantResourceQuotaChecker) CheckQuota(ctx context.Context, tenantID, resourceType string, count int) error {
return c.tenantService.CheckQuota(ctx, tenantID, resourceType, count)
}
// UpdateUsage updates resource usage after an operation
func (c *TenantResourceQuotaChecker) UpdateUsage(ctx context.Context, tenantID, resourceType string, count int) error {
return c.tenantService.UpdateUsage(ctx, tenantID, resourceType, count)
}
// TenantEventLogger logs tenant events for audit purposes
type TenantEventLogger struct {
tenantService Service
logger *logrus.Logger
}
// NewTenantEventLogger creates a new tenant event logger
func NewTenantEventLogger(tenantService Service, logger *logrus.Logger) *TenantEventLogger {
return &TenantEventLogger{
tenantService: tenantService,
logger: logger,
}
}
// LogResourceAccess logs resource access events
func (l *TenantEventLogger) LogResourceAccess(ctx context.Context, tenantID, userID, resourceType, resourceID, action string) {
event := &TenantEvent{
ID: generateID(),
TenantID: tenantID,
UserID: userID,
EventType: TenantEventType("resource_access"),
EventData: map[string]interface{}{
"resource_type": resourceType,
"resource_id": resourceID,
"action": action,
},
Timestamp: getCurrentTimestamp(),
}
if err := l.tenantService.LogTenantEvent(ctx, event); err != nil {
l.logger.WithError(err).Error("Failed to log resource access event")
}
}
// LogQuotaViolation logs quota violation events
func (l *TenantEventLogger) LogQuotaViolation(ctx context.Context, tenantID, resourceType string, usage, limit int) {
event := &TenantEvent{
ID: generateID(),
TenantID: tenantID,
UserID: "",
EventType: TenantEventQuotaExceeded,
EventData: map[string]interface{}{
"resource_type": resourceType,
"usage": usage,
"limit": limit,
},
Timestamp: getCurrentTimestamp(),
}
if err := l.tenantService.LogTenantEvent(ctx, event); err != nil {
l.logger.WithError(err).Error("Failed to log quota violation event")
}
}
@@ -1,771 +0,0 @@
package tenant
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt"
)
// ServiceImpl implements the tenant service interface
type ServiceImpl struct {
repository Repository
rateLimiter RateLimiter
eventPublisher EventPublisher
configManager ConfigManager
auditLogger AuditLogger
metricsCollector MetricsCollector
logger *logrus.Logger
}
// NewService creates a new tenant service
func NewService(
repository Repository,
rateLimiter RateLimiter,
eventPublisher EventPublisher,
configManager ConfigManager,
auditLogger AuditLogger,
metricsCollector MetricsCollector,
logger *logrus.Logger,
) Service {
return &ServiceImpl{
repository: repository,
rateLimiter: rateLimiter,
eventPublisher: eventPublisher,
configManager: configManager,
auditLogger: auditLogger,
metricsCollector: metricsCollector,
logger: logger,
}
}
// CreateTenant creates a new tenant
func (s *ServiceImpl) CreateTenant(ctx context.Context, req *CreateTenantRequest) (*Tenant, error) {
// Validate request
if err := s.validateCreateTenantRequest(req); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Check if domain already exists
existing, err := s.repository.GetTenantByDomain(ctx, req.Domain)
if err == nil && existing != nil {
return nil, errors.New("domain already exists")
}
// Create tenant
tenant := &Tenant{
ID: uuid.New().String(),
Name: req.Name,
Domain: req.Domain,
Status: TenantStatusActive,
Plan: req.Plan,
MaxUsers: req.MaxUsers,
MaxProfiles: req.MaxProfiles,
MaxCarriers: req.MaxCarriers,
Settings: req.Settings,
Metadata: req.Metadata,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Set default settings if not provided
if tenant.Settings == nil {
tenant.Settings = s.getDefaultSettings(req.Plan)
}
// Save tenant
if err := s.repository.CreateTenant(ctx, tenant); err != nil {
s.logger.WithError(err).Error("Failed to create tenant")
return nil, fmt.Errorf("failed to create tenant: %w", err)
}
// Create initial configuration
config := &TenantConfig{
TenantID: tenant.ID,
Config: make(map[string]interface{}),
Settings: tenant.Settings,
Quotas: s.getDefaultQuotas(req.Plan),
Features: s.getDefaultFeatures(req.Plan),
}
if err := s.repository.UpdateConfig(ctx, config); err != nil {
s.logger.WithError(err).Error("Failed to create tenant config")
}
// Publish tenant created event
event := &TenantEvent{
ID: uuid.New().String(),
TenantID: tenant.ID,
UserID: "",
EventType: TenantEventCreated,
EventData: map[string]interface{}{
"tenant_id": tenant.ID,
"name": tenant.Name,
"domain": tenant.Domain,
"plan": tenant.Plan,
},
Timestamp: time.Now(),
}
if err := s.repository.CreateEvent(ctx, event); err != nil {
s.logger.WithError(err).Error("Failed to create tenant event")
}
s.logger.WithFields(logrus.Fields{
"tenant_id": tenant.ID,
"name": tenant.Name,
"domain": tenant.Domain,
}).Info("Tenant created successfully")
return tenant, nil
}
// GetTenant retrieves a tenant by ID
func (s *ServiceImpl) GetTenant(ctx context.Context, id string) (*Tenant, error) {
tenant, err := s.repository.GetTenant(ctx, id)
if err != nil {
s.logger.WithError(err).WithField("tenant_id", id).Error("Failed to get tenant")
return nil, err
}
return tenant, nil
}
// GetTenantByDomain retrieves a tenant by domain
func (s *ServiceImpl) GetTenantByDomain(ctx context.Context, domain string) (*Tenant, error) {
tenant, err := s.repository.GetTenantByDomain(ctx, domain)
if err != nil {
s.logger.WithError(err).WithField("domain", domain).Error("Failed to get tenant by domain")
return nil, err
}
return tenant, nil
}
// UpdateTenant updates an existing tenant
func (s *ServiceImpl) UpdateTenant(ctx context.Context, id string, req *UpdateTenantRequest) (*Tenant, error) {
// Get existing tenant
tenant, err := s.repository.GetTenant(ctx, id)
if err != nil {
return nil, err
}
// Apply updates
if req.Name != nil {
tenant.Name = *req.Name
}
if req.Status != nil {
tenant.Status = *req.Status
}
if req.Plan != nil {
tenant.Plan = *req.Plan
}
if req.MaxUsers != nil {
tenant.MaxUsers = *req.MaxUsers
}
if req.MaxProfiles != nil {
tenant.MaxProfiles = *req.MaxProfiles
}
if req.MaxCarriers != nil {
tenant.MaxCarriers = *req.MaxCarriers
}
if req.Settings != nil {
tenant.Settings = req.Settings
}
if req.Metadata != nil {
tenant.Metadata = req.Metadata
}
tenant.UpdatedAt = time.Now()
// Save tenant
if err := s.repository.UpdateTenant(ctx, tenant); err != nil {
s.logger.WithError(err).Error("Failed to update tenant")
return nil, fmt.Errorf("failed to update tenant: %w", err)
}
// Log tenant updated event
event := &TenantEvent{
ID: uuid.New().String(),
TenantID: tenant.ID,
UserID: "",
EventType: TenantEventUpdated,
EventData: map[string]interface{}{
"tenant_id": tenant.ID,
"updates": req,
},
Timestamp: time.Now(),
}
if err := s.repository.CreateEvent(ctx, event); err != nil {
s.logger.WithError(err).Error("Failed to create tenant event")
}
s.logger.WithField("tenant_id", tenant.ID).Info("Tenant updated successfully")
return tenant, nil
}
// DeleteTenant deletes a tenant
func (s *ServiceImpl) DeleteTenant(ctx context.Context, id string) error {
// Get tenant for logging
tenant, err := s.repository.GetTenant(ctx, id)
if err != nil {
return err
}
// Delete tenant
if err := s.repository.DeleteTenant(ctx, id); err != nil {
s.logger.WithError(err).Error("Failed to delete tenant")
return fmt.Errorf("failed to delete tenant: %w", err)
}
// Log tenant deleted event
event := &TenantEvent{
ID: uuid.New().String(),
TenantID: id,
UserID: "",
EventType: TenantEventDeleted,
EventData: map[string]interface{}{
"tenant_id": id,
"name": tenant.Name,
},
Timestamp: time.Now(),
}
if err := s.repository.CreateEvent(ctx, event); err != nil {
s.logger.WithError(err).Error("Failed to create tenant event")
}
s.logger.WithField("tenant_id", id).Info("Tenant deleted successfully")
return nil
}
// ListTenants lists tenants with filtering
func (s *ServiceImpl) ListTenants(ctx context.Context, filter *TenantFilter) ([]*Tenant, error) {
tenants, err := s.repository.ListTenants(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to list tenants")
return nil, err
}
return tenants, nil
}
// AddUserToTenant adds a user to a tenant
func (s *ServiceImpl) AddUserToTenant(ctx context.Context, req *CreateTenantUserRequest) (*TenantUser, error) {
// Validate request
if err := s.validateCreateTenantUserRequest(req); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
// Check if user already exists
existing, err := s.repository.GetTenantUser(ctx, req.TenantID, req.UserID)
if err == nil && existing != nil {
return nil, errors.New("user already exists in tenant")
}
// Create tenant user
tenantUser := &TenantUser{
ID: uuid.New().String(),
TenantID: req.TenantID,
UserID: req.UserID,
Email: req.Email,
Role: req.Role,
Status: TenantUserStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save tenant user
if err := s.repository.CreateTenantUser(ctx, tenantUser); err != nil {
s.logger.WithError(err).Error("Failed to create tenant user")
return nil, fmt.Errorf("failed to add user to tenant: %w", err)
}
// Log user added event
event := &TenantEvent{
ID: uuid.New().String(),
TenantID: req.TenantID,
UserID: req.UserID,
EventType: TenantEventUserAdded,
EventData: map[string]interface{}{
"tenant_id": req.TenantID,
"user_id": req.UserID,
"email": req.Email,
"role": req.Role,
},
Timestamp: time.Now(),
}
if err := s.repository.CreateEvent(ctx, event); err != nil {
s.logger.WithError(err).Error("Failed to create tenant event")
}
s.logger.WithFields(logrus.Fields{
"tenant_id": req.TenantID,
"user_id": req.UserID,
"role": req.Role,
}).Info("User added to tenant successfully")
return tenantUser, nil
}
// GetTenantUser retrieves a tenant user
func (s *ServiceImpl) GetTenantUser(ctx context.Context, tenantID, userID string) (*TenantUser, error) {
user, err := s.repository.GetTenantUser(ctx, tenantID, userID)
if err != nil {
s.logger.WithError(err).WithFields(logrus.Fields{
"tenant_id": tenantID,
"user_id": userID,
}).Error("Failed to get tenant user")
return nil, err
}
return user, nil
}
// UpdateTenantUser updates a tenant user
func (s *ServiceImpl) UpdateTenantUser(ctx context.Context, tenantID, userID string, req *UpdateTenantUserRequest) (*TenantUser, error) {
// Get existing user
user, err := s.repository.GetTenantUser(ctx, tenantID, userID)
if err != nil {
return nil, err
}
// Apply updates
if req.Role != nil {
user.Role = *req.Role
}
if req.Status != nil {
user.Status = *req.Status
}
user.UpdatedAt = time.Now()
// Save user
if err := s.repository.UpdateTenantUser(ctx, user); err != nil {
s.logger.WithError(err).Error("Failed to update tenant user")
return nil, fmt.Errorf("failed to update tenant user: %w", err)
}
// Log user updated event
event := &TenantEvent{
ID: uuid.New().String(),
TenantID: tenantID,
UserID: userID,
EventType: TenantEventUserUpdated,
EventData: map[string]interface{}{
"tenant_id": tenantID,
"user_id": userID,
"updates": req,
},
Timestamp: time.Now(),
}
if err := s.repository.CreateEvent(ctx, event); err != nil {
s.logger.WithError(err).Error("Failed to create tenant event")
}
s.logger.WithFields(logrus.Fields{
"tenant_id": tenantID,
"user_id": userID,
}).Info("Tenant user updated successfully")
return user, nil
}
// RemoveUserFromTenant removes a user from a tenant
func (s *ServiceImpl) RemoveUserFromTenant(ctx context.Context, tenantID, userID string) error {
// Delete tenant user
if err := s.repository.DeleteTenantUser(ctx, tenantID, userID); err != nil {
s.logger.WithError(err).Error("Failed to remove user from tenant")
return fmt.Errorf("failed to remove user from tenant: %w", err)
}
// Log user removed event
event := &TenantEvent{
ID: uuid.New().String(),
TenantID: tenantID,
UserID: userID,
EventType: TenantEventUserRemoved,
EventData: map[string]interface{}{
"tenant_id": tenantID,
"user_id": userID,
},
Timestamp: time.Now(),
}
if err := s.repository.CreateEvent(ctx, event); err != nil {
s.logger.WithError(err).Error("Failed to create tenant event")
}
s.logger.WithFields(logrus.Fields{
"tenant_id": tenantID,
"user_id": userID,
}).Info("User removed from tenant successfully")
return nil
}
// ListTenantUsers lists tenant users with filtering
func (s *ServiceImpl) ListTenantUsers(ctx context.Context, filter *TenantUserFilter) ([]*TenantUser, error) {
users, err := s.repository.ListTenantUsers(ctx, filter)
if err != nil {
s.logger.WithError(err).Error("Failed to list tenant users")
return nil, err
}
return users, nil
}
// CreateAPIKey creates a new API key for a tenant
func (s *ServiceImpl) CreateAPIKey(ctx context.Context, tenantID string, req *CreateAPIKeyRequest) (*TenantAPIKey, string, error) {
// Validate request
if err := s.validateCreateAPIKeyRequest(req); err != nil {
return nil, "", fmt.Errorf("validation failed: %w", err)
}
// Generate API key
apiKey := s.generateAPIKey()
keyHash, err := s.hashAPIKey(apiKey)
if err != nil {
return nil, "", fmt.Errorf("failed to hash API key: %w", err)
}
// Create API key record
apiKeyRecord := &TenantAPIKey{
ID: uuid.New().String(),
TenantID: tenantID,
Name: req.Name,
KeyHash: keyHash,
KeyPrefix: apiKey[:8],
Permissions: req.Permissions,
RateLimit: req.RateLimit,
ExpiresAt: req.ExpiresAt,
Status: APIKeyStatusActive,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save API key
if err := s.repository.CreateAPIKey(ctx, apiKeyRecord); err != nil {
s.logger.WithError(err).Error("Failed to create API key")
return nil, "", fmt.Errorf("failed to create API key: %w", err)
}
// Log API key created event
event := &TenantEvent{
ID: uuid.New().String(),
TenantID: tenantID,
UserID: "",
EventType: TenantEventAPIKeyCreated,
EventData: map[string]interface{}{
"tenant_id": tenantID,
"key_id": apiKeyRecord.ID,
"name": req.Name,
},
Timestamp: time.Now(),
}
if err := s.repository.CreateEvent(ctx, event); err != nil {
s.logger.WithError(err).Error("Failed to create tenant event")
}
s.logger.WithFields(logrus.Fields{
"tenant_id": tenantID,
"key_id": apiKeyRecord.ID,
"name": req.Name,
}).Info("API key created successfully")
return apiKeyRecord, apiKey, nil
}
// GetAPIKey retrieves an API key by ID
func (s *ServiceImpl) GetAPIKey(ctx context.Context, id string) (*TenantAPIKey, error) {
apiKey, err := s.repository.GetAPIKey(ctx, id)
if err != nil {
s.logger.WithError(err).WithField("key_id", id).Error("Failed to get API key")
return nil, err
}
return apiKey, nil
}
// UpdateAPIKey updates an API key
func (s *ServiceImpl) UpdateAPIKey(ctx context.Context, id string, req *UpdateAPIKeyRequest) (*TenantAPIKey, error) {
// Get existing API key
apiKey, err := s.repository.GetAPIKey(ctx, id)
if err != nil {
return nil, err
}
// Apply updates
if req.Name != nil {
apiKey.Name = *req.Name
}
if req.Permissions != nil {
apiKey.Permissions = req.Permissions
}
if req.RateLimit != nil {
apiKey.RateLimit = *req.RateLimit
}
if req.ExpiresAt != nil {
apiKey.ExpiresAt = req.ExpiresAt
}
if req.Status != nil {
apiKey.Status = *req.Status
}
apiKey.UpdatedAt = time.Now()
// Save API key
if err := s.repository.UpdateAPIKey(ctx, apiKey); err != nil {
s.logger.WithError(err).Error("Failed to update API key")
return nil, fmt.Errorf("failed to update API key: %w", err)
}
s.logger.WithField("key_id", id).Info("API key updated successfully")
return apiKey, nil
}
// DeleteAPIKey deletes an API key
func (s *ServiceImpl) DeleteAPIKey(ctx context.Context, id string) error {
// Get API key for logging
apiKey, err := s.repository.GetAPIKey(ctx, id)
if err != nil {
return err
}
// Delete API key
if err := s.repository.DeleteAPIKey(ctx, id); err != nil {
s.logger.WithError(err).Error("Failed to delete API key")
return fmt.Errorf("failed to delete API key: %w", err)
}
// Log API key revoked event
event := &TenantEvent{
ID: uuid.New().String(),
TenantID: apiKey.TenantID,
UserID: "",
EventType: TenantEventAPIKeyRevoked,
EventData: map[string]interface{}{
"tenant_id": apiKey.TenantID,
"key_id": id,
"name": apiKey.Name,
},
Timestamp: time.Now(),
}
if err := s.repository.CreateEvent(ctx, event); err != nil {
s.logger.WithError(err).Error("Failed to create tenant event")
}
s.logger.WithField("key_id", id).Info("API key deleted successfully")
return nil
}
// ListAPIKeys lists API keys for a tenant
func (s *ServiceImpl) ListAPIKeys(ctx context.Context, tenantID string) ([]*TenantAPIKey, error) {
apiKeys, err := s.repository.ListAPIKeys(ctx, tenantID)
if err != nil {
s.logger.WithError(err).WithField("tenant_id", tenantID).Error("Failed to list API keys")
return nil, err
}
return apiKeys, nil
}
// ValidateAPIKey validates an API key and returns the key record
func (s *ServiceImpl) ValidateAPIKey(ctx context.Context, key string) (*TenantAPIKey, error) {
// Hash the provided key
keyHash, err := s.hashAPIKey(key)
if err != nil {
return nil, fmt.Errorf("failed to hash API key: %w", err)
}
// Look up API key by hash
apiKey, err := s.repository.GetAPIKeyByHash(ctx, keyHash)
if err != nil {
return nil, err
}
// Check if key is active
if apiKey.Status != APIKeyStatusActive {
return nil, errors.New("API key is not active")
}
// Check if key has expired
if apiKey.ExpiresAt != nil && time.Now().After(*apiKey.ExpiresAt) {
return nil, errors.New("API key has expired")
}
// Update last used timestamp
now := time.Now()
apiKey.LastUsed = &now
if err := s.repository.UpdateAPIKey(ctx, apiKey); err != nil {
s.logger.WithError(err).Error("Failed to update API key last used")
}
return apiKey, nil
}
// Helper methods
func (s *ServiceImpl) validateCreateTenantRequest(req *CreateTenantRequest) error {
if req.Name == "" {
return errors.New("name is required")
}
if req.Domain == "" {
return errors.New("domain is required")
}
if req.Plan == "" {
return errors.New("plan is required")
}
return nil
}
func (s *ServiceImpl) validateCreateTenantUserRequest(req *CreateTenantUserRequest) error {
if req.TenantID == "" {
return errors.New("tenant ID is required")
}
if req.UserID == "" {
return errors.New("user ID is required")
}
if req.Email == "" {
return errors.New("email is required")
}
if req.Role == "" {
return errors.New("role is required")
}
return nil
}
func (s *ServiceImpl) validateCreateAPIKeyRequest(req *CreateAPIKeyRequest) error {
if req.Name == "" {
return errors.New("name is required")
}
return nil
}
func (s *ServiceImpl) getDefaultSettings(plan TenantPlan) *TenantSettings {
settings := &TenantSettings{
DefaultCurrency: "USD",
SupportedCurrencies: []string{"USD", "EUR", "GBP"},
APIRateLimitPerMinute: 60,
APIRateLimitPerHour: 1000,
SessionTimeout: 120, // 2 hours
DataRetentionDays: 90,
ComplianceRegions: []string{"US", "EU"},
}
switch plan {
case TenantPlanFree:
settings.EnableMultiCurrency = false
settings.EnableAdvancedAnalytics = false
settings.EnableAPIAccess = true
settings.EnableWebhooks = false
settings.Require2FA = false
case TenantPlanBasic:
settings.EnableMultiCurrency = true
settings.EnableAdvancedAnalytics = false
settings.EnableAPIAccess = true
settings.EnableWebhooks = false
settings.Require2FA = false
case TenantPlanPro:
settings.EnableMultiCurrency = true
settings.EnableAdvancedAnalytics = true
settings.EnableAPIAccess = true
settings.EnableWebhooks = true
settings.Require2FA = true
case TenantPlanEnterprise:
settings.EnableMultiCurrency = true
settings.EnableAdvancedAnalytics = true
settings.EnableAPIAccess = true
settings.EnableWebhooks = true
settings.Require2FA = true
settings.APIRateLimitPerMinute = 1000
settings.APIRateLimitPerHour = 10000
}
return settings
}
func (s *ServiceImpl) getDefaultQuotas(plan TenantPlan) []ResourceQuota {
quotas := []ResourceQuota{}
switch plan {
case TenantPlanFree:
quotas = append(quotas, ResourceQuota{ResourceType: "users", Limit: 5, Period: "monthly"})
quotas = append(quotas, ResourceQuota{ResourceType: "profiles", Limit: 100, Period: "monthly"})
quotas = append(quotas, ResourceQuota{ResourceType: "carriers", Limit: 3, Period: "monthly"})
case TenantPlanBasic:
quotas = append(quotas, ResourceQuota{ResourceType: "users", Limit: 25, Period: "monthly"})
quotas = append(quotas, ResourceQuota{ResourceType: "profiles", Limit: 1000, Period: "monthly"})
quotas = append(quotas, ResourceQuota{ResourceType: "carriers", Limit: 10, Period: "monthly"})
case TenantPlanPro:
quotas = append(quotas, ResourceQuota{ResourceType: "users", Limit: 100, Period: "monthly"})
quotas = append(quotas, ResourceQuota{ResourceType: "profiles", Limit: 10000, Period: "monthly"})
quotas = append(quotas, ResourceQuota{ResourceType: "carriers", Limit: 50, Period: "monthly"})
case TenantPlanEnterprise:
quotas = append(quotas, ResourceQuota{ResourceType: "users", Limit: -1, Period: "monthly"}) // Unlimited
quotas = append(quotas, ResourceQuota{ResourceType: "profiles", Limit: -1, Period: "monthly"}) // Unlimited
quotas = append(quotas, ResourceQuota{ResourceType: "carriers", Limit: -1, Period: "monthly"}) // Unlimited
}
return quotas
}
func (s *ServiceImpl) getDefaultFeatures(plan TenantPlan) map[string]bool {
features := map[string]bool{
"multi_currency": false,
"advanced_analytics": false,
"api_access": true,
"webhooks": false,
"custom_branding": false,
"priority_support": false,
}
switch plan {
case TenantPlanBasic:
features["multi_currency"] = true
case TenantPlanPro:
features["multi_currency"] = true
features["advanced_analytics"] = true
features["webhooks"] = true
features["custom_branding"] = true
case TenantPlanEnterprise:
features["multi_currency"] = true
features["advanced_analytics"] = true
features["webhooks"] = true
features["custom_branding"] = true
features["priority_support"] = true
}
return features
}
func (s *ServiceImpl) generateAPIKey() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
// Fallback to less secure method if crypto/rand fails
return uuid.New().String()
}
return "tk_" + hex.EncodeToString(bytes)
}
func (s *ServiceImpl) hashAPIKey(key string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(key), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
@@ -1,476 +0,0 @@
package tenant
import (
"context"
"errors"
"fmt"
"time"
)
// CheckQuota checks if tenant has sufficient quota for a resource
func (s *ServiceImpl) CheckQuota(ctx context.Context, tenantID, resourceType string, count int) error {
// Get current usage
usage, err := s.repository.GetUsage(ctx, tenantID, resourceType)
if err != nil {
// If no usage record exists, create one
usage = &TenantUsage{
ID: generateID(),
TenantID: tenantID,
ResourceType: resourceType,
ResourceCount: 0,
QuotaUsed: 0,
QuotaRemaining: 0,
PeriodStart: time.Now().Truncate(24 * time.Hour),
PeriodEnd: time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
// Get tenant configuration
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return fmt.Errorf("failed to get tenant config: %w", err)
}
// Find quota for resource type
var quotaLimit int
for _, quota := range config.Quotas {
if quota.ResourceType == resourceType {
quotaLimit = quota.Limit
break
}
}
// Check if quota is unlimited (-1)
if quotaLimit == -1 {
return nil
}
// Check if adding count would exceed quota
if usage.QuotaUsed+count > quotaLimit {
// Log quota exceeded event
event := &TenantEvent{
ID: generateID(),
TenantID: tenantID,
UserID: "",
EventType: TenantEventQuotaExceeded,
EventData: map[string]interface{}{
"resource_type": resourceType,
"requested": count,
"current_usage": usage.QuotaUsed,
"quota_limit": quotaLimit,
},
Timestamp: time.Now(),
}
if err := s.repository.CreateEvent(ctx, event); err != nil {
s.logger.WithError(err).Error("Failed to create quota exceeded event")
}
return fmt.Errorf("quota exceeded for %s: %d/%d used", resourceType, usage.QuotaUsed, quotaLimit)
}
return nil
}
// GetUsageStats retrieves usage statistics for a tenant
func (s *ServiceImpl) GetUsageStats(ctx context.Context, tenantID string) (*TenantUsageStats, error) {
// Get usage records
usageFilter := &TenantUsageFilter{
TenantID: tenantID,
}
usageRecords, err := s.repository.ListUsage(ctx, usageFilter)
if err != nil {
return nil, fmt.Errorf("failed to get usage records: %w", err)
}
// Get tenant configuration
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
// Get tenant users
userFilter := &TenantUserFilter{
TenantID: tenantID,
Status: TenantUserStatusActive,
}
users, err := s.repository.ListTenantUsers(ctx, userFilter)
if err != nil {
return nil, fmt.Errorf("failed to get tenant users: %w", err)
}
// Build usage stats
stats := &TenantUsageStats{
TenantID: tenantID,
TotalUsers: len(users),
ActiveUsers: len(users),
ResourceBreakdown: make(map[string]int64),
QuotaStatus: make(map[string]QuotaStatus),
}
// Process usage records
for _, usage := range usageRecords {
stats.ResourceBreakdown[usage.ResourceType] = int64(usage.QuotaUsed)
// Calculate quota status
var quotaLimit int
for _, quota := range config.Quotas {
if quota.ResourceType == usage.ResourceType {
quotaLimit = quota.Limit
break
}
}
quotaStatus := QuotaStatus{
Used: usage.QuotaUsed,
Limit: quotaLimit,
Remaining: quotaLimit - usage.QuotaUsed,
}
if quotaLimit > 0 {
quotaStatus.Percent = float64(usage.QuotaUsed) / float64(quotaLimit) * 100
quotaStatus.Warning = quotaStatus.Percent >= 80
quotaStatus.Critical = quotaStatus.Percent >= 95
}
stats.QuotaStatus[usage.ResourceType] = quotaStatus
}
return stats, nil
}
// UpdateUsage updates resource usage for a tenant
func (s *ServiceImpl) UpdateUsage(ctx context.Context, tenantID, resourceType string, count int) error {
// Get current usage
usage, err := s.repository.GetUsage(ctx, tenantID, resourceType)
if err != nil {
// Create new usage record
usage = &TenantUsage{
ID: generateID(),
TenantID: tenantID,
ResourceType: resourceType,
ResourceCount: count,
QuotaUsed: count,
PeriodStart: time.Now().Truncate(24 * time.Hour),
PeriodEnd: time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
} else {
// Update existing usage
usage.ResourceCount += count
usage.QuotaUsed += count
usage.UpdatedAt = time.Now()
}
// Save usage
if err := s.repository.UpdateUsage(ctx, usage); err != nil {
return fmt.Errorf("failed to update usage: %w", err)
}
return nil
}
// GetQuotaStatus retrieves quota status for all tenant resources
func (s *ServiceImpl) GetQuotaStatus(ctx context.Context, tenantID string) (map[string]QuotaStatus, error) {
// Get usage stats
stats, err := s.GetUsageStats(ctx, tenantID)
if err != nil {
return nil, err
}
return stats.QuotaStatus, nil
}
// GetTenantConfig retrieves tenant configuration
func (s *ServiceImpl) GetTenantConfig(ctx context.Context, tenantID string) (*TenantConfig, error) {
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
return config, nil
}
// UpdateTenantConfig updates tenant configuration
func (s *ServiceImpl) UpdateTenantConfig(ctx context.Context, tenantID string, config *TenantConfig) error {
config.TenantID = tenantID
if err := s.repository.UpdateConfig(ctx, config); err != nil {
return fmt.Errorf("failed to update tenant config: %w", err)
}
s.logger.WithField("tenant_id", tenantID).Info("Tenant config updated successfully")
return nil
}
// GetTenantSettings retrieves tenant settings
func (s *ServiceImpl) GetTenantSettings(ctx context.Context, tenantID string) (*TenantSettings, error) {
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
return config.Settings, nil
}
// ValidateTenantAccess validates tenant access
func (s *ServiceImpl) ValidateTenantAccess(ctx context.Context, tenantID, userID string) (*TenantContext, error) {
// Get tenant
tenant, err := s.repository.GetTenant(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("tenant not found: %w", err)
}
// Check tenant status
if tenant.Status != TenantStatusActive {
return nil, errors.New("tenant is not active")
}
// Get tenant configuration
config, err := s.repository.GetConfig(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get tenant config: %w", err)
}
// Get user role if userID provided
var userRole TenantRole
if userID != "" {
user, err := s.repository.GetTenantUser(ctx, tenantID, userID)
if err != nil {
return nil, fmt.Errorf("user not found in tenant: %w", err)
}
if user.Status != TenantUserStatusActive {
return nil, errors.New("user is not active in tenant")
}
userRole = user.Role
}
// Create tenant context
tenantCtx := &TenantContext{
TenantID: tenantID,
TenantName: tenant.Name,
Plan: tenant.Plan,
UserID: userID,
UserRole: userRole,
Settings: config.Settings,
Metadata: tenant.Metadata,
}
return tenantCtx, nil
}
// HasPermission checks if user has specific permission
func (s *ServiceImpl) HasPermission(ctx context.Context, tenantID, userID string, permission string) (bool, error) {
// Get tenant user
user, err := s.repository.GetTenantUser(ctx, tenantID, userID)
if err != nil {
return false, fmt.Errorf("user not found in tenant: %w", err)
}
// Define permission matrix
permissionMatrix := map[TenantRole]map[string]bool{
TenantRoleOwner: {
"tenant:read": true,
"tenant:write": true,
"tenant:delete": true,
"user:read": true,
"user:write": true,
"user:delete": true,
"apikey:read": true,
"apikey:write": true,
"apikey:delete": true,
"config:read": true,
"config:write": true,
},
TenantRoleAdmin: {
"tenant:read": true,
"tenant:write": true,
"user:read": true,
"user:write": true,
"user:delete": true,
"apikey:read": true,
"apikey:write": true,
"apikey:delete": true,
"config:read": true,
"config:write": true,
},
TenantRoleManager: {
"tenant:read": true,
"user:read": true,
"user:write": true,
"apikey:read": true,
"apikey:write": true,
"config:read": true,
},
TenantRoleUser: {
"tenant:read": true,
"apikey:read": true,
"config:read": true,
},
TenantRoleViewer: {
"tenant:read": true,
"apikey:read": true,
"config:read": true,
},
}
// Check permission
rolePermissions, exists := permissionMatrix[user.Role]
if !exists {
return false, nil
}
hasPermission, exists := rolePermissions[permission]
if !exists {
return false, nil
}
return hasPermission, nil
}
// GetTenantContext retrieves tenant context
func (s *ServiceImpl) GetTenantContext(ctx context.Context, tenantID string) (*TenantContext, error) {
return s.ValidateTenantAccess(ctx, tenantID, "")
}
// CheckRateLimit checks rate limit for tenant
func (s *ServiceImpl) CheckRateLimit(ctx context.Context, tenantCtx *TenantContext, endpoint string) error {
// Use rate limiter to check if request is allowed
key := fmt.Sprintf("%s:%s", tenantCtx.TenantID, endpoint)
if !s.rateLimiter.Allow(ctx, tenantCtx.TenantID, key) {
return errors.New("rate limit exceeded")
}
return nil
}
// RecordAPIUsage records API usage for rate limiting
func (s *ServiceImpl) RecordAPIUsage(ctx context.Context, tenantCtx *TenantContext, endpoint string) {
// Record metrics
if s.metricsCollector != nil {
s.metricsCollector.RecordAPIRequest(ctx, tenantCtx.TenantID, endpoint, "", "", 0)
}
}
// ValidateResourceAccess validates resource access
func (s *ServiceImpl) ValidateResourceAccess(ctx context.Context, tenantCtx *TenantContext, resource string, resourceID string) error {
// For now, implement basic validation
// In a real implementation, this would check resource ownership
// and access patterns based on resource type
switch resource {
case "tenant":
if resourceID != tenantCtx.TenantID {
return errors.New("access denied: tenant mismatch")
}
case "user", "apikey", "config":
// These resources belong to the tenant, so basic tenant validation is sufficient
return nil
default:
// Unknown resource type, deny access
return errors.New("access denied: unknown resource type")
}
return nil
}
// GetTenantMetrics retrieves tenant metrics
func (s *ServiceImpl) GetTenantMetrics(ctx context.Context, tenantID string) (*TenantMetrics, error) {
// Get usage stats
usageStats, err := s.GetUsageStats(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("failed to get usage stats: %w", err)
}
// Get recent events
events, err := s.repository.ListEvents(ctx, tenantID, 100)
if err != nil {
return nil, fmt.Errorf("failed to get tenant events: %w", err)
}
// Calculate metrics
metrics := &TenantMetrics{
TenantID: tenantID,
ActiveUsers: usageStats.ActiveUsers,
StorageUsed: 0, // Would be calculated from actual storage usage
HealthScore: 100.0,
Alerts: []string{},
}
// Calculate last activity
if len(events) > 0 {
metrics.LastActivity = events[0].Timestamp
}
// Calculate error rate and response time from events
errorCount := 0
totalRequests := 0
var totalResponseTime time.Duration
for _, event := range events {
if event.EventType == "api_request" {
totalRequests++
if statusCode, exists := event.EventData["status_code"]; exists {
if code, ok := statusCode.(float64); ok && code >= 400 {
errorCount++
}
}
if responseTime, exists := event.EventData["response_time"]; exists {
if rt, ok := responseTime.(float64); ok {
totalResponseTime += time.Duration(rt) * time.Millisecond
}
}
}
}
if totalRequests > 0 {
metrics.ErrorRate = float64(errorCount) / float64(totalRequests) * 100
metrics.ResponseTime = float64(totalResponseTime) / float64(totalRequests) / float64(time.Millisecond)
}
// Check for alerts
for resourceType, quotaStatus := range usageStats.QuotaStatus {
if quotaStatus.Critical {
metrics.Alerts = append(metrics.Alerts, fmt.Sprintf("Critical: %s quota at %.1f%%", resourceType, quotaStatus.Percent))
metrics.HealthScore -= 20
} else if quotaStatus.Warning {
metrics.Alerts = append(metrics.Alerts, fmt.Sprintf("Warning: %s quota at %.1f%%", resourceType, quotaStatus.Percent))
metrics.HealthScore -= 10
}
}
return metrics, nil
}
// GetTenantEvents retrieves tenant events
func (s *ServiceImpl) GetTenantEvents(ctx context.Context, tenantID string, limit int) ([]*TenantEvent, error) {
events, err := s.repository.ListEvents(ctx, tenantID, limit)
if err != nil {
return nil, fmt.Errorf("failed to get tenant events: %w", err)
}
return events, nil
}
// LogTenantEvent logs a tenant event
func (s *ServiceImpl) LogTenantEvent(ctx context.Context, event *TenantEvent) error {
if err := s.repository.CreateEvent(ctx, event); err != nil {
return fmt.Errorf("failed to create tenant event: %w", err)
}
return nil
}
// Helper functions
func generateID() string {
return fmt.Sprintf("tnt_%d", time.Now().UnixNano())
}