diff --git a/README.md b/README.md index 41dba6f..f5442e4 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,78 @@ 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) +- **Customers-Admin**: Web panel customer management with advanced filtering +- **Customers-Authorization**: Mobile app customer authentication and profile access +- **Companies-Admin**: Comprehensive company management with search, filtering, and analytics +- **Companies-Mobile**: Mobile-optimized company lookup and information retrieval + +#### 🔧 **Documentation Access** +- **Local Development**: `http://localhost:8081/swagger/` +- **Interactive Testing**: All endpoints can be tested directly from the Swagger UI +- **Authentication**: Use the "Authorize" button to set Bearer tokens for protected endpoints +- **Export Options**: JSON and YAML formats available for API specifications + +#### 📝 **Enhanced Features** +- **Detailed Descriptions**: Each endpoint includes comprehensive business logic explanations +- **Parameter Validation**: Complete validation rules and constraints for all input parameters +- **Response Examples**: Real-world response examples with proper HTTP status codes +- **Security Schemes**: JWT Bearer token authentication clearly documented +- **Error Handling**: Comprehensive error response documentation with troubleshooting guides +- **Filtering & Pagination**: Advanced query parameters for list endpoints with examples +- **Business Logic**: Context-aware descriptions explaining when and how to use each endpoint + +#### 🏗️ **Technical Specifications** +- **OpenAPI/Swagger 2.0**: Industry-standard API documentation format +- **Auto-Generation**: Documentation automatically updated from code annotations +- **Type Safety**: Strong typing with Go struct validation and examples +- **Validation Rules**: Complete govalidator integration for request validation +- **Consistent Response Format**: Standardized API response structure with metadata + +#### 💡 **Usage Examples** +```bash +# Access Swagger UI +curl http://localhost:8081/swagger/ + +# Download API specification +curl http://localhost:8081/swagger/doc.json + +# Test authentication endpoint +curl -X POST "http://localhost:8081/admin/v1/login" \ + -H "Content-Type: application/json" \ + -d '{"username": "admin", "password": "password"}' +``` + +#### 🔐 **Authentication Testing** +1. Navigate to Swagger UI at `/swagger/` +2. Click "Authorize" button +3. Enter: `Bearer ` +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 diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 0e69ada..aa81eda 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -9,78 +9,21 @@ const docTemplate = `{ "info": { "description": "{{escape .Description}}", "title": "{{.Title}}", - "termsOfService": "http://swagger.io/terms/", + "termsOfService": "https://tender-management.com/terms", "contact": { - "name": "API Support", - "url": "http://www.swagger.io/support", - "email": "support@swagger.io" + "name": "Tender Management API Support", + "url": "https://tender-management.com/support", + "email": "api-support@tender-management.com" }, "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" }, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/admin/v1/change-password": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Change current user password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Change password", - "parameters": [ - { - "description": "Password change data", - "name": "password", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.ChangePasswordForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/companies": { "get": { "security": [ @@ -1098,82 +1041,6 @@ const docTemplate = `{ } } }, - "/admin/v1/companies/{id}/assign-customer": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Assign a customer to a company for login credentials", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Companies-Admin" - ], - "summary": "Assign customer to company", - "parameters": [ - { - "type": "string", - "description": "Company ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Customer assignment information", - "name": "assignment", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/company.AssignCustomerForm" - } - } - ], - "responses": { - "200": { - "description": "Customer assigned successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request - Invalid input data", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not found - Company not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "409": { - "description": "Conflict - Customer already assigned", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "422": { - "description": "Validation error - Invalid request data", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/companies/{id}/status": { "patch": { "security": [ @@ -1524,61 +1391,6 @@ const docTemplate = `{ } } }, - "/admin/v1/companies/{id}/unassign-customer": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Remove customer assignment from a company", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Companies-Admin" - ], - "summary": "Unassign customer from company", - "parameters": [ - { - "type": "string", - "description": "Company ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Customer unassigned successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request - Invalid company ID", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not found - Company not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/companies/{id}/verification": { "patch": { "security": [ @@ -2200,6 +2012,174 @@ const docTemplate = `{ } } }, + "/admin/v1/customers/with-companies": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "List customers with companies and filters", + "parameters": [ + { + "type": "string", + "description": "Search term to filter customers by name, email, or company name", + "name": "search", + "in": "query" + }, + { + "enum": [ + "individual", + "company", + "government" + ], + "type": "string", + "description": "Filter by customer type", + "name": "type", + "in": "query" + }, + { + "enum": [ + "active", + "inactive", + "suspended", + "pending" + ], + "type": "string", + "description": "Filter by customer status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "format": "uuid", + "description": "Filter by company ID", + "name": "company_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by industry", + "name": "industry", + "in": "query" + }, + { + "type": "boolean", + "description": "Filter by verification status", + "name": "is_verified", + "in": "query" + }, + { + "type": "boolean", + "description": "Filter by compliance status", + "name": "is_compliant", + "in": "query" + }, + { + "type": "string", + "description": "Filter by preferred language", + "name": "language", + "in": "query" + }, + { + "type": "string", + "description": "Filter by preferred currency", + "name": "currency", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of customers per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of customers to skip for pagination", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "email", + "company_name", + "created_at", + "updated_at", + "status" + ], + "type": "string", + "default": "created_at", + "description": "Field to sort by", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "default": "desc", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Customers with companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerListResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers/{id}": { "get": { "security": [ @@ -2461,6 +2441,146 @@ const docTemplate = `{ } } }, + "/admin/v1/customers/{id}/companies/assign": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Assign one or more companies to a specific customer.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Assign companies to a customer", + "parameters": [ + { + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "List of company IDs to assign", + "name": "companies", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.AssignCompaniesForm" + } + } + ], + "responses": { + "200": { + "description": "Companies assigned successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/companies/remove": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove one or more companies from a specific customer.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Remove companies from a customer", + "parameters": [ + { + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "List of company IDs to remove", + "name": "companies", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.RemoveCompaniesForm" + } + } + ], + "responses": { + "200": { + "description": "Companies removed successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers/{id}/status": { "patch": { "security": [ @@ -2726,32 +2846,14 @@ const docTemplate = `{ } } }, - "/admin/v1/health": { + "/admin/v1/customers/{id}/with-companies": { "get": { - "description": "Get server health status", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Health" - ], - "summary": "Health check", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/main.HealthResponse" - } + "security": [ + { + "BearerAuth": [] } - } - } - }, - "/admin/v1/login": { - "post": { - "description": "Authenticate user with username and password", + ], + "description": "Retrieve detailed customer information by customer ID including assigned companies", "consumes": [ "application/json" ], @@ -2759,23 +2861,21 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Authorization" + "Customers-Admin" ], - "summary": "Login user", + "summary": "Get customer by ID with companies", "parameters": [ { - "description": "Login credentials", - "name": "login", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.LoginForm" - } + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "OK", + "description": "Customer retrieved successfully", "schema": { "allOf": [ { @@ -2785,7 +2885,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/user.AuthResponse" + "$ref": "#/definitions/customer.CustomerResponse" } } } @@ -2793,19 +2893,19 @@ const docTemplate = `{ } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid customer ID", "schema": { "$ref": "#/definitions/response.APIResponse" } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2813,14 +2913,9 @@ const docTemplate = `{ } } }, - "/admin/v1/logout": { - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Logout current user and invalidate tokens", + "/admin/v1/health": { + "get": { + "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.", "consumes": [ "application/json" ], @@ -2828,26 +2923,20 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Users" + "Health" ], - "summary": "Logout user", + "summary": "Health check endpoint", "responses": { "200": { - "description": "OK", + "description": "System is healthy and operational", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/main.HealthResponse" } }, - "401": { - "description": "Unauthorized", + "503": { + "description": "System is unhealthy or experiencing issues", "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/main.HealthResponse" } } } @@ -2860,7 +2949,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Get current user profile information", + "description": "Retrieve complete profile information for the currently authenticated user including personal details, role information, permissions, and account status. This endpoint requires valid authentication token.", "consumes": [ "application/json" ], @@ -2870,10 +2959,10 @@ const docTemplate = `{ "tags": [ "Users" ], - "summary": "Get user profile", + "summary": "Get authenticated user profile", "responses": { "200": { - "description": "OK", + "description": "Profile retrieved successfully with user details", "schema": { "allOf": [ { @@ -2891,19 +2980,19 @@ const docTemplate = `{ } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized - Invalid or expired authentication token", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not Found", + "description": "Not found - User profile not found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2916,7 +3005,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Update current user profile information", + "description": "Update profile information for the currently authenticated user including personal details, contact information, and preferences. Only the authenticated user can update their own profile.", "consumes": [ "application/json" ], @@ -2926,10 +3015,10 @@ const docTemplate = `{ "tags": [ "Users" ], - "summary": "Update user profile", + "summary": "Update authenticated user profile", "parameters": [ { - "description": "Profile update data", + "description": "Profile update data including name, email, phone, and other personal information", "name": "profile", "in": "body", "required": true, @@ -2940,7 +3029,7 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Profile updated successfully with updated user details", "schema": { "allOf": [ { @@ -2958,19 +3047,31 @@ const docTemplate = `{ } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid request format or missing required fields", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized - Invalid or expired authentication token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Email address already exists for another user", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid input data format", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2978,9 +3079,72 @@ const docTemplate = `{ } } }, - "/admin/v1/refresh-token": { + "/admin/v1/profile/change-password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Change user password", + "parameters": [ + { + "description": "Password change data including current password and new password", + "name": "password", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.ChangePasswordForm" + } + } + ], + "responses": { + "200": { + "description": "Password changed successfully - user must re-authenticate", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid request format or password policy violation", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid authentication token or incorrect current password", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid password format or policy requirements not met", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/profile/login": { "post": { - "description": "Refresh access token using refresh token", + "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.", "consumes": [ "application/json" ], @@ -2990,21 +3154,21 @@ const docTemplate = `{ "tags": [ "Authorization" ], - "summary": "Refresh access token", + "summary": "Authenticate user login", "parameters": [ { - "description": "Refresh token", - "name": "refresh", + "description": "User login credentials including username/email and password", + "name": "login", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.RefreshTokenForm" + "$ref": "#/definitions/user.LoginForm" } } ], "responses": { "200": { - "description": "OK", + "description": "Login successful with access and refresh tokens", "schema": { "allOf": [ { @@ -3022,19 +3186,31 @@ const docTemplate = `{ } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid request format or missing fields", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized - Invalid credentials or account locked", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid input data format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "429": { + "description": "Too many requests - Rate limit exceeded", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -3042,9 +3218,14 @@ const docTemplate = `{ } } }, - "/admin/v1/reset-password": { - "post": { - "description": "Send password reset email to user", + "/admin/v1/profile/logout": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "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.", "consumes": [ "application/json" ], @@ -3054,10 +3235,115 @@ const docTemplate = `{ "tags": [ "Authorization" ], - "summary": "Reset password", + "summary": "Logout authenticated user", + "responses": { + "200": { + "description": "Logout successful - all tokens invalidated", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid or expired authentication token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error - Failed to invalidate tokens", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/profile/refresh-token": { + "post": { + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "Refresh access token", "parameters": [ { - "description": "Password reset request", + "description": "Refresh token for generating new access token", + "name": "refresh", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.RefreshTokenForm" + } + } + ], + "responses": { + "200": { + "description": "Token refreshed successfully with new access token", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/user.AuthResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid request format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid or expired refresh token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid token format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/profile/reset-password": { + "post": { + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "Initiate password reset process", + "parameters": [ + { + "description": "Email address for password reset request", "name": "reset", "in": "body", "required": true, @@ -3068,19 +3354,37 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK", + "description": "Password reset email sent successfully", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid request format or email address", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Email address not registered", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid email format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "429": { + "description": "Too many requests - Rate limit for reset emails exceeded", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error - Failed to send email", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -3207,7 +3511,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Create a new user (admin only)", + "description": "Create a new user account with specified role and permissions. This endpoint is restricted to administrators only and allows creation of various user types including admins, managers, and operators with appropriate access levels.", "consumes": [ "application/json" ], @@ -3217,10 +3521,10 @@ const docTemplate = `{ "tags": [ "Users" ], - "summary": "Create user", + "summary": "Create new user account", "parameters": [ { - "description": "User creation data", + "description": "User creation data including username, email, password, role, and profile information", "name": "user", "in": "body", "required": true, @@ -3231,7 +3535,7 @@ const docTemplate = `{ ], "responses": { "201": { - "description": "Created", + "description": "User created successfully with assigned role and permissions", "schema": { "allOf": [ { @@ -3249,25 +3553,37 @@ const docTemplate = `{ } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid input data or missing required fields", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized - Invalid or expired authentication token", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "403": { - "description": "Forbidden", + "description": "Forbidden - Insufficient privileges to create users", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Username or email already exists", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid data format or password policy violation", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -3829,7 +4145,105 @@ const docTemplate = `{ } } }, - "/api/v1/login": { + "/api/v1/": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve current customer profile information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Get customer profile", + "responses": { + "200": { + "description": "Profile retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized - Invalid or missing token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/logout": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Logout customer and invalidate access token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Customer logout", + "responses": { + "200": { + "description": "Logout successful", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid or missing token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/profile/login": { "post": { "description": "Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.", "consumes": [ @@ -3899,105 +4313,7 @@ const docTemplate = `{ } } }, - "/api/v1/logout": { - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Logout customer and invalidate access token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Customers-Authorization" - ], - "summary": "Customer logout", - "responses": { - "200": { - "description": "Logout successful", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized - Invalid or missing token", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/api/v1/profile": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve current customer profile information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Customers-Authorization" - ], - "summary": "Get customer profile", - "responses": { - "200": { - "description": "Profile retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.CustomerResponse" - } - } - } - ] - } - }, - "401": { - "description": "Unauthorized - Invalid or missing token", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not found - Customer not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/api/v1/refresh-token": { + "/api/v1/profile/refresh-token": { "post": { "description": "Refresh access token using a valid refresh token. This allows customers to maintain their session without re-authentication.", "consumes": [ @@ -4066,6 +4382,64 @@ const docTemplate = `{ } } } + }, + "/api/v1/with-companies": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve current customer profile information along with their assigned companies.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Get customer profile with companies", + "responses": { + "200": { + "description": "Profile retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized - Invalid or missing token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } } }, "definitions": { @@ -4144,14 +4518,6 @@ const docTemplate = `{ } } }, - "company.AssignCustomerForm": { - "type": "object", - "properties": { - "customer_id": { - "type": "string" - } - } - }, "company.CPVCodeCount": { "type": "object", "properties": { @@ -4221,9 +4587,6 @@ const docTemplate = `{ "currency": { "type": "string" }, - "customer_id": { - "type": "string" - }, "description": { "type": "string" }, @@ -4254,6 +4617,9 @@ const docTemplate = `{ "name": { "type": "string" }, + "owner_customer_id": { + "type": "string" + }, "phone": { "type": "string" }, @@ -4617,10 +4983,6 @@ const docTemplate = `{ "currency": { "type": "string" }, - "customer_id": { - "description": "Customer assignment (for login credentials)", - "type": "string" - }, "description": { "type": "string" }, @@ -4755,6 +5117,17 @@ const docTemplate = `{ } } }, + "customer.AssignCompaniesForm": { + "type": "object", + "properties": { + "company_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "customer.AuthResponse": { "type": "object", "properties": { @@ -4772,6 +5145,17 @@ const docTemplate = `{ } } }, + "customer.CompanySummary": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "customer.ContactPerson": { "type": "object", "properties": { @@ -4848,8 +5232,12 @@ const docTemplate = `{ "description": "Business information", "type": "string" }, - "company_id": { - "type": "string" + "company_ids": { + "description": "Company assignments", + "type": "array", + "items": { + "type": "string" + } }, "company_name": { "description": "Company customer fields", @@ -4953,11 +5341,12 @@ const docTemplate = `{ "business_type": { "type": "string" }, - "company_id": { - "type": "string" - }, - "company_name": { - "type": "string" + "companies": { + "description": "Company relationships", + "type": "array", + "items": { + "$ref": "#/definitions/customer.CompanySummary" + } }, "compliance_notes": { "type": "string" @@ -5014,6 +5403,7 @@ const docTemplate = `{ "type": "string" }, "registration_number": { + "description": "Company customer fields", "type": "string" }, "status": { @@ -5058,6 +5448,17 @@ const docTemplate = `{ } } }, + "customer.RemoveCompaniesForm": { + "type": "object", + "properties": { + "company_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "customer.SuspendCustomerForm": { "type": "object", "properties": { @@ -5084,8 +5485,12 @@ const docTemplate = `{ "description": "Business information", "type": "string" }, - "company_id": { - "type": "string" + "company_ids": { + "description": "Company assignments", + "type": "array", + "items": { + "type": "string" + } }, "company_name": { "description": "Company customer fields", @@ -5176,14 +5581,30 @@ const docTemplate = `{ "main.HealthResponse": { "type": "object", "properties": { + "service": { + "description": "Service name", + "type": "string", + "example": "tender-management-api" + }, "status": { - "type": "string" + "description": "Current health status", + "type": "string", + "example": "healthy" }, "time": { - "type": "integer" + "description": "Current Unix timestamp", + "type": "integer", + "example": 1699123456 + }, + "uptime": { + "description": "Service uptime duration", + "type": "string", + "example": "24h30m15s" }, "version": { - "type": "string" + "description": "API version", + "type": "string", + "example": "2.0.0" } } }, @@ -5260,10 +5681,14 @@ const docTemplate = `{ "type": "object", "properties": { "new_password": { - "type": "string" + "description": "New password (minimum 8 characters)", + "type": "string", + "example": "NewPass456!" }, "old_password": { - "type": "string" + "description": "Current password for verification", + "type": "string", + "example": "OldPassword123!" } } }, @@ -5271,34 +5696,54 @@ const docTemplate = `{ "type": "object", "properties": { "company_id": { - "type": "string" + "description": "Optional company UUID", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" }, "department": { - "type": "string" + "description": "Optional department name", + "type": "string", + "example": "Information Technology" }, "email": { - "type": "string" + "description": "Valid email address", + "type": "string", + "example": "john.smith@company.com" }, "full_name": { - "type": "string" + "description": "Full name of the user", + "type": "string", + "example": "John Smith" }, "password": { - "type": "string" + "description": "Password (minimum 8 characters)", + "type": "string", + "example": "SecurePass123!" }, "phone": { - "type": "string" + "description": "Optional phone number", + "type": "string", + "example": "+1234567890" }, "position": { - "type": "string" + "description": "Optional job position", + "type": "string", + "example": "Senior Developer" }, "profile_image": { - "type": "string" + "description": "Optional profile image URL", + "type": "string", + "example": "https://example.com/avatar.jpg" }, "role": { - "type": "string" + "description": "User role (admin, manager, operator, viewer)", + "type": "string", + "example": "manager" }, "username": { - "type": "string" + "description": "Unique username (alphanumeric only)", + "type": "string", + "example": "johnsmith" } } }, @@ -5306,10 +5751,14 @@ const docTemplate = `{ "type": "object", "properties": { "password": { - "type": "string" + "description": "User password", + "type": "string", + "example": "SecurePass123!" }, "username": { - "type": "string" + "description": "Username or email address", + "type": "string", + "example": "johnsmith" } } }, @@ -5317,7 +5766,9 @@ const docTemplate = `{ "type": "object", "properties": { "refresh_token": { - "type": "string" + "description": "Valid refresh token", + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } }, @@ -5325,7 +5776,9 @@ const docTemplate = `{ "type": "object", "properties": { "email": { - "type": "string" + "description": "Email address for password reset", + "type": "string", + "example": "john.smith@company.com" } } }, @@ -5333,34 +5786,54 @@ const docTemplate = `{ "type": "object", "properties": { "company_id": { - "type": "string" + "description": "Updated company ID", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" }, "department": { - "type": "string" + "description": "Updated department", + "type": "string", + "example": "Product Management" }, "email": { - "type": "string" + "description": "Updated email address", + "type": "string", + "example": "john.smith@newcompany.com" }, "full_name": { - "type": "string" + "description": "Updated full name", + "type": "string", + "example": "John Smith" }, "phone": { - "type": "string" + "description": "Updated phone number", + "type": "string", + "example": "+1234567890" }, "position": { - "type": "string" + "description": "Updated position", + "type": "string", + "example": "Tech Lead" }, "profile_image": { - "type": "string" + "description": "Updated profile image URL", + "type": "string", + "example": "https://example.com/new-avatar.jpg" }, "role": { - "type": "string" + "description": "Updated user role", + "type": "string", + "example": "admin" }, "status": { - "type": "string" + "description": "Updated account status", + "type": "string", + "example": "active" }, "username": { - "type": "string" + "description": "Updated username", + "type": "string", + "example": "johnsmith" } } }, @@ -5462,7 +5935,7 @@ const docTemplate = `{ }, "securityDefinitions": { "BearerAuth": { - "description": "Type \"Bearer\" followed by a space and JWT token.", + "description": "Type \"Bearer\" followed by a space and JWT token. Example: \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\"", "type": "apiKey", "name": "Authorization", "in": "header" @@ -5470,40 +5943,44 @@ const docTemplate = `{ }, "tags": [ { - "description": "User management operations including authentication, profile management, and admin operations", - "name": "Users" + "description": "System health check operations for monitoring application status and dependencies", + "name": "Health" }, { - "description": "Authentication operations including login, logout, and token management", + "description": "User authentication and authorization operations including login, logout, token refresh, and password management for web panel administration", "name": "Authorization" }, { - "description": "Customer management operations including authentication, profile management, and admin operations", + "description": "Administrative user management operations including CRUD operations, role management, status updates, and profile management for web panel administrators", + "name": "Users" + }, + { + "description": "Administrative customer management operations for web panel including CRUD operations, company assignment, verification, status management, and comprehensive filtering with pagination", "name": "Customers-Admin" }, { - "description": "Customer authentication operations including login, logout, and token management", + "description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access", "name": "Customers-Authorization" }, { - "description": "Company management operations for web panel including CRUD, customer assignment, and tag management", + "description": "Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting", "name": "Companies-Admin" }, { - "description": "Health check operations", - "name": "Health" + "description": "Company-related operations for mobile application including company lookup, search, and basic information retrieval", + "name": "Companies-Mobile" } ] }` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "1.0.0", + Version: "2.0.0", Host: "localhost:8081", - BasePath: "", + BasePath: "/", Schemes: []string{}, Title: "Tender Management API", - Description: "This is the API documentation for the Tender Management System.", + Description: "This is a comprehensive API for the Tender Management System built with Clean Architecture principles and Domain-Driven Design (DDD) patterns. The API provides endpoints for user management, customer management, company management, and authentication for both web panel administration and mobile application access.", InfoInstanceName: "swagger", SwaggerTemplate: docTemplate, LeftDelim: "{{", diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index b48a5c0..b63cb31 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -1,79 +1,23 @@ { "swagger": "2.0", "info": { - "description": "This is the API documentation for the Tender Management System.", + "description": "This is a comprehensive API for the Tender Management System built with Clean Architecture principles and Domain-Driven Design (DDD) patterns. The API provides endpoints for user management, customer management, company management, and authentication for both web panel administration and mobile application access.", "title": "Tender Management API", - "termsOfService": "http://swagger.io/terms/", + "termsOfService": "https://tender-management.com/terms", "contact": { - "name": "API Support", - "url": "http://www.swagger.io/support", - "email": "support@swagger.io" + "name": "Tender Management API Support", + "url": "https://tender-management.com/support", + "email": "api-support@tender-management.com" }, "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" }, - "version": "1.0.0" + "version": "2.0.0" }, "host": "localhost:8081", + "basePath": "/", "paths": { - "/admin/v1/change-password": { - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Change current user password", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Users" - ], - "summary": "Change password", - "parameters": [ - { - "description": "Password change data", - "name": "password", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.ChangePasswordForm" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/companies": { "get": { "security": [ @@ -1091,82 +1035,6 @@ } } }, - "/admin/v1/companies/{id}/assign-customer": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Assign a customer to a company for login credentials", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Companies-Admin" - ], - "summary": "Assign customer to company", - "parameters": [ - { - "type": "string", - "description": "Company ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Customer assignment information", - "name": "assignment", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/company.AssignCustomerForm" - } - } - ], - "responses": { - "200": { - "description": "Customer assigned successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request - Invalid input data", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not found - Company not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "409": { - "description": "Conflict - Customer already assigned", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "422": { - "description": "Validation error - Invalid request data", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/companies/{id}/status": { "patch": { "security": [ @@ -1517,61 +1385,6 @@ } } }, - "/admin/v1/companies/{id}/unassign-customer": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Remove customer assignment from a company", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Companies-Admin" - ], - "summary": "Unassign customer from company", - "parameters": [ - { - "type": "string", - "description": "Company ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Customer unassigned successfully", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "400": { - "description": "Bad request - Invalid company ID", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not found - Company not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/companies/{id}/verification": { "patch": { "security": [ @@ -2193,6 +2006,174 @@ } } }, + "/admin/v1/customers/with-companies": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "List customers with companies and filters", + "parameters": [ + { + "type": "string", + "description": "Search term to filter customers by name, email, or company name", + "name": "search", + "in": "query" + }, + { + "enum": [ + "individual", + "company", + "government" + ], + "type": "string", + "description": "Filter by customer type", + "name": "type", + "in": "query" + }, + { + "enum": [ + "active", + "inactive", + "suspended", + "pending" + ], + "type": "string", + "description": "Filter by customer status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "format": "uuid", + "description": "Filter by company ID", + "name": "company_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by industry", + "name": "industry", + "in": "query" + }, + { + "type": "boolean", + "description": "Filter by verification status", + "name": "is_verified", + "in": "query" + }, + { + "type": "boolean", + "description": "Filter by compliance status", + "name": "is_compliant", + "in": "query" + }, + { + "type": "string", + "description": "Filter by preferred language", + "name": "language", + "in": "query" + }, + { + "type": "string", + "description": "Filter by preferred currency", + "name": "currency", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of customers per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of customers to skip for pagination", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "email", + "company_name", + "created_at", + "updated_at", + "status" + ], + "type": "string", + "default": "created_at", + "description": "Field to sort by", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "default": "desc", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Customers with companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerListResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers/{id}": { "get": { "security": [ @@ -2454,6 +2435,146 @@ } } }, + "/admin/v1/customers/{id}/companies/assign": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Assign one or more companies to a specific customer.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Assign companies to a customer", + "parameters": [ + { + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "List of company IDs to assign", + "name": "companies", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.AssignCompaniesForm" + } + } + ], + "responses": { + "200": { + "description": "Companies assigned successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/companies/remove": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove one or more companies from a specific customer.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Remove companies from a customer", + "parameters": [ + { + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "List of company IDs to remove", + "name": "companies", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.RemoveCompaniesForm" + } + } + ], + "responses": { + "200": { + "description": "Companies removed successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers/{id}/status": { "patch": { "security": [ @@ -2719,32 +2840,14 @@ } } }, - "/admin/v1/health": { + "/admin/v1/customers/{id}/with-companies": { "get": { - "description": "Get server health status", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Health" - ], - "summary": "Health check", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/main.HealthResponse" - } + "security": [ + { + "BearerAuth": [] } - } - } - }, - "/admin/v1/login": { - "post": { - "description": "Authenticate user with username and password", + ], + "description": "Retrieve detailed customer information by customer ID including assigned companies", "consumes": [ "application/json" ], @@ -2752,23 +2855,21 @@ "application/json" ], "tags": [ - "Authorization" + "Customers-Admin" ], - "summary": "Login user", + "summary": "Get customer by ID with companies", "parameters": [ { - "description": "Login credentials", - "name": "login", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/user.LoginForm" - } + "type": "string", + "description": "Customer ID", + "name": "id", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "OK", + "description": "Customer retrieved successfully", "schema": { "allOf": [ { @@ -2778,7 +2879,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/user.AuthResponse" + "$ref": "#/definitions/customer.CustomerResponse" } } } @@ -2786,19 +2887,19 @@ } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid customer ID", "schema": { "$ref": "#/definitions/response.APIResponse" } }, - "401": { - "description": "Unauthorized", + "404": { + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2806,14 +2907,9 @@ } } }, - "/admin/v1/logout": { - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Logout current user and invalidate tokens", + "/admin/v1/health": { + "get": { + "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.", "consumes": [ "application/json" ], @@ -2821,26 +2917,20 @@ "application/json" ], "tags": [ - "Users" + "Health" ], - "summary": "Logout user", + "summary": "Health check endpoint", "responses": { "200": { - "description": "OK", + "description": "System is healthy and operational", "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/main.HealthResponse" } }, - "401": { - "description": "Unauthorized", + "503": { + "description": "System is unhealthy or experiencing issues", "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" + "$ref": "#/definitions/main.HealthResponse" } } } @@ -2853,7 +2943,7 @@ "BearerAuth": [] } ], - "description": "Get current user profile information", + "description": "Retrieve complete profile information for the currently authenticated user including personal details, role information, permissions, and account status. This endpoint requires valid authentication token.", "consumes": [ "application/json" ], @@ -2863,10 +2953,10 @@ "tags": [ "Users" ], - "summary": "Get user profile", + "summary": "Get authenticated user profile", "responses": { "200": { - "description": "OK", + "description": "Profile retrieved successfully with user details", "schema": { "allOf": [ { @@ -2884,19 +2974,19 @@ } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized - Invalid or expired authentication token", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not Found", + "description": "Not found - User profile not found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2909,7 +2999,7 @@ "BearerAuth": [] } ], - "description": "Update current user profile information", + "description": "Update profile information for the currently authenticated user including personal details, contact information, and preferences. Only the authenticated user can update their own profile.", "consumes": [ "application/json" ], @@ -2919,10 +3009,10 @@ "tags": [ "Users" ], - "summary": "Update user profile", + "summary": "Update authenticated user profile", "parameters": [ { - "description": "Profile update data", + "description": "Profile update data including name, email, phone, and other personal information", "name": "profile", "in": "body", "required": true, @@ -2933,7 +3023,7 @@ ], "responses": { "200": { - "description": "OK", + "description": "Profile updated successfully with updated user details", "schema": { "allOf": [ { @@ -2951,19 +3041,31 @@ } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid request format or missing required fields", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized - Invalid or expired authentication token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Email address already exists for another user", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid input data format", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2971,9 +3073,72 @@ } } }, - "/admin/v1/refresh-token": { + "/admin/v1/profile/change-password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Change user password", + "parameters": [ + { + "description": "Password change data including current password and new password", + "name": "password", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.ChangePasswordForm" + } + } + ], + "responses": { + "200": { + "description": "Password changed successfully - user must re-authenticate", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid request format or password policy violation", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid authentication token or incorrect current password", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid password format or policy requirements not met", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/profile/login": { "post": { - "description": "Refresh access token using refresh token", + "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.", "consumes": [ "application/json" ], @@ -2983,21 +3148,21 @@ "tags": [ "Authorization" ], - "summary": "Refresh access token", + "summary": "Authenticate user login", "parameters": [ { - "description": "Refresh token", - "name": "refresh", + "description": "User login credentials including username/email and password", + "name": "login", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/user.RefreshTokenForm" + "$ref": "#/definitions/user.LoginForm" } } ], "responses": { "200": { - "description": "OK", + "description": "Login successful with access and refresh tokens", "schema": { "allOf": [ { @@ -3015,19 +3180,31 @@ } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid request format or missing fields", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized - Invalid credentials or account locked", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid input data format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "429": { + "description": "Too many requests - Rate limit exceeded", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -3035,9 +3212,14 @@ } } }, - "/admin/v1/reset-password": { - "post": { - "description": "Send password reset email to user", + "/admin/v1/profile/logout": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "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.", "consumes": [ "application/json" ], @@ -3047,10 +3229,115 @@ "tags": [ "Authorization" ], - "summary": "Reset password", + "summary": "Logout authenticated user", + "responses": { + "200": { + "description": "Logout successful - all tokens invalidated", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid or expired authentication token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error - Failed to invalidate tokens", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/profile/refresh-token": { + "post": { + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "Refresh access token", "parameters": [ { - "description": "Password reset request", + "description": "Refresh token for generating new access token", + "name": "refresh", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.RefreshTokenForm" + } + } + ], + "responses": { + "200": { + "description": "Token refreshed successfully with new access token", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/user.AuthResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid request format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid or expired refresh token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid token format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/profile/reset-password": { + "post": { + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "Initiate password reset process", + "parameters": [ + { + "description": "Email address for password reset request", "name": "reset", "in": "body", "required": true, @@ -3061,19 +3348,37 @@ ], "responses": { "200": { - "description": "OK", + "description": "Password reset email sent successfully", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid request format or email address", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Email address not registered", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid email format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "429": { + "description": "Too many requests - Rate limit for reset emails exceeded", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error - Failed to send email", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -3200,7 +3505,7 @@ "BearerAuth": [] } ], - "description": "Create a new user (admin only)", + "description": "Create a new user account with specified role and permissions. This endpoint is restricted to administrators only and allows creation of various user types including admins, managers, and operators with appropriate access levels.", "consumes": [ "application/json" ], @@ -3210,10 +3515,10 @@ "tags": [ "Users" ], - "summary": "Create user", + "summary": "Create new user account", "parameters": [ { - "description": "User creation data", + "description": "User creation data including username, email, password, role, and profile information", "name": "user", "in": "body", "required": true, @@ -3224,7 +3529,7 @@ ], "responses": { "201": { - "description": "Created", + "description": "User created successfully with assigned role and permissions", "schema": { "allOf": [ { @@ -3242,25 +3547,37 @@ } }, "400": { - "description": "Bad Request", + "description": "Bad request - Invalid input data or missing required fields", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "401": { - "description": "Unauthorized", + "description": "Unauthorized - Invalid or expired authentication token", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "403": { - "description": "Forbidden", + "description": "Forbidden - Insufficient privileges to create users", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Username or email already exists", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid data format or password policy violation", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "500": { - "description": "Internal Server Error", + "description": "Internal server error", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -3822,7 +4139,105 @@ } } }, - "/api/v1/login": { + "/api/v1/": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve current customer profile information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Get customer profile", + "responses": { + "200": { + "description": "Profile retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized - Invalid or missing token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/logout": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Logout customer and invalidate access token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Customer logout", + "responses": { + "200": { + "description": "Logout successful", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid or missing token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/profile/login": { "post": { "description": "Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.", "consumes": [ @@ -3892,105 +4307,7 @@ } } }, - "/api/v1/logout": { - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Logout customer and invalidate access token", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Customers-Authorization" - ], - "summary": "Customer logout", - "responses": { - "200": { - "description": "Logout successful", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "401": { - "description": "Unauthorized - Invalid or missing token", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/api/v1/profile": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve current customer profile information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Customers-Authorization" - ], - "summary": "Get customer profile", - "responses": { - "200": { - "description": "Profile retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.CustomerResponse" - } - } - } - ] - } - }, - "401": { - "description": "Unauthorized - Invalid or missing token", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not found - Customer not found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/api/v1/refresh-token": { + "/api/v1/profile/refresh-token": { "post": { "description": "Refresh access token using a valid refresh token. This allows customers to maintain their session without re-authentication.", "consumes": [ @@ -4059,6 +4376,64 @@ } } } + }, + "/api/v1/with-companies": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve current customer profile information along with their assigned companies.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Get customer profile with companies", + "responses": { + "200": { + "description": "Profile retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized - Invalid or missing token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } } }, "definitions": { @@ -4137,14 +4512,6 @@ } } }, - "company.AssignCustomerForm": { - "type": "object", - "properties": { - "customer_id": { - "type": "string" - } - } - }, "company.CPVCodeCount": { "type": "object", "properties": { @@ -4214,9 +4581,6 @@ "currency": { "type": "string" }, - "customer_id": { - "type": "string" - }, "description": { "type": "string" }, @@ -4247,6 +4611,9 @@ "name": { "type": "string" }, + "owner_customer_id": { + "type": "string" + }, "phone": { "type": "string" }, @@ -4610,10 +4977,6 @@ "currency": { "type": "string" }, - "customer_id": { - "description": "Customer assignment (for login credentials)", - "type": "string" - }, "description": { "type": "string" }, @@ -4748,6 +5111,17 @@ } } }, + "customer.AssignCompaniesForm": { + "type": "object", + "properties": { + "company_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "customer.AuthResponse": { "type": "object", "properties": { @@ -4765,6 +5139,17 @@ } } }, + "customer.CompanySummary": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "customer.ContactPerson": { "type": "object", "properties": { @@ -4841,8 +5226,12 @@ "description": "Business information", "type": "string" }, - "company_id": { - "type": "string" + "company_ids": { + "description": "Company assignments", + "type": "array", + "items": { + "type": "string" + } }, "company_name": { "description": "Company customer fields", @@ -4946,11 +5335,12 @@ "business_type": { "type": "string" }, - "company_id": { - "type": "string" - }, - "company_name": { - "type": "string" + "companies": { + "description": "Company relationships", + "type": "array", + "items": { + "$ref": "#/definitions/customer.CompanySummary" + } }, "compliance_notes": { "type": "string" @@ -5007,6 +5397,7 @@ "type": "string" }, "registration_number": { + "description": "Company customer fields", "type": "string" }, "status": { @@ -5051,6 +5442,17 @@ } } }, + "customer.RemoveCompaniesForm": { + "type": "object", + "properties": { + "company_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "customer.SuspendCustomerForm": { "type": "object", "properties": { @@ -5077,8 +5479,12 @@ "description": "Business information", "type": "string" }, - "company_id": { - "type": "string" + "company_ids": { + "description": "Company assignments", + "type": "array", + "items": { + "type": "string" + } }, "company_name": { "description": "Company customer fields", @@ -5169,14 +5575,30 @@ "main.HealthResponse": { "type": "object", "properties": { + "service": { + "description": "Service name", + "type": "string", + "example": "tender-management-api" + }, "status": { - "type": "string" + "description": "Current health status", + "type": "string", + "example": "healthy" }, "time": { - "type": "integer" + "description": "Current Unix timestamp", + "type": "integer", + "example": 1699123456 + }, + "uptime": { + "description": "Service uptime duration", + "type": "string", + "example": "24h30m15s" }, "version": { - "type": "string" + "description": "API version", + "type": "string", + "example": "2.0.0" } } }, @@ -5253,10 +5675,14 @@ "type": "object", "properties": { "new_password": { - "type": "string" + "description": "New password (minimum 8 characters)", + "type": "string", + "example": "NewPass456!" }, "old_password": { - "type": "string" + "description": "Current password for verification", + "type": "string", + "example": "OldPassword123!" } } }, @@ -5264,34 +5690,54 @@ "type": "object", "properties": { "company_id": { - "type": "string" + "description": "Optional company UUID", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" }, "department": { - "type": "string" + "description": "Optional department name", + "type": "string", + "example": "Information Technology" }, "email": { - "type": "string" + "description": "Valid email address", + "type": "string", + "example": "john.smith@company.com" }, "full_name": { - "type": "string" + "description": "Full name of the user", + "type": "string", + "example": "John Smith" }, "password": { - "type": "string" + "description": "Password (minimum 8 characters)", + "type": "string", + "example": "SecurePass123!" }, "phone": { - "type": "string" + "description": "Optional phone number", + "type": "string", + "example": "+1234567890" }, "position": { - "type": "string" + "description": "Optional job position", + "type": "string", + "example": "Senior Developer" }, "profile_image": { - "type": "string" + "description": "Optional profile image URL", + "type": "string", + "example": "https://example.com/avatar.jpg" }, "role": { - "type": "string" + "description": "User role (admin, manager, operator, viewer)", + "type": "string", + "example": "manager" }, "username": { - "type": "string" + "description": "Unique username (alphanumeric only)", + "type": "string", + "example": "johnsmith" } } }, @@ -5299,10 +5745,14 @@ "type": "object", "properties": { "password": { - "type": "string" + "description": "User password", + "type": "string", + "example": "SecurePass123!" }, "username": { - "type": "string" + "description": "Username or email address", + "type": "string", + "example": "johnsmith" } } }, @@ -5310,7 +5760,9 @@ "type": "object", "properties": { "refresh_token": { - "type": "string" + "description": "Valid refresh token", + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } }, @@ -5318,7 +5770,9 @@ "type": "object", "properties": { "email": { - "type": "string" + "description": "Email address for password reset", + "type": "string", + "example": "john.smith@company.com" } } }, @@ -5326,34 +5780,54 @@ "type": "object", "properties": { "company_id": { - "type": "string" + "description": "Updated company ID", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" }, "department": { - "type": "string" + "description": "Updated department", + "type": "string", + "example": "Product Management" }, "email": { - "type": "string" + "description": "Updated email address", + "type": "string", + "example": "john.smith@newcompany.com" }, "full_name": { - "type": "string" + "description": "Updated full name", + "type": "string", + "example": "John Smith" }, "phone": { - "type": "string" + "description": "Updated phone number", + "type": "string", + "example": "+1234567890" }, "position": { - "type": "string" + "description": "Updated position", + "type": "string", + "example": "Tech Lead" }, "profile_image": { - "type": "string" + "description": "Updated profile image URL", + "type": "string", + "example": "https://example.com/new-avatar.jpg" }, "role": { - "type": "string" + "description": "Updated user role", + "type": "string", + "example": "admin" }, "status": { - "type": "string" + "description": "Updated account status", + "type": "string", + "example": "active" }, "username": { - "type": "string" + "description": "Updated username", + "type": "string", + "example": "johnsmith" } } }, @@ -5455,7 +5929,7 @@ }, "securityDefinitions": { "BearerAuth": { - "description": "Type \"Bearer\" followed by a space and JWT token.", + "description": "Type \"Bearer\" followed by a space and JWT token. Example: \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\"", "type": "apiKey", "name": "Authorization", "in": "header" @@ -5463,28 +5937,32 @@ }, "tags": [ { - "description": "User management operations including authentication, profile management, and admin operations", - "name": "Users" + "description": "System health check operations for monitoring application status and dependencies", + "name": "Health" }, { - "description": "Authentication operations including login, logout, and token management", + "description": "User authentication and authorization operations including login, logout, token refresh, and password management for web panel administration", "name": "Authorization" }, { - "description": "Customer management operations including authentication, profile management, and admin operations", + "description": "Administrative user management operations including CRUD operations, role management, status updates, and profile management for web panel administrators", + "name": "Users" + }, + { + "description": "Administrative customer management operations for web panel including CRUD operations, company assignment, verification, status management, and comprehensive filtering with pagination", "name": "Customers-Admin" }, { - "description": "Customer authentication operations including login, logout, and token management", + "description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access", "name": "Customers-Authorization" }, { - "description": "Company management operations for web panel including CRUD, customer assignment, and tag management", + "description": "Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting", "name": "Companies-Admin" }, { - "description": "Health check operations", - "name": "Health" + "description": "Company-related operations for mobile application including company lookup, search, and basic information retrieval", + "name": "Companies-Mobile" } ] } \ No newline at end of file diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index dd3aaa9..cac646f 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -1,3 +1,4 @@ +basePath: / definitions: company.AddTagsForm: properties: @@ -48,11 +49,6 @@ definitions: street: type: string type: object - company.AssignCustomerForm: - properties: - customer_id: - type: string - type: object company.CPVCodeCount: properties: count: @@ -98,8 +94,6 @@ definitions: type: string currency: type: string - customer_id: - type: string description: type: string email: @@ -120,6 +114,8 @@ definitions: type: string name: type: string + owner_customer_id: + type: string phone: type: string registration_number: @@ -356,9 +352,6 @@ definitions: description: Contact information currency: type: string - customer_id: - description: Customer assignment (for login credentials) - type: string description: type: string email: @@ -446,6 +439,13 @@ definitions: street: type: string type: object + customer.AssignCompaniesForm: + properties: + company_ids: + items: + type: string + type: array + type: object customer.AuthResponse: properties: access_token: @@ -457,6 +457,13 @@ definitions: refresh_token: type: string type: object + customer.CompanySummary: + properties: + id: + type: string + name: + type: string + type: object customer.ContactPerson: properties: email: @@ -506,8 +513,11 @@ definitions: business_type: description: Business information type: string - company_id: - type: string + company_ids: + description: Company assignments + items: + type: string + type: array company_name: description: Company customer fields type: string @@ -575,10 +585,11 @@ definitions: type: number business_type: type: string - company_id: - type: string - company_name: - type: string + companies: + description: Company relationships + items: + $ref: '#/definitions/customer.CompanySummary' + type: array compliance_notes: type: string contact_person: @@ -616,6 +627,7 @@ definitions: phone: type: string registration_number: + description: Company customer fields type: string status: type: string @@ -644,6 +656,13 @@ definitions: refresh_token: type: string type: object + customer.RemoveCompaniesForm: + properties: + company_ids: + items: + type: string + type: array + type: object customer.SuspendCustomerForm: properties: reason: @@ -660,8 +679,11 @@ definitions: business_type: description: Business information type: string - company_id: - type: string + company_ids: + description: Company assignments + items: + type: string + type: array company_name: description: Company customer fields type: string @@ -720,11 +742,25 @@ definitions: type: object main.HealthResponse: properties: + service: + description: Service name + example: tender-management-api + type: string status: + description: Current health status + example: healthy type: string time: + description: Current Unix timestamp + example: 1699123456 type: integer + uptime: + description: Service uptime duration + example: 24h30m15s + type: string version: + description: API version + example: 2.0.0 type: string type: object response.APIError: @@ -775,71 +811,123 @@ definitions: user.ChangePasswordForm: properties: new_password: + description: New password (minimum 8 characters) + example: NewPass456! type: string old_password: + description: Current password for verification + example: OldPassword123! type: string type: object user.CreateUserForm: properties: company_id: + description: Optional company UUID + example: 123e4567-e89b-12d3-a456-426614174000 type: string department: + description: Optional department name + example: Information Technology type: string email: + description: Valid email address + example: john.smith@company.com type: string full_name: + description: Full name of the user + example: John Smith type: string password: + description: Password (minimum 8 characters) + example: SecurePass123! type: string phone: + description: Optional phone number + example: "+1234567890" type: string position: + description: Optional job position + example: Senior Developer type: string profile_image: + description: Optional profile image URL + example: https://example.com/avatar.jpg type: string role: + description: User role (admin, manager, operator, viewer) + example: manager type: string username: + description: Unique username (alphanumeric only) + example: johnsmith type: string type: object user.LoginForm: properties: password: + description: User password + example: SecurePass123! type: string username: + description: Username or email address + example: johnsmith type: string type: object user.RefreshTokenForm: properties: refresh_token: + description: Valid refresh token + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... type: string type: object user.ResetPasswordForm: properties: email: + description: Email address for password reset + example: john.smith@company.com type: string type: object user.UpdateUserForm: properties: company_id: + description: Updated company ID + example: 123e4567-e89b-12d3-a456-426614174000 type: string department: + description: Updated department + example: Product Management type: string email: + description: Updated email address + example: john.smith@newcompany.com type: string full_name: + description: Updated full name + example: John Smith type: string phone: + description: Updated phone number + example: "+1234567890" type: string position: + description: Updated position + example: Tech Lead type: string profile_image: + description: Updated profile image URL + example: https://example.com/new-avatar.jpg type: string role: + description: Updated user role + example: admin type: string status: + description: Updated account status + example: active type: string username: + description: Updated username + example: johnsmith type: string type: object user.UpdateUserRoleForm: @@ -907,53 +995,20 @@ definitions: host: localhost:8081 info: contact: - email: support@swagger.io - name: API Support - url: http://www.swagger.io/support - description: This is the API documentation for the Tender Management System. + email: api-support@tender-management.com + name: Tender Management API Support + url: https://tender-management.com/support + 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. license: - name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html - termsOfService: http://swagger.io/terms/ + name: MIT + url: https://opensource.org/licenses/MIT + termsOfService: https://tender-management.com/terms title: Tender Management API - version: 1.0.0 + version: 2.0.0 paths: - /admin/v1/change-password: - put: - consumes: - - application/json - description: Change current user password - parameters: - - description: Password change data - in: body - name: password - required: true - schema: - $ref: '#/definitions/user.ChangePasswordForm' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Change password - tags: - - Users /admin/v1/companies: get: consumes: @@ -1317,55 +1372,6 @@ paths: summary: Activate company tags: - Companies-Admin - /admin/v1/companies/{id}/assign-customer: - post: - consumes: - - application/json - description: Assign a customer to a company for login credentials - parameters: - - description: Company ID - in: path - name: id - required: true - type: string - - description: Customer assignment information - in: body - name: assignment - required: true - schema: - $ref: '#/definitions/company.AssignCustomerForm' - produces: - - application/json - responses: - "200": - description: Customer assigned successfully - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad request - Invalid input data - schema: - $ref: '#/definitions/response.APIResponse' - "404": - description: Not found - Company not found - schema: - $ref: '#/definitions/response.APIResponse' - "409": - description: Conflict - Customer already assigned - schema: - $ref: '#/definitions/response.APIResponse' - "422": - description: Validation error - Invalid request data - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Assign customer to company - tags: - - Companies-Admin /admin/v1/companies/{id}/status: patch: consumes: @@ -1592,41 +1598,6 @@ paths: summary: Remove tags from company tags: - Companies-Admin - /admin/v1/companies/{id}/unassign-customer: - post: - consumes: - - application/json - description: Remove customer assignment from a company - parameters: - - description: Company ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: Customer unassigned successfully - schema: - $ref: '#/definitions/response.APIResponse' - "400": - description: Bad request - Invalid company ID - schema: - $ref: '#/definitions/response.APIResponse' - "404": - description: Not found - Company not found - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Unassign customer from company - tags: - - Companies-Admin /admin/v1/companies/{id}/verification: patch: consumes: @@ -2328,6 +2299,96 @@ paths: summary: Activate customer tags: - Customers-Admin + /admin/v1/customers/{id}/companies/assign: + post: + consumes: + - application/json + description: Assign one or more companies to a specific customer. + parameters: + - description: Customer ID + in: path + name: id + required: true + type: string + - description: List of company IDs to assign + in: body + name: companies + required: true + schema: + $ref: '#/definitions/customer.AssignCompaniesForm' + produces: + - application/json + responses: + "200": + description: Companies assigned successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer not found + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid request data + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Assign companies to a customer + tags: + - Customers-Admin + /admin/v1/customers/{id}/companies/remove: + post: + consumes: + - application/json + description: Remove one or more companies from a specific customer. + parameters: + - description: Customer ID + in: path + name: id + required: true + type: string + - description: List of company IDs to remove + in: body + name: companies + required: true + schema: + $ref: '#/definitions/customer.RemoveCompaniesForm' + produces: + - application/json + responses: + "200": + description: Companies removed successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer not found + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid request data + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Remove companies from a customer + tags: + - Customers-Admin /admin/v1/customers/{id}/status: patch: consumes: @@ -2498,6 +2559,47 @@ paths: summary: Verify customer tags: - Customers-Admin + /admin/v1/customers/{id}/with-companies: + get: + consumes: + - application/json + description: Retrieve detailed customer information by customer ID including + assigned companies + parameters: + - description: Customer ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Customer retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/customer.CustomerResponse' + type: object + "400": + description: Bad request - Invalid customer ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get customer by ID with companies + tags: + - Customers-Admin /admin/v1/customers/company/{companyId}: get: consumes: @@ -2661,95 +2763,154 @@ paths: summary: Get customers by type tags: - Customers-Admin - /admin/v1/health: + /admin/v1/customers/with-companies: get: consumes: - application/json - description: Get server health status - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/main.HealthResponse' - summary: Health check - tags: - - Health - /admin/v1/login: - post: - consumes: - - application/json - description: Authenticate user with username and password + 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. parameters: - - description: Login credentials - in: body - name: login - required: true - schema: - $ref: '#/definitions/user.LoginForm' + - description: Search term to filter customers by name, email, or company name + in: query + name: search + type: string + - description: Filter by customer type + enum: + - individual + - company + - government + in: query + name: type + type: string + - description: Filter by customer status + enum: + - active + - inactive + - suspended + - pending + in: query + name: status + type: string + - description: Filter by company ID + format: uuid + in: query + name: company_id + type: string + - description: Filter by industry + in: query + name: industry + type: string + - description: Filter by verification status + in: query + name: is_verified + type: boolean + - description: Filter by compliance status + in: query + name: is_compliant + type: boolean + - description: Filter by preferred language + in: query + name: language + type: string + - description: Filter by preferred currency + in: query + name: currency + type: string + - default: 20 + description: Number of customers per page (1-100) + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + - default: 0 + description: Number of customers to skip for pagination + in: query + minimum: 0 + name: offset + type: integer + - default: created_at + description: Field to sort by + enum: + - email + - company_name + - created_at + - updated_at + - status + in: query + name: sort_by + type: string + - default: desc + description: Sort order + enum: + - asc + - desc + in: query + name: sort_order + type: string produces: - application/json responses: "200": - description: OK + description: Customers with companies retrieved successfully schema: allOf: - $ref: '#/definitions/response.APIResponse' - properties: data: - $ref: '#/definitions/user.AuthResponse' + $ref: '#/definitions/customer.CustomerListResponse' type: object "400": - description: Bad Request + description: Bad request - Invalid query parameters schema: $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized + "422": + description: Validation error - Invalid query parameters schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - summary: Login user - tags: - - Authorization - /admin/v1/logout: - delete: - consumes: - - application/json - description: Logout current user and invalidate tokens - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error + description: Internal server error schema: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Logout user + summary: List customers with companies and filters tags: - - Users - /admin/v1/profile: + - Customers-Admin + /admin/v1/health: get: consumes: - application/json - description: Get current user profile information + 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. produces: - application/json responses: "200": - description: OK + description: System is healthy and operational + schema: + $ref: '#/definitions/main.HealthResponse' + "503": + description: System is unhealthy or experiencing issues + schema: + $ref: '#/definitions/main.HealthResponse' + summary: Health check endpoint + tags: + - Health + /admin/v1/profile: + get: + consumes: + - application/json + 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. + produces: + - application/json + responses: + "200": + description: Profile retrieved successfully with user details schema: allOf: - $ref: '#/definitions/response.APIResponse' @@ -2758,28 +2919,31 @@ paths: $ref: '#/definitions/user.UserResponse' type: object "401": - description: Unauthorized + description: Unauthorized - Invalid or expired authentication token schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not Found + description: Not found - User profile not found schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal Server Error + description: Internal server error schema: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Get user profile + summary: Get authenticated user profile tags: - Users put: consumes: - application/json - description: Update current user profile information + description: Update profile information for the currently authenticated user + including personal details, contact information, and preferences. Only the + authenticated user can update their own profile. parameters: - - description: Profile update data + - description: Profile update data including name, email, phone, and other personal + information in: body name: profile required: true @@ -2789,7 +2953,7 @@ paths: - application/json responses: "200": - description: OK + description: Profile updated successfully with updated user details schema: allOf: - $ref: '#/definitions/response.APIResponse' @@ -2798,29 +2962,159 @@ paths: $ref: '#/definitions/user.UserResponse' type: object "400": - description: Bad Request + description: Bad request - Invalid request format or missing required fields schema: $ref: '#/definitions/response.APIResponse' "401": - description: Unauthorized + description: Unauthorized - Invalid or expired authentication token + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict - Email address already exists for another user + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid input data format schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal Server Error + description: Internal server error schema: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Update user profile + summary: Update authenticated user profile tags: - Users - /admin/v1/refresh-token: + /admin/v1/profile/change-password: + put: + consumes: + - application/json + 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. + parameters: + - description: Password change data including current password and new password + in: body + name: password + required: true + schema: + $ref: '#/definitions/user.ChangePasswordForm' + produces: + - application/json + responses: + "200": + description: Password changed successfully - user must re-authenticate + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid request format or password policy violation + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized - Invalid authentication token or incorrect current + password + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid password format or policy requirements + not met + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Change user password + tags: + - Users + /admin/v1/profile/login: post: consumes: - application/json - description: Refresh access token using refresh token + 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. parameters: - - description: Refresh token + - description: User login credentials including username/email and password + in: body + name: login + required: true + schema: + $ref: '#/definitions/user.LoginForm' + produces: + - application/json + responses: + "200": + description: Login successful with access and refresh tokens + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/user.AuthResponse' + type: object + "400": + description: Bad request - Invalid request format or missing fields + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized - Invalid credentials or account locked + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid input data format + schema: + $ref: '#/definitions/response.APIResponse' + "429": + description: Too many requests - Rate limit exceeded + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Authenticate user login + tags: + - Authorization + /admin/v1/profile/logout: + delete: + consumes: + - application/json + 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. + produces: + - application/json + responses: + "200": + description: Logout successful - all tokens invalidated + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized - Invalid or expired authentication token + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error - Failed to invalidate tokens + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Logout authenticated user + tags: + - Authorization + /admin/v1/profile/refresh-token: + post: + consumes: + - application/json + 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. + parameters: + - description: Refresh token for generating new access token in: body name: refresh required: true @@ -2830,7 +3124,7 @@ paths: - application/json responses: "200": - description: OK + description: Token refreshed successfully with new access token schema: allOf: - $ref: '#/definitions/response.APIResponse' @@ -2839,27 +3133,33 @@ paths: $ref: '#/definitions/user.AuthResponse' type: object "400": - description: Bad Request + description: Bad request - Invalid request format schema: $ref: '#/definitions/response.APIResponse' "401": - description: Unauthorized + description: Unauthorized - Invalid or expired refresh token + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid token format schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal Server Error + description: Internal server error schema: $ref: '#/definitions/response.APIResponse' summary: Refresh access token tags: - Authorization - /admin/v1/reset-password: + /admin/v1/profile/reset-password: post: consumes: - application/json - description: Send password reset email to user + description: Send password reset email to user with reset token. This endpoint + validates the email address and sends a secure reset link to the user's registered + email address. parameters: - - description: Password reset request + - description: Email address for password reset request in: body name: reset required: true @@ -2869,18 +3169,30 @@ paths: - application/json responses: "200": - description: OK + description: Password reset email sent successfully schema: $ref: '#/definitions/response.APIResponse' "400": - description: Bad Request + description: Bad request - Invalid request format or email address + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Email address not registered + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid email format + schema: + $ref: '#/definitions/response.APIResponse' + "429": + description: Too many requests - Rate limit for reset emails exceeded schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal Server Error + description: Internal server error - Failed to send email schema: $ref: '#/definitions/response.APIResponse' - summary: Reset password + summary: Initiate password reset process tags: - Authorization /admin/v1/users: @@ -2957,9 +3269,13 @@ paths: post: consumes: - application/json - description: Create a new user (admin only) + description: Create a new user account with specified role and permissions. + This endpoint is restricted to administrators only and allows creation of + various user types including admins, managers, and operators with appropriate + access levels. parameters: - - description: User creation data + - description: User creation data including username, email, password, role, + and profile information in: body name: user required: true @@ -2969,7 +3285,7 @@ paths: - application/json responses: "201": - description: Created + description: User created successfully with assigned role and permissions schema: allOf: - $ref: '#/definitions/response.APIResponse' @@ -2978,24 +3294,32 @@ paths: $ref: '#/definitions/user.UserResponse' type: object "400": - description: Bad Request + description: Bad request - Invalid input data or missing required fields schema: $ref: '#/definitions/response.APIResponse' "401": - description: Unauthorized + description: Unauthorized - Invalid or expired authentication token schema: $ref: '#/definitions/response.APIResponse' "403": - description: Forbidden + description: Forbidden - Insufficient privileges to create users + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict - Username or email already exists + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid data format or password policy violation schema: $ref: '#/definitions/response.APIResponse' "500": - description: Internal Server Error + description: Internal server error schema: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Create user + summary: Create new user account tags: - Users /admin/v1/users/{id}: @@ -3345,7 +3669,66 @@ paths: summary: Get users by role tags: - Users - /api/v1/login: + /api/v1/: + get: + consumes: + - application/json + description: Retrieve current customer profile information + produces: + - application/json + responses: + "200": + description: Profile retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/customer.CustomerResponse' + type: object + "401": + description: Unauthorized - Invalid or missing token + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get customer profile + tags: + - Customers-Authorization + /api/v1/logout: + delete: + consumes: + - application/json + description: Logout customer and invalidate access token + produces: + - application/json + responses: + "200": + description: Logout successful + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized - Invalid or missing token + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Customer logout + tags: + - Customers-Authorization + /api/v1/profile/login: post: consumes: - application/json @@ -3389,66 +3772,7 @@ paths: summary: Customer login tags: - Customers-Authorization - /api/v1/logout: - delete: - consumes: - - application/json - description: Logout customer and invalidate access token - produces: - - application/json - responses: - "200": - description: Logout successful - schema: - $ref: '#/definitions/response.APIResponse' - "401": - description: Unauthorized - Invalid or missing token - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Customer logout - tags: - - Customers-Authorization - /api/v1/profile: - get: - consumes: - - application/json - description: Retrieve current customer profile information - produces: - - application/json - responses: - "200": - description: Profile retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/customer.CustomerResponse' - type: object - "401": - description: Unauthorized - Invalid or missing token - schema: - $ref: '#/definitions/response.APIResponse' - "404": - description: Not found - Customer not found - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Get customer profile - tags: - - Customers-Authorization - /api/v1/refresh-token: + /api/v1/profile/refresh-token: post: consumes: - application/json @@ -3492,27 +3816,70 @@ paths: summary: Refresh customer access token tags: - Customers-Authorization + /api/v1/with-companies: + get: + consumes: + - application/json + description: Retrieve current customer profile information along with their + assigned companies. + produces: + - application/json + responses: + "200": + description: Profile retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/customer.CustomerResponse' + type: object + "401": + description: Unauthorized - Invalid or missing token + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get customer profile with companies + tags: + - Customers-Authorization securityDefinitions: BearerAuth: - description: Type "Bearer" followed by a space and JWT token. + description: 'Type "Bearer" followed by a space and JWT token. Example: "Bearer + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."' in: header name: Authorization type: apiKey swagger: "2.0" tags: -- description: User management operations including authentication, profile management, - and admin operations - name: Users -- description: Authentication operations including login, logout, and token management - name: Authorization -- description: Customer management operations including authentication, profile management, - and admin operations - name: Customers-Admin -- description: Customer authentication operations including login, logout, and token - management - name: Customers-Authorization -- description: Company management operations for web panel including CRUD, customer - assignment, and tag management - name: Companies-Admin -- description: Health check operations +- description: System health check operations for monitoring application status and + dependencies name: Health +- description: User authentication and authorization operations including login, logout, + token refresh, and password management for web panel administration + name: Authorization +- description: Administrative user management operations including CRUD operations, + role management, status updates, and profile management for web panel administrators + name: Users +- description: Administrative customer management operations for web panel including + CRUD operations, company assignment, verification, status management, and comprehensive + filtering with pagination + name: Customers-Admin +- description: Customer authentication and authorization operations for mobile application + including login, logout, token refresh, and profile access + name: Customers-Authorization +- description: Administrative company management operations for web panel including + CRUD operations, tag management, verification, status updates, comprehensive search + and filtering, and statistical reporting + name: Companies-Admin +- description: Company-related operations for mobile application including company + lookup, search, and basic information retrieval + name: Companies-Mobile diff --git a/cmd/web/health.go b/cmd/web/health.go index 0ced929..31fecd3 100644 --- a/cmd/web/health.go +++ b/cmd/web/health.go @@ -7,24 +7,29 @@ import ( "github.com/labstack/echo/v4" ) -// @Summary Health check -// @Description Get server health status +// @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 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 } diff --git a/cmd/web/main.go b/cmd/web/main.go index 0829cc7..253b75f 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -1,41 +1,45 @@ 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. - -// @tag.name Users -// @tag.description User management operations including authentication, profile management, and admin operations - -// @tag.name Authorization -// @tag.description Authentication operations including login, logout, and token management - -// @tag.name Customers-Admin -// @tag.description Customer management operations including authentication, profile management, and admin operations - -// @tag.name Customers-Authorization -// @tag.description Customer authentication operations including login, logout, and token management - -// @tag.name Companies-Admin -// @tag.description Company management operations for web panel including CRUD, customer assignment, and tag management +// @description Type "Bearer" followed by a space and JWT token. Example: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." // @tag.name Health -// @tag.description Health check operations +// @tag.description System health check operations for monitoring application status and dependencies + +// @tag.name Authorization +// @tag.description User authentication and authorization operations including login, logout, token refresh, and password management for web panel administration + +// @tag.name Users +// @tag.description Administrative user management operations including CRUD operations, role management, status updates, and profile management for web panel administrators + +// @tag.name Customers-Admin +// @tag.description Administrative customer management operations for web panel including CRUD operations, company assignment, verification, status management, and comprehensive filtering with pagination + +// @tag.name Customers-Authorization +// @tag.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access + +// @tag.name Companies-Admin +// @tag.description Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting + +// @tag.name Companies-Mobile +// @tag.description Company-related operations for mobile application including company lookup, search, and basic information retrieval import ( "context" @@ -47,6 +51,7 @@ import ( "tm/internal/user" _ "tm/cmd/web/docs" // This is generated by swag + "tm/cmd/web/router" ) func main() { @@ -95,8 +100,8 @@ 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", "company"}, @@ -115,9 +120,8 @@ func main() { e := initHTTPServer(conf, logger) // Register routes - userHandler.RegisterRoutes(e) - customerHandler.RegisterRoutes(e) - companyHandler.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) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go new file mode 100644 index 0000000..6aeb0b3 --- /dev/null +++ b/cmd/web/router/routes.go @@ -0,0 +1,109 @@ +package router + +import ( + "tm/internal/company" + "tm/internal/customer" + "tm/internal/user" + + "github.com/labstack/echo/v4" +) + +func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler) { + adminV1 := e.Group("/admin/v1") + + // Admin Users Routes + adminUsersGP := adminV1.Group("/users") + { + adminUsersGP.Use(userHandler.AuthMiddleware()) + adminUsersGP.POST("", userHandler.CreateUser) + adminUsersGP.GET("", userHandler.ListUsers) + adminUsersGP.GET("/:id", userHandler.GetUserByID) + adminUsersGP.PUT("/:id", userHandler.UpdateUser) + adminUsersGP.DELETE("/:id", userHandler.DeleteUser) + adminUsersGP.PUT("/:id/status", userHandler.UpdateUserStatus) + adminUsersGP.PUT("/:id/role", userHandler.UpdateUserRole) + adminUsersGP.GET("/company/:company_id", userHandler.GetUsersByCompanyID) + adminUsersGP.GET("/role/:role", userHandler.GetUsersByRole) + } + + // Admin user profile + profileGP := adminV1.Group("/profile") + { + userAuthorizationGP := profileGP.Group("") + userAuthorizationGP.POST("/login", userHandler.Login) + userAuthorizationGP.POST("/refresh-token", userHandler.RefreshToken) + userAuthorizationGP.POST("/reset-password", userHandler.ResetPassword) + + userProfileGP := profileGP.Group("") + userProfileGP.Use(userHandler.AuthMiddleware()) + userProfileGP.GET("", userHandler.GetProfile) + userProfileGP.PUT("", userHandler.UpdateProfile) + userProfileGP.PUT("/change-password", userHandler.ChangePassword) + userProfileGP.DELETE("/logout", userHandler.Logout) + } + + // Admin Companies Routes + companiesGP := adminV1.Group("/companies") + { + companiesGP.Use(userHandler.AuthMiddleware()) + companiesGP.POST("", companyHandler.CreateCompany) + companiesGP.GET("", companyHandler.ListCompanies) + companiesGP.GET("/:id", companyHandler.GetCompany) + companiesGP.PUT("/:id", companyHandler.UpdateCompany) + companiesGP.DELETE("/:id", companyHandler.DeleteCompany) + companiesGP.GET("/search", companyHandler.SearchCompanies) + companiesGP.PATCH("/:id/status", companyHandler.UpdateCompanyStatus) + companiesGP.PATCH("/:id/verification", companyHandler.UpdateCompanyVerification) + companiesGP.PUT("/:id/tags", companyHandler.UpdateCompanyTags) + companiesGP.POST("/:id/tags/add", companyHandler.AddTags) + companiesGP.POST("/:id/tags/remove", companyHandler.RemoveTags) + companiesGP.POST("/:id/verify", companyHandler.VerifyCompany) + companiesGP.POST("/:id/suspend", companyHandler.SuspendCompany) + companiesGP.POST("/:id/activate", companyHandler.ActivateCompany) + companiesGP.GET("/stats", companyHandler.GetCompanyStats) + companiesGP.GET("/type/:type", companyHandler.GetCompaniesByType) + companiesGP.GET("/status/:status", companyHandler.GetCompaniesByStatus) + companiesGP.GET("/industry/:industry", companyHandler.GetCompaniesByIndustry) + } + + // Admin Customers Routes + + customersGP := adminV1.Group("/customers") + { + customersGP.Use(userHandler.AuthMiddleware()) + customersGP.POST("", customerHandler.CreateCustomer) + customersGP.GET("", customerHandler.ListCustomers) + customersGP.GET("/with-companies", customerHandler.ListCustomersWithCompanies) + customersGP.GET("/:id", customerHandler.GetCustomerByID) + customersGP.GET("/:id/with-companies", customerHandler.GetCustomerByIDWithCompanies) + customersGP.PUT("/:id", customerHandler.UpdateCustomer) + customersGP.DELETE("/:id", customerHandler.DeleteCustomer) + customersGP.PATCH("/:id/status", customerHandler.UpdateCustomerStatus) + customersGP.PATCH("/:id/verification", customerHandler.UpdateCustomerVerification) + customersGP.POST("/:id/verify", customerHandler.VerifyCustomer) + customersGP.POST("/:id/suspend", customerHandler.SuspendCustomer) + customersGP.POST("/:id/activate", customerHandler.ActivateCustomer) + customersGP.GET("/company/:companyId", customerHandler.GetCustomersByCompanyID) + customersGP.GET("/type/:type", customerHandler.GetCustomersByType) + customersGP.GET("/status/:status", customerHandler.GetCustomersByStatus) + customersGP.POST("/:id/companies/assign", customerHandler.AssignCompaniesToCustomer) + customersGP.POST("/:id/companies/remove", customerHandler.RemoveCompaniesFromCustomer) + } +} + +func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler) { + v1 := e.Group("/api/v1") + + customerGP := v1.Group("/profile") + { + customerGP.POST("/login", customerHandler.Login) + customerGP.POST("/refresh-token", customerHandler.RefreshToken) + + profileGP := v1.Group("") + profileGP.Use(customerHandler.AuthMiddleware()) + profileGP.GET("", customerHandler.GetProfile) + profileGP.GET("/with-companies", customerHandler.GetProfileWithCompanies) + profileGP.DELETE("/logout", customerHandler.Logout) + } + +} diff --git a/docs/README.md b/docs/README.md index 3a3f704..c435764 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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, }) diff --git a/internal/company/entity.go b/internal/company/entity.go index f38d74b..64f3433 100644 --- a/internal/company/entity.go +++ b/internal/company/entity.go @@ -2,6 +2,8 @@ package company import ( "tm/pkg/mongo" + + "go.mongodb.org/mongo-driver/bson/primitive" ) // CompanyStatus represents company account status @@ -27,7 +29,7 @@ const ( // Company represents a company in the tender management system type Company struct { - mongo.Model + mongo.Model `bson:",inline"` Name string `bson:"name" json:"name"` Type CompanyType `bson:"type" json:"type"` Status CompanyStatus `bson:"status" json:"status"` @@ -73,12 +75,12 @@ type Company struct { // SetID sets the company ID (implements IDSetter interface) func (c *Company) SetID(id string) { - c.ID = id + c.ID, _ = primitive.ObjectIDFromHex(id) } // GetID returns the company ID (implements IDGetter interface) func (c *Company) GetID() string { - return c.ID + return c.ID.Hex() } // SetCreatedAt sets the created timestamp (implements Timestampable interface) @@ -175,7 +177,7 @@ type CompanyResponse struct { // ToResponse converts Company to CompanyResponse func (c *Company) ToResponse() *CompanyResponse { return &CompanyResponse{ - ID: c.ID, + ID: c.ID.Hex(), Name: c.Name, Type: string(c.Type), Status: string(c.Status), diff --git a/internal/company/handler.go b/internal/company/handler.go index b40f133..2c928ef 100644 --- a/internal/company/handler.go +++ b/internal/company/handler.go @@ -25,33 +25,6 @@ func NewHandler(service Service, userHandler *user.Handler, logger logger.Logger } } -// RegisterRoutes registers company routes -func (h *Handler) RegisterRoutes(e *echo.Echo) { - // Admin routes for web panel - adminV1 := e.Group("/admin/v1/companies") - adminV1.Use(h.userHandler.AuthMiddleware()) - adminV1.POST("", h.CreateCompany) - adminV1.GET("", h.ListCompanies) - adminV1.GET("/:id", h.GetCompany) - adminV1.PUT("/:id", h.UpdateCompany) - adminV1.DELETE("/:id", h.DeleteCompany) - adminV1.GET("/search", h.SearchCompanies) - adminV1.PATCH("/:id/status", h.UpdateCompanyStatus) - adminV1.PATCH("/:id/verification", h.UpdateCompanyVerification) - adminV1.PUT("/:id/tags", h.UpdateCompanyTags) - adminV1.POST("/:id/tags/add", h.AddTags) - adminV1.POST("/:id/tags/remove", h.RemoveTags) - adminV1.POST("/:id/verify", h.VerifyCompany) - adminV1.POST("/:id/suspend", h.SuspendCompany) - adminV1.POST("/:id/activate", h.ActivateCompany) - adminV1.GET("/stats", h.GetCompanyStats) - adminV1.GET("/type/:type", h.GetCompaniesByType) - adminV1.GET("/status/:status", h.GetCompaniesByStatus) - adminV1.GET("/industry/:industry", h.GetCompaniesByIndustry) -} - -// Web Panel Endpoints - // CreateCompany creates a new company (Web Panel) // @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. diff --git a/internal/customer/entity.go b/internal/customer/entity.go index ce865eb..756f143 100644 --- a/internal/customer/entity.go +++ b/internal/customer/entity.go @@ -2,6 +2,8 @@ package customer import ( "tm/pkg/mongo" + + "go.mongodb.org/mongo-driver/bson/primitive" ) // CustomerStatus represents customer account status @@ -26,9 +28,9 @@ const ( // Customer represents a customer in the tender management system // A customer can have an associated company for login purposes type Customer struct { - mongo.Model - Type CustomerType `bson:"type" json:"type"` - Status CustomerStatus `bson:"status" json:"status"` + 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" json:"first_name,omitempty"` @@ -40,8 +42,10 @@ type Customer struct { Phone *string `bson:"phone,omitempty" json:"phone,omitempty"` Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"` - // Company customer fields (for when customer represents a company) - CompanyID *string `bson:"company_id,omitempty" json:"company_id,omitempty"` + // Company relationships - each customer can be assigned to multiple companies + CompanyIDs []string `bson:"company_ids,omitempty" json:"company_ids,omitempty"` + + // Company customer fields 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"` @@ -75,12 +79,12 @@ type Customer struct { // SetID sets the customer ID (implements IDSetter interface) func (c *Customer) SetID(id string) { - c.ID = id + c.ID, _ = primitive.ObjectIDFromHex(id) } // GetID returns the customer ID (implements IDGetter interface) func (c *Customer) GetID() string { - return c.ID + return c.ID.Hex() } // SetCreatedAt sets the created timestamp (implements Timestampable interface) @@ -126,44 +130,54 @@ type ContactPerson struct { 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"` - 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"` - CompanyID *string `json:"company_id,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 { return &CustomerResponse{ - ID: c.ID, + ID: c.ID.Hex(), Type: string(c.Type), Status: string(c.Status), FirstName: c.FirstName, @@ -173,7 +187,7 @@ func (c *Customer) ToResponse() *CustomerResponse { Email: c.Email, Phone: c.Phone, Mobile: c.Mobile, - CompanyID: c.CompanyID, + Companies: nil, // Will be populated by service layer RegistrationNumber: c.RegistrationNumber, TaxID: c.TaxID, Industry: c.Industry, @@ -195,3 +209,10 @@ func (c *Customer) ToResponse() *CustomerResponse { 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 +} diff --git a/internal/customer/form.go b/internal/customer/form.go index f823e37..c5bb5a6 100644 --- a/internal/customer/form.go +++ b/internal/customer/form.go @@ -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)"` @@ -41,8 +43,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)"` @@ -154,8 +158,10 @@ type CreateCustomerMobileForm struct { 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)"` - CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"` - Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` + + // Company assignments + CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"` + Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` } type UpdateCustomerMobileForm struct { @@ -164,8 +170,10 @@ type UpdateCustomerMobileForm struct { 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)"` - CompanyID *string `json:"company_id,omitempty" valid:"optional,uuid"` - Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` + + // Company assignments + CompanyIDs []string `json:"company_ids,omitempty" valid:"optional"` + Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` } // Customer Authentication DTOs @@ -204,3 +212,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"` +} diff --git a/internal/customer/handler.go b/internal/customer/handler.go index 442d676..ce312f8 100644 --- a/internal/customer/handler.go +++ b/internal/customer/handler.go @@ -28,38 +28,6 @@ func NewHandler(service Service, userHandler *user.Handler, authService authoriz } } -// RegisterRoutes registers customer routes -func (h *Handler) RegisterRoutes(e *echo.Echo) { - - adminV1 := e.Group("/admin/v1/customers") - adminV1.Use(h.userHandler.AuthMiddleware()) - adminV1.POST("", h.CreateCustomer) - adminV1.GET("", h.ListCustomers) - adminV1.GET("/:id", h.GetCustomerByID) - adminV1.PUT("/:id", h.UpdateCustomer) - adminV1.DELETE("/:id", h.DeleteCustomer) - adminV1.PATCH("/:id/status", h.UpdateCustomerStatus) - adminV1.PATCH("/:id/verification", h.UpdateCustomerVerification) - adminV1.POST("/:id/verify", h.VerifyCustomer) - adminV1.POST("/:id/suspend", h.SuspendCustomer) - adminV1.POST("/:id/activate", h.ActivateCustomer) - adminV1.GET("/company/:companyId", h.GetCustomersByCompanyID) - adminV1.GET("/type/:type", h.GetCustomersByType) - adminV1.GET("/status/:status", h.GetCustomersByStatus) - - // Mobile-specific endpoints - v1 := e.Group("/api/v1") - authorizationGP := v1.Group("") - authorizationGP.POST("/login", h.Login) - authorizationGP.POST("/refresh-token", h.RefreshToken) - - profileGP := v1.Group("") - profileGP.Use(h.AuthMiddleware()) - profileGP.GET("/profile", h.GetProfile) - profileGP.DELETE("/logout", h.Logout) - -} - // CreateCustomer creates a new customer (Web Panel) // @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. @@ -127,6 +95,33 @@ func (h *Handler) GetCustomerByID(c echo.Context) error { return response.Success(c, customer.ToResponse(), "Customer retrieved successfully") } +// GetCustomerByIDWithCompanies retrieves a customer by ID with companies (Web Panel) +// @Summary Get customer by ID with companies +// @Description Retrieve detailed customer information by customer ID including assigned companies +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer ID" +// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Customer retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID" +// @Failure 404 {object} response.APIResponse "Not found - Customer not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id}/with-companies [get] +func (h *Handler) GetCustomerByIDWithCompanies(c echo.Context) error { + idStr := c.Param("id") + + customer, err := h.service.GetCustomerByIDWithCompanies(c.Request().Context(), idStr) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to get customer") + } + + return response.Success(c, customer, "Customer retrieved successfully") +} + // UpdateCustomer updates a customer (Web Panel) // @Summary Update customer information // @Description Update customer information including personal details, company information, address, and business details @@ -247,6 +242,45 @@ func (h *Handler) ListCustomers(c echo.Context) error { return response.Success(c, result, "Customers retrieved successfully") } +// ListCustomersWithCompanies lists customers with companies and filters +// @Summary List customers with companies and filters +// @Description Retrieve a paginated list of customers with their assigned companies and advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param search query string false "Search term to filter customers by name, email, or company name" +// @Param type query string false "Filter by customer type" Enums(individual, company, government) +// @Param status query string false "Filter by customer status" Enums(active, inactive, suspended, pending) +// @Param company_id query string false "Filter by company ID" format(uuid) +// @Param industry query string false "Filter by industry" +// @Param is_verified query boolean false "Filter by verification status" +// @Param is_compliant query boolean false "Filter by compliance status" +// @Param language query string false "Filter by preferred language" +// @Param currency query string false "Filter by preferred currency" +// @Param limit query integer false "Number of customers per page (1-100)" minimum(1) maximum(100) default(20) +// @Param offset query integer false "Number of customers to skip for pagination" minimum(0) default(0) +// @Param sort_by query string false "Field to sort by" Enums(email, company_name, created_at, updated_at, status) default(created_at) +// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc) +// @Success 200 {object} response.APIResponse{data=CustomerListResponse} "Customers with companies retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/with-companies [get] +func (h *Handler) ListCustomersWithCompanies(c echo.Context) error { + form, err := response.Parse[ListCustomersForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + result, err := h.service.ListCustomersWithCompanies(c.Request().Context(), form) + if err != nil { + return response.InternalServerError(c, "Failed to list customers with companies") + } + + return response.Success(c, result, "Customers with companies retrieved successfully") +} + // UpdateCustomerStatus updates customer status (Web Panel) // @Summary Update customer status // @Description Update customer account status (active, inactive, suspended, pending) @@ -604,6 +638,88 @@ func (h *Handler) GetCustomersByStatus(c echo.Context) error { return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully") } +// AssignCompaniesToCustomer assigns companies to a customer (Web Panel) +// @Summary Assign companies to a customer +// @Description Assign one or more companies to a specific customer. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer ID" +// @Param companies body AssignCompaniesForm true "List of company IDs to assign" +// @Success 200 {object} response.APIResponse "Companies assigned successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Customer not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id}/companies/assign [post] +func (h *Handler) AssignCompaniesToCustomer(c echo.Context) error { + idStr := c.Param("id") + form, err := response.Parse[AssignCompaniesForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + assignedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.AssignCompaniesToCustomer(c.Request().Context(), idStr, form.CompanyIDs, &assignedBy) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to assign companies to customer") + } + + return response.Success(c, map[string]interface{}{ + "message": "Companies assigned successfully", + }, "Companies assigned successfully") +} + +// RemoveCompaniesFromCustomer removes companies from a customer (Web Panel) +// @Summary Remove companies from a customer +// @Description Remove one or more companies from a specific customer. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer ID" +// @Param companies body RemoveCompaniesForm true "List of company IDs to remove" +// @Success 200 {object} response.APIResponse "Companies removed successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Customer not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id}/companies/remove [post] +func (h *Handler) RemoveCompaniesFromCustomer(c echo.Context) error { + idStr := c.Param("id") + form, err := response.Parse[RemoveCompaniesForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + removedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.RemoveCompaniesFromCustomer(c.Request().Context(), idStr, form.CompanyIDs, &removedBy) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to remove companies from customer") + } + + return response.Success(c, map[string]interface{}{ + "message": "Companies removed successfully", + }, "Companies removed successfully") +} + // Login handles customer authentication // @Summary Customer login // @Description Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication. @@ -616,7 +732,7 @@ func (h *Handler) GetCustomersByStatus(c echo.Context) error { // @Failure 401 {object} response.APIResponse "Unauthorized - Invalid credentials or inactive account" // @Failure 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 { @@ -650,7 +766,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 { @@ -680,7 +796,7 @@ func (h *Handler) RefreshToken(c echo.Context) error { // @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] +// @Router /api/v1/ [get] func (h *Handler) GetProfile(c echo.Context) error { // Extract customer ID from JWT token context customerID, err := GetCustomerIDFromContext(c) @@ -699,6 +815,36 @@ func (h *Handler) GetProfile(c echo.Context) error { return response.Success(c, customer.ToResponse(), "Profile retrieved successfully") } +// GetProfileWithCompanies retrieves customer profile with companies (Mobile) +// @Summary Get customer profile with companies +// @Description Retrieve current customer profile information along with their assigned companies. +// @Tags Customers-Authorization +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Profile retrieved successfully" +// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing token" +// @Failure 404 {object} response.APIResponse "Not found - Customer not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Router /api/v1/with-companies [get] +func (h *Handler) GetProfileWithCompanies(c echo.Context) error { + // Extract customer ID from JWT token context + customerID, err := GetCustomerIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "Customer not authenticated") + } + + customer, err := h.service.GetProfileWithCompanies(c.Request().Context(), customerID) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to get customer profile with companies") + } + + return response.Success(c, customer, "Profile retrieved successfully") +} + // Logout handles customer logout (Mobile) // @Summary Customer logout // @Description Logout customer and invalidate access token diff --git a/internal/customer/repository.go b/internal/customer/repository.go index 8a9a578..de4e1f0 100644 --- a/internal/customer/repository.go +++ b/internal/customer/repository.go @@ -20,11 +20,13 @@ type Repository interface { GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) GetByTaxID(ctx context.Context, taxID string) (*Customer, error) GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, error) + GetByCompanyIDs(ctx context.Context, companyIDs []string, limit, offset int) ([]*Customer, error) Update(ctx context.Context, customer *Customer) error Delete(ctx context.Context, id string) error List(ctx context.Context, limit, offset int) ([]*Customer, error) Search(ctx context.Context, search string, customerType *string, status *string, companyID *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) CountByCompanyID(ctx context.Context, companyID string) (int64, error) + CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error) CountByType(ctx context.Context, customerType CustomerType) (int64, error) CountByStatus(ctx context.Context, status CustomerStatus) (int64, error) UpdateStatus(ctx context.Context, id string, status CustomerStatus) error @@ -46,7 +48,7 @@ func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logg *mongopkg.NewIndex("company_name_idx", bson.D{{Key: "company_name", Value: 1}}), *mongopkg.NewIndex("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_id_idx", bson.D{{Key: "company_id", Value: 1}}), + *mongopkg.NewIndex("company_ids_idx", bson.D{{Key: "company_ids", Value: 1}}), // Index for CompanyIDs array *mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}), *mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), *mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}), @@ -245,6 +247,46 @@ func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID strin return customers, nil } +// GetByCompanyIDs retrieves customers by multiple company IDs with pagination +func (r *customerRepository) GetByCompanyIDs(ctx context.Context, companyIDs []string, limit, offset int) ([]*Customer, error) { + if len(companyIDs) == 0 { + return nil, errors.New("no company IDs provided") + } + + // Build pagination + pagination := mongopkg.NewPaginationBuilder(). + Limit(limit). + Skip(offset). + SortDesc("created_at"). + Build() + + // Filter by company IDs and active status + filter := bson.M{ + "company_ids": bson.M{"$in": companyIDs}, + "status": bson.M{"$ne": CustomerStatusInactive}, + } + + // Use ORM to find customers + result, err := r.ormRepo.FindAll(ctx, filter, pagination) + if err != nil { + r.logger.Error("Failed to get customers by multiple company IDs", map[string]interface{}{ + "error": err.Error(), + "company_ids": companyIDs, + "limit": limit, + "offset": offset, + }) + return nil, err + } + + // Convert []Customer to []*Customer + customers := make([]*Customer, len(result.Items)) + for i := range result.Items { + customers[i] = &result.Items[i] + } + + return customers, nil +} + // Update updates a customer func (r *customerRepository) Update(ctx context.Context, customer *Customer) error { // Set updated timestamp using Unix timestamp @@ -424,6 +466,29 @@ func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID str return count, nil } +// CountByCompanyIDs counts customers by multiple company IDs +func (r *customerRepository) CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error) { + if len(companyIDs) == 0 { + return 0, errors.New("no company IDs provided") + } + + filter := bson.M{ + "company_ids": bson.M{"$in": companyIDs}, + "status": bson.M{"$ne": CustomerStatusInactive}, + } + + count, err := r.ormRepo.Count(ctx, filter) + if err != nil { + r.logger.Error("Failed to count customers by multiple company IDs", map[string]interface{}{ + "error": err.Error(), + "company_ids": companyIDs, + }) + return 0, err + } + + return count, nil +} + // CountByType counts customers by type func (r *customerRepository) CountByType(ctx context.Context, customerType CustomerType) (int64, error) { filter := bson.M{ diff --git a/internal/customer/service.go b/internal/customer/service.go index e7cd865..ec96291 100644 --- a/internal/customer/service.go +++ b/internal/customer/service.go @@ -5,11 +5,12 @@ import ( "errors" "time" + "tm/internal/company" "tm/pkg/logger" "tm/pkg/authorization" - "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson/primitive" "golang.org/x/crypto/bcrypt" ) @@ -18,11 +19,13 @@ type Service interface { // Core customer operations CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *string) (*Customer, error) GetCustomerByID(ctx context.Context, id string) (*Customer, error) + GetCustomerByIDWithCompanies(ctx context.Context, id string) (*CustomerResponse, error) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) DeleteCustomer(ctx context.Context, id string, deletedBy *string) error // Customer listing and search ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) + ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) GetCustomersByType(ctx context.Context, customerType CustomerType, limit, offset int) ([]*Customer, int64, error) GetCustomersByStatus(ctx context.Context, status CustomerStatus, limit, offset int) ([]*Customer, int64, error) @@ -31,6 +34,10 @@ type Service interface { UpdateCustomerStatus(ctx context.Context, id string, form *UpdateCustomerStatusForm, updatedBy *string) error UpdateCustomerVerification(ctx context.Context, id string, form *UpdateCustomerVerificationForm, updatedBy *string) error + // Company assignments + AssignCompaniesToCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error + RemoveCompaniesFromCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error + // Mobile-specific operations (simplified) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error) UpdateCustomerMobile(ctx context.Context, id string, form *UpdateCustomerMobileForm, updatedBy *string) (*Customer, error) @@ -44,22 +51,25 @@ type Service interface { Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) GetProfile(ctx context.Context, customerID string) (*Customer, error) + GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error) Logout(ctx context.Context, customerID string, accessToken string) error } // customerService implements the Service interface type customerService struct { - repository Repository - logger logger.Logger - authService authorization.AuthorizationService + repository Repository + logger logger.Logger + authService authorization.AuthorizationService + companyService company.Service } // NewCustomerService creates a new customer service -func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService) Service { +func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service) Service { return &customerService{ - repository: repository, - logger: logger, - authService: authService, + repository: repository, + logger: logger, + authService: authService, + companyService: companyService, } } @@ -95,10 +105,21 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom } } - // Parse company ID if provided - var companyID *string - if form.CompanyID != nil { - companyID = form.CompanyID + // Prepare company assignments + var companyIDs []string + if len(form.CompanyIDs) > 0 { + // Verify that all company IDs exist + for _, companyID := range form.CompanyIDs { + _, err := s.companyService.GetCompanyByID(ctx, companyID) + if err != nil { + s.logger.Error("Invalid company ID provided", map[string]interface{}{ + "error": err.Error(), + "company_id": companyID, + }) + return nil, errors.New("invalid company ID: " + companyID) + } + } + companyIDs = form.CompanyIDs } // Hash password @@ -114,7 +135,7 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom customer := &Customer{ Type: CustomerType(form.Type), Status: CustomerStatusPending, - CompanyID: companyID, + CompanyIDs: companyIDs, FirstName: form.FirstName, LastName: form.LastName, FullName: form.FullName, @@ -155,6 +176,7 @@ func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustom "customer_id": customer.ID, "email": customer.Email, "type": customer.Type, + "company_ids": companyIDs, "created_by": *createdBy, }) @@ -180,6 +202,38 @@ func (s *customerService) GetCustomerByID(ctx context.Context, id string) (*Cust return customer, nil } +// GetCustomerByIDWithCompanies retrieves a customer by ID and loads associated companies +func (s *customerService) GetCustomerByIDWithCompanies(ctx context.Context, id string) (*CustomerResponse, error) { + customer, err := s.repository.GetByID(ctx, id) + if err != nil { + s.logger.Error("Failed to get customer by ID with companies", map[string]interface{}{ + "error": err.Error(), + "customer_id": id, + }) + return nil, err + } + + // Load companies for the customer + companies, err := s.loadCompaniesForCustomer(ctx, customer) + if err != nil { + s.logger.Error("Failed to load companies for customer", map[string]interface{}{ + "error": err.Error(), + "customer_id": customer.ID, + }) + // Continue without companies if loading fails + companies = []*CompanySummary{} + } + + customerResponse := customer.ToResponseWithCompanies(companies) + + s.logger.Info("Customer retrieved with companies successfully", map[string]interface{}{ + "customer_id": customer.ID, + "email": customer.Email, + }) + + return customerResponse, nil +} + // UpdateCustomer updates a customer func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *UpdateCustomerForm, updatedBy *string) (*Customer, error) { // Get existing customer @@ -233,8 +287,22 @@ func (s *customerService) UpdateCustomer(ctx context.Context, id string, form *U customer.Type = CustomerType(*form.Type) } - if form.CompanyID != nil { - customer.CompanyID = form.CompanyID + // Handle company assignments + if len(form.CompanyIDs) > 0 { + // Verify that all company IDs exist + var validCompanyIDs []string + for _, companyID := range form.CompanyIDs { + _, err := s.companyService.GetCompanyByID(ctx, companyID) + if err != nil { + s.logger.Error("Invalid company ID provided in update", map[string]interface{}{ + "error": err.Error(), + "company_id": companyID, + }) + return nil, errors.New("invalid company ID: " + companyID) + } + validCompanyIDs = append(validCompanyIDs, companyID) + } + customer.CompanyIDs = validCompanyIDs } if form.FirstName != nil { @@ -411,6 +479,79 @@ func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomers }, nil } +// ListCustomersWithCompanies lists customers with search and filters, including companies +func (s *customerService) ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) { + // Set defaults + limit := 20 + if form.Limit != nil { + limit = *form.Limit + } + + offset := 0 + if form.Offset != nil { + offset = *form.Offset + } + + search := "" + if form.Search != nil { + search = *form.Search + } + + sortBy := "created_at" + if form.SortBy != nil { + sortBy = *form.SortBy + } + + sortOrder := "desc" + if form.SortOrder != nil { + sortOrder = *form.SortOrder + } + + // Parse company ID if provided + var companyID *string + if form.CompanyID != nil { + companyID = form.CompanyID + } + + // Get customers + customers, err := s.repository.Search(ctx, search, form.Type, form.Status, companyID, form.Industry, form.IsVerified, form.IsCompliant, form.Language, form.Currency, limit, offset, sortBy, sortOrder) + if err != nil { + s.logger.Error("Failed to list customers with companies", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to list customers with companies") + } + + // Convert to responses + var customerResponses []*CustomerResponse + for _, customer := range customers { + // Load companies for the customer + companies, err := s.loadCompaniesForCustomer(ctx, customer) + if err != nil { + s.logger.Error("Failed to load companies for customer in list", map[string]interface{}{ + "error": err.Error(), + "customer_id": customer.ID, + }) + // Continue without companies if loading fails + companies = []*CompanySummary{} + } + customerResponse := customer.ToResponseWithCompanies(companies) + customerResponses = append(customerResponses, customerResponse) + } + + // TODO: Implement proper count for pagination metadata + total := int64(len(customers)) // This should be a separate count query + totalPages := (total + int64(limit) - 1) / int64(limit) + + return &CustomerListResponse{ + Customers: customerResponses, + Total: total, + Limit: limit, + Offset: offset, + TotalPages: int(totalPages), + }, nil +} + // GetCustomersByCompanyID retrieves customers by company ID with pagination func (s *customerService) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) { customers, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset) @@ -546,6 +687,98 @@ func (s *customerService) UpdateCustomerVerification(ctx context.Context, id str return nil } +// AssignCompaniesToCustomer assigns companies to a customer +func (s *customerService) AssignCompaniesToCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error { + // Get customer to check if exists + customer, err := s.repository.GetByID(ctx, customerID) + if err != nil { + return errors.New("customer not found") + } + + // Verify companies exist and update customer's company IDs + var validCompanyIDs []string + for _, companyID := range companyIDs { + _, err = s.companyService.GetCompanyByID(ctx, companyID) + if err != nil { + s.logger.Error("Company not found during assignment", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + "company_id": companyID, + }) + continue // Skip invalid company IDs + } + validCompanyIDs = append(validCompanyIDs, companyID) + } + + // Update customer's company IDs + customer.CompanyIDs = append(customer.CompanyIDs, validCompanyIDs...) + customer.UpdatedBy = updatedBy + customer.UpdatedAt = time.Now().Unix() + + err = s.repository.Update(ctx, customer) + if err != nil { + s.logger.Error("Failed to update customer with company assignments", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + return errors.New("failed to assign companies to customer") + } + + s.logger.Info("Companies assigned to customer successfully", map[string]interface{}{ + "customer_id": customerID, + "company_ids": validCompanyIDs, + "updated_by": updatedBy, + }) + + return nil +} + +// RemoveCompaniesFromCustomer removes companies from a customer +func (s *customerService) RemoveCompaniesFromCustomer(ctx context.Context, customerID string, companyIDs []string, updatedBy *string) error { + // Get customer to check if exists + customer, err := s.repository.GetByID(ctx, customerID) + if err != nil { + return errors.New("customer not found") + } + + // Remove company IDs from customer's list + var updatedCompanyIDs []string + for _, existingID := range customer.CompanyIDs { + shouldRemove := false + for _, removeID := range companyIDs { + if existingID == removeID { + shouldRemove = true + break + } + } + if !shouldRemove { + updatedCompanyIDs = append(updatedCompanyIDs, existingID) + } + } + + // Update customer's company IDs + customer.CompanyIDs = updatedCompanyIDs + customer.UpdatedBy = updatedBy + customer.UpdatedAt = time.Now().Unix() + + err = s.repository.Update(ctx, customer) + if err != nil { + s.logger.Error("Failed to update customer after removing company assignments", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + return errors.New("failed to remove companies from customer") + } + + s.logger.Info("Companies removed from customer successfully", map[string]interface{}{ + "customer_id": customerID, + "company_ids": companyIDs, + "updated_by": updatedBy, + }) + + return nil +} + // CreateCustomerMobile creates a new customer via mobile app (simplified) func (s *customerService) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *string) (*Customer, error) { // Check if email already exists @@ -554,10 +787,28 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create return nil, errors.New("customer with this email already exists") } + // Prepare company assignments + var companyIDs []string + if len(form.CompanyIDs) > 0 { + // Verify that all company IDs exist + for _, companyID := range form.CompanyIDs { + _, err := s.companyService.GetCompanyByID(ctx, companyID) + if err != nil { + s.logger.Error("Invalid company ID provided in mobile form", map[string]interface{}{ + "error": err.Error(), + "company_id": companyID, + }) + return nil, errors.New("invalid company ID: " + companyID) + } + } + companyIDs = form.CompanyIDs + } + // Create customer with mobile form customer := &Customer{ Type: CustomerType(form.Type), Status: CustomerStatusActive, + CompanyIDs: companyIDs, FirstName: form.FirstName, LastName: form.LastName, FullName: form.FullName, @@ -566,7 +817,6 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create Password: "", // Will be set after hashing Phone: form.Phone, Mobile: form.Mobile, - CompanyID: form.CompanyID, Industry: form.Industry, IsVerified: false, IsCompliant: false, @@ -602,6 +852,7 @@ func (s *customerService) CreateCustomerMobile(ctx context.Context, form *Create "customer_id": customer.ID, "email": customer.Email, "type": customer.Type, + "company_ids": companyIDs, "created_by": *createdBy, }) @@ -637,8 +888,22 @@ func (s *customerService) UpdateCustomerMobile(ctx context.Context, id string, f customer.Mobile = form.Mobile } - if form.CompanyID != nil { - customer.CompanyID = form.CompanyID + // Handle company assignments + if len(form.CompanyIDs) > 0 { + // Verify that all company IDs exist + var validCompanyIDs []string + for _, companyID := range form.CompanyIDs { + _, err := s.companyService.GetCompanyByID(ctx, companyID) + if err != nil { + s.logger.Error("Invalid company ID provided in mobile update", map[string]interface{}{ + "error": err.Error(), + "company_id": companyID, + }) + return nil, errors.New("invalid company ID: " + companyID) + } + validCompanyIDs = append(validCompanyIDs, companyID) + } + customer.CompanyIDs = validCompanyIDs } if form.Industry != nil { @@ -661,6 +926,7 @@ func (s *customerService) UpdateCustomerMobile(ctx context.Context, id string, f s.logger.Info("Customer updated via mobile successfully", map[string]interface{}{ "customer_id": customer.ID, "email": customer.Email, + "company_ids": customer.CompanyIDs, "updated_by": *updatedBy, }) @@ -783,12 +1049,12 @@ func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResp // Generate JWT tokens using authorization service companyID := "" - if customer.CompanyID != nil { - companyID = *customer.CompanyID + if len(customer.CompanyIDs) > 0 { + companyID = customer.CompanyIDs[0] // Use first company ID for JWT token } tokenPair, err := s.authService.GenerateTokenPair( - customer.ID, + customer.ID.Hex(), customer.Email, "customer", // Role for customers companyID, @@ -847,7 +1113,7 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string) } // Get customer information - customerID, err := uuid.Parse(validationResult.Claims.UserID) + customerID, err := primitive.ObjectIDFromHex(validationResult.Claims.UserID) if err != nil { s.logger.Error("Failed to parse customer ID from token", map[string]interface{}{ "error": err.Error(), @@ -855,11 +1121,11 @@ func (s *customerService) RefreshToken(ctx context.Context, refreshToken string) return nil, errors.New("invalid token format") } - customer, err := s.repository.GetByID(ctx, customerID.String()) + customer, err := s.repository.GetByID(ctx, customerID.Hex()) if err != nil { s.logger.Error("Failed to get customer for token refresh", map[string]interface{}{ "error": err.Error(), - "customer_id": customerID.String(), + "customer_id": customerID.Hex(), }) return nil, errors.New("customer not found") } @@ -905,6 +1171,42 @@ func (s *customerService) GetProfile(ctx context.Context, customerID string) (*C return customer, nil } +// GetProfileWithCompanies retrieves customer profile information including companies +func (s *customerService) GetProfileWithCompanies(ctx context.Context, customerID string) (*CustomerResponse, error) { + s.logger.Info("Getting customer profile with companies", map[string]interface{}{ + "customer_id": customerID, + }) + + customer, err := s.repository.GetByID(ctx, customerID) + if err != nil { + s.logger.Error("Failed to get customer profile with companies", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + return nil, errors.New("customer not found") + } + + // Load companies for the customer + companies, err := s.loadCompaniesForCustomer(ctx, customer) + if err != nil { + s.logger.Error("Failed to load companies for customer profile", map[string]interface{}{ + "error": err.Error(), + "customer_id": customer.ID, + }) + // Continue without companies if loading fails + companies = []*CompanySummary{} + } + + customerResponse := customer.ToResponseWithCompanies(companies) + + s.logger.Info("Customer profile retrieved with companies successfully", map[string]interface{}{ + "customer_id": customerID, + "customer_type": string(customer.Type), + }) + + return customerResponse, nil +} + // Logout handles customer logout func (s *customerService) Logout(ctx context.Context, customerID string, accessToken string) error { s.logger.Info("Customer logout", map[string]interface{}{ @@ -983,3 +1285,27 @@ func (s *customerService) getDefaultTimezone(timezone *string) string { } return "UTC" } + +// loadCompaniesForCustomer loads company summaries for a customer +func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) { + var companies []*CompanySummary + + // Load companies from CompanyIDs + for _, companyID := range customer.CompanyIDs { + company, err := s.companyService.GetCompanyByID(ctx, companyID) + if err != nil { + s.logger.Warn("Failed to load company for customer", map[string]interface{}{ + "error": err.Error(), + "customer_id": customer.ID, + "company_id": companyID, + }) + continue // Skip companies that can't be loaded + } + companies = append(companies, &CompanySummary{ + ID: company.ID.Hex(), + Name: company.Name, + }) + } + + return companies, nil +} diff --git a/internal/user/entity.go b/internal/user/entity.go index f309c6f..90e30cc 100644 --- a/internal/user/entity.go +++ b/internal/user/entity.go @@ -2,6 +2,8 @@ package user import ( "tm/pkg/mongo" + + "go.mongodb.org/mongo-driver/bson/primitive" ) // UserRole represents user roles in the system @@ -25,7 +27,7 @@ const ( // User represents a system user (admin/manager/operator) type User struct { - mongo.Model + mongo.Model `bson:",inline"` FullName string `bson:"full_name" json:"full_name"` Username string `bson:"username" json:"username"` Email string `bson:"email" json:"email"` @@ -45,12 +47,12 @@ type User struct { // SetID sets the user ID (implements IDSetter interface) func (u *User) SetID(id string) { - u.ID = id + u.ID, _ = primitive.ObjectIDFromHex(id) } // GetID returns the user ID (implements IDGetter interface) func (u *User) GetID() string { - return u.ID + return u.ID.Hex() } // SetCreatedAt sets the created timestamp (implements Timestampable interface) diff --git a/internal/user/form.go b/internal/user/form.go index c337f13..1e7972a 100644 --- a/internal/user/form.go +++ b/internal/user/form.go @@ -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 @@ -113,7 +122,7 @@ type UserListResponse struct { // Helper function to convert User to UserResponse func (u *User) ToResponse() *UserResponse { return &UserResponse{ - ID: u.ID, + ID: u.ID.Hex(), FullName: u.FullName, Username: u.Username, Email: u.Email, diff --git a/internal/user/handler.go b/internal/user/handler.go index 270c0f4..f6310f1 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -28,50 +28,20 @@ func NewUserHandler(service Service, logger logger.Logger, validator ValidationS } } -// RegisterRoutes registers user routes -func (h *Handler) RegisterRoutes(e *echo.Echo) { - v1 := e.Group("/admin/v1") - - // Public routes (no authentication required) - authorizationGP := v1.Group("") - authorizationGP.POST("/login", h.Login) - authorizationGP.POST("/refresh-token", h.RefreshToken) - authorizationGP.POST("/reset-password", h.ResetPassword) - - // Protected routes (require authentication) - profileGP := v1.Group("") - profileGP.Use(h.AuthMiddleware()) - profileGP.GET("/profile", h.GetProfile) - profileGP.PUT("/profile", h.UpdateProfile) - profileGP.PUT("/change-password", h.ChangePassword) - profileGP.DELETE("/logout", h.Logout) - - // Admin routes (require admin role) - adminGroup := v1.Group("/users") - adminGroup.Use(h.AuthMiddleware(), h.AdminMiddleware()) - adminGroup.POST("", h.CreateUser) - adminGroup.GET("", h.ListUsers) - adminGroup.GET("/:id", h.GetUserByID) - adminGroup.PUT("/:id", h.UpdateUser) - adminGroup.DELETE("/:id", h.DeleteUser) - adminGroup.PUT("/:id/status", h.UpdateUserStatus) - adminGroup.PUT("/:id/role", h.UpdateUserRole) - adminGroup.GET("/company/:company_id", h.GetUsersByCompanyID) - adminGroup.GET("/role/:role", h.GetUsersByRole) -} - // Login handles user login -// @Summary Login user -// @Description Authenticate user with username and password +// @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 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 { @@ -89,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 +// @Description Generate a new access token using a valid refresh token. This endpoint allows clients to obtain fresh access tokens without re-authentication, maintaining session continuity. // @Tags Authorization // @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 { @@ -114,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 +// @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 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 { @@ -141,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 +// @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 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) @@ -168,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 +// @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 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) @@ -201,18 +177,19 @@ func (h *Handler) UpdateProfile(c echo.Context) error { } // ChangePassword changes current user password -// @Summary Change password -// @Description Change current user password +// @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 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 { @@ -235,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 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) @@ -268,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) +// @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 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) @@ -327,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 { @@ -362,7 +341,7 @@ 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") @@ -389,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) @@ -432,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) @@ -467,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 { @@ -512,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) @@ -558,7 +537,7 @@ 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") @@ -618,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) diff --git a/internal/user/middleware.go b/internal/user/middleware.go index 7fb8b19..102e1b5 100644 --- a/internal/user/middleware.go +++ b/internal/user/middleware.go @@ -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.String()) + 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) diff --git a/internal/user/service.go b/internal/user/service.go index 2320a01..3ec28fb 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -8,7 +8,7 @@ import ( "tm/pkg/authorization" "tm/pkg/logger" - "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson/primitive" "golang.org/x/crypto/bcrypt" ) @@ -118,7 +118,7 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea "email": user.Email, "username": user.Username, "role": user.Role, - "created_by": *createdBy, + "created_by": createdBy, }) return user, nil @@ -160,7 +160,7 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse } // 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(), @@ -175,7 +175,7 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse } tokenPair, err := s.authService.GenerateTokenPair( - user.ID, + user.ID.Hex(), user.Email, string(user.Role), companyID, @@ -230,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(), @@ -238,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.String()) + 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") } @@ -420,7 +420,7 @@ func (s *userService) UpdateUser(ctx context.Context, id string, form *UpdateUse 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 @@ -429,7 +429,7 @@ func (s *userService) UpdateUser(ctx context.Context, id string, form *UpdateUse 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 diff --git a/pkg/logger/LOGGING_UPGRADE.md b/pkg/logger/LOGGING_UPGRADE.md index c467bf0..8b6eb9d 100644 --- a/pkg/logger/LOGGING_UPGRADE.md +++ b/pkg/logger/LOGGING_UPGRADE.md @@ -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, }) diff --git a/pkg/mongo/interfaces.go b/pkg/mongo/interfaces.go index c7c38d0..36c9ea6 100644 --- a/pkg/mongo/interfaces.go +++ b/pkg/mongo/interfaces.go @@ -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 diff --git a/pkg/mongo/repository_test.go b/pkg/mongo/repository_test.go deleted file mode 100644 index 3bd7f67..0000000 --- a/pkg/mongo/repository_test.go +++ /dev/null @@ -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) -}