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 }