Merge branch 'feature/company' into develop
This commit is contained in:
@@ -47,6 +47,77 @@ This system implements **Clean Architecture** with:
|
||||
- **Time Handling**: Unix timestamps throughout the application
|
||||
- **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)
|
||||
- **Admin-Customers**: Web panel customer management with advanced filtering
|
||||
- **Customers-Authorization**: Mobile app customer authentication and profile access
|
||||
- **Admin-Companies**: Comprehensive company management with search, filtering, and analytics
|
||||
|
||||
#### 🔧 **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
|
||||
|
||||
### Prerequisites
|
||||
|
||||
+2982
-396
File diff suppressed because it is too large
Load Diff
+2982
-395
File diff suppressed because it is too large
Load Diff
+2012
-295
File diff suppressed because it is too large
Load Diff
+14
-9
@@ -7,24 +7,29 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// @Summary Health check
|
||||
// @Description Get server health status
|
||||
// @Tags Health
|
||||
// @Summary Health check endpoint
|
||||
// @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 Admin-Health
|
||||
// @Accept 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]
|
||||
func healthHandler(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, HealthResponse{
|
||||
Status: "healthy",
|
||||
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 {
|
||||
Status string `json:"status"`
|
||||
Time int64 `json:"time"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status" example:"healthy"` // Current health status
|
||||
Time int64 `json:"time" example:"1699123456"` // Current Unix timestamp
|
||||
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
|
||||
}
|
||||
|
||||
+36
-27
@@ -1,48 +1,54 @@
|
||||
package main
|
||||
|
||||
// @title Tender Management API
|
||||
// @version 1.0.0
|
||||
// @description This is the API documentation for the Tender Management System.
|
||||
// @termsOfService http://swagger.io/terms/
|
||||
// @version 2.0.0
|
||||
// @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 https://tender-management.com/terms
|
||||
|
||||
// @contact.name API Support
|
||||
// @contact.url http://www.swagger.io/support
|
||||
// @contact.email support@swagger.io
|
||||
// @contact.name Tender Management API Support
|
||||
// @contact.url https://tender-management.com/support
|
||||
// @contact.email api-support@tender-management.com
|
||||
|
||||
// @license.name Apache 2.0
|
||||
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
// @license.name MIT
|
||||
// @license.url https://opensource.org/licenses/MIT
|
||||
|
||||
// @host localhost:8081
|
||||
// @BasePath /
|
||||
|
||||
// @securityDefinitions.apikey BearerAuth
|
||||
// @in header
|
||||
// @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 Admin-Health
|
||||
// @tag.description System health check operations for monitoring application status and dependencies
|
||||
|
||||
// @tag.name Admin-Authorization
|
||||
// @tag.description User authentication and authorization operations including login, logout, token refresh, and password management for web panel administration
|
||||
|
||||
// @tag.name Admin-Users
|
||||
// @tag.description Administrative user management operations including CRUD operations, role management, status updates, and profile management for web panel administrators
|
||||
|
||||
// @tag.name Admin-Customers
|
||||
// @tag.description Administrative customer management operations for web panel including CRUD operations, company assignment, verification, status management, and comprehensive filtering with pagination
|
||||
|
||||
// @tag.name Admin-Companies
|
||||
// @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 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 Health
|
||||
// @tag.description Health check operations
|
||||
// @tag.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
"tm/internal/company"
|
||||
"tm/internal/customer"
|
||||
"tm/internal/user"
|
||||
|
||||
_ "tm/cmd/web/docs" // This is generated by swag
|
||||
"tm/cmd/web/router"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -80,9 +86,10 @@ func main() {
|
||||
// Initialize repositories with MongoDB connection manager
|
||||
userRepository := user.NewUserRepository(mongoManager, logger)
|
||||
customerRepository := customer.NewCustomerRepository(mongoManager, logger)
|
||||
companyRepository := company.NewCompanyRepository(mongoManager, logger)
|
||||
|
||||
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
||||
"repositories": []string{"customer", "user"},
|
||||
"repositories": []string{"customer", "user", "company"},
|
||||
})
|
||||
|
||||
// Initialize validation service
|
||||
@@ -90,26 +97,28 @@ func main() {
|
||||
|
||||
// Initialize services with repositories
|
||||
userService := user.NewUserService(userRepository, logger, userAuthService, userValidator)
|
||||
customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService)
|
||||
companyService := company.NewCompanyService(companyRepository, logger)
|
||||
customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService)
|
||||
|
||||
logger.Info("Services initialized successfully", map[string]interface{}{
|
||||
"services": []string{"customer", "user"},
|
||||
"services": []string{"customer", "user", "company"},
|
||||
})
|
||||
|
||||
// Initialize handlers with services
|
||||
userHandler := user.NewUserHandler(userService, logger, userValidator, userAuthService)
|
||||
customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger)
|
||||
companyHandler := company.NewHandler(companyService, userHandler, logger)
|
||||
|
||||
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
||||
"handlers": []string{"customer", "user"},
|
||||
"handlers": []string{"customer", "user", "company"},
|
||||
})
|
||||
|
||||
// Initialize HTTP server
|
||||
e := initHTTPServer(conf, logger)
|
||||
|
||||
// Register routes
|
||||
userHandler.RegisterRoutes(e)
|
||||
customerHandler.RegisterRoutes(e)
|
||||
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler)
|
||||
router.RegisterPublicRoutes(e, customerHandler)
|
||||
|
||||
// Start server
|
||||
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
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("/:id", customerHandler.GetCustomerByID)
|
||||
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 := customerGP.Group("")
|
||||
profileGP.Use(customerHandler.AuthMiddleware())
|
||||
profileGP.GET("", customerHandler.GetProfile)
|
||||
profileGP.DELETE("/logout", customerHandler.Logout)
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -90,7 +90,7 @@ internal/{domain}/
|
||||
### Structured Logging
|
||||
```go
|
||||
log.Info("Customer authenticated successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"customer_id": customer.ID.Hex(),
|
||||
"email": customer.Email,
|
||||
"ip": clientIP,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package company
|
||||
|
||||
import (
|
||||
"tm/pkg/mongo"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// CompanyStatus represents company account status
|
||||
type CompanyStatus string
|
||||
|
||||
const (
|
||||
CompanyStatusActive CompanyStatus = "active"
|
||||
CompanyStatusInactive CompanyStatus = "inactive"
|
||||
CompanyStatusSuspended CompanyStatus = "suspended"
|
||||
CompanyStatusPending CompanyStatus = "pending"
|
||||
)
|
||||
|
||||
// CompanyType represents the type of company
|
||||
type CompanyType string
|
||||
|
||||
const (
|
||||
CompanyTypePrivate CompanyType = "private"
|
||||
CompanyTypePublic CompanyType = "public"
|
||||
CompanyTypeGovernment CompanyType = "government"
|
||||
CompanyTypeNgo CompanyType = "ngo"
|
||||
CompanyTypeStartup CompanyType = "startup"
|
||||
)
|
||||
|
||||
// Company represents a company in the tender management system
|
||||
type Company struct {
|
||||
mongo.Model `bson:",inline"`
|
||||
Name string `bson:"name" json:"name"`
|
||||
Type CompanyType `bson:"type" json:"type"`
|
||||
Status CompanyStatus `bson:"status" json:"status"`
|
||||
RegistrationNumber string `bson:"registration_number" json:"registration_number"`
|
||||
TaxID string `bson:"tax_id" json:"tax_id"`
|
||||
Industry string `bson:"industry" json:"industry"`
|
||||
Description *string `bson:"description,omitempty" json:"description,omitempty"`
|
||||
Website *string `bson:"website,omitempty" json:"website,omitempty"`
|
||||
|
||||
// Business information
|
||||
EmployeeCount *int `bson:"employee_count,omitempty" json:"employee_count,omitempty"`
|
||||
AnnualRevenue *float64 `bson:"annual_revenue,omitempty" json:"annual_revenue,omitempty"`
|
||||
FoundedYear *int `bson:"founded_year,omitempty" json:"founded_year,omitempty"`
|
||||
|
||||
// Address information
|
||||
Address *Address `bson:"address,omitempty" json:"address,omitempty"`
|
||||
|
||||
// Contact information
|
||||
ContactPerson *ContactPerson `bson:"contact_person,omitempty" json:"contact_person,omitempty"`
|
||||
Phone *string `bson:"phone,omitempty" json:"phone,omitempty"`
|
||||
Email *string `bson:"email,omitempty" json:"email,omitempty"`
|
||||
|
||||
// Tags for categorization and search
|
||||
Tags *CompanyTags `bson:"tags,omitempty" json:"tags,omitempty"`
|
||||
|
||||
// Verification and compliance
|
||||
IsVerified bool `bson:"is_verified" json:"is_verified"`
|
||||
IsCompliant bool `bson:"is_compliant" json:"is_compliant"`
|
||||
ComplianceNotes *string `bson:"compliance_notes,omitempty" json:"compliance_notes,omitempty"`
|
||||
|
||||
// Settings
|
||||
Language string `bson:"language" json:"language"` // Default: "en"
|
||||
Currency string `bson:"currency" json:"currency"` // Default: "USD"
|
||||
Timezone string `bson:"timezone" json:"timezone"` // Default: "UTC"
|
||||
|
||||
// Audit fields
|
||||
CreatedBy *string `bson:"created_by,omitempty" json:"created_by,omitempty"`
|
||||
UpdatedBy *string `bson:"updated_by,omitempty" json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
// SetID sets the company ID (implements IDSetter interface)
|
||||
func (c *Company) SetID(id string) {
|
||||
c.ID, _ = primitive.ObjectIDFromHex(id)
|
||||
}
|
||||
|
||||
// GetID returns the company ID (implements IDGetter interface)
|
||||
func (c *Company) GetID() string {
|
||||
return c.ID.Hex()
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the created timestamp (implements Timestampable interface)
|
||||
func (c *Company) SetCreatedAt(timestamp int64) {
|
||||
c.CreatedAt = timestamp
|
||||
}
|
||||
|
||||
// SetUpdatedAt sets the updated timestamp (implements Timestampable interface)
|
||||
func (c *Company) SetUpdatedAt(timestamp int64) {
|
||||
c.UpdatedAt = timestamp
|
||||
}
|
||||
|
||||
// GetCreatedAt returns the created timestamp (implements Timestampable interface)
|
||||
func (c *Company) GetCreatedAt() int64 {
|
||||
return c.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt returns the updated timestamp (implements Timestampable interface)
|
||||
func (c *Company) GetUpdatedAt() int64 {
|
||||
return c.UpdatedAt
|
||||
}
|
||||
|
||||
// Address represents a company's address
|
||||
type Address struct {
|
||||
Street string `bson:"street" json:"street"`
|
||||
City string `bson:"city" json:"city"`
|
||||
State string `bson:"state" json:"state"`
|
||||
PostalCode string `bson:"postal_code" json:"postal_code"`
|
||||
Country string `bson:"country" json:"country"`
|
||||
}
|
||||
|
||||
// ContactPerson represents a contact person for the company
|
||||
type ContactPerson struct {
|
||||
FirstName string `bson:"first_name" json:"first_name"`
|
||||
LastName string `bson:"last_name" json:"last_name"`
|
||||
FullName string `bson:"full_name" json:"full_name"`
|
||||
Position string `bson:"position" json:"position"`
|
||||
Email string `bson:"email" json:"email"`
|
||||
Phone string `bson:"phone" json:"phone"`
|
||||
Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"`
|
||||
IsPrimary bool `bson:"is_primary" json:"is_primary"`
|
||||
}
|
||||
|
||||
// CompanyTags represents tags for company categorization and search
|
||||
type CompanyTags struct {
|
||||
// CPV codes (Common Procurement Vocabulary)
|
||||
CPVCodes []string `bson:"cpv_codes,omitempty" json:"cpv_codes,omitempty"`
|
||||
|
||||
// Categories
|
||||
Categories []string `bson:"categories,omitempty" json:"categories,omitempty"`
|
||||
|
||||
// Keywords for search and matching
|
||||
Keywords []string `bson:"keywords,omitempty" json:"keywords,omitempty"`
|
||||
|
||||
// Specializations
|
||||
Specializations []string `bson:"specializations,omitempty" json:"specializations,omitempty"`
|
||||
|
||||
// Certifications
|
||||
Certifications []string `bson:"certifications,omitempty" json:"certifications,omitempty"`
|
||||
}
|
||||
|
||||
// CompanyResponse represents the company data sent in API responses
|
||||
type CompanyResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
RegistrationNumber string `json:"registration_number"`
|
||||
TaxID string `json:"tax_id"`
|
||||
Industry string `json:"industry"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Website *string `json:"website,omitempty"`
|
||||
EmployeeCount *int `json:"employee_count,omitempty"`
|
||||
AnnualRevenue *float64 `json:"annual_revenue,omitempty"`
|
||||
FoundedYear *int `json:"founded_year,omitempty"`
|
||||
Address *Address `json:"address,omitempty"`
|
||||
ContactPerson *ContactPerson `json:"contact_person,omitempty"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Email *string `json:"email,omitempty"`
|
||||
Tags *CompanyTags `json:"tags,omitempty"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
IsCompliant bool `json:"is_compliant"`
|
||||
ComplianceNotes *string `json:"compliance_notes,omitempty"`
|
||||
Language string `json:"language"`
|
||||
Currency string `json:"currency"`
|
||||
Timezone string `json:"timezone"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
CreatedBy *string `json:"created_by,omitempty"`
|
||||
UpdatedBy *string `json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
// ToResponse converts Company to CompanyResponse
|
||||
func (c *Company) ToResponse() *CompanyResponse {
|
||||
return &CompanyResponse{
|
||||
ID: c.ID.Hex(),
|
||||
Name: c.Name,
|
||||
Type: string(c.Type),
|
||||
Status: string(c.Status),
|
||||
RegistrationNumber: c.RegistrationNumber,
|
||||
TaxID: c.TaxID,
|
||||
Industry: c.Industry,
|
||||
Description: c.Description,
|
||||
Website: c.Website,
|
||||
EmployeeCount: c.EmployeeCount,
|
||||
AnnualRevenue: c.AnnualRevenue,
|
||||
FoundedYear: c.FoundedYear,
|
||||
Address: c.Address,
|
||||
ContactPerson: c.ContactPerson,
|
||||
Phone: c.Phone,
|
||||
Email: c.Email,
|
||||
Tags: c.Tags,
|
||||
IsVerified: c.IsVerified,
|
||||
IsCompliant: c.IsCompliant,
|
||||
ComplianceNotes: c.ComplianceNotes,
|
||||
Language: c.Language,
|
||||
Currency: c.Currency,
|
||||
Timezone: c.Timezone,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
CreatedBy: c.CreatedBy,
|
||||
UpdatedBy: c.UpdatedBy,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package company
|
||||
|
||||
// CreateCompanyForm represents the form for creating a new company
|
||||
type CreateCompanyForm struct {
|
||||
Name string `json:"name" valid:"required,length(2|200)"`
|
||||
Type string `json:"type" valid:"required,in(private|public|government|ngo|startup)"`
|
||||
RegistrationNumber string `json:"registration_number" valid:"required,length(5|50)"`
|
||||
TaxID string `json:"tax_id" valid:"required,length(5|50)"`
|
||||
Industry string `json:"industry" valid:"required,length(2|100)"`
|
||||
Description *string `json:"description,omitempty" valid:"optional,length(10|1000)"`
|
||||
Website *string `json:"website,omitempty" valid:"optional,url"`
|
||||
|
||||
// Business information
|
||||
EmployeeCount *int `json:"employee_count,omitempty" valid:"optional,range(1|100000)"`
|
||||
AnnualRevenue *float64 `json:"annual_revenue,omitempty" valid:"optional,range(0|999999999999)"`
|
||||
FoundedYear *int `json:"founded_year,omitempty" valid:"optional,range(1800|2100)"`
|
||||
|
||||
// Address information
|
||||
Address *AddressForm `json:"address,omitempty"`
|
||||
|
||||
// Contact information
|
||||
ContactPerson *ContactPersonForm `json:"contact_person,omitempty"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
|
||||
Email *string `json:"email,omitempty" valid:"optional,email"`
|
||||
|
||||
// Tags for categorization and search
|
||||
Tags *CompanyTagsForm `json:"tags,omitempty"`
|
||||
|
||||
// Settings
|
||||
Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"`
|
||||
Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"`
|
||||
Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"`
|
||||
}
|
||||
|
||||
// UpdateCompanyForm represents the form for updating a company
|
||||
type UpdateCompanyForm struct {
|
||||
Name *string `json:"name,omitempty" valid:"optional,length(2|200)"`
|
||||
Type *string `json:"type,omitempty" valid:"optional,in(private|public|government|ngo|startup)"`
|
||||
RegistrationNumber *string `json:"registration_number,omitempty" valid:"optional,length(5|50)"`
|
||||
TaxID *string `json:"tax_id,omitempty" valid:"optional,length(5|50)"`
|
||||
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
|
||||
Description *string `json:"description,omitempty" valid:"optional,length(10|1000)"`
|
||||
Website *string `json:"website,omitempty" valid:"optional,url"`
|
||||
|
||||
// Business information
|
||||
EmployeeCount *int `json:"employee_count,omitempty" valid:"optional,range(1|100000)"`
|
||||
AnnualRevenue *float64 `json:"annual_revenue,omitempty" valid:"optional,range(0|999999999999)"`
|
||||
FoundedYear *int `json:"founded_year,omitempty" valid:"optional,range(1800|2100)"`
|
||||
|
||||
// Address information
|
||||
Address *AddressForm `json:"address,omitempty"`
|
||||
|
||||
// Contact information
|
||||
ContactPerson *ContactPersonForm `json:"contact_person,omitempty"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
|
||||
Email *string `json:"email,omitempty" valid:"optional,email"`
|
||||
|
||||
// Tags for categorization and search
|
||||
Tags *CompanyTagsForm `json:"tags,omitempty"`
|
||||
|
||||
// Settings
|
||||
Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"`
|
||||
Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"`
|
||||
Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"`
|
||||
}
|
||||
|
||||
// ListCompaniesForm represents the form for listing companies with filters
|
||||
type ListCompaniesForm struct {
|
||||
Search *string `query:"search" valid:"optional"`
|
||||
Type *string `query:"type" valid:"optional,in(private|public|government|ngo|startup)"`
|
||||
Status *string `query:"status" valid:"optional,in(active|inactive|suspended|pending)"`
|
||||
Industry *string `query:"industry" valid:"optional"`
|
||||
IsVerified *bool `query:"is_verified" valid:"optional"`
|
||||
IsCompliant *bool `query:"is_compliant" valid:"optional"`
|
||||
Language *string `query:"language" valid:"optional"`
|
||||
Currency *string `query:"currency" valid:"optional"`
|
||||
CPVCodes []string `query:"cpv_codes" valid:"optional"`
|
||||
Categories []string `query:"categories" valid:"optional"`
|
||||
Keywords []string `query:"keywords" valid:"optional"`
|
||||
Specializations []string `query:"specializations" valid:"optional"`
|
||||
EmployeeCountMin *int `query:"employee_count_min" valid:"optional,min(1)"`
|
||||
EmployeeCountMax *int `query:"employee_count_max" valid:"optional,min(1)"`
|
||||
AnnualRevenueMin *float64 `query:"annual_revenue_min" valid:"optional,min(0)"`
|
||||
AnnualRevenueMax *float64 `query:"annual_revenue_max" valid:"optional,min(0)"`
|
||||
FoundedYearMin *int `query:"founded_year_min" valid:"optional,range(1800|2100)"`
|
||||
FoundedYearMax *int `query:"founded_year_max" valid:"optional,range(1800|2100)"`
|
||||
Limit *int `query:"limit" valid:"optional,range(1|100)"`
|
||||
Offset *int `query:"offset" valid:"optional,min(0)"`
|
||||
SortBy *string `query:"sort_by" valid:"optional,in(name|type|industry|created_at|updated_at|status|employee_count|annual_revenue)"`
|
||||
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"`
|
||||
}
|
||||
|
||||
// UpdateCompanyStatusForm represents the form for updating company status
|
||||
type UpdateCompanyStatusForm struct {
|
||||
Status string `json:"status" valid:"required,in(active|inactive|suspended|pending)"`
|
||||
}
|
||||
|
||||
// UpdateCompanyVerificationForm represents the form for updating company verification status
|
||||
type UpdateCompanyVerificationForm struct {
|
||||
IsVerified bool `json:"is_verified"`
|
||||
IsCompliant bool `json:"is_compliant"`
|
||||
ComplianceNotes *string `json:"compliance_notes,omitempty" valid:"optional,length(0|1000)"`
|
||||
}
|
||||
|
||||
// UpdateCompanyTagsForm represents the form for updating company tags
|
||||
type UpdateCompanyTagsForm struct {
|
||||
Tags *CompanyTagsForm `json:"tags" valid:"required"`
|
||||
}
|
||||
|
||||
// AddTagsForm represents the form for adding tags to a company
|
||||
type AddTagsForm struct {
|
||||
CPVCodes []string `json:"cpv_codes,omitempty" valid:"optional"`
|
||||
Categories []string `json:"categories,omitempty" valid:"optional"`
|
||||
Keywords []string `json:"keywords,omitempty" valid:"optional"`
|
||||
Specializations []string `json:"specializations,omitempty" valid:"optional"`
|
||||
Certifications []string `json:"certifications,omitempty" valid:"optional"`
|
||||
}
|
||||
|
||||
// RemoveTagsForm represents the form for removing tags from a company
|
||||
type RemoveTagsForm struct {
|
||||
CPVCodes []string `json:"cpv_codes,omitempty" valid:"optional"`
|
||||
Categories []string `json:"categories,omitempty" valid:"optional"`
|
||||
Keywords []string `json:"keywords,omitempty" valid:"optional"`
|
||||
Specializations []string `json:"specializations,omitempty" valid:"optional"`
|
||||
Certifications []string `json:"certifications,omitempty" valid:"optional"`
|
||||
}
|
||||
|
||||
// SuspendCompanyForm represents the form for suspending a company
|
||||
type SuspendCompanyForm struct {
|
||||
Reason string `json:"reason" valid:"required,length(10|500)"`
|
||||
}
|
||||
|
||||
// AddressForm represents the form for company address
|
||||
type AddressForm struct {
|
||||
Street string `json:"street" valid:"required,length(5|200)"`
|
||||
City string `json:"city" valid:"required,length(2|100)"`
|
||||
State string `json:"state" valid:"required,length(2|100)"`
|
||||
PostalCode string `json:"postal_code" valid:"required,length(3|20)"`
|
||||
Country string `json:"country" valid:"required,length(2|100)"`
|
||||
}
|
||||
|
||||
// ContactPersonForm represents the form for contact person
|
||||
type ContactPersonForm struct {
|
||||
FirstName string `json:"first_name" valid:"required,length(2|50)"`
|
||||
LastName string `json:"last_name" valid:"required,length(2|50)"`
|
||||
FullName string `json:"full_name" valid:"required,length(2|100)"`
|
||||
Position string `json:"position" valid:"required,length(2|100)"`
|
||||
Email string `json:"email" valid:"required,email"`
|
||||
Phone string `json:"phone" valid:"required,length(10|20)"`
|
||||
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
}
|
||||
|
||||
// CompanyTagsForm represents the form for company tags
|
||||
type CompanyTagsForm struct {
|
||||
CPVCodes []string `json:"cpv_codes,omitempty" valid:"optional"`
|
||||
Categories []string `json:"categories,omitempty" valid:"optional"`
|
||||
Keywords []string `json:"keywords,omitempty" valid:"optional"`
|
||||
Specializations []string `json:"specializations,omitempty" valid:"optional"`
|
||||
Certifications []string `json:"certifications,omitempty" valid:"optional"`
|
||||
}
|
||||
|
||||
// CompanyListResponse represents the response for listing companies
|
||||
type CompanyListResponse struct {
|
||||
Companies []*CompanyResponse `json:"companies"`
|
||||
Total int64 `json:"total"`
|
||||
Limit int `json:"limit"`
|
||||
Offset int `json:"offset"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// Company search and matching DTOs
|
||||
type CompanySearchForm struct {
|
||||
Query *string `query:"q" valid:"optional"`
|
||||
CPVCodes []string `query:"cpv_codes" valid:"optional"`
|
||||
Categories []string `query:"categories" valid:"optional"`
|
||||
Keywords []string `query:"keywords" valid:"optional"`
|
||||
Specializations []string `query:"specializations" valid:"optional"`
|
||||
Industry *string `query:"industry" valid:"optional"`
|
||||
Location *string `query:"location" valid:"optional"`
|
||||
MinEmployees *int `query:"min_employees" valid:"optional,min(1)"`
|
||||
MaxEmployees *int `query:"max_employees" valid:"optional,min(1)"`
|
||||
MinRevenue *float64 `query:"min_revenue" valid:"optional,min(0)"`
|
||||
MaxRevenue *float64 `query:"max_revenue" valid:"optional,min(0)"`
|
||||
IsVerified *bool `query:"is_verified" valid:"optional"`
|
||||
IsCompliant *bool `query:"is_compliant" valid:"optional"`
|
||||
Limit *int `query:"limit" valid:"optional,range(1|100)"`
|
||||
Offset *int `query:"offset" valid:"optional,min(0)"`
|
||||
}
|
||||
|
||||
// Company statistics DTOs
|
||||
type CompanyStatsResponse struct {
|
||||
TotalCompanies int64 `json:"total_companies"`
|
||||
ActiveCompanies int64 `json:"active_companies"`
|
||||
VerifiedCompanies int64 `json:"verified_companies"`
|
||||
CompaniesByType map[string]int64 `json:"companies_by_type"`
|
||||
CompaniesByIndustry map[string]int64 `json:"companies_by_industry"`
|
||||
CompaniesByStatus map[string]int64 `json:"companies_by_status"`
|
||||
TopCategories []CategoryCount `json:"top_categories"`
|
||||
TopCPVCodes []CPVCodeCount `json:"top_cpv_codes"`
|
||||
}
|
||||
|
||||
type CategoryCount struct {
|
||||
Category string `json:"category"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type CPVCodeCount struct {
|
||||
CPVCode string `json:"cpv_code"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
@@ -0,0 +1,759 @@
|
||||
package company
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"tm/internal/user"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Handler handles HTTP requests for company operations
|
||||
type Handler struct {
|
||||
service Service
|
||||
userHandler *user.Handler
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewHandler creates a new company handler
|
||||
func NewHandler(service Service, userHandler *user.Handler, logger logger.Logger) *Handler {
|
||||
return &Handler{
|
||||
service: service,
|
||||
userHandler: userHandler,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCompany creates a new company (Web Panel)
|
||||
// @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.
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param company body CreateCompanyForm true "Company information including type, business details, address, tags, and optional customer assignment"
|
||||
// @Success 201 {object} response.APIResponse{data=CompanyResponse} "Company created successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 409 {object} response.APIResponse "Conflict - Company with this name/registration/tax ID already exists"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies [post]
|
||||
func (h *Handler) CreateCompany(c echo.Context) error {
|
||||
form, err := response.Parse[CreateCompanyForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from JWT token context
|
||||
createdBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
company, err := h.service.CreateCompany(c.Request().Context(), form, &createdBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company with this name already exists" ||
|
||||
err.Error() == "company with this registration number already exists" ||
|
||||
err.Error() == "company with this tax ID already exists" {
|
||||
return response.Conflict(c, err.Error())
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to create company")
|
||||
}
|
||||
|
||||
return response.Created(c, company.ToResponse(), "Company created successfully")
|
||||
}
|
||||
|
||||
// GetCompany retrieves a company by ID (Web Panel)
|
||||
// @Summary Get company by ID
|
||||
// @Description Retrieve detailed company information by company ID
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Success 200 {object} response.APIResponse{data=CompanyResponse} "Company retrieved successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies/{id} [get]
|
||||
func (h *Handler) GetCompany(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
company, err := h.service.GetCompanyByID(c.Request().Context(), id)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to get company")
|
||||
}
|
||||
|
||||
return response.Success(c, company.ToResponse(), "Company retrieved successfully")
|
||||
}
|
||||
|
||||
// UpdateCompany updates a company (Web Panel)
|
||||
// @Summary Update company information
|
||||
// @Description Update company information including business details, address, tags, and customer assignment
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Param company body UpdateCompanyForm true "Company update information"
|
||||
// @Success 200 {object} response.APIResponse{data=CompanyResponse} "Company updated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
|
||||
// @Failure 409 {object} response.APIResponse "Conflict - Company with this name/registration/tax ID already exists"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies/{id} [put]
|
||||
func (h *Handler) UpdateCompany(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
form, err := response.Parse[UpdateCompanyForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from JWT token context
|
||||
updatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
company, err := h.service.UpdateCompany(c.Request().Context(), id, form, &updatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
if err.Error() == "company with this name already exists" ||
|
||||
err.Error() == "company with this registration number already exists" ||
|
||||
err.Error() == "company with this tax ID already exists" {
|
||||
return response.Conflict(c, err.Error())
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to update company")
|
||||
}
|
||||
|
||||
return response.Success(c, company.ToResponse(), "Company updated successfully")
|
||||
}
|
||||
|
||||
// DeleteCompany deletes a company (Web Panel)
|
||||
// @Summary Delete company
|
||||
// @Description Soft delete a company by setting status to inactive
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Success 200 {object} response.APIResponse "Company deleted successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies/{id} [delete]
|
||||
func (h *Handler) DeleteCompany(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
// Get user ID from JWT token context
|
||||
deletedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.DeleteCompany(c.Request().Context(), id, &deletedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to delete company")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Company deleted successfully",
|
||||
}, "Company deleted successfully")
|
||||
}
|
||||
|
||||
// ListCompanies lists companies with filters and pagination (Web Panel)
|
||||
// @Summary List companies with filters and pagination
|
||||
// @Description Retrieve a paginated list of companies with advanced filtering options including search, type, status, industry, verification status, tags, and business criteria. Supports sorting and pagination.
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param search query string false "Search term to filter companies by name, description, or industry"
|
||||
// @Param type query string false "Filter by company type" Enums(private, public, government, ngo, startup)
|
||||
// @Param status query string false "Filter by company status" Enums(active, inactive, suspended, pending)
|
||||
// @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 cpv_codes query array false "Filter by CPV codes"
|
||||
// @Param categories query array false "Filter by categories"
|
||||
// @Param keywords query array false "Filter by keywords"
|
||||
// @Param specializations query array false "Filter by specializations"
|
||||
// @Param employee_count_min query integer false "Minimum employee count"
|
||||
// @Param employee_count_max query integer false "Maximum employee count"
|
||||
// @Param annual_revenue_min query number false "Minimum annual revenue"
|
||||
// @Param annual_revenue_max query number false "Maximum annual revenue"
|
||||
// @Param founded_year_min query integer false "Minimum founded year"
|
||||
// @Param founded_year_max query integer false "Maximum founded year"
|
||||
// @Param limit query integer false "Number of companies per page (1-100)" minimum(1) maximum(100) default(20)
|
||||
// @Param offset query integer false "Number of companies to skip for pagination" minimum(0) default(0)
|
||||
// @Param sort_by query string false "Field to sort by" Enums(name, type, industry, created_at, updated_at, status, employee_count, annual_revenue) default(created_at)
|
||||
// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc)
|
||||
// @Success 200 {object} response.APIResponse{data=CompanyListResponse} "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/companies [get]
|
||||
func (h *Handler) ListCompanies(c echo.Context) error {
|
||||
form, err := response.Parse[ListCompaniesForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
result, err := h.service.ListCompanies(c.Request().Context(), form)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to list companies")
|
||||
}
|
||||
|
||||
return response.Success(c, result, "Companies retrieved successfully")
|
||||
}
|
||||
|
||||
// SearchCompanies searches companies with advanced filters (Web Panel)
|
||||
// @Summary Advanced search for companies
|
||||
// @Description Search companies with advanced filtering capabilities including tags, business criteria, and location
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param q query string false "Search query"
|
||||
// @Param cpv_codes query array false "CPV codes filter"
|
||||
// @Param categories query array false "Categories filter"
|
||||
// @Param keywords query array false "Keywords filter"
|
||||
// @Param specializations query array false "Specializations filter"
|
||||
// @Param industry query string false "Industry filter"
|
||||
// @Param location query string false "Location filter"
|
||||
// @Param min_employees query integer false "Minimum employees"
|
||||
// @Param max_employees query integer false "Maximum employees"
|
||||
// @Param min_revenue query number false "Minimum revenue"
|
||||
// @Param max_revenue query number false "Maximum revenue"
|
||||
// @Param is_verified query boolean false "Verification status"
|
||||
// @Param is_compliant query boolean false "Compliance status"
|
||||
// @Param limit query integer false "Limit" default(20)
|
||||
// @Param offset query integer false "Offset" default(0)
|
||||
// @Success 200 {object} response.APIResponse{data=CompanyListResponse} "Companies search completed"
|
||||
// @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/companies/search [get]
|
||||
func (h *Handler) SearchCompanies(c echo.Context) error {
|
||||
form, err := response.Parse[CompanySearchForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
result, err := h.service.SearchCompanies(c.Request().Context(), form)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to search companies")
|
||||
}
|
||||
|
||||
return response.Success(c, result, "Companies search completed")
|
||||
}
|
||||
|
||||
// UpdateCompanyStatus updates company status (Web Panel)
|
||||
// @Summary Update company status
|
||||
// @Description Update company account status (active, inactive, suspended, pending)
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Param status body UpdateCompanyStatusForm true "New company status"
|
||||
// @Success 200 {object} response.APIResponse "Company status updated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company 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/companies/{id}/status [patch]
|
||||
func (h *Handler) UpdateCompanyStatus(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
form, err := response.Parse[UpdateCompanyStatusForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from JWT token context
|
||||
updatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.UpdateCompanyStatus(c.Request().Context(), id, form, &updatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to update company status")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Company status updated successfully",
|
||||
}, "Company status updated successfully")
|
||||
}
|
||||
|
||||
// UpdateCompanyVerification updates company verification status (Web Panel)
|
||||
// @Summary Update company verification status
|
||||
// @Description Update company verification and compliance status
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Param verification body UpdateCompanyVerificationForm true "Verification status update"
|
||||
// @Success 200 {object} response.APIResponse "Company verification updated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company 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/companies/{id}/verification [patch]
|
||||
func (h *Handler) UpdateCompanyVerification(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
form, err := response.Parse[UpdateCompanyVerificationForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from JWT token context
|
||||
updatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.UpdateCompanyVerification(c.Request().Context(), id, form, &updatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to update company verification")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Company verification updated successfully",
|
||||
}, "Company verification updated successfully")
|
||||
}
|
||||
|
||||
// UpdateCompanyTags updates company tags (Web Panel)
|
||||
// @Summary Update company tags
|
||||
// @Description Update company tags (CPV, categories, keywords, specializations, certifications)
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Param tags body UpdateCompanyTagsForm true "Tags update information"
|
||||
// @Success 200 {object} response.APIResponse "Company tags updated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company 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/companies/{id}/tags [put]
|
||||
func (h *Handler) UpdateCompanyTags(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
form, err := response.Parse[UpdateCompanyTagsForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from JWT token context
|
||||
updatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.UpdateCompanyTags(c.Request().Context(), id, form, &updatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to update company tags")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Company tags updated successfully",
|
||||
}, "Company tags updated successfully")
|
||||
}
|
||||
|
||||
// AddTags adds tags to a company (Web Panel)
|
||||
// @Summary Add tags to company
|
||||
// @Description Add specific tags to a company
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Param tags body AddTagsForm true "Tags to add"
|
||||
// @Success 200 {object} response.APIResponse "Tags added successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company 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/companies/{id}/tags/add [post]
|
||||
func (h *Handler) AddTags(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
form, err := response.Parse[AddTagsForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from JWT token context
|
||||
updatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.AddTags(c.Request().Context(), id, form, &updatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to add tags")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Tags added successfully",
|
||||
}, "Tags added successfully")
|
||||
}
|
||||
|
||||
// RemoveTags removes tags from a company (Web Panel)
|
||||
// @Summary Remove tags from company
|
||||
// @Description Remove specific tags from a company
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Param tags body RemoveTagsForm true "Tags to remove"
|
||||
// @Success 200 {object} response.APIResponse "Tags removed successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company 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/companies/{id}/tags/remove [post]
|
||||
func (h *Handler) RemoveTags(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
form, err := response.Parse[RemoveTagsForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from JWT token context
|
||||
updatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.RemoveTags(c.Request().Context(), id, form, &updatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to remove tags")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Tags removed successfully",
|
||||
}, "Tags removed successfully")
|
||||
}
|
||||
|
||||
// VerifyCompany verifies a company (Web Panel)
|
||||
// @Summary Verify company
|
||||
// @Description Mark a company as verified
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Success 200 {object} response.APIResponse "Company verified successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies/{id}/verify [post]
|
||||
func (h *Handler) VerifyCompany(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
// Get user ID from JWT token context
|
||||
verifiedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.VerifyCompany(c.Request().Context(), id, &verifiedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to verify company")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Company verified successfully",
|
||||
}, "Company verified successfully")
|
||||
}
|
||||
|
||||
// SuspendCompany suspends a company (Web Panel)
|
||||
// @Summary Suspend company
|
||||
// @Description Suspend a company account with reason
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Param suspension body SuspendCompanyForm true "Suspension information"
|
||||
// @Success 200 {object} response.APIResponse "Company suspended successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company 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/companies/{id}/suspend [post]
|
||||
func (h *Handler) SuspendCompany(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
form, err := response.Parse[SuspendCompanyForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// Get user ID from JWT token context
|
||||
suspendedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.SuspendCompany(c.Request().Context(), id, form.Reason, &suspendedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to suspend company")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Company suspended successfully",
|
||||
}, "Company suspended successfully")
|
||||
}
|
||||
|
||||
// ActivateCompany activates a company (Web Panel)
|
||||
// @Summary Activate company
|
||||
// @Description Activate a suspended company account
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Company ID"
|
||||
// @Success 200 {object} response.APIResponse "Company activated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Company not found"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies/{id}/activate [post]
|
||||
func (h *Handler) ActivateCompany(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
// Get user ID from JWT token context
|
||||
activatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.ActivateCompany(c.Request().Context(), id, &activatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "company not found" {
|
||||
return response.NotFound(c, "Company not found")
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to activate company")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Company activated successfully",
|
||||
}, "Company activated successfully")
|
||||
}
|
||||
|
||||
// GetCompanyStats returns company statistics (Web Panel)
|
||||
// @Summary Get company statistics
|
||||
// @Description Get comprehensive company statistics for dashboard
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.APIResponse{data=CompanyStatsResponse} "Company statistics retrieved successfully"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies/stats [get]
|
||||
func (h *Handler) GetCompanyStats(c echo.Context) error {
|
||||
stats, err := h.service.GetCompanyStats(c.Request().Context())
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to get company statistics")
|
||||
}
|
||||
|
||||
return response.Success(c, stats, "Company statistics retrieved successfully")
|
||||
}
|
||||
|
||||
// GetCompaniesByType retrieves companies by type (Web Panel)
|
||||
// @Summary Get companies by type
|
||||
// @Description Retrieve companies filtered by their type (private, public, government, ngo, startup)
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param type path string true "Company type" Enums(private, public, government, ngo, startup)
|
||||
// @Param limit query integer false "Number of companies per page (1-100)" minimum(1) maximum(100) default(20)
|
||||
// @Param offset query integer false "Number of companies to skip for pagination" minimum(0) default(0)
|
||||
// @Success 200 {object} response.APIResponse{data=[]CompanyResponse,meta=response.Meta} "Companies retrieved successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company type"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies/type/{type} [get]
|
||||
func (h *Handler) GetCompaniesByType(c echo.Context) error {
|
||||
companyTypeStr := c.Param("type")
|
||||
companyType := CompanyType(companyTypeStr)
|
||||
|
||||
// Validate company type
|
||||
if companyType != CompanyTypePrivate && companyType != CompanyTypePublic &&
|
||||
companyType != CompanyTypeGovernment && companyType != CompanyTypeNgo &&
|
||||
companyType != CompanyTypeStartup {
|
||||
return response.BadRequest(c, "Invalid company type", "Must be private, public, government, ngo, or startup")
|
||||
}
|
||||
|
||||
limit := 20
|
||||
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
|
||||
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
|
||||
offset = parsedOffset
|
||||
}
|
||||
}
|
||||
|
||||
companies, total, err := h.service.GetCompaniesByType(c.Request().Context(), companyType, limit, offset)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve companies by type")
|
||||
}
|
||||
|
||||
var companyResponses []*CompanyResponse
|
||||
for _, company := range companies {
|
||||
companyResponses = append(companyResponses, company.ToResponse())
|
||||
}
|
||||
|
||||
meta := &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
return response.SuccessWithMeta(c, companyResponses, meta, "Companies retrieved successfully")
|
||||
}
|
||||
|
||||
// GetCompaniesByStatus retrieves companies by status (Web Panel)
|
||||
// @Summary Get companies by status
|
||||
// @Description Retrieve companies filtered by their status (active, inactive, suspended, pending)
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param status path string true "Company status" Enums(active, inactive, suspended, pending)
|
||||
// @Param limit query integer false "Number of companies per page (1-100)" minimum(1) maximum(100) default(20)
|
||||
// @Param offset query integer false "Number of companies to skip for pagination" minimum(0) default(0)
|
||||
// @Success 200 {object} response.APIResponse{data=[]CompanyResponse,meta=response.Meta} "Companies retrieved successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company status"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies/status/{status} [get]
|
||||
func (h *Handler) GetCompaniesByStatus(c echo.Context) error {
|
||||
statusStr := c.Param("status")
|
||||
status := CompanyStatus(statusStr)
|
||||
|
||||
// Validate company status
|
||||
if status != CompanyStatusActive && status != CompanyStatusInactive &&
|
||||
status != CompanyStatusSuspended && status != CompanyStatusPending {
|
||||
return response.BadRequest(c, "Invalid company status", "Must be active, inactive, suspended, or pending")
|
||||
}
|
||||
|
||||
limit := 20
|
||||
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
|
||||
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
|
||||
offset = parsedOffset
|
||||
}
|
||||
}
|
||||
|
||||
companies, total, err := h.service.GetCompaniesByStatus(c.Request().Context(), status, limit, offset)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve companies by status")
|
||||
}
|
||||
|
||||
var companyResponses []*CompanyResponse
|
||||
for _, company := range companies {
|
||||
companyResponses = append(companyResponses, company.ToResponse())
|
||||
}
|
||||
|
||||
meta := &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
return response.SuccessWithMeta(c, companyResponses, meta, "Companies retrieved successfully")
|
||||
}
|
||||
|
||||
// GetCompaniesByIndustry retrieves companies by industry (Web Panel)
|
||||
// @Summary Get companies by industry
|
||||
// @Description Retrieve companies filtered by their industry
|
||||
// @Tags Admin-Companies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param industry path string true "Industry"
|
||||
// @Param limit query integer false "Number of companies per page (1-100)" minimum(1) maximum(100) default(20)
|
||||
// @Param offset query integer false "Number of companies to skip for pagination" minimum(0) default(0)
|
||||
// @Success 200 {object} response.APIResponse{data=[]CompanyResponse,meta=response.Meta} "Companies retrieved successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid industry"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/companies/industry/{industry} [get]
|
||||
func (h *Handler) GetCompaniesByIndustry(c echo.Context) error {
|
||||
industry := c.Param("industry")
|
||||
|
||||
limit := 20
|
||||
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
|
||||
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
|
||||
offset = parsedOffset
|
||||
}
|
||||
}
|
||||
|
||||
companies, total, err := h.service.GetCompaniesByIndustry(c.Request().Context(), industry, limit, offset)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve companies by industry")
|
||||
}
|
||||
|
||||
var companyResponses []*CompanyResponse
|
||||
for _, company := range companies {
|
||||
companyResponses = append(companyResponses, company.ToResponse())
|
||||
}
|
||||
|
||||
meta := &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
return response.SuccessWithMeta(c, companyResponses, meta, "Companies retrieved successfully")
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
package company
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
"tm/pkg/logger"
|
||||
mongopkg "tm/pkg/mongo"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// Repository defines the interface for company data operations
|
||||
type Repository interface {
|
||||
Create(ctx context.Context, company *Company) error
|
||||
GetByID(ctx context.Context, id string) (*Company, error)
|
||||
GetByName(ctx context.Context, name string) (*Company, error)
|
||||
GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Company, error)
|
||||
GetByTaxID(ctx context.Context, taxID string) (*Company, error)
|
||||
GetByIDs(ctx context.Context, ids []string) ([]*Company, error)
|
||||
Update(ctx context.Context, company *Company) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
List(ctx context.Context, limit, offset int) ([]*Company, error)
|
||||
Search(ctx context.Context, search string, companyType *string, status *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, cpvCodes []string, categories []string, keywords []string, specializations []string, employeeCountMin *int, employeeCountMax *int, annualRevenueMin *float64, annualRevenueMax *float64, foundedYearMin *int, foundedYearMax *int, limit, offset int, sortBy, sortOrder string) ([]*Company, error)
|
||||
Count(ctx context.Context) (int64, error)
|
||||
CountByType(ctx context.Context, companyType CompanyType) (int64, error)
|
||||
CountByStatus(ctx context.Context, status CompanyStatus) (int64, error)
|
||||
CountByIndustry(ctx context.Context, industry string) (int64, error)
|
||||
CountVerified(ctx context.Context) (int64, error)
|
||||
UpdateStatus(ctx context.Context, id string, status CompanyStatus) error
|
||||
UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error
|
||||
UpdateTags(ctx context.Context, id string, tags *CompanyTags) error
|
||||
AddTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error
|
||||
RemoveTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error
|
||||
GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error)
|
||||
SearchByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error)
|
||||
}
|
||||
|
||||
// companyRepository implements the Repository interface using the MongoDB ORM
|
||||
type companyRepository struct {
|
||||
ormRepo mongopkg.Repository[Company]
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCompanyRepository creates a new company repository
|
||||
func NewCompanyRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
|
||||
// Create indexes using the ORM's index management
|
||||
indexes := []mongopkg.Index{
|
||||
*mongopkg.CreateUniqueIndex("name_idx", bson.D{{Key: "name", Value: 1}}),
|
||||
*mongopkg.CreateUniqueIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
|
||||
*mongopkg.CreateUniqueIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}),
|
||||
*mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}),
|
||||
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
||||
*mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}),
|
||||
*mongopkg.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}),
|
||||
*mongopkg.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}),
|
||||
*mongopkg.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}),
|
||||
*mongopkg.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}),
|
||||
*mongopkg.NewIndex("employee_count_idx", bson.D{{Key: "employee_count", Value: 1}}),
|
||||
*mongopkg.NewIndex("annual_revenue_idx", bson.D{{Key: "annual_revenue", Value: 1}}),
|
||||
*mongopkg.NewIndex("founded_year_idx", bson.D{{Key: "founded_year", Value: 1}}),
|
||||
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||
// Tag indexes for efficient search
|
||||
*mongopkg.NewIndex("cpv_codes_idx", bson.D{{Key: "tags.cpv_codes", Value: 1}}),
|
||||
*mongopkg.NewIndex("categories_idx", bson.D{{Key: "tags.categories", Value: 1}}),
|
||||
*mongopkg.NewIndex("keywords_idx", bson.D{{Key: "tags.keywords", Value: 1}}),
|
||||
*mongopkg.NewIndex("specializations_idx", bson.D{{Key: "tags.specializations", Value: 1}}),
|
||||
*mongopkg.NewIndex("certifications_idx", bson.D{{Key: "tags.certifications", Value: 1}}),
|
||||
// Text index for search
|
||||
*mongopkg.CreateTextIndex("search_idx", "name", "description", "industry"),
|
||||
}
|
||||
|
||||
// Create indexes
|
||||
err := mongoManager.CreateIndexes("companies", indexes)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create company indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Create ORM repository
|
||||
ormRepo := mongopkg.NewRepository[Company](mongoManager.GetCollection("companies"), logger)
|
||||
|
||||
return &companyRepository{
|
||||
ormRepo: ormRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new company
|
||||
func (r *companyRepository) Create(ctx context.Context, company *Company) error {
|
||||
// Set created/updated timestamps using Unix timestamps
|
||||
now := time.Now().Unix()
|
||||
company.SetCreatedAt(now)
|
||||
company.SetUpdatedAt(now)
|
||||
|
||||
// Use ORM to create company
|
||||
err := r.ormRepo.Create(ctx, company)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to create company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"name": company.Name,
|
||||
"registration_number": company.RegistrationNumber,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Company created successfully", map[string]interface{}{
|
||||
"company_id": company.ID,
|
||||
"name": company.Name,
|
||||
"type": company.Type,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID retrieves a company by ID
|
||||
func (r *companyRepository) GetByID(ctx context.Context, id string) (*Company, error) {
|
||||
company, err := r.ormRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
r.logger.Error("Failed to get company by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return company, nil
|
||||
}
|
||||
|
||||
// GetByName retrieves a company by name
|
||||
func (r *companyRepository) GetByName(ctx context.Context, name string) (*Company, error) {
|
||||
filter := bson.M{"name": name}
|
||||
company, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
r.logger.Error("Failed to get company by name", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"name": name,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return company, nil
|
||||
}
|
||||
|
||||
// GetByRegistrationNumber retrieves a company by registration number
|
||||
func (r *companyRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Company, error) {
|
||||
filter := bson.M{"registration_number": registrationNumber}
|
||||
company, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
r.logger.Error("Failed to get company by registration number", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"registration_number": registrationNumber,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return company, nil
|
||||
}
|
||||
|
||||
// GetByTaxID retrieves a company by tax ID
|
||||
func (r *companyRepository) GetByTaxID(ctx context.Context, taxID string) (*Company, error) {
|
||||
filter := bson.M{"tax_id": taxID}
|
||||
company, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
r.logger.Error("Failed to get company by tax ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tax_id": taxID,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return company, nil
|
||||
}
|
||||
|
||||
// GetByIDs retrieves multiple companies by their IDs
|
||||
func (r *companyRepository) GetByIDs(ctx context.Context, ids []string) ([]*Company, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*Company{}, nil
|
||||
}
|
||||
|
||||
// Convert string IDs to ObjectIDs for MongoDB query
|
||||
objectIDs := make([]interface{}, len(ids))
|
||||
for i, id := range ids {
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
r.logger.Warn("Invalid company ID format", map[string]interface{}{
|
||||
"id": id,
|
||||
"error": err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
objectIDs[i] = objectID
|
||||
}
|
||||
|
||||
if len(objectIDs) == 0 {
|
||||
return []*Company{}, nil
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": bson.M{"$in": objectIDs}}
|
||||
companies, err := r.ormRepo.FindMany(ctx, filter, len(ids))
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get companies by IDs", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"ids": ids,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert to pointer slice
|
||||
result := make([]*Company, len(companies))
|
||||
for i := range companies {
|
||||
result[i] = &companies[i]
|
||||
}
|
||||
|
||||
r.logger.Info("Retrieved companies by IDs", map[string]interface{}{
|
||||
"requested_count": len(ids),
|
||||
"found_count": len(result),
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Update updates a company
|
||||
func (r *companyRepository) Update(ctx context.Context, company *Company) error {
|
||||
// Set updated timestamp using Unix timestamp
|
||||
company.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Use ORM to update company
|
||||
err := r.ormRepo.Update(ctx, company)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": company.ID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Company updated successfully", map[string]interface{}{
|
||||
"company_id": company.ID,
|
||||
"name": company.Name,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a company (soft delete by setting status to inactive)
|
||||
func (r *companyRepository) Delete(ctx context.Context, id string) error {
|
||||
// Get company first
|
||||
company, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update status to inactive
|
||||
company.Status = CompanyStatusInactive
|
||||
company.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, company)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Company deleted successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List retrieves companies with pagination
|
||||
func (r *companyRepository) List(ctx context.Context, limit, offset int) ([]*Company, error) {
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
// Only active companies by default
|
||||
filter := bson.M{"status": bson.M{"$ne": CompanyStatusInactive}}
|
||||
|
||||
// Use ORM to find companies
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to list companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert []Company to []*Company
|
||||
companies := make([]*Company, len(result.Items))
|
||||
for i := range result.Items {
|
||||
companies[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return companies, nil
|
||||
}
|
||||
|
||||
// Search retrieves companies with search and filters
|
||||
func (r *companyRepository) Search(ctx context.Context, search string, companyType *string, status *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, cpvCodes []string, categories []string, keywords []string, specializations []string, employeeCountMin *int, employeeCountMax *int, annualRevenueMin *float64, annualRevenueMax *float64, foundedYearMin *int, foundedYearMax *int, limit, offset int, sortBy, sortOrder string) ([]*Company, error) {
|
||||
// Build filter
|
||||
filter := bson.M{}
|
||||
|
||||
if search != "" {
|
||||
filter["$text"] = bson.M{"$search": search}
|
||||
}
|
||||
|
||||
if companyType != nil {
|
||||
filter["type"] = *companyType
|
||||
}
|
||||
|
||||
if status != nil {
|
||||
filter["status"] = *status
|
||||
}
|
||||
|
||||
if industry != nil {
|
||||
filter["industry"] = *industry
|
||||
}
|
||||
|
||||
if isVerified != nil {
|
||||
filter["is_verified"] = *isVerified
|
||||
}
|
||||
|
||||
if isCompliant != nil {
|
||||
filter["is_compliant"] = *isCompliant
|
||||
}
|
||||
|
||||
if language != nil {
|
||||
filter["language"] = *language
|
||||
}
|
||||
|
||||
if currency != nil {
|
||||
filter["currency"] = *currency
|
||||
}
|
||||
|
||||
// Tag filters
|
||||
if len(cpvCodes) > 0 {
|
||||
filter["tags.cpv_codes"] = bson.M{"$in": cpvCodes}
|
||||
}
|
||||
|
||||
if len(categories) > 0 {
|
||||
filter["tags.categories"] = bson.M{"$in": categories}
|
||||
}
|
||||
|
||||
if len(keywords) > 0 {
|
||||
filter["tags.keywords"] = bson.M{"$in": keywords}
|
||||
}
|
||||
|
||||
if len(specializations) > 0 {
|
||||
filter["tags.specializations"] = bson.M{"$in": specializations}
|
||||
}
|
||||
|
||||
// Range filters
|
||||
if employeeCountMin != nil || employeeCountMax != nil {
|
||||
rangeFilter := bson.M{}
|
||||
if employeeCountMin != nil {
|
||||
rangeFilter["$gte"] = *employeeCountMin
|
||||
}
|
||||
if employeeCountMax != nil {
|
||||
rangeFilter["$lte"] = *employeeCountMax
|
||||
}
|
||||
filter["employee_count"] = rangeFilter
|
||||
}
|
||||
|
||||
if annualRevenueMin != nil || annualRevenueMax != nil {
|
||||
rangeFilter := bson.M{}
|
||||
if annualRevenueMin != nil {
|
||||
rangeFilter["$gte"] = *annualRevenueMin
|
||||
}
|
||||
if annualRevenueMax != nil {
|
||||
rangeFilter["$lte"] = *annualRevenueMax
|
||||
}
|
||||
filter["annual_revenue"] = rangeFilter
|
||||
}
|
||||
|
||||
if foundedYearMin != nil || foundedYearMax != nil {
|
||||
rangeFilter := bson.M{}
|
||||
if foundedYearMin != nil {
|
||||
rangeFilter["$gte"] = *foundedYearMin
|
||||
}
|
||||
if foundedYearMax != nil {
|
||||
rangeFilter["$lte"] = *foundedYearMax
|
||||
}
|
||||
filter["founded_year"] = rangeFilter
|
||||
}
|
||||
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset)
|
||||
|
||||
// Set sort
|
||||
if sortBy != "" {
|
||||
if sortOrder == "desc" {
|
||||
pagination.SortDesc(sortBy)
|
||||
} else {
|
||||
pagination.SortAsc(sortBy)
|
||||
}
|
||||
} else {
|
||||
pagination.SortDesc("created_at") // Default sort
|
||||
}
|
||||
|
||||
// Use ORM to find companies
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build())
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to search companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"search": search,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert []Company to []*Company
|
||||
companies := make([]*Company, len(result.Items))
|
||||
for i := range result.Items {
|
||||
companies[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return companies, nil
|
||||
}
|
||||
|
||||
// Count counts all companies
|
||||
func (r *companyRepository) Count(ctx context.Context) (int64, error) {
|
||||
filter := bson.M{"status": bson.M{"$ne": CompanyStatusInactive}}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByType counts companies by type
|
||||
func (r *companyRepository) CountByType(ctx context.Context, companyType CompanyType) (int64, error) {
|
||||
filter := bson.M{
|
||||
"type": companyType,
|
||||
"status": bson.M{"$ne": CompanyStatusInactive},
|
||||
}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count companies by type", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_type": companyType,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByStatus counts companies by status
|
||||
func (r *companyRepository) CountByStatus(ctx context.Context, status CompanyStatus) (int64, error) {
|
||||
filter := bson.M{"status": status}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count companies by status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"status": status,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByIndustry counts companies by industry
|
||||
func (r *companyRepository) CountByIndustry(ctx context.Context, industry string) (int64, error) {
|
||||
filter := bson.M{
|
||||
"industry": industry,
|
||||
"status": bson.M{"$ne": CompanyStatusInactive},
|
||||
}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count companies by industry", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"industry": industry,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountVerified counts verified companies
|
||||
func (r *companyRepository) CountVerified(ctx context.Context) (int64, error) {
|
||||
filter := bson.M{
|
||||
"is_verified": true,
|
||||
"status": bson.M{"$ne": CompanyStatusInactive},
|
||||
}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count verified companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// UpdateStatus updates company status
|
||||
func (r *companyRepository) UpdateStatus(ctx context.Context, id string, status CompanyStatus) error {
|
||||
// Get company first
|
||||
company, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update status
|
||||
company.Status = status
|
||||
company.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, company)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update company status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
"status": status,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Company status updated successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"status": status,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateVerification updates company verification status
|
||||
func (r *companyRepository) UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error {
|
||||
// Get company first
|
||||
company, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update verification fields
|
||||
company.IsVerified = isVerified
|
||||
company.IsCompliant = isCompliant
|
||||
company.ComplianceNotes = complianceNotes
|
||||
company.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, company)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update company verification", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Company verification updated successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"is_verified": isVerified,
|
||||
"is_compliant": isCompliant,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTags updates company tags
|
||||
func (r *companyRepository) UpdateTags(ctx context.Context, id string, tags *CompanyTags) error {
|
||||
// Get company first
|
||||
company, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update tags
|
||||
company.Tags = tags
|
||||
company.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, company)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update company tags", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Company tags updated successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTags adds tags to a company
|
||||
func (r *companyRepository) AddTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error {
|
||||
// Get company first
|
||||
company, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize tags if nil
|
||||
if company.Tags == nil {
|
||||
company.Tags = &CompanyTags{}
|
||||
}
|
||||
|
||||
// Add tags (avoiding duplicates)
|
||||
company.Tags.CPVCodes = r.addUniqueStrings(company.Tags.CPVCodes, cpvCodes)
|
||||
company.Tags.Categories = r.addUniqueStrings(company.Tags.Categories, categories)
|
||||
company.Tags.Keywords = r.addUniqueStrings(company.Tags.Keywords, keywords)
|
||||
company.Tags.Specializations = r.addUniqueStrings(company.Tags.Specializations, specializations)
|
||||
company.Tags.Certifications = r.addUniqueStrings(company.Tags.Certifications, certifications)
|
||||
|
||||
company.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, company)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to add tags to company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Tags added to company successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTags removes tags from a company
|
||||
func (r *companyRepository) RemoveTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error {
|
||||
// Get company first
|
||||
company, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize tags if nil
|
||||
if company.Tags == nil {
|
||||
company.Tags = &CompanyTags{}
|
||||
}
|
||||
|
||||
// Remove tags
|
||||
company.Tags.CPVCodes = r.removeStrings(company.Tags.CPVCodes, cpvCodes)
|
||||
company.Tags.Categories = r.removeStrings(company.Tags.Categories, categories)
|
||||
company.Tags.Keywords = r.removeStrings(company.Tags.Keywords, keywords)
|
||||
company.Tags.Specializations = r.removeStrings(company.Tags.Specializations, specializations)
|
||||
company.Tags.Certifications = r.removeStrings(company.Tags.Certifications, certifications)
|
||||
|
||||
company.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, company)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to remove tags from company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Tags removed from company successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompanyStats returns company statistics
|
||||
func (r *companyRepository) GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error) {
|
||||
// This would typically use aggregation pipelines for better performance
|
||||
// For now, we'll use basic counts
|
||||
|
||||
totalCompanies, _ := r.Count(ctx)
|
||||
activeCompanies, _ := r.CountByStatus(ctx, CompanyStatusActive)
|
||||
verifiedCompanies, _ := r.CountVerified(ctx)
|
||||
|
||||
// For this implementation, we'll return basic stats
|
||||
// In a real implementation, you'd use MongoDB aggregation pipelines
|
||||
stats := &CompanyStatsResponse{
|
||||
TotalCompanies: totalCompanies,
|
||||
ActiveCompanies: activeCompanies,
|
||||
VerifiedCompanies: verifiedCompanies,
|
||||
CompaniesByType: make(map[string]int64),
|
||||
CompaniesByIndustry: make(map[string]int64),
|
||||
CompaniesByStatus: make(map[string]int64),
|
||||
TopCategories: []CategoryCount{},
|
||||
TopCPVCodes: []CPVCodeCount{},
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// SearchByTags searches companies by tags
|
||||
func (r *companyRepository) SearchByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error) {
|
||||
filter := bson.M{}
|
||||
|
||||
// Build tag filters
|
||||
var tagFilters []bson.M
|
||||
|
||||
if len(cpvCodes) > 0 {
|
||||
tagFilters = append(tagFilters, bson.M{"tags.cpv_codes": bson.M{"$in": cpvCodes}})
|
||||
}
|
||||
|
||||
if len(categories) > 0 {
|
||||
tagFilters = append(tagFilters, bson.M{"tags.categories": bson.M{"$in": categories}})
|
||||
}
|
||||
|
||||
if len(keywords) > 0 {
|
||||
tagFilters = append(tagFilters, bson.M{"tags.keywords": bson.M{"$in": keywords}})
|
||||
}
|
||||
|
||||
if len(specializations) > 0 {
|
||||
tagFilters = append(tagFilters, bson.M{"tags.specializations": bson.M{"$in": specializations}})
|
||||
}
|
||||
|
||||
if len(tagFilters) > 0 {
|
||||
filter["$or"] = tagFilters
|
||||
}
|
||||
|
||||
// Only active companies
|
||||
filter["status"] = bson.M{"$ne": CompanyStatusInactive}
|
||||
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
// Use ORM to find companies
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to search companies by tags", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert []Company to []*Company
|
||||
companies := make([]*Company, len(result.Items))
|
||||
for i := range result.Items {
|
||||
companies[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return companies, nil
|
||||
}
|
||||
|
||||
// Helper functions for tag management
|
||||
func (r *companyRepository) addUniqueStrings(existing []string, new []string) []string {
|
||||
existingMap := make(map[string]bool)
|
||||
for _, s := range existing {
|
||||
existingMap[s] = true
|
||||
}
|
||||
|
||||
result := make([]string, len(existing))
|
||||
copy(result, existing)
|
||||
|
||||
for _, s := range new {
|
||||
if !existingMap[s] {
|
||||
result = append(result, s)
|
||||
existingMap[s] = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *companyRepository) removeStrings(existing []string, toRemove []string) []string {
|
||||
removeMap := make(map[string]bool)
|
||||
for _, s := range toRemove {
|
||||
removeMap[s] = true
|
||||
}
|
||||
|
||||
var result []string
|
||||
for _, s := range existing {
|
||||
if !removeMap[s] {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,809 @@
|
||||
package company
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tm/pkg/logger"
|
||||
)
|
||||
|
||||
// Service defines business logic for company operations
|
||||
type Service interface {
|
||||
// Core company operations
|
||||
CreateCompany(ctx context.Context, form *CreateCompanyForm, createdBy *string) (*Company, error)
|
||||
GetCompanyByID(ctx context.Context, id string) (*Company, error)
|
||||
GetCompaniesByIDs(ctx context.Context, ids []string) ([]*Company, error)
|
||||
UpdateCompany(ctx context.Context, id string, form *UpdateCompanyForm, updatedBy *string) (*Company, error)
|
||||
DeleteCompany(ctx context.Context, id string, deletedBy *string) error
|
||||
|
||||
// Company listing and search
|
||||
ListCompanies(ctx context.Context, form *ListCompaniesForm) (*CompanyListResponse, error)
|
||||
SearchCompanies(ctx context.Context, form *CompanySearchForm) (*CompanyListResponse, error)
|
||||
SearchCompaniesByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error)
|
||||
|
||||
// Company management
|
||||
UpdateCompanyStatus(ctx context.Context, id string, form *UpdateCompanyStatusForm, updatedBy *string) error
|
||||
UpdateCompanyVerification(ctx context.Context, id string, form *UpdateCompanyVerificationForm, updatedBy *string) error
|
||||
|
||||
// Tag management
|
||||
UpdateCompanyTags(ctx context.Context, id string, form *UpdateCompanyTagsForm, updatedBy *string) error
|
||||
AddTags(ctx context.Context, id string, form *AddTagsForm, updatedBy *string) error
|
||||
RemoveTags(ctx context.Context, id string, form *RemoveTagsForm, updatedBy *string) error
|
||||
|
||||
// Business operations
|
||||
VerifyCompany(ctx context.Context, id string, verifiedBy *string) error
|
||||
SuspendCompany(ctx context.Context, id string, reason string, suspendedBy *string) error
|
||||
ActivateCompany(ctx context.Context, id string, activatedBy *string) error
|
||||
|
||||
// Statistics and analytics
|
||||
GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error)
|
||||
|
||||
// Search and filtering helpers
|
||||
GetCompaniesByType(ctx context.Context, companyType CompanyType, limit, offset int) ([]*Company, int64, error)
|
||||
GetCompaniesByStatus(ctx context.Context, status CompanyStatus, limit, offset int) ([]*Company, int64, error)
|
||||
GetCompaniesByIndustry(ctx context.Context, industry string, limit, offset int) ([]*Company, int64, error)
|
||||
}
|
||||
|
||||
// companyService implements the Service interface
|
||||
type companyService struct {
|
||||
repository Repository
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCompanyService creates a new company service
|
||||
func NewCompanyService(repository Repository, logger logger.Logger) Service {
|
||||
return &companyService{
|
||||
repository: repository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCompany creates a new company
|
||||
func (s *companyService) CreateCompany(ctx context.Context, form *CreateCompanyForm, createdBy *string) (*Company, error) {
|
||||
// Check if company name already exists
|
||||
existingCompany, _ := s.repository.GetByName(ctx, form.Name)
|
||||
if existingCompany != nil {
|
||||
return nil, errors.New("company with this name already exists")
|
||||
}
|
||||
|
||||
// Check if registration number already exists
|
||||
existingCompany, _ = s.repository.GetByRegistrationNumber(ctx, form.RegistrationNumber)
|
||||
if existingCompany != nil {
|
||||
return nil, errors.New("company with this registration number already exists")
|
||||
}
|
||||
|
||||
// Check if tax ID already exists
|
||||
existingCompany, _ = s.repository.GetByTaxID(ctx, form.TaxID)
|
||||
if existingCompany != nil {
|
||||
return nil, errors.New("company with this tax ID already exists")
|
||||
}
|
||||
|
||||
// Create company
|
||||
company := &Company{
|
||||
Name: form.Name,
|
||||
Type: CompanyType(form.Type),
|
||||
Status: CompanyStatusPending,
|
||||
RegistrationNumber: form.RegistrationNumber,
|
||||
TaxID: form.TaxID,
|
||||
Industry: form.Industry,
|
||||
Description: form.Description,
|
||||
Website: form.Website,
|
||||
EmployeeCount: form.EmployeeCount,
|
||||
AnnualRevenue: form.AnnualRevenue,
|
||||
FoundedYear: form.FoundedYear,
|
||||
Address: s.convertAddressForm(form.Address),
|
||||
ContactPerson: s.convertContactPersonForm(form.ContactPerson),
|
||||
Phone: form.Phone,
|
||||
Email: form.Email,
|
||||
Tags: s.convertCompanyTagsForm(form.Tags),
|
||||
IsVerified: false,
|
||||
IsCompliant: false,
|
||||
Language: s.getDefaultLanguage(form.Language),
|
||||
Currency: s.getDefaultCurrency(form.Currency),
|
||||
Timezone: s.getDefaultTimezone(form.Timezone),
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
|
||||
// Save to database
|
||||
err := s.repository.Create(ctx, company)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"name": form.Name,
|
||||
"registration_number": form.RegistrationNumber,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Company created successfully", map[string]interface{}{
|
||||
"company_id": company.ID,
|
||||
"name": company.Name,
|
||||
"type": company.Type,
|
||||
"created_by": createdBy,
|
||||
})
|
||||
|
||||
return company, nil
|
||||
}
|
||||
|
||||
// GetCompanyByID retrieves a company by ID
|
||||
func (s *companyService) GetCompanyByID(ctx context.Context, id string) (*Company, error) {
|
||||
company, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get company by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Company retrieved successfully", map[string]interface{}{
|
||||
"company_id": company.ID,
|
||||
"name": company.Name,
|
||||
})
|
||||
|
||||
return company, nil
|
||||
}
|
||||
|
||||
// GetCompaniesByIDs retrieves multiple companies by their IDs
|
||||
func (s *companyService) GetCompaniesByIDs(ctx context.Context, ids []string) ([]*Company, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*Company{}, nil
|
||||
}
|
||||
|
||||
companies, err := s.repository.GetByIDs(ctx, ids)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get companies by IDs", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"ids": ids,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Companies retrieved successfully", map[string]interface{}{
|
||||
"requested_count": len(ids),
|
||||
"found_count": len(companies),
|
||||
})
|
||||
|
||||
return companies, nil
|
||||
}
|
||||
|
||||
// UpdateCompany updates a company
|
||||
func (s *companyService) UpdateCompany(ctx context.Context, id string, form *UpdateCompanyForm, updatedBy *string) (*Company, error) {
|
||||
// Get existing company
|
||||
company, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get company for update", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return nil, errors.New("company not found")
|
||||
}
|
||||
|
||||
// Check if name already exists (if changing name)
|
||||
if form.Name != nil && *form.Name != company.Name {
|
||||
existingCompany, _ := s.repository.GetByName(ctx, *form.Name)
|
||||
if existingCompany != nil {
|
||||
return nil, errors.New("company with this name already exists")
|
||||
}
|
||||
company.Name = *form.Name
|
||||
}
|
||||
|
||||
// Check if registration number already exists (if changing)
|
||||
if form.RegistrationNumber != nil && *form.RegistrationNumber != company.RegistrationNumber {
|
||||
existingCompany, _ := s.repository.GetByRegistrationNumber(ctx, *form.RegistrationNumber)
|
||||
if existingCompany != nil {
|
||||
return nil, errors.New("company with this registration number already exists")
|
||||
}
|
||||
company.RegistrationNumber = *form.RegistrationNumber
|
||||
}
|
||||
|
||||
// Check if tax ID already exists (if changing)
|
||||
if form.TaxID != nil && *form.TaxID != company.TaxID {
|
||||
existingCompany, _ := s.repository.GetByTaxID(ctx, *form.TaxID)
|
||||
if existingCompany != nil {
|
||||
return nil, errors.New("company with this tax ID already exists")
|
||||
}
|
||||
company.TaxID = *form.TaxID
|
||||
}
|
||||
|
||||
// Update fields if provided
|
||||
if form.Type != nil {
|
||||
company.Type = CompanyType(*form.Type)
|
||||
}
|
||||
|
||||
if form.Industry != nil {
|
||||
company.Industry = *form.Industry
|
||||
}
|
||||
|
||||
if form.Description != nil {
|
||||
company.Description = form.Description
|
||||
}
|
||||
|
||||
if form.Website != nil {
|
||||
company.Website = form.Website
|
||||
}
|
||||
|
||||
if form.EmployeeCount != nil {
|
||||
company.EmployeeCount = form.EmployeeCount
|
||||
}
|
||||
|
||||
if form.AnnualRevenue != nil {
|
||||
company.AnnualRevenue = form.AnnualRevenue
|
||||
}
|
||||
|
||||
if form.FoundedYear != nil {
|
||||
company.FoundedYear = form.FoundedYear
|
||||
}
|
||||
|
||||
if form.Address != nil {
|
||||
company.Address = s.convertAddressForm(form.Address)
|
||||
}
|
||||
|
||||
if form.ContactPerson != nil {
|
||||
company.ContactPerson = s.convertContactPersonForm(form.ContactPerson)
|
||||
}
|
||||
|
||||
if form.Phone != nil {
|
||||
company.Phone = form.Phone
|
||||
}
|
||||
|
||||
if form.Email != nil {
|
||||
company.Email = form.Email
|
||||
}
|
||||
|
||||
if form.Tags != nil {
|
||||
company.Tags = s.convertCompanyTagsForm(form.Tags)
|
||||
}
|
||||
|
||||
if form.Language != nil {
|
||||
company.Language = *form.Language
|
||||
}
|
||||
|
||||
if form.Currency != nil {
|
||||
company.Currency = *form.Currency
|
||||
}
|
||||
|
||||
if form.Timezone != nil {
|
||||
company.Timezone = *form.Timezone
|
||||
}
|
||||
|
||||
// Set updated by
|
||||
company.UpdatedBy = updatedBy
|
||||
company.UpdatedAt = time.Now().Unix()
|
||||
|
||||
// Update in database
|
||||
err = s.repository.Update(ctx, company)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return nil, errors.New("failed to update company")
|
||||
}
|
||||
|
||||
s.logger.Info("Company updated successfully", map[string]interface{}{
|
||||
"company_id": company.ID,
|
||||
"name": company.Name,
|
||||
"updated_by": updatedBy,
|
||||
})
|
||||
|
||||
return company, nil
|
||||
}
|
||||
|
||||
// DeleteCompany deletes a company (soft delete)
|
||||
func (s *companyService) DeleteCompany(ctx context.Context, id string, deletedBy *string) error {
|
||||
// Get company to check if exists
|
||||
company, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
// Set updated by before deletion
|
||||
company.UpdatedBy = deletedBy
|
||||
|
||||
// Delete company (soft delete)
|
||||
err = s.repository.Delete(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to delete company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return errors.New("failed to delete company")
|
||||
}
|
||||
|
||||
s.logger.Info("Company deleted successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"deleted_by": deletedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListCompanies lists companies with search and filters
|
||||
func (s *companyService) ListCompanies(ctx context.Context, form *ListCompaniesForm) (*CompanyListResponse, 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
|
||||
}
|
||||
|
||||
// Get companies
|
||||
companies, err := s.repository.Search(ctx, search, form.Type, form.Status, form.Industry, form.IsVerified, form.IsCompliant, form.Language, form.Currency, form.CPVCodes, form.Categories, form.Keywords, form.Specializations, form.EmployeeCountMin, form.EmployeeCountMax, form.AnnualRevenueMin, form.AnnualRevenueMax, form.FoundedYearMin, form.FoundedYearMax, limit, offset, sortBy, sortOrder)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to list companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to list companies")
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
var companyResponses []*CompanyResponse
|
||||
for _, company := range companies {
|
||||
companyResponses = append(companyResponses, company.ToResponse())
|
||||
}
|
||||
|
||||
// TODO: Implement proper count for pagination metadata
|
||||
total := int64(len(companies)) // This should be a separate count query
|
||||
totalPages := (total + int64(limit) - 1) / int64(limit)
|
||||
|
||||
return &CompanyListResponse{
|
||||
Companies: companyResponses,
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
TotalPages: int(totalPages),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchCompanies searches companies with advanced filters
|
||||
func (s *companyService) SearchCompanies(ctx context.Context, form *CompanySearchForm) (*CompanyListResponse, error) {
|
||||
// Set defaults
|
||||
limit := 20
|
||||
if form.Limit != nil {
|
||||
limit = *form.Limit
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if form.Offset != nil {
|
||||
offset = *form.Offset
|
||||
}
|
||||
|
||||
query := ""
|
||||
if form.Query != nil {
|
||||
query = *form.Query
|
||||
}
|
||||
|
||||
// Get companies using search
|
||||
companies, err := s.repository.Search(ctx, query, nil, nil, form.Industry, form.IsVerified, form.IsCompliant, nil, nil, form.CPVCodes, form.Categories, form.Keywords, form.Specializations, form.MinEmployees, form.MaxEmployees, form.MinRevenue, form.MaxRevenue, nil, nil, limit, offset, "created_at", "desc")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to search companies", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"query": query,
|
||||
})
|
||||
return nil, errors.New("failed to search companies")
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
var companyResponses []*CompanyResponse
|
||||
for _, company := range companies {
|
||||
companyResponses = append(companyResponses, company.ToResponse())
|
||||
}
|
||||
|
||||
// TODO: Implement proper count for pagination metadata
|
||||
total := int64(len(companies)) // This should be a separate count query
|
||||
totalPages := (total + int64(limit) - 1) / int64(limit)
|
||||
|
||||
return &CompanyListResponse{
|
||||
Companies: companyResponses,
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
TotalPages: int(totalPages),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchCompaniesByTags searches companies by tags
|
||||
func (s *companyService) SearchCompaniesByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error) {
|
||||
companies, err := s.repository.SearchByTags(ctx, cpvCodes, categories, keywords, specializations, limit, offset)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to search companies by tags", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to search companies by tags")
|
||||
}
|
||||
|
||||
return companies, nil
|
||||
}
|
||||
|
||||
// UpdateCompanyStatus updates company status
|
||||
func (s *companyService) UpdateCompanyStatus(ctx context.Context, id string, form *UpdateCompanyStatusForm, updatedBy *string) error {
|
||||
// Get company to check if exists
|
||||
_, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
// Update status
|
||||
status := CompanyStatus(form.Status)
|
||||
err = s.repository.UpdateStatus(ctx, id, status)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update company status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
"status": form.Status,
|
||||
})
|
||||
return errors.New("failed to update company status")
|
||||
}
|
||||
|
||||
s.logger.Info("Company status updated successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"status": form.Status,
|
||||
"updated_by": updatedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCompanyVerification updates company verification status
|
||||
func (s *companyService) UpdateCompanyVerification(ctx context.Context, id string, form *UpdateCompanyVerificationForm, updatedBy *string) error {
|
||||
// Get company to check if exists
|
||||
_, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
// Update verification
|
||||
err = s.repository.UpdateVerification(ctx, id, form.IsVerified, form.IsCompliant, form.ComplianceNotes)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update company verification", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return errors.New("failed to update company verification")
|
||||
}
|
||||
|
||||
s.logger.Info("Company verification updated successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"is_verified": form.IsVerified,
|
||||
"is_compliant": form.IsCompliant,
|
||||
"updated_by": updatedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCompanyTags updates company tags
|
||||
func (s *companyService) UpdateCompanyTags(ctx context.Context, id string, form *UpdateCompanyTagsForm, updatedBy *string) error {
|
||||
// Get company to check if exists
|
||||
_, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
// Update tags
|
||||
tags := s.convertCompanyTagsForm(form.Tags)
|
||||
err = s.repository.UpdateTags(ctx, id, tags)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update company tags", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return errors.New("failed to update company tags")
|
||||
}
|
||||
|
||||
s.logger.Info("Company tags updated successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"updated_by": updatedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTags adds tags to a company
|
||||
func (s *companyService) AddTags(ctx context.Context, id string, form *AddTagsForm, updatedBy *string) error {
|
||||
// Get company to check if exists
|
||||
_, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
// Add tags
|
||||
err = s.repository.AddTags(ctx, id, form.CPVCodes, form.Categories, form.Keywords, form.Specializations, form.Certifications)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to add tags to company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return errors.New("failed to add tags to company")
|
||||
}
|
||||
|
||||
s.logger.Info("Tags added to company successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"updated_by": updatedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTags removes tags from a company
|
||||
func (s *companyService) RemoveTags(ctx context.Context, id string, form *RemoveTagsForm, updatedBy *string) error {
|
||||
// Get company to check if exists
|
||||
_, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
// Remove tags
|
||||
err = s.repository.RemoveTags(ctx, id, form.CPVCodes, form.Categories, form.Keywords, form.Specializations, form.Certifications)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to remove tags from company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return errors.New("failed to remove tags from company")
|
||||
}
|
||||
|
||||
s.logger.Info("Tags removed from company successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"updated_by": updatedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyCompany verifies a company
|
||||
func (s *companyService) VerifyCompany(ctx context.Context, id string, verifiedBy *string) error {
|
||||
// Get company to check if exists
|
||||
company, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
// Update verification
|
||||
err = s.repository.UpdateVerification(ctx, id, true, company.IsCompliant, company.ComplianceNotes)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to verify company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return errors.New("failed to verify company")
|
||||
}
|
||||
|
||||
s.logger.Info("Company verified successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"verified_by": verifiedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SuspendCompany suspends a company
|
||||
func (s *companyService) SuspendCompany(ctx context.Context, id string, reason string, suspendedBy *string) error {
|
||||
// Get company to check if exists
|
||||
_, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
// Update status to suspended
|
||||
err = s.repository.UpdateStatus(ctx, id, CompanyStatusSuspended)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to suspend company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return errors.New("failed to suspend company")
|
||||
}
|
||||
|
||||
s.logger.Info("Company suspended successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"reason": reason,
|
||||
"suspended_by": suspendedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActivateCompany activates a company
|
||||
func (s *companyService) ActivateCompany(ctx context.Context, id string, activatedBy *string) error {
|
||||
// Get company to check if exists
|
||||
_, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return errors.New("company not found")
|
||||
}
|
||||
|
||||
// Update status to active
|
||||
err = s.repository.UpdateStatus(ctx, id, CompanyStatusActive)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to activate company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": id,
|
||||
})
|
||||
return errors.New("failed to activate company")
|
||||
}
|
||||
|
||||
s.logger.Info("Company activated successfully", map[string]interface{}{
|
||||
"company_id": id,
|
||||
"activated_by": activatedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompanyStats returns company statistics
|
||||
func (s *companyService) GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error) {
|
||||
stats, err := s.repository.GetCompanyStats(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get company stats", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to get company statistics")
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetCompaniesByType retrieves companies by type with pagination
|
||||
func (s *companyService) GetCompaniesByType(ctx context.Context, companyType CompanyType, limit, offset int) ([]*Company, int64, error) {
|
||||
// Use search method with type filter
|
||||
typeStr := string(companyType)
|
||||
companies, err := s.repository.Search(ctx, "", &typeStr, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get companies by type", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_type": companyType,
|
||||
})
|
||||
return nil, 0, errors.New("failed to get companies by type")
|
||||
}
|
||||
|
||||
// Get total count
|
||||
total, err := s.repository.CountByType(ctx, companyType)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to count companies by type", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_type": companyType,
|
||||
})
|
||||
return companies, 0, nil // Return companies even if count fails
|
||||
}
|
||||
|
||||
return companies, total, nil
|
||||
}
|
||||
|
||||
// GetCompaniesByStatus retrieves companies by status with pagination
|
||||
func (s *companyService) GetCompaniesByStatus(ctx context.Context, status CompanyStatus, limit, offset int) ([]*Company, int64, error) {
|
||||
// Use search method with status filter
|
||||
statusStr := string(status)
|
||||
companies, err := s.repository.Search(ctx, "", nil, &statusStr, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get companies by status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"status": status,
|
||||
})
|
||||
return nil, 0, errors.New("failed to get companies by status")
|
||||
}
|
||||
|
||||
// Get total count
|
||||
total, err := s.repository.CountByStatus(ctx, status)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to count companies by status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"status": status,
|
||||
})
|
||||
return companies, 0, nil
|
||||
}
|
||||
|
||||
return companies, total, nil
|
||||
}
|
||||
|
||||
// GetCompaniesByIndustry retrieves companies by industry with pagination
|
||||
func (s *companyService) GetCompaniesByIndustry(ctx context.Context, industry string, limit, offset int) ([]*Company, int64, error) {
|
||||
// Use search method with industry filter
|
||||
companies, err := s.repository.Search(ctx, "", nil, nil, &industry, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc")
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get companies by industry", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"industry": industry,
|
||||
})
|
||||
return nil, 0, errors.New("failed to get companies by industry")
|
||||
}
|
||||
|
||||
// Get total count
|
||||
total, err := s.repository.CountByIndustry(ctx, industry)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to count companies by industry", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"industry": industry,
|
||||
})
|
||||
return companies, 0, nil
|
||||
}
|
||||
|
||||
return companies, total, nil
|
||||
}
|
||||
|
||||
// Helper methods for converting forms to domain objects
|
||||
func (s *companyService) convertAddressForm(form *AddressForm) *Address {
|
||||
if form == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Address{
|
||||
Street: form.Street,
|
||||
City: form.City,
|
||||
State: form.State,
|
||||
PostalCode: form.PostalCode,
|
||||
Country: form.Country,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *companyService) convertContactPersonForm(form *ContactPersonForm) *ContactPerson {
|
||||
if form == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ContactPerson{
|
||||
FirstName: form.FirstName,
|
||||
LastName: form.LastName,
|
||||
FullName: form.FullName,
|
||||
Position: form.Position,
|
||||
Email: form.Email,
|
||||
Phone: form.Phone,
|
||||
Mobile: form.Mobile,
|
||||
IsPrimary: form.IsPrimary,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *companyService) convertCompanyTagsForm(form *CompanyTagsForm) *CompanyTags {
|
||||
if form == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &CompanyTags{
|
||||
CPVCodes: form.CPVCodes,
|
||||
Categories: form.Categories,
|
||||
Keywords: form.Keywords,
|
||||
Specializations: form.Specializations,
|
||||
Certifications: form.Certifications,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *companyService) getDefaultLanguage(language *string) string {
|
||||
if language != nil {
|
||||
return *language
|
||||
}
|
||||
return "en"
|
||||
}
|
||||
|
||||
func (s *companyService) getDefaultCurrency(currency *string) string {
|
||||
if currency != nil {
|
||||
return *currency
|
||||
}
|
||||
return "USD"
|
||||
}
|
||||
|
||||
func (s *companyService) getDefaultTimezone(timezone *string) string {
|
||||
if timezone != nil {
|
||||
return *timezone
|
||||
}
|
||||
return "UTC"
|
||||
}
|
||||
+123
-91
@@ -1,7 +1,9 @@
|
||||
package customer
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"tm/pkg/mongo"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// CustomerStatus represents customer account status
|
||||
@@ -24,137 +26,160 @@ const (
|
||||
)
|
||||
|
||||
// Customer represents a customer in the tender management system
|
||||
// A customer can have an associated company for login purposes
|
||||
type Customer struct {
|
||||
ID uuid.UUID `bson:"_id"`
|
||||
Type CustomerType `bson:"type"`
|
||||
Status CustomerStatus `bson:"status"`
|
||||
CompanyID *uuid.UUID `bson:"company_id,omitempty"`
|
||||
mongo.Model `bson:",inline"`
|
||||
Type CustomerType `bson:"type" json:"type"`
|
||||
Status CustomerStatus `bson:"status" json:"status"`
|
||||
|
||||
// Individual customer fields
|
||||
FirstName *string `bson:"first_name,omitempty"`
|
||||
LastName *string `bson:"last_name,omitempty"`
|
||||
FullName *string `bson:"full_name,omitempty"`
|
||||
Username string `bson:"username"` // Username for authentication
|
||||
Email string `bson:"email"`
|
||||
Password string `bson:"password"` // Hashed password for authentication
|
||||
Phone *string `bson:"phone,omitempty"`
|
||||
Mobile *string `bson:"mobile,omitempty"`
|
||||
FirstName *string `bson:"first_name,omitempty" json:"first_name,omitempty"`
|
||||
LastName *string `bson:"last_name,omitempty" json:"last_name,omitempty"`
|
||||
FullName *string `bson:"full_name,omitempty" json:"full_name,omitempty"`
|
||||
Username string `bson:"username" json:"username"` // Username for authentication
|
||||
Email string `bson:"email" json:"email"`
|
||||
Password string `bson:"password" json:"-"` // Hashed password for authentication
|
||||
Phone *string `bson:"phone,omitempty" json:"phone,omitempty"`
|
||||
Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"`
|
||||
|
||||
// Company relationships - each customer can be assigned to multiple companies
|
||||
CompanyIDs []string `bson:"company_ids,omitempty" json:"company_ids,omitempty"`
|
||||
|
||||
// Company customer fields
|
||||
CompanyName *string `bson:"company_name,omitempty"`
|
||||
RegistrationNumber *string `bson:"registration_number,omitempty"`
|
||||
TaxID *string `bson:"tax_id,omitempty"`
|
||||
Industry *string `bson:"industry,omitempty"`
|
||||
RegistrationNumber *string `bson:"registration_number,omitempty" json:"registration_number,omitempty"`
|
||||
TaxID *string `bson:"tax_id,omitempty" json:"tax_id,omitempty"`
|
||||
Industry *string `bson:"industry,omitempty" json:"industry,omitempty"`
|
||||
|
||||
// Address information
|
||||
Address *Address `bson:"address,omitempty"`
|
||||
Address *Address `bson:"address,omitempty" json:"address,omitempty"`
|
||||
|
||||
// Business information
|
||||
BusinessType *string `bson:"business_type,omitempty"`
|
||||
EmployeeCount *int `bson:"employee_count,omitempty"`
|
||||
AnnualRevenue *float64 `bson:"annual_revenue,omitempty"`
|
||||
FoundedYear *int `bson:"founded_year,omitempty"`
|
||||
BusinessType *string `bson:"business_type,omitempty" json:"business_type,omitempty"`
|
||||
EmployeeCount *int `bson:"employee_count,omitempty" json:"employee_count,omitempty"`
|
||||
AnnualRevenue *float64 `bson:"annual_revenue,omitempty" json:"annual_revenue,omitempty"`
|
||||
FoundedYear *int `bson:"founded_year,omitempty" json:"founded_year,omitempty"`
|
||||
|
||||
// Contact person (for company customers)
|
||||
ContactPerson *ContactPerson `bson:"contact_person,omitempty"`
|
||||
ContactPerson *ContactPerson `bson:"contact_person,omitempty" json:"contact_person,omitempty"`
|
||||
|
||||
// Verification and compliance
|
||||
IsVerified bool `bson:"is_verified"`
|
||||
IsCompliant bool `bson:"is_compliant"`
|
||||
ComplianceNotes *string `bson:"compliance_notes,omitempty"`
|
||||
IsVerified bool `bson:"is_verified" json:"is_verified"`
|
||||
IsCompliant bool `bson:"is_compliant" json:"is_compliant"`
|
||||
ComplianceNotes *string `bson:"compliance_notes,omitempty" json:"compliance_notes,omitempty"`
|
||||
|
||||
// Preferences and settings
|
||||
Language string `bson:"language"` // Default: "en"
|
||||
Currency string `bson:"currency"` // Default: "USD"
|
||||
Timezone string `bson:"timezone"` // Default: "UTC"
|
||||
// Settings
|
||||
Language string `bson:"language" json:"language"` // Default: "en"
|
||||
Currency string `bson:"currency" json:"currency"` // Default: "USD"
|
||||
Timezone string `bson:"timezone" json:"timezone"` // Default: "UTC"
|
||||
|
||||
// Audit fields
|
||||
CreatedAt int64 `bson:"created_at"` // Unix timestamp
|
||||
UpdatedAt int64 `bson:"updated_at"` // Unix timestamp
|
||||
CreatedBy *uuid.UUID `bson:"created_by,omitempty"`
|
||||
UpdatedBy *uuid.UUID `bson:"updated_by,omitempty"`
|
||||
CreatedBy *string `bson:"created_by,omitempty" json:"created_by,omitempty"`
|
||||
UpdatedBy *string `bson:"updated_by,omitempty" json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
// SetID sets the customer ID (implements IDSetter interface)
|
||||
func (c *Customer) SetID(id string) {
|
||||
c.ID, _ = primitive.ObjectIDFromHex(id)
|
||||
}
|
||||
|
||||
// GetID returns the customer ID (implements IDGetter interface)
|
||||
func (c *Customer) GetID() string {
|
||||
return c.ID.Hex()
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the created timestamp (implements Timestampable interface)
|
||||
func (c *Customer) SetCreatedAt(timestamp int64) {
|
||||
c.CreatedAt = timestamp
|
||||
}
|
||||
|
||||
// SetUpdatedAt sets the updated timestamp (implements Timestampable interface)
|
||||
func (c *Customer) SetUpdatedAt(timestamp int64) {
|
||||
c.UpdatedAt = timestamp
|
||||
}
|
||||
|
||||
// GetCreatedAt returns the created timestamp (implements Timestampable interface)
|
||||
func (c *Customer) GetCreatedAt() int64 {
|
||||
return c.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt returns the updated timestamp (implements Timestampable interface)
|
||||
func (c *Customer) GetUpdatedAt() int64 {
|
||||
return c.UpdatedAt
|
||||
}
|
||||
|
||||
// Address represents a customer's address
|
||||
type Address struct {
|
||||
Street string `bson:"street"`
|
||||
City string `bson:"city"`
|
||||
State string `bson:"state"`
|
||||
PostalCode string `bson:"postal_code"`
|
||||
Country string `bson:"country"`
|
||||
AddressType string `bson:"address_type"` // billing, shipping, etc.
|
||||
IsDefault bool `bson:"is_default"`
|
||||
Street string `bson:"street" json:"street"`
|
||||
City string `bson:"city" json:"city"`
|
||||
State string `bson:"state" json:"state"`
|
||||
PostalCode string `bson:"postal_code" json:"postal_code"`
|
||||
Country string `bson:"country" json:"country"`
|
||||
AddressType string `bson:"address_type" json:"address_type"` // billing, shipping, etc.
|
||||
IsDefault bool `bson:"is_default" json:"is_default"`
|
||||
}
|
||||
|
||||
// ContactPerson represents a contact person for company customers
|
||||
type ContactPerson struct {
|
||||
FirstName string `bson:"first_name"`
|
||||
LastName string `bson:"last_name"`
|
||||
FullName string `bson:"full_name"`
|
||||
Position string `bson:"position"`
|
||||
Email string `bson:"email"`
|
||||
Phone string `bson:"phone"`
|
||||
Mobile *string `bson:"mobile,omitempty"`
|
||||
IsPrimary bool `bson:"is_primary"`
|
||||
FirstName string `bson:"first_name" json:"first_name"`
|
||||
LastName string `bson:"last_name" json:"last_name"`
|
||||
FullName string `bson:"full_name" json:"full_name"`
|
||||
Position string `bson:"position" json:"position"`
|
||||
Email string `bson:"email" json:"email"`
|
||||
Phone string `bson:"phone" json:"phone"`
|
||||
Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"`
|
||||
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
|
||||
type CustomerResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
CompanyID *string `json:"company_id,omitempty"`
|
||||
FirstName *string `json:"first_name,omitempty"`
|
||||
LastName *string `json:"last_name,omitempty"`
|
||||
FullName *string `json:"full_name,omitempty"`
|
||||
Email string `json:"email"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Mobile *string `json:"mobile,omitempty"`
|
||||
CompanyName *string `json:"company_name,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
FirstName *string `json:"first_name,omitempty"`
|
||||
LastName *string `json:"last_name,omitempty"`
|
||||
FullName *string `json:"full_name,omitempty"`
|
||||
Email string `json:"email"`
|
||||
Phone *string `json:"phone,omitempty"`
|
||||
Mobile *string `json:"mobile,omitempty"`
|
||||
|
||||
// Company relationships
|
||||
Companies []*CompanySummary `json:"companies,omitempty"`
|
||||
|
||||
// Company customer fields
|
||||
RegistrationNumber *string `json:"registration_number,omitempty"`
|
||||
TaxID *string `json:"tax_id,omitempty"`
|
||||
Industry *string `json:"industry,omitempty"`
|
||||
Address *Address `json:"address,omitempty"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
EmployeeCount *int `json:"employee_count,omitempty"`
|
||||
AnnualRevenue *float64 `json:"annual_revenue,omitempty"`
|
||||
FoundedYear *int `json:"founded_year,omitempty"`
|
||||
ContactPerson *ContactPerson `json:"contact_person,omitempty"`
|
||||
TaxID *string `json:"tax_id"`
|
||||
Industry *string `json:"industry"`
|
||||
Address *Address `json:"address"`
|
||||
BusinessType *string `json:"business_type"`
|
||||
EmployeeCount *int `json:"employee_count"`
|
||||
AnnualRevenue *float64 `json:"annual_revenue"`
|
||||
FoundedYear *int `json:"founded_year"`
|
||||
ContactPerson *ContactPerson `json:"contact_person"`
|
||||
IsVerified bool `json:"is_verified"`
|
||||
IsCompliant bool `json:"is_compliant"`
|
||||
ComplianceNotes *string `json:"compliance_notes,omitempty"`
|
||||
ComplianceNotes *string `json:"compliance_notes"`
|
||||
Language string `json:"language"`
|
||||
Username string `json:"username"`
|
||||
Currency string `json:"currency"`
|
||||
Timezone string `json:"timezone"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
CreatedBy *string `json:"created_by,omitempty"`
|
||||
UpdatedBy *string `json:"updated_by,omitempty"`
|
||||
CreatedBy *string `json:"created_by"`
|
||||
UpdatedBy *string `json:"updated_by"`
|
||||
}
|
||||
|
||||
// ToResponse converts Customer to CustomerResponse
|
||||
func (c *Customer) ToResponse() *CustomerResponse {
|
||||
companyID := ""
|
||||
if c.CompanyID != nil {
|
||||
companyID = c.CompanyID.String()
|
||||
}
|
||||
|
||||
createdBy := ""
|
||||
if c.CreatedBy != nil {
|
||||
createdBy = c.CreatedBy.String()
|
||||
}
|
||||
|
||||
updatedBy := ""
|
||||
if c.UpdatedBy != nil {
|
||||
updatedBy = c.UpdatedBy.String()
|
||||
}
|
||||
|
||||
return &CustomerResponse{
|
||||
ID: c.ID.String(),
|
||||
ID: c.ID.Hex(),
|
||||
Type: string(c.Type),
|
||||
Status: string(c.Status),
|
||||
CompanyID: &companyID,
|
||||
FirstName: c.FirstName,
|
||||
LastName: c.LastName,
|
||||
FullName: c.FullName,
|
||||
@@ -162,7 +187,7 @@ func (c *Customer) ToResponse() *CustomerResponse {
|
||||
Email: c.Email,
|
||||
Phone: c.Phone,
|
||||
Mobile: c.Mobile,
|
||||
CompanyName: c.CompanyName,
|
||||
Companies: nil, // Will be populated by service layer
|
||||
RegistrationNumber: c.RegistrationNumber,
|
||||
TaxID: c.TaxID,
|
||||
Industry: c.Industry,
|
||||
@@ -180,7 +205,14 @@ func (c *Customer) ToResponse() *CustomerResponse {
|
||||
Timezone: c.Timezone,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
CreatedBy: &createdBy,
|
||||
UpdatedBy: &updatedBy,
|
||||
CreatedBy: c.CreatedBy,
|
||||
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
|
||||
}
|
||||
|
||||
+45
-24
@@ -2,8 +2,10 @@ package customer
|
||||
|
||||
// CreateCustomerForm represents the form for creating a new customer
|
||||
type CreateCustomerForm struct {
|
||||
Type string `json:"type" valid:"required,in(individual|company|government)"`
|
||||
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
|
||||
Type string `json:"type" valid:"required,in(individual|company|government)"`
|
||||
|
||||
// Company assignments
|
||||
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
|
||||
|
||||
// Individual customer fields
|
||||
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
|
||||
@@ -16,7 +18,6 @@ type CreateCustomerForm struct {
|
||||
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
|
||||
|
||||
// Company customer fields
|
||||
CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"`
|
||||
RegistrationNumber *string `json:"registration_number,omitempty" valid:"optional,length(5|50)"`
|
||||
TaxID *string `json:"tax_id,omitempty" valid:"optional,length(5|50)"`
|
||||
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
|
||||
@@ -41,8 +42,10 @@ type CreateCustomerForm struct {
|
||||
|
||||
// UpdateCustomerForm represents the form for updating a customer
|
||||
type UpdateCustomerForm struct {
|
||||
Type *string `json:"type,omitempty" valid:"optional,in(individual|company|government)"`
|
||||
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
|
||||
Type *string `json:"type,omitempty" valid:"optional,in(individual|company|government)"`
|
||||
|
||||
// Company assignments
|
||||
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
|
||||
|
||||
// Individual customer fields
|
||||
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
|
||||
@@ -99,13 +102,18 @@ type UpdateCustomerStatusForm struct {
|
||||
Status string `json:"status" valid:"required,in(active|inactive|suspended|pending)"`
|
||||
}
|
||||
|
||||
// UpdateCustomerVerificationForm represents the form for updating customer verification
|
||||
// UpdateCustomerVerificationForm represents the form for updating customer verification status
|
||||
type UpdateCustomerVerificationForm struct {
|
||||
IsVerified bool `json:"is_verified"`
|
||||
IsCompliant bool `json:"is_compliant"`
|
||||
ComplianceNotes *string `json:"compliance_notes,omitempty" valid:"optional,length(0|1000)"`
|
||||
}
|
||||
|
||||
// SuspendCustomerForm represents the form for suspending a customer
|
||||
type SuspendCustomerForm struct {
|
||||
Reason string `json:"reason" valid:"required,length(10|500)"`
|
||||
}
|
||||
|
||||
// AddressForm represents the form for customer address
|
||||
type AddressForm struct {
|
||||
Street string `json:"street" valid:"required,length(5|200)"`
|
||||
@@ -140,27 +148,31 @@ type CustomerListResponse struct {
|
||||
|
||||
// Mobile-specific forms (simplified for mobile app)
|
||||
type CreateCustomerMobileForm struct {
|
||||
Type string `json:"type" valid:"required,in(individual|company)"`
|
||||
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
|
||||
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"`
|
||||
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
|
||||
Username string `json:"username" valid:"required,alphanum,length(3|30)"`
|
||||
Email string `json:"email" valid:"required,email"`
|
||||
Password string `json:"password" valid:"required,length(8|128)"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
|
||||
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
|
||||
CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"`
|
||||
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
|
||||
Type string `json:"type" valid:"required,in(individual|company)"`
|
||||
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
|
||||
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"`
|
||||
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
|
||||
Username string `json:"username" valid:"required,alphanum,length(3|30)"`
|
||||
Email string `json:"email" valid:"required,email"`
|
||||
Password string `json:"password" valid:"required,length(8|128)"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
|
||||
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
|
||||
|
||||
// Company assignments
|
||||
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
|
||||
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
|
||||
}
|
||||
|
||||
type UpdateCustomerMobileForm struct {
|
||||
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
|
||||
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"`
|
||||
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
|
||||
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
|
||||
CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"`
|
||||
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
|
||||
FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"`
|
||||
LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"`
|
||||
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
|
||||
Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"`
|
||||
|
||||
// Company assignments
|
||||
CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"`
|
||||
Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"`
|
||||
}
|
||||
|
||||
// Customer Authentication DTOs
|
||||
@@ -199,3 +211,12 @@ type AuthResponse struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
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"`
|
||||
}
|
||||
|
||||
+299
-213
@@ -7,7 +7,6 @@ import (
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
@@ -29,42 +28,10 @@ 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)
|
||||
// @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.
|
||||
// @Tags Customers-Admin
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param customer body CreateCustomerForm true "Customer information including type (individual|company|government), personal details, company info, address, and business details"
|
||||
@@ -81,8 +48,11 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// TODO: Get user ID from JWT token
|
||||
createdBy := uuid.New() // This should come from JWT token
|
||||
// Get user ID from JWT token context
|
||||
createdBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
customer, err := h.service.CreateCustomer(c.Request().Context(), form, &createdBy)
|
||||
if err != nil {
|
||||
@@ -98,48 +68,44 @@ func (h *Handler) CreateCustomer(c echo.Context) error {
|
||||
return response.Created(c, customer.ToResponse(), "Customer created successfully")
|
||||
}
|
||||
|
||||
// GetCustomerByID retrieves a customer by ID
|
||||
// GetCustomerByID retrieves a customer by ID (Web Panel)
|
||||
// @Summary Get customer by ID
|
||||
// @Description Retrieve detailed customer information by unique customer ID. Returns comprehensive customer data including personal details, company information, address, verification status, and audit fields.
|
||||
// @Tags Customers-Admin
|
||||
// @Description Retrieve detailed customer information by customer ID
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Customer UUID" format(uuid)
|
||||
// @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 format"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
|
||||
// @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} [get]
|
||||
func (h *Handler) GetCustomerByID(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid customer ID", err.Error())
|
||||
}
|
||||
|
||||
customer, err := h.service.GetCustomerByID(c.Request().Context(), 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 retrieve customer")
|
||||
return response.InternalServerError(c, "Failed to get customer")
|
||||
}
|
||||
|
||||
return response.Success(c, customer.ToResponse(), "Customer retrieved successfully")
|
||||
return response.Success(c, customer, "Customer retrieved successfully")
|
||||
}
|
||||
|
||||
// UpdateCustomer updates a customer (Web Panel)
|
||||
// @Summary Update customer information
|
||||
// @Description Update existing customer information with comprehensive details. All fields are optional and only provided fields will be updated. This endpoint is used by the web panel for full customer management.
|
||||
// @Tags Customers-Admin
|
||||
// @Description Update customer information including personal details, company information, address, and business details
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Customer UUID" format(uuid)
|
||||
// @Param customer body UpdateCustomerForm true "Customer update information - all fields are optional"
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Param customer body UpdateCustomerForm true "Updated customer information"
|
||||
// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Customer updated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID or input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer not found"
|
||||
// @Failure 409 {object} response.APIResponse "Conflict - Customer with this email/company already exists"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
@@ -147,20 +113,18 @@ func (h *Handler) GetCustomerByID(c echo.Context) error {
|
||||
// @Router /admin/v1/customers/{id} [put]
|
||||
func (h *Handler) UpdateCustomer(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid customer ID", err.Error())
|
||||
}
|
||||
|
||||
form, err := response.Parse[UpdateCustomerForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// TODO: Get user ID from JWT token
|
||||
updatedBy := uuid.New() // This should come from JWT token
|
||||
// Get user ID from JWT token context
|
||||
updatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
customer, err := h.service.UpdateCustomer(c.Request().Context(), id, form, &updatedBy)
|
||||
customer, err := h.service.UpdateCustomer(c.Request().Context(), idStr, form, &updatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "customer not found" {
|
||||
return response.NotFound(c, "Customer not found")
|
||||
@@ -177,30 +141,29 @@ func (h *Handler) UpdateCustomer(c echo.Context) error {
|
||||
return response.Success(c, customer.ToResponse(), "Customer updated successfully")
|
||||
}
|
||||
|
||||
// DeleteCustomer deletes a customer (soft delete)
|
||||
// DeleteCustomer deletes a customer (Web Panel)
|
||||
// @Summary Delete customer
|
||||
// @Description Soft delete a customer by setting status to inactive. The customer record is preserved but marked as deleted. This is a reversible operation.
|
||||
// @Tags Customers-Admin
|
||||
// @Description Soft delete a customer by setting status to inactive
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Customer UUID" format(uuid)
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Success 200 {object} response.APIResponse "Customer deleted successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID format"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
|
||||
// @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} [delete]
|
||||
func (h *Handler) DeleteCustomer(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
|
||||
// Get user ID from JWT token context
|
||||
deletedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid customer ID", err.Error())
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
// TODO: Get user ID from JWT token
|
||||
deletedBy := uuid.New() // This should come from JWT token
|
||||
|
||||
err = h.service.DeleteCustomer(c.Request().Context(), id, &deletedBy)
|
||||
err = h.service.DeleteCustomer(c.Request().Context(), idStr, &deletedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "customer not found" {
|
||||
return response.NotFound(c, "Customer not found")
|
||||
@@ -208,13 +171,15 @@ func (h *Handler) DeleteCustomer(c echo.Context) error {
|
||||
return response.InternalServerError(c, "Failed to delete customer")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Customer deleted successfully")
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Customer deleted successfully",
|
||||
}, "Customer deleted successfully")
|
||||
}
|
||||
|
||||
// ListCustomers lists customers with filters and pagination
|
||||
// @Summary List customers with filters and pagination
|
||||
// @Description Retrieve a paginated list of customers with advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination.
|
||||
// @Tags Customers-Admin
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param search query string false "Search term to filter customers by name, email, or company name"
|
||||
@@ -242,7 +207,7 @@ func (h *Handler) ListCustomers(c echo.Context) error {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
result, err := h.service.ListCustomers(c.Request().Context(), form)
|
||||
result, err := h.service.ListCustomersWithCompanies(c.Request().Context(), form)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to list customers")
|
||||
}
|
||||
@@ -250,37 +215,74 @@ func (h *Handler) ListCustomers(c echo.Context) error {
|
||||
return response.Success(c, result, "Customers retrieved successfully")
|
||||
}
|
||||
|
||||
// UpdateCustomerStatus updates customer status
|
||||
// @Summary Update customer status
|
||||
// @Description Update the status of a customer account. Valid statuses include active, inactive, suspended, and pending. This affects the customer's ability to access the system.
|
||||
// @Tags Customers-Admin
|
||||
// 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 Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Customer UUID" format(uuid)
|
||||
// @Param status body UpdateCustomerStatusForm true "Status update information"
|
||||
// @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)
|
||||
// @Summary Update customer status
|
||||
// @Description Update customer account status (active, inactive, suspended, pending)
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Param status body UpdateCustomerStatusForm true "New customer status"
|
||||
// @Success 200 {object} response.APIResponse "Customer status updated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID or status"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid status value"
|
||||
// @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}/status [patch]
|
||||
func (h *Handler) UpdateCustomerStatus(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid customer ID", err.Error())
|
||||
}
|
||||
|
||||
form, err := response.Parse[UpdateCustomerStatusForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// TODO: Get user ID from JWT token
|
||||
updatedBy := uuid.New() // This should come from JWT token
|
||||
// Get user ID from JWT token context
|
||||
updatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.UpdateCustomerStatus(c.Request().Context(), id, form, &updatedBy)
|
||||
err = h.service.UpdateCustomerStatus(c.Request().Context(), idStr, form, &updatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "customer not found" {
|
||||
return response.NotFound(c, "Customer not found")
|
||||
@@ -288,40 +290,40 @@ func (h *Handler) UpdateCustomerStatus(c echo.Context) error {
|
||||
return response.InternalServerError(c, "Failed to update customer status")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Customer status updated successfully")
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Customer status updated successfully",
|
||||
}, "Customer status updated successfully")
|
||||
}
|
||||
|
||||
// UpdateCustomerVerification updates customer verification status
|
||||
// @Summary Update customer verification and compliance status
|
||||
// @Description Update the verification and compliance status of a customer. This includes marking customers as verified and compliant, with optional compliance notes for audit purposes.
|
||||
// @Tags Customers-Admin
|
||||
// UpdateCustomerVerification updates customer verification status (Web Panel)
|
||||
// @Summary Update customer verification status
|
||||
// @Description Update customer verification and compliance status
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Customer UUID" format(uuid)
|
||||
// @Param verification body UpdateCustomerVerificationForm true "Verification and compliance update information"
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Param verification body UpdateCustomerVerificationForm true "Verification status update"
|
||||
// @Success 200 {object} response.APIResponse "Customer verification updated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid verification data"
|
||||
// @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}/verification [patch]
|
||||
func (h *Handler) UpdateCustomerVerification(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid customer ID", err.Error())
|
||||
}
|
||||
|
||||
form, err := response.Parse[UpdateCustomerVerificationForm](c)
|
||||
if err != nil {
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
// TODO: Get user ID from JWT token
|
||||
updatedBy := uuid.New() // This should come from JWT token
|
||||
// Get user ID from JWT token context
|
||||
updatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
err = h.service.UpdateCustomerVerification(c.Request().Context(), id, form, &updatedBy)
|
||||
err = h.service.UpdateCustomerVerification(c.Request().Context(), idStr, form, &updatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "customer not found" {
|
||||
return response.NotFound(c, "Customer not found")
|
||||
@@ -329,33 +331,34 @@ func (h *Handler) UpdateCustomerVerification(c echo.Context) error {
|
||||
return response.InternalServerError(c, "Failed to update customer verification")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Customer verification updated successfully")
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Customer verification updated successfully",
|
||||
}, "Customer verification updated successfully")
|
||||
}
|
||||
|
||||
// VerifyCustomer verifies a customer
|
||||
// VerifyCustomer verifies a customer (Web Panel)
|
||||
// @Summary Verify customer
|
||||
// @Description Mark a customer as verified. This is a simple verification action that sets the customer's verification status to true.
|
||||
// @Tags Customers-Admin
|
||||
// @Description Mark a customer as verified
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Customer UUID" format(uuid)
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Success 200 {object} response.APIResponse "Customer verified successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
|
||||
// @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}/verify [post]
|
||||
func (h *Handler) VerifyCustomer(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
|
||||
// Get user ID from JWT token context
|
||||
verifiedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid customer ID", err.Error())
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
// TODO: Get user ID from JWT token
|
||||
verifiedBy := uuid.New() // This should come from JWT token
|
||||
|
||||
err = h.service.VerifyCustomer(c.Request().Context(), id, &verifiedBy)
|
||||
err = h.service.VerifyCustomer(c.Request().Context(), idStr, &verifiedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "customer not found" {
|
||||
return response.NotFound(c, "Customer not found")
|
||||
@@ -363,44 +366,40 @@ func (h *Handler) VerifyCustomer(c echo.Context) error {
|
||||
return response.InternalServerError(c, "Failed to verify customer")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Customer verified successfully")
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Customer verified successfully",
|
||||
}, "Customer verified successfully")
|
||||
}
|
||||
|
||||
// SuspendCustomer suspends a customer
|
||||
// @Summary Suspend customer account
|
||||
// @Description Suspend a customer account by setting their status to suspended. Suspended customers cannot access the system. A reason for suspension must be provided.
|
||||
// @Tags Customers-Admin
|
||||
// SuspendCustomer suspends a customer (Web Panel)
|
||||
// @Summary Suspend customer
|
||||
// @Description Suspend a customer account with optional reason
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Customer UUID" format(uuid)
|
||||
// @Param reason body object true "Suspension reason" SchemaExample({"reason": "Violation of terms of service"})
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Param suspension body SuspendCustomerForm true "Suspension information"
|
||||
// @Success 200 {object} response.APIResponse "Customer suspended successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID or missing reason"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
|
||||
// @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}/suspend [post]
|
||||
func (h *Handler) SuspendCustomer(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
form, err := response.Parse[SuspendCustomerForm](c)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid customer ID", err.Error())
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
var requestBody map[string]string
|
||||
if err := c.Bind(&requestBody); err != nil {
|
||||
return response.BadRequest(c, "Invalid request body", err.Error())
|
||||
// Get user ID from JWT token context
|
||||
suspendedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
reason := requestBody["reason"]
|
||||
if reason == "" {
|
||||
return response.BadRequest(c, "Suspension reason is required", "")
|
||||
}
|
||||
|
||||
// TODO: Get user ID from JWT token
|
||||
suspendedBy := uuid.New() // This should come from JWT token
|
||||
|
||||
err = h.service.SuspendCustomer(c.Request().Context(), id, reason, &suspendedBy)
|
||||
err = h.service.SuspendCustomer(c.Request().Context(), idStr, form.Reason, &suspendedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "customer not found" {
|
||||
return response.NotFound(c, "Customer not found")
|
||||
@@ -408,33 +407,34 @@ func (h *Handler) SuspendCustomer(c echo.Context) error {
|
||||
return response.InternalServerError(c, "Failed to suspend customer")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Customer suspended successfully")
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Customer suspended successfully",
|
||||
}, "Customer suspended successfully")
|
||||
}
|
||||
|
||||
// ActivateCustomer activates a customer
|
||||
// @Summary Activate suspended customer account
|
||||
// @Description Reactivate a suspended customer account by setting their status back to active. This reverses the suspension action.
|
||||
// @Tags Customers-Admin
|
||||
// ActivateCustomer activates a customer (Web Panel)
|
||||
// @Summary Activate customer
|
||||
// @Description Activate a suspended customer account
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Customer UUID" format(uuid)
|
||||
// @Param id path string true "Customer ID"
|
||||
// @Success 200 {object} response.APIResponse "Customer activated successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist"
|
||||
// @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}/activate [post]
|
||||
func (h *Handler) ActivateCustomer(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
|
||||
// Get user ID from JWT token context
|
||||
activatedBy, err := user.GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid customer ID", err.Error())
|
||||
return response.Unauthorized(c, "User not authenticated")
|
||||
}
|
||||
|
||||
// TODO: Get user ID from JWT token
|
||||
activatedBy := uuid.New() // This should come from JWT token
|
||||
|
||||
err = h.service.ActivateCustomer(c.Request().Context(), id, &activatedBy)
|
||||
err = h.service.ActivateCustomer(c.Request().Context(), idStr, &activatedBy)
|
||||
if err != nil {
|
||||
if err.Error() == "customer not found" {
|
||||
return response.NotFound(c, "Customer not found")
|
||||
@@ -442,67 +442,67 @@ func (h *Handler) ActivateCustomer(c echo.Context) error {
|
||||
return response.InternalServerError(c, "Failed to activate customer")
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Customer activated successfully")
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Customer activated successfully",
|
||||
}, "Customer activated successfully")
|
||||
}
|
||||
|
||||
// GetCustomersByCompanyID retrieves customers by company ID
|
||||
// GetCustomersByCompanyID retrieves customers by company ID (Web Panel)
|
||||
// @Summary Get customers by company ID
|
||||
// @Description Retrieve all customers associated with a specific company. This is useful for company administrators to see all users within their organization.
|
||||
// @Tags Customers-Admin
|
||||
// @Description Retrieve all customers associated with a specific company
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param companyId path string true "Company UUID" format(uuid)
|
||||
// @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)
|
||||
// @Success 200 {object} response.APIResponse{data=[]CustomerResponse,meta=response.Meta} "Customers retrieved successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID format"
|
||||
// @Param companyId path string true "Company ID"
|
||||
// @Param limit query int false "Number of customers to return (default: 10, max: 100)"
|
||||
// @Param offset query int false "Number of customers to skip (default: 0)"
|
||||
// @Success 200 {object} response.APIResponse{data=[]CustomerResponse} "Customers retrieved successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security BearerAuth
|
||||
// @Router /admin/v1/customers/company/{companyId} [get]
|
||||
func (h *Handler) GetCustomersByCompanyID(c echo.Context) error {
|
||||
companyIDStr := c.Param("companyId")
|
||||
companyID, err := uuid.Parse(companyIDStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid company ID", err.Error())
|
||||
}
|
||||
|
||||
limit := 20
|
||||
// Parse pagination parameters
|
||||
limit := 10 // Default limit
|
||||
offset := 0 // Default offset
|
||||
|
||||
if limitStr := c.QueryParam("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if offsetStr := c.QueryParam("offset"); offsetStr != "" {
|
||||
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
|
||||
offset = parsedOffset
|
||||
}
|
||||
}
|
||||
|
||||
customers, total, err := h.service.GetCustomersByCompanyID(c.Request().Context(), companyID, limit, offset)
|
||||
customers, total, err := h.service.GetCustomersByCompanyID(c.Request().Context(), companyIDStr, limit, offset)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve customers by company")
|
||||
return response.InternalServerError(c, "Failed to get customers by company")
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
var customerResponses []*CustomerResponse
|
||||
for _, customer := range customers {
|
||||
customerResponses = append(customerResponses, customer.ToResponse())
|
||||
}
|
||||
|
||||
meta := &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully")
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"customers": customerResponses,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}, "Customers retrieved successfully")
|
||||
}
|
||||
|
||||
// GetCustomersByType retrieves customers by type
|
||||
// @Summary Get customers by type
|
||||
// @Description Retrieve customers filtered by their type (individual, company, or government). Useful for administrative reporting and management.
|
||||
// @Tags Customers-Admin
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param type path string true "Customer type" Enums(individual, company, government)
|
||||
@@ -558,7 +558,7 @@ func (h *Handler) GetCustomersByType(c echo.Context) error {
|
||||
// GetCustomersByStatus retrieves customers by status
|
||||
// @Summary Get customers by status
|
||||
// @Description Retrieve customers filtered by their account status (active, inactive, suspended, or pending). Useful for administrative monitoring and management.
|
||||
// @Tags Customers-Admin
|
||||
// @Tags Admin-Customers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param status path string true "Customer status" Enums(active, inactive, suspended, pending)
|
||||
@@ -611,10 +611,92 @@ func (h *Handler) GetCustomersByStatus(c echo.Context) error {
|
||||
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 Admin-Customers
|
||||
// @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 Admin-Customers
|
||||
// @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
|
||||
// @Summary Customer login
|
||||
// @Description Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.
|
||||
// @Tags Customers-Authorization
|
||||
// @Tags Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body LoginForm true "Login credentials (username/email and password)"
|
||||
@@ -623,7 +705,7 @@ func (h *Handler) GetCustomersByStatus(c echo.Context) error {
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid credentials or inactive account"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
|
||||
// @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 {
|
||||
form, err := response.Parse[LoginForm](c)
|
||||
if err != nil {
|
||||
@@ -648,7 +730,7 @@ func (h *Handler) Login(c echo.Context) error {
|
||||
// RefreshToken handles customer token refresh
|
||||
// @Summary Refresh customer access token
|
||||
// @Description Refresh access token using a valid refresh token. This allows customers to maintain their session without re-authentication.
|
||||
// @Tags Customers-Authorization
|
||||
// @Tags Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param refresh body RefreshTokenForm true "Refresh token for generating new access token"
|
||||
@@ -657,7 +739,7 @@ func (h *Handler) Login(c echo.Context) error {
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired refresh token"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
|
||||
// @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 {
|
||||
form, err := response.Parse[RefreshTokenForm](c)
|
||||
if err != nil {
|
||||
@@ -676,66 +758,70 @@ func (h *Handler) RefreshToken(c echo.Context) error {
|
||||
return response.Success(c, authResponse, "Token refreshed successfully")
|
||||
}
|
||||
|
||||
// GetProfile retrieves customer profile information
|
||||
// GetProfile retrieves customer profile information (Mobile)
|
||||
// @Summary Get customer profile
|
||||
// @Description Retrieve authenticated customer's profile information. This endpoint requires a valid access token and returns the customer's own profile data.
|
||||
// @Tags Customers-Authorization
|
||||
// @Description Retrieve current customer profile information
|
||||
// @Tags 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 access token"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Customer profile not found"
|
||||
// @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/profile [get]
|
||||
func (h *Handler) GetProfile(c echo.Context) error {
|
||||
// Get customer ID from context (set by AuthMiddleware)
|
||||
// Extract customer ID from JWT token context
|
||||
customerID, err := GetCustomerIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "Customer ID not found in context")
|
||||
return response.Unauthorized(c, "Customer not authenticated")
|
||||
}
|
||||
|
||||
// Call service
|
||||
customer, err := h.service.GetProfile(c.Request().Context(), customerID)
|
||||
resp, 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 retrieve customer profile")
|
||||
return response.InternalServerError(c, "Failed to get customer profile")
|
||||
}
|
||||
|
||||
return response.Success(c, customer.ToResponse(), "Profile retrieved successfully")
|
||||
return response.Success(c, resp, "Profile retrieved successfully")
|
||||
}
|
||||
|
||||
// Logout handles customer logout
|
||||
// Logout handles customer logout (Mobile)
|
||||
// @Summary Customer logout
|
||||
// @Description Logout customer and invalidate access token. This ensures the token cannot be used for future requests.
|
||||
// @Tags Customers-Authorization
|
||||
// @Description Logout customer and invalidate access token
|
||||
// @Tags Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.APIResponse "Logout successful"
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing access token"
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Router /api/v1/logout [delete]
|
||||
// @Router /api/v1/profile/logout [delete]
|
||||
func (h *Handler) Logout(c echo.Context) error {
|
||||
// Get customer ID from context (set by AuthMiddleware)
|
||||
// Extract customer ID from JWT token context
|
||||
customerID, err := GetCustomerIDFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "Customer ID not found in context")
|
||||
return response.Unauthorized(c, "Customer not authenticated")
|
||||
}
|
||||
|
||||
// Get access token from context
|
||||
accessToken, err := GetAccessTokenFromContext(c)
|
||||
if err != nil {
|
||||
return response.Unauthorized(c, "Access token not found in context")
|
||||
return response.Unauthorized(c, "Access token not found")
|
||||
}
|
||||
|
||||
// Call service
|
||||
err = h.service.Logout(c.Request().Context(), customerID, accessToken)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to logout customer")
|
||||
// Don't return error here as the customer should still be logged out
|
||||
h.logger.Warn("Failed to invalidate access token during logout", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customerID,
|
||||
})
|
||||
}
|
||||
|
||||
return response.Success(c, nil, "Logout successful")
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Logout successful",
|
||||
}, "Logout successful")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
"tm/pkg/response"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
@@ -44,13 +43,7 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
|
||||
}
|
||||
|
||||
// Extract customer information from token
|
||||
customerID, err := uuid.Parse(validationResult.Claims.UserID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to parse customer ID from token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.Unauthorized(c, "Invalid token format")
|
||||
}
|
||||
customerID := validationResult.Claims.UserID
|
||||
|
||||
// Store customer information in context for handlers to use
|
||||
c.Set("customer_id", customerID)
|
||||
@@ -66,10 +59,10 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
|
||||
}
|
||||
|
||||
// GetCustomerIDFromContext extracts customer ID from Echo context
|
||||
func GetCustomerIDFromContext(c echo.Context) (uuid.UUID, error) {
|
||||
customerID, ok := c.Get("customer_id").(uuid.UUID)
|
||||
func GetCustomerIDFromContext(c echo.Context) (string, error) {
|
||||
customerID, ok := c.Get("customer_id").(string)
|
||||
if !ok {
|
||||
return uuid.Nil, echo.NewHTTPError(http.StatusUnauthorized, "Customer ID not found in context")
|
||||
return "", echo.NewHTTPError(http.StatusUnauthorized, "Customer ID not found in context")
|
||||
}
|
||||
return customerID, nil
|
||||
}
|
||||
|
||||
+328
-264
@@ -7,162 +7,96 @@ import (
|
||||
"tm/pkg/logger"
|
||||
mongopkg "tm/pkg/mongo"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// Repository defines the interface for customer data operations
|
||||
type Repository interface {
|
||||
Create(ctx context.Context, customer *Customer) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*Customer, error)
|
||||
GetByID(ctx context.Context, id string) (*Customer, error)
|
||||
GetByEmail(ctx context.Context, email string) (*Customer, error)
|
||||
GetByUsername(ctx context.Context, username string) (*Customer, error)
|
||||
GetByCompanyName(ctx context.Context, companyName string) (*Customer, error)
|
||||
GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error)
|
||||
GetByTaxID(ctx context.Context, taxID string) (*Customer, error)
|
||||
GetByCompanyID(ctx context.Context, companyID uuid.UUID, 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
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
List(ctx context.Context, limit, offset int) ([]*Customer, error)
|
||||
Search(ctx context.Context, search string, customerType *string, status *string, companyID *uuid.UUID, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error)
|
||||
CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error)
|
||||
Search(ctx context.Context, search string, customerType *string, status *string, companyID *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error)
|
||||
CountByCompanyID(ctx context.Context, companyID string) (int64, error)
|
||||
CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error)
|
||||
CountByType(ctx context.Context, customerType CustomerType) (int64, error)
|
||||
CountByStatus(ctx context.Context, status CustomerStatus) (int64, error)
|
||||
UpdateStatus(ctx context.Context, id uuid.UUID, status CustomerStatus) error
|
||||
UpdateVerification(ctx context.Context, id uuid.UUID, isVerified, isCompliant bool, complianceNotes *string) error
|
||||
UpdateStatus(ctx context.Context, id string, status CustomerStatus) error
|
||||
UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error
|
||||
}
|
||||
|
||||
// customerRepository implements the Repository interface
|
||||
// customerRepository implements the Repository interface using the MongoDB ORM
|
||||
type customerRepository struct {
|
||||
collection *mongo.Collection
|
||||
logger logger.Logger
|
||||
ormRepo mongopkg.Repository[Customer]
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewCustomerRepository creates a new customer repository
|
||||
func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
|
||||
collection := mongoManager.GetCollection("customers")
|
||||
|
||||
// Create indexes
|
||||
repo := &customerRepository{
|
||||
collection: collection,
|
||||
logger: logger,
|
||||
// Create indexes using the ORM's index management
|
||||
indexes := []mongopkg.Index{
|
||||
*mongopkg.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
|
||||
*mongopkg.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}),
|
||||
*mongopkg.NewIndex("company_name_idx", bson.D{{Key: "company_name", Value: 1}}),
|
||||
*mongopkg.NewIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
|
||||
*mongopkg.NewIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}),
|
||||
*mongopkg.NewIndex("company_ids_idx", bson.D{{Key: "company_ids", Value: 1}}), // Index for CompanyIDs array
|
||||
*mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}),
|
||||
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
||||
*mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}),
|
||||
*mongopkg.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}),
|
||||
*mongopkg.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}),
|
||||
*mongopkg.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}),
|
||||
*mongopkg.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}),
|
||||
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||
*mongopkg.CreateTextIndex("search_idx", "first_name", "last_name", "full_name", "company_name", "email", "username"),
|
||||
}
|
||||
|
||||
repo.createIndexes()
|
||||
return repo
|
||||
}
|
||||
|
||||
// createIndexes creates necessary database indexes
|
||||
func (r *customerRepository) createIndexes() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create indexes
|
||||
indexes := []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "email", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "username", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "company_name", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetSparse(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "registration_number", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetSparse(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "tax_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetSparse(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "company_id", Value: 1},
|
||||
},
|
||||
Options: options.Index().SetSparse(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "type", Value: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "status", Value: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "industry", Value: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "created_at", Value: -1},
|
||||
},
|
||||
},
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: "updated_at", Value: -1},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := r.collection.Indexes().CreateMany(ctx, indexes)
|
||||
err := mongoManager.CreateIndexes("customers", indexes)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to create customer indexes", map[string]interface{}{
|
||||
logger.Warn("Failed to create customer indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
} else {
|
||||
r.logger.Info("Customer indexes created successfully", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// Create ORM repository
|
||||
ormRepo := mongopkg.NewRepository[Customer](mongoManager.GetCollection("customers"), logger)
|
||||
|
||||
return &customerRepository{
|
||||
ormRepo: ormRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new customer
|
||||
func (r *customerRepository) Create(ctx context.Context, customer *Customer) error {
|
||||
// Set timestamps
|
||||
// Set created/updated timestamps using Unix timestamps
|
||||
now := time.Now().Unix()
|
||||
customer.CreatedAt = now
|
||||
customer.UpdatedAt = now
|
||||
customer.SetCreatedAt(now)
|
||||
customer.SetUpdatedAt(now)
|
||||
|
||||
// Set defaults if not provided
|
||||
if customer.Language == "" {
|
||||
customer.Language = "en"
|
||||
}
|
||||
if customer.Currency == "" {
|
||||
customer.Currency = "USD"
|
||||
}
|
||||
if customer.Timezone == "" {
|
||||
customer.Timezone = "UTC"
|
||||
}
|
||||
|
||||
_, err := r.collection.InsertOne(ctx, customer)
|
||||
// Use ORM to create customer
|
||||
err := r.ormRepo.Create(ctx, customer)
|
||||
if err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return errors.New("customer with this email already exists")
|
||||
}
|
||||
r.logger.Error("Failed to create customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": customer.Email,
|
||||
"username": customer.Username,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Customer created successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"customer_id": customer.ID,
|
||||
"email": customer.Email,
|
||||
"type": customer.Type,
|
||||
})
|
||||
@@ -171,130 +105,205 @@ func (r *customerRepository) Create(ctx context.Context, customer *Customer) err
|
||||
}
|
||||
|
||||
// GetByID retrieves a customer by ID
|
||||
func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) {
|
||||
var customer Customer
|
||||
err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&customer)
|
||||
func (r *customerRepository) GetByID(ctx context.Context, id string) (*Customer, error) {
|
||||
customer, err := r.ormRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
r.logger.Error("Failed to get customer by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &customer, nil
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// GetByEmail retrieves a customer by email
|
||||
func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Customer, error) {
|
||||
var customer Customer
|
||||
err := r.collection.FindOne(ctx, bson.M{"email": email}).Decode(&customer)
|
||||
filter := bson.M{"email": email}
|
||||
customer, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
r.logger.Error("Failed to get customer by email", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": email,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &customer, nil
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// GetByUsername retrieves a customer by username
|
||||
func (r *customerRepository) GetByUsername(ctx context.Context, username string) (*Customer, error) {
|
||||
var customer Customer
|
||||
err := r.collection.FindOne(ctx, bson.M{"username": username}).Decode(&customer)
|
||||
filter := bson.M{"username": username}
|
||||
customer, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
r.logger.Error("Failed to get customer by username", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"username": username,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &customer, nil
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// GetByCompanyName retrieves a customer by company name
|
||||
func (r *customerRepository) GetByCompanyName(ctx context.Context, companyName string) (*Customer, error) {
|
||||
var customer Customer
|
||||
err := r.collection.FindOne(ctx, bson.M{"company_name": companyName}).Decode(&customer)
|
||||
filter := bson.M{"company_name": companyName}
|
||||
customer, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
r.logger.Error("Failed to get customer by company name", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_name": companyName,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &customer, nil
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// GetByRegistrationNumber retrieves a customer by registration number
|
||||
func (r *customerRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) {
|
||||
var customer Customer
|
||||
err := r.collection.FindOne(ctx, bson.M{"registration_number": registrationNumber}).Decode(&customer)
|
||||
filter := bson.M{"registration_number": registrationNumber}
|
||||
customer, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
r.logger.Error("Failed to get customer by registration number", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"registration_number": registrationNumber,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &customer, nil
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// GetByTaxID retrieves a customer by tax ID
|
||||
func (r *customerRepository) GetByTaxID(ctx context.Context, taxID string) (*Customer, error) {
|
||||
var customer Customer
|
||||
err := r.collection.FindOne(ctx, bson.M{"tax_id": taxID}).Decode(&customer)
|
||||
filter := bson.M{"tax_id": taxID}
|
||||
customer, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("customer not found")
|
||||
}
|
||||
r.logger.Error("Failed to get customer by tax ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"tax_id": taxID,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &customer, nil
|
||||
return customer, nil
|
||||
}
|
||||
|
||||
// GetByCompanyID retrieves customers by company ID with pagination
|
||||
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) {
|
||||
filter := bson.M{"company_id": companyID}
|
||||
func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, error) {
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
opts := options.Find().
|
||||
SetLimit(int64(limit)).
|
||||
SetSkip(int64(offset)).
|
||||
SetSort(bson.D{{Key: "created_at", Value: -1}})
|
||||
// Filter by company ID and active status
|
||||
filter := bson.M{
|
||||
"company_id": companyID,
|
||||
"status": bson.M{"$ne": CustomerStatusInactive},
|
||||
}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
// Use ORM to find customers
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get customers by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var customers []*Customer
|
||||
if err = cursor.All(ctx, &customers); err != nil {
|
||||
// Convert []Customer to []*Customer
|
||||
customers := make([]*Customer, len(result.Items))
|
||||
for i := range result.Items {
|
||||
customers[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
// GetByCompanyIDs retrieves customers by multiple company IDs with pagination
|
||||
func (r *customerRepository) GetByCompanyIDs(ctx context.Context, companyIDs []string, limit, offset int) ([]*Customer, error) {
|
||||
if len(companyIDs) == 0 {
|
||||
return nil, errors.New("no company IDs provided")
|
||||
}
|
||||
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
// Filter by company IDs and active status
|
||||
filter := bson.M{
|
||||
"company_ids": bson.M{"$in": companyIDs},
|
||||
"status": bson.M{"$ne": CustomerStatusInactive},
|
||||
}
|
||||
|
||||
// Use ORM to find customers
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get customers by multiple company IDs", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_ids": companyIDs,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert []Customer to []*Customer
|
||||
customers := make([]*Customer, len(result.Items))
|
||||
for i := range result.Items {
|
||||
customers[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
// Update updates a customer
|
||||
func (r *customerRepository) Update(ctx context.Context, customer *Customer) error {
|
||||
customer.UpdatedAt = time.Now().Unix()
|
||||
// Set updated timestamp using Unix timestamp
|
||||
customer.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
filter := bson.M{"_id": customer.ID}
|
||||
update := bson.M{"$set": customer}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
// Use ORM to update customer
|
||||
err := r.ormRepo.Update(ctx, customer)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": customer.ID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Customer updated successfully", map[string]interface{}{
|
||||
"customer_id": customer.ID.String(),
|
||||
"customer_id": customer.ID,
|
||||
"email": customer.Email,
|
||||
})
|
||||
|
||||
@@ -302,26 +311,29 @@ func (r *customerRepository) Update(ctx context.Context, customer *Customer) err
|
||||
}
|
||||
|
||||
// Delete deletes a customer (soft delete by setting status to inactive)
|
||||
func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"status": CustomerStatusInactive,
|
||||
"updated_at": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
func (r *customerRepository) Delete(ctx context.Context, id string) error {
|
||||
// Get customer first
|
||||
customer, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
// Update status to inactive
|
||||
customer.Status = CustomerStatusInactive
|
||||
customer.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, customer)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete customer", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Customer deleted successfully", map[string]interface{}{
|
||||
"customer_id": id.String(),
|
||||
"customer_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
@@ -329,124 +341,148 @@ func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
|
||||
// List retrieves customers with pagination
|
||||
func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) {
|
||||
opts := options.Find().
|
||||
SetLimit(int64(limit)).
|
||||
SetSkip(int64(offset)).
|
||||
SetSort(bson.D{{Key: "created_at", Value: -1}})
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
cursor, err := r.collection.Find(ctx, bson.M{}, opts)
|
||||
// Only active customers by default
|
||||
filter := bson.M{"status": bson.M{"$ne": CustomerStatusInactive}}
|
||||
|
||||
// Use ORM to find customers
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to list customers", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var customers []*Customer
|
||||
if err = cursor.All(ctx, &customers); err != nil {
|
||||
return nil, err
|
||||
// Convert []Customer to []*Customer
|
||||
customers := make([]*Customer, len(result.Items))
|
||||
for i := range result.Items {
|
||||
customers[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
// Search searches customers with filters
|
||||
func (r *customerRepository) Search(ctx context.Context, search string, customerType *string, status *string, companyID *uuid.UUID, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) {
|
||||
// Search retrieves customers with search and filters
|
||||
func (r *customerRepository) Search(ctx context.Context, search string, customerType *string, status *string, companyID *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) {
|
||||
// Build filter
|
||||
filter := bson.M{}
|
||||
|
||||
// Add search filter
|
||||
if search != "" {
|
||||
filter["$or"] = []bson.M{
|
||||
{"email": primitive.Regex{Pattern: search, Options: "i"}},
|
||||
{"company_name": primitive.Regex{Pattern: search, Options: "i"}},
|
||||
{"first_name": primitive.Regex{Pattern: search, Options: "i"}},
|
||||
{"last_name": primitive.Regex{Pattern: search, Options: "i"}},
|
||||
{"full_name": primitive.Regex{Pattern: search, Options: "i"}},
|
||||
}
|
||||
filter["$text"] = bson.M{"$search": search}
|
||||
}
|
||||
|
||||
// Add type filter
|
||||
if customerType != nil {
|
||||
filter["type"] = *customerType
|
||||
}
|
||||
|
||||
// Add status filter
|
||||
if status != nil {
|
||||
filter["status"] = *status
|
||||
}
|
||||
|
||||
// Add company ID filter
|
||||
if companyID != nil {
|
||||
filter["company_id"] = *companyID
|
||||
}
|
||||
|
||||
// Add industry filter
|
||||
if industry != nil {
|
||||
filter["industry"] = *industry
|
||||
}
|
||||
|
||||
// Add verification filter
|
||||
if isVerified != nil {
|
||||
filter["is_verified"] = *isVerified
|
||||
}
|
||||
|
||||
// Add compliance filter
|
||||
if isCompliant != nil {
|
||||
filter["is_compliant"] = *isCompliant
|
||||
}
|
||||
|
||||
// Add language filter
|
||||
if language != nil {
|
||||
filter["language"] = *language
|
||||
}
|
||||
|
||||
// Add currency filter
|
||||
if currency != nil {
|
||||
filter["currency"] = *currency
|
||||
}
|
||||
|
||||
// Set sort options
|
||||
sortDirection := 1
|
||||
if sortOrder == "desc" {
|
||||
sortDirection = -1
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset)
|
||||
|
||||
// Set sort
|
||||
if sortBy != "" {
|
||||
if sortOrder == "desc" {
|
||||
pagination.SortDesc(sortBy)
|
||||
} else {
|
||||
pagination.SortAsc(sortBy)
|
||||
}
|
||||
} else {
|
||||
pagination.SortDesc("created_at") // Default sort
|
||||
}
|
||||
|
||||
var sortField string
|
||||
switch sortBy {
|
||||
case "email":
|
||||
sortField = "email"
|
||||
case "company_name":
|
||||
sortField = "company_name"
|
||||
case "updated_at":
|
||||
sortField = "updated_at"
|
||||
case "status":
|
||||
sortField = "status"
|
||||
default:
|
||||
sortField = "created_at"
|
||||
}
|
||||
|
||||
opts := options.Find().
|
||||
SetLimit(int64(limit)).
|
||||
SetSkip(int64(offset)).
|
||||
SetSort(bson.D{{Key: sortField, Value: sortDirection}})
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
// Use ORM to find customers
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build())
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to search customers", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"search": search,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var customers []*Customer
|
||||
if err = cursor.All(ctx, &customers); err != nil {
|
||||
return nil, err
|
||||
// Convert []Customer to []*Customer
|
||||
customers := make([]*Customer, len(result.Items))
|
||||
for i := range result.Items {
|
||||
customers[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return customers, nil
|
||||
}
|
||||
|
||||
// CountByCompanyID counts customers by company ID
|
||||
func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) {
|
||||
filter := bson.M{"company_id": companyID}
|
||||
count, err := r.collection.CountDocuments(ctx, filter)
|
||||
func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID string) (int64, error) {
|
||||
filter := bson.M{
|
||||
"company_id": companyID,
|
||||
"status": bson.M{"$ne": CustomerStatusInactive},
|
||||
}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count customers by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// CountByCompanyIDs counts customers by multiple company IDs
|
||||
func (r *customerRepository) CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error) {
|
||||
if len(companyIDs) == 0 {
|
||||
return 0, errors.New("no company IDs provided")
|
||||
}
|
||||
|
||||
filter := bson.M{
|
||||
"company_ids": bson.M{"$in": companyIDs},
|
||||
"status": bson.M{"$ne": CustomerStatusInactive},
|
||||
}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count customers by multiple company IDs", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_ids": companyIDs,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -455,9 +491,17 @@ func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID uui
|
||||
|
||||
// CountByType counts customers by type
|
||||
func (r *customerRepository) CountByType(ctx context.Context, customerType CustomerType) (int64, error) {
|
||||
filter := bson.M{"type": customerType}
|
||||
count, err := r.collection.CountDocuments(ctx, filter)
|
||||
filter := bson.M{
|
||||
"type": customerType,
|
||||
"status": bson.M{"$ne": CustomerStatusInactive},
|
||||
}
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count customers by type", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_type": customerType,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -467,8 +511,13 @@ func (r *customerRepository) CountByType(ctx context.Context, customerType Custo
|
||||
// CountByStatus counts customers by status
|
||||
func (r *customerRepository) CountByStatus(ctx context.Context, status CustomerStatus) (int64, error) {
|
||||
filter := bson.M{"status": status}
|
||||
count, err := r.collection.CountDocuments(ctx, filter)
|
||||
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count customers by status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"status": status,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -476,50 +525,65 @@ func (r *customerRepository) CountByStatus(ctx context.Context, status CustomerS
|
||||
}
|
||||
|
||||
// UpdateStatus updates customer status
|
||||
func (r *customerRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status CustomerStatus) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"status": status,
|
||||
"updated_at": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
func (r *customerRepository) UpdateStatus(ctx context.Context, id string, status CustomerStatus) error {
|
||||
// Get customer first
|
||||
customer, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
// Update status
|
||||
customer.Status = status
|
||||
customer.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, customer)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update customer status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id,
|
||||
"status": status,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Customer status updated successfully", map[string]interface{}{
|
||||
"customer_id": id,
|
||||
"status": status,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateVerification updates customer verification status
|
||||
func (r *customerRepository) UpdateVerification(ctx context.Context, id uuid.UUID, isVerified, isCompliant bool, complianceNotes *string) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"is_verified": isVerified,
|
||||
"is_compliant": isCompliant,
|
||||
"updated_at": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
if complianceNotes != nil {
|
||||
update["$set"].(bson.M)["compliance_notes"] = *complianceNotes
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
func (r *customerRepository) UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error {
|
||||
// Get customer first
|
||||
customer, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("customer not found")
|
||||
// Update verification fields
|
||||
customer.IsVerified = isVerified
|
||||
customer.IsCompliant = isCompliant
|
||||
customer.ComplianceNotes = complianceNotes
|
||||
customer.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, customer)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update customer verification", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Customer verification updated successfully", map[string]interface{}{
|
||||
"customer_id": id,
|
||||
"is_verified": isVerified,
|
||||
"is_compliant": isCompliant,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+448
-150
File diff suppressed because it is too large
Load Diff
+49
-19
@@ -1,7 +1,9 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"tm/pkg/mongo"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// UserRole represents user roles in the system
|
||||
@@ -25,22 +27,50 @@ const (
|
||||
|
||||
// User represents a system user (admin/manager/operator)
|
||||
type User struct {
|
||||
ID uuid.UUID `bson:"_id"`
|
||||
FullName string `bson:"full_name"`
|
||||
Username string `bson:"username"`
|
||||
Email string `bson:"email"`
|
||||
Password string `bson:"password"` // Never serialize password
|
||||
Role UserRole `bson:"role"`
|
||||
Status UserStatus `bson:"status"`
|
||||
CompanyID *uuid.UUID `bson:"company_id,omitempty"`
|
||||
Department *string `bson:"department,omitempty"`
|
||||
Position *string `bson:"position,omitempty"`
|
||||
Phone *string `bson:"phone,omitempty"`
|
||||
ProfileImage *string `bson:"profile_image,omitempty"`
|
||||
IsVerified bool `bson:"is_verified"`
|
||||
LastLoginAt *int64 `bson:"last_login_at,omitempty"` // Unix timestamp
|
||||
CreatedAt int64 `bson:"created_at"` // Unix timestamp
|
||||
UpdatedAt int64 `bson:"updated_at"` // Unix timestamp
|
||||
CreatedBy *uuid.UUID `bson:"created_by,omitempty"`
|
||||
UpdatedBy *uuid.UUID `bson:"updated_by,omitempty"`
|
||||
mongo.Model `bson:",inline"`
|
||||
FullName string `bson:"full_name" json:"full_name"`
|
||||
Username string `bson:"username" json:"username"`
|
||||
Email string `bson:"email" json:"email"`
|
||||
Password string `bson:"password" json:"-"` // Never serialize password
|
||||
Role UserRole `bson:"role" json:"role"`
|
||||
Status UserStatus `bson:"status" json:"status"`
|
||||
CompanyID *string `bson:"company_id,omitempty" json:"company_id,omitempty"`
|
||||
Department *string `bson:"department,omitempty" json:"department,omitempty"`
|
||||
Position *string `bson:"position,omitempty" json:"position,omitempty"`
|
||||
Phone *string `bson:"phone,omitempty" json:"phone,omitempty"`
|
||||
ProfileImage *string `bson:"profile_image,omitempty" json:"profile_image,omitempty"`
|
||||
IsVerified bool `bson:"is_verified" json:"is_verified"`
|
||||
LastLoginAt *int64 `bson:"last_login_at,omitempty" json:"last_login_at,omitempty"` // Unix timestamp
|
||||
CreatedBy *string `bson:"created_by,omitempty" json:"created_by,omitempty"`
|
||||
UpdatedBy *string `bson:"updated_by,omitempty" json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
// SetID sets the user ID (implements IDSetter interface)
|
||||
func (u *User) SetID(id string) {
|
||||
u.ID, _ = primitive.ObjectIDFromHex(id)
|
||||
}
|
||||
|
||||
// GetID returns the user ID (implements IDGetter interface)
|
||||
func (u *User) GetID() string {
|
||||
return u.ID.Hex()
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the created timestamp (implements Timestampable interface)
|
||||
func (u *User) SetCreatedAt(timestamp int64) {
|
||||
u.CreatedAt = timestamp
|
||||
}
|
||||
|
||||
// SetUpdatedAt sets the updated timestamp (implements Timestampable interface)
|
||||
func (u *User) SetUpdatedAt(timestamp int64) {
|
||||
u.UpdatedAt = timestamp
|
||||
}
|
||||
|
||||
// GetCreatedAt returns the created timestamp (implements Timestampable interface)
|
||||
func (u *User) GetCreatedAt() int64 {
|
||||
return u.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt returns the updated timestamp (implements Timestampable interface)
|
||||
func (u *User) GetUpdatedAt() int64 {
|
||||
return u.UpdatedAt
|
||||
}
|
||||
|
||||
+44
-50
@@ -1,57 +1,66 @@
|
||||
package user
|
||||
|
||||
// User Registration DTOs
|
||||
|
||||
// CreateUserForm represents the data required to create a new user account
|
||||
type CreateUserForm struct {
|
||||
FullName string `json:"full_name" valid:"required,length(2|100)"`
|
||||
Username string `json:"username" valid:"required,alphanum,length(3|30)"`
|
||||
Email string `json:"email" valid:"required,email"`
|
||||
Password string `json:"password" valid:"required,length(8|128)"`
|
||||
Role string `json:"role" valid:"required,in(admin|manager|operator|viewer)"`
|
||||
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
|
||||
Department *string `json:"department,omitempty" valid:"optional,length(2|100)"`
|
||||
Position *string `json:"position,omitempty" valid:"optional,length(2|100)"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
|
||||
ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url"`
|
||||
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)" example:"johnsmith"` // Unique username (alphanumeric only)
|
||||
Email string `json:"email" valid:"required,email" example:"john.smith@company.com"` // Valid email address
|
||||
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)" example:"manager"` // User role (admin, manager, operator, viewer)
|
||||
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)" example:"Information Technology"` // Optional department name
|
||||
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)" example:"+1234567890"` // Optional phone number
|
||||
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 {
|
||||
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
|
||||
Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)"`
|
||||
Email *string `json:"email,omitempty" valid:"optional,email"`
|
||||
Role *string `json:"role,omitempty" valid:"optional,in(admin|manager|operator|viewer)"`
|
||||
CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"`
|
||||
Department *string `json:"department,omitempty" valid:"optional,length(2|100)"`
|
||||
Position *string `json:"position,omitempty" valid:"optional,length(2|100)"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
|
||||
ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url"`
|
||||
Status *string `json:"status,omitempty" valid:"optional,in(active|inactive|suspended)"`
|
||||
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)" example:"johnsmith"` // Updated username
|
||||
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)" example:"admin"` // Updated user role
|
||||
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)" example:"Product Management"` // Updated department
|
||||
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)" example:"+1234567890"` // Updated phone number
|
||||
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)" example:"active"` // Updated account status
|
||||
}
|
||||
|
||||
// User Authentication DTOs
|
||||
|
||||
// LoginForm represents the credentials required for user authentication
|
||||
type LoginForm struct {
|
||||
Username string `json:"username" valid:"required"`
|
||||
Password string `json:"password" valid:"required"`
|
||||
Username string `json:"username" valid:"required" example:"nakhostin"` // Username or email address
|
||||
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 {
|
||||
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 {
|
||||
OldPassword string `json:"old_password" valid:"required"`
|
||||
NewPassword string `json:"new_password" valid:"required,length(8|128)"`
|
||||
OldPassword string `json:"old_password" valid:"required" example:"OldPassword123!"` // Current password for verification
|
||||
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 {
|
||||
FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"`
|
||||
Department *string `json:"department,omitempty" valid:"optional,length(2|100)"`
|
||||
Position *string `json:"position,omitempty" valid:"optional,length(2|100)"`
|
||||
Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"`
|
||||
ProfileImage *string `json:"profile_image,omitempty" valid:"optional,url"`
|
||||
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)" example:"Engineering"` // Updated department
|
||||
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)" example:"+1234567890"` // Updated phone number
|
||||
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 {
|
||||
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
|
||||
@@ -112,29 +121,14 @@ type UserListResponse struct {
|
||||
|
||||
// Helper function to convert User to UserResponse
|
||||
func (u *User) ToResponse() *UserResponse {
|
||||
companyID := ""
|
||||
if u.CompanyID != nil {
|
||||
companyID = u.CompanyID.String()
|
||||
}
|
||||
|
||||
createdBy := ""
|
||||
if u.CreatedBy != nil {
|
||||
createdBy = u.CreatedBy.String()
|
||||
}
|
||||
|
||||
updatedBy := ""
|
||||
if u.UpdatedBy != nil {
|
||||
updatedBy = u.UpdatedBy.String()
|
||||
}
|
||||
|
||||
return &UserResponse{
|
||||
ID: u.ID.String(),
|
||||
ID: u.ID.Hex(),
|
||||
FullName: u.FullName,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
Role: string(u.Role),
|
||||
Status: string(u.Status),
|
||||
CompanyID: &companyID,
|
||||
CompanyID: u.CompanyID,
|
||||
Department: u.Department,
|
||||
Position: u.Position,
|
||||
Phone: u.Phone,
|
||||
@@ -143,7 +137,7 @@ func (u *User) ToResponse() *UserResponse {
|
||||
LastLoginAt: u.LastLoginAt,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
CreatedBy: &createdBy,
|
||||
UpdatedBy: &updatedBy,
|
||||
CreatedBy: u.CreatedBy,
|
||||
UpdatedBy: u.UpdatedBy,
|
||||
}
|
||||
}
|
||||
|
||||
+101
-147
@@ -7,7 +7,6 @@ import (
|
||||
"tm/pkg/response"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
@@ -29,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
|
||||
// @Summary Login user
|
||||
// @Description Authenticate user with username and password
|
||||
// @Tags Authorization
|
||||
// @Summary Authenticate user login
|
||||
// @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 Admin-Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param login body LoginForm true "Login credentials"
|
||||
// @Success 200 {object} response.APIResponse{data=AuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/v1/login [post]
|
||||
// @Param login body LoginForm true "User login credentials including username/email and password"
|
||||
// @Success 200 {object} response.APIResponse{data=AuthResponse} "Login successful with access and refresh tokens"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid request format or missing fields"
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid credentials or account locked"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid input data format"
|
||||
// @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 {
|
||||
form, err := response.Parse[LoginForm](c)
|
||||
if err != nil {
|
||||
@@ -90,16 +59,17 @@ func (h *Handler) Login(c echo.Context) error {
|
||||
|
||||
// RefreshToken handles token refresh
|
||||
// @Summary Refresh access token
|
||||
// @Description Refresh access token using refresh token
|
||||
// @Tags Authorization
|
||||
// @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 Admin-Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param refresh body RefreshTokenForm true "Refresh token"
|
||||
// @Success 200 {object} response.APIResponse{data=AuthResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/v1/refresh-token [post]
|
||||
// @Param refresh body RefreshTokenForm true "Refresh token for generating new access token"
|
||||
// @Success 200 {object} response.APIResponse{data=AuthResponse} "Token refreshed successfully with new access token"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid request format"
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired refresh token"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid token format"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Router /admin/v1/profile/refresh-token [post]
|
||||
func (h *Handler) RefreshToken(c echo.Context) error {
|
||||
form, err := response.Parse[RefreshTokenForm](c)
|
||||
if err != nil {
|
||||
@@ -115,16 +85,19 @@ func (h *Handler) RefreshToken(c echo.Context) error {
|
||||
}
|
||||
|
||||
// ResetPassword handles password reset request
|
||||
// @Summary Reset password
|
||||
// @Description Send password reset email to user
|
||||
// @Tags Authorization
|
||||
// @Summary Initiate password reset process
|
||||
// @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 Admin-Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param reset body ResetPasswordForm true "Password reset request"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/v1/reset-password [post]
|
||||
// @Param reset body ResetPasswordForm true "Email address for password reset request"
|
||||
// @Success 200 {object} response.APIResponse "Password reset email sent successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid request format or email address"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Email address not registered"
|
||||
// @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 {
|
||||
form, err := response.Parse[ResetPasswordForm](c)
|
||||
if err != nil {
|
||||
@@ -142,17 +115,17 @@ func (h *Handler) ResetPassword(c echo.Context) error {
|
||||
}
|
||||
|
||||
// GetProfile gets current user profile
|
||||
// @Summary Get user profile
|
||||
// @Description Get current user profile information
|
||||
// @Tags Users
|
||||
// @Summary Get authenticated user profile
|
||||
// @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 Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.APIResponse{data=UserResponse}
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 404 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/v1/profile [get]
|
||||
// @Success 200 {object} response.APIResponse{data=UserResponse} "Profile retrieved successfully with user details"
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired authentication token"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - User profile not found"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Router /admin/v1/profile [get]
|
||||
func (h *Handler) GetProfile(c echo.Context) error {
|
||||
// Extract user ID from JWT token context
|
||||
userID, err := GetUserIDFromContext(c)
|
||||
@@ -169,18 +142,20 @@ func (h *Handler) GetProfile(c echo.Context) error {
|
||||
}
|
||||
|
||||
// UpdateProfile updates current user profile
|
||||
// @Summary Update user profile
|
||||
// @Description Update current user profile information
|
||||
// @Tags Users
|
||||
// @Summary Update authenticated user profile
|
||||
// @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 Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param profile body UpdateUserForm true "Profile update data"
|
||||
// @Success 200 {object} response.APIResponse{data=UserResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/v1/profile [put]
|
||||
// @Param profile body UpdateUserForm true "Profile update data including name, email, phone, and other personal information"
|
||||
// @Success 200 {object} response.APIResponse{data=UserResponse} "Profile updated successfully with updated user details"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid request format or missing required fields"
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired authentication token"
|
||||
// @Failure 409 {object} response.APIResponse "Conflict - Email address already exists for another user"
|
||||
// @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 {
|
||||
// Extract user ID from JWT token context
|
||||
userID, err := GetUserIDFromContext(c)
|
||||
@@ -202,18 +177,19 @@ func (h *Handler) UpdateProfile(c echo.Context) error {
|
||||
}
|
||||
|
||||
// ChangePassword changes current user password
|
||||
// @Summary Change password
|
||||
// @Description Change current user password
|
||||
// @Tags Users
|
||||
// @Summary Change 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 Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param password body ChangePasswordForm true "Password change data"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/v1/change-password [put]
|
||||
// @Param password body ChangePasswordForm true "Password change data including current password and new password"
|
||||
// @Success 200 {object} response.APIResponse "Password changed successfully - user must re-authenticate"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid request format or password policy violation"
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid authentication token or incorrect current password"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid password format or policy requirements not met"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Router /admin/v1/profile/change-password [put]
|
||||
func (h *Handler) ChangePassword(c echo.Context) error {
|
||||
userID, err := GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
@@ -236,16 +212,16 @@ func (h *Handler) ChangePassword(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Logout handles user logout
|
||||
// @Summary Logout user
|
||||
// @Description Logout current user and invalidate tokens
|
||||
// @Tags Users
|
||||
// @Summary Logout authenticated user
|
||||
// @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 Admin-Authorization
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/v1/logout [delete]
|
||||
// @Success 200 {object} response.APIResponse "Logout successful - all tokens invalidated"
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired authentication token"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error - Failed to invalidate tokens"
|
||||
// @Router /admin/v1/profile/logout [delete]
|
||||
func (h *Handler) Logout(c echo.Context) error {
|
||||
// Extract user ID and access token from context
|
||||
userID, err := GetUserIDFromContext(c)
|
||||
@@ -269,19 +245,21 @@ func (h *Handler) Logout(c echo.Context) error {
|
||||
}
|
||||
|
||||
// CreateUser creates a new user (admin only)
|
||||
// @Summary Create user
|
||||
// @Description Create a new user (admin only)
|
||||
// @Tags Users
|
||||
// @Summary Create new user account
|
||||
// @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 Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param user body CreateUserForm true "User creation data"
|
||||
// @Success 201 {object} response.APIResponse{data=UserResponse}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/v1/users [post]
|
||||
// @Param user body CreateUserForm true "User creation data including username, email, password, role, and profile information"
|
||||
// @Success 201 {object} response.APIResponse{data=UserResponse} "User created successfully with assigned role and permissions"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data or missing required fields"
|
||||
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired authentication token"
|
||||
// @Failure 403 {object} response.APIResponse "Forbidden - Insufficient privileges to create users"
|
||||
// @Failure 409 {object} response.APIResponse "Conflict - Username or email already exists"
|
||||
// @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 {
|
||||
// Get current user ID from JWT token
|
||||
currentUserID, err := GetUserIDFromContext(c)
|
||||
@@ -311,7 +289,7 @@ func (h *Handler) CreateUser(c echo.Context) error {
|
||||
// ListUsers lists users with search and filters (admin only)
|
||||
// @Summary List users
|
||||
// @Description List users with search and filters (admin only)
|
||||
// @Tags Users
|
||||
// @Tags Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
@@ -328,7 +306,7 @@ func (h *Handler) CreateUser(c echo.Context) error {
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 403 {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 {
|
||||
var form ListUsersForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
@@ -352,7 +330,7 @@ func (h *Handler) ListUsers(c echo.Context) error {
|
||||
// GetUserByID gets a user by ID (admin only)
|
||||
// @Summary Get user by ID
|
||||
// @Description Get user details by ID (admin only)
|
||||
// @Tags Users
|
||||
// @Tags Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
@@ -363,15 +341,11 @@ func (h *Handler) ListUsers(c echo.Context) error {
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 404 {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 {
|
||||
idStr := c.Param("id")
|
||||
userID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid user ID", "")
|
||||
}
|
||||
|
||||
user, err := h.service.GetUserByID(c.Request().Context(), userID)
|
||||
user, err := h.service.GetUserByID(c.Request().Context(), idStr)
|
||||
if err != nil {
|
||||
return response.NotFound(c, "User not found")
|
||||
}
|
||||
@@ -382,7 +356,7 @@ func (h *Handler) GetUserByID(c echo.Context) error {
|
||||
// UpdateUser updates a user (admin only)
|
||||
// @Summary Update user
|
||||
// @Description Update user information (admin only)
|
||||
// @Tags Users
|
||||
// @Tags Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
@@ -394,7 +368,7 @@ func (h *Handler) GetUserByID(c echo.Context) error {
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 404 {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 {
|
||||
// Extract user ID from JWT token context
|
||||
currentUserID, err := GetUserIDFromContext(c)
|
||||
@@ -403,10 +377,6 @@ func (h *Handler) UpdateUser(c echo.Context) error {
|
||||
}
|
||||
|
||||
idStr := c.Param("id")
|
||||
userID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid user ID", "")
|
||||
}
|
||||
|
||||
var form UpdateUserForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
@@ -419,7 +389,7 @@ func (h *Handler) UpdateUser(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Call service
|
||||
user, err := h.service.UpdateUser(c.Request().Context(), userID, &form, ¤tUserID)
|
||||
user, err := h.service.UpdateUser(c.Request().Context(), idStr, &form, ¤tUserID)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
@@ -430,7 +400,7 @@ func (h *Handler) UpdateUser(c echo.Context) error {
|
||||
// DeleteUser deletes a user (admin only)
|
||||
// @Summary Delete user
|
||||
// @Description Delete a user (admin only)
|
||||
// @Tags Users
|
||||
// @Tags Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
@@ -441,7 +411,7 @@ func (h *Handler) UpdateUser(c echo.Context) error {
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 404 {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 {
|
||||
// Extract user ID from JWT token context
|
||||
currentUserID, err := GetUserIDFromContext(c)
|
||||
@@ -450,12 +420,8 @@ func (h *Handler) DeleteUser(c echo.Context) error {
|
||||
}
|
||||
|
||||
idStr := c.Param("id")
|
||||
userID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid user ID", "")
|
||||
}
|
||||
|
||||
err = h.service.DeleteUser(c.Request().Context(), userID, ¤tUserID)
|
||||
err = h.service.DeleteUser(c.Request().Context(), idStr, ¤tUserID)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
@@ -468,7 +434,7 @@ func (h *Handler) DeleteUser(c echo.Context) error {
|
||||
// UpdateUserStatus updates user status (admin only)
|
||||
// @Summary Update user status
|
||||
// @Description Update user account status (admin only)
|
||||
// @Tags Users
|
||||
// @Tags Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
@@ -480,7 +446,7 @@ func (h *Handler) DeleteUser(c echo.Context) error {
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 404 {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 {
|
||||
currentUserID, err := GetUserIDFromContext(c)
|
||||
if err != nil {
|
||||
@@ -488,10 +454,6 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error {
|
||||
}
|
||||
|
||||
idStr := c.Param("id")
|
||||
userID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid user ID", "")
|
||||
}
|
||||
|
||||
var form UpdateUserStatusForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
@@ -504,7 +466,7 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Call service
|
||||
err = h.service.UpdateUserStatus(c.Request().Context(), userID, &form, ¤tUserID)
|
||||
err = h.service.UpdateUserStatus(c.Request().Context(), idStr, &form, ¤tUserID)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
@@ -517,7 +479,7 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error {
|
||||
// UpdateUserRole updates user role (admin only)
|
||||
// @Summary Update user role
|
||||
// @Description Update user role (admin only)
|
||||
// @Tags Users
|
||||
// @Tags Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
@@ -529,7 +491,7 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error {
|
||||
// @Failure 403 {object} response.APIResponse
|
||||
// @Failure 404 {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 {
|
||||
// Extract user ID from JWT token context
|
||||
currentUserID, err := GetUserIDFromContext(c)
|
||||
@@ -538,10 +500,6 @@ func (h *Handler) UpdateUserRole(c echo.Context) error {
|
||||
}
|
||||
|
||||
idStr := c.Param("id")
|
||||
userID, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid user ID", "")
|
||||
}
|
||||
|
||||
var form UpdateUserRoleForm
|
||||
if err := c.Bind(&form); err != nil {
|
||||
@@ -554,7 +512,7 @@ func (h *Handler) UpdateUserRole(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Call service
|
||||
err = h.service.UpdateUserRole(c.Request().Context(), userID, &form, ¤tUserID)
|
||||
err = h.service.UpdateUserRole(c.Request().Context(), idStr, &form, ¤tUserID)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
@@ -567,7 +525,7 @@ func (h *Handler) UpdateUserRole(c echo.Context) error {
|
||||
// GetUsersByCompanyID gets users by company ID (admin only)
|
||||
// @Summary Get users by company ID
|
||||
// @Description Get users belonging to a specific company (admin only)
|
||||
// @Tags Users
|
||||
// @Tags Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
@@ -579,13 +537,9 @@ func (h *Handler) UpdateUserRole(c echo.Context) error {
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 403 {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 {
|
||||
companyIDStr := c.Param("company_id")
|
||||
companyID, err := uuid.Parse(companyIDStr)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid company ID", "")
|
||||
}
|
||||
|
||||
// Get query parameters
|
||||
limitStr := c.QueryParam("limit")
|
||||
@@ -608,7 +562,7 @@ func (h *Handler) GetUsersByCompanyID(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Call service
|
||||
users, total, err := h.service.GetUsersByCompanyID(c.Request().Context(), companyID, limit, offset)
|
||||
users, total, err := h.service.GetUsersByCompanyID(c.Request().Context(), companyIDStr, limit, offset)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to get users by company")
|
||||
}
|
||||
@@ -631,7 +585,7 @@ func (h *Handler) GetUsersByCompanyID(c echo.Context) error {
|
||||
// GetUsersByRole gets users by role (admin only)
|
||||
// @Summary Get users by role
|
||||
// @Description Get users with a specific role (admin only)
|
||||
// @Tags Users
|
||||
// @Tags Admin-Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
@@ -643,7 +597,7 @@ func (h *Handler) GetUsersByCompanyID(c echo.Context) error {
|
||||
// @Failure 401 {object} response.APIResponse
|
||||
// @Failure 403 {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 {
|
||||
roleStr := c.Param("role")
|
||||
role := UserRole(roleStr)
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
"tm/pkg/response"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// AuthMiddleware validates JWT access tokens and extracts user information
|
||||
@@ -44,7 +44,7 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
|
||||
}
|
||||
|
||||
// Extract user information from token
|
||||
userID, err := uuid.Parse(validationResult.Claims.UserID)
|
||||
userID, err := primitive.ObjectIDFromHex(validationResult.Claims.UserID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to parse user ID from token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -53,7 +53,7 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc {
|
||||
}
|
||||
|
||||
// Store user information in context for handlers to use
|
||||
c.Set("user_id", userID)
|
||||
c.Set("user_id", userID.Hex())
|
||||
c.Set("user_email", validationResult.Claims.Email)
|
||||
c.Set("user_role", validationResult.Claims.Role)
|
||||
c.Set("company_id", validationResult.Claims.CompanyID)
|
||||
@@ -147,10 +147,10 @@ func (h *Handler) CompanyAccessMiddleware() echo.MiddlewareFunc {
|
||||
}
|
||||
|
||||
// GetUserIDFromContext extracts user ID from Echo context
|
||||
func GetUserIDFromContext(c echo.Context) (uuid.UUID, error) {
|
||||
userID, ok := c.Get("user_id").(uuid.UUID)
|
||||
func GetUserIDFromContext(c echo.Context) (string, error) {
|
||||
userID, ok := c.Get("user_id").(string)
|
||||
if !ok {
|
||||
return uuid.Nil, echo.NewHTTPError(http.StatusUnauthorized, "User ID not found in context")
|
||||
return "", echo.NewHTTPError(http.StatusUnauthorized, "User ID not found in context")
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
+129
-207
@@ -7,104 +7,58 @@ import (
|
||||
"tm/pkg/logger"
|
||||
mongopkg "tm/pkg/mongo"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// Repository defines methods for user data access
|
||||
type Repository interface {
|
||||
Create(ctx context.Context, user *User) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
GetByID(ctx context.Context, id string) (*User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
GetByUsername(ctx context.Context, username string) (*User, error)
|
||||
Update(ctx context.Context, user *User) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
List(ctx context.Context, limit, offset int) ([]*User, error)
|
||||
Search(ctx context.Context, search string, status *string, role *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*User, error)
|
||||
GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, error)
|
||||
Search(ctx context.Context, search string, status *string, role *string, companyID *string, limit, offset int, sortBy, sortOrder string) ([]*User, error)
|
||||
GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, error)
|
||||
GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error)
|
||||
UpdateLastLogin(ctx context.Context, id uuid.UUID) error
|
||||
CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error)
|
||||
UpdateLastLogin(ctx context.Context, id string) error
|
||||
CountByCompanyID(ctx context.Context, companyID string) (int64, error)
|
||||
}
|
||||
|
||||
// userRepository implements the Repository interface
|
||||
// userRepository implements the Repository interface using the MongoDB ORM
|
||||
type userRepository struct {
|
||||
collection *mongo.Collection
|
||||
logger logger.Logger
|
||||
ormRepo mongopkg.Repository[User]
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewUserRepository creates a new user repository
|
||||
func NewUserRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
|
||||
collection := mongoManager.GetCollection("users")
|
||||
// Create indexes using the ORM's index management
|
||||
indexes := []mongopkg.Index{
|
||||
*mongopkg.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}),
|
||||
*mongopkg.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}),
|
||||
*mongopkg.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}),
|
||||
*mongopkg.NewIndex("role_idx", bson.D{{Key: "role", Value: 1}}),
|
||||
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
||||
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||
*mongopkg.CreateTextIndex("search_idx", "full_name", "email", "username", "department", "position"),
|
||||
}
|
||||
|
||||
// Create indexes
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Email index (unique)
|
||||
emailIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "email", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
}
|
||||
|
||||
// Username index (unique)
|
||||
usernameIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "username", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
}
|
||||
|
||||
// Company ID index
|
||||
companyIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "company_id", Value: 1}},
|
||||
}
|
||||
|
||||
// Role index
|
||||
roleIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "role", Value: 1}},
|
||||
}
|
||||
|
||||
// Status index
|
||||
statusIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "status", Value: 1}},
|
||||
}
|
||||
|
||||
// Created at index
|
||||
createdAtIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "created_at", Value: -1}},
|
||||
}
|
||||
|
||||
// Full text search index
|
||||
textIndex := mongo.IndexModel{
|
||||
Keys: bson.D{
|
||||
{Key: "full_name", Value: "text"},
|
||||
{Key: "email", Value: "text"},
|
||||
{Key: "username", Value: "text"},
|
||||
{Key: "department", Value: "text"},
|
||||
{Key: "position", Value: "text"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
emailIndex,
|
||||
usernameIndex,
|
||||
companyIndex,
|
||||
roleIndex,
|
||||
statusIndex,
|
||||
createdAtIndex,
|
||||
textIndex,
|
||||
})
|
||||
|
||||
err := mongoManager.CreateIndexes("users", indexes)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create user indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Create ORM repository
|
||||
ormRepo := mongopkg.NewRepository[User](mongoManager.GetCollection("users"), logger)
|
||||
|
||||
return &userRepository{
|
||||
collection: collection,
|
||||
logger: logger,
|
||||
ormRepo: ormRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,25 +66,22 @@ func NewUserRepository(mongoManager *mongopkg.ConnectionManager, logger logger.L
|
||||
func (r *userRepository) Create(ctx context.Context, user *User) error {
|
||||
// Set created/updated timestamps using Unix timestamps
|
||||
now := time.Now().Unix()
|
||||
user.CreatedAt = now
|
||||
user.UpdatedAt = now
|
||||
user.SetCreatedAt(now)
|
||||
user.SetUpdatedAt(now)
|
||||
|
||||
// Insert user
|
||||
_, err := r.collection.InsertOne(ctx, user)
|
||||
// Use ORM to create user
|
||||
err := r.ormRepo.Create(ctx, user)
|
||||
if err != nil {
|
||||
if mongo.IsDuplicateKeyError(err) {
|
||||
return errors.New("user already exists")
|
||||
}
|
||||
r.logger.Error("Failed to create user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"email": user.Email,
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("User created successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
})
|
||||
@@ -139,35 +90,28 @@ func (r *userRepository) Create(ctx context.Context, user *User) error {
|
||||
}
|
||||
|
||||
// GetByID retrieves a user by ID
|
||||
func (r *userRepository) GetByID(ctx context.Context, id uuid.UUID) (*User, error) {
|
||||
var user User
|
||||
|
||||
filter := bson.M{"_id": id}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&user)
|
||||
|
||||
func (r *userRepository) GetByID(ctx context.Context, id string) (*User, error) {
|
||||
user, err := r.ormRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
r.logger.Error("Failed to get user by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetByEmail retrieves a user by email
|
||||
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*User, error) {
|
||||
var user User
|
||||
|
||||
filter := bson.M{"email": email}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&user)
|
||||
|
||||
user, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
r.logger.Error("Failed to get user by email", map[string]interface{}{
|
||||
@@ -177,18 +121,15 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*User, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetByUsername retrieves a user by username
|
||||
func (r *userRepository) GetByUsername(ctx context.Context, username string) (*User, error) {
|
||||
var user User
|
||||
|
||||
filter := bson.M{"username": username}
|
||||
err := r.collection.FindOne(ctx, filter).Decode(&user)
|
||||
|
||||
user, err := r.ormRepo.FindOne(ctx, filter)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
r.logger.Error("Failed to get user by username", map[string]interface{}{
|
||||
@@ -198,32 +139,26 @@ func (r *userRepository) GetByUsername(ctx context.Context, username string) (*U
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Update updates a user
|
||||
func (r *userRepository) Update(ctx context.Context, user *User) error {
|
||||
// Set updated timestamp using Unix timestamp
|
||||
user.UpdatedAt = time.Now().Unix()
|
||||
user.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
filter := bson.M{"_id": user.ID}
|
||||
update := bson.M{"$set": user}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
// Use ORM to update user
|
||||
err := r.ormRepo.Update(ctx, user)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
r.logger.Info("User updated successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
})
|
||||
|
||||
@@ -231,30 +166,29 @@ func (r *userRepository) Update(ctx context.Context, user *User) error {
|
||||
}
|
||||
|
||||
// Delete deletes a user (soft delete by setting status to inactive)
|
||||
func (r *userRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"status": UserStatusInactive,
|
||||
"updated_at": time.Now().Unix(),
|
||||
},
|
||||
func (r *userRepository) Delete(ctx context.Context, id string) error {
|
||||
// Get user first
|
||||
user, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
// Update status to inactive
|
||||
user.Status = UserStatusInactive
|
||||
user.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, user)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
r.logger.Info("User deleted successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
@@ -262,18 +196,18 @@ func (r *userRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
|
||||
// List retrieves users with pagination
|
||||
func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*User, error) {
|
||||
var users []*User
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
// Only active users by default
|
||||
filter := bson.M{"status": bson.M{"$ne": UserStatusInactive}}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
// Use ORM to find users
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to list users", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -282,22 +216,18 @@ func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*User,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &users); err != nil {
|
||||
r.logger.Error("Failed to decode users", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
// Convert []User to []*User
|
||||
users := make([]*User, len(result.Items))
|
||||
for i := range result.Items {
|
||||
users[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// Search retrieves users with search and filters
|
||||
func (r *userRepository) Search(ctx context.Context, search string, status *string, role *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*User, error) {
|
||||
var users []*User
|
||||
|
||||
func (r *userRepository) Search(ctx context.Context, search string, status *string, role *string, companyID *string, limit, offset int, sortBy, sortOrder string) ([]*User, error) {
|
||||
// Build filter
|
||||
filter := bson.M{}
|
||||
|
||||
@@ -317,23 +247,24 @@ func (r *userRepository) Search(ctx context.Context, search string, status *stri
|
||||
filter["company_id"] = *companyID
|
||||
}
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset)
|
||||
|
||||
// Set sort
|
||||
if sortBy != "" {
|
||||
sortValue := 1
|
||||
if sortOrder == "desc" {
|
||||
sortValue = -1
|
||||
pagination.SortDesc(sortBy)
|
||||
} else {
|
||||
pagination.SortAsc(sortBy)
|
||||
}
|
||||
opts.SetSort(bson.D{{Key: sortBy, Value: sortValue}})
|
||||
} else {
|
||||
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Default sort
|
||||
pagination.SortDesc("created_at") // Default sort
|
||||
}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
// Use ORM to find users
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build())
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to search users", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -341,27 +272,24 @@ func (r *userRepository) Search(ctx context.Context, search string, status *stri
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &users); err != nil {
|
||||
r.logger.Error("Failed to decode users from search", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, err
|
||||
// Convert []User to []*User
|
||||
users := make([]*User, len(result.Items))
|
||||
for i := range result.Items {
|
||||
users[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetByCompanyID retrieves users by company ID with pagination
|
||||
func (r *userRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, error) {
|
||||
var users []*User
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
|
||||
func (r *userRepository) GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, error) {
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
// Filter by company ID and active status
|
||||
filter := bson.M{
|
||||
@@ -369,24 +297,22 @@ func (r *userRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID
|
||||
"status": bson.M{"$ne": UserStatusInactive},
|
||||
}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
// Use ORM to find users
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get users by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID.String(),
|
||||
"company_id": companyID,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &users); err != nil {
|
||||
r.logger.Error("Failed to decode users by company", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID.String(),
|
||||
})
|
||||
return nil, err
|
||||
// Convert []User to []*User
|
||||
users := make([]*User, len(result.Items))
|
||||
for i := range result.Items {
|
||||
users[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return users, nil
|
||||
@@ -394,13 +320,12 @@ func (r *userRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID
|
||||
|
||||
// GetByRole retrieves users by role with pagination
|
||||
func (r *userRepository) GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) {
|
||||
var users []*User
|
||||
|
||||
// Build options
|
||||
opts := options.Find()
|
||||
opts.SetLimit(int64(limit))
|
||||
opts.SetSkip(int64(offset))
|
||||
opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc
|
||||
// Build pagination
|
||||
pagination := mongopkg.NewPaginationBuilder().
|
||||
Limit(limit).
|
||||
Skip(offset).
|
||||
SortDesc("created_at").
|
||||
Build()
|
||||
|
||||
// Filter by role and active status
|
||||
filter := bson.M{
|
||||
@@ -408,7 +333,8 @@ func (r *userRepository) GetByRole(ctx context.Context, role UserRole, limit, of
|
||||
"status": bson.M{"$ne": UserStatusInactive},
|
||||
}
|
||||
|
||||
cursor, err := r.collection.Find(ctx, filter, opts)
|
||||
// Use ORM to find users
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to get users by role", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -418,61 +344,57 @@ func (r *userRepository) GetByRole(ctx context.Context, role UserRole, limit, of
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
if err = cursor.All(ctx, &users); err != nil {
|
||||
r.logger.Error("Failed to decode users by role", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"role": role,
|
||||
})
|
||||
return nil, err
|
||||
// Convert []User to []*User
|
||||
users := make([]*User, len(result.Items))
|
||||
for i := range result.Items {
|
||||
users[i] = &result.Items[i]
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UpdateLastLogin updates the last login timestamp
|
||||
func (r *userRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID) error {
|
||||
filter := bson.M{"_id": id}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"last_login_at": time.Now().Unix(),
|
||||
"updated_at": time.Now().Unix(),
|
||||
},
|
||||
func (r *userRepository) UpdateLastLogin(ctx context.Context, id string) error {
|
||||
// Get user first
|
||||
user, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := r.collection.UpdateOne(ctx, filter, update)
|
||||
// Update last login timestamp
|
||||
user.LastLoginAt = &[]int64{time.Now().Unix()}[0]
|
||||
user.SetUpdatedAt(time.Now().Unix())
|
||||
|
||||
// Update in database
|
||||
err = r.ormRepo.Update(ctx, user)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update last login", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
|
||||
r.logger.Info("Last login updated successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountByCompanyID counts users by company ID
|
||||
func (r *userRepository) CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) {
|
||||
func (r *userRepository) CountByCompanyID(ctx context.Context, companyID string) (int64, error) {
|
||||
filter := bson.M{
|
||||
"company_id": companyID,
|
||||
"status": bson.M{"$ne": UserStatusInactive},
|
||||
}
|
||||
|
||||
count, err := r.collection.CountDocuments(ctx, filter)
|
||||
count, err := r.ormRepo.Count(ctx, filter)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to count users by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID.String(),
|
||||
"company_id": companyID,
|
||||
})
|
||||
return 0, err
|
||||
}
|
||||
|
||||
+68
-81
@@ -8,28 +8,28 @@ import (
|
||||
"tm/pkg/authorization"
|
||||
"tm/pkg/logger"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Service defines business logic for user operations
|
||||
type Service interface {
|
||||
CreateUser(ctx context.Context, form *CreateUserForm, createdBy *uuid.UUID) (*User, error)
|
||||
CreateUser(ctx context.Context, form *CreateUserForm, createdBy *string) (*User, error)
|
||||
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
|
||||
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
|
||||
GetProfile(ctx context.Context, userID uuid.UUID) (*User, error)
|
||||
UpdateProfile(ctx context.Context, userID uuid.UUID, form *UpdateProfileForm) error
|
||||
ChangePassword(ctx context.Context, userID uuid.UUID, form *ChangePasswordForm) error
|
||||
GetProfile(ctx context.Context, userID string) (*User, error)
|
||||
UpdateProfile(ctx context.Context, userID string, form *UpdateProfileForm) error
|
||||
ChangePassword(ctx context.Context, userID string, form *ChangePasswordForm) error
|
||||
ResetPassword(ctx context.Context, form *ResetPasswordForm) error
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (*User, error)
|
||||
UpdateUser(ctx context.Context, id uuid.UUID, form *UpdateUserForm, updatedBy *uuid.UUID) (*User, error)
|
||||
DeleteUser(ctx context.Context, id uuid.UUID, deletedBy *uuid.UUID) error
|
||||
GetUserByID(ctx context.Context, id string) (*User, error)
|
||||
UpdateUser(ctx context.Context, id string, form *UpdateUserForm, updatedBy *string) (*User, error)
|
||||
DeleteUser(ctx context.Context, id string, deletedBy *string) error
|
||||
ListUsers(ctx context.Context, form *ListUsersForm) (*UserListResponse, error)
|
||||
UpdateUserStatus(ctx context.Context, id uuid.UUID, form *UpdateUserStatusForm, updatedBy *uuid.UUID) error
|
||||
UpdateUserRole(ctx context.Context, id uuid.UUID, form *UpdateUserRoleForm, updatedBy *uuid.UUID) error
|
||||
GetUsersByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, int64, error)
|
||||
UpdateUserStatus(ctx context.Context, id string, form *UpdateUserStatusForm, updatedBy *string) error
|
||||
UpdateUserRole(ctx context.Context, id string, form *UpdateUserRoleForm, updatedBy *string) error
|
||||
GetUsersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, int64, error)
|
||||
GetUsersByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error)
|
||||
Logout(ctx context.Context, userID uuid.UUID, accessToken string) error
|
||||
Logout(ctx context.Context, userID string, accessToken string) error
|
||||
}
|
||||
|
||||
// userService implements the Service interface
|
||||
@@ -51,7 +51,7 @@ func NewUserService(repository Repository, logger logger.Logger, authService aut
|
||||
}
|
||||
|
||||
// CreateUser creates a new user
|
||||
func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, createdBy *uuid.UUID) (*User, error) {
|
||||
func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, createdBy *string) (*User, error) {
|
||||
// Check if email already exists
|
||||
existingUser, _ := s.repository.GetByEmail(ctx, form.Email)
|
||||
if existingUser != nil {
|
||||
@@ -74,13 +74,9 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea
|
||||
}
|
||||
|
||||
// Parse optional fields
|
||||
var companyID *uuid.UUID
|
||||
var companyID *string
|
||||
if form.CompanyID != nil {
|
||||
parsedID, err := uuid.Parse(*form.CompanyID)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid company ID")
|
||||
}
|
||||
companyID = &parsedID
|
||||
companyID = form.CompanyID
|
||||
}
|
||||
|
||||
// Validate role
|
||||
@@ -91,7 +87,6 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea
|
||||
|
||||
// Create user
|
||||
user := &User{
|
||||
ID: uuid.New(),
|
||||
FullName: form.FullName,
|
||||
Username: form.Username,
|
||||
Email: form.Email,
|
||||
@@ -119,11 +114,11 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea
|
||||
}
|
||||
|
||||
s.logger.Info("User created successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"created_by": createdBy.String(),
|
||||
"created_by": createdBy,
|
||||
})
|
||||
|
||||
return user, nil
|
||||
@@ -158,29 +153,29 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(form.Password))
|
||||
if err != nil {
|
||||
s.logger.Warn("Login attempt with wrong password", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
})
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// Update last login
|
||||
err = s.repository.UpdateLastLogin(ctx, user.ID)
|
||||
err = s.repository.UpdateLastLogin(ctx, user.ID.Hex())
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update last login", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// Generate JWT tokens using authorization service
|
||||
companyID := ""
|
||||
if user.CompanyID != nil {
|
||||
companyID = user.CompanyID.String()
|
||||
companyID = *user.CompanyID
|
||||
}
|
||||
|
||||
tokenPair, err := s.authService.GenerateTokenPair(
|
||||
user.ID.String(),
|
||||
user.ID.Hex(),
|
||||
user.Email,
|
||||
string(user.Role),
|
||||
companyID,
|
||||
@@ -188,13 +183,13 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate JWT tokens", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
})
|
||||
return nil, errors.New("failed to generate authentication tokens")
|
||||
}
|
||||
|
||||
s.logger.Info("User logged in successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
})
|
||||
@@ -235,7 +230,7 @@ func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*A
|
||||
}
|
||||
|
||||
// Get user information
|
||||
userID, err := uuid.Parse(validationResult.Claims.UserID)
|
||||
userID, err := primitive.ObjectIDFromHex(validationResult.Claims.UserID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to parse user ID from token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -243,11 +238,11 @@ func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*A
|
||||
return nil, errors.New("invalid token format")
|
||||
}
|
||||
|
||||
user, err := s.repository.GetByID(ctx, userID)
|
||||
user, err := s.repository.GetByID(ctx, userID.Hex())
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get user for token refresh", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID.Hex(),
|
||||
})
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
@@ -258,7 +253,7 @@ func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*A
|
||||
}
|
||||
|
||||
s.logger.Info("Access token refreshed successfully", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
})
|
||||
|
||||
@@ -271,7 +266,7 @@ func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*A
|
||||
}
|
||||
|
||||
// ChangePassword changes user password
|
||||
func (s *userService) ChangePassword(ctx context.Context, userID uuid.UUID, form *ChangePasswordForm) error {
|
||||
func (s *userService) ChangePassword(ctx context.Context, userID string, form *ChangePasswordForm) error {
|
||||
// Get user
|
||||
user, err := s.repository.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
@@ -289,7 +284,7 @@ func (s *userService) ChangePassword(ctx context.Context, userID uuid.UUID, form
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to hash new password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
return errors.New("failed to process new password")
|
||||
}
|
||||
@@ -300,13 +295,13 @@ func (s *userService) ChangePassword(ctx context.Context, userID uuid.UUID, form
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
return errors.New("failed to update password")
|
||||
}
|
||||
|
||||
s.logger.Info("Password changed successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
|
||||
return nil
|
||||
@@ -326,7 +321,7 @@ func (s *userService) ResetPassword(ctx context.Context, form *ResetPasswordForm
|
||||
|
||||
// TODO: Implement password reset logic (send email with reset link)
|
||||
s.logger.Info("Password reset requested", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
})
|
||||
|
||||
@@ -334,7 +329,7 @@ func (s *userService) ResetPassword(ctx context.Context, form *ResetPasswordForm
|
||||
}
|
||||
|
||||
// GetUserByID retrieves a user by ID
|
||||
func (s *userService) GetUserByID(ctx context.Context, id uuid.UUID) (*User, error) {
|
||||
func (s *userService) GetUserByID(ctx context.Context, id string) (*User, error) {
|
||||
user, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -344,30 +339,30 @@ func (s *userService) GetUserByID(ctx context.Context, id uuid.UUID) (*User, err
|
||||
}
|
||||
|
||||
// GetProfile retrieves the current user's profile
|
||||
func (s *userService) GetProfile(ctx context.Context, userID uuid.UUID) (*User, error) {
|
||||
func (s *userService) GetProfile(ctx context.Context, userID string) (*User, error) {
|
||||
user, err := s.repository.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get user profile", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
|
||||
s.logger.Info("User profile retrieved successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdateProfile updates the current user's profile
|
||||
func (s *userService) UpdateProfile(ctx context.Context, userID uuid.UUID, form *UpdateProfileForm) error {
|
||||
func (s *userService) UpdateProfile(ctx context.Context, userID string, form *UpdateProfileForm) error {
|
||||
user, err := s.repository.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get user for profile update", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
return errors.New("user not found")
|
||||
}
|
||||
@@ -397,20 +392,20 @@ func (s *userService) UpdateProfile(ctx context.Context, userID uuid.UUID, form
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update user profile", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
return errors.New("failed to update profile")
|
||||
}
|
||||
|
||||
s.logger.Info("User profile updated successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUser updates user information
|
||||
func (s *userService) UpdateUser(ctx context.Context, id uuid.UUID, form *UpdateUserForm, updatedBy *uuid.UUID) (*User, error) {
|
||||
func (s *userService) UpdateUser(ctx context.Context, id string, form *UpdateUserForm, updatedBy *string) (*User, error) {
|
||||
// Get user
|
||||
user, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
@@ -425,7 +420,7 @@ func (s *userService) UpdateUser(ctx context.Context, id uuid.UUID, form *Update
|
||||
if form.Username != nil {
|
||||
// Check if username already exists
|
||||
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")
|
||||
}
|
||||
user.Username = *form.Username
|
||||
@@ -434,7 +429,7 @@ func (s *userService) UpdateUser(ctx context.Context, id uuid.UUID, form *Update
|
||||
if form.Email != nil {
|
||||
// Check if email already exists
|
||||
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")
|
||||
}
|
||||
user.Email = *form.Email
|
||||
@@ -449,11 +444,7 @@ func (s *userService) UpdateUser(ctx context.Context, id uuid.UUID, form *Update
|
||||
}
|
||||
|
||||
if form.CompanyID != nil {
|
||||
parsedID, err := uuid.Parse(*form.CompanyID)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid company ID")
|
||||
}
|
||||
user.CompanyID = &parsedID
|
||||
user.CompanyID = form.CompanyID
|
||||
}
|
||||
|
||||
if form.Department != nil {
|
||||
@@ -484,21 +475,21 @@ func (s *userService) UpdateUser(ctx context.Context, id uuid.UUID, form *Update
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
})
|
||||
return nil, errors.New("failed to update user")
|
||||
}
|
||||
|
||||
s.logger.Info("User updated successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
"updated_by": updatedBy.String(),
|
||||
"user_id": id,
|
||||
"updated_by": *updatedBy,
|
||||
})
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// DeleteUser deletes a user (soft delete)
|
||||
func (s *userService) DeleteUser(ctx context.Context, id uuid.UUID, deletedBy *uuid.UUID) error {
|
||||
func (s *userService) DeleteUser(ctx context.Context, id string, deletedBy *string) error {
|
||||
// Get user to check if exists
|
||||
user, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
@@ -514,14 +505,14 @@ func (s *userService) DeleteUser(ctx context.Context, id uuid.UUID, deletedBy *u
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to delete user", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
})
|
||||
return errors.New("failed to delete user")
|
||||
}
|
||||
|
||||
s.logger.Info("User deleted successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
"deleted_by": deletedBy.String(),
|
||||
"user_id": id,
|
||||
"deleted_by": *deletedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
@@ -556,13 +547,9 @@ func (s *userService) ListUsers(ctx context.Context, form *ListUsersForm) (*User
|
||||
}
|
||||
|
||||
// Parse company ID if provided
|
||||
var companyID *uuid.UUID
|
||||
var companyID *string
|
||||
if form.CompanyID != nil {
|
||||
parsedID, err := uuid.Parse(*form.CompanyID)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid company ID")
|
||||
}
|
||||
companyID = &parsedID
|
||||
companyID = form.CompanyID
|
||||
}
|
||||
|
||||
// Get users
|
||||
@@ -594,7 +581,7 @@ func (s *userService) ListUsers(ctx context.Context, form *ListUsersForm) (*User
|
||||
}
|
||||
|
||||
// UpdateUserStatus updates user status
|
||||
func (s *userService) UpdateUserStatus(ctx context.Context, id uuid.UUID, form *UpdateUserStatusForm, updatedBy *uuid.UUID) error {
|
||||
func (s *userService) UpdateUserStatus(ctx context.Context, id string, form *UpdateUserStatusForm, updatedBy *string) error {
|
||||
// Get user
|
||||
user, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
@@ -610,23 +597,23 @@ func (s *userService) UpdateUserStatus(ctx context.Context, id uuid.UUID, form *
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update user status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
"status": form.Status,
|
||||
})
|
||||
return errors.New("failed to update user status")
|
||||
}
|
||||
|
||||
s.logger.Info("User status updated successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
"status": form.Status,
|
||||
"updated_by": updatedBy.String(),
|
||||
"updated_by": *updatedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserRole updates user role
|
||||
func (s *userService) UpdateUserRole(ctx context.Context, id uuid.UUID, form *UpdateUserRoleForm, updatedBy *uuid.UUID) error {
|
||||
func (s *userService) UpdateUserRole(ctx context.Context, id string, form *UpdateUserRoleForm, updatedBy *string) error {
|
||||
// Get user
|
||||
user, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
@@ -648,28 +635,28 @@ func (s *userService) UpdateUserRole(ctx context.Context, id uuid.UUID, form *Up
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update user role", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
"role": form.Role,
|
||||
})
|
||||
return errors.New("failed to update user role")
|
||||
}
|
||||
|
||||
s.logger.Info("User role updated successfully", map[string]interface{}{
|
||||
"user_id": id.String(),
|
||||
"user_id": id,
|
||||
"role": form.Role,
|
||||
"updated_by": updatedBy.String(),
|
||||
"updated_by": *updatedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUsersByCompanyID retrieves users by company ID with pagination
|
||||
func (s *userService) GetUsersByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, int64, error) {
|
||||
func (s *userService) GetUsersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, int64, error) {
|
||||
users, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get users by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID.String(),
|
||||
"company_id": companyID,
|
||||
})
|
||||
return nil, 0, errors.New("failed to get users by company")
|
||||
}
|
||||
@@ -679,7 +666,7 @@ func (s *userService) GetUsersByCompanyID(ctx context.Context, companyID uuid.UU
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to count users by company ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"company_id": companyID.String(),
|
||||
"company_id": companyID,
|
||||
})
|
||||
return users, 0, nil // Return users even if count fails
|
||||
}
|
||||
@@ -702,19 +689,19 @@ func (s *userService) GetUsersByRole(ctx context.Context, role UserRole, limit,
|
||||
}
|
||||
|
||||
// Logout logs out a user and invalidates tokens
|
||||
func (s *userService) Logout(ctx context.Context, userID uuid.UUID, accessToken string) error {
|
||||
func (s *userService) Logout(ctx context.Context, userID string, accessToken string) error {
|
||||
// Invalidate the access token
|
||||
err := s.authService.InvalidateToken(accessToken)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to invalidate access token", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
// Don't return error here as the user should still be logged out
|
||||
}
|
||||
|
||||
s.logger.Info("User logged out successfully", map[string]interface{}{
|
||||
"user_id": userID.String(),
|
||||
"user_id": userID,
|
||||
})
|
||||
|
||||
return nil
|
||||
|
||||
@@ -56,7 +56,7 @@ The logger interface remains the same - **no code changes required** in existing
|
||||
```go
|
||||
// All existing code continues to work
|
||||
log.Info("User logged in", map[string]interface{}{
|
||||
"user_id": user.ID.String(),
|
||||
"user_id": user.ID.Hex(),
|
||||
"email": user.Email,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// Logger defines the interface for logging operations
|
||||
type Logger interface {
|
||||
Info(message string, fields map[string]interface{})
|
||||
@@ -28,19 +32,19 @@ type IDSetter interface {
|
||||
|
||||
// Model provides a common base for MongoDB documents
|
||||
type Model struct {
|
||||
ID string `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
CreatedAt int64 `bson:"created_at" json:"created_at"`
|
||||
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
|
||||
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
CreatedAt int64 `bson:"created_at" json:"created_at"`
|
||||
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// GetID returns the document ID
|
||||
func (b *Model) GetID() string {
|
||||
return b.ID
|
||||
return b.ID.Hex()
|
||||
}
|
||||
|
||||
// SetID sets the document ID
|
||||
func (b *Model) SetID(id string) {
|
||||
b.ID = id
|
||||
b.ID, _ = primitive.ObjectIDFromHex(id)
|
||||
}
|
||||
|
||||
// SetCreatedAt sets the creation timestamp
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user