Files
tm_back/internal/customer/repository.go
T

475 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)
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
}