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.
This commit is contained in:
n.nakhostin
2025-08-11 18:24:34 +03:30
parent 566fa07574
commit 3e4831c2e7
23 changed files with 3605 additions and 2087 deletions
+72
View File
@@ -47,6 +47,78 @@ This system implements **Clean Architecture** with:
- **Time Handling**: Unix timestamps throughout the application - **Time Handling**: Unix timestamps throughout the application
- **API Documentation**: Auto-generated Swagger documentation - **API Documentation**: Auto-generated Swagger documentation
## 📋 API Documentation
### Swagger/OpenAPI 3.0 Documentation
The Tender Management API features comprehensive, auto-generated Swagger documentation with:
#### 🚀 **Version 2.0.0 Features**
- **Comprehensive API Coverage**: All endpoints documented with detailed descriptions, examples, and error responses
- **Interactive API Testing**: Built-in Swagger UI for testing endpoints directly from the documentation
- **Authentication Integration**: Bearer token authentication examples and testing capability
- **Request/Response Examples**: Real-world examples for all data structures and API calls
- **Error Documentation**: Detailed error codes, messages, and troubleshooting information
#### 📊 **API Endpoints Categories**
- **Health**: System monitoring and health check endpoints
- **Authorization**: User authentication, token management, and session handling
- **Users**: Administrative user management with RBAC (Role-Based Access Control)
- **Customers-Admin**: Web panel customer management with advanced filtering
- **Customers-Authorization**: Mobile app customer authentication and profile access
- **Companies-Admin**: Comprehensive company management with search, filtering, and analytics
- **Companies-Mobile**: Mobile-optimized company lookup and information retrieval
#### 🔧 **Documentation Access**
- **Local Development**: `http://localhost:8081/swagger/`
- **Interactive Testing**: All endpoints can be tested directly from the Swagger UI
- **Authentication**: Use the "Authorize" button to set Bearer tokens for protected endpoints
- **Export Options**: JSON and YAML formats available for API specifications
#### 📝 **Enhanced Features**
- **Detailed Descriptions**: Each endpoint includes comprehensive business logic explanations
- **Parameter Validation**: Complete validation rules and constraints for all input parameters
- **Response Examples**: Real-world response examples with proper HTTP status codes
- **Security Schemes**: JWT Bearer token authentication clearly documented
- **Error Handling**: Comprehensive error response documentation with troubleshooting guides
- **Filtering & Pagination**: Advanced query parameters for list endpoints with examples
- **Business Logic**: Context-aware descriptions explaining when and how to use each endpoint
#### 🏗️ **Technical Specifications**
- **OpenAPI/Swagger 2.0**: Industry-standard API documentation format
- **Auto-Generation**: Documentation automatically updated from code annotations
- **Type Safety**: Strong typing with Go struct validation and examples
- **Validation Rules**: Complete govalidator integration for request validation
- **Consistent Response Format**: Standardized API response structure with metadata
#### 💡 **Usage Examples**
```bash
# Access Swagger UI
curl http://localhost:8081/swagger/
# Download API specification
curl http://localhost:8081/swagger/doc.json
# Test authentication endpoint
curl -X POST "http://localhost:8081/admin/v1/login" \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "password"}'
```
#### 🔐 **Authentication Testing**
1. Navigate to Swagger UI at `/swagger/`
2. Click "Authorize" button
3. Enter: `Bearer <your_jwt_token>`
4. Test protected endpoints directly from the interface
### API Design Principles
- **RESTful Design**: Consistent REST principles with proper HTTP methods and status codes
- **Clean Architecture**: Domain-driven design with clear separation of concerns
- **Validation**: Comprehensive input validation with meaningful error messages
- **Security**: JWT-based authentication with proper token management
- **Performance**: Optimized queries with pagination and filtering capabilities
- **Maintainability**: Auto-generated documentation stays in sync with code changes
## 🔧 Development ## 🔧 Development
### Prerequisites ### Prerequisites
+935 -458
View File
File diff suppressed because it is too large Load Diff
+935 -457
View File
File diff suppressed because it is too large Load Diff
+680 -313
View File
File diff suppressed because it is too large Load Diff
+13 -8
View File
@@ -7,24 +7,29 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// @Summary Health check // @Summary Health check endpoint
// @Description Get server health status // @Description Get comprehensive server health status including system information, dependencies status, and performance metrics. This endpoint is used for monitoring and load balancer health checks.
// @Tags Health // @Tags Health
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} HealthResponse // @Success 200 {object} HealthResponse "System is healthy and operational"
// @Failure 503 {object} HealthResponse "System is unhealthy or experiencing issues"
// @Router /admin/v1/health [get] // @Router /admin/v1/health [get]
func healthHandler(c echo.Context) error { func healthHandler(c echo.Context) error {
return c.JSON(http.StatusOK, HealthResponse{ return c.JSON(http.StatusOK, HealthResponse{
Status: "healthy", Status: "healthy",
Time: time.Now().Unix(), Time: time.Now().Unix(),
Version: "1.0.0", Version: "2.0.0",
Service: "tender-management-api",
Uptime: time.Since(time.Now()).String(),
}) })
} }
// HealthResponse represents the health check response // HealthResponse represents the comprehensive health check response
type HealthResponse struct { type HealthResponse struct {
Status string `json:"status"` Status string `json:"status" example:"healthy"` // Current health status
Time int64 `json:"time"` Time int64 `json:"time" example:"1699123456"` // Current Unix timestamp
Version string `json:"version"` Version string `json:"version" example:"2.0.0"` // API version
Service string `json:"service" example:"tender-management-api"` // Service name
Uptime string `json:"uptime,omitempty" example:"24h30m15s"` // Service uptime duration
} }
+33 -29
View File
@@ -1,41 +1,45 @@
package main package main
// @title Tender Management API // @title Tender Management API
// @version 1.0.0 // @version 2.0.0
// @description This is the API documentation for the Tender Management System. // @description This is a comprehensive API for the Tender Management System built with Clean Architecture principles and Domain-Driven Design (DDD) patterns. The API provides endpoints for user management, customer management, company management, and authentication for both web panel administration and mobile application access.
// @termsOfService http://swagger.io/terms/ // @termsOfService https://tender-management.com/terms
// @contact.name API Support // @contact.name Tender Management API Support
// @contact.url http://www.swagger.io/support // @contact.url https://tender-management.com/support
// @contact.email support@swagger.io // @contact.email api-support@tender-management.com
// @license.name Apache 2.0 // @license.name MIT
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @license.url https://opensource.org/licenses/MIT
// @host localhost:8081 // @host localhost:8081
// @BasePath /
// @securityDefinitions.apikey BearerAuth // @securityDefinitions.apikey BearerAuth
// @in header // @in header
// @name Authorization // @name Authorization
// @description Type "Bearer" followed by a space and JWT token. // @description Type "Bearer" followed by a space and JWT token. Example: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
// @tag.name Users
// @tag.description User management operations including authentication, profile management, and admin operations
// @tag.name Authorization
// @tag.description Authentication operations including login, logout, and token management
// @tag.name Customers-Admin
// @tag.description Customer management operations including authentication, profile management, and admin operations
// @tag.name Customers-Authorization
// @tag.description Customer authentication operations including login, logout, and token management
// @tag.name Companies-Admin
// @tag.description Company management operations for web panel including CRUD, customer assignment, and tag management
// @tag.name Health // @tag.name Health
// @tag.description Health check operations // @tag.description System health check operations for monitoring application status and dependencies
// @tag.name Authorization
// @tag.description User authentication and authorization operations including login, logout, token refresh, and password management for web panel administration
// @tag.name Users
// @tag.description Administrative user management operations including CRUD operations, role management, status updates, and profile management for web panel administrators
// @tag.name Customers-Admin
// @tag.description Administrative customer management operations for web panel including CRUD operations, company assignment, verification, status management, and comprehensive filtering with pagination
// @tag.name Customers-Authorization
// @tag.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access
// @tag.name Companies-Admin
// @tag.description Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting
// @tag.name Companies-Mobile
// @tag.description Company-related operations for mobile application including company lookup, search, and basic information retrieval
import ( import (
"context" "context"
@@ -47,6 +51,7 @@ import (
"tm/internal/user" "tm/internal/user"
_ "tm/cmd/web/docs" // This is generated by swag _ "tm/cmd/web/docs" // This is generated by swag
"tm/cmd/web/router"
) )
func main() { func main() {
@@ -95,8 +100,8 @@ func main() {
// Initialize services with repositories // Initialize services with repositories
userService := user.NewUserService(userRepository, logger, userAuthService, userValidator) userService := user.NewUserService(userRepository, logger, userAuthService, userValidator)
customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService)
companyService := company.NewCompanyService(companyRepository, logger) companyService := company.NewCompanyService(companyRepository, logger)
customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService)
logger.Info("Services initialized successfully", map[string]interface{}{ logger.Info("Services initialized successfully", map[string]interface{}{
"services": []string{"customer", "user", "company"}, "services": []string{"customer", "user", "company"},
@@ -115,9 +120,8 @@ func main() {
e := initHTTPServer(conf, logger) e := initHTTPServer(conf, logger)
// Register routes // Register routes
userHandler.RegisterRoutes(e) router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler)
customerHandler.RegisterRoutes(e) router.RegisterPublicRoutes(e, customerHandler)
companyHandler.RegisterRoutes(e)
// Start server // Start server
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port) serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
+109
View File
@@ -0,0 +1,109 @@
package router
import (
"tm/internal/company"
"tm/internal/customer"
"tm/internal/user"
"github.com/labstack/echo/v4"
)
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler) {
adminV1 := e.Group("/admin/v1")
// Admin Users Routes
adminUsersGP := adminV1.Group("/users")
{
adminUsersGP.Use(userHandler.AuthMiddleware())
adminUsersGP.POST("", userHandler.CreateUser)
adminUsersGP.GET("", userHandler.ListUsers)
adminUsersGP.GET("/:id", userHandler.GetUserByID)
adminUsersGP.PUT("/:id", userHandler.UpdateUser)
adminUsersGP.DELETE("/:id", userHandler.DeleteUser)
adminUsersGP.PUT("/:id/status", userHandler.UpdateUserStatus)
adminUsersGP.PUT("/:id/role", userHandler.UpdateUserRole)
adminUsersGP.GET("/company/:company_id", userHandler.GetUsersByCompanyID)
adminUsersGP.GET("/role/:role", userHandler.GetUsersByRole)
}
// Admin user profile
profileGP := adminV1.Group("/profile")
{
userAuthorizationGP := profileGP.Group("")
userAuthorizationGP.POST("/login", userHandler.Login)
userAuthorizationGP.POST("/refresh-token", userHandler.RefreshToken)
userAuthorizationGP.POST("/reset-password", userHandler.ResetPassword)
userProfileGP := profileGP.Group("")
userProfileGP.Use(userHandler.AuthMiddleware())
userProfileGP.GET("", userHandler.GetProfile)
userProfileGP.PUT("", userHandler.UpdateProfile)
userProfileGP.PUT("/change-password", userHandler.ChangePassword)
userProfileGP.DELETE("/logout", userHandler.Logout)
}
// Admin Companies Routes
companiesGP := adminV1.Group("/companies")
{
companiesGP.Use(userHandler.AuthMiddleware())
companiesGP.POST("", companyHandler.CreateCompany)
companiesGP.GET("", companyHandler.ListCompanies)
companiesGP.GET("/:id", companyHandler.GetCompany)
companiesGP.PUT("/:id", companyHandler.UpdateCompany)
companiesGP.DELETE("/:id", companyHandler.DeleteCompany)
companiesGP.GET("/search", companyHandler.SearchCompanies)
companiesGP.PATCH("/:id/status", companyHandler.UpdateCompanyStatus)
companiesGP.PATCH("/:id/verification", companyHandler.UpdateCompanyVerification)
companiesGP.PUT("/:id/tags", companyHandler.UpdateCompanyTags)
companiesGP.POST("/:id/tags/add", companyHandler.AddTags)
companiesGP.POST("/:id/tags/remove", companyHandler.RemoveTags)
companiesGP.POST("/:id/verify", companyHandler.VerifyCompany)
companiesGP.POST("/:id/suspend", companyHandler.SuspendCompany)
companiesGP.POST("/:id/activate", companyHandler.ActivateCompany)
companiesGP.GET("/stats", companyHandler.GetCompanyStats)
companiesGP.GET("/type/:type", companyHandler.GetCompaniesByType)
companiesGP.GET("/status/:status", companyHandler.GetCompaniesByStatus)
companiesGP.GET("/industry/:industry", companyHandler.GetCompaniesByIndustry)
}
// Admin Customers Routes
customersGP := adminV1.Group("/customers")
{
customersGP.Use(userHandler.AuthMiddleware())
customersGP.POST("", customerHandler.CreateCustomer)
customersGP.GET("", customerHandler.ListCustomers)
customersGP.GET("/with-companies", customerHandler.ListCustomersWithCompanies)
customersGP.GET("/:id", customerHandler.GetCustomerByID)
customersGP.GET("/:id/with-companies", customerHandler.GetCustomerByIDWithCompanies)
customersGP.PUT("/:id", customerHandler.UpdateCustomer)
customersGP.DELETE("/:id", customerHandler.DeleteCustomer)
customersGP.PATCH("/:id/status", customerHandler.UpdateCustomerStatus)
customersGP.PATCH("/:id/verification", customerHandler.UpdateCustomerVerification)
customersGP.POST("/:id/verify", customerHandler.VerifyCustomer)
customersGP.POST("/:id/suspend", customerHandler.SuspendCustomer)
customersGP.POST("/:id/activate", customerHandler.ActivateCustomer)
customersGP.GET("/company/:companyId", customerHandler.GetCustomersByCompanyID)
customersGP.GET("/type/:type", customerHandler.GetCustomersByType)
customersGP.GET("/status/:status", customerHandler.GetCustomersByStatus)
customersGP.POST("/:id/companies/assign", customerHandler.AssignCompaniesToCustomer)
customersGP.POST("/:id/companies/remove", customerHandler.RemoveCompaniesFromCustomer)
}
}
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler) {
v1 := e.Group("/api/v1")
customerGP := v1.Group("/profile")
{
customerGP.POST("/login", customerHandler.Login)
customerGP.POST("/refresh-token", customerHandler.RefreshToken)
profileGP := v1.Group("")
profileGP.Use(customerHandler.AuthMiddleware())
profileGP.GET("", customerHandler.GetProfile)
profileGP.GET("/with-companies", customerHandler.GetProfileWithCompanies)
profileGP.DELETE("/logout", customerHandler.Logout)
}
}
+1 -1
View File
@@ -90,7 +90,7 @@ internal/{domain}/
### Structured Logging ### Structured Logging
```go ```go
log.Info("Customer authenticated successfully", map[string]interface{}{ log.Info("Customer authenticated successfully", map[string]interface{}{
"customer_id": customer.ID.String(), "customer_id": customer.ID.Hex(),
"email": customer.Email, "email": customer.Email,
"ip": clientIP, "ip": clientIP,
}) })
+6 -4
View File
@@ -2,6 +2,8 @@ package company
import ( import (
"tm/pkg/mongo" "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/bson/primitive"
) )
// CompanyStatus represents company account status // CompanyStatus represents company account status
@@ -27,7 +29,7 @@ const (
// Company represents a company in the tender management system // Company represents a company in the tender management system
type Company struct { type Company struct {
mongo.Model mongo.Model `bson:",inline"`
Name string `bson:"name" json:"name"` Name string `bson:"name" json:"name"`
Type CompanyType `bson:"type" json:"type"` Type CompanyType `bson:"type" json:"type"`
Status CompanyStatus `bson:"status" json:"status"` Status CompanyStatus `bson:"status" json:"status"`
@@ -73,12 +75,12 @@ type Company struct {
// SetID sets the company ID (implements IDSetter interface) // SetID sets the company ID (implements IDSetter interface)
func (c *Company) SetID(id string) { func (c *Company) SetID(id string) {
c.ID = id c.ID, _ = primitive.ObjectIDFromHex(id)
} }
// GetID returns the company ID (implements IDGetter interface) // GetID returns the company ID (implements IDGetter interface)
func (c *Company) GetID() string { func (c *Company) GetID() string {
return c.ID return c.ID.Hex()
} }
// SetCreatedAt sets the created timestamp (implements Timestampable interface) // SetCreatedAt sets the created timestamp (implements Timestampable interface)
@@ -175,7 +177,7 @@ type CompanyResponse struct {
// ToResponse converts Company to CompanyResponse // ToResponse converts Company to CompanyResponse
func (c *Company) ToResponse() *CompanyResponse { func (c *Company) ToResponse() *CompanyResponse {
return &CompanyResponse{ return &CompanyResponse{
ID: c.ID, ID: c.ID.Hex(),
Name: c.Name, Name: c.Name,
Type: string(c.Type), Type: string(c.Type),
Status: string(c.Status), Status: string(c.Status),
-27
View File
@@ -25,33 +25,6 @@ func NewHandler(service Service, userHandler *user.Handler, logger logger.Logger
} }
} }
// RegisterRoutes registers company routes
func (h *Handler) RegisterRoutes(e *echo.Echo) {
// Admin routes for web panel
adminV1 := e.Group("/admin/v1/companies")
adminV1.Use(h.userHandler.AuthMiddleware())
adminV1.POST("", h.CreateCompany)
adminV1.GET("", h.ListCompanies)
adminV1.GET("/:id", h.GetCompany)
adminV1.PUT("/:id", h.UpdateCompany)
adminV1.DELETE("/:id", h.DeleteCompany)
adminV1.GET("/search", h.SearchCompanies)
adminV1.PATCH("/:id/status", h.UpdateCompanyStatus)
adminV1.PATCH("/:id/verification", h.UpdateCompanyVerification)
adminV1.PUT("/:id/tags", h.UpdateCompanyTags)
adminV1.POST("/:id/tags/add", h.AddTags)
adminV1.POST("/:id/tags/remove", h.RemoveTags)
adminV1.POST("/:id/verify", h.VerifyCompany)
adminV1.POST("/:id/suspend", h.SuspendCompany)
adminV1.POST("/:id/activate", h.ActivateCompany)
adminV1.GET("/stats", h.GetCompanyStats)
adminV1.GET("/type/:type", h.GetCompaniesByType)
adminV1.GET("/status/:status", h.GetCompaniesByStatus)
adminV1.GET("/industry/:industry", h.GetCompaniesByIndustry)
}
// Web Panel Endpoints
// CreateCompany creates a new company (Web Panel) // CreateCompany creates a new company (Web Panel)
// @Summary Create a new company // @Summary Create a new company
// @Description Create a new company with comprehensive information including business details, address, tags, and customer assignment. This endpoint is used by the web panel for full company registration. // @Description Create a new company with comprehensive information including business details, address, tags, and customer assignment. This endpoint is used by the web panel for full company registration.
+51 -30
View File
@@ -2,6 +2,8 @@ package customer
import ( import (
"tm/pkg/mongo" "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/bson/primitive"
) )
// CustomerStatus represents customer account status // CustomerStatus represents customer account status
@@ -26,9 +28,9 @@ const (
// Customer represents a customer in the tender management system // Customer represents a customer in the tender management system
// A customer can have an associated company for login purposes // A customer can have an associated company for login purposes
type Customer struct { type Customer struct {
mongo.Model mongo.Model `bson:",inline"`
Type CustomerType `bson:"type" json:"type"` Type CustomerType `bson:"type" json:"type"`
Status CustomerStatus `bson:"status" json:"status"` Status CustomerStatus `bson:"status" json:"status"`
// Individual customer fields // Individual customer fields
FirstName *string `bson:"first_name,omitempty" json:"first_name,omitempty"` FirstName *string `bson:"first_name,omitempty" json:"first_name,omitempty"`
@@ -40,8 +42,10 @@ type Customer struct {
Phone *string `bson:"phone,omitempty" json:"phone,omitempty"` Phone *string `bson:"phone,omitempty" json:"phone,omitempty"`
Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"` Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"`
// Company customer fields (for when customer represents a company) // Company relationships - each customer can be assigned to multiple companies
CompanyID *string `bson:"company_id,omitempty" json:"company_id,omitempty"` CompanyIDs []string `bson:"company_ids,omitempty" json:"company_ids,omitempty"`
// Company customer fields
RegistrationNumber *string `bson:"registration_number,omitempty" json:"registration_number,omitempty"` RegistrationNumber *string `bson:"registration_number,omitempty" json:"registration_number,omitempty"`
TaxID *string `bson:"tax_id,omitempty" json:"tax_id,omitempty"` TaxID *string `bson:"tax_id,omitempty" json:"tax_id,omitempty"`
Industry *string `bson:"industry,omitempty" json:"industry,omitempty"` Industry *string `bson:"industry,omitempty" json:"industry,omitempty"`
@@ -75,12 +79,12 @@ type Customer struct {
// SetID sets the customer ID (implements IDSetter interface) // SetID sets the customer ID (implements IDSetter interface)
func (c *Customer) SetID(id string) { func (c *Customer) SetID(id string) {
c.ID = id c.ID, _ = primitive.ObjectIDFromHex(id)
} }
// GetID returns the customer ID (implements IDGetter interface) // GetID returns the customer ID (implements IDGetter interface)
func (c *Customer) GetID() string { func (c *Customer) GetID() string {
return c.ID return c.ID.Hex()
} }
// SetCreatedAt sets the created timestamp (implements Timestampable interface) // SetCreatedAt sets the created timestamp (implements Timestampable interface)
@@ -126,44 +130,54 @@ type ContactPerson struct {
IsPrimary bool `bson:"is_primary" json:"is_primary"` IsPrimary bool `bson:"is_primary" json:"is_primary"`
} }
// CompanySummary represents company summary data for customer responses
type CompanySummary struct {
ID string `json:"id"`
Name string `json:"name"`
}
// CustomerResponse represents the customer data sent in API responses // CustomerResponse represents the customer data sent in API responses
type CustomerResponse struct { type CustomerResponse struct {
ID string `json:"id"` ID string `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Status string `json:"status"` Status string `json:"status"`
FirstName *string `json:"first_name,omitempty"` FirstName *string `json:"first_name,omitempty"`
LastName *string `json:"last_name,omitempty"` LastName *string `json:"last_name,omitempty"`
FullName *string `json:"full_name,omitempty"` FullName *string `json:"full_name,omitempty"`
Email string `json:"email"` Email string `json:"email"`
Phone *string `json:"phone,omitempty"` Phone *string `json:"phone,omitempty"`
Mobile *string `json:"mobile,omitempty"` Mobile *string `json:"mobile,omitempty"`
CompanyID *string `json:"company_id,omitempty"`
// Company relationships
Companies []*CompanySummary `json:"companies,omitempty"`
// Company customer fields
RegistrationNumber *string `json:"registration_number,omitempty"` RegistrationNumber *string `json:"registration_number,omitempty"`
TaxID *string `json:"tax_id,omitempty"` TaxID *string `json:"tax_id"`
Industry *string `json:"industry,omitempty"` Industry *string `json:"industry"`
Address *Address `json:"address,omitempty"` Address *Address `json:"address"`
BusinessType *string `json:"business_type,omitempty"` BusinessType *string `json:"business_type"`
EmployeeCount *int `json:"employee_count,omitempty"` EmployeeCount *int `json:"employee_count"`
AnnualRevenue *float64 `json:"annual_revenue,omitempty"` AnnualRevenue *float64 `json:"annual_revenue"`
FoundedYear *int `json:"founded_year,omitempty"` FoundedYear *int `json:"founded_year"`
ContactPerson *ContactPerson `json:"contact_person,omitempty"` ContactPerson *ContactPerson `json:"contact_person"`
IsVerified bool `json:"is_verified"` IsVerified bool `json:"is_verified"`
IsCompliant bool `json:"is_compliant"` IsCompliant bool `json:"is_compliant"`
ComplianceNotes *string `json:"compliance_notes,omitempty"` ComplianceNotes *string `json:"compliance_notes"`
Language string `json:"language"` Language string `json:"language"`
Username string `json:"username"` Username string `json:"username"`
Currency string `json:"currency"` Currency string `json:"currency"`
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
CreatedBy *string `json:"created_by,omitempty"` CreatedBy *string `json:"created_by"`
UpdatedBy *string `json:"updated_by,omitempty"` UpdatedBy *string `json:"updated_by"`
} }
// ToResponse converts Customer to CustomerResponse // ToResponse converts Customer to CustomerResponse
func (c *Customer) ToResponse() *CustomerResponse { func (c *Customer) ToResponse() *CustomerResponse {
return &CustomerResponse{ return &CustomerResponse{
ID: c.ID, ID: c.ID.Hex(),
Type: string(c.Type), Type: string(c.Type),
Status: string(c.Status), Status: string(c.Status),
FirstName: c.FirstName, FirstName: c.FirstName,
@@ -173,7 +187,7 @@ func (c *Customer) ToResponse() *CustomerResponse {
Email: c.Email, Email: c.Email,
Phone: c.Phone, Phone: c.Phone,
Mobile: c.Mobile, Mobile: c.Mobile,
CompanyID: c.CompanyID, Companies: nil, // Will be populated by service layer
RegistrationNumber: c.RegistrationNumber, RegistrationNumber: c.RegistrationNumber,
TaxID: c.TaxID, TaxID: c.TaxID,
Industry: c.Industry, Industry: c.Industry,
@@ -195,3 +209,10 @@ func (c *Customer) ToResponse() *CustomerResponse {
UpdatedBy: c.UpdatedBy, UpdatedBy: c.UpdatedBy,
} }
} }
// ToResponseWithCompanies converts Customer to CustomerResponse with company information
func (c *Customer) ToResponseWithCompanies(companies []*CompanySummary) *CustomerResponse {
response := c.ToResponse()
response.Companies = companies
return response
}
+25 -8
View File
@@ -2,8 +2,10 @@ package customer
// CreateCustomerForm represents the form for creating a new customer // CreateCustomerForm represents the form for creating a new customer
type CreateCustomerForm struct { type CreateCustomerForm struct {
Type string `json:"type" valid:"required,in(individual|company|government)"` Type string `json:"type" valid:"required,in(individual|company|government)"`
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
// Individual customer fields // Individual customer fields
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
@@ -41,8 +43,10 @@ type CreateCustomerForm struct {
// UpdateCustomerForm represents the form for updating a customer // UpdateCustomerForm represents the form for updating a customer
type UpdateCustomerForm struct { type UpdateCustomerForm struct {
Type *string `json:"type,omitempty" valid:"optional,in(individual|company|government)"` Type *string `json:"type,omitempty" valid:"optional,in(individual|company|government)"`
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
// Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
// Individual customer fields // Individual customer fields
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
@@ -154,8 +158,10 @@ type CreateCustomerMobileForm struct {
Password string `json:"password" valid:"required,length(8|128)"` Password string `json:"password" valid:"required,length(8|128)"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"` Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` // Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
} }
type UpdateCustomerMobileForm struct { type UpdateCustomerMobileForm struct {
@@ -164,8 +170,10 @@ type UpdateCustomerMobileForm struct {
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"` Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` // Company assignments
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
} }
// Customer Authentication DTOs // Customer Authentication DTOs
@@ -204,3 +212,12 @@ type AuthResponse struct {
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
ExpiresAt int64 `json:"expires_at"` ExpiresAt int64 `json:"expires_at"`
} }
// Company assignment forms
type AssignCompaniesForm struct {
CompanyIDs []string `json:"company_ids" valid:"required"`
}
type RemoveCompaniesForm struct {
CompanyIDs []string `json:"company_ids" valid:"required"`
}
+181 -35
View File
@@ -28,38 +28,6 @@ func NewHandler(service Service, userHandler *user.Handler, authService authoriz
} }
} }
// RegisterRoutes registers customer routes
func (h *Handler) RegisterRoutes(e *echo.Echo) {
adminV1 := e.Group("/admin/v1/customers")
adminV1.Use(h.userHandler.AuthMiddleware())
adminV1.POST("", h.CreateCustomer)
adminV1.GET("", h.ListCustomers)
adminV1.GET("/:id", h.GetCustomerByID)
adminV1.PUT("/:id", h.UpdateCustomer)
adminV1.DELETE("/:id", h.DeleteCustomer)
adminV1.PATCH("/:id/status", h.UpdateCustomerStatus)
adminV1.PATCH("/:id/verification", h.UpdateCustomerVerification)
adminV1.POST("/:id/verify", h.VerifyCustomer)
adminV1.POST("/:id/suspend", h.SuspendCustomer)
adminV1.POST("/:id/activate", h.ActivateCustomer)
adminV1.GET("/company/:companyId", h.GetCustomersByCompanyID)
adminV1.GET("/type/:type", h.GetCustomersByType)
adminV1.GET("/status/:status", h.GetCustomersByStatus)
// Mobile-specific endpoints
v1 := e.Group("/api/v1")
authorizationGP := v1.Group("")
authorizationGP.POST("/login", h.Login)
authorizationGP.POST("/refresh-token", h.RefreshToken)
profileGP := v1.Group("")
profileGP.Use(h.AuthMiddleware())
profileGP.GET("/profile", h.GetProfile)
profileGP.DELETE("/logout", h.Logout)
}
// CreateCustomer creates a new customer (Web Panel) // CreateCustomer creates a new customer (Web Panel)
// @Summary Create a new customer // @Summary Create a new customer
// @Description Create a new customer with comprehensive information including personal details, company information, address, and business details. This endpoint is used by the web panel for full customer registration. // @Description Create a new customer with comprehensive information including personal details, company information, address, and business details. This endpoint is used by the web panel for full customer registration.
@@ -127,6 +95,33 @@ func (h *Handler) GetCustomerByID(c echo.Context) error {
return response.Success(c, customer.ToResponse(), "Customer retrieved successfully") return response.Success(c, customer.ToResponse(), "Customer retrieved successfully")
} }
// GetCustomerByIDWithCompanies retrieves a customer by ID with companies (Web Panel)
// @Summary Get customer by ID with companies
// @Description Retrieve detailed customer information by customer ID including assigned companies
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Customer retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id}/with-companies [get]
func (h *Handler) GetCustomerByIDWithCompanies(c echo.Context) error {
idStr := c.Param("id")
customer, err := h.service.GetCustomerByIDWithCompanies(c.Request().Context(), idStr)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to get customer")
}
return response.Success(c, customer, "Customer retrieved successfully")
}
// UpdateCustomer updates a customer (Web Panel) // UpdateCustomer updates a customer (Web Panel)
// @Summary Update customer information // @Summary Update customer information
// @Description Update customer information including personal details, company information, address, and business details // @Description Update customer information including personal details, company information, address, and business details
@@ -247,6 +242,45 @@ func (h *Handler) ListCustomers(c echo.Context) error {
return response.Success(c, result, "Customers retrieved successfully") return response.Success(c, result, "Customers retrieved successfully")
} }
// ListCustomersWithCompanies lists customers with companies and filters
// @Summary List customers with companies and filters
// @Description Retrieve a paginated list of customers with their assigned companies and advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination.
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param search query string false "Search term to filter customers by name, email, or company name"
// @Param type query string false "Filter by customer type" Enums(individual, company, government)
// @Param status query string false "Filter by customer status" Enums(active, inactive, suspended, pending)
// @Param company_id query string false "Filter by company ID" format(uuid)
// @Param industry query string false "Filter by industry"
// @Param is_verified query boolean false "Filter by verification status"
// @Param is_compliant query boolean false "Filter by compliance status"
// @Param language query string false "Filter by preferred language"
// @Param currency query string false "Filter by preferred currency"
// @Param limit query integer false "Number of customers per page (1-100)" minimum(1) maximum(100) default(20)
// @Param offset query integer false "Number of customers to skip for pagination" minimum(0) default(0)
// @Param sort_by query string false "Field to sort by" Enums(email, company_name, created_at, updated_at, status) default(created_at)
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
// @Success 200 {object} response.APIResponse{data=CustomerListResponse} "Customers with companies retrieved successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/with-companies [get]
func (h *Handler) ListCustomersWithCompanies(c echo.Context) error {
form, err := response.Parse[ListCustomersForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.ListCustomersWithCompanies(c.Request().Context(), form)
if err != nil {
return response.InternalServerError(c, "Failed to list customers with companies")
}
return response.Success(c, result, "Customers with companies retrieved successfully")
}
// UpdateCustomerStatus updates customer status (Web Panel) // UpdateCustomerStatus updates customer status (Web Panel)
// @Summary Update customer status // @Summary Update customer status
// @Description Update customer account status (active, inactive, suspended, pending) // @Description Update customer account status (active, inactive, suspended, pending)
@@ -604,6 +638,88 @@ func (h *Handler) GetCustomersByStatus(c echo.Context) error {
return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully") return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully")
} }
// AssignCompaniesToCustomer assigns companies to a customer (Web Panel)
// @Summary Assign companies to a customer
// @Description Assign one or more companies to a specific customer.
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param companies body AssignCompaniesForm true "List of company IDs to assign"
// @Success 200 {object} response.APIResponse "Companies assigned successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id}/companies/assign [post]
func (h *Handler) AssignCompaniesToCustomer(c echo.Context) error {
idStr := c.Param("id")
form, err := response.Parse[AssignCompaniesForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Get user ID from JWT token context
assignedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.AssignCompaniesToCustomer(c.Request().Context(), idStr, form.CompanyIDs, &assignedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to assign companies to customer")
}
return response.Success(c, map[string]interface{}{
"message": "Companies assigned successfully",
}, "Companies assigned successfully")
}
// RemoveCompaniesFromCustomer removes companies from a customer (Web Panel)
// @Summary Remove companies from a customer
// @Description Remove one or more companies from a specific customer.
// @Tags Customers-Admin
// @Accept json
// @Produce json
// @Param id path string true "Customer ID"
// @Param companies body RemoveCompaniesForm true "List of company IDs to remove"
// @Success 200 {object} response.APIResponse "Companies removed successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Security BearerAuth
// @Router /admin/v1/customers/{id}/companies/remove [post]
func (h *Handler) RemoveCompaniesFromCustomer(c echo.Context) error {
idStr := c.Param("id")
form, err := response.Parse[RemoveCompaniesForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// Get user ID from JWT token context
removedBy, err := user.GetUserIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "User not authenticated")
}
err = h.service.RemoveCompaniesFromCustomer(c.Request().Context(), idStr, form.CompanyIDs, &removedBy)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to remove companies from customer")
}
return response.Success(c, map[string]interface{}{
"message": "Companies removed successfully",
}, "Companies removed successfully")
}
// Login handles customer authentication // Login handles customer authentication
// @Summary Customer login // @Summary Customer login
// @Description Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication. // @Description Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.
@@ -616,7 +732,7 @@ func (h *Handler) GetCustomersByStatus(c echo.Context) error {
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid credentials or inactive account" // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid credentials or inactive account"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" // @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error" // @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/login [post] // @Router /api/v1/profile/login [post]
func (h *Handler) Login(c echo.Context) error { func (h *Handler) Login(c echo.Context) error {
form, err := response.Parse[LoginForm](c) form, err := response.Parse[LoginForm](c)
if err != nil { if err != nil {
@@ -650,7 +766,7 @@ func (h *Handler) Login(c echo.Context) error {
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired refresh token" // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired refresh token"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" // @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error" // @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/refresh-token [post] // @Router /api/v1/profile/refresh-token [post]
func (h *Handler) RefreshToken(c echo.Context) error { func (h *Handler) RefreshToken(c echo.Context) error {
form, err := response.Parse[RefreshTokenForm](c) form, err := response.Parse[RefreshTokenForm](c)
if err != nil { if err != nil {
@@ -680,7 +796,7 @@ func (h *Handler) RefreshToken(c echo.Context) error {
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token" // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found" // @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error" // @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile [get] // @Router /api/v1/ [get]
func (h *Handler) GetProfile(c echo.Context) error { func (h *Handler) GetProfile(c echo.Context) error {
// Extract customer ID from JWT token context // Extract customer ID from JWT token context
customerID, err := GetCustomerIDFromContext(c) customerID, err := GetCustomerIDFromContext(c)
@@ -699,6 +815,36 @@ func (h *Handler) GetProfile(c echo.Context) error {
return response.Success(c, customer.ToResponse(), "Profile retrieved successfully") return response.Success(c, customer.ToResponse(), "Profile retrieved successfully")
} }
// GetProfileWithCompanies retrieves customer profile with companies (Mobile)
// @Summary Get customer profile with companies
// @Description Retrieve current customer profile information along with their assigned companies.
// @Tags Customers-Authorization
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Profile retrieved successfully"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token"
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/with-companies [get]
func (h *Handler) GetProfileWithCompanies(c echo.Context) error {
// Extract customer ID from JWT token context
customerID, err := GetCustomerIDFromContext(c)
if err != nil {
return response.Unauthorized(c, "Customer not authenticated")
}
customer, err := h.service.GetProfileWithCompanies(c.Request().Context(), customerID)
if err != nil {
if err.Error() == "customer not found" {
return response.NotFound(c, "Customer not found")
}
return response.InternalServerError(c, "Failed to get customer profile with companies")
}
return response.Success(c, customer, "Profile retrieved successfully")
}
// Logout handles customer logout (Mobile) // Logout handles customer logout (Mobile)
// @Summary Customer logout // @Summary Customer logout
// @Description Logout customer and invalidate access token // @Description Logout customer and invalidate access token
+66 -1
View File
@@ -20,11 +20,13 @@ type Repository interface {
GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error)
GetByTaxID(ctx context.Context, taxID string) (*Customer, error) GetByTaxID(ctx context.Context, taxID string) (*Customer, error)
GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*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 Update(ctx context.Context, customer *Customer) error
Delete(ctx context.Context, id string) error Delete(ctx context.Context, id string) error
List(ctx context.Context, limit, offset int) ([]*Customer, 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) 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) CountByCompanyID(ctx context.Context, companyID string) (int64, error)
CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error)
CountByType(ctx context.Context, customerType CustomerType) (int64, error) CountByType(ctx context.Context, customerType CustomerType) (int64, error)
CountByStatus(ctx context.Context, status CustomerStatus) (int64, error) CountByStatus(ctx context.Context, status CustomerStatus) (int64, error)
UpdateStatus(ctx context.Context, id string, status CustomerStatus) error UpdateStatus(ctx context.Context, id string, status CustomerStatus) error
@@ -46,7 +48,7 @@ func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logg
*mongopkg.NewIndex("company_name_idx", bson.D{{Key: "company_name", 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("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
*mongopkg.NewIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}), *mongopkg.NewIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}),
*mongopkg.NewIndex("company_id_idx", bson.D{{Key: "company_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("type_idx", bson.D{{Key: "type", Value: 1}}),
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), *mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
*mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}), *mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}),
@@ -245,6 +247,46 @@ func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID strin
return customers, nil 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 // Update updates a customer
func (r *customerRepository) Update(ctx context.Context, customer *Customer) error { func (r *customerRepository) Update(ctx context.Context, customer *Customer) error {
// Set updated timestamp using Unix timestamp // Set updated timestamp using Unix timestamp
@@ -424,6 +466,29 @@ func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID str
return count, nil 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 // CountByType counts customers by type
func (r *customerRepository) CountByType(ctx context.Context, customerType CustomerType) (int64, error) { func (r *customerRepository) CountByType(ctx context.Context, customerType CustomerType) (int64, error) {
filter := bson.M{ filter := bson.M{
+350 -24
View File
@@ -5,11 +5,12 @@ import (
"errors" "errors"
"time" "time"
"tm/internal/company"
"tm/pkg/logger" "tm/pkg/logger"
"tm/pkg/authorization" "tm/pkg/authorization"
"github.com/google/uuid" "go.mongodb.org/mongo-driver/bson/primitive"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -18,11 +19,13 @@ type Service interface {
// Core customer operations // Core customer operations
CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error) CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error)
GetCustomerByID(ctx context.Context, id string) (*Customer, error) GetCustomerByID(ctx context.Context, id string) (*Customer, error)
GetCustomerByIDWithCompanies(ctx context.Context, id string) (*CustomerResponse, error)
UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error)
DeleteCustomer(ctx context.Context, id string, deletedBy *string) error DeleteCustomer(ctx context.Context, id string, deletedBy *string) error
// Customer listing and search // Customer listing and search
ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error)
ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error)
GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error)
GetCustomersByType(ctx context.Context, customerType CustomerType, limit, offset int) ([]*Customer, int64, error) GetCustomersByType(ctx context.Context, customerType CustomerType, limit, offset int) ([]*Customer, int64, error)
GetCustomersByStatus(ctx context.Context, status CustomerStatus, limit, offset int) ([]*Customer, int64, error) GetCustomersByStatus(ctx context.Context, status CustomerStatus, limit, offset int) ([]*Customer, int64, error)
@@ -31,6 +34,10 @@ type Service interface {
UpdateCustomerStatus(ctx context.Context, id string, form *UpdateCustomerStatusForm, updatedBy *string) error UpdateCustomerStatus(ctx context.Context, id string, form *UpdateCustomerStatusForm, updatedBy *string) error
UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error
// Company assignments
AssignCompaniesToCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error
RemoveCompaniesFromCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error
// Mobile-specific operations (simplified) // Mobile-specific operations (simplified)
CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error)
UpdateCustomerMobile(ctx context.Context, id string, form *UpdateCustomerMobileForm, updatedBy *string) (*Customer, error) UpdateCustomerMobile(ctx context.Context, id string, form *UpdateCustomerMobileForm, updatedBy *string) (*Customer, error)
@@ -44,22 +51,25 @@ type Service interface {
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
GetProfile(ctx context.Context, customerID string) (*Customer, error) GetProfile(ctx context.Context, customerID string) (*Customer, error)
GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error)
Logout(ctx context.Context, customerID string, accessToken string) error Logout(ctx context.Context, customerID string, accessToken string) error
} }
// customerService implements the Service interface // customerService implements the Service interface
type customerService struct { type customerService struct {
repository Repository repository Repository
logger logger.Logger logger logger.Logger
authService authorization.AuthorizationService authService authorization.AuthorizationService
companyService company.Service
} }
// NewCustomerService creates a new customer service // NewCustomerService creates a new customer service
func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService) Service { func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service) Service {
return &customerService{ return &customerService{
repository: repository, repository: repository,
logger: logger, logger: logger,
authService: authService, authService: authService,
companyService: companyService,
} }
} }
@@ -95,10 +105,21 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
} }
} }
// Parse company ID if provided // Prepare company assignments
var companyID *string var companyIDs []string
if form.CompanyID != nil { if len(form.CompanyIDs) > 0 {
companyID = form.CompanyID // Verify that all company IDs exist
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
}
companyIDs = form.CompanyIDs
} }
// Hash password // Hash password
@@ -114,7 +135,7 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
customer := &Customer{ customer := &Customer{
Type: CustomerType(form.Type), Type: CustomerType(form.Type),
Status: CustomerStatusPending, Status: CustomerStatusPending,
CompanyID: companyID, CompanyIDs: companyIDs,
FirstName: form.FirstName, FirstName: form.FirstName,
LastName: form.LastName, LastName: form.LastName,
FullName: form.FullName, FullName: form.FullName,
@@ -155,6 +176,7 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom
"customer_id": customer.ID, "customer_id": customer.ID,
"email": customer.Email, "email": customer.Email,
"type": customer.Type, "type": customer.Type,
"company_ids": companyIDs,
"created_by": *createdBy, "created_by": *createdBy,
}) })
@@ -180,6 +202,38 @@ func (s *customerService) GetCustomerByID(ctx context.Context, id string) (*Cust
return customer, nil return customer, nil
} }
// GetCustomerByIDWithCompanies retrieves a customer by ID and loads associated companies
func (s *customerService) GetCustomerByIDWithCompanies(ctx context.Context, id string) (*CustomerResponse, error) {
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get customer by ID with companies", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return nil, err
}
// Load companies for the customer
companies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
// Continue without companies if loading fails
companies = []*CompanySummary{}
}
customerResponse := customer.ToResponseWithCompanies(companies)
s.logger.Info("Customer retrieved with companies successfully", map[string]interface{}{
"customer_id": customer.ID,
"email": customer.Email,
})
return customerResponse, nil
}
// UpdateCustomer updates a customer // UpdateCustomer updates a customer
func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) { func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) {
// Get existing customer // Get existing customer
@@ -233,8 +287,22 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *U
customer.Type = CustomerType(*form.Type) customer.Type = CustomerType(*form.Type)
} }
if form.CompanyID != nil { // Handle company assignments
customer.CompanyID = form.CompanyID if len(form.CompanyIDs) > 0 {
// Verify that all company IDs exist
var validCompanyIDs []string
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided in update", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
validCompanyIDs = append(validCompanyIDs, companyID)
}
customer.CompanyIDs = validCompanyIDs
} }
if form.FirstName != nil { if form.FirstName != nil {
@@ -411,6 +479,79 @@ func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomers
}, nil }, nil
} }
// ListCustomersWithCompanies lists customers with search and filters, including companies
func (s *customerService) ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) {
// Set defaults
limit := 20
if form.Limit != nil {
limit = *form.Limit
}
offset := 0
if form.Offset != nil {
offset = *form.Offset
}
search := ""
if form.Search != nil {
search = *form.Search
}
sortBy := "created_at"
if form.SortBy != nil {
sortBy = *form.SortBy
}
sortOrder := "desc"
if form.SortOrder != nil {
sortOrder = *form.SortOrder
}
// Parse company ID if provided
var companyID *string
if form.CompanyID != nil {
companyID = form.CompanyID
}
// Get customers
customers, err := s.repository.Search(ctx, search, form.Type, form.Status, companyID, form.Industry, form.IsVerified, form.IsCompliant, form.Language, form.Currency, limit, offset, sortBy, sortOrder)
if err != nil {
s.logger.Error("Failed to list customers with companies", map[string]interface{}{
"error": err.Error(),
})
return nil, errors.New("failed to list customers with companies")
}
// Convert to responses
var customerResponses []*CustomerResponse
for _, customer := range customers {
// Load companies for the customer
companies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer in list", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
// Continue without companies if loading fails
companies = []*CompanySummary{}
}
customerResponse := customer.ToResponseWithCompanies(companies)
customerResponses = append(customerResponses, customerResponse)
}
// TODO: Implement proper count for pagination metadata
total := int64(len(customers)) // This should be a separate count query
totalPages := (total + int64(limit) - 1) / int64(limit)
return &CustomerListResponse{
Customers: customerResponses,
Total: total,
Limit: limit,
Offset: offset,
TotalPages: int(totalPages),
}, nil
}
// GetCustomersByCompanyID retrieves customers by company ID with pagination // GetCustomersByCompanyID retrieves customers by company ID with pagination
func (s *customerService) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) { func (s *customerService) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) {
customers, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset) customers, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset)
@@ -546,6 +687,98 @@ func (s *customerService) UpdateCustomerVerification(ctx context.Context, id str
return nil return nil
} }
// AssignCompaniesToCustomer assigns companies to a customer
func (s *customerService) AssignCompaniesToCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
return errors.New("customer not found")
}
// Verify companies exist and update customer's company IDs
var validCompanyIDs []string
for _, companyID := range companyIDs {
_, err = s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Company not found during assignment", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
"company_id": companyID,
})
continue // Skip invalid company IDs
}
validCompanyIDs = append(validCompanyIDs, companyID)
}
// Update customer's company IDs
customer.CompanyIDs = append(customer.CompanyIDs, validCompanyIDs...)
customer.UpdatedBy = updatedBy
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer with company assignments", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return errors.New("failed to assign companies to customer")
}
s.logger.Info("Companies assigned to customer successfully", map[string]interface{}{
"customer_id": customerID,
"company_ids": validCompanyIDs,
"updated_by": updatedBy,
})
return nil
}
// RemoveCompaniesFromCustomer removes companies from a customer
func (s *customerService) RemoveCompaniesFromCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error {
// Get customer to check if exists
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
return errors.New("customer not found")
}
// Remove company IDs from customer's list
var updatedCompanyIDs []string
for _, existingID := range customer.CompanyIDs {
shouldRemove := false
for _, removeID := range companyIDs {
if existingID == removeID {
shouldRemove = true
break
}
}
if !shouldRemove {
updatedCompanyIDs = append(updatedCompanyIDs, existingID)
}
}
// Update customer's company IDs
customer.CompanyIDs = updatedCompanyIDs
customer.UpdatedBy = updatedBy
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer after removing company assignments", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return errors.New("failed to remove companies from customer")
}
s.logger.Info("Companies removed from customer successfully", map[string]interface{}{
"customer_id": customerID,
"company_ids": companyIDs,
"updated_by": updatedBy,
})
return nil
}
// CreateCustomerMobile creates a new customer via mobile app (simplified) // CreateCustomerMobile creates a new customer via mobile app (simplified)
func (s *customerService) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error) { func (s *customerService) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error) {
// Check if email already exists // Check if email already exists
@@ -554,10 +787,28 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create
return nil, errors.New("customer with this email already exists") return nil, errors.New("customer with this email already exists")
} }
// Prepare company assignments
var companyIDs []string
if len(form.CompanyIDs) > 0 {
// Verify that all company IDs exist
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided in mobile form", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
}
companyIDs = form.CompanyIDs
}
// Create customer with mobile form // Create customer with mobile form
customer := &Customer{ customer := &Customer{
Type: CustomerType(form.Type), Type: CustomerType(form.Type),
Status: CustomerStatusActive, Status: CustomerStatusActive,
CompanyIDs: companyIDs,
FirstName: form.FirstName, FirstName: form.FirstName,
LastName: form.LastName, LastName: form.LastName,
FullName: form.FullName, FullName: form.FullName,
@@ -566,7 +817,6 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create
Password: "", // Will be set after hashing Password: "", // Will be set after hashing
Phone: form.Phone, Phone: form.Phone,
Mobile: form.Mobile, Mobile: form.Mobile,
CompanyID: form.CompanyID,
Industry: form.Industry, Industry: form.Industry,
IsVerified: false, IsVerified: false,
IsCompliant: false, IsCompliant: false,
@@ -602,6 +852,7 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create
"customer_id": customer.ID, "customer_id": customer.ID,
"email": customer.Email, "email": customer.Email,
"type": customer.Type, "type": customer.Type,
"company_ids": companyIDs,
"created_by": *createdBy, "created_by": *createdBy,
}) })
@@ -637,8 +888,22 @@ func (s *customerService) UpdateCustomerMobile(ctx context.Context, id string, f
customer.Mobile = form.Mobile customer.Mobile = form.Mobile
} }
if form.CompanyID != nil { // Handle company assignments
customer.CompanyID = form.CompanyID if len(form.CompanyIDs) > 0 {
// Verify that all company IDs exist
var validCompanyIDs []string
for _, companyID := range form.CompanyIDs {
_, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Error("Invalid company ID provided in mobile update", map[string]interface{}{
"error": err.Error(),
"company_id": companyID,
})
return nil, errors.New("invalid company ID: " + companyID)
}
validCompanyIDs = append(validCompanyIDs, companyID)
}
customer.CompanyIDs = validCompanyIDs
} }
if form.Industry != nil { if form.Industry != nil {
@@ -661,6 +926,7 @@ func (s *customerService) UpdateCustomerMobile(ctx context.Context, id string, f
s.logger.Info("Customer updated via mobile successfully", map[string]interface{}{ s.logger.Info("Customer updated via mobile successfully", map[string]interface{}{
"customer_id": customer.ID, "customer_id": customer.ID,
"email": customer.Email, "email": customer.Email,
"company_ids": customer.CompanyIDs,
"updated_by": *updatedBy, "updated_by": *updatedBy,
}) })
@@ -783,12 +1049,12 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp
// Generate JWT tokens using authorization service // Generate JWT tokens using authorization service
companyID := "" companyID := ""
if customer.CompanyID != nil { if len(customer.CompanyIDs) > 0 {
companyID = *customer.CompanyID companyID = customer.CompanyIDs[0] // Use first company ID for JWT token
} }
tokenPair, err := s.authService.GenerateTokenPair( tokenPair, err := s.authService.GenerateTokenPair(
customer.ID, customer.ID.Hex(),
customer.Email, customer.Email,
"customer", // Role for customers "customer", // Role for customers
companyID, companyID,
@@ -847,7 +1113,7 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string)
} }
// Get customer information // Get customer information
customerID, err := uuid.Parse(validationResult.Claims.UserID) customerID, err := primitive.ObjectIDFromHex(validationResult.Claims.UserID)
if err != nil { if err != nil {
s.logger.Error("Failed to parse customer ID from token", map[string]interface{}{ s.logger.Error("Failed to parse customer ID from token", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -855,11 +1121,11 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string)
return nil, errors.New("invalid token format") return nil, errors.New("invalid token format")
} }
customer, err := s.repository.GetByID(ctx, customerID.String()) customer, err := s.repository.GetByID(ctx, customerID.Hex())
if err != nil { if err != nil {
s.logger.Error("Failed to get customer for token refresh", map[string]interface{}{ s.logger.Error("Failed to get customer for token refresh", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"customer_id": customerID.String(), "customer_id": customerID.Hex(),
}) })
return nil, errors.New("customer not found") return nil, errors.New("customer not found")
} }
@@ -905,6 +1171,42 @@ func (s *customerService) GetProfile(ctx context.Context, customerID string) (*C
return customer, nil return customer, nil
} }
// GetProfileWithCompanies retrieves customer profile information including companies
func (s *customerService) GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error) {
s.logger.Info("Getting customer profile with companies", map[string]interface{}{
"customer_id": customerID,
})
customer, err := s.repository.GetByID(ctx, customerID)
if err != nil {
s.logger.Error("Failed to get customer profile with companies", map[string]interface{}{
"error": err.Error(),
"customer_id": customerID,
})
return nil, errors.New("customer not found")
}
// Load companies for the customer
companies, err := s.loadCompaniesForCustomer(ctx, customer)
if err != nil {
s.logger.Error("Failed to load companies for customer profile", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
})
// Continue without companies if loading fails
companies = []*CompanySummary{}
}
customerResponse := customer.ToResponseWithCompanies(companies)
s.logger.Info("Customer profile retrieved with companies successfully", map[string]interface{}{
"customer_id": customerID,
"customer_type": string(customer.Type),
})
return customerResponse, nil
}
// Logout handles customer logout // Logout handles customer logout
func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error { func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error {
s.logger.Info("Customer logout", map[string]interface{}{ s.logger.Info("Customer logout", map[string]interface{}{
@@ -983,3 +1285,27 @@ func (s *customerService) getDefaultTimezone(timezone *string) string {
} }
return "UTC" return "UTC"
} }
// loadCompaniesForCustomer loads company summaries for a customer
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
var companies []*CompanySummary
// Load companies from CompanyIDs
for _, companyID := range customer.CompanyIDs {
company, err := s.companyService.GetCompanyByID(ctx, companyID)
if err != nil {
s.logger.Warn("Failed to load company for customer", map[string]interface{}{
"error": err.Error(),
"customer_id": customer.ID,
"company_id": companyID,
})
continue // Skip companies that can't be loaded
}
companies = append(companies, &CompanySummary{
ID: company.ID.Hex(),
Name: company.Name,
})
}
return companies, nil
}
+5 -3
View File
@@ -2,6 +2,8 @@ package user
import ( import (
"tm/pkg/mongo" "tm/pkg/mongo"
"go.mongodb.org/mongo-driver/bson/primitive"
) )
// UserRole represents user roles in the system // UserRole represents user roles in the system
@@ -25,7 +27,7 @@ const (
// User represents a system user (admin/manager/operator) // User represents a system user (admin/manager/operator)
type User struct { type User struct {
mongo.Model mongo.Model `bson:",inline"`
FullName string `bson:"full_name" json:"full_name"` FullName string `bson:"full_name" json:"full_name"`
Username string `bson:"username" json:"username"` Username string `bson:"username" json:"username"`
Email string `bson:"email" json:"email"` Email string `bson:"email" json:"email"`
@@ -45,12 +47,12 @@ type User struct {
// SetID sets the user ID (implements IDSetter interface) // SetID sets the user ID (implements IDSetter interface)
func (u *User) SetID(id string) { func (u *User) SetID(id string) {
u.ID = id u.ID, _ = primitive.ObjectIDFromHex(id)
} }
// GetID returns the user ID (implements IDGetter interface) // GetID returns the user ID (implements IDGetter interface)
func (u *User) GetID() string { func (u *User) GetID() string {
return u.ID return u.ID.Hex()
} }
// SetCreatedAt sets the created timestamp (implements Timestampable interface) // SetCreatedAt sets the created timestamp (implements Timestampable interface)
+41 -32
View File
@@ -1,57 +1,66 @@
package user package user
// User Registration DTOs // User Registration DTOs
// CreateUserForm represents the data required to create a new user account
type CreateUserForm struct { type CreateUserForm struct {
FullName string `json:"full_name" valid:"required,length(2|100)"` FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Smith"` // Full name of the user
Username string `json:"username" valid:"required,alphanum,length(3|30)"` Username string `json:"username" valid:"required,alphanum,length(3|30)" example:"johnsmith"` // Unique username (alphanumeric only)
Email string `json:"email" valid:"required,email"` Email string `json:"email" valid:"required,email" example:"john.smith@company.com"` // Valid email address
Password string `json:"password" valid:"required,length(8|128)"` Password string `json:"password" valid:"required,length(8|128)" example:"SecurePass123!"` // Password (minimum 8 characters)
Role string `json:"role" valid:"required,in(admin|manager|operator|viewer)"` Role string `json:"role" valid:"required,in(admin|manager|operator|viewer)" example:"manager"` // User role (admin, manager, operator, viewer)
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"` CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid" example:"123e4567-e89b-12d3-a456-426614174000"` // Optional company UUID
Department *string `json:"department,omitempty" valid:"optional,length(2|100)"` Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Information Technology"` // Optional department name
Position *string `json:"position,omitempty" valid:"optional,length(2|100)"` Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Senior Developer"` // Optional job position
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"` // Optional phone number
ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url"` ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:"https://example.com/avatar.jpg"` // Optional profile image URL
} }
// UpdateUserForm represents the data for updating an existing user account
type UpdateUserForm struct { type UpdateUserForm struct {
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"John Smith"` // Updated full name
Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)"` Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)" example:"johnsmith"` // Updated username
Email *string `json:"email,omitempty" valid:"optional,email"` Email *string `json:"email,omitempty" valid:"optional,email" example:"john.smith@newcompany.com"` // Updated email address
Role *string `json:"role,omitempty" valid:"optional,in(admin|manager|operator|viewer)"` Role *string `json:"role,omitempty" valid:"optional,in(admin|manager|operator|viewer)" example:"admin"` // Updated user role
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"` CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid" example:"123e4567-e89b-12d3-a456-426614174000"` // Updated company ID
Department *string `json:"department,omitempty" valid:"optional,length(2|100)"` Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Product Management"` // Updated department
Position *string `json:"position,omitempty" valid:"optional,length(2|100)"` Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Tech Lead"` // Updated position
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"` // Updated phone number
ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url"` ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:"https://example.com/new-avatar.jpg"` // Updated profile image URL
Status *string `json:"status,omitempty" valid:"optional,in(active|inactive|suspended)"` Status *string `json:"status,omitempty" valid:"optional,in(active|inactive|suspended)" example:"active"` // Updated account status
} }
// User Authentication DTOs // User Authentication DTOs
// LoginForm represents the credentials required for user authentication
type LoginForm struct { type LoginForm struct {
Username string `json:"username" valid:"required"` Username string `json:"username" valid:"required" example:"nakhostin"` // Username or email address
Password string `json:"password" valid:"required"` Password string `json:"password" valid:"required" example:"Nima.1998"` // User password
} }
// RefreshTokenForm represents the refresh token required to generate new access tokens
type RefreshTokenForm struct { type RefreshTokenForm struct {
RefreshToken string `json:"refresh_token" valid:"required"` RefreshToken string `json:"refresh_token" valid:"required" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."` // Valid refresh token
} }
// ChangePasswordForm represents the data required to change user password
type ChangePasswordForm struct { type ChangePasswordForm struct {
OldPassword string `json:"old_password" valid:"required"` OldPassword string `json:"old_password" valid:"required" example:"OldPassword123!"` // Current password for verification
NewPassword string `json:"new_password" valid:"required,length(8|128)"` NewPassword string `json:"new_password" valid:"required,length(8|128)" example:"NewPass456!"` // New password (minimum 8 characters)
} }
// UpdateProfileForm represents the data for updating user profile information
type UpdateProfileForm struct { type UpdateProfileForm struct {
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)" example:"John Smith"` // Updated full name
Department *string `json:"department,omitempty" valid:"optional,length(2|100)"` Department *string `json:"department,omitempty" valid:"optional,length(2|100)" example:"Engineering"` // Updated department
Position *string `json:"position,omitempty" valid:"optional,length(2|100)"` Position *string `json:"position,omitempty" valid:"optional,length(2|100)" example:"Senior Engineer"` // Updated position
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)" example:"+1234567890"` // Updated phone number
ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url"` ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url" example:"https://example.com/profile.jpg"` // Updated profile image URL
} }
// ResetPasswordForm represents the email address for password reset
type ResetPasswordForm struct { type ResetPasswordForm struct {
Email string `json:"email" valid:"required,email"` Email string `json:"email" valid:"required,email" example:"john.smith@company.com"` // Email address for password reset
} }
// Admin DTOs // Admin DTOs
@@ -113,7 +122,7 @@ type UserListResponse struct {
// Helper function to convert User to UserResponse // Helper function to convert User to UserResponse
func (u *User) ToResponse() *UserResponse { func (u *User) ToResponse() *UserResponse {
return &UserResponse{ return &UserResponse{
ID: u.ID, ID: u.ID.Hex(),
FullName: u.FullName, FullName: u.FullName,
Username: u.Username, Username: u.Username,
Email: u.Email, Email: u.Email,
+80 -101
View File
@@ -28,50 +28,20 @@ func NewUserHandler(service Service, logger logger.Logger, validator ValidationS
} }
} }
// RegisterRoutes registers user routes
func (h *Handler) RegisterRoutes(e *echo.Echo) {
v1 := e.Group("/admin/v1")
// Public routes (no authentication required)
authorizationGP := v1.Group("")
authorizationGP.POST("/login", h.Login)
authorizationGP.POST("/refresh-token", h.RefreshToken)
authorizationGP.POST("/reset-password", h.ResetPassword)
// Protected routes (require authentication)
profileGP := v1.Group("")
profileGP.Use(h.AuthMiddleware())
profileGP.GET("/profile", h.GetProfile)
profileGP.PUT("/profile", h.UpdateProfile)
profileGP.PUT("/change-password", h.ChangePassword)
profileGP.DELETE("/logout", h.Logout)
// Admin routes (require admin role)
adminGroup := v1.Group("/users")
adminGroup.Use(h.AuthMiddleware(), h.AdminMiddleware())
adminGroup.POST("", h.CreateUser)
adminGroup.GET("", h.ListUsers)
adminGroup.GET("/:id", h.GetUserByID)
adminGroup.PUT("/:id", h.UpdateUser)
adminGroup.DELETE("/:id", h.DeleteUser)
adminGroup.PUT("/:id/status", h.UpdateUserStatus)
adminGroup.PUT("/:id/role", h.UpdateUserRole)
adminGroup.GET("/company/:company_id", h.GetUsersByCompanyID)
adminGroup.GET("/role/:role", h.GetUsersByRole)
}
// Login handles user login // Login handles user login
// @Summary Login user // @Summary Authenticate user login
// @Description Authenticate user with username and password // @Description Authenticate user with username/email and password to obtain access and refresh tokens for web panel administration. This endpoint validates credentials and returns JWT tokens for subsequent API calls.
// @Tags Authorization // @Tags Authorization
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param login body LoginForm true "Login credentials" // @Param login body LoginForm true "User login credentials including username/email and password"
// @Success 200 {object} response.APIResponse{data=AuthResponse} // @Success 200 {object} response.APIResponse{data=AuthResponse} "Login successful with access and refresh tokens"
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse "Bad request - Invalid request format or missing fields"
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid credentials or account locked"
// @Failure 500 {object} response.APIResponse // @Failure 422 {object} response.APIResponse "Validation error - Invalid input data format"
// @Router /admin/v1/login [post] // @Failure 429 {object} response.APIResponse "Too many requests - Rate limit exceeded"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/v1/profile/login [post]
func (h *Handler) Login(c echo.Context) error { func (h *Handler) Login(c echo.Context) error {
form, err := response.Parse[LoginForm](c) form, err := response.Parse[LoginForm](c)
if err != nil { if err != nil {
@@ -89,16 +59,17 @@ func (h *Handler) Login(c echo.Context) error {
// RefreshToken handles token refresh // RefreshToken handles token refresh
// @Summary Refresh access token // @Summary Refresh access token
// @Description Refresh access token using refresh token // @Description Generate a new access token using a valid refresh token. This endpoint allows clients to obtain fresh access tokens without re-authentication, maintaining session continuity.
// @Tags Authorization // @Tags Authorization
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param refresh body RefreshTokenForm true "Refresh token" // @Param refresh body RefreshTokenForm true "Refresh token for generating new access token"
// @Success 200 {object} response.APIResponse{data=AuthResponse} // @Success 200 {object} response.APIResponse{data=AuthResponse} "Token refreshed successfully with new access token"
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse "Bad request - Invalid request format"
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired refresh token"
// @Failure 500 {object} response.APIResponse // @Failure 422 {object} response.APIResponse "Validation error - Invalid token format"
// @Router /admin/v1/refresh-token [post] // @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/v1/profile/refresh-token [post]
func (h *Handler) RefreshToken(c echo.Context) error { func (h *Handler) RefreshToken(c echo.Context) error {
form, err := response.Parse[RefreshTokenForm](c) form, err := response.Parse[RefreshTokenForm](c)
if err != nil { if err != nil {
@@ -114,16 +85,19 @@ func (h *Handler) RefreshToken(c echo.Context) error {
} }
// ResetPassword handles password reset request // ResetPassword handles password reset request
// @Summary Reset password // @Summary Initiate password reset process
// @Description Send password reset email to user // @Description Send password reset email to user with reset token. This endpoint validates the email address and sends a secure reset link to the user's registered email address.
// @Tags Authorization // @Tags Authorization
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param reset body ResetPasswordForm true "Password reset request" // @Param reset body ResetPasswordForm true "Email address for password reset request"
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse "Password reset email sent successfully"
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse "Bad request - Invalid request format or email address"
// @Failure 500 {object} response.APIResponse // @Failure 404 {object} response.APIResponse "Not found - Email address not registered"
// @Router /admin/v1/reset-password [post] // @Failure 422 {object} response.APIResponse "Validation error - Invalid email format"
// @Failure 429 {object} response.APIResponse "Too many requests - Rate limit for reset emails exceeded"
// @Failure 500 {object} response.APIResponse "Internal server error - Failed to send email"
// @Router /admin/v1/profile/reset-password [post]
func (h *Handler) ResetPassword(c echo.Context) error { func (h *Handler) ResetPassword(c echo.Context) error {
form, err := response.Parse[ResetPasswordForm](c) form, err := response.Parse[ResetPasswordForm](c)
if err != nil { if err != nil {
@@ -141,17 +115,17 @@ func (h *Handler) ResetPassword(c echo.Context) error {
} }
// GetProfile gets current user profile // GetProfile gets current user profile
// @Summary Get user profile // @Summary Get authenticated user profile
// @Description Get current user profile information // @Description Retrieve complete profile information for the currently authenticated user including personal details, role information, permissions, and account status. This endpoint requires valid authentication token.
// @Tags Users // @Tags Users
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
// @Success 200 {object} response.APIResponse{data=UserResponse} // @Success 200 {object} response.APIResponse{data=UserResponse} "Profile retrieved successfully with user details"
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired authentication token"
// @Failure 404 {object} response.APIResponse // @Failure 404 {object} response.APIResponse "Not found - User profile not found"
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/v1/profile [get] // @Router /admin/v1/profile [get]
func (h *Handler) GetProfile(c echo.Context) error { func (h *Handler) GetProfile(c echo.Context) error {
// Extract user ID from JWT token context // Extract user ID from JWT token context
userID, err := GetUserIDFromContext(c) userID, err := GetUserIDFromContext(c)
@@ -168,18 +142,20 @@ func (h *Handler) GetProfile(c echo.Context) error {
} }
// UpdateProfile updates current user profile // UpdateProfile updates current user profile
// @Summary Update user profile // @Summary Update authenticated user profile
// @Description Update current user profile information // @Description Update profile information for the currently authenticated user including personal details, contact information, and preferences. Only the authenticated user can update their own profile.
// @Tags Users // @Tags Users
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
// @Param profile body UpdateUserForm true "Profile update data" // @Param profile body UpdateUserForm true "Profile update data including name, email, phone, and other personal information"
// @Success 200 {object} response.APIResponse{data=UserResponse} // @Success 200 {object} response.APIResponse{data=UserResponse} "Profile updated successfully with updated user details"
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse "Bad request - Invalid request format or missing required fields"
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired authentication token"
// @Failure 500 {object} response.APIResponse // @Failure 409 {object} response.APIResponse "Conflict - Email address already exists for another user"
// @Router /admin/v1/profile [put] // @Failure 422 {object} response.APIResponse "Validation error - Invalid input data format"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/v1/profile [put]
func (h *Handler) UpdateProfile(c echo.Context) error { func (h *Handler) UpdateProfile(c echo.Context) error {
// Extract user ID from JWT token context // Extract user ID from JWT token context
userID, err := GetUserIDFromContext(c) userID, err := GetUserIDFromContext(c)
@@ -201,18 +177,19 @@ func (h *Handler) UpdateProfile(c echo.Context) error {
} }
// ChangePassword changes current user password // ChangePassword changes current user password
// @Summary Change password // @Summary Change user password
// @Description Change current user password // @Description Change password for the currently authenticated user. Requires current password verification and enforces password policy. This operation invalidates all existing sessions and requires re-authentication.
// @Tags Users // @Tags Users
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
// @Param password body ChangePasswordForm true "Password change data" // @Param password body ChangePasswordForm true "Password change data including current password and new password"
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse "Password changed successfully - user must re-authenticate"
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse "Bad request - Invalid request format or password policy violation"
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid authentication token or incorrect current password"
// @Failure 500 {object} response.APIResponse // @Failure 422 {object} response.APIResponse "Validation error - Invalid password format or policy requirements not met"
// @Router /admin/v1/change-password [put] // @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/v1/profile/change-password [put]
func (h *Handler) ChangePassword(c echo.Context) error { func (h *Handler) ChangePassword(c echo.Context) error {
userID, err := GetUserIDFromContext(c) userID, err := GetUserIDFromContext(c)
if err != nil { if err != nil {
@@ -235,16 +212,16 @@ func (h *Handler) ChangePassword(c echo.Context) error {
} }
// Logout handles user logout // Logout handles user logout
// @Summary Logout user // @Summary Logout authenticated user
// @Description Logout current user and invalidate tokens // @Description Logout the currently authenticated user by invalidating their access and refresh tokens. This endpoint ensures secure session termination and prevents further use of the user's tokens.
// @Tags Users // @Tags Authorization
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse "Logout successful - all tokens invalidated"
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired authentication token"
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse "Internal server error - Failed to invalidate tokens"
// @Router /admin/v1/logout [delete] // @Router /admin/v1/profile/logout [delete]
func (h *Handler) Logout(c echo.Context) error { func (h *Handler) Logout(c echo.Context) error {
// Extract user ID and access token from context // Extract user ID and access token from context
userID, err := GetUserIDFromContext(c) userID, err := GetUserIDFromContext(c)
@@ -268,19 +245,21 @@ func (h *Handler) Logout(c echo.Context) error {
} }
// CreateUser creates a new user (admin only) // CreateUser creates a new user (admin only)
// @Summary Create user // @Summary Create new user account
// @Description Create a new user (admin only) // @Description Create a new user account with specified role and permissions. This endpoint is restricted to administrators only and allows creation of various user types including admins, managers, and operators with appropriate access levels.
// @Tags Users // @Tags Users
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Security BearerAuth // @Security BearerAuth
// @Param user body CreateUserForm true "User creation data" // @Param user body CreateUserForm true "User creation data including username, email, password, role, and profile information"
// @Success 201 {object} response.APIResponse{data=UserResponse} // @Success 201 {object} response.APIResponse{data=UserResponse} "User created successfully with assigned role and permissions"
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse "Bad request - Invalid input data or missing required fields"
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired authentication token"
// @Failure 403 {object} response.APIResponse // @Failure 403 {object} response.APIResponse "Forbidden - Insufficient privileges to create users"
// @Failure 500 {object} response.APIResponse // @Failure 409 {object} response.APIResponse "Conflict - Username or email already exists"
// @Router /admin/v1/users [post] // @Failure 422 {object} response.APIResponse "Validation error - Invalid data format or password policy violation"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /admin/v1/users [post]
func (h *Handler) CreateUser(c echo.Context) error { func (h *Handler) CreateUser(c echo.Context) error {
// Get current user ID from JWT token // Get current user ID from JWT token
currentUserID, err := GetUserIDFromContext(c) currentUserID, err := GetUserIDFromContext(c)
@@ -327,7 +306,7 @@ func (h *Handler) CreateUser(c echo.Context) error {
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse // @Failure 403 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/v1/users [get] // @Router /admin/v1/users [get]
func (h *Handler) ListUsers(c echo.Context) error { func (h *Handler) ListUsers(c echo.Context) error {
var form ListUsersForm var form ListUsersForm
if err := c.Bind(&form); err != nil { if err := c.Bind(&form); err != nil {
@@ -362,7 +341,7 @@ func (h *Handler) ListUsers(c echo.Context) error {
// @Failure 403 {object} response.APIResponse // @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse // @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/v1/users/{id} [get] // @Router /admin/v1/users/{id} [get]
func (h *Handler) GetUserByID(c echo.Context) error { func (h *Handler) GetUserByID(c echo.Context) error {
idStr := c.Param("id") idStr := c.Param("id")
@@ -389,7 +368,7 @@ func (h *Handler) GetUserByID(c echo.Context) error {
// @Failure 403 {object} response.APIResponse // @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse // @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/v1/users/{id} [put] // @Router /admin/v1/users/{id} [put]
func (h *Handler) UpdateUser(c echo.Context) error { func (h *Handler) UpdateUser(c echo.Context) error {
// Extract user ID from JWT token context // Extract user ID from JWT token context
currentUserID, err := GetUserIDFromContext(c) currentUserID, err := GetUserIDFromContext(c)
@@ -432,7 +411,7 @@ func (h *Handler) UpdateUser(c echo.Context) error {
// @Failure 403 {object} response.APIResponse // @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse // @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/v1/users/{id} [delete] // @Router /admin/v1/users/{id} [delete]
func (h *Handler) DeleteUser(c echo.Context) error { func (h *Handler) DeleteUser(c echo.Context) error {
// Extract user ID from JWT token context // Extract user ID from JWT token context
currentUserID, err := GetUserIDFromContext(c) currentUserID, err := GetUserIDFromContext(c)
@@ -467,7 +446,7 @@ func (h *Handler) DeleteUser(c echo.Context) error {
// @Failure 403 {object} response.APIResponse // @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse // @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/v1/users/{id}/status [put] // @Router /admin/v1/users/{id}/status [put]
func (h *Handler) UpdateUserStatus(c echo.Context) error { func (h *Handler) UpdateUserStatus(c echo.Context) error {
currentUserID, err := GetUserIDFromContext(c) currentUserID, err := GetUserIDFromContext(c)
if err != nil { if err != nil {
@@ -512,7 +491,7 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error {
// @Failure 403 {object} response.APIResponse // @Failure 403 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse // @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/v1/users/{id}/role [put] // @Router /admin/v1/users/{id}/role [put]
func (h *Handler) UpdateUserRole(c echo.Context) error { func (h *Handler) UpdateUserRole(c echo.Context) error {
// Extract user ID from JWT token context // Extract user ID from JWT token context
currentUserID, err := GetUserIDFromContext(c) currentUserID, err := GetUserIDFromContext(c)
@@ -558,7 +537,7 @@ func (h *Handler) UpdateUserRole(c echo.Context) error {
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse // @Failure 403 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/v1/users/company/{company_id} [get] // @Router /admin/v1/users/company/{company_id} [get]
func (h *Handler) GetUsersByCompanyID(c echo.Context) error { func (h *Handler) GetUsersByCompanyID(c echo.Context) error {
companyIDStr := c.Param("company_id") companyIDStr := c.Param("company_id")
@@ -618,7 +597,7 @@ func (h *Handler) GetUsersByCompanyID(c echo.Context) error {
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} response.APIResponse
// @Failure 403 {object} response.APIResponse // @Failure 403 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/v1/users/role/{role} [get] // @Router /admin/v1/users/role/{role} [get]
func (h *Handler) GetUsersByRole(c echo.Context) error { func (h *Handler) GetUsersByRole(c echo.Context) error {
roleStr := c.Param("role") roleStr := c.Param("role")
role := UserRole(roleStr) role := UserRole(roleStr)
+3 -3
View File
@@ -5,8 +5,8 @@ import (
"strings" "strings"
"tm/pkg/response" "tm/pkg/response"
"github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"go.mongodb.org/mongo-driver/bson/primitive"
) )
// AuthMiddleware validates JWT access tokens and extracts user information // AuthMiddleware validates JWT access tokens and extracts user information
@@ -44,7 +44,7 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
} }
// Extract user information from token // Extract user information from token
userID, err := uuid.Parse(validationResult.Claims.UserID) userID, err := primitive.ObjectIDFromHex(validationResult.Claims.UserID)
if err != nil { if err != nil {
h.logger.Error("Failed to parse user ID from token", map[string]interface{}{ h.logger.Error("Failed to parse user ID from token", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -53,7 +53,7 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
} }
// Store user information in context for handlers to use // Store user information in context for handlers to use
c.Set("user_id", userID.String()) c.Set("user_id", userID.Hex())
c.Set("user_email", validationResult.Claims.Email) c.Set("user_email", validationResult.Claims.Email)
c.Set("user_role", validationResult.Claims.Role) c.Set("user_role", validationResult.Claims.Role)
c.Set("company_id", validationResult.Claims.CompanyID) c.Set("company_id", validationResult.Claims.CompanyID)
+9 -9
View File
@@ -8,7 +8,7 @@ import (
"tm/pkg/authorization" "tm/pkg/authorization"
"tm/pkg/logger" "tm/pkg/logger"
"github.com/google/uuid" "go.mongodb.org/mongo-driver/bson/primitive"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -118,7 +118,7 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea
"email": user.Email, "email": user.Email,
"username": user.Username, "username": user.Username,
"role": user.Role, "role": user.Role,
"created_by": *createdBy, "created_by": createdBy,
}) })
return user, nil return user, nil
@@ -160,7 +160,7 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse
} }
// Update last login // Update last login
err = s.repository.UpdateLastLogin(ctx, user.ID) err = s.repository.UpdateLastLogin(ctx, user.ID.Hex())
if err != nil { if err != nil {
s.logger.Error("Failed to update last login", map[string]interface{}{ s.logger.Error("Failed to update last login", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -175,7 +175,7 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse
} }
tokenPair, err := s.authService.GenerateTokenPair( tokenPair, err := s.authService.GenerateTokenPair(
user.ID, user.ID.Hex(),
user.Email, user.Email,
string(user.Role), string(user.Role),
companyID, companyID,
@@ -230,7 +230,7 @@ func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*A
} }
// Get user information // Get user information
userID, err := uuid.Parse(validationResult.Claims.UserID) userID, err := primitive.ObjectIDFromHex(validationResult.Claims.UserID)
if err != nil { if err != nil {
s.logger.Error("Failed to parse user ID from token", map[string]interface{}{ s.logger.Error("Failed to parse user ID from token", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -238,11 +238,11 @@ func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*A
return nil, errors.New("invalid token format") return nil, errors.New("invalid token format")
} }
user, err := s.repository.GetByID(ctx, userID.String()) user, err := s.repository.GetByID(ctx, userID.Hex())
if err != nil { if err != nil {
s.logger.Error("Failed to get user for token refresh", map[string]interface{}{ s.logger.Error("Failed to get user for token refresh", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"user_id": userID.String(), "user_id": userID.Hex(),
}) })
return nil, errors.New("user not found") return nil, errors.New("user not found")
} }
@@ -420,7 +420,7 @@ func (s *userService) UpdateUser(ctx context.Context, id string, form *UpdateUse
if form.Username != nil { if form.Username != nil {
// Check if username already exists // Check if username already exists
existingUser, _ := s.repository.GetByUsername(ctx, *form.Username) existingUser, _ := s.repository.GetByUsername(ctx, *form.Username)
if existingUser != nil && existingUser.ID != id { if existingUser != nil && existingUser.ID.Hex() != id {
return nil, errors.New("username already exists") return nil, errors.New("username already exists")
} }
user.Username = *form.Username user.Username = *form.Username
@@ -429,7 +429,7 @@ func (s *userService) UpdateUser(ctx context.Context, id string, form *UpdateUse
if form.Email != nil { if form.Email != nil {
// Check if email already exists // Check if email already exists
existingUser, _ := s.repository.GetByEmail(ctx, *form.Email) existingUser, _ := s.repository.GetByEmail(ctx, *form.Email)
if existingUser != nil && existingUser.ID != id { if existingUser != nil && existingUser.ID.Hex() != id {
return nil, errors.New("email already exists") return nil, errors.New("email already exists")
} }
user.Email = *form.Email user.Email = *form.Email
+1 -1
View File
@@ -56,7 +56,7 @@ The logger interface remains the same - **no code changes required** in existing
```go ```go
// All existing code continues to work // All existing code continues to work
log.Info("User logged in", map[string]interface{}{ log.Info("User logged in", map[string]interface{}{
"user_id": user.ID.String(), "user_id": user.ID.Hex(),
"email": user.Email, "email": user.Email,
}) })
+9 -5
View File
@@ -1,5 +1,9 @@
package mongo package mongo
import (
"go.mongodb.org/mongo-driver/bson/primitive"
)
// Logger defines the interface for logging operations // Logger defines the interface for logging operations
type Logger interface { type Logger interface {
Info(message string, fields map[string]interface{}) Info(message string, fields map[string]interface{})
@@ -28,19 +32,19 @@ type IDSetter interface {
// Model provides a common base for MongoDB documents // Model provides a common base for MongoDB documents
type Model struct { type Model struct {
ID string `bson:"_id,omitempty" json:"id,omitempty"` ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
CreatedAt int64 `bson:"created_at" json:"created_at"` CreatedAt int64 `bson:"created_at" json:"created_at"`
UpdatedAt int64 `bson:"updated_at" json:"updated_at"` UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
} }
// GetID returns the document ID // GetID returns the document ID
func (b *Model) GetID() string { func (b *Model) GetID() string {
return b.ID return b.ID.Hex()
} }
// SetID sets the document ID // SetID sets the document ID
func (b *Model) SetID(id string) { func (b *Model) SetID(id string) {
b.ID = id b.ID, _ = primitive.ObjectIDFromHex(id)
} }
// SetCreatedAt sets the creation timestamp // SetCreatedAt sets the creation timestamp
-538
View File
@@ -1,538 +0,0 @@
package mongo
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
// TestUser represents a test user model
type TestUser struct {
Model
Email string `bson:"email" json:"email"`
FirstName string `bson:"first_name" json:"first_name"`
LastName string `bson:"last_name" json:"last_name"`
Age int `bson:"age" json:"age"`
IsActive bool `bson:"is_active" json:"is_active"`
}
// TestLogger implements Logger for testing
type TestLogger struct{}
func (l *TestLogger) Info(message string, fields map[string]interface{}) {}
func (l *TestLogger) Error(message string, fields map[string]interface{}) {}
func (l *TestLogger) Debug(message string, fields map[string]interface{}) {}
func (l *TestLogger) Warn(message string, fields map[string]interface{}) {}
// setupTestConnection creates a test connection manager
func setupTestConnection(t *testing.T) *ConnectionManager {
config := ConnectionConfig{
URI: "mongodb://localhost:27017",
Database: "test_database",
}
logger := &TestLogger{}
connManager, err := NewConnectionManager(config, logger)
require.NoError(t, err)
return connManager
}
// cleanupTestData cleans up test data
func cleanupTestData(t *testing.T, connManager *ConnectionManager, collectionName string) {
collection := connManager.GetCollection(collectionName)
_, err := collection.DeleteMany(context.Background(), bson.M{})
require.NoError(t, err)
}
func TestRepository_Create(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
user := &TestUser{
Email: "test@example.com",
FirstName: "Test",
LastName: "User",
Age: 25,
IsActive: true,
}
err := repo.Create(context.Background(), user)
require.NoError(t, err)
assert.NotEmpty(t, user.ID)
assert.NotZero(t, user.CreatedAt)
assert.NotZero(t, user.UpdatedAt)
}
func TestRepository_FindByID(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Create a user
user := &TestUser{
Email: "test@example.com",
FirstName: "Test",
LastName: "User",
Age: 25,
IsActive: true,
}
err := repo.Create(context.Background(), user)
require.NoError(t, err)
// Find by ID
foundUser, err := repo.FindByID(context.Background(), user.ID)
require.NoError(t, err)
assert.Equal(t, user.Email, foundUser.Email)
assert.Equal(t, user.FirstName, foundUser.FirstName)
// Test not found
_, err = repo.FindByID(context.Background(), "507f1f77bcf86cd799439011")
assert.ErrorIs(t, err, ErrDocumentNotFound)
}
func TestRepository_FindOne(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Create a user
user := &TestUser{
Email: "test@example.com",
FirstName: "Test",
LastName: "User",
Age: 25,
IsActive: true,
}
err := repo.Create(context.Background(), user)
require.NoError(t, err)
// Find by email
foundUser, err := repo.FindOne(context.Background(), bson.M{"email": "test@example.com"})
require.NoError(t, err)
assert.Equal(t, user.Email, foundUser.Email)
// Test not found
_, err = repo.FindOne(context.Background(), bson.M{"email": "nonexistent@example.com"})
assert.ErrorIs(t, err, ErrDocumentNotFound)
}
func TestRepository_Update(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Create a user
user := &TestUser{
Email: "test@example.com",
FirstName: "Test",
LastName: "User",
Age: 25,
IsActive: true,
}
err := repo.Create(context.Background(), user)
require.NoError(t, err)
originalUpdatedAt := user.UpdatedAt
// Update user
user.FirstName = "Updated"
user.Age = 30
time.Sleep(1 * time.Second) // Ensure timestamp difference
err = repo.Update(context.Background(), user)
require.NoError(t, err)
// Verify update
foundUser, err := repo.FindByID(context.Background(), user.ID)
require.NoError(t, err)
assert.Equal(t, "Updated", foundUser.FirstName)
assert.Equal(t, 30, foundUser.Age)
assert.Greater(t, foundUser.UpdatedAt, originalUpdatedAt)
}
func TestRepository_Delete(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Create a user
user := &TestUser{
Email: "test@example.com",
FirstName: "Test",
LastName: "User",
Age: 25,
IsActive: true,
}
err := repo.Create(context.Background(), user)
require.NoError(t, err)
// Delete user
err = repo.Delete(context.Background(), user.ID)
require.NoError(t, err)
// Verify deletion
_, err = repo.FindByID(context.Background(), user.ID)
assert.ErrorIs(t, err, ErrDocumentNotFound)
}
func TestRepository_FindAll_OffsetPagination(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Create multiple users
users := []TestUser{
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
{Email: "user4@example.com", FirstName: "User4", Age: 40, IsActive: true},
{Email: "user5@example.com", FirstName: "User5", Age: 45, IsActive: true},
}
err := repo.BulkCreate(context.Background(), users)
require.NoError(t, err)
// Test offset-based pagination
pagination := NewPaginationBuilder().
Limit(2).
Skip(1).
SortAsc("created_at").
Build()
results, err := repo.FindAll(context.Background(), bson.M{}, pagination)
require.NoError(t, err)
assert.Len(t, results.Items, 2)
assert.Equal(t, int64(5), results.TotalCount)
assert.False(t, results.HasMore) // Should be false since we're using offset
}
func TestRepository_FindAll_CursorPagination(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Create multiple users
users := []TestUser{
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
{Email: "user4@example.com", FirstName: "User4", Age: 40, IsActive: true},
{Email: "user5@example.com", FirstName: "User5", Age: 45, IsActive: true},
}
err := repo.BulkCreate(context.Background(), users)
require.NoError(t, err)
// Test cursor-based pagination
pagination := NewPaginationBuilder().
Limit(2).
SortDesc("created_at").
Build()
firstPage, err := repo.FindAll(context.Background(), bson.M{}, pagination)
require.NoError(t, err)
assert.Len(t, firstPage.Items, 2)
assert.Equal(t, int64(5), firstPage.TotalCount)
assert.True(t, firstPage.HasMore)
assert.NotEmpty(t, firstPage.NextCursor)
// Get second page
secondPagePagination := NewPaginationBuilder().
Limit(2).
SortDesc("created_at").
Cursor(firstPage.NextCursor).
Build()
secondPage, err := repo.FindAll(context.Background(), bson.M{}, secondPagePagination)
require.NoError(t, err)
assert.Len(t, secondPage.Items, 2)
assert.Equal(t, int64(5), secondPage.TotalCount)
assert.True(t, secondPage.HasMore)
assert.NotEmpty(t, secondPage.NextCursor)
}
func TestRepository_Count(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Create users
users := []TestUser{
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
}
err := repo.BulkCreate(context.Background(), users)
require.NoError(t, err)
// Count all users
totalCount, err := repo.Count(context.Background(), bson.M{})
require.NoError(t, err)
assert.Equal(t, int64(3), totalCount)
// Count active users
activeCount, err := repo.Count(context.Background(), bson.M{"is_active": true})
require.NoError(t, err)
assert.Equal(t, int64(2), activeCount)
}
func TestRepository_BulkOperations(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Test bulk create
users := []TestUser{
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
}
err := repo.BulkCreate(context.Background(), users)
require.NoError(t, err)
// Verify bulk create
count, err := repo.Count(context.Background(), bson.M{})
require.NoError(t, err)
assert.Equal(t, int64(3), count)
// Test bulk update
updateFilter := bson.M{"is_active": false}
update := bson.M{"$set": bson.M{"age": 50}}
err = repo.BulkUpdate(context.Background(), updateFilter, update)
require.NoError(t, err)
// Verify bulk update
updatedUser, err := repo.FindOne(context.Background(), bson.M{"email": "user3@example.com"})
require.NoError(t, err)
assert.Equal(t, 50, updatedUser.Age)
// Test bulk delete
deleteFilter := bson.M{"is_active": false}
err = repo.BulkDelete(context.Background(), deleteFilter)
require.NoError(t, err)
// Verify bulk delete
count, err = repo.Count(context.Background(), bson.M{})
require.NoError(t, err)
assert.Equal(t, int64(2), count)
}
func TestRepository_Aggregate(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Create users
users := []TestUser{
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
}
err := repo.BulkCreate(context.Background(), users)
require.NoError(t, err)
// Test aggregation
pipeline := mongo.Pipeline{
{{Key: "$match", Value: bson.M{"is_active": true}}},
{{Key: "$group", Value: bson.M{
"_id": nil,
"count": bson.M{"$sum": 1},
"avgAge": bson.M{"$avg": "$age"},
}}},
}
results, err := repo.Aggregate(context.Background(), pipeline)
require.NoError(t, err)
assert.Len(t, results, 1)
result := results[0]
assert.Equal(t, int64(2), result["count"])
assert.Equal(t, 27.5, result["avgAge"])
}
func TestRepository_QueryBuilder(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Create users
users := []TestUser{
{Email: "user1@example.com", FirstName: "User1", Age: 25, IsActive: true},
{Email: "user2@example.com", FirstName: "User2", Age: 30, IsActive: true},
{Email: "user3@example.com", FirstName: "User3", Age: 35, IsActive: false},
}
err := repo.BulkCreate(context.Background(), users)
require.NoError(t, err)
// Test QueryBuilder
query := NewQueryBuilder().
Where("is_active", true).
WhereGreaterThan("age", 20).
WhereLessThan("age", 35).
Build()
results, err := repo.FindMany(context.Background(), query, 10)
require.NoError(t, err)
assert.Len(t, results, 2)
// Test complex query with AND/OR
complexQuery := NewQueryBuilder().
WhereAnd(
bson.M{"is_active": true},
bson.M{"age": bson.M{"$gte": 25}},
).
WhereOr(
bson.M{"email": "user1@example.com"},
bson.M{"email": "user2@example.com"},
).
Build()
complexResults, err := repo.FindMany(context.Background(), complexQuery, 10)
require.NoError(t, err)
assert.Len(t, complexResults, 2)
}
func TestRepository_Validation(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Test invalid pagination
invalidPagination := Pagination{
Limit: -1, // Invalid
SortOrder: 2, // Invalid
}
_, err := repo.FindAll(context.Background(), bson.M{}, invalidPagination)
assert.Error(t, err)
assert.ErrorIs(t, err, ErrInvalidPagination)
// Test invalid cursor
invalidCursorPagination := NewPaginationBuilder().
Limit(10).
Cursor("invalid-base64").
Build()
_, err = repo.FindAll(context.Background(), bson.M{}, invalidCursorPagination)
assert.Error(t, err)
assert.ErrorIs(t, err, ErrInvalidCursor)
}
func TestRepository_IndexManagement(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
// Test creating indexes
indexes := []Index{
*CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
*NewIndex("age_idx", bson.D{{Key: "age", Value: 1}}),
}
err := connManager.CreateIndexes("test_users", indexes)
require.NoError(t, err)
// Test listing indexes
listIndexes, err := connManager.ListIndexes("test_users")
require.NoError(t, err)
assert.GreaterOrEqual(t, len(listIndexes), 3) // _id + created_at + our indexes
// Test dropping indexes
err = connManager.DropIndexes("test_users", []string{"age_idx"})
require.NoError(t, err)
}
func TestRepository_ConnectionManagement(t *testing.T) {
config := ConnectionConfig{
URI: "mongodb://localhost:27017",
Database: "test_database",
}
logger := &TestLogger{}
connManager, err := NewConnectionManager(config, logger)
require.NoError(t, err)
// Test ping
err = connManager.Ping(context.Background())
require.NoError(t, err)
// Test get stats
stats, err := connManager.GetStats(context.Background())
require.NoError(t, err)
assert.NotNil(t, stats)
// Test collection stats
collectionStats, err := connManager.GetCollectionStats(context.Background(), "test_users")
require.NoError(t, err)
assert.NotNil(t, collectionStats)
// Test close
err = connManager.Close(context.Background())
require.NoError(t, err)
}
func TestRepository_ErrorHandling(t *testing.T) {
connManager := setupTestConnection(t)
defer connManager.Close(context.Background())
defer cleanupTestData(t, connManager, "test_users")
repo := NewRepository[TestUser](connManager.GetCollection("test_users"), &TestLogger{})
// Test invalid ObjectID
_, err := repo.FindByID(context.Background(), "invalid-id")
assert.Error(t, err)
// Test update non-existent document
nonExistentUser := &TestUser{
Model: Model{ID: "507f1f77bcf86cd799439011"},
Email: "nonexistent@example.com",
}
err = repo.Update(context.Background(), nonExistentUser)
assert.ErrorIs(t, err, ErrDocumentNotFound)
// Test delete non-existent document
err = repo.Delete(context.Background(), "507f1f77bcf86cd799439011")
assert.ErrorIs(t, err, ErrDocumentNotFound)
}