Files
tm_back/internal/customer/repository.go
T
n.nakhostin 9119e2383e Add configuration file and update server settings
- Introduced a new `config.yaml` file for managing server, database, cache, queue, AI, scraping, logging, and rate limiting configurations.
- Updated `docker-compose.yml` to reflect the new server port (8081) and adjusted health check endpoints accordingly.
- Modified `Dockerfile` to expose the new server port.
- Updated `README.md` to reflect changes in server configuration and added documentation for the new configuration structure.
- Added test scripts for server and Swagger documentation testing.
- Refactored customer domain structure to align with new configuration settings and improve maintainability.
2025-08-02 12:37:04 +03:30

555 lines
14 KiB
Go

package customer
import (
"context"
"errors"
"time"
"tm/pkg/logger"
mongopkg "tm/pkg/mongo"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Repository defines methods for customer (mobile user) data access
type Repository interface {
Create(ctx context.Context, customer *Customer) error
GetByID(ctx context.Context, id uuid.UUID) (*Customer, error)
GetByEmail(ctx context.Context, email string) (*Customer, error)
GetByUsername(ctx context.Context, username string) (*Customer, error)
GetByMobile(ctx context.Context, mobile string) (*Customer, error)
Update(ctx context.Context, customer *Customer) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, limit, offset int) ([]*Customer, error)
Search(ctx context.Context, search string, status *string, gender *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*Customer, error)
GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error)
AddDeviceToken(ctx context.Context, id uuid.UUID, deviceToken DeviceToken) error
RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error
UpdateLastLogin(ctx context.Context, id uuid.UUID) error
GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error)
}
// customerRepository implements the Repository interface
type customerRepository struct {
collection *mongo.Collection
logger logger.Logger
}
// NewCustomerRepository creates a new customer repository
func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
collection := mongoManager.GetCollection("customers")
// Create indexes
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Email index (unique)
emailIndex := mongo.IndexModel{
Keys: bson.D{{Key: "email", Value: 1}},
Options: options.Index().SetUnique(true),
}
// Username index (unique)
usernameIndex := mongo.IndexModel{
Keys: bson.D{{Key: "username", Value: 1}},
Options: options.Index().SetUnique(true),
}
// Mobile index (unique)
mobileIndex := mongo.IndexModel{
Keys: bson.D{{Key: "mobile", Value: 1}},
Options: options.Index().SetUnique(true),
}
// Company ID index
companyIndex := mongo.IndexModel{
Keys: bson.D{{Key: "company_id", Value: 1}},
}
// Status index
statusIndex := mongo.IndexModel{
Keys: bson.D{{Key: "status", Value: 1}},
}
// Gender index
genderIndex := mongo.IndexModel{
Keys: bson.D{{Key: "gender", Value: 1}},
}
// Created at index
createdAtIndex := mongo.IndexModel{
Keys: bson.D{{Key: "created_at", Value: -1}},
}
// Full text search index
textIndex := mongo.IndexModel{
Keys: bson.D{
{Key: "full_name", Value: "text"},
{Key: "email", Value: "text"},
{Key: "mobile", Value: "text"},
{Key: "username", Value: "text"},
},
}
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
emailIndex,
usernameIndex,
mobileIndex,
companyIndex,
statusIndex,
genderIndex,
createdAtIndex,
textIndex,
})
if err != nil {
logger.Warn("Failed to create customer indexes", map[string]interface{}{
"error": err.Error(),
})
}
return &customerRepository{
collection: collection,
logger: logger,
}
}
// Create creates a new customer
func (r *customerRepository) Create(ctx context.Context, customer *Customer) error {
// Set created/updated timestamps using Unix timestamps
now := time.Now().Unix()
customer.CreatedAt = now
customer.UpdatedAt = now
// Insert customer
_, err := r.collection.InsertOne(ctx, customer)
if err != nil {
if mongo.IsDuplicateKeyError(err) {
return errors.New("customer already exists")
}
r.logger.Error("Failed to create customer", map[string]interface{}{
"error": err.Error(),
"email": customer.Email,
"customer_id": customer.ID.String(),
})
return err
}
r.logger.Info("Customer created successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
})
return nil
}
// GetByID retrieves a customer by ID
func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
var customer Customer
filter := bson.M{"_id": id}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by ID", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
})
return nil, err
}
return &customer, nil
}
// GetByEmail retrieves a customer by email
func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Customer, error) {
var customer Customer
filter := bson.M{"email": email}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
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) {
var customer Customer
filter := bson.M{"username": username}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by username", map[string]interface{}{
"error": err.Error(),
"username": username,
})
return nil, err
}
return &customer, nil
}
// GetByMobile retrieves a customer by mobile
func (r *customerRepository) GetByMobile(ctx context.Context, mobile string) (*Customer, error) {
var customer Customer
filter := bson.M{"mobile": mobile}
err := r.collection.FindOne(ctx, filter).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by mobile", map[string]interface{}{
"error": err.Error(),
"mobile": mobile,
})
return nil, err
}
return &customer, nil
}
// Update updates a customer
func (r *customerRepository) Update(ctx context.Context, customer *Customer) error {
// Set updated timestamp using Unix timestamp
customer.UpdatedAt = time.Now().Unix()
filter := bson.M{"_id": customer.ID}
update := bson.M{"$set": customer}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to update customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID.String(),
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Customer updated successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
})
return nil
}
// Delete deletes a customer (soft delete by setting is_active to false)
func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"is_active": false,
"updated_at": time.Now().Unix(),
},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to delete customer", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Customer deleted successfully", map[string]interface{}{
"customer_id": id.String(),
})
return nil
}
// List retrieves customers with pagination
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) {
var customers []*Customer
// Build options
opts := options.Find()
opts.SetLimit(int64(limit))
opts.SetSkip(int64(offset))
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
// Only active customers by default
filter := bson.M{"status": "active"}
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to list customers", map[string]interface{}{
"error": err.Error(),
"limit": limit,
"offset": offset,
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return customers, nil
}
// Search retrieves customers with search and filters
func (r *customerRepository) Search(ctx context.Context, search string, status *string, gender *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) {
var customers []*Customer
// Build filter
filter := bson.M{}
if search != "" {
filter["$text"] = bson.M{"$search": search}
}
if status != nil {
filter["status"] = *status
}
if gender != nil {
filter["gender"] = *gender
}
if companyID != nil {
filter["company_id"] = *companyID
}
// Build options
opts := options.Find()
opts.SetLimit(int64(limit))
opts.SetSkip(int64(offset))
// Set sort
if sortBy != "" {
sortValue := 1
if sortOrder == "desc" {
sortValue = -1
}
opts.SetSort(bson.D{{Key: sortBy, Value: sortValue}})
} else {
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Default sort
}
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
"search": search,
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers from search", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
return customers, nil
}
// GetByCompanyID retrieves customers by company ID with pagination
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) {
var customers []*Customer
// Build options
opts := options.Find()
opts.SetLimit(int64(limit))
opts.SetSkip(int64(offset))
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
// Filter by company ID and active status
filter := bson.M{
"company_id": companyID,
"status": "active",
}
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to get customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID.String(),
"limit": limit,
"offset": offset,
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers by company", map[string]interface{}{
"error": err.Error(),
"company_id": companyID.String(),
})
return nil, err
}
return customers, nil
}
// AddDeviceToken adds a device token for push notifications
func (r *customerRepository) AddDeviceToken(ctx context.Context, id uuid.UUID, deviceToken DeviceToken) error {
// Set timestamps
now := time.Now().Unix()
deviceToken.CreatedAt = now
deviceToken.UpdatedAt = now
filter := bson.M{"_id": id}
update := bson.M{
"$push": bson.M{"device_tokens": deviceToken}, // Use $push to add new token
"$set": bson.M{"updated_at": now},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to add device token", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"token": deviceToken.Token,
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Device token added successfully", map[string]interface{}{
"customer_id": id.String(),
"token": deviceToken.Token,
"device_type": deviceToken.DeviceType,
})
return nil
}
// RemoveDeviceToken removes a device token
func (r *customerRepository) RemoveDeviceToken(ctx context.Context, id uuid.UUID, token string) error {
filter := bson.M{"_id": id}
update := bson.M{
"$pull": bson.M{"device_tokens": bson.M{"token": token}}, // Use $pull to remove the token
"$set": bson.M{"updated_at": time.Now().Unix()},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to remove device token", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
"token": token,
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Device token removed successfully", map[string]interface{}{
"customer_id": id.String(),
"token": token,
})
return nil
}
// UpdateLastLogin updates the last login timestamp
func (r *customerRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"last_login_at": time.Now().Unix(),
"updated_at": time.Now().Unix(),
},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
r.logger.Error("Failed to update last login", map[string]interface{}{
"error": err.Error(),
"customer_id": id.String(),
})
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
r.logger.Info("Last login updated successfully", map[string]interface{}{
"customer_id": id.String(),
})
return nil
}
// GetAllDeviceTokens retrieves all device tokens for push notifications
func (r *customerRepository) GetAllDeviceTokens(ctx context.Context) ([]DeviceToken, error) {
var customers []*Customer
// Get all active customers
filter := bson.M{"status": "active"}
opts := options.Find().SetProjection(bson.M{"device_tokens": 1})
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
r.logger.Error("Failed to get all device tokens", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
defer cursor.Close(ctx)
if err = cursor.All(ctx, &customers); err != nil {
r.logger.Error("Failed to decode customers for device tokens", map[string]interface{}{
"error": err.Error(),
})
return nil, err
}
// Collect all device tokens
var allTokens []DeviceToken
for _, customer := range customers {
allTokens = append(allTokens, customer.DeviceTokens...)
}
r.logger.Info("Retrieved all device tokens", map[string]interface{}{
"total_tokens": len(allTokens),
})
return allTokens, nil
}