package services import ( "context" "fmt" "time" "github.com/google/uuid" "go.uber.org/zap" "github.com/RyanCopley/skybridge/user/internal/domain" "github.com/RyanCopley/skybridge/user/internal/repository/interfaces" ) // UserService defines the interface for user business logic type UserService interface { // Create creates a new user Create(ctx context.Context, req *domain.CreateUserRequest, actorID string) (*domain.User, error) // GetByID retrieves a user by ID GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) // GetByEmail retrieves a user by email GetByEmail(ctx context.Context, email string) (*domain.User, error) // Update updates an existing user Update(ctx context.Context, id uuid.UUID, req *domain.UpdateUserRequest, actorID string) (*domain.User, error) // Delete deletes a user by ID Delete(ctx context.Context, id uuid.UUID, actorID string) error // List retrieves users with filtering and pagination List(ctx context.Context, req *domain.ListUsersRequest) (*domain.ListUsersResponse, error) // UpdateLastLogin updates the last login timestamp UpdateLastLogin(ctx context.Context, id uuid.UUID) error // ExistsByEmail checks if a user exists with the given email ExistsByEmail(ctx context.Context, email string) (bool, error) } type userService struct { userRepo interfaces.UserRepository profileRepo interfaces.UserProfileRepository auditRepo interfaces.AuditRepository logger *zap.Logger } // NewUserService creates a new user service func NewUserService( userRepo interfaces.UserRepository, profileRepo interfaces.UserProfileRepository, auditRepo interfaces.AuditRepository, logger *zap.Logger, ) UserService { return &userService{ userRepo: userRepo, profileRepo: profileRepo, auditRepo: auditRepo, logger: logger, } } func (s *userService) Create(ctx context.Context, req *domain.CreateUserRequest, actorID string) (*domain.User, error) { // Validate email uniqueness exists, err := s.userRepo.ExistsByEmail(ctx, req.Email) if err != nil { s.logger.Error("Failed to check email uniqueness", zap.String("email", req.Email), zap.Error(err)) return nil, fmt.Errorf("failed to validate email uniqueness: %w", err) } if exists { return nil, fmt.Errorf("user with email %s already exists", req.Email) } // Create user domain object user := &domain.User{ ID: uuid.New(), Email: req.Email, FirstName: req.FirstName, LastName: req.LastName, DisplayName: req.DisplayName, Avatar: req.Avatar, Role: req.Role, Status: req.Status, CreatedBy: actorID, UpdatedBy: actorID, } if user.Status == "" { user.Status = domain.UserStatusPending } // Create user in database err = s.userRepo.Create(ctx, user) if err != nil { s.logger.Error("Failed to create user", zap.String("email", req.Email), zap.Error(err)) return nil, fmt.Errorf("failed to create user: %w", err) } // Create default user profile profile := &domain.UserProfile{ UserID: user.ID, Bio: "", Location: "", Website: "", Timezone: "UTC", Language: "en", Preferences: make(map[string]interface{}), } err = s.profileRepo.Create(ctx, profile) if err != nil { s.logger.Warn("Failed to create user profile", zap.String("user_id", user.ID.String()), zap.Error(err)) // Don't fail user creation if profile creation fails } // Log audit event if s.auditRepo != nil { auditEvent := &interfaces.AuditEvent{ ID: uuid.New(), Type: "user.created", Severity: "info", Status: "success", Timestamp: time.Now().Format(time.RFC3339), ActorID: actorID, ActorType: "user", ResourceID: user.ID.String(), ResourceType: "user", Action: "create", Description: fmt.Sprintf("User %s created", user.Email), Details: map[string]interface{}{ "user_id": user.ID.String(), "email": user.Email, "role": user.Role, "status": user.Status, }, } _ = s.auditRepo.LogEvent(ctx, auditEvent) } s.logger.Info("User created successfully", zap.String("user_id", user.ID.String()), zap.String("email", user.Email), zap.String("actor", actorID)) return user, nil } func (s *userService) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) { user, err := s.userRepo.GetByID(ctx, id) if err != nil { s.logger.Debug("Failed to get user by ID", zap.String("id", id.String()), zap.Error(err)) return nil, err } return user, nil } func (s *userService) GetByEmail(ctx context.Context, email string) (*domain.User, error) { user, err := s.userRepo.GetByEmail(ctx, email) if err != nil { s.logger.Debug("Failed to get user by email", zap.String("email", email), zap.Error(err)) return nil, err } return user, nil } func (s *userService) Update(ctx context.Context, id uuid.UUID, req *domain.UpdateUserRequest, actorID string) (*domain.User, error) { // Get existing user existingUser, err := s.userRepo.GetByID(ctx, id) if err != nil { return nil, err } // Check email uniqueness if email is being updated if req.Email != nil && *req.Email != existingUser.Email { exists, err := s.userRepo.ExistsByEmail(ctx, *req.Email) if err != nil { s.logger.Error("Failed to check email uniqueness", zap.String("email", *req.Email), zap.Error(err)) return nil, fmt.Errorf("failed to validate email uniqueness: %w", err) } if exists { return nil, fmt.Errorf("user with email %s already exists", *req.Email) } } // Update fields if req.Email != nil { existingUser.Email = *req.Email } if req.FirstName != nil { existingUser.FirstName = *req.FirstName } if req.LastName != nil { existingUser.LastName = *req.LastName } if req.DisplayName != nil { existingUser.DisplayName = req.DisplayName } if req.Avatar != nil { existingUser.Avatar = req.Avatar } if req.Role != nil { existingUser.Role = *req.Role } if req.Status != nil { existingUser.Status = *req.Status } existingUser.UpdatedBy = actorID // Update user in database err = s.userRepo.Update(ctx, existingUser) if err != nil { s.logger.Error("Failed to update user", zap.String("id", id.String()), zap.Error(err)) return nil, fmt.Errorf("failed to update user: %w", err) } // Log audit event if s.auditRepo != nil { auditEvent := &interfaces.AuditEvent{ ID: uuid.New(), Type: "user.updated", Severity: "info", Status: "success", Timestamp: time.Now().Format(time.RFC3339), ActorID: actorID, ActorType: "user", ResourceID: id.String(), ResourceType: "user", Action: "update", Description: fmt.Sprintf("User %s updated", existingUser.Email), Details: map[string]interface{}{ "user_id": id.String(), "email": existingUser.Email, "role": existingUser.Role, "status": existingUser.Status, }, } _ = s.auditRepo.LogEvent(ctx, auditEvent) } s.logger.Info("User updated successfully", zap.String("user_id", id.String()), zap.String("email", existingUser.Email), zap.String("actor", actorID)) return existingUser, nil } func (s *userService) Delete(ctx context.Context, id uuid.UUID, actorID string) error { // Get user for audit logging user, err := s.userRepo.GetByID(ctx, id) if err != nil { return err } // Delete user profile first _ = s.profileRepo.Delete(ctx, id) // Don't fail if profile doesn't exist // Delete user err = s.userRepo.Delete(ctx, id) if err != nil { s.logger.Error("Failed to delete user", zap.String("id", id.String()), zap.Error(err)) return fmt.Errorf("failed to delete user: %w", err) } // Log audit event if s.auditRepo != nil { auditEvent := &interfaces.AuditEvent{ ID: uuid.New(), Type: "user.deleted", Severity: "warn", Status: "success", Timestamp: time.Now().Format(time.RFC3339), ActorID: actorID, ActorType: "user", ResourceID: id.String(), ResourceType: "user", Action: "delete", Description: fmt.Sprintf("User %s deleted", user.Email), Details: map[string]interface{}{ "user_id": id.String(), "email": user.Email, }, } _ = s.auditRepo.LogEvent(ctx, auditEvent) } s.logger.Info("User deleted successfully", zap.String("user_id", id.String()), zap.String("email", user.Email), zap.String("actor", actorID)) return nil } func (s *userService) List(ctx context.Context, req *domain.ListUsersRequest) (*domain.ListUsersResponse, error) { response, err := s.userRepo.List(ctx, req) if err != nil { s.logger.Error("Failed to list users", zap.Error(err)) return nil, fmt.Errorf("failed to list users: %w", err) } return response, nil } func (s *userService) UpdateLastLogin(ctx context.Context, id uuid.UUID) error { err := s.userRepo.UpdateLastLogin(ctx, id) if err != nil { s.logger.Error("Failed to update last login", zap.String("id", id.String()), zap.Error(err)) return fmt.Errorf("failed to update last login: %w", err) } return nil } func (s *userService) ExistsByEmail(ctx context.Context, email string) (bool, error) { exists, err := s.userRepo.ExistsByEmail(ctx, email) if err != nil { s.logger.Error("Failed to check email existence", zap.String("email", email), zap.Error(err)) return false, fmt.Errorf("failed to check email existence: %w", err) } return exists, nil }