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 }