package repository import ( "context" "errors" "time" "github.com/google/uuid" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "tm/internal/domain" "tm/pkg/logger" ) // CustomerRepositoryImpl implements the CustomerRepository interface type CustomerRepositoryImpl struct { collection *mongo.Collection logger logger.Logger } // NewCustomerRepository creates a new customer repository func NewCustomerRepository(db *mongo.Database, logger logger.Logger) domain.CustomerRepository { collection := db.Collection("customers") // Create indexes ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Email index (unique) emailIndex := mongo.IndexModel{ Keys: bson.D{{"email", 1}}, Options: options.Index().SetUnique(true), } // Company ID index companyIndex := mongo.IndexModel{ Keys: bson.D{{"company_id", 1}}, } // Active status index activeIndex := mongo.IndexModel{ Keys: bson.D{{"is_active", 1}}, } // Created at index createdAtIndex := mongo.IndexModel{ Keys: bson.D{{"created_at", -1}}, } _, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{ emailIndex, companyIndex, activeIndex, createdAtIndex, }) if err != nil { logger.Warn("Failed to create customer indexes", map[string]interface{}{ "error": err.Error(), }) } return &CustomerRepositoryImpl{ collection: collection, logger: logger, } } // Create creates a new customer func (r *CustomerRepositoryImpl) Create(ctx context.Context, customer *domain.Customer) error { // Set created/updated timestamps now := time.Now() 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 *CustomerRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*domain.Customer, error) { var customer domain.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 *CustomerRepositoryImpl) GetByEmail(ctx context.Context, email string) (*domain.Customer, error) { var customer domain.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 } // Update updates a customer func (r *CustomerRepositoryImpl) Update(ctx context.Context, customer *domain.Customer) error { // Set updated timestamp customer.UpdatedAt = time.Now() 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 *CustomerRepositoryImpl) 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(), }, } 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 *CustomerRepositoryImpl) List(ctx context.Context, limit, offset int) ([]*domain.Customer, error) { var customers []*domain.Customer // Build options opts := options.Find() opts.SetLimit(int64(limit)) opts.SetSkip(int64(offset)) opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc // Only active customers by default filter := bson.M{"is_active": true} 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 } // GetByCompanyID retrieves customers by company ID with pagination func (r *CustomerRepositoryImpl) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*domain.Customer, error) { var customers []*domain.Customer // Build options opts := options.Find() opts.SetLimit(int64(limit)) opts.SetSkip(int64(offset)) opts.SetSort(bson.D{{"created_at", -1}}) // Sort by created_at desc // Filter by company ID and active status filter := bson.M{ "company_id": companyID, "is_active": true, } 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 } // UpdatePreferences updates customer preferences func (r *CustomerRepositoryImpl) UpdatePreferences(ctx context.Context, id uuid.UUID, preferences *domain.CustomerPreferences) error { filter := bson.M{"_id": id} update := bson.M{ "$set": bson.M{ "preferences": preferences, "updated_at": time.Now(), }, } result, err := r.collection.UpdateOne(ctx, filter, update) if err != nil { r.logger.Error("Failed to update customer preferences", 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 preferences updated successfully", map[string]interface{}{ "customer_id": id.String(), }) return nil } // AddDeviceToken adds a device token for push notifications func (r *CustomerRepositoryImpl) AddDeviceToken(ctx context.Context, id uuid.UUID, token string) error { filter := bson.M{"_id": id} update := bson.M{ "$addToSet": bson.M{"device_tokens": token}, // Use $addToSet to avoid duplicates "$set": bson.M{"updated_at": time.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": 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": token, }) return nil } // RemoveDeviceToken removes a device token func (r *CustomerRepositoryImpl) RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error { filter := bson.M{"_id": id} update := bson.M{ "$pull": bson.M{"device_tokens": token}, // Use $pull to remove the token "$set": bson.M{"updated_at": time.Now()}, } 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 }