Implement customer management domain with authorization and API enhancements

- Introduced a new customer management domain, including entities, services, handlers, and forms for customer operations.
- Added JWT-based user authorization settings in the configuration file for both user and customer management.
- Updated API endpoints to reflect the new structure, including changes to health check and user management routes.
- Enhanced Swagger documentation to include new customer-related endpoints and authorization details.
- Refactored the Makefile to include a target for generating API documentation.
- Removed obsolete documentation files to streamline the project structure.
This commit is contained in:
n.nakhostin
2025-08-11 12:55:08 +03:30
parent 29d859de37
commit 8c1e593686
23 changed files with 7485 additions and 2608 deletions
+525
View File
@@ -0,0 +1,525 @@
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/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// Repository defines the interface for customer data operations
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)
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 uuid.UUID, limit, offset int) ([]*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, customerType *string, status *string, companyID *uuid.UUID, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error)
CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error)
CountByType(ctx context.Context, customerType CustomerType) (int64, error)
CountByStatus(ctx context.Context, status CustomerStatus) (int64, error)
UpdateStatus(ctx context.Context, id uuid.UUID, status CustomerStatus) error
UpdateVerification(ctx context.Context, id uuid.UUID, isVerified, isCompliant bool, complianceNotes *string) 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
repo := &customerRepository{
collection: collection,
logger: logger,
}
repo.createIndexes()
return repo
}
// createIndexes creates necessary database indexes
func (r *customerRepository) createIndexes() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Create indexes
indexes := []mongo.IndexModel{
{
Keys: bson.D{
{Key: "email", Value: 1},
},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{
{Key: "username", Value: 1},
},
Options: options.Index().SetUnique(true),
},
{
Keys: bson.D{
{Key: "company_name", Value: 1},
},
Options: options.Index().SetSparse(true),
},
{
Keys: bson.D{
{Key: "registration_number", Value: 1},
},
Options: options.Index().SetSparse(true),
},
{
Keys: bson.D{
{Key: "tax_id", Value: 1},
},
Options: options.Index().SetSparse(true),
},
{
Keys: bson.D{
{Key: "company_id", Value: 1},
},
Options: options.Index().SetSparse(true),
},
{
Keys: bson.D{
{Key: "type", Value: 1},
},
},
{
Keys: bson.D{
{Key: "status", Value: 1},
},
},
{
Keys: bson.D{
{Key: "industry", Value: 1},
},
},
{
Keys: bson.D{
{Key: "created_at", Value: -1},
},
},
{
Keys: bson.D{
{Key: "updated_at", Value: -1},
},
},
}
_, err := r.collection.Indexes().CreateMany(ctx, indexes)
if err != nil {
r.logger.Error("Failed to create customer indexes", map[string]interface{}{
"error": err.Error(),
})
} else {
r.logger.Info("Customer indexes created successfully", map[string]interface{}{})
}
}
// Create creates a new customer
func (r *customerRepository) Create(ctx context.Context, customer *Customer) error {
// Set timestamps
now := time.Now().Unix()
customer.CreatedAt = now
customer.UpdatedAt = now
// Set defaults if not provided
if customer.Language == "" {
customer.Language = "en"
}
if customer.Currency == "" {
customer.Currency = "USD"
}
if customer.Timezone == "" {
customer.Timezone = "UTC"
}
_, err := r.collection.InsertOne(ctx, customer)
if err != nil {
if mongo.IsDuplicateKeyError(err) {
return errors.New("customer with this email already exists")
}
return err
}
r.logger.Info("Customer created successfully", map[string]interface{}{
"customer_id": customer.ID.String(),
"email": customer.Email,
"type": customer.Type,
})
return nil
}
// GetByID retrieves a customer by ID
func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
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
err := r.collection.FindOne(ctx, bson.M{"email": email}).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
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
err := r.collection.FindOne(ctx, bson.M{"username": username}).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
return nil, err
}
return &customer, nil
}
// GetByCompanyName retrieves a customer by company name
func (r *customerRepository) GetByCompanyName(ctx context.Context, companyName string) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"company_name": companyName}).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
return nil, err
}
return &customer, nil
}
// GetByRegistrationNumber retrieves a customer by registration number
func (r *customerRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"registration_number": registrationNumber}).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
return nil, err
}
return &customer, nil
}
// GetByTaxID retrieves a customer by tax ID
func (r *customerRepository) GetByTaxID(ctx context.Context, taxID string) (*Customer, error) {
var customer Customer
err := r.collection.FindOne(ctx, bson.M{"tax_id": taxID}).Decode(&customer)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, errors.New("customer not found")
}
return nil, err
}
return &customer, nil
}
// GetByCompanyID retrieves customers by company ID with pagination
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) {
filter := bson.M{"company_id": companyID}
opts := options.Find().
SetLimit(int64(limit)).
SetSkip(int64(offset)).
SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var customers []*Customer
if err = cursor.All(ctx, &customers); err != nil {
return nil, err
}
return customers, nil
}
// Update updates a customer
func (r *customerRepository) Update(ctx context.Context, customer *Customer) error {
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 {
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 status to inactive)
func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"status": CustomerStatusInactive,
"updated_at": time.Now().Unix(),
},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
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) {
opts := options.Find().
SetLimit(int64(limit)).
SetSkip(int64(offset)).
SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := r.collection.Find(ctx, bson.M{}, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var customers []*Customer
if err = cursor.All(ctx, &customers); err != nil {
return nil, err
}
return customers, nil
}
// Search searches customers with filters
func (r *customerRepository) Search(ctx context.Context, search string, customerType *string, status *string, companyID *uuid.UUID, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) {
filter := bson.M{}
// Add search filter
if search != "" {
filter["$or"] = []bson.M{
{"email": primitive.Regex{Pattern: search, Options: "i"}},
{"company_name": primitive.Regex{Pattern: search, Options: "i"}},
{"first_name": primitive.Regex{Pattern: search, Options: "i"}},
{"last_name": primitive.Regex{Pattern: search, Options: "i"}},
{"full_name": primitive.Regex{Pattern: search, Options: "i"}},
}
}
// Add type filter
if customerType != nil {
filter["type"] = *customerType
}
// Add status filter
if status != nil {
filter["status"] = *status
}
// Add company ID filter
if companyID != nil {
filter["company_id"] = *companyID
}
// Add industry filter
if industry != nil {
filter["industry"] = *industry
}
// Add verification filter
if isVerified != nil {
filter["is_verified"] = *isVerified
}
// Add compliance filter
if isCompliant != nil {
filter["is_compliant"] = *isCompliant
}
// Add language filter
if language != nil {
filter["language"] = *language
}
// Add currency filter
if currency != nil {
filter["currency"] = *currency
}
// Set sort options
sortDirection := 1
if sortOrder == "desc" {
sortDirection = -1
}
var sortField string
switch sortBy {
case "email":
sortField = "email"
case "company_name":
sortField = "company_name"
case "updated_at":
sortField = "updated_at"
case "status":
sortField = "status"
default:
sortField = "created_at"
}
opts := options.Find().
SetLimit(int64(limit)).
SetSkip(int64(offset)).
SetSort(bson.D{{Key: sortField, Value: sortDirection}})
cursor, err := r.collection.Find(ctx, filter, opts)
if err != nil {
return nil, err
}
defer cursor.Close(ctx)
var customers []*Customer
if err = cursor.All(ctx, &customers); err != nil {
return nil, err
}
return customers, nil
}
// CountByCompanyID counts customers by company ID
func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) {
filter := bson.M{"company_id": companyID}
count, err := r.collection.CountDocuments(ctx, filter)
if err != nil {
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}
count, err := r.collection.CountDocuments(ctx, filter)
if err != nil {
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.collection.CountDocuments(ctx, filter)
if err != nil {
return 0, err
}
return count, nil
}
// UpdateStatus updates customer status
func (r *customerRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status CustomerStatus) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"status": status,
"updated_at": time.Now().Unix(),
},
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
return nil
}
// UpdateVerification updates customer verification status
func (r *customerRepository) UpdateVerification(ctx context.Context, id uuid.UUID, isVerified, isCompliant bool, complianceNotes *string) error {
filter := bson.M{"_id": id}
update := bson.M{
"$set": bson.M{
"is_verified": isVerified,
"is_compliant": isCompliant,
"updated_at": time.Now().Unix(),
},
}
if complianceNotes != nil {
update["$set"].(bson.M)["compliance_notes"] = *complianceNotes
}
result, err := r.collection.UpdateOne(ctx, filter, update)
if err != nil {
return err
}
if result.MatchedCount == 0 {
return errors.New("customer not found")
}
return nil
}