mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
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:
+1
-1
@@ -1,4 +1,4 @@
|
||||
package tenant
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
+19
-18
@@ -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 = ¤cy.TransactionFilter{}
|
||||
}
|
||||
|
||||
filter.Status = currency.TransactionStatusCompleted
|
||||
|
||||
transactions, err := s.repository.ListTransactions(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get transactions for revenue analysis")
|
||||
return nil, fmt.Errorf("failed to get transactions: %w", err)
|
||||
}
|
||||
|
||||
revenueByCurrency := make(map[string]float64)
|
||||
|
||||
for _, tx := range transactions {
|
||||
if tx.Type == currency.TransactionTypeSubscription || tx.Type == currency.TransactionTypeUsage || tx.Type == currency.TransactionTypeOverage {
|
||||
revenueByCurrency[tx.Currency] += tx.Amount
|
||||
}
|
||||
}
|
||||
|
||||
return revenueByCurrency, nil
|
||||
}
|
||||
|
||||
// GetTransactionVolumeByCurrency calculates transaction volume by currency
|
||||
func (s *AnalyticsServiceImpl) GetTransactionVolumeByCurrency(ctx context.Context, filter *currency.TransactionFilter) (map[string]int64, error) {
|
||||
if filter == nil {
|
||||
filter = ¤cy.TransactionFilter{}
|
||||
}
|
||||
|
||||
transactions, err := s.repository.ListTransactions(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get transactions for volume analysis")
|
||||
return nil, fmt.Errorf("failed to get transactions: %w", err)
|
||||
}
|
||||
|
||||
volumeByCurrency := make(map[string]int64)
|
||||
|
||||
for _, tx := range transactions {
|
||||
volumeByCurrency[tx.Currency]++
|
||||
}
|
||||
|
||||
return volumeByCurrency, nil
|
||||
}
|
||||
|
||||
// GetExchangeRateTrends retrieves exchange rate trends for a currency pair
|
||||
func (s *AnalyticsServiceImpl) GetExchangeRateTrends(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*currency.ExchangeRate, error) {
|
||||
filter := ¤cy.ExchangeRateFilter{
|
||||
FromCurrency: fromCurrency,
|
||||
ToCurrency: toCurrency,
|
||||
IsValid: &[]bool{false}[0], // Include historical rates
|
||||
Limit: days,
|
||||
SortBy: "valid_from",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
|
||||
rates, err := s.repository.ListExchangeRates(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get exchange rate trends")
|
||||
return nil, fmt.Errorf("failed to get exchange rate trends: %w", err)
|
||||
}
|
||||
|
||||
return rates, nil
|
||||
}
|
||||
|
||||
// GetCurrencyUsageStats retrieves currency usage statistics
|
||||
func (s *AnalyticsServiceImpl) GetCurrencyUsageStats(ctx context.Context) (*currency.CurrencyUsageStats, error) {
|
||||
// Get total currencies
|
||||
totalCurrencies, err := s.repository.CountCurrencies(ctx, ¤cy.CurrencyFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count currencies: %w", err)
|
||||
}
|
||||
|
||||
// Get active currencies
|
||||
activeCurrencies, err := s.repository.CountCurrencies(ctx, ¤cy.CurrencyFilter{
|
||||
IsActive: &[]bool{true}[0],
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count active currencies: %w", err)
|
||||
}
|
||||
|
||||
// Get total transactions
|
||||
totalTransactions, err := s.repository.CountTransactions(ctx, ¤cy.TransactionFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count transactions: %w", err)
|
||||
}
|
||||
|
||||
// Get total volume
|
||||
transactions, err := s.repository.ListTransactions(ctx, ¤cy.TransactionFilter{
|
||||
Status: currency.TransactionStatusCompleted,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get transactions for volume: %w", err)
|
||||
}
|
||||
|
||||
totalVolume := 0.0
|
||||
currencyDistribution := make(map[string]int64)
|
||||
|
||||
for _, tx := range transactions {
|
||||
totalVolume += tx.BaseAmount
|
||||
currencyDistribution[tx.Currency]++
|
||||
}
|
||||
|
||||
// Find most used currency
|
||||
mostUsedCurrency := ""
|
||||
maxCount := int64(0)
|
||||
|
||||
for currency, count := range currencyDistribution {
|
||||
if count > maxCount {
|
||||
maxCount = count
|
||||
mostUsedCurrency = currency
|
||||
}
|
||||
}
|
||||
|
||||
// Get exchange rate count (using ListExchangeRates for now since CountExchangeRates doesn't exist)
|
||||
exchangeRates, err := s.repository.ListExchangeRates(ctx, ¤cy.ExchangeRateFilter{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count exchange rates: %w", err)
|
||||
}
|
||||
|
||||
return ¤cy.CurrencyUsageStats{
|
||||
TotalCurrencies: totalCurrencies,
|
||||
ActiveCurrencies: activeCurrencies,
|
||||
TotalTransactions: int64(totalTransactions),
|
||||
TotalVolume: totalVolume,
|
||||
MostUsedCurrency: mostUsedCurrency,
|
||||
CurrencyDistribution: currencyDistribution,
|
||||
ExchangeRateCount: len(exchangeRates),
|
||||
LastUpdated: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetMonthlyRevenueTrends calculates monthly revenue trends
|
||||
func (s *AnalyticsServiceImpl) GetMonthlyRevenueTrends(ctx context.Context, months int) (map[string]float64, error) {
|
||||
endDate := time.Now()
|
||||
startDate := endDate.AddDate(0, -months, 0)
|
||||
|
||||
filter := ¤cy.TransactionFilter{
|
||||
Status: currency.TransactionStatusCompleted,
|
||||
FromDate: &startDate,
|
||||
ToDate: &endDate,
|
||||
}
|
||||
|
||||
transactions, err := s.repository.ListTransactions(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get transactions for monthly trends")
|
||||
return nil, fmt.Errorf("failed to get transactions: %w", err)
|
||||
}
|
||||
|
||||
monthlyRevenue := make(map[string]float64)
|
||||
|
||||
for _, tx := range transactions {
|
||||
monthKey := tx.CreatedAt.Format("2006-01")
|
||||
monthlyRevenue[monthKey] += tx.BaseAmount
|
||||
}
|
||||
|
||||
return monthlyRevenue, nil
|
||||
}
|
||||
|
||||
// GetTopCurrenciesByRevenue returns top currencies by revenue
|
||||
func (s *AnalyticsServiceImpl) GetTopCurrenciesByRevenue(ctx context.Context, limit int) ([]*currency.CurrencyRevenue, error) {
|
||||
revenueByCurrency, err := s.GetRevenueByCurrency(ctx, ¤cy.TransactionFilter{
|
||||
Status: currency.TransactionStatusCompleted,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to slice and sort
|
||||
var currencyRevenues []*currency.CurrencyRevenue
|
||||
for currCode, revenue := range revenueByCurrency {
|
||||
currencyRevenues = append(currencyRevenues, ¤cy.CurrencyRevenue{
|
||||
Currency: currCode,
|
||||
Revenue: revenue,
|
||||
})
|
||||
}
|
||||
|
||||
// Simple sort (in production, use proper sorting)
|
||||
if len(currencyRevenues) > limit {
|
||||
currencyRevenues = currencyRevenues[:limit]
|
||||
}
|
||||
|
||||
return currencyRevenues, nil
|
||||
}
|
||||
|
||||
// GetTransactionTypeAnalytics returns analytics by transaction type
|
||||
func (s *AnalyticsServiceImpl) GetTransactionTypeAnalytics(ctx context.Context, filter *currency.TransactionFilter) (map[string]*currency.TransactionTypeStats, error) {
|
||||
if filter == nil {
|
||||
filter = ¤cy.TransactionFilter{}
|
||||
}
|
||||
|
||||
transactions, err := s.repository.ListTransactions(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get transactions for type analytics")
|
||||
return nil, fmt.Errorf("failed to get transactions: %w", err)
|
||||
}
|
||||
|
||||
typeStats := make(map[string]*currency.TransactionTypeStats)
|
||||
|
||||
for _, tx := range transactions {
|
||||
typeKey := string(tx.Type)
|
||||
|
||||
if _, exists := typeStats[typeKey]; !exists {
|
||||
typeStats[typeKey] = ¤cy.TransactionTypeStats{
|
||||
Type: tx.Type,
|
||||
Count: 0,
|
||||
Amount: 0.0,
|
||||
Currency: tx.Currency,
|
||||
}
|
||||
}
|
||||
|
||||
stats := typeStats[typeKey]
|
||||
stats.Count++
|
||||
stats.Amount += tx.BaseAmount
|
||||
}
|
||||
|
||||
return typeStats, nil
|
||||
}
|
||||
@@ -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 := ¤cy.Transaction{
|
||||
ID: uuid.New().String(),
|
||||
ProfileID: req.ProfileID,
|
||||
SubscriptionID: req.SubscriptionID,
|
||||
Type: currency.TransactionTypeSubscription,
|
||||
Amount: req.Amount,
|
||||
Currency: req.Currency,
|
||||
BaseAmount: baseAmount,
|
||||
BaseCurrency: s.baseCurrency,
|
||||
ExchangeRate: exchangeRate,
|
||||
Description: req.Description,
|
||||
Status: currency.TransactionStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Save transaction
|
||||
if err := s.repository.CreateTransaction(ctx, transaction); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to create billing transaction")
|
||||
return nil, fmt.Errorf("failed to create transaction: %w", err)
|
||||
}
|
||||
|
||||
// Process payment (in real implementation, this would integrate with payment gateway)
|
||||
transaction.Status = currency.TransactionStatusCompleted
|
||||
if err := s.repository.UpdateTransaction(ctx, transaction); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to update transaction status")
|
||||
return nil, fmt.Errorf("failed to update transaction: %w", err)
|
||||
}
|
||||
|
||||
s.logger.WithFields(logrus.Fields{
|
||||
"transaction_id": transaction.ID,
|
||||
"profile_id": req.ProfileID,
|
||||
"amount": req.Amount,
|
||||
"currency": req.Currency,
|
||||
"base_amount": baseAmount,
|
||||
"base_currency": s.baseCurrency,
|
||||
}).Info("Billing processed successfully")
|
||||
|
||||
return ¤cy.BillingResponse{
|
||||
TransactionID: transaction.ID,
|
||||
Amount: transaction.Amount,
|
||||
Currency: transaction.Currency,
|
||||
BaseAmount: transaction.BaseAmount,
|
||||
BaseCurrency: transaction.BaseCurrency,
|
||||
ExchangeRate: transaction.ExchangeRate,
|
||||
Status: string(transaction.Status),
|
||||
ProcessedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ConvertAmount converts an amount between currencies
|
||||
func (s *BillingServiceImpl) ConvertAmount(ctx context.Context, req *currency.CurrencyConversionRequest) (*currency.CurrencyConversionResponse, error) {
|
||||
return s.exchangeService.ConvertAmount(ctx, req.Amount, req.FromCurrency, req.ToCurrency)
|
||||
}
|
||||
|
||||
// GetBillingHistory retrieves billing history for a profile
|
||||
func (s *BillingServiceImpl) GetBillingHistory(ctx context.Context, profileID string, filter *currency.TransactionFilter) ([]*currency.Transaction, error) {
|
||||
if filter == nil {
|
||||
filter = ¤cy.TransactionFilter{}
|
||||
}
|
||||
|
||||
filter.ProfileID = profileID
|
||||
|
||||
transactions, err := s.repository.ListTransactions(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.WithError(err).Error("Failed to get billing history")
|
||||
return nil, fmt.Errorf("failed to get billing history: %w", err)
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
// validateBillingRequest validates a billing request
|
||||
func (s *BillingServiceImpl) validateBillingRequest(req *currency.BillingRequest) error {
|
||||
if req.ProfileID == "" {
|
||||
return fmt.Errorf("profile ID is required")
|
||||
}
|
||||
if req.SubscriptionID == "" {
|
||||
return fmt.Errorf("subscription ID is required")
|
||||
}
|
||||
if req.Amount <= 0 {
|
||||
return fmt.Errorf("amount must be positive")
|
||||
}
|
||||
if req.Currency == "" {
|
||||
return fmt.Errorf("currency is required")
|
||||
}
|
||||
if req.BillingDate.IsZero() {
|
||||
req.BillingDate = time.Now()
|
||||
}
|
||||
|
||||
// Validate currency
|
||||
if err := s.exchangeService.ValidateCurrencyPair(context.Background(), req.Currency, s.baseCurrency); err != nil {
|
||||
return fmt.Errorf("invalid currency: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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 ¤cy.ExchangeRate{
|
||||
FromCurrency: fromCurrency,
|
||||
ToCurrency: toCurrency,
|
||||
Rate: 1.0,
|
||||
Source: "direct",
|
||||
ValidFrom: time.Now(),
|
||||
IsActive: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
rate, err := s.repository.GetLatestExchangeRate(ctx, fromCurrency, toCurrency)
|
||||
if err == nil {
|
||||
return rate, nil
|
||||
}
|
||||
|
||||
for _, provider := range s.providers {
|
||||
providerRate, err := provider.GetRate(ctx, fromCurrency, toCurrency)
|
||||
if err == nil {
|
||||
newRate := ¤cy.ExchangeRate{
|
||||
ID: fmt.Sprintf("%s_%s_%d", fromCurrency, toCurrency, time.Now().Unix()),
|
||||
FromCurrency: fromCurrency,
|
||||
ToCurrency: toCurrency,
|
||||
Rate: providerRate,
|
||||
Source: "provider",
|
||||
ValidFrom: time.Now(),
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.repository.CreateExchangeRate(ctx, newRate); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to save exchange rate")
|
||||
}
|
||||
|
||||
return newRate, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("exchange rate not found: %s to %s", fromCurrency, toCurrency)
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) ConvertAmount(ctx context.Context, amount float64, fromCurrency, toCurrency string) (*currency.CurrencyConversionResponse, error) {
|
||||
rate, err := s.GetExchangeRate(ctx, fromCurrency, toCurrency)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get exchange rate: %w", err)
|
||||
}
|
||||
|
||||
convertedAmount := amount * rate.Rate
|
||||
|
||||
return ¤cy.CurrencyConversionResponse{
|
||||
OriginalAmount: amount,
|
||||
OriginalCurrency: fromCurrency,
|
||||
ConvertedAmount: convertedAmount,
|
||||
ConvertedCurrency: toCurrency,
|
||||
ExchangeRate: rate.Rate,
|
||||
ConvertedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) RefreshRates(ctx context.Context) error {
|
||||
s.logger.Info("Refreshing exchange rates")
|
||||
|
||||
for _, provider := range s.providers {
|
||||
if err := provider.RefreshRates(ctx); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to refresh rates from provider")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Exchange rates refreshed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) GetRateHistory(ctx context.Context, fromCurrency, toCurrency string, days int) ([]*currency.ExchangeRate, error) {
|
||||
filter := ¤cy.ExchangeRateFilter{
|
||||
FromCurrency: fromCurrency,
|
||||
ToCurrency: toCurrency,
|
||||
IsValid: &[]bool{false}[0], // Include historical rates
|
||||
Limit: days,
|
||||
SortBy: "valid_from",
|
||||
SortOrder: "desc",
|
||||
}
|
||||
|
||||
rates, err := s.repository.ListExchangeRates(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rate history: %w", err)
|
||||
}
|
||||
|
||||
return rates, nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) UpdateExchangeRate(ctx context.Context, rate *currency.ExchangeRate) error {
|
||||
if rate.Rate <= 0 {
|
||||
return fmt.Errorf("invalid exchange rate: must be positive")
|
||||
}
|
||||
|
||||
if rate.FromCurrency == rate.ToCurrency {
|
||||
return fmt.Errorf("invalid currency pair: from and to currencies cannot be the same")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
rate.ValidFrom = now
|
||||
rate.IsActive = true
|
||||
|
||||
filter := ¤cy.ExchangeRateFilter{
|
||||
FromCurrency: rate.FromCurrency,
|
||||
ToCurrency: rate.ToCurrency,
|
||||
IsValid: &[]bool{true}[0],
|
||||
}
|
||||
|
||||
oldRates, err := s.repository.ListExchangeRates(ctx, filter)
|
||||
if err == nil {
|
||||
for _, oldRate := range oldRates {
|
||||
oldRate.IsActive = false
|
||||
if err := s.repository.UpdateExchangeRate(ctx, oldRate); err != nil {
|
||||
s.logger.WithError(err).Error("Failed to deactivate old exchange rate")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rate.ID = fmt.Sprintf("%s_%s_%d", rate.FromCurrency, rate.ToCurrency, time.Now().Unix())
|
||||
if err := s.repository.CreateExchangeRate(ctx, rate); err != nil {
|
||||
return fmt.Errorf("failed to create exchange rate: %w", err)
|
||||
}
|
||||
|
||||
s.logger.WithFields(logrus.Fields{
|
||||
"from_currency": rate.FromCurrency,
|
||||
"to_currency": rate.ToCurrency,
|
||||
"rate": rate.Rate,
|
||||
"source": rate.Source,
|
||||
}).Info("Exchange rate updated")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) GetSupportedCurrencies(ctx context.Context) ([]*currency.Currency, error) {
|
||||
filter := ¤cy.CurrencyFilter{
|
||||
IsActive: &[]bool{true}[0],
|
||||
}
|
||||
|
||||
currencies, err := s.repository.ListCurrencies(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get supported currencies: %w", err)
|
||||
}
|
||||
|
||||
return currencies, nil
|
||||
}
|
||||
|
||||
func (s *ExchangeRateService) ValidateCurrencyPair(ctx context.Context, fromCurrency, toCurrency string) error {
|
||||
_, err := s.repository.GetCurrency(ctx, fromCurrency)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unsupported from currency: %s", fromCurrency)
|
||||
}
|
||||
|
||||
_, err = s.repository.GetCurrency(ctx, toCurrency)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unsupported to currency: %s", toCurrency)
|
||||
}
|
||||
|
||||
_, err = s.GetExchangeRate(ctx, fromCurrency, toCurrency)
|
||||
if err != nil {
|
||||
return fmt.Errorf("no exchange rate available: %s to %s", fromCurrency, toCurrency)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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 := ¤cy.BillingRequest{
|
||||
ProfileID: profileID,
|
||||
SubscriptionID: createdSubscription.ID,
|
||||
Amount: subscriptionPrice,
|
||||
Currency: targetCurrency,
|
||||
Description: fmt.Sprintf("Initial subscription to %s", plan.Name),
|
||||
BillingDate: time.Now(),
|
||||
}
|
||||
|
||||
_, err = rpci.billingService.ProcessBilling(ctx, billingReq)
|
||||
if err != nil {
|
||||
rpci.logger.WithError(err).Error("Failed to process initial billing")
|
||||
// Don't fail the subscription if billing fails, but log it
|
||||
}
|
||||
|
||||
rpci.logger.WithFields(logrus.Fields{
|
||||
"profile_id": profileID,
|
||||
"plan_id": planID,
|
||||
"currency": targetCurrency,
|
||||
"subscription_id": createdSubscription.ID,
|
||||
}).Info("Rate plan subscription created with currency support")
|
||||
|
||||
return createdSubscription, nil
|
||||
}
|
||||
|
||||
// CalculatePlanCostInCurrency calculates the cost of a rate plan in a specific currency
|
||||
func (rpci *RatePlanCurrencyIntegrator) CalculatePlanCostInCurrency(ctx context.Context, planID string, targetCurrency string, usageData *rateplan.RatePlanUsage) (*currency.BillingSummary, error) {
|
||||
// Get the rate plan
|
||||
plan, err := rpci.ratePlanService.GetRatePlan(ctx, planID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rate plan: %w", err)
|
||||
}
|
||||
|
||||
// Calculate base cost
|
||||
baseCost := plan.BasePrice
|
||||
|
||||
// Add overage costs if usage data is provided
|
||||
if usageData != nil {
|
||||
overageCost, err := rpci.calculateOverageCost(ctx, plan, usageData)
|
||||
if err != nil {
|
||||
rpci.logger.WithError(err).Warn("Failed to calculate overage cost")
|
||||
} else {
|
||||
baseCost += overageCost
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to requested currency
|
||||
convertedCost := baseCost
|
||||
exchangeRate := 1.0
|
||||
|
||||
if targetCurrency != plan.Currency {
|
||||
conversion, err := rpci.exchangeService.ConvertAmount(ctx, baseCost, plan.Currency, targetCurrency)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("currency conversion failed: %w", err)
|
||||
}
|
||||
convertedCost = conversion.ConvertedAmount
|
||||
exchangeRate = conversion.ExchangeRate
|
||||
}
|
||||
|
||||
// Create billing summary
|
||||
summary := ¤cy.BillingSummary{
|
||||
ProfileID: usageData.ProfileID,
|
||||
TotalAmount: convertedCost,
|
||||
Currency: targetCurrency,
|
||||
BaseTotalAmount: baseCost,
|
||||
BaseCurrency: plan.Currency,
|
||||
TransactionCount: 1,
|
||||
FromDate: time.Now().AddDate(0, -1, 0),
|
||||
ToDate: time.Now(),
|
||||
Breakdown: map[string]interface{}{
|
||||
"plan_id": planID,
|
||||
"plan_name": plan.Name,
|
||||
"base_cost": plan.BasePrice,
|
||||
"overage_cost": baseCost - plan.BasePrice,
|
||||
"exchange_rate": exchangeRate,
|
||||
"original_currency": plan.Currency,
|
||||
},
|
||||
}
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user