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

- Move GormTenantRepository implementation from repository package to tenant package
- Move TenantResourceValidator from repository to tenant package
- Update tenant_aware_repository.go to import tenant package for Tenant and TenantStatusActive types
- Remove duplicate tenant model definitions and helper methods from repository package
- Update ValidateTenant to use tenant.Tenant and tenant.TenantStatusActive types
This commit is contained in:
nutcas3
2026-05-02 03:18:18 +03:00
parent 77483349c9
commit a11ad09109
7 changed files with 590 additions and 594 deletions
@@ -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 {
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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)
}
@@ -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}
}
@@ -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 := &currency.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 := &currency.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 := &currency.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 := &currency.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
}