mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-07 20:45:15 +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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
+13
-12
@@ -1,4 +1,4 @@
|
|||||||
package tenant
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ func (r *GormTenantRepository) DeleteTenant(ctx context.Context, id string) erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListTenants lists tenants with filtering
|
// 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{})
|
query := r.db.WithContext(ctx).Model(&Tenant{})
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
@@ -100,7 +101,7 @@ func (r *GormTenantRepository) ListTenants(ctx context.Context, filter *TenantFi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CountTenants counts tenants with filtering
|
// 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{})
|
query := r.db.WithContext(ctx).Model(&Tenant{})
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
@@ -151,7 +152,7 @@ func (r *GormTenantRepository) DeleteTenantUser(ctx context.Context, tenantID, u
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListTenantUsers lists tenant users with filtering
|
// 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{})
|
query := r.db.WithContext(ctx).Model(&TenantUser{})
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
@@ -185,7 +186,7 @@ func (r *GormTenantRepository) ListTenantUsers(ctx context.Context, filter *Tena
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CountTenantUsers counts tenant users with filtering
|
// 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{})
|
query := r.db.WithContext(ctx).Model(&TenantUser{})
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
@@ -273,7 +274,7 @@ func (r *GormTenantRepository) UpdateUsage(ctx context.Context, usage *TenantUsa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListUsage lists usage records with filtering
|
// 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{})
|
query := r.db.WithContext(ctx).Model(&TenantUsage{})
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
@@ -304,17 +305,17 @@ func (r *GormTenantRepository) ListUsage(ctx context.Context, filter *TenantUsag
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUsageStats retrieves usage statistics for a tenant
|
// 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
|
// This is a complex query that would typically involve joins and aggregations
|
||||||
// For now, return a basic implementation
|
// For now, return a basic implementation
|
||||||
stats := &TenantUsageStats{
|
stats := &tenant.TenantUsageStats{
|
||||||
TenantID: tenantID,
|
TenantID: tenantID,
|
||||||
ResourceBreakdown: make(map[string]int64),
|
ResourceBreakdown: make(map[string]int64),
|
||||||
QuotaStatus: make(map[string]QuotaStatus),
|
QuotaStatus: make(map[string]tenant.QuotaStatus),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all usage records for the tenant
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -322,7 +323,7 @@ func (r *GormTenantRepository) GetUsageStats(ctx context.Context, tenantID strin
|
|||||||
// Process usage records
|
// Process usage records
|
||||||
for _, usage := range usageRecords {
|
for _, usage := range usageRecords {
|
||||||
stats.ResourceBreakdown[usage.ResourceType] = int64(usage.QuotaUsed)
|
stats.ResourceBreakdown[usage.ResourceType] = int64(usage.QuotaUsed)
|
||||||
stats.QuotaStatus[usage.ResourceType] = QuotaStatus{
|
stats.QuotaStatus[usage.ResourceType] = tenant.QuotaStatus{
|
||||||
Used: usage.QuotaUsed,
|
Used: usage.QuotaUsed,
|
||||||
Limit: usage.QuotaLimit,
|
Limit: usage.QuotaLimit,
|
||||||
Remaining: usage.QuotaRemaining,
|
Remaining: usage.QuotaRemaining,
|
||||||
@@ -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