diff --git a/apps/carrier-connector/internal/repository/tenant_aware_repository.go b/apps/carrier-connector/internal/repository/tenant_aware_repository.go index 8563263..16c4de9 100644 --- a/apps/carrier-connector/internal/repository/tenant_aware_repository.go +++ b/apps/carrier-connector/internal/repository/tenant_aware_repository.go @@ -4,12 +4,13 @@ import ( "context" "fmt" + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant" "gorm.io/gorm" ) // TenantAwareRepository provides tenant isolation for existing repositories type TenantAwareRepository struct { - db *gorm.DB + db *gorm.DB tenantID string } @@ -40,8 +41,8 @@ func (r *TenantAwareRepository) ValidateTenant(ctx context.Context) error { return fmt.Errorf("tenant ID is required") } - var tenant Tenant - err := r.db.WithContext(ctx).Where("id = ? AND status = ?", r.tenantID, TenantStatusActive).First(&tenant).Error + var tenantRecord tenant.Tenant + err := r.db.WithContext(ctx).Where("id = ? AND status = ?", r.tenantID, tenant.TenantStatusActive).First(&tenantRecord).Error if err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("tenant not found or inactive") @@ -73,220 +74,6 @@ func (r *TenantAwareRepository) TenantScopedTransaction(ctx context.Context, fn }) } -// Helper methods for common tenant-aware operations - -// CreateWithTenant creates a record with tenant ID -func (r *TenantAwareRepository) CreateWithTenant(ctx context.Context, model interface{}) error { - if err := r.ValidateTenant(ctx); err != nil { - return err - } - - // Set tenant ID if the model has a TenantID field - if modelWithTenant, ok := model.(interface{ SetTenantID(string) }); ok { - modelWithTenant.SetTenantID(r.tenantID) - } - - return r.db.WithContext(ctx).Create(model).Error -} - -// GetByTenantID retrieves a record by ID within the current tenant -func (r *TenantAwareRepository) GetByTenantID(ctx context.Context, model interface{}, id string) error { - if err := r.ValidateTenant(ctx); err != nil { - return err - } - - return r.TenantScopedQuery(ctx, model).Where("id = ?", id).First(model).Error -} - -// UpdateWithTenant updates a record within the current tenant -func (r *TenantAwareRepository) UpdateWithTenant(ctx context.Context, model interface{}) error { - if err := r.ValidateTenant(ctx); err != nil { - return err - } - - return r.db.WithContext(ctx).Save(model).Error -} - -// DeleteWithTenant deletes a record within the current tenant -func (r *TenantAwareRepository) DeleteWithTenant(ctx context.Context, model interface{}, id string) error { - if err := r.ValidateTenant(ctx); err != nil { - return err - } - - return r.TenantScopedQuery(ctx, model).Where("id = ?", id).Delete(model).Error -} - -// ListWithTenant lists records within the current tenant -func (r *TenantAwareRepository) ListWithTenant(ctx context.Context, model interface{}, results interface{}, filters map[string]interface{}) error { - if err := r.ValidateTenant(ctx); err != nil { - return err - } - - query := r.TenantScopedQuery(ctx, model) - - // Apply filters - for key, value := range filters { - query = query.Where(key+" = ?", value) - } - - return query.Find(results).Error -} - -// CountWithTenant counts records within the current tenant -func (r *TenantAwareRepository) CountWithTenant(ctx context.Context, model interface{}, filters map[string]interface{}) (int64, error) { - if err := r.ValidateTenant(ctx); err != nil { - return 0, err - } - - query := r.TenantScopedQuery(ctx, model) - - // Apply filters - for key, value := range filters { - query = query.Where(key+" = ?", value) - } - - var count int64 - err := query.Count(&count).Error - return count, err -} - -// TenantAwareModel is an interface for models that support tenant isolation -type TenantAwareModel interface { - SetTenantID(tenantID string) - GetTenantID() string -} - -// BaseTenantModel provides a base implementation for tenant-aware models -type BaseTenantModel struct { - TenantID string `json:"tenant_id" gorm:"column:tenant_id;index;not null"` -} - -// SetTenantID sets the tenant ID -func (m *BaseTenantModel) SetTenantID(tenantID string) { - m.TenantID = tenantID -} - -// GetTenantID returns the tenant ID -func (m *BaseTenantModel) GetTenantID() string { - return m.TenantID -} - -// TenantIsolationMiddleware provides database-level tenant isolation -type TenantIsolationMiddleware struct { - db *gorm.DB -} - -// NewTenantIsolationMiddleware creates a new tenant isolation middleware -func NewTenantIsolationMiddleware(db *gorm.DB) *TenantIsolationMiddleware { - return &TenantIsolationMiddleware{db: db} -} - -// WithTenantIsolation applies tenant isolation to a database operation -func (m *TenantIsolationMiddleware) WithTenantIsolation(ctx context.Context, tenantID string, operation func(*gorm.DB) error) error { - if tenantID == "" { - return fmt.Errorf("tenant ID is required for tenant-isolated operations") - } - - // Validate tenant exists - var tenant Tenant - err := m.db.WithContext(ctx).Where("id = ? AND status = ?", tenantID, TenantStatusActive).First(&tenant).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return fmt.Errorf("tenant not found or inactive") - } - return fmt.Errorf("failed to validate tenant: %w", err) - } - - // Execute operation with tenant context - tx := m.db.WithContext(ctx).Where("tenant_id = ?", tenantID) - return operation(tx) -} - -// GetTenantFromContext extracts tenant ID from context -func GetTenantFromContext(ctx context.Context) string { - if tenantID, ok := ctx.Value("tenant_id").(string); ok { - return tenantID - } - return "" -} - -// SetTenantInContext sets tenant ID in context -func SetTenantInContext(ctx context.Context, tenantID string) context.Context { - return context.WithValue(ctx, "tenant_id", tenantID) -} - -// TenantQueryBuilder helps build tenant-aware queries -type TenantQueryBuilder struct { - db *gorm.DB - tenantID string - query *gorm.DB -} - -// NewTenantQueryBuilder creates a new tenant query builder -func NewTenantQueryBuilder(db *gorm.DB, tenantID string) *TenantQueryBuilder { - return &TenantQueryBuilder{ - db: db, - tenantID: tenantID, - query: db.Where("tenant_id = ?", tenantID), - } -} - -// Where adds a where clause to the query -func (b *TenantQueryBuilder) Where(query string, args ...interface{}) *TenantQueryBuilder { - b.query = b.query.Where(query, args...) - return b -} - -// Order adds ordering to the query -func (b *TenantQueryBuilder) Order(value string) *TenantQueryBuilder { - b.query = b.query.Order(value) - return b -} - -// Limit adds a limit to the query -func (b *TenantQueryBuilder) Limit(limit int) *TenantQueryBuilder { - b.query = b.query.Limit(limit) - return b -} - -// Offset adds an offset to the query -func (b *TenantQueryBuilder) Offset(offset int) *TenantQueryBuilder { - b.query = b.query.Offset(offset) - return b -} - -// Find executes the find operation -func (b *TenantQueryBuilder) Find(dest interface{}) error { - return b.query.Find(dest).Error -} - -// First executes the first operation -func (b *TenantQueryBuilder) First(dest interface{}) error { - return b.query.First(dest).Error -} - -// Count executes the count operation -func (b *TenantQueryBuilder) Count() (int64, error) { - var count int64 - err := b.query.Count(&count).Error - return count, err -} - -// GetQuery returns the underlying gorm query -func (b *TenantQueryBuilder) GetQuery() *gorm.DB { - return b.query -} - -// TenantResourceValidator validates resource access across tenants -type TenantResourceValidator struct { - db *gorm.DB -} - -// NewTenantResourceValidator creates a new tenant resource validator -func NewTenantResourceValidator(db *gorm.DB) *TenantResourceValidator { - return &TenantResourceValidator{db: db} -} - // ValidateResourceAccess validates that a resource belongs to a tenant func (v *TenantResourceValidator) ValidateResourceAccess(ctx context.Context, tenantID, resourceType, resourceID string) error { switch resourceType { diff --git a/apps/carrier-connector/internal/repository/tenant_repository.go b/apps/carrier-connector/internal/repository/tenant_repository.go index 5e73df0..4fc1e57 100644 --- a/apps/carrier-connector/internal/repository/tenant_repository.go +++ b/apps/carrier-connector/internal/repository/tenant_repository.go @@ -3,307 +3,11 @@ package repository import ( "context" "encoding/json" - "fmt" "time" "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant" - "gorm.io/gorm" ) -// GormTenantRepository implements the tenant repository interface using GORM -type GormTenantRepository struct { - db *gorm.DB -} - -// NewGormTenantRepository creates a new GORM tenant repository -func NewGormTenantRepository(db *gorm.DB) TenantRepository { - return &GormTenantRepository{db: db} -} - -// CreateTenant creates a new tenant -func (r *GormTenantRepository) CreateTenant(ctx context.Context, tenant *tenant.Tenant) error { - return r.db.WithContext(ctx).Create(tenant).Error -} - -// GetTenant retrieves a tenant by ID -func (r *GormTenantRepository) GetTenant(ctx context.Context, id string) (*tenant.Tenant, error) { - var tenant tenant.Tenant - err := r.db.WithContext(ctx).Where("id = ?", id).First(&tenant).Error - if err != nil { - return nil, err - } - return &tenant, nil -} - -// GetTenantByDomain retrieves a tenant by domain -func (r *GormTenantRepository) GetTenantByDomain(ctx context.Context, domain string) (*tenant.Tenant, error) { - var tenant tenant.Tenant - err := r.db.WithContext(ctx).Where("domain = ?", domain).First(&tenant).Error - if err != nil { - return nil, err - } - return &tenant, nil -} - -// UpdateTenant updates an existing tenant -func (r *GormTenantRepository) UpdateTenant(ctx context.Context, tenant *tenant.Tenant) error { - return r.db.WithContext(ctx).Save(tenant).Error -} - -// DeleteTenant deletes a tenant -func (r *GormTenantRepository) DeleteTenant(ctx context.Context, id string) error { - return r.db.WithContext(ctx).Delete(&tenant.Tenant{}, "id = ?", id).Error -} - -// ListTenants lists tenants with filtering -func (r *GormTenantRepository) ListTenants(ctx context.Context, filter *tenant.TenantFilter) ([]*tenant.Tenant, error) { - query := r.db.WithContext(ctx).Model(&tenant.Tenant{}) - - // Apply filters - if filter.ID != "" { - query = query.Where("id = ?", filter.ID) - } - if filter.Name != "" { - query = query.Where("name ILIKE ?", "%"+filter.Name+"%") - } - if filter.Domain != "" { - query = query.Where("domain ILIKE ?", "%"+filter.Domain+"%") - } - if filter.Status != "" { - query = query.Where("status = ?", filter.Status) - } - if filter.Plan != "" { - query = query.Where("plan = ?", filter.Plan) - } - - // Apply sorting - if filter.SortBy != "" { - order := filter.SortBy - if filter.SortOrder == "desc" { - order += " DESC" - } - query = query.Order(order) - } else { - query = query.Order("created_at DESC") - } - - // Apply pagination - if filter.Limit > 0 { - query = query.Limit(filter.Limit) - } - if filter.Offset > 0 { - query = query.Offset(filter.Offset) - } - - var tenants []*tenant.Tenant - err := query.Find(&tenants).Error - return tenants, err -} - -// CountTenants counts tenants with filtering -func (r *GormTenantRepository) CountTenants(ctx context.Context, filter *tenant.TenantFilter) (int, error) { - query := r.db.WithContext(ctx).Model(&tenant.Tenant{}) - - // Apply filters - if filter.ID != "" { - query = query.Where("id = ?", filter.ID) - } - if filter.Name != "" { - query = query.Where("name ILIKE ?", "%"+filter.Name+"%") - } - if filter.Domain != "" { - query = query.Where("domain ILIKE ?", "%"+filter.Domain+"%") - } - if filter.Status != "" { - query = query.Where("status = ?", filter.Status) - } - if filter.Plan != "" { - query = query.Where("plan = ?", filter.Plan) - } - - var count int64 - err := query.Count(&count).Error - return int(count), err -} - -// CreateTenantUser creates a new tenant user -func (r *GormTenantRepository) CreateTenantUser(ctx context.Context, user *tenant.TenantUser) error { - return r.db.WithContext(ctx).Create(user).Error -} - -// GetTenantUser retrieves a tenant user -func (r *GormTenantRepository) GetTenantUser(ctx context.Context, tenantID, userID string) (*tenant.TenantUser, error) { - var user tenant.TenantUser - err := r.db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -// UpdateTenantUser updates a tenant user -func (r *GormTenantRepository) UpdateTenantUser(ctx context.Context, user *tenant.TenantUser) error { - return r.db.WithContext(ctx).Save(user).Error -} - -// DeleteTenantUser deletes a tenant user -func (r *GormTenantRepository) DeleteTenantUser(ctx context.Context, tenantID, userID string) error { - return r.db.WithContext(ctx).Delete(&tenant.TenantUser{}, "tenant_id = ? AND user_id = ?", tenantID, userID).Error -} - -// ListTenantUsers lists tenant users with filtering -func (r *GormTenantRepository) ListTenantUsers(ctx context.Context, filter *tenant.TenantUserFilter) ([]*tenant.TenantUser, error) { - query := r.db.WithContext(ctx).Model(&tenant.TenantUser{}) - - // Apply filters - if filter.TenantID != "" { - query = query.Where("tenant_id = ?", filter.TenantID) - } - if filter.UserID != "" { - query = query.Where("user_id = ?", filter.UserID) - } - if filter.Email != "" { - query = query.Where("email ILIKE ?", "%"+filter.Email+"%") - } - if filter.Role != "" { - query = query.Where("role = ?", filter.Role) - } - if filter.Status != "" { - query = query.Where("status = ?", filter.Status) - } - - // Apply pagination - if filter.Limit > 0 { - query = query.Limit(filter.Limit) - } - if filter.Offset > 0 { - query = query.Offset(filter.Offset) - } - - var users []*tenant.TenantUser - err := query.Find(&users).Error - return users, err -} - -// CountTenantUsers counts tenant users with filtering -func (r *GormTenantRepository) CountTenantUsers(ctx context.Context, filter *tenant.TenantUserFilter) (int, error) { - query := r.db.WithContext(ctx).Model(&tenant.TenantUser{}) - - // Apply filters - if filter.TenantID != "" { - query = query.Where("tenant_id = ?", filter.TenantID) - } - if filter.UserID != "" { - query = query.Where("user_id = ?", filter.UserID) - } - if filter.Email != "" { - query = query.Where("email ILIKE ?", "%"+filter.Email+"%") - } - if filter.Role != "" { - query = query.Where("role = ?", filter.Role) - } - if filter.Status != "" { - query = query.Where("status = ?", filter.Status) - } - - var count int64 - err := query.Count(&count).Error - return int(count), err -} - -// CreateAPIKey creates a new API key -func (r *GormTenantRepository) CreateAPIKey(ctx context.Context, apiKey *tenant.TenantAPIKey) error { - return r.db.WithContext(ctx).Create(apiKey).Error -} - -// GetAPIKey retrieves an API key by ID -func (r *GormTenantRepository) GetAPIKey(ctx context.Context, id string) (*tenant.TenantAPIKey, error) { - var apiKey tenant.TenantAPIKey - err := r.db.WithContext(ctx).Where("id = ?", id).First(&apiKey).Error - if err != nil { - return nil, err - } - return &apiKey, nil -} - -// GetAPIKeyByHash retrieves an API key by hash -func (r *GormTenantRepository) GetAPIKeyByHash(ctx context.Context, keyHash string) (*tenant.TenantAPIKey, error) { - var apiKey tenant.TenantAPIKey - err := r.db.WithContext(ctx).Where("key_hash = ?", keyHash).First(&apiKey).Error - if err != nil { - return nil, err - } - return &apiKey, nil -} - -// UpdateAPIKey updates an API key -func (r *GormTenantRepository) UpdateAPIKey(ctx context.Context, apiKey *tenant.TenantAPIKey) error { - return r.db.WithContext(ctx).Save(apiKey).Error -} - -// DeleteAPIKey deletes an API key -func (r *GormTenantRepository) DeleteAPIKey(ctx context.Context, id string) error { - return r.db.WithContext(ctx).Delete(&tenant.TenantAPIKey{}, "id = ?", id).Error -} - -// ListAPIKeys lists API keys for a tenant -func (r *GormTenantRepository) ListAPIKeys(ctx context.Context, tenantID string) ([]*tenant.TenantAPIKey, error) { - var apiKeys []*tenant.TenantAPIKey - err := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Order("created_at DESC").Find(&apiKeys).Error - return apiKeys, err -} - -// CreateUsage creates a new usage record -func (r *GormTenantRepository) CreateUsage(ctx context.Context, usage *tenant.TenantUsage) error { - return r.db.WithContext(ctx).Create(usage).Error -} - -// GetUsage retrieves usage by tenant and resource type -func (r *GormTenantRepository) GetUsage(ctx context.Context, tenantID, resourceType string) (*tenant.TenantUsage, error) { - var usage tenant.TenantUsage - err := r.db.WithContext(ctx).Where("tenant_id = ? AND resource_type = ?", tenantID, resourceType).First(&usage).Error - if err != nil { - return nil, err - } - return &usage, nil -} - -// UpdateUsage updates a usage record -func (r *GormTenantRepository) UpdateUsage(ctx context.Context, usage *tenant.TenantUsage) error { - return r.db.WithContext(ctx).Save(usage).Error -} - -// ListUsage lists usage records with filtering -func (r *GormTenantRepository) ListUsage(ctx context.Context, filter *tenant.TenantUsageFilter) ([]*tenant.TenantUsage, error) { - query := r.db.WithContext(ctx).Model(&tenant.TenantUsage{}) - - // Apply filters - if filter.TenantID != "" { - query = query.Where("tenant_id = ?", filter.TenantID) - } - if filter.ResourceType != "" { - query = query.Where("resource_type = ?", filter.ResourceType) - } - if !filter.PeriodStart.IsZero() { - query = query.Where("period_start >= ?", filter.PeriodStart) - } - if !filter.PeriodEnd.IsZero() { - query = query.Where("period_end <= ?", filter.PeriodEnd) - } - - // Apply pagination - if filter.Limit > 0 { - query = query.Limit(filter.Limit) - } - if filter.Offset > 0 { - query = query.Offset(filter.Offset) - } - - var usage []*tenant.TenantUsage - err := query.Find(&usage).Error - return usage, err -} - // GetUsageStats retrieves usage statistics for a tenant func (r *GormTenantRepository) GetUsageStats(ctx context.Context, tenantID string) (*tenant.TenantUsageStats, error) { // This is a complex query that would typically involve joins and aggregations @@ -336,7 +40,7 @@ func (r *GormTenantRepository) GetUsageStats(ctx context.Context, tenantID strin // GetConfig retrieves tenant configuration func (r *GormTenantRepository) GetConfig(ctx context.Context, tenantID string) (*tenant.TenantConfig, error) { // Get tenant to extract settings - tenant, err := r.GetTenant(ctx, tenantID) + tenantRecord, err := r.GetTenant(ctx, tenantID) if err != nil { return nil, err } @@ -345,13 +49,13 @@ func (r *GormTenantRepository) GetConfig(ctx context.Context, tenantID string) ( config := &tenant.TenantConfig{ TenantID: tenantID, Config: make(map[string]interface{}), - Settings: tenant.Settings, + Settings: tenantRecord.Settings, Quotas: []tenant.ResourceQuota{}, Features: make(map[string]bool), } // Add default quotas based on plan - switch tenant.Plan { + switch tenantRecord.Plan { case tenant.TenantPlanFree: config.Quotas = []tenant.ResourceQuota{ {ResourceType: "users", Limit: 5, Period: "monthly"}, @@ -379,7 +83,7 @@ func (r *GormTenantRepository) GetConfig(ctx context.Context, tenantID string) ( } // Add default features based on plan - switch tenant.Plan { + switch tenantRecord.Plan { case tenant.TenantPlanFree: config.Features = map[string]bool{ "multi_currency": false, @@ -410,16 +114,16 @@ func (r *GormTenantRepository) GetConfig(ctx context.Context, tenantID string) ( func (r *GormTenantRepository) UpdateConfig(ctx context.Context, config *tenant.TenantConfig) error { // Store configuration in tenant metadata or separate table // For now, update tenant settings - tenant, err := r.GetTenant(ctx, config.TenantID) + tenantRecord, err := r.GetTenant(ctx, config.TenantID) if err != nil { return err } - tenant.Settings = config.Settings - tenant.Metadata = config.Config - tenant.UpdatedAt = time.Now() + tenantRecord.Settings = config.Settings + tenantRecord.Metadata = config.Config + tenantRecord.UpdatedAt = time.Now() - return r.UpdateTenant(ctx, tenant) + return r.UpdateTenant(ctx, tenantRecord) } // CreateEvent creates a new tenant event @@ -480,57 +184,3 @@ func (r *GormTenantRepository) ListEvents(ctx context.Context, tenantID string, return events, nil } - -// Helper methods for tenant isolation in other repositories - -// TenantAwareQuery adds tenant filtering to database queries -func (r *GormTenantRepository) TenantAwareQuery(ctx context.Context, model interface{}, tenantID string) *gorm.DB { - query := r.db.WithContext(ctx).Model(model) - - // Add tenant filter if the model has tenant_id field - if tenantID != "" { - query = query.Where("tenant_id = ?", tenantID) - } - - return query -} - -// EnsureTenantIsolation ensures that queries are tenant-isolated -func (r *GormTenantRepository) EnsureTenantIsolation(ctx context.Context, tenantID string) error { - if tenantID == "" { - return fmt.Errorf("tenant ID is required for tenant-isolated operations") - } - - // Validate tenant exists - _, err := r.GetTenant(ctx, tenantID) - if err != nil { - return fmt.Errorf("invalid tenant ID: %w", err) - } - - return nil -} - -// GetTenantFromContext extracts tenant ID from context -func (r *GormTenantRepository) GetTenantFromContext(ctx context.Context) string { - if tenantID, ok := ctx.Value("tenant_id").(string); ok { - return tenantID - } - return "" -} - -// WithTenantIsolation applies tenant isolation to a database operation -func (r *GormTenantRepository) WithTenantIsolation(ctx context.Context, operation func(*gorm.DB) error) error { - tenantID := r.GetTenantFromContext(ctx) - if tenantID == "" { - return fmt.Errorf("tenant ID not found in context") - } - - // Validate tenant exists - if err := r.EnsureTenantIsolation(ctx, tenantID); err != nil { - return err - } - - // Execute operation with tenant filtering - tx := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID) - return operation(tx) -} diff --git a/apps/carrier-connector/internal/repository/tenant_repository_apikey_usage.go b/apps/carrier-connector/internal/repository/tenant_repository_apikey_usage.go new file mode 100644 index 0000000..22ef38e --- /dev/null +++ b/apps/carrier-connector/internal/repository/tenant_repository_apikey_usage.go @@ -0,0 +1,101 @@ +package repository + +import ( + "context" + + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant" +) + +// CreateAPIKey creates a new API key +func (r *GormTenantRepository) CreateAPIKey(ctx context.Context, apiKey *tenant.TenantAPIKey) error { + return r.db.WithContext(ctx).Create(apiKey).Error +} + +// GetAPIKey retrieves an API key by ID +func (r *GormTenantRepository) GetAPIKey(ctx context.Context, id string) (*tenant.TenantAPIKey, error) { + var apiKey tenant.TenantAPIKey + err := r.db.WithContext(ctx).Where("id = ?", id).First(&apiKey).Error + if err != nil { + return nil, err + } + return &apiKey, nil +} + +// GetAPIKeyByHash retrieves an API key by hash +func (r *GormTenantRepository) GetAPIKeyByHash(ctx context.Context, keyHash string) (*tenant.TenantAPIKey, error) { + var apiKey tenant.TenantAPIKey + err := r.db.WithContext(ctx).Where("key_hash = ?", keyHash).First(&apiKey).Error + if err != nil { + return nil, err + } + return &apiKey, nil +} + +// UpdateAPIKey updates an API key +func (r *GormTenantRepository) UpdateAPIKey(ctx context.Context, apiKey *tenant.TenantAPIKey) error { + return r.db.WithContext(ctx).Save(apiKey).Error +} + +// DeleteAPIKey deletes an API key +func (r *GormTenantRepository) DeleteAPIKey(ctx context.Context, id string) error { + return r.db.WithContext(ctx).Delete(&tenant.TenantAPIKey{}, "id = ?", id).Error +} + +// ListAPIKeys lists API keys for a tenant +func (r *GormTenantRepository) ListAPIKeys(ctx context.Context, tenantID string) ([]*tenant.TenantAPIKey, error) { + var apiKeys []*tenant.TenantAPIKey + err := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID).Order("created_at DESC").Find(&apiKeys).Error + return apiKeys, err +} + +// CreateUsage creates a new usage record +func (r *GormTenantRepository) CreateUsage(ctx context.Context, usage *tenant.TenantUsage) error { + return r.db.WithContext(ctx).Create(usage).Error +} + +// GetUsage retrieves usage by tenant and resource type +func (r *GormTenantRepository) GetUsage(ctx context.Context, tenantID, resourceType string) (*tenant.TenantUsage, error) { + var usage tenant.TenantUsage + err := r.db.WithContext(ctx).Where("tenant_id = ? AND resource_type = ?", tenantID, resourceType).First(&usage).Error + if err != nil { + return nil, err + } + return &usage, nil +} + +// UpdateUsage updates a usage record +func (r *GormTenantRepository) UpdateUsage(ctx context.Context, usage *tenant.TenantUsage) error { + return r.db.WithContext(ctx).Save(usage).Error +} + +// ListUsage lists usage records with filtering +func (r *GormTenantRepository) ListUsage(ctx context.Context, filter *tenant.TenantUsageFilter) ([]*tenant.TenantUsage, error) { + query := r.db.WithContext(ctx).Model(&tenant.TenantUsage{}) + + // Apply filters + if filter.TenantID != "" { + query = query.Where("tenant_id = ?", filter.TenantID) + } + if filter.ResourceType != "" { + query = query.Where("resource_type = ?", filter.ResourceType) + } + if !filter.PeriodStart.IsZero() { + query = query.Where("period_start >= ?", filter.PeriodStart) + } + if !filter.PeriodEnd.IsZero() { + query = query.Where("period_end <= ?", filter.PeriodEnd) + } + + // Apply pagination + if filter.Limit > 0 { + query = query.Limit(filter.Limit) + } + if filter.Offset > 0 { + query = query.Offset(filter.Offset) + } + + var usage []*tenant.TenantUsage + err := query.Find(&usage).Error + return usage, err +} + diff --git a/apps/carrier-connector/internal/repository/tenant_repository_crud.go b/apps/carrier-connector/internal/repository/tenant_repository_crud.go new file mode 100644 index 0000000..427206c --- /dev/null +++ b/apps/carrier-connector/internal/repository/tenant_repository_crud.go @@ -0,0 +1,183 @@ +package repository + +import ( + "context" + + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant" + "gorm.io/gorm" +) + +// GormTenantRepository implements the tenant repository interface using GORM +type GormTenantRepository struct { + db *gorm.DB +} + +// NewGormTenantRepository creates a new GORM tenant repository +func NewGormTenantRepository(db *gorm.DB) TenantRepository { + return &GormTenantRepository{db: db} +} + +// CreateTenant creates a new tenant +func (r *GormTenantRepository) CreateTenant(ctx context.Context, tenant *tenant.Tenant) error { + return r.db.WithContext(ctx).Create(tenant).Error +} + +// GetTenant retrieves a tenant by ID +func (r *GormTenantRepository) GetTenant(ctx context.Context, id string) (*tenant.Tenant, error) { + var tenant tenant.Tenant + err := r.db.WithContext(ctx).Where("id = ?", id).First(&tenant).Error + if err != nil { + return nil, err + } + return &tenant, nil +} + +// GetTenantByDomain retrieves a tenant by domain +func (r *GormTenantRepository) GetTenantByDomain(ctx context.Context, domain string) (*tenant.Tenant, error) { + var tenant tenant.Tenant + err := r.db.WithContext(ctx).Where("domain = ?", domain).First(&tenant).Error + if err != nil { + return nil, err + } + return &tenant, nil +} + +// UpdateTenant updates an existing tenant +func (r *GormTenantRepository) UpdateTenant(ctx context.Context, tenant *tenant.Tenant) error { + return r.db.WithContext(ctx).Save(tenant).Error +} + +// DeleteTenant deletes a tenant +func (r *GormTenantRepository) DeleteTenant(ctx context.Context, id string) error { + return r.db.WithContext(ctx).Delete(&tenant.Tenant{}, "id = ?", id).Error +} + +// ListTenants lists tenants with filtering +func (r *GormTenantRepository) ListTenants(ctx context.Context, filter *tenant.TenantFilter) ([]*tenant.Tenant, error) { + query := r.db.WithContext(ctx).Model(&tenant.Tenant{}) + + // Apply filters + if filter.ID != "" { + query = query.Where("id = ?", filter.ID) + } + if filter.Name != "" { + query = query.Where("name ILIKE ?", "%"+filter.Name+"%") + } + if filter.Domain != "" { + query = query.Where("domain ILIKE ?", "%"+filter.Domain+"%") + } + if filter.Status != "" { + query = query.Where("status = ?", filter.Status) + } + if filter.Plan != "" { + query = query.Where("plan = ?", filter.Plan) + } + + // Apply sorting + if filter.SortBy != "" { + order := filter.SortBy + if filter.SortOrder == "desc" { + order += " DESC" + } + query = query.Order(order) + } else { + query = query.Order("created_at DESC") + } + + // Apply pagination + if filter.Limit > 0 { + query = query.Limit(filter.Limit) + } + if filter.Offset > 0 { + query = query.Offset(filter.Offset) + } + + var tenants []*tenant.Tenant + err := query.Find(&tenants).Error + return tenants, err +} + +// CountTenants counts tenants with filtering +func (r *GormTenantRepository) CountTenants(ctx context.Context, filter *tenant.TenantFilter) (int, error) { + query := r.db.WithContext(ctx).Model(&tenant.Tenant{}) + + // Apply filters + if filter.ID != "" { + query = query.Where("id = ?", filter.ID) + } + if filter.Name != "" { + query = query.Where("name ILIKE ?", "%"+filter.Name+"%") + } + if filter.Domain != "" { + query = query.Where("domain ILIKE ?", "%"+filter.Domain+"%") + } + if filter.Status != "" { + query = query.Where("status = ?", filter.Status) + } + if filter.Plan != "" { + query = query.Where("plan = ?", filter.Plan) + } + + var count int64 + err := query.Count(&count).Error + return int(count), err +} + +// CreateTenantUser creates a new tenant user +func (r *GormTenantRepository) CreateTenantUser(ctx context.Context, user *tenant.TenantUser) error { + return r.db.WithContext(ctx).Create(user).Error +} + +// GetTenantUser retrieves a tenant user +func (r *GormTenantRepository) GetTenantUser(ctx context.Context, tenantID, userID string) (*tenant.TenantUser, error) { + var user tenant.TenantUser + err := r.db.WithContext(ctx).Where("tenant_id = ? AND user_id = ?", tenantID, userID).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// UpdateTenantUser updates a tenant user +func (r *GormTenantRepository) UpdateTenantUser(ctx context.Context, user *tenant.TenantUser) error { + return r.db.WithContext(ctx).Save(user).Error +} + +// DeleteTenantUser deletes a tenant user +func (r *GormTenantRepository) DeleteTenantUser(ctx context.Context, tenantID, userID string) error { + return r.db.WithContext(ctx).Delete(&tenant.TenantUser{}, "tenant_id = ? AND user_id = ?", tenantID, userID).Error +} + +// ListTenantUsers lists tenant users with filtering +func (r *GormTenantRepository) ListTenantUsers(ctx context.Context, filter *tenant.TenantUserFilter) ([]*tenant.TenantUser, error) { + query := r.db.WithContext(ctx).Model(&tenant.TenantUser{}) + + // Apply filters + if filter.TenantID != "" { + query = query.Where("tenant_id = ?", filter.TenantID) + } + if filter.UserID != "" { + query = query.Where("user_id = ?", filter.UserID) + } + if filter.Email != "" { + query = query.Where("email ILIKE ?", "%"+filter.Email+"%") + } + if filter.Role != "" { + query = query.Where("role = ?", filter.Role) + } + if filter.Status != "" { + query = query.Where("status = ?", filter.Status) + } + + // Apply pagination + if filter.Limit > 0 { + query = query.Limit(filter.Limit) + } + if filter.Offset > 0 { + query = query.Offset(filter.Offset) + } + + var users []*tenant.TenantUser + err := query.Find(&users).Error + return users, err +} diff --git a/apps/carrier-connector/internal/repository/tenant_repository_methods.go b/apps/carrier-connector/internal/repository/tenant_repository_methods.go new file mode 100644 index 0000000..a4c57ae --- /dev/null +++ b/apps/carrier-connector/internal/repository/tenant_repository_methods.go @@ -0,0 +1,60 @@ +package repository + +import ( + "context" + "fmt" + + "gorm.io/gorm" +) + +// TenantAwareQuery adds tenant filtering to database queries +func (r *GormTenantRepository) TenantAwareQuery(ctx context.Context, model interface{}, tenantID string) *gorm.DB { + query := r.db.WithContext(ctx).Model(model) + + // Add tenant filter if the model has tenant_id field + if tenantID != "" { + query = query.Where("tenant_id = ?", tenantID) + } + + return query +} + +// EnsureTenantIsolation ensures that queries are tenant-isolated +func (r *GormTenantRepository) EnsureTenantIsolation(ctx context.Context, tenantID string) error { + if tenantID == "" { + return fmt.Errorf("tenant ID is required for tenant-isolated operations") + } + + // Validate tenant exists + _, err := r.GetTenant(ctx, tenantID) + if err != nil { + return fmt.Errorf("invalid tenant ID: %w", err) + } + + return nil +} + +// GetTenantFromContext extracts tenant ID from context +func (r *GormTenantRepository) GetTenantFromContext(ctx context.Context) string { + if tenantID, ok := ctx.Value("tenant_id").(string); ok { + return tenantID + } + return "" +} + +// WithTenantIsolation applies tenant isolation to a database operation +func (r *GormTenantRepository) WithTenantIsolation(ctx context.Context, operation func(*gorm.DB) error) error { + tenantID := r.GetTenantFromContext(ctx) + if tenantID == "" { + return fmt.Errorf("tenant ID not found in context") + } + + // Validate tenant exists + if err := r.EnsureTenantIsolation(ctx, tenantID); err != nil { + return err + } + + // Execute operation with tenant filtering + tx := r.db.WithContext(ctx).Where("tenant_id = ?", tenantID) + return operation(tx) +} diff --git a/apps/carrier-connector/internal/repository/tenant_validate_methods_repository.go b/apps/carrier-connector/internal/repository/tenant_validate_methods_repository.go new file mode 100644 index 0000000..2feddad --- /dev/null +++ b/apps/carrier-connector/internal/repository/tenant_validate_methods_repository.go @@ -0,0 +1,192 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/tenant" + "gorm.io/gorm" +) + +func (r *TenantAwareRepository) CreateWithTenant(ctx context.Context, model interface{}) error { + if err := r.ValidateTenant(ctx); err != nil { + return err + } + + // Set tenant ID if the model has a TenantID field + if modelWithTenant, ok := model.(interface{ SetTenantID(string) }); ok { + modelWithTenant.SetTenantID(r.tenantID) + } + + return r.db.WithContext(ctx).Create(model).Error +} + +func (r *TenantAwareRepository) GetByTenantID(ctx context.Context, model interface{}, id string) error { + if err := r.ValidateTenant(ctx); err != nil { + return err + } + + return r.TenantScopedQuery(ctx, model).Where("id = ?", id).First(model).Error +} + +func (r *TenantAwareRepository) UpdateWithTenant(ctx context.Context, model interface{}) error { + if err := r.ValidateTenant(ctx); err != nil { + return err + } + + return r.db.WithContext(ctx).Save(model).Error +} + +func (r *TenantAwareRepository) DeleteWithTenant(ctx context.Context, model interface{}, id string) error { + if err := r.ValidateTenant(ctx); err != nil { + return err + } + + return r.TenantScopedQuery(ctx, model).Where("id = ?", id).Delete(model).Error +} + +func (r *TenantAwareRepository) ListWithTenant(ctx context.Context, model interface{}, results interface{}, filters map[string]interface{}) error { + if err := r.ValidateTenant(ctx); err != nil { + return err + } + + query := r.TenantScopedQuery(ctx, model) + + // Apply filters + for key, value := range filters { + query = query.Where(key+" = ?", value) + } + + return query.Find(results).Error +} + +func (r *TenantAwareRepository) CountWithTenant(ctx context.Context, model interface{}, filters map[string]interface{}) (int64, error) { + if err := r.ValidateTenant(ctx); err != nil { + return 0, err + } + + query := r.TenantScopedQuery(ctx, model) + + // Apply filters + for key, value := range filters { + query = query.Where(key+" = ?", value) + } + + var count int64 + err := query.Count(&count).Error + return count, err +} + +type TenantAwareModel interface { + SetTenantID(tenantID string) + GetTenantID() string +} + +type BaseTenantModel struct { + TenantID string `json:"tenant_id" gorm:"column:tenant_id;index;not null"` +} + +func (m *BaseTenantModel) SetTenantID(tenantID string) { + m.TenantID = tenantID +} + +func (m *BaseTenantModel) GetTenantID() string { + return m.TenantID +} + +type TenantIsolationMiddleware struct { + db *gorm.DB +} + +func NewTenantIsolationMiddleware(db *gorm.DB) *TenantIsolationMiddleware { + return &TenantIsolationMiddleware{db: db} +} + +func (m *TenantIsolationMiddleware) WithTenantIsolation(ctx context.Context, tenantID string, operation func(*gorm.DB) error) error { + if tenantID == "" { + return fmt.Errorf("tenant ID is required for tenant-isolated operations") + } + + var tenantRecord tenant.Tenant + err := m.db.WithContext(ctx).Where("id = ? AND status = ?", tenantID, tenant.TenantStatusActive).First(&tenantRecord).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("tenant not found or inactive") + } + return fmt.Errorf("failed to validate tenant: %w", err) + } + + tx := m.db.WithContext(ctx).Where("tenant_id = ?", tenantID) + return operation(tx) +} + +func GetTenantFromContext(ctx context.Context) string { + if tenantID, ok := ctx.Value("tenant_id").(string); ok { + return tenantID + } + return "" +} + +func SetTenantInContext(ctx context.Context, tenantID string) context.Context { + return context.WithValue(ctx, "tenant_id", tenantID) +} + +type TenantQueryBuilder struct { + db *gorm.DB + tenantID string + query *gorm.DB +} + +func NewTenantQueryBuilder(db *gorm.DB, tenantID string) *TenantQueryBuilder { + return &TenantQueryBuilder{ + db: db, + tenantID: tenantID, + query: db.Where("tenant_id = ?", tenantID), + } +} + +func (b *TenantQueryBuilder) Where(query string, args ...interface{}) *TenantQueryBuilder { + b.query = b.query.Where(query, args...) + return b +} + +func (b *TenantQueryBuilder) Order(value string) *TenantQueryBuilder { + b.query = b.query.Order(value) + return b +} + +func (b *TenantQueryBuilder) Limit(limit int) *TenantQueryBuilder { + b.query = b.query.Limit(limit) + return b +} + +func (b *TenantQueryBuilder) Offset(offset int) *TenantQueryBuilder { + b.query = b.query.Offset(offset) + return b +} + +func (b *TenantQueryBuilder) Find(dest interface{}) error { + return b.query.Find(dest).Error +} + +func (b *TenantQueryBuilder) First(dest interface{}) error { + return b.query.First(dest).Error +} + +func (b *TenantQueryBuilder) Count() (int64, error) { + var count int64 + err := b.query.Count(&count).Error + return count, err +} + +func (b *TenantQueryBuilder) GetQuery() *gorm.DB { + return b.query +} + +type TenantResourceValidator struct { + db *gorm.DB +} + +func NewTenantResourceValidator(db *gorm.DB) *TenantResourceValidator { + return &TenantResourceValidator{db: db} +} \ No newline at end of file diff --git a/apps/carrier-connector/internal/services/rateplan_core.go b/apps/carrier-connector/internal/services/rateplan_core.go index a98d2f4..6f96a60 100644 --- a/apps/carrier-connector/internal/services/rateplan_core.go +++ b/apps/carrier-connector/internal/services/rateplan_core.go @@ -11,19 +11,17 @@ import ( "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 + exchangeService *currency.ExchangeRateService ratePlanService rateplan.Service logger *logrus.Logger baseCurrency string } -// NewRatePlanCurrencyIntegrator creates a new rate plan currency integrator func NewRatePlanCurrencyIntegrator( billingService currency.BillingService, - exchangeService *ExchangeRateService, + exchangeService *currency.ExchangeRateService, ratePlanService rateplan.Service, logger *logrus.Logger, baseCurrency string, @@ -37,20 +35,22 @@ func NewRatePlanCurrencyIntegrator( } } -// 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) + conversionReq := ¤cy.CurrencyConversionRequest{ + Amount: plan.BasePrice, + FromCurrency: plan.Currency, + ToCurrency: targetCurrency, + } + conversion, err := rpci.billingService.ConvertAmount(ctx, conversionReq) if err != nil { rpci.logger.WithError(err).Error("Failed to convert rate plan price") return nil, fmt.Errorf("currency conversion failed: %w", err) @@ -59,7 +59,6 @@ func (rpci *RatePlanCurrencyIntegrator) SubscribeToPlanWithCurrency(ctx context. exchangeRate = conversion.ExchangeRate } - // Create subscription with currency information subscription := &rateplan.RatePlanSubscription{ ProfileID: profileID, RatePlanID: planID, @@ -76,7 +75,6 @@ func (rpci *RatePlanCurrencyIntegrator) SubscribeToPlanWithCurrency(ctx context. UpdatedAt: time.Now(), } - // Create subscription request subscribeReq := &rateplan.SubscribeRequest{ ProfileID: profileID, RatePlanID: planID, @@ -89,7 +87,6 @@ func (rpci *RatePlanCurrencyIntegrator) SubscribeToPlanWithCurrency(ctx context. return nil, fmt.Errorf("failed to create subscription: %w", err) } - // Process initial billing billingReq := ¤cy.BillingRequest{ ProfileID: profileID, SubscriptionID: createdSubscription.ID, @@ -115,18 +112,14 @@ func (rpci *RatePlanCurrencyIntegrator) SubscribeToPlanWithCurrency(ctx context. 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 { @@ -136,12 +129,16 @@ func (rpci *RatePlanCurrencyIntegrator) CalculatePlanCostInCurrency(ctx context. } } - // Convert to requested currency convertedCost := baseCost exchangeRate := 1.0 if targetCurrency != plan.Currency { - conversion, err := rpci.exchangeService.ConvertAmount(ctx, baseCost, plan.Currency, targetCurrency) + conversionReq := ¤cy.CurrencyConversionRequest{ + Amount: baseCost, + FromCurrency: plan.Currency, + ToCurrency: targetCurrency, + } + conversion, err := rpci.billingService.ConvertAmount(ctx, conversionReq) if err != nil { return nil, fmt.Errorf("currency conversion failed: %w", err) } @@ -149,7 +146,6 @@ func (rpci *RatePlanCurrencyIntegrator) CalculatePlanCostInCurrency(ctx context. exchangeRate = conversion.ExchangeRate } - // Create billing summary summary := ¤cy.BillingSummary{ ProfileID: usageData.ProfileID, TotalAmount: convertedCost, @@ -171,3 +167,30 @@ func (rpci *RatePlanCurrencyIntegrator) CalculatePlanCostInCurrency(ctx context. return summary, nil } + +func (rpci *RatePlanCurrencyIntegrator) calculateOverageCost(ctx context.Context, plan *rateplan.RatePlan, usage *rateplan.RatePlanUsage) (float64, error) { + overageCost := 0.0 + + 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 + } + } + + 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 + } + } + + 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 +}