package user import ( "context" "errors" "time" "tm/pkg/logger" mongopkg "tm/pkg/mongo" "github.com/google/uuid" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) // Repository defines methods for user data access type Repository interface { Create(ctx context.Context, user *User) error GetByID(ctx context.Context, id uuid.UUID) (*User, error) GetByEmail(ctx context.Context, email string) (*User, error) GetByUsername(ctx context.Context, username string) (*User, error) Update(ctx context.Context, user *User) error Delete(ctx context.Context, id uuid.UUID) error List(ctx context.Context, limit, offset int) ([]*User, error) Search(ctx context.Context, search string, status *string, role *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*User, error) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, error) GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) UpdateLastLogin(ctx context.Context, id uuid.UUID) error CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) } // userRepository implements the Repository interface type userRepository struct { collection *mongo.Collection logger logger.Logger } // NewUserRepository creates a new user repository func NewUserRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository { collection := mongoManager.GetCollection("users") // Create indexes ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Email index (unique) emailIndex := mongo.IndexModel{ Keys: bson.D{{Key: "email", Value: 1}}, Options: options.Index().SetUnique(true), } // Username index (unique) usernameIndex := mongo.IndexModel{ Keys: bson.D{{Key: "username", Value: 1}}, Options: options.Index().SetUnique(true), } // Company ID index companyIndex := mongo.IndexModel{ Keys: bson.D{{Key: "company_id", Value: 1}}, } // Role index roleIndex := mongo.IndexModel{ Keys: bson.D{{Key: "role", Value: 1}}, } // Status index statusIndex := mongo.IndexModel{ Keys: bson.D{{Key: "status", Value: 1}}, } // Created at index createdAtIndex := mongo.IndexModel{ Keys: bson.D{{Key: "created_at", Value: -1}}, } // Full text search index textIndex := mongo.IndexModel{ Keys: bson.D{ {Key: "full_name", Value: "text"}, {Key: "email", Value: "text"}, {Key: "username", Value: "text"}, {Key: "department", Value: "text"}, {Key: "position", Value: "text"}, }, } _, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{ emailIndex, usernameIndex, companyIndex, roleIndex, statusIndex, createdAtIndex, textIndex, }) if err != nil { logger.Warn("Failed to create user indexes", map[string]interface{}{ "error": err.Error(), }) } return &userRepository{ collection: collection, logger: logger, } } // Create creates a new user func (r *userRepository) Create(ctx context.Context, user *User) error { // Set created/updated timestamps using Unix timestamps now := time.Now().Unix() user.CreatedAt = now user.UpdatedAt = now // Insert user _, err := r.collection.InsertOne(ctx, user) if err != nil { if mongo.IsDuplicateKeyError(err) { return errors.New("user already exists") } r.logger.Error("Failed to create user", map[string]interface{}{ "error": err.Error(), "email": user.Email, "user_id": user.ID.String(), }) return err } r.logger.Info("User created successfully", map[string]interface{}{ "user_id": user.ID.String(), "email": user.Email, "role": user.Role, }) return nil } // GetByID retrieves a user by ID func (r *userRepository) GetByID(ctx context.Context, id uuid.UUID) (*User, error) { var user User filter := bson.M{"_id": id} err := r.collection.FindOne(ctx, filter).Decode(&user) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("user not found") } r.logger.Error("Failed to get user by ID", map[string]interface{}{ "error": err.Error(), "user_id": id.String(), }) return nil, err } return &user, nil } // GetByEmail retrieves a user by email func (r *userRepository) GetByEmail(ctx context.Context, email string) (*User, error) { var user User filter := bson.M{"email": email} err := r.collection.FindOne(ctx, filter).Decode(&user) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("user not found") } r.logger.Error("Failed to get user by email", map[string]interface{}{ "error": err.Error(), "email": email, }) return nil, err } return &user, nil } // GetByUsername retrieves a user by username func (r *userRepository) GetByUsername(ctx context.Context, username string) (*User, error) { var user User filter := bson.M{"username": username} err := r.collection.FindOne(ctx, filter).Decode(&user) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("user not found") } r.logger.Error("Failed to get user by username", map[string]interface{}{ "error": err.Error(), "username": username, }) return nil, err } return &user, nil } // Update updates a user func (r *userRepository) Update(ctx context.Context, user *User) error { // Set updated timestamp using Unix timestamp user.UpdatedAt = time.Now().Unix() filter := bson.M{"_id": user.ID} update := bson.M{"$set": user} result, err := r.collection.UpdateOne(ctx, filter, update) if err != nil { r.logger.Error("Failed to update user", map[string]interface{}{ "error": err.Error(), "user_id": user.ID.String(), }) return err } if result.MatchedCount == 0 { return errors.New("user not found") } r.logger.Info("User updated successfully", map[string]interface{}{ "user_id": user.ID.String(), "email": user.Email, }) return nil } // Delete deletes a user (soft delete by setting status to inactive) func (r *userRepository) Delete(ctx context.Context, id uuid.UUID) error { filter := bson.M{"_id": id} update := bson.M{ "$set": bson.M{ "status": UserStatusInactive, "updated_at": time.Now().Unix(), }, } result, err := r.collection.UpdateOne(ctx, filter, update) if err != nil { r.logger.Error("Failed to delete user", map[string]interface{}{ "error": err.Error(), "user_id": id.String(), }) return err } if result.MatchedCount == 0 { return errors.New("user not found") } r.logger.Info("User deleted successfully", map[string]interface{}{ "user_id": id.String(), }) return nil } // List retrieves users with pagination func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*User, error) { var users []*User // Build options opts := options.Find() opts.SetLimit(int64(limit)) opts.SetSkip(int64(offset)) opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc // Only active users by default filter := bson.M{"status": bson.M{"$ne": UserStatusInactive}} cursor, err := r.collection.Find(ctx, filter, opts) if err != nil { r.logger.Error("Failed to list users", map[string]interface{}{ "error": err.Error(), "limit": limit, "offset": offset, }) return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &users); err != nil { r.logger.Error("Failed to decode users", map[string]interface{}{ "error": err.Error(), }) return nil, err } return users, nil } // Search retrieves users with search and filters func (r *userRepository) Search(ctx context.Context, search string, status *string, role *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*User, error) { var users []*User // Build filter filter := bson.M{} if search != "" { filter["$text"] = bson.M{"$search": search} } if status != nil { filter["status"] = *status } if role != nil { filter["role"] = *role } if companyID != nil { filter["company_id"] = *companyID } // Build options opts := options.Find() opts.SetLimit(int64(limit)) opts.SetSkip(int64(offset)) // Set sort if sortBy != "" { sortValue := 1 if sortOrder == "desc" { sortValue = -1 } opts.SetSort(bson.D{{Key: sortBy, Value: sortValue}}) } else { opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Default sort } cursor, err := r.collection.Find(ctx, filter, opts) if err != nil { r.logger.Error("Failed to search users", map[string]interface{}{ "error": err.Error(), "search": search, }) return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &users); err != nil { r.logger.Error("Failed to decode users from search", map[string]interface{}{ "error": err.Error(), }) return nil, err } return users, nil } // GetByCompanyID retrieves users by company ID with pagination func (r *userRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, error) { var users []*User // Build options opts := options.Find() opts.SetLimit(int64(limit)) opts.SetSkip(int64(offset)) opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc // Filter by company ID and active status filter := bson.M{ "company_id": companyID, "status": bson.M{"$ne": UserStatusInactive}, } cursor, err := r.collection.Find(ctx, filter, opts) if err != nil { r.logger.Error("Failed to get users by company ID", map[string]interface{}{ "error": err.Error(), "company_id": companyID.String(), "limit": limit, "offset": offset, }) return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &users); err != nil { r.logger.Error("Failed to decode users by company", map[string]interface{}{ "error": err.Error(), "company_id": companyID.String(), }) return nil, err } return users, nil } // GetByRole retrieves users by role with pagination func (r *userRepository) GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) { var users []*User // Build options opts := options.Find() opts.SetLimit(int64(limit)) opts.SetSkip(int64(offset)) opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc // Filter by role and active status filter := bson.M{ "role": role, "status": bson.M{"$ne": UserStatusInactive}, } cursor, err := r.collection.Find(ctx, filter, opts) if err != nil { r.logger.Error("Failed to get users by role", map[string]interface{}{ "error": err.Error(), "role": role, "limit": limit, "offset": offset, }) return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &users); err != nil { r.logger.Error("Failed to decode users by role", map[string]interface{}{ "error": err.Error(), "role": role, }) return nil, err } return users, nil } // UpdateLastLogin updates the last login timestamp func (r *userRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID) error { filter := bson.M{"_id": id} update := bson.M{ "$set": bson.M{ "last_login_at": time.Now().Unix(), "updated_at": time.Now().Unix(), }, } result, err := r.collection.UpdateOne(ctx, filter, update) if err != nil { r.logger.Error("Failed to update last login", map[string]interface{}{ "error": err.Error(), "user_id": id.String(), }) return err } if result.MatchedCount == 0 { return errors.New("user not found") } r.logger.Info("Last login updated successfully", map[string]interface{}{ "user_id": id.String(), }) return nil } // CountByCompanyID counts users by company ID func (r *userRepository) CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) { filter := bson.M{ "company_id": companyID, "status": bson.M{"$ne": UserStatusInactive}, } count, err := r.collection.CountDocuments(ctx, filter) if err != nil { r.logger.Error("Failed to count users by company ID", map[string]interface{}{ "error": err.Error(), "company_id": companyID.String(), }) return 0, err } return count, nil }