3aeb8630e3
continuous-integration/drone/push Build is passing
- Introduced new functionality in `form_companies.go` to handle company selection from various input formats, including legacy `company_ids` and company summary objects. - Implemented `UnmarshalJSON` methods for `CreateCustomerForm`, `UpdateCustomerForm`, and `AssignCompaniesForm` to support flexible company data parsing. - Added unit tests in `form_companies_test.go` to validate the correct unmarshalling of company IDs and summaries, ensuring robust handling of different input scenarios. - Enhanced the `GetByCompanies` method in the customer repository to directly use string company IDs, improving data retrieval efficiency. This update significantly improves the handling of company data in customer forms, ensuring accurate processing and validation of company selections, while expanding test coverage for these functionalities.
491 lines
13 KiB
Go
491 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) {
|
|
if len(companies) == 0 {
|
|
return []Customer{}, nil
|
|
}
|
|
|
|
customers, err := r.ormRepo.FindAll(ctx, bson.M{"companies": bson.M{"$in": companies}}, 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 assigned company ID and active status
|
|
filter := bson.M{
|
|
"companies": 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
|
|
}
|