package processor import ( "context" "fmt" "golang.org/x/crypto/bcrypt" "eslogad-be/internal/contract" "eslogad-be/internal/entities" "eslogad-be/internal/transformer" "github.com/google/uuid" ) type UserProcessorImpl struct { userRepo UserRepository profileRepo UserProfileRepository } type UserProfileRepository interface { GetByUserID(ctx context.Context, userID uuid.UUID) (*entities.UserProfile, error) Create(ctx context.Context, profile *entities.UserProfile) error Upsert(ctx context.Context, profile *entities.UserProfile) error Update(ctx context.Context, profile *entities.UserProfile) error } func NewUserProcessor( userRepo UserRepository, profileRepo UserProfileRepository, ) *UserProcessorImpl { return &UserProcessorImpl{ userRepo: userRepo, profileRepo: profileRepo, } } func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error) { existingUser, err := p.userRepo.GetByEmail(ctx, req.Email) if err == nil && existingUser != nil { return nil, fmt.Errorf("user with email %s already exists", req.Email) } passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return nil, fmt.Errorf("failed to hash password: %w", err) } userEntity := transformer.CreateUserRequestToEntity(req, string(passwordHash)) err = p.userRepo.Create(ctx, userEntity) if err != nil { return nil, fmt.Errorf("failed to create user: %w", err) } defaultFullName := userEntity.Name profile := &entities.UserProfile{ UserID: userEntity.ID, FullName: defaultFullName, Timezone: "Asia/Jakarta", Locale: "id-ID", Preferences: entities.JSONB{}, NotificationPrefs: entities.JSONB{}, } _ = p.profileRepo.Create(ctx, profile) return transformer.EntityToContract(userEntity), nil } func (p *UserProcessorImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error) { existingUser, err := p.userRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } if req.Email != nil && *req.Email != existingUser.Email { existingUserByEmail, err := p.userRepo.GetByEmail(ctx, *req.Email) if err == nil && existingUserByEmail != nil && existingUserByEmail.ID != id { return nil, fmt.Errorf("user with email %s already exists", *req.Email) } } updated := transformer.UpdateUserEntity(existingUser, req) err = p.userRepo.Update(ctx, updated) if err != nil { return nil, fmt.Errorf("failed to update user: %w", err) } return transformer.EntityToContract(updated), nil } func (p *UserProcessorImpl) DeleteUser(ctx context.Context, id uuid.UUID) error { _, err := p.userRepo.GetByID(ctx, id) if err != nil { return fmt.Errorf("user not found: %w", err) } err = p.userRepo.Delete(ctx, id) if err != nil { return fmt.Errorf("failed to delete user: %w", err) } return nil } func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) { user, err := p.userRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } resp := transformer.EntityToContract(user) if resp != nil { // Roles are loaded separately since they're not preloaded if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil { resp.Roles = transformer.RolesToContract(roles) } // Departments are now preloaded, so they're already in the response } return resp, nil } func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) { user, err := p.userRepo.GetByEmail(ctx, email) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } // Departments are now preloaded, so they're already in the response return transformer.EntityToContract(user), nil } func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error) { page := req.Page if page <= 0 { page = 1 } limit := req.Limit if limit <= 0 { limit = 10 } offset := (page - 1) * limit users, totalCount, err := p.userRepo.ListWithFilters(ctx, req.Search, req.RoleCode, req.IsActive, limit, offset) if err != nil { return nil, 0, fmt.Errorf("failed to get users: %w", err) } responses := transformer.EntitiesToContracts(users) userIDs := make([]uuid.UUID, 0, len(responses)) for i := range responses { userIDs = append(userIDs, responses[i].ID) } // Roles are loaded separately since they're not preloaded rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs) if err == nil { for i := range responses { if roles, ok := rolesMap[responses[i].ID]; ok { responses[i].Roles = transformer.RolesToContract(roles) } } } // Departments are now preloaded, so they're already in the responses return responses, int(totalCount), nil } func (p *UserProcessorImpl) GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error) { user, err := p.userRepo.GetByEmail(ctx, email) if err != nil { return nil, fmt.Errorf("user not found: %w", err) } return user, nil } func (p *UserProcessorImpl) ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error { user, err := p.userRepo.GetByID(ctx, userID) if err != nil { return fmt.Errorf("user not found: %w", err) } err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)) if err != nil { return fmt.Errorf("current password is incorrect") } newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("failed to hash new password: %w", err) } err = p.userRepo.UpdatePassword(ctx, userID, string(newPasswordHash)) if err != nil { return fmt.Errorf("failed to update password: %w", err) } return nil } func (p *UserProcessorImpl) ActivateUser(ctx context.Context, userID uuid.UUID) error { _, err := p.userRepo.GetByID(ctx, userID) if err != nil { return fmt.Errorf("user not found: %w", err) } err = p.userRepo.UpdateActiveStatus(ctx, userID, true) if err != nil { return fmt.Errorf("failed to activate user: %w", err) } return nil } func (p *UserProcessorImpl) DeactivateUser(ctx context.Context, userID uuid.UUID) error { _, err := p.userRepo.GetByID(ctx, userID) if err != nil { return fmt.Errorf("user not found: %w", err) } err = p.userRepo.UpdateActiveStatus(ctx, userID, false) if err != nil { return fmt.Errorf("failed to deactivate user: %w", err) } return nil } // RBAC implementations func (p *UserProcessorImpl) GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error) { roles, err := p.userRepo.GetRolesByUserID(ctx, userID) if err != nil { return nil, err } return transformer.RolesToContract(roles), nil } func (p *UserProcessorImpl) GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error) { perms, err := p.userRepo.GetPermissionsByUserID(ctx, userID) if err != nil { return nil, err } codes := make([]string, 0, len(perms)) for _, p := range perms { codes = append(codes, p.Code) } return codes, nil } func (p *UserProcessorImpl) GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error) { departments, err := p.userRepo.GetDepartmentsByUserID(ctx, userID) if err != nil { return nil, err } return transformer.DepartmentsToContract(departments), nil } func (p *UserProcessorImpl) GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) { prof, err := p.profileRepo.GetByUserID(ctx, userID) if err != nil { return nil, err } return transformer.ProfileEntityToContract(prof), nil } func (p *UserProcessorImpl) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) { existing, _ := p.profileRepo.GetByUserID(ctx, userID) entity := transformer.ProfileUpdateToEntity(userID, req, existing) if existing == nil { if err := p.profileRepo.Create(ctx, entity); err != nil { return nil, err } } else { if err := p.profileRepo.Update(ctx, entity); err != nil { return nil, err } } return transformer.ProfileEntityToContract(entity), nil } // GetActiveUsersForMention retrieves active users for mention purposes with optional username search func (p *UserProcessorImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) { if limit <= 0 { limit = 50 // Default limit for mention suggestions } if limit > 100 { limit = 100 // Max limit for mention suggestions } // Set isActive to true to only get active users isActive := true users, _, err := p.userRepo.ListWithFilters(ctx, search, nil, &isActive, limit, 0) if err != nil { return nil, fmt.Errorf("failed to get active users: %w", err) } responses := transformer.EntitiesToContracts(users) userIDs := make([]uuid.UUID, 0, len(responses)) for i := range responses { userIDs = append(userIDs, responses[i].ID) } // Load roles for the users rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs) if err == nil { for i := range responses { if roles, ok := rolesMap[responses[i].ID]; ok { responses[i].Roles = transformer.RolesToContract(roles) } } } return responses, nil } // BulkCreateUsersWithTransaction creates multiple users in a transaction with proper error handling func (p *UserProcessorImpl) BulkCreateUsersWithTransaction(ctx context.Context, userRequests []contract.BulkUserRequest) ([]contract.UserResponse, []contract.BulkUserErrorResult, error) { created := []contract.UserResponse{} failed := []contract.BulkUserErrorResult{} // Pre-validate all users usersToCreate := []*entities.User{} emailMap := make(map[string]bool) for _, req := range userRequests { // Check for duplicate emails in the batch if emailMap[req.Email] { failed = append(failed, contract.BulkUserErrorResult{ User: req, Error: "Duplicate email in batch", }) continue } emailMap[req.Email] = true // Check if email already exists in database existing, _ := p.userRepo.GetByEmail(ctx, req.Email) if existing != nil { failed = append(failed, contract.BulkUserErrorResult{ User: req, Error: "Email already exists", }) continue } // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { failed = append(failed, contract.BulkUserErrorResult{ User: req, Error: "Failed to hash password", }) continue } // Create user entity user := &entities.User{ ID: uuid.New(), Name: req.Name, Email: req.Email, PasswordHash: string(hashedPassword), IsActive: true, } usersToCreate = append(usersToCreate, user) } // Bulk create valid users if len(usersToCreate) > 0 { // Use CreateInBatches for large datasets err := p.userRepo.CreateInBatches(ctx, usersToCreate, 50) if err != nil { // If bulk creation fails, try individual creation for i, user := range usersToCreate { err := p.userRepo.Create(ctx, user) if err != nil { failed = append(failed, contract.BulkUserErrorResult{ User: userRequests[i], Error: err.Error(), }) } else { // Create default profile for the user profile := &entities.UserProfile{ UserID: user.ID, FullName: user.Name, } _ = p.profileRepo.Create(ctx, profile) created = append(created, *transformer.EntityToContract(user)) } } } else { // Create profiles for all successfully created users for _, user := range usersToCreate { profile := &entities.UserProfile{ UserID: user.ID, FullName: user.Name, } _ = p.profileRepo.Create(ctx, profile) created = append(created, *transformer.EntityToContract(user)) } } } return created, failed, nil }