aurganize-backend/backend/internal/services/tenant_service.go

406 lines
12 KiB
Go

package services
import (
"context"
"errors"
"fmt"
"strings"
"github.com/creativenoz/aurganize-v62/backend/internal/config"
"github.com/creativenoz/aurganize-v62/backend/internal/models"
"github.com/creativenoz/aurganize-v62/backend/internal/repositories"
"github.com/creativenoz/aurganize-v62/backend/pkg/slug"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"github.com/rs/zerolog/log"
)
var (
ErrTenantNotFound = errors.New("tenant not found")
ErrSlugExists = errors.New("tenant slug already exists")
)
type TenantService struct {
config *config.Config
tenantRepo *repositories.TenantRepository
userRepo *repositories.UserRepository
db *sqlx.DB
}
func NewTenantService(
config *config.Config,
tenantRepo *repositories.TenantRepository,
userRepo *repositories.UserRepository,
db *sqlx.DB,
) *TenantService {
log.Info().
Str("service", "tenant").
Str("component", "service_init").
Bool("has_tenant_repo", tenantRepo != nil).
Bool("has_user_repo", userRepo != nil).
Bool("has_db", db != nil).
Msg("tenant service initialized")
return &TenantService{
config: config,
tenantRepo: tenantRepo,
userRepo: userRepo,
db: db,
}
}
func (ts *TenantService) Create(ctx context.Context, input *models.CreateTenantInput) (*models.Tenant, error) {
const maxRetries = 5
log.Info().
Str("service", "tenant").
Str("action", "create_tenant_started").
Str("tenant_name", input.Name).
Str("email", *input.Email).
Int("max_retries", maxRetries).
Msg("creating tenant (standalone, not with user)")
if input.Name == "" {
log.Warn().
Str("service", "tenant").
Str("action", "create_tenant_validation_failed").
Str("validation_error", "empty_name").
Msg("tenant creation failed - tenant name is required")
return nil, fmt.Errorf("tenant name is required")
}
if *input.Email == "" {
log.Warn().
Str("service", "tenant").
Str("action", "create_tenant_validation_failed").
Str("validation_error", "empty_email").
Msg("tenant creation failed - tenant email is required")
return nil, fmt.Errorf("tenant email is required")
}
for attempt := 0; attempt < maxRetries; attempt++ {
baseSlug := slug.Generate(input.Name)
log.Debug().
Str("service", "tenant").
Str("action", "generating_unique_slug").
Int("attempt", attempt+1).
Int("max_retries", maxRetries).
Str("tenant_name", input.Name).
Str("base_slug", baseSlug).
Msg("attempting to generate unique slug for tenant")
uniqueSlug := slug.GenerateUnique(baseSlug, func(candidate string) bool {
exits, err := ts.tenantRepo.SlugExists(ctx, candidate)
if err != nil {
log.Error().
Str("service", "tenant").
Str("action", "slug_exists_check_error").
Str("candidate_slug", candidate).
Err(err).
Msg("database error while checking slug existence")
return true
}
return exits
})
log.Debug().
Str("service", "tenant").
Str("action", "unique_slug_generated").
Str("tenant_name", input.Name).
Str("unique_slug", uniqueSlug).
Int("attempt", attempt+1).
Msg("unique slug generated for tenant")
tenant, err := ts.tenantRepo.Create(ctx, input, uniqueSlug)
if err == nil {
log.Info().
Str("service", "tenant").
Str("action", "create_tenant_success").
Str("tenant_id", tenant.ID.String()).
Str("tenant_name", tenant.Name).
Str("slug", tenant.Slug).
Int("attempts_needed", attempt+1).
Msg("tenant created successfully")
return tenant, nil
}
if isUniqueViolationError(err, "tenant_slug_key") {
log.Warn().
Str("service", "tenant").
Str("action", "slug_collision_detected").
Str("tenant_name", input.Name).
Str("attempted_slug", uniqueSlug).
Int("attempt", attempt+1).
Int("remaining_attempts", maxRetries-attempt-1).
Msg("slug collision detected, retrying with different slug")
continue
}
log.Error().
Str("service", "tenant").
Str("action", "create_tenant_database_error").
Str("tenant_name", input.Name).
Str("slug", uniqueSlug).
Err(err).
Msg("tenant creation failed with database error")
return nil, fmt.Errorf("failed to create tenant : %w", err)
}
log.Error().
Str("service", "tenant").
Str("action", "create_tenant_max_retries_exceeded").
Str("tenant_name", input.Name).
Int("max_retries", maxRetries).
Msg("failed to create tenant - exhausted all slug generation attempts")
return nil, fmt.Errorf("failed to create unique slug after %d attempts", maxRetries)
}
func (ts *TenantService) CreateWithUser(ctx context.Context, input *models.CreateTenantWithUserInput) (*models.Tenant, *models.User, error) {
log.Info().
Str("service", "tenant").
Str("action", "create_tenant_with_user_started").
Str("tenant_name", input.TenantName).
Str("email", *input.Email).
Bool("has_first_name", input.FirstName != nil).
Bool("has_last_name", input.LastName != nil).
Msg("starting tenant and user creation transaction (registration)")
tx, err := ts.db.BeginTxx(ctx, nil)
if err != nil {
log.Error().
Str("service", "tenant").
Str("action", "transaction_begin_failed").
Err(err).
Msg("CRITICAL: failed to begin database transaction - registration broken")
return nil, nil, fmt.Errorf("failed to begin transaction : %w", err)
}
defer func() {
if err != nil {
log.Info().
Str("service", "tenant").
Str("action", "transaction_rollback").
Str("tenant_name", input.TenantName).
Msg("rolling back transaction due to error")
tx.Rollback()
}
}()
if input.TenantName == "" {
log.Warn().
Str("service", "tenant").
Str("action", "registration_validation_failed").
Str("validation_error", "empty_tenant_name").
Msg("registration failed - tenant name is required")
return nil, nil, fmt.Errorf("tenant name is required")
}
if *input.Email == "" {
log.Warn().
Str("service", "tenant").
Str("action", "registration_validation_failed").
Str("validation_error", "empty_email").
Msg("registration failed - email is required")
return nil, nil, fmt.Errorf("tenant email is required")
}
if *input.Password == "" {
log.Warn().
Str("service", "tenant").
Str("action", "registration_validation_failed").
Str("validation_error", "empty_password").
Msg("registration failed - password is required")
return nil, nil, fmt.Errorf("password is required")
}
log.Debug().
Str("service", "tenant").
Str("action", "generating_registration_slug").
Str("tenant_name", input.TenantName).
Msg("generating unique slug for registration")
baseSlug := slug.Generate(input.TenantName)
uniqueSlug := slug.GenerateUnique(baseSlug, func(candidate string) bool {
exists, err := ts.tenantRepo.SlugExists(ctx, candidate)
if err != nil {
log.Error().
Str("service", "tenant").
Str("action", "registration_slug_check_error").
Str("candidate_slug", candidate).
Err(err).
Msg("error checking slug existence during registration")
return true
}
return exists
})
log.Debug().
Str("service", "tenant").
Str("action", "registration_slug_generated").
Str("tenant_name", input.TenantName).
Str("unique_slug", uniqueSlug).
Msg("unique slug generated for registration")
tenantInput := &models.CreateTenantInput{
Name: input.TenantName,
Email: input.Email,
Timezone: "UTC",
Currency: "INR",
Locale: "en-US",
}
log.Debug().
Str("service", "tenant").
Str("action", "creating_tenant_in_transaction").
Str("tenant_name", input.TenantName).
Str("slug", uniqueSlug).
Msg("creating tenant record in transaction")
tenant, err := ts.tenantRepo.CreateTx(ctx, tx, tenantInput, uniqueSlug)
if err != nil {
log.Error().
Str("service", "tenant").
Str("action", "tenant_creation_in_transaction_failed").
Str("tenant_name", input.TenantName).
Str("slug", uniqueSlug).
Err(err).
Msg("failed to create tenant in transaction - registration will fail")
return nil, nil, fmt.Errorf("failed to create tenant: %w", err)
}
log.Debug().
Str("service", "tenant").
Str("action", "creating_user_in_transaction").
Str("tenant_id", tenant.ID.String()).
Str("tenant_name", tenant.Name).
Str("email", *input.Email).
Msg("tenant created, now creating user in same transaction")
userInput := &models.CreateUserInput{
TenantID: tenant.ID,
Email: *input.Email,
Password: *input.Password,
FirstName: input.FirstName,
LastName: input.LastName,
Role: "admin",
Status: "active",
}
user, err := ts.userRepo.CreateTx(ctx, tx, userInput)
if err != nil {
log.Error().
Str("service", "tenant").
Str("action", "user_creation_in_transaction_failed").
Str("tenant_id", tenant.ID.String()).
Str("email", *input.Email).
Err(err).
Msg("failed to create user in transaction - entire registration will rollback")
return nil, nil, fmt.Errorf("failed to create user: %w", err)
}
log.Debug().
Str("service", "tenant").
Str("action", "committing_registration_transaction").
Str("tenant_id", tenant.ID.String()).
Str("user_id", user.ID.String()).
Msg("committing registration transaction")
if err = tx.Commit(); err != nil {
log.Error().
Str("service", "tenant").
Str("action", "transaction_commit_failed").
Str("tenant_id", tenant.ID.String()).
Str("user_id", user.ID.String()).
Err(err).
Msg("CRITICAL: failed to commit registration transaction - data may be lost")
return nil, nil, fmt.Errorf("failed to commit transaction: %w", err)
}
log.Info().
Str("service", "tenant").
Str("action", "create_tenant_with_user_success").
Str("tenant_id", tenant.ID.String()).
Str("tenant_name", tenant.Name).
Str("slug", tenant.Slug).
Str("user_id", user.ID.String()).
Str("email", user.Email).
Str("role", user.Role).
Str("timezone", tenant.Timezone).
Str("currency", tenant.Currency).
Str("locale", tenant.Locale).
Msg("registration completed successfully - new tenant and user created")
return tenant, user, nil
}
func (ts *TenantService) GetByID(ctx context.Context, id uuid.UUID) (*models.Tenant, error) {
log.Debug().
Str("service", "tenant").
Str("action", "get_tenant_by_id").
Str("tenant_id", id.String()).
Msg("retrieving tenant by id")
tenant, err := ts.tenantRepo.FindByID(ctx, id)
if err != nil {
log.Error().
Str("service", "tenant").
Str("action", "get_tenant_error").
Str("tenant_id", id.String()).
Err(err).
Msg("database error while retrieving tenant")
return nil, fmt.Errorf("database error : %w", err)
}
if tenant == nil {
log.Warn().
Str("service", "tenant").
Str("action", "tenant_not_found").
Str("tenant_id", id.String()).
Msg("tenant not found by id")
return nil, ErrTenantNotFound
}
log.Debug().
Str("service", "tenant").
Str("action", "get_tenant_success").
Str("tenant_id", tenant.ID.String()).
Str("tenant_name", tenant.Name).
Msg("tenant retrieved successfully")
return tenant, nil
}
func (ts *TenantService) SlugExists(ctx context.Context, name string) (bool, error) {
log.Debug().
Str("service", "tenant").
Str("action", "check_slug_exists").
Str("name", name).
Msg("checking if tenant name/slug is available")
slug := slug.Generate(name)
exists, err := ts.tenantRepo.SlugExists(ctx, slug)
if err != nil {
log.Error().
Str("service", "tenant").
Str("action", "slug_exists_check_error").
Str("name", name).
Str("slug", slug).
Err(err).
Msg("error checking slug existence")
}
if exists {
log.Info().
Str("service", "tenant").
Str("action", "slug_unavailable").
Str("name", name).
Str("slug", slug).
Msg("tenant name/slug is already taken")
} else {
log.Debug().
Str("service", "tenant").
Str("action", "slug_available").
Str("name", name).
Str("slug", slug).
Msg("tenant name/slug is available")
}
return exists, nil
}
func isUniqueViolationError(err error, constraintName string) bool {
if pqErr, ok := err.(*pq.Error); ok {
return pqErr.Code == "23505" &&
(constraintName == "" || strings.Contains(pqErr.Constraint, constraintName))
}
return false
}