Files
tm_back/internal/customer/repository.go
T
n.nakhostin 3e4831c2e7 Enhance API documentation and restructure routes for improved clarity
- Updated README.md to include comprehensive Swagger documentation details, highlighting new features and endpoint categories.
- Introduced a new router structure to streamline route registration for admin and public endpoints, enhancing maintainability.
- Updated health check endpoint documentation to reflect comprehensive server status information.
- Enhanced customer and company management routes with improved descriptions and examples in Swagger.
- Refactored customer and user handlers to remove obsolete route registrations, aligning with the new router structure.
- Improved response handling in customer and user services to utilize string IDs consistently.
- Updated validation rules and forms for customer management to support multiple company assignments.
- Enhanced logging practices across services to ensure better traceability and error handling.
2025-08-11 18:24:34 +03:30

590 lines
17 KiB
Go

package customer
import (
"context"
"errors"
"time"
"tm/pkg/logger"
mongopkg "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/bson"
)
// Repository defines the interface for customer data operations
type Repository interface {
Create(ctx context.Context, customer *Customer) 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)
GetByCompanyName(ctx context.Context, companyName string) (*Customer, error)
GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error)
GetByTaxID(ctx context.Context, taxID string) (*Customer, error)
GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, error)
GetByCompanyIDs(ctx context.Context, companyIDs []string, limit, offset int) ([]*Customer, error)
Update(ctx context.Context, customer *Customer) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, limit, offset int) ([]*Customer, error)
Search(ctx context.Context, search string, customerType *string, status *string, companyID *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error)
CountByCompanyID(ctx context.Context, companyID string) (int64, error)
CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error)
CountByType(ctx context.Context, customerType CustomerType) (int64, error)
CountByStatus(ctx context.Context, status CustomerStatus) (int64, error)
UpdateStatus(ctx context.Context, id string, status CustomerStatus) error
UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error
}
// customerRepository implements the Repository interface using the MongoDB ORM
type customerRepository struct {
ormRepo mongopkg.Repository[Customer]
logger logger.Logger
}
// NewCustomerRepository creates a new customer repository
func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
// Create indexes using the ORM's index management
indexes := []mongopkg.Index{
*mongopkg.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
*mongopkg.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}),
*mongopkg.NewIndex("company_name_idx", bson.D{{Key: "company_name", Value: 1}}),
*mongopkg.NewIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
*mongopkg.NewIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}),
*mongopkg.NewIndex("company_ids_idx", bson.D{{Key: "company_ids", Value: 1}}), // Index for CompanyIDs array
*mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}),
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
*mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}),
*mongopkg.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}),
*mongopkg.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}),
*mongopkg.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}),
*mongopkg.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}),
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
*mongopkg.CreateTextIndex("search_idx", "first_name", "last_name", "full_name", "company_name", "email", "username"),
}
// Create indexes
err := mongoManager.CreateIndexes("customers", indexes)
if err != nil {
logger.Warn("Failed to create customer indexes", map[string]interface{}{
"error": err.Error(),
})
}
// Create ORM repository
ormRepo := mongopkg.NewRepository[Customer](mongoManager.GetCollection("customers"), logger)
return &customerRepository{
ormRepo: ormRepo,
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.SetCreatedAt(now)
customer.SetUpdatedAt(now)
// Use ORM to create customer
err := r.ormRepo.Create(ctx, customer)
if err != nil {
r.logger.Error("Failed to create customer", map[string]interface{}{
"error": err.Error(),
"email": customer.Email,
"username": customer.Username,
})
return err
}
r.logger.Info("Customer created successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
"type": customer.Type,
})
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, mongopkg.ErrDocumentNotFound) {
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,
})
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, mongopkg.ErrDocumentNotFound) {
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) {
filter := bson.M{"username": username}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
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
}
// GetByCompanyName retrieves a customer by company name
func (r *customerRepository) GetByCompanyName(ctx context.Context, companyName string) (*Customer, error) {
filter := bson.M{"company_name": companyName}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by company name", map[string]interface{}{
"error": err.Error(),
"company_name": companyName,
})
return nil, err
}
return customer, nil
}
// GetByRegistrationNumber retrieves a customer by registration number
func (r *customerRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) {
filter := bson.M{"registration_number": registrationNumber}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by registration number", map[string]interface{}{
"error": err.Error(),
"registration_number": registrationNumber,
})
return nil, err
}
return customer, nil
}
// GetByTaxID retrieves a customer by tax ID
func (r *customerRepository) GetByTaxID(ctx context.Context, taxID string) (*Customer, error) {
filter := bson.M{"tax_id": taxID}
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
}
r.logger.Error("Failed to get customer by tax ID", map[string]interface{}{
"error": err.Error(),
"tax_id": taxID,
})
return nil, err
}
return customer, nil
}
// GetByCompanyID retrieves customers by company ID with pagination
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, error) {
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
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, pagination)
if err != nil {
r.logger.Error("Failed to get customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
"limit": limit,
"offset": offset,
})
return nil, err
}
// Convert []Customer to []*Customer
customers := make([]*Customer, len(result.Items))
for i := range result.Items {
customers[i] = &result.Items[i]
}
return customers, nil
}
// GetByCompanyIDs retrieves customers by multiple company IDs with pagination
func (r *customerRepository) GetByCompanyIDs(ctx context.Context, companyIDs []string, limit, offset int) ([]*Customer, error) {
if len(companyIDs) == 0 {
return nil, errors.New("no company IDs provided")
}
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
Build()
// Filter by company IDs and active status
filter := bson.M{
"company_ids": bson.M{"$in": companyIDs},
"status": bson.M{"$ne": CustomerStatusInactive},
}
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to get customers by multiple company IDs", map[string]interface{}{
"error": err.Error(),
"company_ids": companyIDs,
"limit": limit,
"offset": offset,
})
return nil, err
}
// Convert []Customer to []*Customer
customers := make([]*Customer, len(result.Items))
for i := range result.Items {
customers[i] = &result.Items[i]
}
return customers, 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
}
// Delete deletes a customer (soft delete by setting status to inactive)
func (r *customerRepository) Delete(ctx context.Context, id string) error {
// Get customer first
customer, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update status to inactive
customer.Status = CustomerStatusInactive
customer.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, customer)
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
}
// List retrieves customers with pagination
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) {
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset).
SortDesc("created_at").
Build()
// Only active customers by default
filter := bson.M{"status": bson.M{"$ne": CustomerStatusInactive}}
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil {
r.logger.Error("Failed to list customers", map[string]interface{}{
"error": err.Error(),
"limit": limit,
"offset": offset,
})
return nil, err
}
// Convert []Customer to []*Customer
customers := make([]*Customer, len(result.Items))
for i := range result.Items {
customers[i] = &result.Items[i]
}
return customers, nil
}
// Search retrieves customers with search and filters
func (r *customerRepository) Search(ctx context.Context, search string, customerType *string, status *string, companyID *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) {
// Build filter
filter := bson.M{}
if search != "" {
filter["$text"] = bson.M{"$search": search}
}
if customerType != nil {
filter["type"] = *customerType
}
if status != nil {
filter["status"] = *status
}
if companyID != nil {
filter["company_id"] = *companyID
}
if industry != nil {
filter["industry"] = *industry
}
if isVerified != nil {
filter["is_verified"] = *isVerified
}
if isCompliant != nil {
filter["is_compliant"] = *isCompliant
}
if language != nil {
filter["language"] = *language
}
if currency != nil {
filter["currency"] = *currency
}
// Build pagination
pagination := mongopkg.NewPaginationBuilder().
Limit(limit).
Skip(offset)
// Set sort
if sortBy != "" {
if sortOrder == "desc" {
pagination.SortDesc(sortBy)
} else {
pagination.SortAsc(sortBy)
}
} else {
pagination.SortDesc("created_at") // Default sort
}
// Use ORM to find customers
result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build())
if err != nil {
r.logger.Error("Failed to search customers", map[string]interface{}{
"error": err.Error(),
"search": search,
})
return nil, err
}
// Convert []Customer to []*Customer
customers := make([]*Customer, len(result.Items))
for i := range result.Items {
customers[i] = &result.Items[i]
}
return customers, nil
}
// CountByCompanyID counts customers by company ID
func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID string) (int64, error) {
filter := bson.M{
"company_id": companyID,
"status": bson.M{"$ne": CustomerStatusInactive},
}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count customers by company ID", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return 0, err
}
return count, nil
}
// CountByCompanyIDs counts customers by multiple company IDs
func (r *customerRepository) CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error) {
if len(companyIDs) == 0 {
return 0, errors.New("no company IDs provided")
}
filter := bson.M{
"company_ids": bson.M{"$in": companyIDs},
"status": bson.M{"$ne": CustomerStatusInactive},
}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count customers by multiple company IDs", map[string]interface{}{
"error": err.Error(),
"company_ids": companyIDs,
})
return 0, err
}
return count, nil
}
// CountByType counts customers by type
func (r *customerRepository) CountByType(ctx context.Context, customerType CustomerType) (int64, error) {
filter := bson.M{
"type": customerType,
"status": bson.M{"$ne": CustomerStatusInactive},
}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count customers by type", map[string]interface{}{
"error": err.Error(),
"customer_type": customerType,
})
return 0, err
}
return count, nil
}
// CountByStatus counts customers by status
func (r *customerRepository) CountByStatus(ctx context.Context, status CustomerStatus) (int64, error) {
filter := bson.M{"status": status}
count, err := r.ormRepo.Count(ctx, filter)
if err != nil {
r.logger.Error("Failed to count customers by status", map[string]interface{}{
"error": err.Error(),
"status": status,
})
return 0, err
}
return count, 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
}
// UpdateVerification updates customer verification status
func (r *customerRepository) UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error {
// Get customer first
customer, err := r.GetByID(ctx, id)
if err != nil {
return err
}
// Update verification fields
customer.IsVerified = isVerified
customer.IsCompliant = isCompliant
customer.ComplianceNotes = complianceNotes
customer.SetUpdatedAt(time.Now().Unix())
// Update in database
err = r.ormRepo.Update(ctx, customer)
if err != nil {
r.logger.Error("Failed to update customer verification", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return err
}
r.logger.Info("Customer verification updated successfully", map[string]interface{}{
"customer_id": id,
"is_verified": isVerified,
"is_compliant": isCompliant,
})
return nil
}