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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user