package customer 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 customer (mobile user) data access type Repository interface { Create(ctx context.Context, customer *Customer) error GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) GetByEmail(ctx context.Context, email string) (*Customer, error) GetByUsername(ctx context.Context, username string) (*Customer, error) GetByMobile(ctx context.Context, mobile string) (*Customer, error) Update(ctx context.Context, customer *Customer) error Delete(ctx context.Context, id uuid.UUID) error List(ctx context.Context, limit, offset int) ([]*Customer, error) Search(ctx context.Context, search string, status *string, gender *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) AddDeviceToken(ctx context.Context, id uuid.UUID, deviceToken DeviceToken) error RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error UpdateLastLogin(ctx context.Context, id uuid.UUID) error GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error) } // customerRepository implements the Repository interface type customerRepository struct { collection *mongo.Collection logger logger.Logger } // NewCustomerRepository creates a new customer repository func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository { collection := mongoManager.GetCollection("customers") // 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), } // Mobile index (unique) mobileIndex := mongo.IndexModel{ Keys: bson.D{{Key: "mobile", Value: 1}}, Options: options.Index().SetUnique(true), } // Company ID index companyIndex := mongo.IndexModel{ Keys: bson.D{{Key: "company_id", Value: 1}}, } // Status index statusIndex := mongo.IndexModel{ Keys: bson.D{{Key: "status", Value: 1}}, } // Gender index genderIndex := mongo.IndexModel{ Keys: bson.D{{Key: "gender", 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: "mobile", Value: "text"}, {Key: "username", Value: "text"}, }, } _, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{ emailIndex, usernameIndex, mobileIndex, companyIndex, statusIndex, genderIndex, createdAtIndex, textIndex, }) if err != nil { logger.Warn("Failed to create customer indexes", map[string]interface{}{ "error": err.Error(), }) } return &customerRepository{ collection: collection, logger: logger, } } // Create creates a new customer func (r *customerRepository) Create(ctx context.Context, customer *Customer) error { // Set created/updated timestamps using Unix timestamps now := time.Now().Unix() customer.CreatedAt = now customer.UpdatedAt = now // Insert customer _, err := r.collection.InsertOne(ctx, customer) if err != nil { if mongo.IsDuplicateKeyError(err) { return errors.New("customer already exists") } r.logger.Error("Failed to create customer", map[string]interface{}{ "error": err.Error(), "email": customer.Email, "customer_id": customer.ID.String(), }) return err } r.logger.Info("Customer created successfully", map[string]interface{}{ "customer_id": customer.ID.String(), "email": customer.Email, }) return nil } // GetByID retrieves a customer by ID func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) { var customer Customer filter := bson.M{"_id": id} err := r.collection.FindOne(ctx, filter).Decode(&customer) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("customer not found") } r.logger.Error("Failed to get customer by ID", map[string]interface{}{ "error": err.Error(), "customer_id": id.String(), }) return nil, err } return &customer, nil } // GetByEmail retrieves a customer by email func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Customer, error) { var customer Customer filter := bson.M{"email": email} err := r.collection.FindOne(ctx, filter).Decode(&customer) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("customer not found") } r.logger.Error("Failed to get customer by email", map[string]interface{}{ "error": err.Error(), "email": email, }) return nil, err } return &customer, nil } // GetByUsername retrieves a customer by username func (r *customerRepository) GetByUsername(ctx context.Context, username string) (*Customer, error) { var customer Customer filter := bson.M{"username": username} err := r.collection.FindOne(ctx, filter).Decode(&customer) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("customer not found") } r.logger.Error("Failed to get customer by username", map[string]interface{}{ "error": err.Error(), "username": username, }) return nil, err } return &customer, nil } // GetByMobile retrieves a customer by mobile func (r *customerRepository) GetByMobile(ctx context.Context, mobile string) (*Customer, error) { var customer Customer filter := bson.M{"mobile": mobile} err := r.collection.FindOne(ctx, filter).Decode(&customer) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("customer not found") } r.logger.Error("Failed to get customer by mobile", map[string]interface{}{ "error": err.Error(), "mobile": mobile, }) return nil, err } return &customer, nil } // Update updates a customer func (r *customerRepository) Update(ctx context.Context, customer *Customer) error { // Set updated timestamp using Unix timestamp customer.UpdatedAt = time.Now().Unix() filter := bson.M{"_id": customer.ID} update := bson.M{"$set": customer} result, err := r.collection.UpdateOne(ctx, filter, update) if err != nil { r.logger.Error("Failed to update customer", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID.String(), }) return err } if result.MatchedCount == 0 { return errors.New("customer not found") } r.logger.Info("Customer updated successfully", map[string]interface{}{ "customer_id": customer.ID.String(), "email": customer.Email, }) return nil } // Delete deletes a customer (soft delete by setting is_active to false) func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error { filter := bson.M{"_id": id} update := bson.M{ "$set": bson.M{ "is_active": false, "updated_at": time.Now().Unix(), }, } result, err := r.collection.UpdateOne(ctx, filter, update) if err != nil { r.logger.Error("Failed to delete customer", map[string]interface{}{ "error": err.Error(), "customer_id": id.String(), }) return err } if result.MatchedCount == 0 { return errors.New("customer not found") } r.logger.Info("Customer deleted successfully", map[string]interface{}{ "customer_id": id.String(), }) return nil } // List retrieves customers with pagination func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) { var customers []*Customer // 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 customers by default filter := bson.M{"status": "active"} cursor, err := r.collection.Find(ctx, filter, opts) if err != nil { r.logger.Error("Failed to list customers", map[string]interface{}{ "error": err.Error(), "limit": limit, "offset": offset, }) return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &customers); err != nil { r.logger.Error("Failed to decode customers", map[string]interface{}{ "error": err.Error(), }) return nil, err } return customers, nil } // Search retrieves customers with search and filters func (r *customerRepository) Search(ctx context.Context, search string, status *string, gender *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) { var customers []*Customer // Build filter filter := bson.M{} if search != "" { filter["$text"] = bson.M{"$search": search} } if status != nil { filter["status"] = *status } if gender != nil { filter["gender"] = *gender } 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 customers", map[string]interface{}{ "error": err.Error(), "search": search, }) return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &customers); err != nil { r.logger.Error("Failed to decode customers from search", map[string]interface{}{ "error": err.Error(), }) return nil, err } return customers, nil } // GetByCompanyID retrieves customers by company ID with pagination func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) { var customers []*Customer // 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": "active", } cursor, err := r.collection.Find(ctx, filter, opts) if err != nil { r.logger.Error("Failed to get customers 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, &customers); err != nil { r.logger.Error("Failed to decode customers by company", map[string]interface{}{ "error": err.Error(), "company_id": companyID.String(), }) return nil, err } return customers, nil } // AddDeviceToken adds a device token for push notifications func (r *customerRepository) AddDeviceToken(ctx context.Context, id uuid.UUID, deviceToken DeviceToken) error { // Set timestamps now := time.Now().Unix() deviceToken.CreatedAt = now deviceToken.UpdatedAt = now filter := bson.M{"_id": id} update := bson.M{ "$push": bson.M{"device_tokens": deviceToken}, // Use $push to add new token "$set": bson.M{"updated_at": now}, } result, err := r.collection.UpdateOne(ctx, filter, update) if err != nil { r.logger.Error("Failed to add device token", map[string]interface{}{ "error": err.Error(), "customer_id": id.String(), "token": deviceToken.Token, }) return err } if result.MatchedCount == 0 { return errors.New("customer not found") } r.logger.Info("Device token added successfully", map[string]interface{}{ "customer_id": id.String(), "token": deviceToken.Token, "device_type": deviceToken.DeviceType, }) return nil } // RemoveDeviceToken removes a device token func (r *customerRepository) RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error { filter := bson.M{"_id": id} update := bson.M{ "$pull": bson.M{"device_tokens": bson.M{"token": token}}, // Use $pull to remove the token "$set": bson.M{"updated_at": time.Now().Unix()}, } result, err := r.collection.UpdateOne(ctx, filter, update) if err != nil { r.logger.Error("Failed to remove device token", map[string]interface{}{ "error": err.Error(), "customer_id": id.String(), "token": token, }) return err } if result.MatchedCount == 0 { return errors.New("customer not found") } r.logger.Info("Device token removed successfully", map[string]interface{}{ "customer_id": id.String(), "token": token, }) return nil } // UpdateLastLogin updates the last login timestamp func (r *customerRepository) 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(), "customer_id": id.String(), }) return err } if result.MatchedCount == 0 { return errors.New("customer not found") } r.logger.Info("Last login updated successfully", map[string]interface{}{ "customer_id": id.String(), }) return nil } // GetAllDeviceTokens retrieves all device tokens for push notifications func (r *customerRepository) GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error) { var customers []*Customer // Get all active customers filter := bson.M{"status": "active"} opts := options.Find().SetProjection(bson.M{"device_tokens": 1}) cursor, err := r.collection.Find(ctx, filter, opts) if err != nil { r.logger.Error("Failed to get all device tokens", map[string]interface{}{ "error": err.Error(), }) return nil, err } defer cursor.Close(ctx) if err = cursor.All(ctx, &customers); err != nil { r.logger.Error("Failed to decode customers for device tokens", map[string]interface{}{ "error": err.Error(), }) return nil, err } // Collect all device tokens var allTokens []DeviceToken for _, customer := range customers { allTokens = append(allTokens, customer.DeviceTokens...) } r.logger.Info("Retrieved all device tokens", map[string]interface{}{ "total_tokens": len(allTokens), }) return allTokens, nil }