406 lines
12 KiB
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
|
|
}
|