Files
tm_back/internal/customer/repository.go
T
Mazyar 0da0c8b8c9 Implement phone number validation for customer and user registration and updates
Added functionality to check for existing customers and users by phone number during registration and updates. Updated the handler, service, and repository layers to include phone number checks, ensuring unique phone numbers across customers and users.
2026-06-06 12:03:04 +03:30

496 lines
13 KiB
Go

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)
GetByPhone(ctx context.Context, phone 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"),
*orm.CreateUniqueIndex("phone_idx", bson.D{{Key: "phone", Value: 1}}).
WithPartialFilterExpression(bson.M{"phone": bson.M{"$type": "string", "$ne": ""}}),
}
// 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
}
// GetByPhone retrieves a customer by phone number
func (r *customerRepository) GetByPhone(ctx context.Context, phone string) (*Customer, error) {
filter := bson.M{"phone": phone}
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 phone", map[string]interface{}{
"error": err.Error(),
"phone": phone,
})
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
}