0da0c8b8c9
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.
496 lines
13 KiB
Go
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
|
|
}
|