package customer import ( "context" "errors" "fmt" "regexp" "strings" "time" "tm/pkg/logger" orm "tm/pkg/mongo" "tm/pkg/response" "go.mongodb.org/mongo-driver/v2/bson" mongopkg "go.mongodb.org/mongo-driver/v2/mongo" ) // Repository defines the interface for customer data operations type Repository interface { Login(ctx context.Context, id, deviceToken string) error Register(ctx context.Context, customer *Customer) error Update(ctx context.Context, customer *Customer) error UpdatePasswordReset(ctx context.Context, id, hashedPassword, updatedBy string) error UpdateStatus(ctx context.Context, id string, status CustomerStatus) error GetByID(ctx context.Context, id string) (*Customer, error) GetByEmail(ctx context.Context, email string) (*Customer, error) GetByUsername(ctx context.Context, username string) (*Customer, error) GetByIDs(ctx context.Context, ids []string) ([]Customer, error) GetByRole(ctx context.Context, role CustomerRole) ([]Customer, error) GetByCompanies(ctx context.Context, companies []string) ([]Customer, error) GetByCompanyID(ctx context.Context, companyID string, pagination *response.Pagination) ([]*Customer, int64, error) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*orm.PaginatedResult[Customer], error) Delete(ctx context.Context, id string) error } // customerRepository implements the Repository interface using the MongoDB ORM type customerRepository struct { ormRepo orm.Repository[Customer] collection *mongopkg.Collection logger logger.Logger } func collection() string { return "customers" } // NewCustomerRepository creates a new customer repository func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository { // Create indexes using the ORM's index management indexes := []orm.Index{ *orm.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}), *orm.CreateTextIndex("search_idx", "email", "username"), } // Create indexes err := mongoManager.CreateIndexes(collection(), indexes) if err != nil { logger.Warn("Failed to create customer indexes", map[string]interface{}{ "error": err.Error(), }) } // Create ORM repository collection := mongoManager.GetCollection(collection()) ormRepo := orm.NewRepository[Customer](collection, logger) return &customerRepository{ ormRepo: ormRepo, collection: collection, logger: logger, } } // Register creates a new customer func (r *customerRepository) Register(ctx context.Context, customer *Customer) error { // Set created/updated timestamps using Unix timestamps now := time.Now().Unix() customer.SetCreatedAt(now) // Use ORM to create customer err := r.ormRepo.Create(ctx, customer) if err != nil { r.logger.Error("Failed to register customer", map[string]interface{}{ "error": err.Error(), "email": customer.Email, "username": customer.Username, }) return err } r.logger.Info("Customer registered successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, "type": customer.Type, }) return nil } // Login updates the last login timestamp func (r *customerRepository) Login(ctx context.Context, id, deviceToken string) error { // Get customer first customer, err := r.GetByID(ctx, id) if err != nil { return err } // append device token to customer if not exists // if !slices.Contains(customer.DeviceToken, deviceToken) { customer.DeviceToken = []string{deviceToken} // } // Update last login timestamp customer.LastLoginAt = &[]int64{time.Now().Unix()}[0] customer.SetUpdatedAt(time.Now().Unix()) // Update in database err = r.ormRepo.Update(ctx, customer) if err != nil { r.logger.Error("Failed to update last login", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return err } r.logger.Info("Last login updated successfully", map[string]interface{}{ "customer_id": id, }) return nil } // GetByID retrieves a customer by ID func (r *customerRepository) GetByID(ctx context.Context, id string) (*Customer, error) { customer, err := r.ormRepo.FindByID(ctx, id) if err != nil { if errors.Is(err, orm.ErrDocumentNotFound) { return nil, ErrCustomerNotFound } r.logger.Error("Failed to get customer by ID", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return nil, err } return customer, nil } // GetByEmail retrieves a customer by email func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Customer, error) { filter := bson.M{"email": email} customer, err := r.ormRepo.FindOne(ctx, filter) if err != nil { if errors.Is(err, orm.ErrDocumentNotFound) { return nil, ErrCustomerNotFound } 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) { filter := bson.M{"username": username} customer, err := r.ormRepo.FindOne(ctx, filter) if err != nil { if errors.Is(err, orm.ErrDocumentNotFound) { return nil, ErrCustomerNotFound } r.logger.Error("Failed to get customer by username", map[string]interface{}{ "error": err.Error(), "username": username, }) return nil, err } return customer, nil } // GetByIDs retrieves customers by IDs func (r *customerRepository) GetByIDs(ctx context.Context, ids []string) ([]Customer, error) { objectIDs := make([]bson.ObjectID, 0) for _, id := range ids { objectID, err := bson.ObjectIDFromHex(id) if err != nil { return nil, err } objectIDs = append(objectIDs, objectID) } customers, err := r.ormRepo.FindAll(ctx, bson.M{"_id": bson.M{"$in": objectIDs}}, orm.NewPaginationBuilder().Limit(len(ids)).Build()) if err != nil { if errors.Is(err, orm.ErrDocumentNotFound) { return nil, errors.New("customers not found") } r.logger.Error("Failed to get customers by IDs", map[string]interface{}{ "error": err.Error(), "ids": ids, }) return nil, err } return customers.Items, nil } // GetByRole retrieves customers by role func (r *customerRepository) GetByRole(ctx context.Context, role CustomerRole) ([]Customer, error) { customers, err := r.ormRepo.FindAll(ctx, bson.M{"role": role}, orm.NewPaginationBuilder().Build()) if err != nil { if errors.Is(err, orm.ErrDocumentNotFound) { return nil, errors.New("customers not found") } r.logger.Error("Failed to get customers by role", map[string]interface{}{ "error": err.Error(), "role": role, }) return nil, err } return customers.Items, nil } // GetByCompanies retrieves customers by companies func (r *customerRepository) GetByCompanies(ctx context.Context, companies []string) ([]Customer, error) { companyIDs := make([]bson.ObjectID, 0) for _, company := range companies { id, err := bson.ObjectIDFromHex(company) if err != nil { return nil, err } companyIDs = append(companyIDs, id) } customers, err := r.ormRepo.FindAll(ctx, bson.M{"companies": bson.M{"$in": companyIDs}}, orm.NewPaginationBuilder().Build()) if err != nil { return nil, err } return customers.Items, nil } // GetByCompanyID retrieves customers by company ID with pagination func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID string, pagination *response.Pagination) ([]*Customer, int64, error) { // Build pagination paginationBuilder := orm.NewPaginationBuilder(). Limit(pagination.Limit). Skip(pagination.Offset). SortBy(pagination.SortBy, pagination.SortOrder). Build() // Filter by company ID and active status filter := bson.M{ "company_id": companyID, "status": bson.M{"$ne": CustomerStatusInactive}, } // Use ORM to find customers result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder) if err != nil { r.logger.Error("Failed to get customers by company ID", map[string]interface{}{ "error": err.Error(), "company_id": companyID, "limit": pagination.Limit, "offset": pagination.Offset, }) return nil, 0, err } // Convert []Customer to []*Customer customers := make([]*Customer, len(result.Items)) for i := range result.Items { customers[i] = &result.Items[i] } return customers, result.TotalCount, nil } // Update updates a customer func (r *customerRepository) Update(ctx context.Context, customer *Customer) error { // Set updated timestamp using Unix timestamp customer.SetUpdatedAt(time.Now().Unix()) // Use ORM to update customer err := r.ormRepo.Update(ctx, customer) if err != nil { r.logger.Error("Failed to update customer", map[string]interface{}{ "error": err.Error(), "customer_id": customer.ID, }) return err } r.logger.Info("Customer updated successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, }) return nil } // UpdatePasswordReset atomically persists a new password hash and audit fields. func (r *customerRepository) UpdatePasswordReset(ctx context.Context, id, hashedPassword, updatedBy string) error { now := time.Now().Unix() updateDoc := bson.M{ "$set": bson.M{ "password": hashedPassword, "updated_at": now, "updated_by": updatedBy, }, } objectID, err := bson.ObjectIDFromHex(id) if err != nil { return fmt.Errorf("invalid id format: %w", err) } result, err := r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, updateDoc) if err != nil { r.logger.Error("Failed to update password reset", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return err } if result.MatchedCount == 0 { return ErrCustomerNotFound } return nil } // Delete deletes a customer (hard delete) func (r *customerRepository) Delete(ctx context.Context, id string) error { // Get customer first _, err := r.GetByID(ctx, id) if err != nil { if errors.Is(err, orm.ErrDocumentNotFound) { return errors.New("customer not found") } r.logger.Error("Failed to delete customer", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return err } // Update in database err = r.ormRepo.Delete(ctx, id) if err != nil { r.logger.Error("Failed to delete customer", map[string]interface{}{ "error": err.Error(), "customer_id": id, }) return err } r.logger.Info("Customer deleted successfully", map[string]interface{}{ "customer_id": id, }) return nil } // Search retrieves customers with search and filters func (r *customerRepository) Search(ctx context.Context, form *SearchCustomersForm, pagination *response.Pagination) (*orm.PaginatedResult[Customer], error) { filter := bson.M{} if form.Search != nil { search := strings.TrimSpace(*form.Search) if search != "" { filter["$text"] = bson.M{"$search": search} } } if form.FullName != nil { fullName := strings.TrimSpace(*form.FullName) if fullName != "" { filter["full_name"] = bson.M{ "$regex": regexp.QuoteMeta(fullName), "$options": "i", } } } if form.Email != nil { email := strings.TrimSpace(*form.Email) if email != "" { filter["email"] = bson.M{ "$regex": regexp.QuoteMeta(email), "$options": "i", } } } if form.Type != nil { filter["type"] = *form.Type } if form.Status != nil { filter["status"] = *form.Status } if form.Role != nil { filter["role"] = *form.Role } if form.CompanyID != nil { companyID := strings.TrimSpace(*form.CompanyID) if companyID != "" { filter["companies"] = companyID } } mongoPagination, err := orm.BuildListPagination( pagination.Limit, pagination.Offset, pagination.Cursor, pagination.SortBy, pagination.SortOrder, filter, orm.ListPaginationOptions{}, ) if err != nil { return nil, err } result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination) if err != nil { r.logger.Error("Failed to search customers", map[string]interface{}{ "error": err.Error(), "search": form.Search, }) return nil, err } return result, nil } // UpdateStatus updates customer status func (r *customerRepository) UpdateStatus(ctx context.Context, id string, status CustomerStatus) error { // Get customer first customer, err := r.GetByID(ctx, id) if err != nil { return err } // Update status customer.Status = status customer.SetUpdatedAt(time.Now().Unix()) // Update in database err = r.ormRepo.Update(ctx, customer) if err != nil { r.logger.Error("Failed to update customer status", map[string]interface{}{ "error": err.Error(), "customer_id": id, "status": status, }) return err } r.logger.Info("Customer status updated successfully", map[string]interface{}{ "customer_id": id, "status": status, }) return nil }