diff --git a/Makefile b/Makefile index 3cc570d..5e75d8b 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Tender Management Backend Makefile -.PHONY: help build run test clean docker-build docker-run +.PHONY: help build run test clean docker-build docker-run docs run-docs # Default target help: @@ -92,6 +92,7 @@ lint: @golangci-lint run # Generate API documentation +# Marked as .PHONY to avoid collision with the top-level docs directory docs: @echo "Generating API documentation..." @swag init -g cmd/web/main.go -o cmd/web/docs diff --git a/cmd/web/config.yaml b/cmd/web/config.yaml index aee0c85..94646fa 100644 --- a/cmd/web/config.yaml +++ b/cmd/web/config.yaml @@ -36,7 +36,14 @@ search: password: "" index_prefix: "tender_" -auth: +user_authorization: + jwt: + access_secret: "your-super-secret-access-token-key-here-make-it-long-and-secure" + refresh_secret: "your-super-secret-refresh-token-key-here-make-it-long-and-secure" + access_expires_in: 3600 # 1 hour in seconds + refresh_expires_in: 2592000 # 30 days in seconds + +customer_authorization: jwt: access_secret: "your-super-secret-access-token-key-here-make-it-long-and-secure" refresh_secret: "your-super-secret-refresh-token-key-here-make-it-long-and-secure" diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 730915d..977972d 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -24,7 +24,7 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/change-password": { + "/admin/v1/change-password": { "put": { "security": [ { @@ -81,9 +81,14 @@ const docTemplate = `{ } } }, - "/health": { + "/admin/v1/customers": { "get": { - "description": "Get server health status", + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a paginated list of customers with advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination.", "consumes": [ "application/json" ], @@ -91,20 +96,1025 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Health" + "Customers-Admin" + ], + "summary": "List customers with filters and pagination", + "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" + } ], - "summary": "Health check", "responses": { "200": { - "description": "OK", + "description": "Customers retrieved successfully", "schema": { - "$ref": "#/definitions/main.HealthResponse" + "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" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Create a new customer", + "parameters": [ + { + "description": "Customer information including type (individual|company|government), personal details, company info, address, and business details", + "name": "customer", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.CreateCustomerForm" + } + } + ], + "responses": { + "201": { + "description": "Customer created successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Customer with this email/company already exists", + "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" } } } } }, - "/login": { + "/admin/v1/customers/company/{companyId}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve all customers associated with a specific company. This is useful for company administrators to see all users within their organization.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Get customers by company ID", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Company UUID", + "name": "companyId", + "in": "path", + "required": true + }, + { + "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" + } + ], + "responses": { + "200": { + "description": "Customers retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/customer.CustomerResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid company ID format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/status/{status}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve customers filtered by their account status (active, inactive, suspended, or pending). Useful for administrative monitoring and management.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Get customers by status", + "parameters": [ + { + "enum": [ + "active", + "inactive", + "suspended", + "pending" + ], + "type": "string", + "description": "Customer status", + "name": "status", + "in": "path", + "required": true + }, + { + "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" + } + ], + "responses": { + "200": { + "description": "Customers retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/customer.CustomerResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid customer status", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/type/{type}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve customers filtered by their type (individual, company, or government). Useful for administrative reporting and management.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Get customers by type", + "parameters": [ + { + "enum": [ + "individual", + "company", + "government" + ], + "type": "string", + "description": "Customer type", + "name": "type", + "in": "path", + "required": true + }, + { + "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" + } + ], + "responses": { + "200": { + "description": "Customers retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/customer.CustomerResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid customer type", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve detailed customer information by unique customer ID. Returns comprehensive customer data including personal details, company information, address, verification status, and audit fields.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Get customer by ID", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid customer ID format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update existing customer information with comprehensive details. All fields are optional and only provided fields will be updated. This endpoint is used by the web panel for full customer management.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Update customer information", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Customer update information - all fields are optional", + "name": "customer", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.UpdateCustomerForm" + } + } + ], + "responses": { + "200": { + "description": "Customer updated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid customer ID or input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Customer with this email/company already exists", + "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" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Soft delete a customer by setting status to inactive. The customer record is preserved but marked as deleted. This is a reversible operation.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Delete customer", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer deleted successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/activate": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Reactivate a suspended customer account by setting their status back to active. This reverses the suspension action.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Activate suspended customer account", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer activated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/status": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the status of a customer account. Valid statuses include active, inactive, suspended, and pending. This affects the customer's ability to access the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Update customer status", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Status update information", + "name": "status", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.UpdateCustomerStatusForm" + } + } + ], + "responses": { + "200": { + "description": "Customer status updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID or status", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid status value", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/suspend": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Suspend a customer account by setting their status to suspended. Suspended customers cannot access the system. A reason for suspension must be provided.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Suspend customer account", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Suspension reason", + "name": "reason", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "Customer suspended successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID or missing reason", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/verification": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the verification and compliance status of a customer. This includes marking customers as verified and compliant, with optional compliance notes for audit purposes.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Update customer verification and compliance status", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Verification and compliance update information", + "name": "verification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.UpdateCustomerVerificationForm" + } + } + ], + "responses": { + "200": { + "description": "Customer verification updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid verification data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/verify": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a customer as verified. This is a simple verification action that sets the customer's verification status to true.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Verify customer", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer verified successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/login": { "post": { "description": "Authenticate user with username and password", "consumes": [ @@ -168,7 +1178,7 @@ const docTemplate = `{ } } }, - "/logout": { + "/admin/v1/logout": { "delete": { "security": [ { @@ -208,7 +1218,7 @@ const docTemplate = `{ } } }, - "/profile": { + "/admin/v1/profile": { "get": { "security": [ { @@ -333,7 +1343,7 @@ const docTemplate = `{ } } }, - "/refresh-token": { + "/admin/v1/refresh-token": { "post": { "description": "Refresh access token using refresh token", "consumes": [ @@ -397,7 +1407,7 @@ const docTemplate = `{ } } }, - "/reset-password": { + "/admin/v1/reset-password": { "post": { "description": "Send password reset email to user", "consumes": [ @@ -443,7 +1453,7 @@ const docTemplate = `{ } } }, - "/users": { + "/admin/v1/users": { "get": { "security": [ { @@ -630,7 +1640,7 @@ const docTemplate = `{ } } }, - "/users/company/{company_id}": { + "/admin/v1/users/company/{company_id}": { "get": { "security": [ { @@ -716,7 +1726,7 @@ const docTemplate = `{ } } }, - "/users/role/{role}": { + "/admin/v1/users/role/{role}": { "get": { "security": [ { @@ -802,7 +1812,7 @@ const docTemplate = `{ } } }, - "/users/{id}": { + "/admin/v1/users/{id}": { "get": { "security": [ { @@ -1032,7 +2042,7 @@ const docTemplate = `{ } } }, - "/users/{id}/role": { + "/admin/v1/users/{id}/role": { "put": { "security": [ { @@ -1108,7 +2118,7 @@ const docTemplate = `{ } } }, - "/users/{id}/status": { + "/admin/v1/users/{id}/status": { "put": { "security": [ { @@ -1183,9 +2193,733 @@ const docTemplate = `{ } } } + }, + "/api/v1/login": { + "post": { + "description": "Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Customer login", + "parameters": [ + { + "description": "Login credentials (username/email and password)", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.LoginForm" + } + } + ], + "responses": { + "200": { + "description": "Login successful", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.AuthResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid credentials or inactive account", + "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" + } + } + } + } + }, + "/api/v1/logout": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Logout customer and invalidate access token. This ensures the token cannot be used for future requests.", + "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 access token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/profile": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve authenticated customer's profile information. This endpoint requires a valid access token and returns the customer's own profile data.", + "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 access token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer profile not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/refresh-token": { + "post": { + "description": "Refresh access token using a valid refresh token. This allows customers to maintain their session without re-authentication.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Refresh customer access token", + "parameters": [ + { + "description": "Refresh token for generating new access token", + "name": "refresh", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.RefreshTokenForm" + } + } + ], + "responses": { + "200": { + "description": "Token refreshed successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.AuthResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid refresh token or feature not implemented", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid or expired refresh token", + "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" + } + } + } + } + }, + "/health": { + "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" + } + } + } + } } }, "definitions": { + "customer.Address": { + "type": "object", + "properties": { + "addressType": { + "description": "billing, shipping, etc.", + "type": "string" + }, + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + }, + "postalCode": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + } + } + }, + "customer.AddressForm": { + "type": "object", + "properties": { + "address_type": { + "type": "string" + }, + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "postal_code": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + } + } + }, + "customer.AuthResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "customer": { + "$ref": "#/definitions/customer.CustomerResponse" + }, + "expires_at": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + } + } + }, + "customer.ContactPerson": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "isPrimary": { + "type": "boolean" + }, + "lastName": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + } + } + }, + "customer.ContactPersonForm": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "is_primary": { + "type": "boolean" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + } + } + }, + "customer.CreateCustomerForm": { + "type": "object", + "properties": { + "address": { + "description": "Address information", + "allOf": [ + { + "$ref": "#/definitions/customer.AddressForm" + } + ] + }, + "annual_revenue": { + "type": "number" + }, + "business_type": { + "description": "Business information", + "type": "string" + }, + "company_id": { + "type": "string" + }, + "company_name": { + "description": "Company customer fields", + "type": "string" + }, + "contact_person": { + "description": "Contact person (for company customers)", + "allOf": [ + { + "$ref": "#/definitions/customer.ContactPersonForm" + } + ] + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "type": "integer" + }, + "first_name": { + "description": "Individual customer fields", + "type": "string" + }, + "founded_year": { + "type": "integer" + }, + "full_name": { + "type": "string" + }, + "industry": { + "type": "string" + }, + "language": { + "description": "Preferences and settings", + "type": "string" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "customer.CustomerListResponse": { + "type": "object", + "properties": { + "customers": { + "type": "array", + "items": { + "$ref": "#/definitions/customer.CustomerResponse" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "customer.CustomerResponse": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/customer.Address" + }, + "annual_revenue": { + "type": "number" + }, + "business_type": { + "type": "string" + }, + "company_id": { + "type": "string" + }, + "company_name": { + "type": "string" + }, + "compliance_notes": { + "type": "string" + }, + "contact_person": { + "$ref": "#/definitions/customer.ContactPerson" + }, + "created_at": { + "type": "integer" + }, + "created_by": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "type": "integer" + }, + "first_name": { + "type": "string" + }, + "founded_year": { + "type": "integer" + }, + "full_name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "industry": { + "type": "string" + }, + "is_compliant": { + "type": "boolean" + }, + "is_verified": { + "type": "boolean" + }, + "language": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "integer" + }, + "updated_by": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "customer.LoginForm": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "customer.RefreshTokenForm": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "customer.UpdateCustomerForm": { + "type": "object", + "properties": { + "address": { + "description": "Address information", + "allOf": [ + { + "$ref": "#/definitions/customer.AddressForm" + } + ] + }, + "annual_revenue": { + "type": "number" + }, + "business_type": { + "description": "Business information", + "type": "string" + }, + "company_id": { + "type": "string" + }, + "company_name": { + "description": "Company customer fields", + "type": "string" + }, + "contact_person": { + "description": "Contact person (for company customers)", + "allOf": [ + { + "$ref": "#/definitions/customer.ContactPersonForm" + } + ] + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "type": "integer" + }, + "first_name": { + "description": "Individual customer fields", + "type": "string" + }, + "founded_year": { + "type": "integer" + }, + "full_name": { + "type": "string" + }, + "industry": { + "type": "string" + }, + "language": { + "description": "Preferences and settings", + "type": "string" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "customer.UpdateCustomerStatusForm": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "customer.UpdateCustomerVerificationForm": { + "type": "object", + "properties": { + "compliance_notes": { + "type": "string" + }, + "is_compliant": { + "type": "boolean" + }, + "is_verified": { + "type": "boolean" + } + } + }, "main.HealthResponse": { "type": "object", "properties": { @@ -1490,6 +3224,14 @@ const docTemplate = `{ "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": "Health check operations", "name": "Health" @@ -1501,7 +3243,7 @@ const docTemplate = `{ var SwaggerInfo = &swag.Spec{ Version: "1.0.0", Host: "localhost:8081", - BasePath: "/admin/v1", + BasePath: "", Schemes: []string{}, Title: "Tender Management API", Description: "This is the API documentation for the Tender Management System.", diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index 4d6022d..d4af547 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -16,9 +16,8 @@ "version": "1.0.0" }, "host": "localhost:8081", - "basePath": "/admin/v1", "paths": { - "/change-password": { + "/admin/v1/change-password": { "put": { "security": [ { @@ -75,9 +74,14 @@ } } }, - "/health": { + "/admin/v1/customers": { "get": { - "description": "Get server health status", + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a paginated list of customers with advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination.", "consumes": [ "application/json" ], @@ -85,20 +89,1025 @@ "application/json" ], "tags": [ - "Health" + "Customers-Admin" + ], + "summary": "List customers with filters and pagination", + "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" + } ], - "summary": "Health check", "responses": { "200": { - "description": "OK", + "description": "Customers retrieved successfully", "schema": { - "$ref": "#/definitions/main.HealthResponse" + "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" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "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.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Create a new customer", + "parameters": [ + { + "description": "Customer information including type (individual|company|government), personal details, company info, address, and business details", + "name": "customer", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.CreateCustomerForm" + } + } + ], + "responses": { + "201": { + "description": "Customer created successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Customer with this email/company already exists", + "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" } } } } }, - "/login": { + "/admin/v1/customers/company/{companyId}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve all customers associated with a specific company. This is useful for company administrators to see all users within their organization.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Get customers by company ID", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Company UUID", + "name": "companyId", + "in": "path", + "required": true + }, + { + "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" + } + ], + "responses": { + "200": { + "description": "Customers retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/customer.CustomerResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid company ID format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/status/{status}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve customers filtered by their account status (active, inactive, suspended, or pending). Useful for administrative monitoring and management.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Get customers by status", + "parameters": [ + { + "enum": [ + "active", + "inactive", + "suspended", + "pending" + ], + "type": "string", + "description": "Customer status", + "name": "status", + "in": "path", + "required": true + }, + { + "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" + } + ], + "responses": { + "200": { + "description": "Customers retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/customer.CustomerResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid customer status", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/type/{type}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve customers filtered by their type (individual, company, or government). Useful for administrative reporting and management.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Get customers by type", + "parameters": [ + { + "enum": [ + "individual", + "company", + "government" + ], + "type": "string", + "description": "Customer type", + "name": "type", + "in": "path", + "required": true + }, + { + "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" + } + ], + "responses": { + "200": { + "description": "Customers retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/customer.CustomerResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid customer type", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve detailed customer information by unique customer ID. Returns comprehensive customer data including personal details, company information, address, verification status, and audit fields.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Get customer by ID", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid customer ID format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update existing customer information with comprehensive details. All fields are optional and only provided fields will be updated. This endpoint is used by the web panel for full customer management.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Update customer information", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Customer update information - all fields are optional", + "name": "customer", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.UpdateCustomerForm" + } + } + ], + "responses": { + "200": { + "description": "Customer updated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.CustomerResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid customer ID or input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Customer with this email/company already exists", + "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" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Soft delete a customer by setting status to inactive. The customer record is preserved but marked as deleted. This is a reversible operation.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Delete customer", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer deleted successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID format", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/activate": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Reactivate a suspended customer account by setting their status back to active. This reverses the suspension action.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Activate suspended customer account", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer activated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/status": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the status of a customer account. Valid statuses include active, inactive, suspended, and pending. This affects the customer's ability to access the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Update customer status", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Status update information", + "name": "status", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.UpdateCustomerStatusForm" + } + } + ], + "responses": { + "200": { + "description": "Customer status updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID or status", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid status value", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/suspend": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Suspend a customer account by setting their status to suspended. Suspended customers cannot access the system. A reason for suspension must be provided.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Suspend customer account", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Suspension reason", + "name": "reason", + "in": "body", + "required": true, + "schema": { + "type": "object" + } + } + ], + "responses": { + "200": { + "description": "Customer suspended successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID or missing reason", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/verification": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the verification and compliance status of a customer. This includes marking customers as verified and compliant, with optional compliance notes for audit purposes.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Update customer verification and compliance status", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Verification and compliance update information", + "name": "verification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.UpdateCustomerVerificationForm" + } + } + ], + "responses": { + "200": { + "description": "Customer verification updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid verification data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/customers/{id}/verify": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a customer as verified. This is a simple verification action that sets the customer's verification status to true.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Admin" + ], + "summary": "Verify customer", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "Customer UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer verified successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid customer ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer does not exist", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/login": { "post": { "description": "Authenticate user with username and password", "consumes": [ @@ -162,7 +1171,7 @@ } } }, - "/logout": { + "/admin/v1/logout": { "delete": { "security": [ { @@ -202,7 +1211,7 @@ } } }, - "/profile": { + "/admin/v1/profile": { "get": { "security": [ { @@ -327,7 +1336,7 @@ } } }, - "/refresh-token": { + "/admin/v1/refresh-token": { "post": { "description": "Refresh access token using refresh token", "consumes": [ @@ -391,7 +1400,7 @@ } } }, - "/reset-password": { + "/admin/v1/reset-password": { "post": { "description": "Send password reset email to user", "consumes": [ @@ -437,7 +1446,7 @@ } } }, - "/users": { + "/admin/v1/users": { "get": { "security": [ { @@ -624,7 +1633,7 @@ } } }, - "/users/company/{company_id}": { + "/admin/v1/users/company/{company_id}": { "get": { "security": [ { @@ -710,7 +1719,7 @@ } } }, - "/users/role/{role}": { + "/admin/v1/users/role/{role}": { "get": { "security": [ { @@ -796,7 +1805,7 @@ } } }, - "/users/{id}": { + "/admin/v1/users/{id}": { "get": { "security": [ { @@ -1026,7 +2035,7 @@ } } }, - "/users/{id}/role": { + "/admin/v1/users/{id}/role": { "put": { "security": [ { @@ -1102,7 +2111,7 @@ } } }, - "/users/{id}/status": { + "/admin/v1/users/{id}/status": { "put": { "security": [ { @@ -1177,9 +2186,733 @@ } } } + }, + "/api/v1/login": { + "post": { + "description": "Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Customer login", + "parameters": [ + { + "description": "Login credentials (username/email and password)", + "name": "login", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.LoginForm" + } + } + ], + "responses": { + "200": { + "description": "Login successful", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.AuthResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid credentials or inactive account", + "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" + } + } + } + } + }, + "/api/v1/logout": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Logout customer and invalidate access token. This ensures the token cannot be used for future requests.", + "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 access token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/profile": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve authenticated customer's profile information. This endpoint requires a valid access token and returns the customer's own profile data.", + "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 access token", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Customer profile not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/api/v1/refresh-token": { + "post": { + "description": "Refresh access token using a valid refresh token. This allows customers to maintain their session without re-authentication.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Customers-Authorization" + ], + "summary": "Refresh customer access token", + "parameters": [ + { + "description": "Refresh token for generating new access token", + "name": "refresh", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/customer.RefreshTokenForm" + } + } + ], + "responses": { + "200": { + "description": "Token refreshed successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/customer.AuthResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid refresh token or feature not implemented", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "401": { + "description": "Unauthorized - Invalid or expired refresh token", + "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" + } + } + } + } + }, + "/health": { + "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" + } + } + } + } } }, "definitions": { + "customer.Address": { + "type": "object", + "properties": { + "addressType": { + "description": "billing, shipping, etc.", + "type": "string" + }, + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "isDefault": { + "type": "boolean" + }, + "postalCode": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + } + } + }, + "customer.AddressForm": { + "type": "object", + "properties": { + "address_type": { + "type": "string" + }, + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "postal_code": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + } + } + }, + "customer.AuthResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "customer": { + "$ref": "#/definitions/customer.CustomerResponse" + }, + "expires_at": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + } + } + }, + "customer.ContactPerson": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "isPrimary": { + "type": "boolean" + }, + "lastName": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + } + } + }, + "customer.ContactPersonForm": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "is_primary": { + "type": "boolean" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "position": { + "type": "string" + } + } + }, + "customer.CreateCustomerForm": { + "type": "object", + "properties": { + "address": { + "description": "Address information", + "allOf": [ + { + "$ref": "#/definitions/customer.AddressForm" + } + ] + }, + "annual_revenue": { + "type": "number" + }, + "business_type": { + "description": "Business information", + "type": "string" + }, + "company_id": { + "type": "string" + }, + "company_name": { + "description": "Company customer fields", + "type": "string" + }, + "contact_person": { + "description": "Contact person (for company customers)", + "allOf": [ + { + "$ref": "#/definitions/customer.ContactPersonForm" + } + ] + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "type": "integer" + }, + "first_name": { + "description": "Individual customer fields", + "type": "string" + }, + "founded_year": { + "type": "integer" + }, + "full_name": { + "type": "string" + }, + "industry": { + "type": "string" + }, + "language": { + "description": "Preferences and settings", + "type": "string" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "customer.CustomerListResponse": { + "type": "object", + "properties": { + "customers": { + "type": "array", + "items": { + "$ref": "#/definitions/customer.CustomerResponse" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "customer.CustomerResponse": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/customer.Address" + }, + "annual_revenue": { + "type": "number" + }, + "business_type": { + "type": "string" + }, + "company_id": { + "type": "string" + }, + "company_name": { + "type": "string" + }, + "compliance_notes": { + "type": "string" + }, + "contact_person": { + "$ref": "#/definitions/customer.ContactPerson" + }, + "created_at": { + "type": "integer" + }, + "created_by": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "type": "integer" + }, + "first_name": { + "type": "string" + }, + "founded_year": { + "type": "integer" + }, + "full_name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "industry": { + "type": "string" + }, + "is_compliant": { + "type": "boolean" + }, + "is_verified": { + "type": "boolean" + }, + "language": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "integer" + }, + "updated_by": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "customer.LoginForm": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "customer.RefreshTokenForm": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "customer.UpdateCustomerForm": { + "type": "object", + "properties": { + "address": { + "description": "Address information", + "allOf": [ + { + "$ref": "#/definitions/customer.AddressForm" + } + ] + }, + "annual_revenue": { + "type": "number" + }, + "business_type": { + "description": "Business information", + "type": "string" + }, + "company_id": { + "type": "string" + }, + "company_name": { + "description": "Company customer fields", + "type": "string" + }, + "contact_person": { + "description": "Contact person (for company customers)", + "allOf": [ + { + "$ref": "#/definitions/customer.ContactPersonForm" + } + ] + }, + "currency": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "type": "integer" + }, + "first_name": { + "description": "Individual customer fields", + "type": "string" + }, + "founded_year": { + "type": "integer" + }, + "full_name": { + "type": "string" + }, + "industry": { + "type": "string" + }, + "language": { + "description": "Preferences and settings", + "type": "string" + }, + "last_name": { + "type": "string" + }, + "mobile": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "customer.UpdateCustomerStatusForm": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "customer.UpdateCustomerVerificationForm": { + "type": "object", + "properties": { + "compliance_notes": { + "type": "string" + }, + "is_compliant": { + "type": "boolean" + }, + "is_verified": { + "type": "boolean" + } + } + }, "main.HealthResponse": { "type": "object", "properties": { @@ -1484,6 +3217,14 @@ "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": "Health check operations", "name": "Health" diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 74261b3..183b5a3 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -1,5 +1,306 @@ -basePath: /admin/v1 definitions: + customer.Address: + properties: + addressType: + description: billing, shipping, etc. + type: string + city: + type: string + country: + type: string + isDefault: + type: boolean + postalCode: + type: string + state: + type: string + street: + type: string + type: object + customer.AddressForm: + properties: + address_type: + type: string + city: + type: string + country: + type: string + is_default: + type: boolean + postal_code: + type: string + state: + type: string + street: + type: string + type: object + customer.AuthResponse: + properties: + access_token: + type: string + customer: + $ref: '#/definitions/customer.CustomerResponse' + expires_at: + type: integer + refresh_token: + type: string + type: object + customer.ContactPerson: + properties: + email: + type: string + firstName: + type: string + fullName: + type: string + isPrimary: + type: boolean + lastName: + type: string + mobile: + type: string + phone: + type: string + position: + type: string + type: object + customer.ContactPersonForm: + properties: + email: + type: string + first_name: + type: string + full_name: + type: string + is_primary: + type: boolean + last_name: + type: string + mobile: + type: string + phone: + type: string + position: + type: string + type: object + customer.CreateCustomerForm: + properties: + address: + allOf: + - $ref: '#/definitions/customer.AddressForm' + description: Address information + annual_revenue: + type: number + business_type: + description: Business information + type: string + company_id: + type: string + company_name: + description: Company customer fields + type: string + contact_person: + allOf: + - $ref: '#/definitions/customer.ContactPersonForm' + description: Contact person (for company customers) + currency: + type: string + email: + type: string + employee_count: + type: integer + first_name: + description: Individual customer fields + type: string + founded_year: + type: integer + full_name: + type: string + industry: + type: string + language: + description: Preferences and settings + type: string + last_name: + type: string + mobile: + type: string + password: + type: string + phone: + type: string + registration_number: + type: string + tax_id: + type: string + timezone: + type: string + type: + type: string + username: + type: string + type: object + customer.CustomerListResponse: + properties: + customers: + items: + $ref: '#/definitions/customer.CustomerResponse' + type: array + limit: + type: integer + offset: + type: integer + total: + type: integer + total_pages: + type: integer + type: object + customer.CustomerResponse: + properties: + address: + $ref: '#/definitions/customer.Address' + annual_revenue: + type: number + business_type: + type: string + company_id: + type: string + company_name: + type: string + compliance_notes: + type: string + contact_person: + $ref: '#/definitions/customer.ContactPerson' + created_at: + type: integer + created_by: + type: string + currency: + type: string + email: + type: string + employee_count: + type: integer + first_name: + type: string + founded_year: + type: integer + full_name: + type: string + id: + type: string + industry: + type: string + is_compliant: + type: boolean + is_verified: + type: boolean + language: + type: string + last_name: + type: string + mobile: + type: string + phone: + type: string + registration_number: + type: string + status: + type: string + tax_id: + type: string + timezone: + type: string + type: + type: string + updated_at: + type: integer + updated_by: + type: string + username: + type: string + type: object + customer.LoginForm: + properties: + password: + type: string + username: + type: string + type: object + customer.RefreshTokenForm: + properties: + refresh_token: + type: string + type: object + customer.UpdateCustomerForm: + properties: + address: + allOf: + - $ref: '#/definitions/customer.AddressForm' + description: Address information + annual_revenue: + type: number + business_type: + description: Business information + type: string + company_id: + type: string + company_name: + description: Company customer fields + type: string + contact_person: + allOf: + - $ref: '#/definitions/customer.ContactPersonForm' + description: Contact person (for company customers) + currency: + type: string + email: + type: string + employee_count: + type: integer + first_name: + description: Individual customer fields + type: string + founded_year: + type: integer + full_name: + type: string + industry: + type: string + language: + description: Preferences and settings + type: string + last_name: + type: string + mobile: + type: string + phone: + type: string + registration_number: + type: string + tax_id: + type: string + timezone: + type: string + type: + type: string + username: + type: string + type: object + customer.UpdateCustomerStatusForm: + properties: + status: + type: string + type: object + customer.UpdateCustomerVerificationForm: + properties: + compliance_notes: + type: string + is_compliant: + type: boolean + is_verified: + type: boolean + type: object main.HealthResponse: properties: status: @@ -200,7 +501,7 @@ info: title: Tender Management API version: 1.0.0 paths: - /change-password: + /admin/v1/change-password: put: consumes: - application/json @@ -236,22 +537,690 @@ paths: summary: Change password tags: - Users - /health: + /admin/v1/customers: get: consumes: - application/json - description: Get server health status + description: Retrieve a paginated list of customers with advanced filtering + options including search, type, status, company, industry, verification status, + and compliance status. Supports sorting and pagination. + parameters: + - 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 retrieved successfully schema: - $ref: '#/definitions/main.HealthResponse' - summary: Health check + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/customer.CustomerListResponse' + type: object + "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' + security: + - BearerAuth: [] + summary: List customers with filters and pagination tags: - - Health - /login: + - Customers-Admin + post: + consumes: + - application/json + 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. + parameters: + - description: Customer information including type (individual|company|government), + personal details, company info, address, and business details + in: body + name: customer + required: true + schema: + $ref: '#/definitions/customer.CreateCustomerForm' + produces: + - application/json + responses: + "201": + description: Customer created successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/customer.CustomerResponse' + type: object + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict - Customer with this email/company already exists + 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: Create a new customer + tags: + - Customers-Admin + /admin/v1/customers/{id}: + delete: + consumes: + - application/json + description: Soft delete a customer by setting status to inactive. The customer + record is preserved but marked as deleted. This is a reversible operation. + parameters: + - description: Customer UUID + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Customer deleted successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid customer ID format + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer does not exist + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Delete customer + tags: + - Customers-Admin + get: + consumes: + - application/json + description: Retrieve detailed customer information by unique customer ID. Returns + comprehensive customer data including personal details, company information, + address, verification status, and audit fields. + parameters: + - description: Customer UUID + format: uuid + 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 format + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer does not exist + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get customer by ID + tags: + - Customers-Admin + put: + consumes: + - application/json + description: Update existing customer information with comprehensive details. + All fields are optional and only provided fields will be updated. This endpoint + is used by the web panel for full customer management. + parameters: + - description: Customer UUID + format: uuid + in: path + name: id + required: true + type: string + - description: Customer update information - all fields are optional + in: body + name: customer + required: true + schema: + $ref: '#/definitions/customer.UpdateCustomerForm' + produces: + - application/json + responses: + "200": + description: Customer updated successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/customer.CustomerResponse' + type: object + "400": + description: Bad request - Invalid customer ID or input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer does not exist + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict - Customer with this email/company already exists + 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: Update customer information + tags: + - Customers-Admin + /admin/v1/customers/{id}/activate: + post: + consumes: + - application/json + description: Reactivate a suspended customer account by setting their status + back to active. This reverses the suspension action. + parameters: + - description: Customer UUID + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Customer activated successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid customer ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer does not exist + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Activate suspended customer account + tags: + - Customers-Admin + /admin/v1/customers/{id}/status: + patch: + consumes: + - application/json + description: Update the status of a customer account. Valid statuses include + active, inactive, suspended, and pending. This affects the customer's ability + to access the system. + parameters: + - description: Customer UUID + format: uuid + in: path + name: id + required: true + type: string + - description: Status update information + in: body + name: status + required: true + schema: + $ref: '#/definitions/customer.UpdateCustomerStatusForm' + produces: + - application/json + responses: + "200": + description: Customer status updated successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid customer ID or status + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer does not exist + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid status value + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Update customer status + tags: + - Customers-Admin + /admin/v1/customers/{id}/suspend: + post: + consumes: + - application/json + description: Suspend a customer account by setting their status to suspended. + Suspended customers cannot access the system. A reason for suspension must + be provided. + parameters: + - description: Customer UUID + format: uuid + in: path + name: id + required: true + type: string + - description: Suspension reason + in: body + name: reason + required: true + schema: + type: object + produces: + - application/json + responses: + "200": + description: Customer suspended successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid customer ID or missing reason + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer does not exist + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Suspend customer account + tags: + - Customers-Admin + /admin/v1/customers/{id}/verification: + patch: + consumes: + - application/json + description: Update the verification and compliance status of a customer. This + includes marking customers as verified and compliant, with optional compliance + notes for audit purposes. + parameters: + - description: Customer UUID + format: uuid + in: path + name: id + required: true + type: string + - description: Verification and compliance update information + in: body + name: verification + required: true + schema: + $ref: '#/definitions/customer.UpdateCustomerVerificationForm' + produces: + - application/json + responses: + "200": + description: Customer verification updated successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid customer ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer does not exist + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid verification data + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Update customer verification and compliance status + tags: + - Customers-Admin + /admin/v1/customers/{id}/verify: + post: + consumes: + - application/json + description: Mark a customer as verified. This is a simple verification action + that sets the customer's verification status to true. + parameters: + - description: Customer UUID + format: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Customer verified successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid customer ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer does not exist + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Verify customer + tags: + - Customers-Admin + /admin/v1/customers/company/{companyId}: + get: + consumes: + - application/json + description: Retrieve all customers associated with a specific company. This + is useful for company administrators to see all users within their organization. + parameters: + - description: Company UUID + format: uuid + in: path + name: companyId + required: true + 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 + produces: + - application/json + responses: + "200": + description: Customers retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + items: + $ref: '#/definitions/customer.CustomerResponse' + type: array + meta: + $ref: '#/definitions/response.Meta' + type: object + "400": + description: Bad request - Invalid company ID format + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get customers by company ID + tags: + - Customers-Admin + /admin/v1/customers/status/{status}: + get: + consumes: + - application/json + description: Retrieve customers filtered by their account status (active, inactive, + suspended, or pending). Useful for administrative monitoring and management. + parameters: + - description: Customer status + enum: + - active + - inactive + - suspended + - pending + in: path + name: status + required: true + 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 + produces: + - application/json + responses: + "200": + description: Customers retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + items: + $ref: '#/definitions/customer.CustomerResponse' + type: array + meta: + $ref: '#/definitions/response.Meta' + type: object + "400": + description: Bad request - Invalid customer status + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get customers by status + tags: + - Customers-Admin + /admin/v1/customers/type/{type}: + get: + consumes: + - application/json + description: Retrieve customers filtered by their type (individual, company, + or government). Useful for administrative reporting and management. + parameters: + - description: Customer type + enum: + - individual + - company + - government + in: path + name: type + required: true + 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 + produces: + - application/json + responses: + "200": + description: Customers retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + items: + $ref: '#/definitions/customer.CustomerResponse' + type: array + meta: + $ref: '#/definitions/response.Meta' + type: object + "400": + description: Bad request - Invalid customer type + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get customers by type + tags: + - Customers-Admin + /admin/v1/login: post: consumes: - application/json @@ -290,7 +1259,7 @@ paths: summary: Login user tags: - Authorization - /logout: + /admin/v1/logout: delete: consumes: - application/json @@ -315,7 +1284,7 @@ paths: summary: Logout user tags: - Users - /profile: + /admin/v1/profile: get: consumes: - application/json @@ -389,7 +1358,7 @@ paths: summary: Update user profile tags: - Users - /refresh-token: + /admin/v1/refresh-token: post: consumes: - application/json @@ -428,7 +1397,7 @@ paths: summary: Refresh access token tags: - Authorization - /reset-password: + /admin/v1/reset-password: post: consumes: - application/json @@ -458,7 +1427,7 @@ paths: summary: Reset password tags: - Authorization - /users: + /admin/v1/users: get: consumes: - application/json @@ -573,7 +1542,7 @@ paths: summary: Create user tags: - Users - /users/{id}: + /admin/v1/users/{id}: delete: consumes: - application/json @@ -716,7 +1685,7 @@ paths: summary: Update user tags: - Users - /users/{id}/role: + /admin/v1/users/{id}/role: put: consumes: - application/json @@ -765,7 +1734,7 @@ paths: summary: Update user role tags: - Users - /users/{id}/status: + /admin/v1/users/{id}/status: put: consumes: - application/json @@ -814,7 +1783,7 @@ paths: summary: Update user status tags: - Users - /users/company/{company_id}: + /admin/v1/users/company/{company_id}: get: consumes: - application/json @@ -867,7 +1836,7 @@ paths: summary: Get users by company ID tags: - Users - /users/role/{role}: + /admin/v1/users/role/{role}: get: consumes: - application/json @@ -920,6 +1889,170 @@ paths: summary: Get users by role tags: - Users + /api/v1/login: + post: + consumes: + - application/json + description: Authenticate customer with username (email) and password. Returns + access token, refresh token, and customer information upon successful authentication. + parameters: + - description: Login credentials (username/email and password) + in: body + name: login + required: true + schema: + $ref: '#/definitions/customer.LoginForm' + produces: + - application/json + responses: + "200": + description: Login successful + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/customer.AuthResponse' + type: object + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized - Invalid credentials or inactive account + 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' + summary: Customer login + tags: + - Customers-Authorization + /api/v1/logout: + delete: + consumes: + - application/json + description: Logout customer and invalidate access token. This ensures the token + cannot be used for future requests. + produces: + - application/json + responses: + "200": + description: Logout successful + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized - Invalid or missing access 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 authenticated customer's profile information. This endpoint + requires a valid access token and returns the customer's own profile data. + 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 access token + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Customer profile 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: + post: + consumes: + - application/json + description: Refresh access token using a valid refresh token. This allows customers + to maintain their session without re-authentication. + parameters: + - description: Refresh token for generating new access token + in: body + name: refresh + required: true + schema: + $ref: '#/definitions/customer.RefreshTokenForm' + produces: + - application/json + responses: + "200": + description: Token refreshed successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/customer.AuthResponse' + type: object + "400": + description: Bad request - Invalid refresh token or feature not implemented + schema: + $ref: '#/definitions/response.APIResponse' + "401": + description: Unauthorized - Invalid or expired refresh token + 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' + summary: Refresh customer access token + tags: + - Customers-Authorization + /health: + 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 securityDefinitions: BearerAuth: description: Type "Bearer" followed by a space and JWT token. @@ -933,5 +2066,11 @@ tags: 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: Health check operations name: Health diff --git a/cmd/web/health.go b/cmd/web/health.go index 73d8332..0ced929 100644 --- a/cmd/web/health.go +++ b/cmd/web/health.go @@ -13,7 +13,7 @@ import ( // @Accept json // @Produce json // @Success 200 {object} HealthResponse -// @Router /health [get] +// @Router /admin/v1/health [get] func healthHandler(c echo.Context) error { return c.JSON(http.StatusOK, HealthResponse{ Status: "healthy", diff --git a/cmd/web/main.go b/cmd/web/main.go index 9532887..8467dae 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -13,7 +13,6 @@ package main // @license.url http://www.apache.org/licenses/LICENSE-2.0.html // @host localhost:8081 -// @BasePath /admin/v1 // @securityDefinitions.apikey BearerAuth // @in header @@ -26,6 +25,12 @@ package main // @tag.name Authorization // @tag.description Authentication operations including login, logout, and token management +// @tag.name Customers-Admin +// @tag.description Customer management operations including authentication, profile management, and admin operations + +// @tag.name Customers-Authorization +// @tag.description Customer authentication operations including login, logout, and token management + // @tag.name Health // @tag.description Health check operations @@ -34,6 +39,7 @@ import ( "fmt" "net/http" "time" + "tm/internal/customer" "tm/internal/user" _ "tm/cmd/web/docs" // This is generated by swag @@ -68,10 +74,12 @@ func main() { }() // Initialize authorization service - authService := initAuthorizationService(conf.Auth.JWT, redisClient, logger) + userAuthService := initAuthorizationService(conf.UserAuthorization.JWT, redisClient, logger) + customerAuthService := initAuthorizationService(conf.CustomerAuthorization.JWT, redisClient, logger) // Initialize repositories with MongoDB connection manager userRepository := user.NewUserRepository(mongoManager, logger) + customerRepository := customer.NewCustomerRepository(mongoManager, logger) logger.Info("Repositories initialized successfully", map[string]interface{}{ "repositories": []string{"customer", "user"}, @@ -81,14 +89,16 @@ func main() { userValidator := user.NewValidationService() // Initialize services with repositories - userService := user.NewUserService(userRepository, logger, authService, userValidator) + userService := user.NewUserService(userRepository, logger, userAuthService, userValidator) + customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService) logger.Info("Services initialized successfully", map[string]interface{}{ "services": []string{"customer", "user"}, }) // Initialize handlers with services - userHandler := user.NewUserHandler(userService, logger, userValidator, authService) + userHandler := user.NewUserHandler(userService, logger, userValidator, userAuthService) + customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger) logger.Info("Handlers initialized successfully", map[string]interface{}{ "handlers": []string{"customer", "user"}, @@ -99,6 +109,7 @@ func main() { // Register routes userHandler.RegisterRoutes(e) + customerHandler.RegisterRoutes(e) // Start server serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port) diff --git a/docs/AEC_TenderManagement.docx b/docs/AEC_TenderManagement.docx new file mode 100644 index 0000000..c7eee3e Binary files /dev/null and b/docs/AEC_TenderManagement.docx differ diff --git a/docs/AEC_TenderManagement.md b/docs/AEC_TenderManagement.md deleted file mode 100644 index 7ae9745..0000000 --- a/docs/AEC_TenderManagement.md +++ /dev/null @@ -1,746 +0,0 @@ -# - -# - -# - -# Development Specification - -# Tender Management - -# - -## **For AEC** - -## ‫‪‬‬ - -# **Document Metadata** {#document-metadata} - -| Field | Information | -| ----- | ----- | -| **Document Name** | Tender Management Development Specification | -| **Version** | 1.0 | -| **Created By** | Niki Sagharidooz | -| **Creation Date** | Jun 09, 2025 | -| **Last Updated** | Jun 28, 2025 | -| **Status** | Draft | - -# - -# Table of Content - -[**Document Metadata 2**](#document-metadata) - -[**Project Overview 5**](#project-overview) - -[Purpose 5](#purpose) - -[Problem Statement: Why This System Is Needed 6](#problem-statement:-why-this-system-is-needed) - -[Scope 8](#scope) - -[Technology Stack 11](#technology-stack) - -[Step-by-Step Workflow 12](#step-by-step-workflow) - -[Technical Implementation 16](#technical-implementation) - -## - -# **Project Overview** {#project-overview} - -## **Purpose** {#purpose} - -The purpose of this project is to develop a smart system that: - -* Uses AI and machine learning to identify tenders that are relevant to a company’s products, services, or interests. -* Automatically extracts and interprets tender deadlines and required documents. -* Sends real-time notifications to customers based on their favorite topics. -* Downloads all tender-related documents automatically, including from websites requiring login, reducing the need for manual monitoring and data entry. - -## - -## **Problem Statement: Why This System Is Needed** {#problem-statement:-why-this-system-is-needed} - -Organizations often face major inefficiencies and missed opportunities when trying to stay updated with relevant tenders. These challenges include: - -### **1\. Manual Tender Discovery** - -* Companies must monitor various tender websites manually—each with different formats, layouts, and login requirements. -* Most tender platforms are designed for desktop, making them inconvenient for mobile-first customers. -* This results in delayed access and increased risk of missing relevant tenders. - -### **2\. Language Barriers** - -* Tenders are often published in local or foreign languages, requiring manual translation for teams to understand the content. -* This slows down decision-making and increases dependency on translators. - -### **3\. Unstructured and Scattered Documents** - -* Tender documents are scattered across platforms and hidden behind logins. -* Required documents may include PDFs, scanned images, Excel sheets, and multi-part attachments. -* Manual download and sorting wastes time and increases the risk of incomplete or incorrect submissions. - -### **4\. Irrelevant Notifications** - -* Existing systems typically send bulk notifications, regardless of a company’s business focus. -* Without AI to understand company profiles or documents, users receive low-value and unrelated tenders, causing disengagement. - -### **5\. Missed Deadlines** - -* Deadlines are often hidden deep in documents, not always in structured form. -* Manual review means companies often respond too late. - -### **6\. Missing Legal Documents or Partnership Requirements** - -* Some tenders require specific legal registrations or partner documentation that the company may lack. -* Without guidance or support, companies are **disqualified** or discouraged from applying. - -## - -## **Scope** {#scope} - -### **Tender Discovery & Matching** - -* Automatically gather and classify tenders from various portals using scraping and APIs. -* Use NLP to categorize tenders by industry, keywords, and relevance. -* Enable users to filter and search based on personalized criteria. - -### **Document Management & Automation** - -* Automate downloading of tender documents from multiple portals, even behind logins. -* Securely store and organize documents, avoiding duplicates using checksums. -* Support uploads via dashboard or email for company files like catalogs or contracts. - -### **Company & Business Knowledge Integration** - -* Extract structured insights from business documents using AI and NLP. -* Understand products, services, and offerings to inform tender matching. -* Continuously enrich the system’s contextual knowledge of each company. - -### **Tender Recommendation Engine** - -* Match tenders with company offerings and user interests using advanced AI models. -* Use semantic similarity, document analysis, and saved preferences to score relevance. -* Provide tailored recommendations and allow user feedback for improved results. - -### **Notification system** - -* Send email and mobile alerts for new and relevant tenders. -* Support customizable frequency settings (real-time, daily, weekly). -* Highlight high-priority tenders and deadline reminders automatically. - -### **Mobile and desktop application** - -* Offer a responsive web and mobile app experience for viewing tenders and alerts. -* Allow users to review, filter, and act on tenders anytime, anywhere. -* Ensure mobile-first UI for busy professionals on the go. - -### **Legal & Compliance Assistance** - -* Identify legal requirements and missing documents for specific tenders. -* Recommend document preparation or suggest potential partners for compliance. -* Support multi-party collaboration for joint tender submissions. - -### **User & Company Profiles** - -* Enable users to save interests, keywords, and preferred industries. -* Store company-specific information to personalize tender discovery. -* Adapt AI recommendations based on evolving preferences and behavior. - -### **Dashboard & Administration** - -* Provide an intuitive dashboard for tender matches, documents, and timelines. -* Include admin tools for managing users, scrapers, and system performance. -* Offer insights and analytics on matching accuracy and user activity. - -### **System Integration & APIs** - -* Expose APIs for integration with external CRMs, ERPs, and tender management tools. -* Use token-based authentication and modular services for extensibility. -* Prepare the system for white-labeling or SaaS partnerships. - -### **Reliability, Monitoring & Deployment** - -* Ensure stable operation with automated testing, error monitoring, and alerts. -* Deploy with Docker on secure cloud infrastructure. -* Log system behavior and provide real-time status for key components. - -### **Continuous Improvement & Scaling** - -* Retrain AI models with new data to increase accuracy over time. -* Update scrapers to adapt to changes in tender portal structures. -* Evolve features based on feedback, business needs, and user behavior. - -## - -## **Technology Stack** {#technology-stack} - -| Component | Technology Choices | -| :---- | :---- | -| **Frontend** | React Native (admin) | -| **Mobile** | Flutter | -| **Backend** | GoLang (Echo) | -| **AI & NLP** | ? | -| **Database** | MongoDB / Redis / RabbitMQ / Elasticsearch | - -## **Step-by-Step Workflow** {#step-by-step-workflow} - -### **1\. Tender Source Monitoring** - -**Objective:** Identify potential tender opportunities in real-time. - -* Maintain a list of trusted tender sources (URLs, portals, APIs like OPIC) in the database. -* Schedule regular checks for each source (via scraping or API). -* Detect and fetch new tenders based on publication date and update frequency. - -### **2\. Tender Discovery & Data Collection** - -**Objective:** Extract structured data from multiple tender platforms. - -* Automatically navigate tender websites or APIs to collect metadata: - * Title, deadline, description, type (RFI, RFP, Tender…), category codes, region, and budget. -* For documents and deep content: - * Use AI-assisted login handling. - * Retrieve attachments (PDFs, Word, Excel) such as tender documents, terms of reference, eligibility criteria. - * Automatically translate non-English documents into English using AI-based translation tools (e.g., DeepL API, open-source models). -* Store raw and normalized tender data in the local database (MongoDB). - -### **3\. Tender Data Normalization & Categorization** - -**Objective:** Make tenders searchable, sortable, and ready for AI matching. - -* Clean and preprocess texts (tokenization, language detection, stemming). -* Use NLP/ML models to: - * Classify tenders by industry, sector, or CPV codes. - * Extract keywords and technical/legal requirement phrases. - * Assign tags for internal indexing and filtering. - -### **4\. Company Onboarding & Knowledge Extraction** - -**Objective:** Understand the company’s business, products, and capabilities. - -* Allow companies to upload documents: - * Product catalogs, brochures, project history, certificates, etc. -* Accept uploads via dashboard or email parser. -* Apply NLP to extract and structure key information: - * Keywords, product types, service areas, past experience, legal readiness. - -### **5\. Interest Modeling (Company Preferences)** - -**Objective:** Understand and learn company-specific tender interests. - -* Build a dynamic interest profile using: - * Onboarding data - * Interactions (liked/disliked tenders) - * Previously applied tenders -* Use vector similarity models (e.g., SBERT, embedding-based matching) to align tender features with company profiles. - -### **6\. AI-Powered Tender Matching & Feedback Loop** - -**Objective:** Suggest best-fit tenders and refine recommendations. - -* Propose tenders on web and mobile interface -* Allow users to like/dislike tenders (swipe or button). -* Log actions and train ML models continuously to refine relevance. -* Use feedback to: - * Improve the similarity score model - * Adjust tagging/weighting for interests - -### **7\. Tender Detail View & Document Requirement Analysis** - -**Objective:** Prepare for successful application. - -* Each tender has a detail page showing: - * Full description, documents, deadline, eligibility -* Analyze attached files (requirements, checklists) using AI. -* Compare against company documents to identify what’s missing: - * Legal documents (e.g., licenses, certificates) - * Technical docs (e.g., specs, experience, financials) -* Show completion progress bar and give upload recommendations. - -### **8\. Document Finalization & Submission Preparation** - -**Objective:** Automate preparation of a complete tender submission package. - -* Once all required documents are available: - * Convert to required formats - * Merge, compress, and sign if needed - Check for compliance or special formatting rules -* Use submission method: - * Self-Apply: Package is downloaded or submitted via tender portal. - * Partnership-Apply: System suggests or connects with partners to complete missing parts (e.g., financial eligibility). - -### **9\. Post-Submission Tracking & Follow-Up (Optional Future Phase)** - -**Objective:** Extend lifecycle tracking for transparency and engagement. - -* Allow companies to track status (submitted, under review, won/lost). -* Gather outcome data to train models on winning patterns. -* Enable reminders for re-submission, future similar tenders. - -## **Technical Implementation** {#technical-implementation} - -### **1\. Tender Source Monitoring** - -Automatically monitor and extract tender metadata and documents from diverse sources (static sites, dynamic pages, APIs), normalize it, and store it in the internal database for further AI processing. - -Goal: Keep track of trusted tender sources and detect the availability of new tenders in near real-time. - -#### **Implementation Steps:** - -##### **1\. Tender Source Configuration Table** - -Maintain a registry of sources to monitor. - -* **Sources (for example:)** - * [**https://app.mercell.com/**](https://app.mercell.com/) - * [**https://ted.europa.eu/**](https://ted.europa.eu/) -* **Schema fields:** - * id, name, type (API, HTML, JS, PDF) - * base\_url - * auth\_required (bool) - * credentials\_id (link to secure store) - * category\_tags, region, language - * check\_interval - * Last\_checked\_at - * status - - ##### **2\. Job Scheduler (Dynamic Source Polling)** - -* Use approaches for scheduling and tracking. -* Each source is polled based on: - * sync\_frequency (e.g., hourly, daily) - * last\_checked\_at timestamp - * Historical success/failure log -* Jobs are batched and distributed to workers based on source type. - - ##### **4\. Logging, Monitoring & Recovery** - -* Use ELK Stack or Prometheus \+ Grafana to monitor: - * Job failures - * Source availability - * Scraping errors -* Retry logic: - * Exponential backoff on failures - * Email alerts on consecutive errors per source - -### - -### **2\. Tender Discovery & Data Collection** - -Automatically discover tenders from multiple trusted sources (public APIs, web portals, data feeds), collect and structure their metadata, extract/download documents, and store them in a standardized format for downstream AI processing. - -Goal: Deep-dive into flagged sources, extract structured tender metadata and documents. - -#### **Implementation Steps:** - -##### **1\. Scraper Engines** - -* For each source, use appropriate adapter: - * **API-based**: REST/SOAP client with pagination and filtering - * **HTML-based**: Headless browser automation (e.g., Playwright) -* Authenticate if required (Basic Auth, OAuth, or Form login via AI-based login engine) - - ##### **2\. Metadata & Document Extraction** - -* Parse listings to extract structured tender metadata: - -| Field | Example | -| :---- | :---- | -| title | Billing system | -| Summary | **Risks & contract compliance** Service Level Agreements (SLAs): Support and operational support should be available on weekdays, excluding holidays, where the lowest level should be 6 hours between 7 am and 5 pm Swedish time. Fines and fees: A penalty is payable for delays in commissioning according to the implementation plan. Compliance and Intellectual Property: The Supplier grants free, non-exclusive and unlimited rights of use for the licenses that are part of the Supplier's commitment. **Scope of delivery** Delivery Timelines \- Expected Delivery: 2025-12-01 | -| type | RFP, RFI, Tender, etc. | -| publication\_date | 2025-08-01T00:00:00Z | -| deadline | 2025-08-15T00:00:00Z | -| category\_codes | 48000000-8 Software and information systems, 48444100-3 Billing system | -| region | Europe, Asia, etc. | -| budget | $500,000 USD | -| source\_link | Tender detail page: [https://www.opic.com/upphandling/debiteringssystem-(laholmsbuktens-va-ab-halmstad)-aid40481e6db4dfa80c3ead781d5a3fe3a9/?p=8](https://www.opic.com/upphandling/debiteringssystem-\(laholmsbuktens-va-ab-halmstad\)-aid40481e6db4dfa80c3ead781d5a3fe3a9/?p=8) | -| document\_links | Tender document pages (Generate SHA256 for deduplication) | - -##### - - ##### **3\. OCR & Translation Pipeline** - -* Detect document language (via langdetect, fastText) -* If not English: - * Use AI for translation -* Store both original and translated versions with versioning. - - ##### **4\. Normalization & Storage** - -* Store in PostgreSQL or MongoDB: - * **PostgreSQL** for structured metadata (good for querying, indexing) - * **MongoDB** for semi-structured documents and text -* Normalize dates, categorize tenders using tags and CPV codes - -### - -### **3\. Tender Data Normalization & Categorization** - -Transform raw tender data into structured, consistent, and semantically enriched records to enable precise search, filtering, and intelligent matching. This step prepares tenders for downstream AI models by cleaning, classifying, and tagging their content. - -Goal: Convert unstructured tender data into a clean, searchable, and categorized format, and enable accurate AI-powered tender matching through classification, tagging, and keyword extraction. - -#### **Implementation Steps:** - -##### **Text Preprocessing Pipeline** - -* Language Detection: Identify the language; translate non-English tenders. -* Text Cleaning: Remove HTML, special characters, and noise. -* Tokenization: Break text into individual words/phrases. -* Stemming/Lemmatization: Reduce words to their root forms for consistency. -* Stopword Removal: Exclude irrelevant filler words to improve keyword clarity. - - ##### **Tender Classification (Industry, Sector, CPV Code)** - -* Model Training: Train classification models using labeled tender datasets with CPV codes. -* Model Inference: Apply the trained model to new tenders to auto-assign categories and codes. -* Confidence Scoring: Assign a probability score to each classification for accuracy evaluation or fallback logic. - - ##### **Keyword & Requirement Extraction** - -* NER (Named Entity Recognition): Extract names, organizations, locations, and monetary amounts. -* Keyphrase Extraction: Use tools to identify key requirements and tender focus areas. -* Pattern Matching: detect technical/legal requirements (e.g., “ISO 9001”). - - ##### **Tagging & Indexing** - -* Tag Assignment: Auto-generate tags (e.g., “LMS”, “CRM”) based on keywords, industry, and tender type. -* Synonym Mapping: Normalize terminology (e.g., “solar” → “renewable energy”) using internal thesauri or mapping tables. -* Elasticsearch Indexing: Store all structured and tagged tenders in a search engine like Elasticsearch for fast retrieval. - - ##### **Data Storage Format** - -* Save normalized tenders in a JSON schema with fields like title, industry, cpv\_code, tags, keywords, description, and deadline. - -### - -### **4\. Company Onboarding & Knowledge Extraction** - -This step enables companies to easily onboard by uploading their business-related documents. Using AI, we extract structured knowledge about their products, services, experience, and legal readiness to enhance tender matching accuracy. - -Goal: Build a structured profile of each company based on uploaded documents and business data, and enable personalized tender recommendations based on real company capabilities and past experience. - -#### **Implementation Steps:** - -##### **Document Intake & Upload** - -* Dashboard Upload: Support uploading files directly via web interface (drag & drop or file selector). -* Email Parser Integration: Set up a dedicated email and use IMAP to retrieve and parse incoming attachments. -* Supported File Types: PDF, DOCX, XLSX, CSV, images (with OCR), and plain text. - - ##### **File Preprocessing** - -* Document Parsing: - * Use tools for text extraction. - * OCR support for scanned/image-based documents. -* Language Detection & Translation: Auto-translate non-English documents. - - ##### **NLP-Based Information Extraction** - -* Named Entity Recognition (NER): Extract key entities such as product names, certifications, partner companies, dates, locations. -* Keyphrase Extraction: - * Product categories - * Service areas - * Experience keywords (e.g., “government projects”, “international tenders”) - * Legal/readiness terms (e.g., “ISO”, “compliance”, “bond”, “bid security”) -* Classification & Tagging: Categorize the company’s domain (e.g., construction, IT services) using multi-label classifiers. - - ##### **Structured Data Output** - -* Create Company Profile Object: Normalize and store extracted data in structured format. -* Store in Database: Save in MongoDB or PostgreSQL for relational access. - - ##### **Dashboard Visualization** - -* Preview Extracted Info: Allow users to review and correct extracted info in their profile dashboard. -* Editable Tags & Interests: Companies can refine their expertise or interests to improve recommendations. - -### - -### **5\. Interest Modeling (Company Preferences)** - -This step creates a dynamic, evolving interest profile for each company based on onboarding data, behavior, and tender interaction history. It allows the system to tailor tender recommendations using AI-powered semantic understanding. - -Goal: Accurately predict and recommend relevant tenders to each company using machine learning and semantic similarity and Continuously refine recommendations based on real-time feedback like likes, dislikes, and application history. - -### - -#### **Implementation Steps:** - -##### **Interest Profile Construction** - -* Initial Profile Creation: - * Extract from onboarding data (e.g., keywords, sectors, product types). - * Store as structured embeddings using models or OpenAI embeddings. -* Behavioral Signals: - * Track: - * Tenders the company liked/disliked (via UI interaction). - * Tenders viewed in detail. - * Tenders the company applied to. - * Assign implicit weights to actions: - * Applied \> Liked \> Viewed \> Ignored \> Disliked. - - ##### **Embedding & Vector Space Modeling** - -* Embed Tender Metadata: - * Use models or OpenAI to embed: - * Title \+ Description \+ Category \+ Keywords of each tender. -* Embed Company Profile: - * Combine onboarding profile \+ interacted tenders to form a composite embedding. - * Update periodically using weighted average of embeddings and attention mechanisms. - - ##### **Matching via Similarity** - -* Calculate Similarity Scores: - * Use similarity between tender vectors and the company interest vector. - * Rank tenders based on similarity scores. -* Dynamic Thresholding: - * Auto-adjust thresholds to optimize for engagement. - - ##### **Continuous Learning** - -* Feedback Loop: - * After each interaction, retrain/update interest vectors with new signals. - * Use weighting (e.g., recent feedback has higher influence). -* Cold Start Handling: - * For new users: rely more on onboarding data and sector-based popularity. - - ##### **Data Storage** - -* Store embeddings in vector databases. - * Maintain a link between company\_id and interest vectors. - - ##### **Dashboard Integration** - -* Allow companies to optionally adjust their visible “interest tags.” -* Show feedback insights: “Why this tender was recommended” using explainable AI labels. - -### - -### **6\. AI-Powered Tender Matching & Feedback Loop** - -This step delivers personalized tender recommendations to users via a user-friendly interface and continuously improves accuracy by learning from their actions (like, dislike, apply). The system refines its AI model in real-time to better align future matches with user preferences. - -Goal: Display top-matching tenders to companies using semantic similarity and preference learning, and continuously refine matching logic using interaction data to improve recommendation precision. - -#### **Implementation Steps:** - -##### **Tender Recommendation Engine** - -* Input: - * Vector embeddings of tenders (from metadata and documents). - * Company interest profile (from onboarding and behavioral history). -* Processing: - * Calculate similarity between tender vectors and company interest vectors. - * Apply filters (e.g., deadlines, budget thresholds, region) as post-processing. - * Return top N matches with ranking score. -* Output: - * List of tenders with scores, tags, and explanations (“matched based on your interest in X”). - - ##### **Frontend Integration (Web & Mobile)** - -* Display Recommendations: - * Swipe interface (Right \= Like, Left \= Dislike). -* Tender Details Page: - * View all tender information, documents, requirements. - * Show AI-generated tags and relevance explanation. -* UX Enhancements: - * Save, share, and bookmark options. - * Quick apply or partner apply triggers. - - ##### **Logging & Feedback Capture** - -* Log every user interaction: - * Swipe/Like/Dislike/Apply/Open/View duration. - * Tag feedback with tender ID and company ID. -* Store in an event table or analytics service. - - ##### **Feedback Loop for Model Refinement** - -* Model Update Strategy: - * Use positive actions (like/apply) to reinforce embeddings. - * Use negative actions (dislike) to reduce weight for similar tenders. -* Training Pipeline: - * Batch trains daily or weekly on logged feedback. - * Fine-tune vector weighting, tagging priorities, and scoring thresholds. - * Use techniques like contrastive learning or reinforcement-based re-ranking. -* Model Versions: - * Track multiple versions of recommendation models. - * Monitor engagement metrics for each. - - ##### **Continuous Learning Architecture** - -* Maintain: - * Embeddings index. - * Interaction history per company. - * Real-time scoring microservice for fast tender matching. -* Periodic retraining with growing interaction dataset. - -### **7\. Tender Detail View & Document Requirement Analysis** - -This feature ensures companies are well-prepared to submit complete, compliant applications by analyzing tender requirements and comparing them against existing company documents. It surfaces gaps and guides users through completion with AI-powered recommendations. - -Goal: Provide an intelligent tender detail view that highlights all requirements and tracks preparation status, and automatically detect missing documents and guide users to complete their application package efficiently. - -### - -#### **Implementation Steps:** - -##### **Tender Detail Page Interface** - -* Display Components: - * Tender metadata: Title, type (RFI/RFP), deadline, budget, country, CPV codes. - * Downloadable tender documents (with preview). - * Eligibility criteria, required legal & technical documents. - * AI-suggested tags and summary (extracted from content). - * “Like / Dislike” or “Apply Now” buttons. -* Progress Features: - * Upload area with drag-and-drop or document selection. - * Visual progress bar (% of required docs completed). - * Real-time upload recommendations: “You’re missing X document for eligibility.” - - ##### **Document Content Analysis** - -* Extraction Pipeline: - * Automatically parse PDFs, Word, and Excel files from tenders. - * Use NLP models to detect requirement sections, deadlines, eligibility criteria, and file checklists. - * Extract named entities (licenses, certifications, formats). - * Identify legal, financial, and technical expectations using keyword and pattern detection. - - ##### **Company Document Matching** - -* Cross-check Requirements vs. Company Assets: - * Use company's previously uploaded/onboarded files. - * Apply classification to each file (license, portfolio, certificate, etc.). - * Match against tender needs using semantic similarity. - * Identify missing or outdated documents (based on expiration or file type). -* Output: - * List of matched vs. missing docs with confidence scores. - * Suggested templates or examples if the required doc is not available. - * Button to request generation or partner document upload if needed. - - ##### **Completion Progress Engine** - -* Logic: - * Calculate completeness score as (Matched docs / Required docs). - * Show color-coded progress bar (e.g., red \<50%, yellow 50–90%, green 90–100%). - * Optional: provide estimated readiness time based on historical upload pace. -* Notifications: - * Trigger alerts for missing documents with deadlines. - * Recommend upload or request from partners. - - ##### **Smart Upload Assistant (Optional AI enhancement)** - -* Auto-suggest appropriate files from user’s document repository. -* Pre-fill metadata for uploaded documents (e.g., expiry date, type). -* Offer translation option for non-English documents before submission. - -### **8\. Document Finalization & Submission Preparation** - -This stage ensures that once all tender requirements are fulfilled, the system automatically assembles a submission-ready package, following each tender’s specific formatting, compliance, and submission rules. Whether submitting solo or through a partner, companies receive a polished, validated package. - -Goal: Streamline and automate the final preparation of all tender documents into a compliant, complete submission package, and support both self-application and partnership-based submission paths - - -#### **Implementation Steps:** - -##### **Document Aggregation & Validation** - -* Trigger: Once all required documents are marked as “uploaded” and “valid.” -* Actions: - * Gather documents from company repository or upload history. - * Validate document types, names, and formats against tender requirements. - * Check for required elements (e.g., stamps, signatures, date fields, headers). - - ##### **Format Conversion & Compilation** - -* Auto-conversion: - * Convert all documents to required formats (e.g., PDF/A, DOCX, XLSX). -* File merging & compression: - * Merge into a single file or grouped ZIP, based on tender rules. - * Compress files (e.g., using zip) to meet size limits. - - ##### **Compliance & Formatting Checks** - -* Use rule-based validation for each tender (stored in tender metadata): - * File size limits - * Naming conventions - * Required fields (e.g., bid amount, bidder name) - * Format-specific checks (password protection, watermarking) -* Highlight compliance issues and block submission until resolved. - - ##### **Submission Path Handling** - -* Self-Apply Path: - * Show final review screen with submission checklist. - * Provide “Download Submission Package” or “Auto-submit” via portal login (if credentials provided). - * Save submission receipt if submitted automatically. -* Partnership-Apply Path: - * Check which documents are missing or out of scope (e.g., large financial guarantees). - * Initiate request to partner with progress tracker. - * Once all parts are covered, follow same compilation and submission flow. - - ##### **Logging & Auditing** - -* Save submission timestamp, method, and document hash for auditing. -* Generate a PDF summary page: tender info, included documents, company details. - -### **9\. Post-Submission Tracking & Follow-Up** - -This step extends the tender lifecycle beyond submission by allowing companies to monitor outcomes, receive updates, and learn from past results. It also feeds valuable outcome data back into the AI to improve future tender matching and interest modeling. - -Goal: Provide full visibility into the status of submitted tenders and enable intelligent follow-ups, and collect feedback data to refine AI recommendations and improve success rates over time - - -#### **Implementation Steps:** - -##### **Submission Status Tracking** - -* Integration points: - * Where supported, integrate with tender portals via API or email scraping to fetch status updates (e.g., “Under Review”, “Awarded”, “Rejected”). - * Otherwise, allow companies to manually update status. -* Statuses supported: - * Submitted - * Under Review - * Clarification Requested - * Won - * Lost -* Implementation: - * Use webhook listeners or scheduled API polling to monitor external status (where available). - * NLP-based email parser to extract outcome data from official tender authority emails. - - ##### **Outcome Logging & Analytics** - -* After a result is confirmed: - * Log result, including winning bidder info (if public), reason for win/loss (if available). - * Store structured outcome data in database (linked to tender ID and company ID). - * Trigger learning module: feed result into AI model to improve future predictions. - - ##### **Feedback to AI Models** - -* Use outcomes to: - * Refine tender-to-company matching algorithm (reward winning features). - * Update company interest vectors to reflect real wins/losses. - * Adjust tender classification/weighting based on what tends to succeed. - - ##### **Follow-Up Reminders & Re-engagement** - -* Features: - * Show follow-up prompts like: - * “This tender reopens annually – set a reminder?” - * “Missed this tender? 3 similar ones are now open.” - * Schedule email/SMS reminders based on historical tender dates. -* Implementation: - * Use clustering or similarity search to suggest future relevant tenders. - * Store and query tender metadata to detect recurring patterns (e.g., same title/code). - - ##### **Auditable History & Notifications** - -* Display a full lifecycle log in the user dashboard: - * Dates of submission, updates, outcome, actions taken. -* Allow download of submission receipts and audit logs. -* Notify teams of key status changes via web app and email. - diff --git a/docs/examples/API_EXAMPLES.md b/docs/examples/API_EXAMPLES.md deleted file mode 100644 index ba8a17a..0000000 --- a/docs/examples/API_EXAMPLES.md +++ /dev/null @@ -1,491 +0,0 @@ -# API Examples - -This document provides examples of how to interact with the Tender Management API using curl commands. - -## Prerequisites - -1. **Start MongoDB**: - ```bash - sudo systemctl start mongod - ``` - -2. **Start the server**: - ```bash - make run - ``` - -3. **Verify server is running**: - ```bash - curl http://localhost:8081/health - ``` - -## Customer Management Examples - -### 1. Register a New Customer (Admin Only) - -```bash -curl -X POST http://localhost:8081/api/admin/customers/register \ - -H "Content-Type: application/json" \ - -d '{ - "full_name": "John Doe", - "username": "johndoe", - "email": "john@example.com", - "mobile": "+1234567890", - "password": "securepassword123", - "national_id": "123456789", - "gender": "male", - "birthdate": 631152000, - "profile_image": "https://example.com/avatar.jpg" - }' -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Customer registered successfully", - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "full_name": "John Doe", - "username": "johndoe", - "email": "john@example.com", - "mobile": "+1234567890", - "national_id": "123456789", - "gender": "male", - "birthdate": 631152000, - "profile_image": "https://example.com/avatar.jpg", - "status": "active", - "is_verified": false, - "created_at": 1703123456, - "updated_at": 1703123456 - } -} -``` - -### 2. Customer Login - -```bash -curl -X POST http://localhost:8081/api/customers/login \ - -H "Content-Type: application/json" \ - -d '{ - "email_or_mobile": "john@example.com", - "password": "securepassword123" - }' -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Login successful", - "data": { - "customer": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "full_name": "John Doe", - "username": "johndoe", - "email": "john@example.com", - "mobile": "+1234567890", - "national_id": "123456789", - "gender": "male", - "birthdate": 631152000, - "profile_image": "https://example.com/avatar.jpg", - "status": "active", - "is_verified": false, - "created_at": 1703123456, - "updated_at": 1703123456 - }, - "access_token": "abc123def456", - "refresh_token": "xyz789uvw012", - "expires_at": 1703127056 - } -} -``` - -### 3. Get Customer Profile (Protected) - -```bash -curl -X GET http://localhost:8081/api/customers/profile \ - -H "X-Customer-ID: 550e8400-e29b-41d4-a716-446655440000" -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Profile retrieved successfully", - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "full_name": "John Doe", - "username": "johndoe", - "email": "john@example.com", - "mobile": "+1234567890", - "national_id": "123456789", - "gender": "male", - "birthdate": 631152000, - "profile_image": "https://example.com/avatar.jpg", - "status": "active", - "is_verified": false, - "created_at": 1703123456, - "updated_at": 1703123456 - } -} -``` - -### 4. Update Customer Profile (Protected) - -```bash -curl -X PUT http://localhost:8081/api/customers/profile \ - -H "Content-Type: application/json" \ - -H "X-Customer-ID: 550e8400-e29b-41d4-a716-446655440000" \ - -d '{ - "full_name": "John Smith", - "profile_image": "https://example.com/new-avatar.jpg" - }' -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Profile updated successfully", - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "full_name": "John Smith", - "username": "johndoe", - "email": "john@example.com", - "mobile": "+1234567890", - "national_id": "123456789", - "gender": "male", - "birthdate": 631152000, - "profile_image": "https://example.com/new-avatar.jpg", - "status": "active", - "is_verified": false, - "created_at": 1703123456, - "updated_at": 1703123456 - } -} -``` - -### 5. Change Password (Protected) - -```bash -curl -X PUT http://localhost:8081/api/customers/change-password \ - -H "Content-Type: application/json" \ - -H "X-Customer-ID: 550e8400-e29b-41d4-a716-446655440000" \ - -d '{ - "old_password": "securepassword123", - "new_password": "newsecurepassword456" - }' -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Password changed successfully", - "data": null -} -``` - -### 6. Add Device Token (Protected) - -```bash -curl -X POST http://localhost:8081/api/customers/device-token \ - -H "Content-Type: application/json" \ - -H "X-Customer-ID: 550e8400-e29b-41d4-a716-446655440000" \ - -d '{ - "device_token": "fcm_token_123456", - "device_type": "android" - }' -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Device token added successfully", - "data": null -} -``` - -### 7. Remove Device Token (Protected) - -```bash -curl -X DELETE http://localhost:8081/api/customers/device-token \ - -H "Content-Type: application/json" \ - -H "X-Customer-ID: 550e8400-e29b-41d4-a716-446655440000" \ - -d '{ - "device_token": "fcm_token_123456" - }' -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Device token removed successfully", - "data": null -} -``` - -### 8. Logout (Protected) - -```bash -curl -X POST http://localhost:8081/api/customers/logout \ - -H "Content-Type: application/json" \ - -H "X-Customer-ID: 550e8400-e29b-41d4-a716-446655440000" \ - -d '{ - "device_token": "fcm_token_123456" - }' -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Logged out successfully", - "data": null -} -``` - -## Admin Management Examples - -### 9. List All Customers (Admin Only) - -```bash -curl -X GET "http://localhost:8081/api/admin/customers?limit=10&offset=0&search=john&status=active" \ - -H "Content-Type: application/json" -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Customers retrieved successfully", - "data": [ - { - "id": "550e8400-e29b-41d4-a716-446655440000", - "full_name": "John Smith", - "username": "johndoe", - "email": "john@example.com", - "mobile": "+1234567890", - "national_id": "123456789", - "gender": "male", - "birthdate": 631152000, - "profile_image": "https://example.com/new-avatar.jpg", - "status": "active", - "is_verified": false, - "created_at": 1703123456, - "updated_at": 1703123456 - } - ], - "meta": { - "total": 1, - "limit": 10, - "offset": 0 - } -} -``` - -### 10. Get Customer by ID (Admin Only) - -```bash -curl -X GET http://localhost:8081/api/admin/customers/550e8400-e29b-41d4-a716-446655440000 \ - -H "Content-Type: application/json" -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Customer retrieved successfully", - "data": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "full_name": "John Smith", - "username": "johndoe", - "email": "john@example.com", - "mobile": "+1234567890", - "national_id": "123456789", - "gender": "male", - "birthdate": 631152000, - "profile_image": "https://example.com/new-avatar.jpg", - "status": "active", - "is_verified": false, - "created_at": 1703123456, - "updated_at": 1703123456 - } -} -``` - -### 11. Update Customer Status (Admin Only) - -```bash -curl -X PUT http://localhost:8081/api/admin/customers/550e8400-e29b-41d4-a716-446655440000/status \ - -H "Content-Type: application/json" \ - -d '{ - "status": "suspended" - }' -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Customer status updated successfully", - "data": null -} -``` - -### 12. Get All Device Tokens (Admin Only) - -```bash -curl -X GET http://localhost:8081/api/admin/customers/device-tokens \ - -H "Content-Type: application/json" -``` - -**Expected Response**: -```json -{ - "success": true, - "message": "Device tokens retrieved successfully", - "data": [ - { - "token": "fcm_token_123456", - "device_type": "android", - "customer_id": "550e8400-e29b-41d4-a716-446655440000" - } - ] -} -``` - -## Error Examples - -### 13. Invalid Email Format - -```bash -curl -X POST http://localhost:8081/api/admin/customers/register \ - -H "Content-Type: application/json" \ - -d '{ - "full_name": "John Doe", - "username": "johndoe", - "email": "invalid-email", - "mobile": "+1234567890", - "password": "securepassword123" - }' -``` - -**Expected Response**: -```json -{ - "success": false, - "message": "Validation failed", - "error": "Email: invalid-email does not validate as email" -} -``` - -### 14. Duplicate Email - -```bash -curl -X POST http://localhost:8081/api/admin/customers/register \ - -H "Content-Type: application/json" \ - -d '{ - "full_name": "Jane Doe", - "username": "janedoe", - "email": "john@example.com", - "mobile": "+1234567891", - "password": "securepassword123" - }' -``` - -**Expected Response**: -```json -{ - "success": false, - "message": "email already exists", - "error": "email already exists" -} -``` - -### 15. Invalid Authentication - -```bash -curl -X GET http://localhost:8081/api/customers/profile -``` - -**Expected Response**: -```json -{ - "success": false, - "message": "Invalid authentication", - "error": "Missing customer ID" -} -``` - -## Testing Script - -You can use the following script to test all endpoints: - -```bash -#!/bin/bash - -BASE_URL="http://localhost:8081" - -echo "Testing Tender Management API..." - -# Test health endpoint -echo "1. Testing health endpoint..." -curl -s "$BASE_URL/health" | jq . - -# Register a customer -echo "2. Registering a customer..." -CUSTOMER_RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/customers/register" \ - -H "Content-Type: application/json" \ - -d '{ - "full_name": "Test User", - "username": "testuser", - "email": "test@example.com", - "mobile": "+1234567890", - "password": "password123" - }') - -echo "$CUSTOMER_RESPONSE" | jq . - -# Extract customer ID -CUSTOMER_ID=$(echo "$CUSTOMER_RESPONSE" | jq -r '.data.id') - -if [ "$CUSTOMER_ID" != "null" ]; then - echo "Customer ID: $CUSTOMER_ID" - - # Test login - echo "3. Testing login..." - curl -s -X POST "$BASE_URL/api/customers/login" \ - -H "Content-Type: application/json" \ - -d '{ - "email_or_mobile": "test@example.com", - "password": "password123" - }' | jq . - - # Test get profile - echo "4. Testing get profile..." - curl -s -X GET "$BASE_URL/api/customers/profile" \ - -H "X-Customer-ID: $CUSTOMER_ID" | jq . - - # Test list customers (admin) - echo "5. Testing list customers..." - curl -s -X GET "$BASE_URL/api/admin/customers" | jq . -fi - -echo "Testing completed!" -``` - -## Notes - -1. **Authentication**: Currently using a simple header-based authentication (`X-Customer-ID`). In production, this should be replaced with proper JWT token validation. - -2. **Timestamps**: All timestamps are Unix timestamps (int64) for consistency. - -3. **Validation**: All requests are validated using govalidator with custom validation rules. - -4. **Error Handling**: All errors return consistent JSON responses with appropriate HTTP status codes. - -5. **CORS**: The server is configured to allow all origins for development. Configure properly for production. \ No newline at end of file diff --git a/docs/implementation/IMPLEMENTATION_SUMMARY.md b/docs/implementation/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index c902bbd..0000000 --- a/docs/implementation/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,276 +0,0 @@ -# Implementation Summary: HTTP Server and Dependency Injection - -## ✅ Completed Implementation - -### 1. HTTP Server Initialization - -**File**: `cmd/web/main.go` -- ✅ Initialized Echo v4 HTTP server -- ✅ Configured server with proper middleware stack -- ✅ Added health check endpoint (`/health`) -- ✅ Implemented structured logging for all requests -- ✅ Added CORS configuration for cross-origin requests -- ✅ Configured server to listen on configured host and port - -### 2. Dependency Injection Chain - -**Complete DI Chain Implemented**: -``` -MongoDB Connection Manager → Repositories → Services → Handlers → HTTP Server -``` - -**Components**: -- ✅ **MongoDB Connection Manager** (`pkg/mongo/connection.go`) -- ✅ **Customer Repository** (`internal/customer/repository.go`) -- ✅ **Customer Service** (`internal/customer/service.go`) -- ✅ **Customer Handler** (`internal/customer/handler.go`) -- ✅ **HTTP Server** (`cmd/web/main.go`) - -### 3. Middleware Stack - -**Implemented Middleware**: -1. ✅ **Recover** - Panic recovery -2. ✅ **RequestID** - Unique request identification -3. ✅ **Logger** - Request logging -4. ✅ **CORS** - Cross-origin resource sharing -5. ✅ **Custom Logging** - Structured request logging with timing - -### 4. API Endpoints - -**Customer Endpoints**: -- ✅ `POST /api/customers/login` - Customer login -- ✅ `POST /api/customers/refresh-token` - Token refresh -- ✅ `GET /api/customers/profile` - Get profile (protected) -- ✅ `PUT /api/customers/profile` - Update profile (protected) -- ✅ `PUT /api/customers/change-password` - Change password (protected) -- ✅ `POST /api/customers/device-token` - Add device token (protected) -- ✅ `DELETE /api/customers/device-token` - Remove device token (protected) -- ✅ `POST /api/customers/logout` - Logout (protected) - -**Admin Endpoints**: -- ✅ `POST /api/admin/customers/register` - Register customer (admin) -- ✅ `GET /api/admin/customers` - List customers (admin) -- ✅ `GET /api/admin/customers/:id` - Get customer by ID (admin) -- ✅ `PUT /api/admin/customers/:id/status` - Update customer status (admin) -- ✅ `GET /api/admin/customers/device-tokens` - Get all device tokens (admin) - -**System Endpoints**: -- ✅ `GET /health` - Server health status - -### 5. Configuration Management - -**File**: `cmd/web/config.yaml` -- ✅ Server configuration (host, port, timeouts) -- ✅ Database configuration (MongoDB URI, pool settings) -- ✅ Logging configuration (level, format, file rotation) -- ✅ Auth configuration (JWT settings) -- ✅ CORS configuration - -### 6. Error Handling - -**Implemented**: -- ✅ Consistent JSON response format -- ✅ Proper HTTP status codes -- ✅ Validation error handling -- ✅ Structured error logging -- ✅ Graceful error recovery - -### 7. Logging System - -**Features**: -- ✅ Structured logging with fields -- ✅ Request/response logging with timing -- ✅ Error logging with context -- ✅ File rotation based on size and age -- ✅ Configurable log levels - -### 8. Build and Deployment - -**Files Created**: -- ✅ `Makefile` - Build and run commands -- ✅ `test_server.sh` - Test script -- ✅ `HTTP_SERVER_SETUP.md` - Comprehensive documentation -- ✅ `API_EXAMPLES.md` - API usage examples - -## 🏗️ Architecture Compliance - -### Clean Architecture Principles -- ✅ **Separation of Concerns** - Clear layer separation -- ✅ **Dependency Inversion** - Depend on interfaces, not implementations -- ✅ **Single Responsibility** - Each component has a single purpose -- ✅ **Open/Closed Principle** - Easy to extend without modification - -### Domain-Driven Design -- ✅ **Domain Entities** - Customer entity with business rules -- ✅ **Aggregates** - Customer as aggregate root -- ✅ **Repositories** - Data access abstraction -- ✅ **Services** - Business logic encapsulation -- ✅ **Value Objects** - DeviceToken, Gender, etc. - -### Go Best Practices -- ✅ **Package Structure** - Flat domain structure -- ✅ **Error Handling** - Explicit error handling -- ✅ **Interface Design** - Repository and service interfaces -- ✅ **Configuration** - Environment-based configuration -- ✅ **Logging** - Structured logging with context - -## 🔧 Technical Implementation - -### Database Layer -- ✅ **MongoDB Connection Manager** - Connection pooling and management -- ✅ **Repository Pattern** - Data access abstraction -- ✅ **Index Management** - Automatic index creation -- ✅ **Transaction Support** - Proper transaction handling - -### Business Logic Layer -- ✅ **Service Layer** - Business logic encapsulation -- ✅ **Validation** - Input validation with govalidator -- ✅ **Password Hashing** - bcrypt for secure password storage -- ✅ **Token Generation** - Secure token generation for authentication - -### Presentation Layer -- ✅ **HTTP Handlers** - Request/response handling -- ✅ **Middleware** - Cross-cutting concerns -- ✅ **CORS** - Cross-origin resource sharing -- ✅ **Authentication** - Basic authentication structure - -## 📊 Performance Features - -### Optimizations -- ✅ **Connection Pooling** - MongoDB connection pooling -- ✅ **Request Logging** - Performance monitoring -- ✅ **Error Recovery** - Graceful error handling -- ✅ **Resource Management** - Proper cleanup and resource management - -### Monitoring -- ✅ **Health Check** - Server health monitoring -- ✅ **Request Metrics** - Request timing and status -- ✅ **Error Tracking** - Structured error logging -- ✅ **Performance Logging** - Request duration tracking - -## 🔐 Security Features - -### Implemented -- ✅ **Input Validation** - Comprehensive request validation -- ✅ **Password Security** - bcrypt password hashing -- ✅ **CORS Configuration** - Cross-origin request handling -- ✅ **Error Sanitization** - No sensitive data exposure - -### Planned (TODO) -- 🔄 **JWT Authentication** - Token-based authentication -- 🔄 **Rate Limiting** - Request rate limiting -- 🔄 **HTTPS** - SSL/TLS encryption -- 🔄 **Input Sanitization** - XSS protection - -## 🧪 Testing and Quality - -### Build System -- ✅ **Go Modules** - Proper dependency management -- ✅ **Makefile** - Build automation -- ✅ **Test Scripts** - Automated testing -- ✅ **Documentation** - Comprehensive documentation - -### Code Quality -- ✅ **Error Handling** - Comprehensive error handling -- ✅ **Logging** - Structured logging throughout -- ✅ **Validation** - Input validation at all layers -- ✅ **Documentation** - Clear code documentation - -## 🚀 Deployment Ready - -### Prerequisites -- ✅ **MongoDB** - Database server -- ✅ **Go 1.23+** - Runtime environment -- ✅ **Configuration** - Environment configuration - -### Build Commands -```bash -# Build the server -make build - -# Run the server -make run - -# Run tests -make test - -# Clean build artifacts -make clean -``` - -### Docker Support -- ✅ **Dockerfile** - Container configuration -- ✅ **Docker Compose** - Multi-service deployment -- ✅ **Build Commands** - Docker build automation - -## 📈 Scalability Considerations - -### Architecture Benefits -- ✅ **Modular Design** - Easy to add new domains -- ✅ **Interface-Based** - Easy to swap implementations -- ✅ **Stateless Design** - Horizontal scaling ready -- ✅ **Connection Pooling** - Database connection optimization - -### Future Enhancements -- 🔄 **Caching Layer** - Redis integration -- 🔄 **Message Queue** - RabbitMQ integration -- 🔄 **Search Engine** - Elasticsearch integration -- 🔄 **Monitoring** - Prometheus metrics - -## 🎯 Success Metrics - -### Functional Requirements -- ✅ **HTTP Server** - Fully functional HTTP server -- ✅ **Dependency Injection** - Complete DI chain implemented -- ✅ **API Endpoints** - All customer management endpoints -- ✅ **Database Integration** - MongoDB integration complete -- ✅ **Error Handling** - Comprehensive error handling -- ✅ **Logging** - Structured logging system - -### Non-Functional Requirements -- ✅ **Performance** - Optimized for performance -- ✅ **Security** - Basic security measures implemented -- ✅ **Maintainability** - Clean, well-documented code -- ✅ **Scalability** - Architecture supports scaling -- ✅ **Testability** - Easy to test and validate - -## 🔄 Next Steps - -### Immediate Tasks -1. **Authentication** - Implement JWT token validation -2. **Testing** - Add comprehensive unit and integration tests -3. **Documentation** - Add API documentation (Swagger) -4. **Monitoring** - Add metrics and monitoring - -### Future Enhancements -1. **Additional Domains** - Add tender, company, bid domains -2. **Advanced Features** - File upload, notifications, search -3. **Production Ready** - SSL, rate limiting, monitoring -4. **Microservices** - Split into microservices if needed - -## 📝 Documentation - -### Created Files -- ✅ `HTTP_SERVER_SETUP.md` - Server setup and configuration -- ✅ `API_EXAMPLES.md` - API usage examples -- ✅ `IMPLEMENTATION_SUMMARY.md` - This summary -- ✅ `test_server.sh` - Test script -- ✅ Updated `Makefile` - Build automation - -### Code Documentation -- ✅ **Inline Comments** - Clear code documentation -- ✅ **Function Documentation** - Go doc comments -- ✅ **Architecture Documentation** - System design docs -- ✅ **API Documentation** - Endpoint documentation - -## 🎉 Conclusion - -The HTTP server has been successfully initialized with proper dependency injection following Clean Architecture principles. The implementation includes: - -- **Complete DI Chain**: MongoDB → Repositories → Services → Handlers → HTTP Server -- **Full API Coverage**: All customer management endpoints implemented -- **Production Ready**: Proper error handling, logging, and configuration -- **Scalable Architecture**: Easy to extend with new domains and features -- **Comprehensive Documentation**: Complete setup and usage documentation - -The server is ready for development and can be easily extended with additional domains and features as needed. \ No newline at end of file diff --git a/docs/setup/HTTP_SERVER_SETUP.md b/docs/setup/HTTP_SERVER_SETUP.md deleted file mode 100644 index 5c9a2d5..0000000 --- a/docs/setup/HTTP_SERVER_SETUP.md +++ /dev/null @@ -1,301 +0,0 @@ -# HTTP Server Setup and Dependency Injection - -## Overview - -The Tender Management API server has been successfully initialized with proper dependency injection following Clean Architecture principles. The server uses Echo v4 as the HTTP framework and implements a complete dependency injection chain. - -## Architecture - -### Dependency Injection Chain - -``` -MongoDB Connection Manager - ↓ - Repositories - ↓ - Services - ↓ - Handlers - ↓ - HTTP Server (Echo) -``` - -### Components - -1. **MongoDB Connection Manager** (`pkg/mongo/connection.go`) - - Manages database connections and pooling - - Provides connection to repositories - -2. **Repositories** (`internal/customer/repository.go`) - - Data access layer - - Implements repository pattern - - Handles database operations - -3. **Services** (`internal/customer/service.go`) - - Business logic layer - - Implements use cases - - Depends on repositories - -4. **Handlers** (`internal/customer/handler.go`) - - HTTP request/response handling - - Input validation - - Depends on services - -5. **HTTP Server** (`cmd/web/main.go`) - - Echo v4 server with middleware - - Route registration - - Server lifecycle management - -## Server Features - -### Middleware Stack - -1. **Recover** - Panic recovery -2. **RequestID** - Unique request identification -3. **Logger** - Request logging -4. **CORS** - Cross-origin resource sharing -5. **Custom Logging** - Structured request logging - -### Endpoints - -#### Health Check -- `GET /health` - Server health status - -#### Customer Endpoints -- `POST /api/customers/login` - Customer login -- `POST /api/customers/refresh-token` - Token refresh -- `GET /api/customers/profile` - Get profile (protected) -- `PUT /api/customers/profile` - Update profile (protected) -- `PUT /api/customers/change-password` - Change password (protected) -- `POST /api/customers/device-token` - Add device token (protected) -- `DELETE /api/customers/device-token` - Remove device token (protected) -- `POST /api/customers/logout` - Logout (protected) - -#### Admin Endpoints -- `POST /api/admin/customers/register` - Register customer (admin) -- `GET /api/admin/customers` - List customers (admin) -- `GET /api/admin/customers/:id` - Get customer by ID (admin) -- `PUT /api/admin/customers/:id/status` - Update customer status (admin) -- `GET /api/admin/customers/device-tokens` - Get all device tokens (admin) - -## Configuration - -### Server Configuration (`config.yaml`) - -```yaml -server: - host: "0.0.0.0" - port: 8081 - timeout: 30s - read_timeout: 10s - write_timeout: 10s -``` - -### Database Configuration - -```yaml -database: - mongodb: - uri: "mongodb://localhost:27017" - name: "tender_management" - timeout: 10s - max_pool_size: 100 -``` - -## Running the Server - -### Prerequisites - -1. **MongoDB** - Must be running locally or accessible -2. **Go 1.23+** - Required for compilation -3. **Configuration** - `config.yaml` must be present - -### Build and Run - -```bash -# Build the server -go build -o bin/web ./cmd/web - -# Run the server -./bin/web -``` - -### Development - -```bash -# Run with hot reload (if using air) -air - -# Or run directly -go run ./cmd/web -``` - -## Testing - -### Health Check - -```bash -curl http://localhost:8081/health -``` - -Expected response: -```json -{ - "status": "healthy", - "time": 1703123456, - "version": "1.0.0" -} -``` - -### Customer Registration - -```bash -curl -X POST http://localhost:8081/api/admin/customers/register \ - -H "Content-Type: application/json" \ - -d '{ - "full_name": "John Doe", - "username": "johndoe", - "email": "john@example.com", - "mobile": "+1234567890", - "password": "securepassword123", - "national_id": "123456789", - "gender": "male" - }' -``` - -## Logging - -The server implements structured logging with the following features: - -- **Request Logging** - All HTTP requests are logged with timing -- **Error Logging** - Errors are logged with context -- **Structured Fields** - Logs include relevant metadata -- **File Rotation** - Logs are rotated based on size and age - -### Log Configuration - -```yaml -logging: - level: "info" - format: "json" - output: "file" - file: - path: "./logs/app.log" - max_size: 100 - max_backups: 5 - max_age: 30 - compress: true -``` - -## Security Features - -### CORS Configuration - -```go -middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"*"}, // Configure for production - AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodOptions}, - AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization}, -}) -``` - -### Authentication (TODO) - -- JWT token validation -- Role-based access control -- Token refresh mechanism - -## Error Handling - -### HTTP Status Codes - -- `200` - Success -- `201` - Created -- `400` - Bad Request -- `401` - Unauthorized -- `404` - Not Found -- `409` - Conflict -- `422` - Validation Error -- `500` - Internal Server Error - -### Response Format - -```json -{ - "success": true, - "message": "Operation successful", - "data": {}, - "meta": {} -} -``` - -## Monitoring - -### Health Check - -The `/health` endpoint provides basic server status: - -- Server status -- Current timestamp -- Version information - -### Request Metrics - -Each request is logged with: - -- HTTP method -- Request path -- Response status -- Processing duration -- User agent -- Remote IP - -## Future Enhancements - -1. **Authentication Middleware** - Implement JWT validation -2. **Rate Limiting** - Add request rate limiting -3. **Metrics** - Add Prometheus metrics -4. **Tracing** - Add distributed tracing -5. **API Documentation** - Add Swagger/OpenAPI docs -6. **Testing** - Add comprehensive test suite - -## Troubleshooting - -### Common Issues - -1. **MongoDB Connection Failed** - - Ensure MongoDB is running - - Check connection URI in config - - Verify network connectivity - -2. **Port Already in Use** - - Change port in config.yaml - - Kill existing process using the port - -3. **Permission Denied** - - Ensure write permissions for log directory - - Check file permissions - -### Debug Mode - -To enable debug logging, change the log level in config.yaml: - -```yaml -logging: - level: "debug" -``` - -## Dependencies - -### Core Dependencies - -- `github.com/labstack/echo/v4` - HTTP framework -- `go.mongodb.org/mongo-driver` - MongoDB driver -- `github.com/google/uuid` - UUID generation -- `golang.org/x/crypto/bcrypt` - Password hashing -- `github.com/asaskevich/govalidator` - Input validation - -### Development Dependencies - -- `github.com/stretchr/testify` - Testing framework -- `go.uber.org/zap` - Logging framework \ No newline at end of file diff --git a/docs/setup/SWAGGER_SETUP.md b/docs/setup/SWAGGER_SETUP.md deleted file mode 100644 index 69fa4b7..0000000 --- a/docs/setup/SWAGGER_SETUP.md +++ /dev/null @@ -1,313 +0,0 @@ -# Swagger API Documentation Setup - -This document explains how to use and maintain the Swagger API documentation for the Tender Management Backend. - -## 📚 Overview - -The API documentation is generated using [Swag](https://github.com/swaggo/swag) and served using [echo-swagger](https://github.com/swaggo/echo-swagger). The documentation provides an interactive interface to explore and test all API endpoints. - -## 🚀 Quick Start - -### 1. Generate Documentation - -```bash -# Generate Swagger documentation -make docs - -# Or manually -swag init -g cmd/web/main.go -o cmd/web/docs -``` - -### 2. Start Server with Documentation - -```bash -# Build and run with documentation -make run-docs - -# Or manually -make build -./bin/web -``` - -### 3. Access Documentation - -Open your browser and navigate to: -- **Swagger UI**: http://localhost:8081/swagger/index.html -- **Health Check**: http://localhost:8081/health - -## 📋 Available Endpoints - -### 🔐 Authentication -- `POST /api/customers/login` - Customer login -- `POST /api/customers/refresh-token` - Refresh access token - -### 👤 Customer Profile (Protected) -- `GET /api/customers/profile` - Get customer profile -- `PUT /api/customers/profile` - Update customer profile -- `PUT /api/customers/change-password` - Change password - -### 📱 Device Management (Protected) -- `POST /api/customers/device-token` - Add device token -- `DELETE /api/customers/device-token` - Remove device token - -### 👥 Admin Operations (Admin Protected) -- `POST /api/admin/customers/register` - Register new customer -- `GET /api/admin/customers` - List customers -- `GET /api/admin/customers/{id}` - Get customer by ID -- `PUT /api/admin/customers/{id}/status` - Update customer status - -## 🛠️ Development - -### Adding New Endpoints - -1. **Add Swagger Annotations** to your handler functions: - -```go -// @Summary Endpoint summary -// @Description Detailed description -// @Tags tag-name -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param param-name param-type param-required "param description" -// @Success 200 {object} response.APIResponse{data=YourResponseType} "Success description" -// @Failure 400 {object} response.APIResponse "Error description" -// @Router /api/endpoint [method] -func (h *Handler) YourHandler(c echo.Context) error { - // Your handler implementation -} -``` - -2. **Regenerate Documentation**: - -```bash -make docs -``` - -### Swagger Annotation Examples - -#### Basic GET Endpoint -```go -// @Summary Get resource -// @Description Get a resource by ID -// @Tags resources -// @Accept json -// @Produce json -// @Param id path string true "Resource ID" -// @Success 200 {object} response.APIResponse{data=ResourceResponse} -// @Failure 404 {object} response.APIResponse -// @Router /api/resources/{id} [get] -``` - -#### POST with Request Body -```go -// @Summary Create resource -// @Description Create a new resource -// @Tags resources -// @Accept json -// @Produce json -// @Param resource body CreateResourceForm true "Resource data" -// @Success 201 {object} response.APIResponse{data=ResourceResponse} -// @Failure 400 {object} response.APIResponse -// @Router /api/resources [post] -``` - -#### Protected Endpoint -```go -// @Summary Protected endpoint -// @Description This endpoint requires authentication -// @Tags resources -// @Accept json -// @Produce json -// @Security BearerAuth -// @Success 200 {object} response.APIResponse -// @Failure 401 {object} response.APIResponse -// @Router /api/protected [get] -``` - -## 📁 File Structure - -``` -cmd/web/ -├── main.go # Main application entry point -├── bootstrap.go # Server initialization -└── docs/ # Generated Swagger documentation - ├── docs.go # Generated docs - ├── swagger.json # OpenAPI specification - └── swagger.yaml # OpenAPI specification (YAML) - -internal/customer/ -├── handler.go # HTTP handlers with Swagger annotations -├── form.go # Request/response forms -└── ... - -pkg/response/ -└── response.go # Standard API response types -``` - -## 🔧 Configuration - -### Swagger Configuration - -The main Swagger configuration is in `cmd/web/docs.go`: - -```go -// @title Tender Management API -// @version 1.0.0 -// @description This is the API documentation for the Tender Management System. -// @host localhost:8081 -// @BasePath /api/v1 -// @securityDefinitions.apikey BearerAuth -// @in header -// @name Authorization -``` - -### Server Configuration - -The Swagger endpoint is registered in `cmd/web/bootstrap.go`: - -```go -// Add Swagger documentation endpoint -e.GET("/swagger/*", echoSwagger.WrapHandler) -``` - -## 🧪 Testing - -### Using the Test Script - -```bash -# Run the test script -./test_swagger.sh -``` - -This script will: -1. Build the application -2. Start the server -3. Display all available endpoints -4. Provide access URLs - -### Manual Testing - -1. **Start the server**: - ```bash - make run-docs - ``` - -2. **Access Swagger UI**: - - Open http://localhost:8081/swagger/index.html - - Explore and test endpoints interactively - -3. **Test Health Check**: - ```bash - curl http://localhost:8081/health - ``` - -## 📝 Response Types - -### Standard Response Structure - -All API responses follow this structure: - -```json -{ - "success": true, - "message": "Operation successful", - "data": { - // Response data - }, - "meta": { - // Pagination metadata (if applicable) - }, - "error": { - // Error details (if applicable) - } -} -``` - -### Error Response - -```json -{ - "success": false, - "message": "Error message", - "error": { - "code": "ERROR_CODE", - "message": "Detailed error message", - "details": "Additional details" - } -} -``` - -## 🔐 Authentication - -### Bearer Token Authentication - -Most endpoints require Bearer token authentication: - -1. **Login** to get access token: - ```bash - curl -X POST http://localhost:8081/api/customers/login \ - -H "Content-Type: application/json" \ - -d '{"email": "user@example.com", "password": "password"}' - ``` - -2. **Use the token** in subsequent requests: - ```bash - curl -X GET http://localhost:8081/api/customers/profile \ - -H "Authorization: Bearer YOUR_ACCESS_TOKEN" - ``` - -## 🚨 Troubleshooting - -### Common Issues - -1. **Documentation not updating**: - ```bash - make clean - make docs - make build - ``` - -2. **Swagger annotations not recognized**: - - Ensure annotations are directly above the function - - Check for syntax errors in annotations - - Verify import paths are correct - -3. **Server won't start**: - - Check if port 8081 is available - - Verify MongoDB connection - - Check configuration files - -### Debug Commands - -```bash -# Check if swag is installed -swag --version - -# Generate docs with verbose output -swag init -g cmd/web/main.go -o cmd/web/docs --parseDependency - -# Check generated files -ls -la cmd/web/docs/ -``` - -## 📚 Additional Resources - -- [Swag Documentation](https://github.com/swaggo/swag) -- [Echo Swagger](https://github.com/swaggo/echo-swagger) -- [OpenAPI Specification](https://swagger.io/specification/) -- [Swagger UI](https://swagger.io/tools/swagger-ui/) - -## 🤝 Contributing - -When adding new endpoints: - -1. Add comprehensive Swagger annotations -2. Include all possible response codes -3. Provide meaningful examples -4. Test the documentation in Swagger UI -5. Update this documentation if needed - -## 📄 License - -This documentation is part of the Tender Management Backend project. \ No newline at end of file diff --git a/docs/setup/SWAGGER_SUCCESS.md b/docs/setup/SWAGGER_SUCCESS.md deleted file mode 100644 index b1d3c61..0000000 --- a/docs/setup/SWAGGER_SUCCESS.md +++ /dev/null @@ -1,177 +0,0 @@ -# ✅ Swagger API Documentation Successfully Implemented - -## 🎉 Implementation Complete - -The Swagger API documentation has been successfully implemented for the Tender Management Backend with comprehensive handler function comments. - -## 📋 What Was Accomplished - -### 1. ✅ Dependencies Added -- `github.com/swaggo/echo-swagger` - Echo Swagger integration -- `github.com/swaggo/swag/cmd/swag` - Swagger documentation generator -- `github.com/swaggo/files` - Swagger UI files - -### 2. ✅ Swagger Configuration -- Added main Swagger annotations to `cmd/web/main.go` -- Configured API metadata (title, version, description, contact info) -- Set up security definitions for Bearer token authentication -- Defined API tags for organization - -### 3. ✅ Handler Function Documentation -All customer handler functions now have comprehensive Swagger annotations: - -#### 🔐 Authentication Endpoints -- `POST /api/customers/login` - Customer login with credentials -- `POST /api/customers/refresh-token` - Refresh access token - -#### 👤 Customer Profile Endpoints (Protected) -- `GET /api/customers/profile` - Get customer profile -- `PUT /api/customers/profile` - Update customer profile -- `PUT /api/customers/change-password` - Change password - -#### 👥 Admin Endpoints (Admin Protected) -- `POST /api/admin/customers/register` - Register new customer -- `GET /api/admin/customers` - List customers with pagination -- `GET /api/admin/customers/{id}` - Get customer by ID - -### 4. ✅ Server Integration -- Added Swagger route to HTTP server (`/swagger/*`) -- Integrated with Echo framework -- Configured proper middleware - -### 5. ✅ Documentation Generation -- Generated comprehensive API documentation -- Created interactive Swagger UI -- Produced OpenAPI specification (JSON/YAML) - -## 🚀 How to Use - -### 1. Start the Server -```bash -# Build and run with documentation -make run-docs - -# Or manually -make build -./bin/web -``` - -### 2. Access Documentation -- **Swagger UI**: http://localhost:8081/swagger/index.html -- **Health Check**: http://localhost:8081/health -- **API JSON**: http://localhost:8081/swagger/doc.json - -### 3. Regenerate Documentation -```bash -# Regenerate after adding new endpoints -make docs - -# Or manually -swag init -g cmd/web/main.go -o cmd/web/docs -``` - -## 📊 Current API Endpoints - -### Available in Swagger Documentation: -1. `POST /api/customers/login` - Customer authentication -2. `POST /api/customers/refresh-token` - Token refresh -3. `GET /api/customers/profile` - Get profile (protected) -4. `PUT /api/customers/profile` - Update profile (protected) -5. `PUT /api/customers/change-password` - Change password (protected) -6. `POST /api/admin/customers/register` - Register customer (admin) -7. `GET /api/admin/customers` - List customers (admin) -8. `GET /api/admin/customers/{id}` - Get customer by ID (admin) - -## 🔧 Technical Details - -### Swagger Annotations Used -- `@Summary` - Brief endpoint description -- `@Description` - Detailed endpoint description -- `@Tags` - API grouping -- `@Accept` - Request content type -- `@Produce` - Response content type -- `@Security` - Authentication requirements -- `@Param` - Request parameters -- `@Success` - Success responses -- `@Failure` - Error responses -- `@Router` - Endpoint path and method - -### Response Types Documented -- `response.APIResponse` - Standard API response structure -- `customer.CustomerResponse` - Customer data response -- `customer.AuthResponse` - Authentication response -- Error responses for all HTTP status codes - -### Security Implementation -- Bearer token authentication -- Protected endpoints require valid JWT -- Admin endpoints require admin privileges - -## 🧪 Testing Results - -### ✅ Server Status -- MongoDB connection: ✅ Working -- HTTP server: ✅ Running on port 8081 -- Swagger UI: ✅ Accessible -- Health endpoint: ✅ Responding - -### ✅ Documentation Features -- Interactive API testing: ✅ Available -- Request/response examples: ✅ Included -- Authentication support: ✅ Configured -- Error documentation: ✅ Complete - -## 📁 File Structure - -``` -cmd/web/ -├── main.go # Main app with Swagger config -├── bootstrap.go # Server setup with Swagger route -└── docs/ # Generated documentation - ├── docs.go # Swagger docs - ├── swagger.json # OpenAPI spec - └── swagger.yaml # OpenAPI spec (YAML) - -internal/customer/ -├── handler.go # HTTP handlers with Swagger annotations -├── form.go # Request/response forms -└── ... - -pkg/response/ -└── response.go # Standard API response types -``` - -## 🎯 Next Steps - -### For Developers -1. **Add New Endpoints**: Follow the annotation pattern in `handler.go` -2. **Update Documentation**: Run `make docs` after changes -3. **Test in Swagger UI**: Use the interactive interface -4. **Maintain Examples**: Keep request/response examples current - -### For API Consumers -1. **Explore APIs**: Use Swagger UI for interactive testing -2. **Authentication**: Use Bearer token for protected endpoints -3. **Error Handling**: Check documented error responses -4. **Pagination**: Use documented pagination parameters - -## 🏆 Success Metrics - -- ✅ All handler functions documented -- ✅ Interactive API testing available -- ✅ Authentication properly configured -- ✅ Error responses documented -- ✅ Request/response examples included -- ✅ Server running successfully -- ✅ Documentation accessible via web interface - -## 📚 Resources - -- **Swagger UI**: http://localhost:8081/swagger/index.html -- **API Documentation**: See `SWAGGER_SETUP.md` -- **Test Script**: Use `./test_swagger.sh` -- **Makefile**: Use `make docs` and `make run-docs` - ---- - -**Status**: ✅ **COMPLETE** - Swagger API documentation successfully implemented with comprehensive handler function comments. \ No newline at end of file diff --git a/infra/config.go b/infra/config.go index 86f2f63..f8a0c93 100644 --- a/infra/config.go +++ b/infra/config.go @@ -8,16 +8,17 @@ import ( // Config holds all configuration for the application type Config struct { - Server ServerConfig `mapstructure:"server"` - Database DatabaseConfig `mapstructure:"database"` - Cache CacheConfig `mapstructure:"cache"` - Queue QueueConfig `mapstructure:"queue"` - Search SearchConfig `mapstructure:"search"` - Auth AuthConfig `mapstructure:"auth"` - AI AIConfig `mapstructure:"ai"` - Scraping ScrapingConfig `mapstructure:"scraping"` - Logging LoggingConfig `mapstructure:"logging"` - RateLimit RateLimitConfig `mapstructure:"rate_limiting"` + Server ServerConfig `mapstructure:"server"` + Database DatabaseConfig `mapstructure:"database"` + Cache CacheConfig `mapstructure:"cache"` + Queue QueueConfig `mapstructure:"queue"` + Search SearchConfig `mapstructure:"search"` + UserAuthorization AuthConfig `mapstructure:"user_authorization"` + CustomerAuthorization AuthConfig `mapstructure:"customer_authorization"` + AI AIConfig `mapstructure:"ai"` + Scraping ScrapingConfig `mapstructure:"scraping"` + Logging LoggingConfig `mapstructure:"logging"` + RateLimit RateLimitConfig `mapstructure:"rate_limiting"` } type ServerConfig struct { diff --git a/internal/customer/entity.go b/internal/customer/entity.go new file mode 100644 index 0000000..3cdb178 --- /dev/null +++ b/internal/customer/entity.go @@ -0,0 +1,186 @@ +package customer + +import ( + "github.com/google/uuid" +) + +// CustomerStatus represents customer account status +type CustomerStatus string + +const ( + CustomerStatusActive CustomerStatus = "active" + CustomerStatusInactive CustomerStatus = "inactive" + CustomerStatusSuspended CustomerStatus = "suspended" + CustomerStatusPending CustomerStatus = "pending" +) + +// CustomerType represents the type of customer +type CustomerType string + +const ( + CustomerTypeIndividual CustomerType = "individual" + CustomerTypeCompany CustomerType = "company" + CustomerTypeGovernment CustomerType = "government" +) + +// Customer represents a customer in the tender management system +type Customer struct { + ID uuid.UUID `bson:"_id"` + Type CustomerType `bson:"type"` + Status CustomerStatus `bson:"status"` + CompanyID *uuid.UUID `bson:"company_id,omitempty"` + + // Individual customer fields + FirstName *string `bson:"first_name,omitempty"` + LastName *string `bson:"last_name,omitempty"` + FullName *string `bson:"full_name,omitempty"` + Username string `bson:"username"` // Username for authentication + Email string `bson:"email"` + Password string `bson:"password"` // Hashed password for authentication + Phone *string `bson:"phone,omitempty"` + Mobile *string `bson:"mobile,omitempty"` + + // Company customer fields + CompanyName *string `bson:"company_name,omitempty"` + RegistrationNumber *string `bson:"registration_number,omitempty"` + TaxID *string `bson:"tax_id,omitempty"` + Industry *string `bson:"industry,omitempty"` + + // Address information + Address *Address `bson:"address,omitempty"` + + // Business information + BusinessType *string `bson:"business_type,omitempty"` + EmployeeCount *int `bson:"employee_count,omitempty"` + AnnualRevenue *float64 `bson:"annual_revenue,omitempty"` + FoundedYear *int `bson:"founded_year,omitempty"` + + // Contact person (for company customers) + ContactPerson *ContactPerson `bson:"contact_person,omitempty"` + + // Verification and compliance + IsVerified bool `bson:"is_verified"` + IsCompliant bool `bson:"is_compliant"` + ComplianceNotes *string `bson:"compliance_notes,omitempty"` + + // Preferences and settings + Language string `bson:"language"` // Default: "en" + Currency string `bson:"currency"` // Default: "USD" + Timezone string `bson:"timezone"` // Default: "UTC" + + // Audit fields + CreatedAt int64 `bson:"created_at"` // Unix timestamp + UpdatedAt int64 `bson:"updated_at"` // Unix timestamp + CreatedBy *uuid.UUID `bson:"created_by,omitempty"` + UpdatedBy *uuid.UUID `bson:"updated_by,omitempty"` +} + +// Address represents a customer's address +type Address struct { + Street string `bson:"street"` + City string `bson:"city"` + State string `bson:"state"` + PostalCode string `bson:"postal_code"` + Country string `bson:"country"` + AddressType string `bson:"address_type"` // billing, shipping, etc. + IsDefault bool `bson:"is_default"` +} + +// ContactPerson represents a contact person for company customers +type ContactPerson struct { + FirstName string `bson:"first_name"` + LastName string `bson:"last_name"` + FullName string `bson:"full_name"` + Position string `bson:"position"` + Email string `bson:"email"` + Phone string `bson:"phone"` + Mobile *string `bson:"mobile,omitempty"` + IsPrimary bool `bson:"is_primary"` +} + +// CustomerResponse represents the customer data sent in API responses +type CustomerResponse struct { + ID string `json:"id"` + Type string `json:"type"` + Status string `json:"status"` + CompanyID *string `json:"company_id,omitempty"` + FirstName *string `json:"first_name,omitempty"` + LastName *string `json:"last_name,omitempty"` + FullName *string `json:"full_name,omitempty"` + Email string `json:"email"` + Phone *string `json:"phone,omitempty"` + Mobile *string `json:"mobile,omitempty"` + CompanyName *string `json:"company_name,omitempty"` + 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"` + IsVerified bool `json:"is_verified"` + IsCompliant bool `json:"is_compliant"` + ComplianceNotes *string `json:"compliance_notes,omitempty"` + 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"` +} + +// ToResponse converts Customer to CustomerResponse +func (c *Customer) ToResponse() *CustomerResponse { + companyID := "" + if c.CompanyID != nil { + companyID = c.CompanyID.String() + } + + createdBy := "" + if c.CreatedBy != nil { + createdBy = c.CreatedBy.String() + } + + updatedBy := "" + if c.UpdatedBy != nil { + updatedBy = c.UpdatedBy.String() + } + + return &CustomerResponse{ + ID: c.ID.String(), + Type: string(c.Type), + Status: string(c.Status), + CompanyID: &companyID, + FirstName: c.FirstName, + LastName: c.LastName, + FullName: c.FullName, + Username: c.Username, + Email: c.Email, + Phone: c.Phone, + Mobile: c.Mobile, + CompanyName: c.CompanyName, + RegistrationNumber: c.RegistrationNumber, + TaxID: c.TaxID, + Industry: c.Industry, + Address: c.Address, + BusinessType: c.BusinessType, + EmployeeCount: c.EmployeeCount, + AnnualRevenue: c.AnnualRevenue, + FoundedYear: c.FoundedYear, + ContactPerson: c.ContactPerson, + IsVerified: c.IsVerified, + IsCompliant: c.IsCompliant, + ComplianceNotes: c.ComplianceNotes, + Language: c.Language, + Currency: c.Currency, + Timezone: c.Timezone, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + CreatedBy: &createdBy, + UpdatedBy: &updatedBy, + } +} diff --git a/internal/customer/form.go b/internal/customer/form.go new file mode 100644 index 0000000..f64b10a --- /dev/null +++ b/internal/customer/form.go @@ -0,0 +1,201 @@ +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"` + + // Individual customer fields + FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` + LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"` + FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` + Username string `json:"username" valid:"required,alphanum,length(3|30)"` + Email string `json:"email" valid:"required,email"` + Password string `json:"password" valid:"required,length(8|128)"` + Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` + Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"` + + // Company customer fields + CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"` + RegistrationNumber *string `json:"registration_number,omitempty" valid:"optional,length(5|50)"` + TaxID *string `json:"tax_id,omitempty" valid:"optional,length(5|50)"` + Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` + + // Address information + Address *AddressForm `json:"address,omitempty"` + + // Business information + BusinessType *string `json:"business_type,omitempty" valid:"optional,length(2|100)"` + EmployeeCount *int `json:"employee_count,omitempty" valid:"optional,range(1|100000)"` + AnnualRevenue *float64 `json:"annual_revenue,omitempty" valid:"optional,range(0|999999999999)"` + FoundedYear *int `json:"founded_year,omitempty" valid:"optional,range(1800|2100)"` + + // Contact person (for company customers) + ContactPerson *ContactPersonForm `json:"contact_person,omitempty"` + + // Preferences and settings + Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"` + Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"` + Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"` +} + +// 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"` + + // Individual customer fields + FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` + LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"` + FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` + Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)"` + Email *string `json:"email,omitempty" valid:"optional,email"` + Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` + Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"` + + // Company customer fields + CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"` + RegistrationNumber *string `json:"registration_number,omitempty" valid:"optional,length(5|50)"` + TaxID *string `json:"tax_id,omitempty" valid:"optional,length(5|50)"` + Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` + + // Address information + Address *AddressForm `json:"address,omitempty"` + + // Business information + BusinessType *string `json:"business_type,omitempty" valid:"optional,length(2|100)"` + EmployeeCount *int `json:"employee_count,omitempty" valid:"optional,range(1|100000)"` + AnnualRevenue *float64 `json:"annual_revenue,omitempty" valid:"optional,range(0|999999999999)"` + FoundedYear *int `json:"founded_year,omitempty" valid:"optional,range(1800|2100)"` + + // Contact person (for company customers) + ContactPerson *ContactPersonForm `json:"contact_person,omitempty"` + + // Preferences and settings + Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"` + Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"` + Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"` +} + +// ListCustomersForm represents the form for listing customers with filters +type ListCustomersForm struct { + Search *string `query:"search" valid:"optional"` + Type *string `query:"type" valid:"optional,in(individual|company|government)"` + Status *string `query:"status" valid:"optional,in(active|inactive|suspended|pending)"` + CompanyID *string `query:"company_id" valid:"optional,uuid"` + Industry *string `query:"industry" valid:"optional"` + IsVerified *bool `query:"is_verified" valid:"optional"` + IsCompliant *bool `query:"is_compliant" valid:"optional"` + Language *string `query:"language" valid:"optional"` + Currency *string `query:"currency" valid:"optional"` + Limit *int `query:"limit" valid:"optional,range(1|100)"` + Offset *int `query:"offset" valid:"optional,min(0)"` + SortBy *string `query:"sort_by" valid:"optional,in(email|company_name|created_at|updated_at|status)"` + SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"` +} + +// UpdateCustomerStatusForm represents the form for updating customer status +type UpdateCustomerStatusForm struct { + Status string `json:"status" valid:"required,in(active|inactive|suspended|pending)"` +} + +// UpdateCustomerVerificationForm represents the form for updating customer verification +type UpdateCustomerVerificationForm struct { + IsVerified bool `json:"is_verified"` + IsCompliant bool `json:"is_compliant"` + ComplianceNotes *string `json:"compliance_notes,omitempty" valid:"optional,length(0|1000)"` +} + +// AddressForm represents the form for customer address +type AddressForm struct { + Street string `json:"street" valid:"required,length(5|200)"` + City string `json:"city" valid:"required,length(2|100)"` + State string `json:"state" valid:"required,length(2|100)"` + PostalCode string `json:"postal_code" valid:"required,length(3|20)"` + Country string `json:"country" valid:"required,length(2|100)"` + AddressType string `json:"address_type" valid:"required,in(billing|shipping|both)"` + IsDefault bool `json:"is_default"` +} + +// ContactPersonForm represents the form for contact person +type ContactPersonForm struct { + FirstName string `json:"first_name" valid:"required,length(2|50)"` + LastName string `json:"last_name" valid:"required,length(2|50)"` + FullName string `json:"full_name" valid:"required,length(2|100)"` + Position string `json:"position" valid:"required,length(2|100)"` + Email string `json:"email" valid:"required,email"` + Phone string `json:"phone" valid:"required,length(10|20)"` + Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"` + IsPrimary bool `json:"is_primary"` +} + +// CustomerListResponse represents the response for listing customers +type CustomerListResponse struct { + Customers []*CustomerResponse `json:"customers"` + Total int64 `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` + TotalPages int `json:"total_pages"` +} + +// Mobile-specific forms (simplified for mobile app) +type CreateCustomerMobileForm struct { + Type string `json:"type" valid:"required,in(individual|company)"` + FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` + LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"` + FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` + Username string `json:"username" valid:"required,alphanum,length(3|30)"` + Email string `json:"email" valid:"required,email"` + Password string `json:"password" valid:"required,length(8|128)"` + Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` + Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"` + CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"` + Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` +} + +type UpdateCustomerMobileForm struct { + FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` + LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"` + FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` + Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` + Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"` + CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"` + Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` +} + +// Customer Authentication DTOs +type LoginForm struct { + Username string `json:"username" valid:"required"` + Password string `json:"password" valid:"required"` +} + +type RefreshTokenForm struct { + RefreshToken string `json:"refresh_token" valid:"required"` +} + +type ChangePasswordForm struct { + OldPassword string `json:"old_password" valid:"required"` + NewPassword string `json:"new_password" valid:"required,length(8|128)"` +} + +type UpdateProfileForm struct { + FirstName *string `json:"first_name,omitempty" valid:"optional,length(2|50)"` + LastName *string `json:"last_name,omitempty" valid:"optional,length(2|50)"` + FullName *string `json:"full_name,omitempty" valid:"optional,length(2|100)"` + Username *string `json:"username,omitempty" valid:"optional,alphanum,length(3|30)"` + Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` + Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"` + CompanyName *string `json:"company_name,omitempty" valid:"optional,length(2|200)"` + Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` + Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"` + Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"` + Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"` +} + +// Customer Authentication Response DTOs +type AuthResponse struct { + Customer *CustomerResponse `json:"customer"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresAt int64 `json:"expires_at"` +} diff --git a/internal/customer/handler.go b/internal/customer/handler.go new file mode 100644 index 0000000..13fba41 --- /dev/null +++ b/internal/customer/handler.go @@ -0,0 +1,741 @@ +package customer + +import ( + "strconv" + "tm/internal/user" + "tm/pkg/authorization" + "tm/pkg/logger" + "tm/pkg/response" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" +) + +// Handler handles HTTP requests for customer operations +type Handler struct { + service Service + authService authorization.AuthorizationService + userHandler *user.Handler + logger logger.Logger +} + +// NewHandler creates a new customer handler +func NewHandler(service Service, userHandler *user.Handler, authService authorization.AuthorizationService, logger logger.Logger) *Handler { + return &Handler{ + service: service, + authService: authService, + userHandler: userHandler, + logger: logger, + } +} + +// RegisterRoutes registers customer routes +func (h *Handler) RegisterRoutes(e *echo.Echo) { + + adminV1 := e.Group("/admin/v1/customers") + adminV1.Use(h.userHandler.AuthMiddleware()) + adminV1.POST("", h.CreateCustomer) + adminV1.GET("", h.ListCustomers) + adminV1.GET("/:id", h.GetCustomerByID) + adminV1.PUT("/:id", h.UpdateCustomer) + adminV1.DELETE("/:id", h.DeleteCustomer) + adminV1.PATCH("/:id/status", h.UpdateCustomerStatus) + adminV1.PATCH("/:id/verification", h.UpdateCustomerVerification) + adminV1.POST("/:id/verify", h.VerifyCustomer) + adminV1.POST("/:id/suspend", h.SuspendCustomer) + adminV1.POST("/:id/activate", h.ActivateCustomer) + adminV1.GET("/company/:companyId", h.GetCustomersByCompanyID) + adminV1.GET("/type/:type", h.GetCustomersByType) + adminV1.GET("/status/:status", h.GetCustomersByStatus) + + // Mobile-specific endpoints + v1 := e.Group("/api/v1") + authorizationGP := v1.Group("") + authorizationGP.POST("/login", h.Login) + authorizationGP.POST("/refresh-token", h.RefreshToken) + + profileGP := v1.Group("") + profileGP.Use(h.AuthMiddleware()) + profileGP.GET("/profile", h.GetProfile) + profileGP.DELETE("/logout", h.Logout) + +} + +// CreateCustomer creates a new customer (Web Panel) +// @Summary Create a new customer +// @Description Create a new customer with comprehensive information including personal details, company information, address, and business details. This endpoint is used by the web panel for full customer registration. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param customer body CreateCustomerForm true "Customer information including type (individual|company|government), personal details, company info, address, and business details" +// @Success 201 {object} response.APIResponse{data=CustomerResponse} "Customer created successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 409 {object} response.APIResponse "Conflict - Customer with this email/company already exists" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers [post] +func (h *Handler) CreateCustomer(c echo.Context) error { + form, err := response.Parse[CreateCustomerForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // TODO: Get user ID from JWT token + createdBy := uuid.New() // This should come from JWT token + + customer, err := h.service.CreateCustomer(c.Request().Context(), form, &createdBy) + if err != nil { + if err.Error() == "customer with this email already exists" || + err.Error() == "company with this name already exists" || + err.Error() == "company with this registration number already exists" || + err.Error() == "company with this tax ID already exists" { + return response.Conflict(c, err.Error()) + } + return response.InternalServerError(c, "Failed to create customer") + } + + return response.Created(c, customer.ToResponse(), "Customer created successfully") +} + +// GetCustomerByID retrieves a customer by ID +// @Summary Get customer by ID +// @Description Retrieve detailed customer information by unique customer ID. Returns comprehensive customer data including personal details, company information, address, verification status, and audit fields. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer UUID" format(uuid) +// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Customer retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID format" +// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id} [get] +func (h *Handler) GetCustomerByID(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return response.BadRequest(c, "Invalid customer ID", err.Error()) + } + + customer, err := h.service.GetCustomerByID(c.Request().Context(), id) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to retrieve customer") + } + + return response.Success(c, customer.ToResponse(), "Customer retrieved successfully") +} + +// UpdateCustomer updates a customer (Web Panel) +// @Summary Update customer information +// @Description Update existing customer information with comprehensive details. All fields are optional and only provided fields will be updated. This endpoint is used by the web panel for full customer management. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer UUID" format(uuid) +// @Param customer body UpdateCustomerForm true "Customer update information - all fields are optional" +// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Customer updated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID or input data" +// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist" +// @Failure 409 {object} response.APIResponse "Conflict - Customer with this email/company already exists" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id} [put] +func (h *Handler) UpdateCustomer(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return response.BadRequest(c, "Invalid customer ID", err.Error()) + } + + form, err := response.Parse[UpdateCustomerForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // TODO: Get user ID from JWT token + updatedBy := uuid.New() // This should come from JWT token + + customer, err := h.service.UpdateCustomer(c.Request().Context(), id, form, &updatedBy) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + if err.Error() == "customer with this email already exists" || + err.Error() == "company with this name already exists" || + err.Error() == "company with this registration number already exists" || + err.Error() == "company with this tax ID already exists" { + return response.Conflict(c, err.Error()) + } + return response.InternalServerError(c, "Failed to update customer") + } + + return response.Success(c, customer.ToResponse(), "Customer updated successfully") +} + +// DeleteCustomer deletes a customer (soft delete) +// @Summary Delete customer +// @Description Soft delete a customer by setting status to inactive. The customer record is preserved but marked as deleted. This is a reversible operation. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer UUID" format(uuid) +// @Success 200 {object} response.APIResponse "Customer deleted successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID format" +// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id} [delete] +func (h *Handler) DeleteCustomer(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return response.BadRequest(c, "Invalid customer ID", err.Error()) + } + + // TODO: Get user ID from JWT token + deletedBy := uuid.New() // This should come from JWT token + + err = h.service.DeleteCustomer(c.Request().Context(), id, &deletedBy) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to delete customer") + } + + return response.Success(c, nil, "Customer deleted successfully") +} + +// ListCustomers lists customers with filters and pagination +// @Summary List customers with filters and pagination +// @Description Retrieve a paginated list of customers with advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination. +// @Tags Customers-Admin +// @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 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 [get] +func (h *Handler) ListCustomers(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.ListCustomers(c.Request().Context(), form) + if err != nil { + return response.InternalServerError(c, "Failed to list customers") + } + + return response.Success(c, result, "Customers retrieved successfully") +} + +// UpdateCustomerStatus updates customer status +// @Summary Update customer status +// @Description Update the status of a customer account. Valid statuses include active, inactive, suspended, and pending. This affects the customer's ability to access the system. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer UUID" format(uuid) +// @Param status body UpdateCustomerStatusForm true "Status update information" +// @Success 200 {object} response.APIResponse "Customer status updated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID or status" +// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid status value" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id}/status [patch] +func (h *Handler) UpdateCustomerStatus(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return response.BadRequest(c, "Invalid customer ID", err.Error()) + } + + form, err := response.Parse[UpdateCustomerStatusForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // TODO: Get user ID from JWT token + updatedBy := uuid.New() // This should come from JWT token + + err = h.service.UpdateCustomerStatus(c.Request().Context(), id, form, &updatedBy) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to update customer status") + } + + return response.Success(c, nil, "Customer status updated successfully") +} + +// UpdateCustomerVerification updates customer verification status +// @Summary Update customer verification and compliance status +// @Description Update the verification and compliance status of a customer. This includes marking customers as verified and compliant, with optional compliance notes for audit purposes. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer UUID" format(uuid) +// @Param verification body UpdateCustomerVerificationForm true "Verification and compliance update information" +// @Success 200 {object} response.APIResponse "Customer verification updated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID" +// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid verification data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id}/verification [patch] +func (h *Handler) UpdateCustomerVerification(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return response.BadRequest(c, "Invalid customer ID", err.Error()) + } + + form, err := response.Parse[UpdateCustomerVerificationForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // TODO: Get user ID from JWT token + updatedBy := uuid.New() // This should come from JWT token + + err = h.service.UpdateCustomerVerification(c.Request().Context(), id, form, &updatedBy) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to update customer verification") + } + + return response.Success(c, nil, "Customer verification updated successfully") +} + +// VerifyCustomer verifies a customer +// @Summary Verify customer +// @Description Mark a customer as verified. This is a simple verification action that sets the customer's verification status to true. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer UUID" format(uuid) +// @Success 200 {object} response.APIResponse "Customer verified successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID" +// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id}/verify [post] +func (h *Handler) VerifyCustomer(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return response.BadRequest(c, "Invalid customer ID", err.Error()) + } + + // TODO: Get user ID from JWT token + verifiedBy := uuid.New() // This should come from JWT token + + err = h.service.VerifyCustomer(c.Request().Context(), id, &verifiedBy) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to verify customer") + } + + return response.Success(c, nil, "Customer verified successfully") +} + +// SuspendCustomer suspends a customer +// @Summary Suspend customer account +// @Description Suspend a customer account by setting their status to suspended. Suspended customers cannot access the system. A reason for suspension must be provided. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer UUID" format(uuid) +// @Param reason body object true "Suspension reason" SchemaExample({"reason": "Violation of terms of service"}) +// @Success 200 {object} response.APIResponse "Customer suspended successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID or missing reason" +// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id}/suspend [post] +func (h *Handler) SuspendCustomer(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return response.BadRequest(c, "Invalid customer ID", err.Error()) + } + + var requestBody map[string]string + if err := c.Bind(&requestBody); err != nil { + return response.BadRequest(c, "Invalid request body", err.Error()) + } + + reason := requestBody["reason"] + if reason == "" { + return response.BadRequest(c, "Suspension reason is required", "") + } + + // TODO: Get user ID from JWT token + suspendedBy := uuid.New() // This should come from JWT token + + err = h.service.SuspendCustomer(c.Request().Context(), id, reason, &suspendedBy) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to suspend customer") + } + + return response.Success(c, nil, "Customer suspended successfully") +} + +// ActivateCustomer activates a customer +// @Summary Activate suspended customer account +// @Description Reactivate a suspended customer account by setting their status back to active. This reverses the suspension action. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param id path string true "Customer UUID" format(uuid) +// @Success 200 {object} response.APIResponse "Customer activated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer ID" +// @Failure 404 {object} response.APIResponse "Not found - Customer does not exist" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/{id}/activate [post] +func (h *Handler) ActivateCustomer(c echo.Context) error { + idStr := c.Param("id") + id, err := uuid.Parse(idStr) + if err != nil { + return response.BadRequest(c, "Invalid customer ID", err.Error()) + } + + // TODO: Get user ID from JWT token + activatedBy := uuid.New() // This should come from JWT token + + err = h.service.ActivateCustomer(c.Request().Context(), id, &activatedBy) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to activate customer") + } + + return response.Success(c, nil, "Customer activated successfully") +} + +// GetCustomersByCompanyID retrieves customers by company ID +// @Summary Get customers by company ID +// @Description Retrieve all customers associated with a specific company. This is useful for company administrators to see all users within their organization. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param companyId path string true "Company UUID" format(uuid) +// @Param limit query integer false "Number of customers per page (1-100)" minimum(1) maximum(100) default(20) +// @Param offset query integer false "Number of customers to skip for pagination" minimum(0) default(0) +// @Success 200 {object} response.APIResponse{data=[]CustomerResponse,meta=response.Meta} "Customers retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID format" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/company/{companyId} [get] +func (h *Handler) GetCustomersByCompanyID(c echo.Context) error { + companyIDStr := c.Param("companyId") + companyID, err := uuid.Parse(companyIDStr) + if err != nil { + return response.BadRequest(c, "Invalid company ID", err.Error()) + } + + limit := 20 + if limitStr := c.QueryParam("limit"); limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 { + limit = parsedLimit + } + } + + offset := 0 + if offsetStr := c.QueryParam("offset"); offsetStr != "" { + if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 { + offset = parsedOffset + } + } + + customers, total, err := h.service.GetCustomersByCompanyID(c.Request().Context(), companyID, limit, offset) + if err != nil { + return response.InternalServerError(c, "Failed to retrieve customers by company") + } + + var customerResponses []*CustomerResponse + for _, customer := range customers { + customerResponses = append(customerResponses, customer.ToResponse()) + } + + meta := &response.Meta{ + Total: int(total), + Limit: limit, + Offset: offset, + } + + return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully") +} + +// GetCustomersByType retrieves customers by type +// @Summary Get customers by type +// @Description Retrieve customers filtered by their type (individual, company, or government). Useful for administrative reporting and management. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param type path string true "Customer type" Enums(individual, company, government) +// @Param limit query integer false "Number of customers per page (1-100)" minimum(1) maximum(100) default(20) +// @Param offset query integer false "Number of customers to skip for pagination" minimum(0) default(0) +// @Success 200 {object} response.APIResponse{data=[]CustomerResponse,meta=response.Meta} "Customers retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer type" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/type/{type} [get] +func (h *Handler) GetCustomersByType(c echo.Context) error { + customerTypeStr := c.Param("type") + customerType := CustomerType(customerTypeStr) + + // Validate customer type + if customerType != CustomerTypeIndividual && customerType != CustomerTypeCompany && customerType != CustomerTypeGovernment { + return response.BadRequest(c, "Invalid customer type", "Must be individual, company, or government") + } + + limit := 20 + if limitStr := c.QueryParam("limit"); limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 { + limit = parsedLimit + } + } + + offset := 0 + if offsetStr := c.QueryParam("offset"); offsetStr != "" { + if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 { + offset = parsedOffset + } + } + + customers, total, err := h.service.GetCustomersByType(c.Request().Context(), customerType, limit, offset) + if err != nil { + return response.InternalServerError(c, "Failed to retrieve customers by type") + } + + var customerResponses []*CustomerResponse + for _, customer := range customers { + customerResponses = append(customerResponses, customer.ToResponse()) + } + + meta := &response.Meta{ + Total: int(total), + Limit: limit, + Offset: offset, + } + + return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully") +} + +// GetCustomersByStatus retrieves customers by status +// @Summary Get customers by status +// @Description Retrieve customers filtered by their account status (active, inactive, suspended, or pending). Useful for administrative monitoring and management. +// @Tags Customers-Admin +// @Accept json +// @Produce json +// @Param status path string true "Customer status" Enums(active, inactive, suspended, pending) +// @Param limit query integer false "Number of customers per page (1-100)" minimum(1) maximum(100) default(20) +// @Param offset query integer false "Number of customers to skip for pagination" minimum(0) default(0) +// @Success 200 {object} response.APIResponse{data=[]CustomerResponse,meta=response.Meta} "Customers retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid customer status" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/customers/status/{status} [get] +func (h *Handler) GetCustomersByStatus(c echo.Context) error { + statusStr := c.Param("status") + status := CustomerStatus(statusStr) + + // Validate customer status + if status != CustomerStatusActive && status != CustomerStatusInactive && status != CustomerStatusSuspended && status != CustomerStatusPending { + return response.BadRequest(c, "Invalid customer status", "Must be active, inactive, suspended, or pending") + } + + limit := 20 + if limitStr := c.QueryParam("limit"); limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 { + limit = parsedLimit + } + } + + offset := 0 + if offsetStr := c.QueryParam("offset"); offsetStr != "" { + if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 { + offset = parsedOffset + } + } + + customers, total, err := h.service.GetCustomersByStatus(c.Request().Context(), status, limit, offset) + if err != nil { + return response.InternalServerError(c, "Failed to retrieve customers by status") + } + + var customerResponses []*CustomerResponse + for _, customer := range customers { + customerResponses = append(customerResponses, customer.ToResponse()) + } + + meta := &response.Meta{ + Total: int(total), + Limit: limit, + Offset: offset, + } + + return response.SuccessWithMeta(c, customerResponses, meta, "Customers retrieved successfully") +} + +// Login handles customer authentication +// @Summary Customer login +// @Description Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication. +// @Tags Customers-Authorization +// @Accept json +// @Produce json +// @Param login body LoginForm true "Login credentials (username/email and password)" +// @Success 200 {object} response.APIResponse{data=AuthResponse} "Login successful" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @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] +func (h *Handler) Login(c echo.Context) error { + form, err := response.Parse[LoginForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Call service + authResponse, err := h.service.Login(c.Request().Context(), form) + if err != nil { + if err.Error() == "invalid credentials" { + return response.Unauthorized(c, err.Error()) + } + if err.Error() == "account is not active" { + return response.Unauthorized(c, err.Error()) + } + return response.InternalServerError(c, "Failed to authenticate customer") + } + + return response.Success(c, authResponse, "Login successful") +} + +// RefreshToken handles customer token refresh +// @Summary Refresh customer access token +// @Description Refresh access token using a valid refresh token. This allows customers to maintain their session without re-authentication. +// @Tags Customers-Authorization +// @Accept json +// @Produce json +// @Param refresh body RefreshTokenForm true "Refresh token for generating new access token" +// @Success 200 {object} response.APIResponse{data=AuthResponse} "Token refreshed successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid refresh token or feature not implemented" +// @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] +func (h *Handler) RefreshToken(c echo.Context) error { + form, err := response.Parse[RefreshTokenForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Call service + authResponse, err := h.service.RefreshToken(c.Request().Context(), form.RefreshToken) + if err != nil { + if err.Error() == "token refresh not implemented yet" { + return response.BadRequest(c, "Token refresh not implemented yet", "") + } + return response.InternalServerError(c, "Failed to refresh token") + } + + return response.Success(c, authResponse, "Token refreshed successfully") +} + +// GetProfile retrieves customer profile information +// @Summary Get customer profile +// @Description Retrieve authenticated customer's profile information. This endpoint requires a valid access token and returns the customer's own profile data. +// @Tags Customers-Authorization +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} response.APIResponse{data=CustomerResponse} "Profile retrieved successfully" +// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing access token" +// @Failure 404 {object} response.APIResponse "Not found - Customer profile not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Router /api/v1/profile [get] +func (h *Handler) GetProfile(c echo.Context) error { + // Get customer ID from context (set by AuthMiddleware) + customerID, err := GetCustomerIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "Customer ID not found in context") + } + + // Call service + customer, err := h.service.GetProfile(c.Request().Context(), customerID) + if err != nil { + if err.Error() == "customer not found" { + return response.NotFound(c, "Customer not found") + } + return response.InternalServerError(c, "Failed to retrieve customer profile") + } + + return response.Success(c, customer.ToResponse(), "Profile retrieved successfully") +} + +// Logout handles customer logout +// @Summary Customer logout +// @Description Logout customer and invalidate access token. This ensures the token cannot be used for future requests. +// @Tags Customers-Authorization +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} response.APIResponse "Logout successful" +// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or missing access token" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Router /api/v1/logout [delete] +func (h *Handler) Logout(c echo.Context) error { + // Get customer ID from context (set by AuthMiddleware) + customerID, err := GetCustomerIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "Customer ID not found in context") + } + + // Get access token from context + accessToken, err := GetAccessTokenFromContext(c) + if err != nil { + return response.Unauthorized(c, "Access token not found in context") + } + + // Call service + err = h.service.Logout(c.Request().Context(), customerID, accessToken) + if err != nil { + return response.InternalServerError(c, "Failed to logout customer") + } + + return response.Success(c, nil, "Logout successful") +} diff --git a/internal/customer/middleware.go b/internal/customer/middleware.go new file mode 100644 index 0000000..f004c75 --- /dev/null +++ b/internal/customer/middleware.go @@ -0,0 +1,84 @@ +package customer + +import ( + "net/http" + "strings" + "tm/pkg/response" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" +) + +// AuthMiddleware validates JWT access tokens and extracts customer information +func (h *Handler) AuthMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // Extract token from Authorization header + authHeader := c.Request().Header.Get("Authorization") + if authHeader == "" { + return response.Unauthorized(c, "Authorization header required") + } + + // Check if it's a Bearer token + if !strings.HasPrefix(authHeader, "Bearer ") { + return response.Unauthorized(c, "Invalid authorization format. Use 'Bearer '") + } + + // Extract the token + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + + // Validate the access token using authorization service + validationResult, err := h.authService.ValidateAccessToken(tokenString) + if err != nil { + h.logger.Error("Token validation error", map[string]interface{}{ + "error": err.Error(), + }) + return response.Unauthorized(c, "Invalid token") + } + + if !validationResult.Valid { + if validationResult.Expired { + return response.Unauthorized(c, "Token expired") + } + return response.Unauthorized(c, validationResult.Error) + } + + // Extract customer information from token + customerID, err := uuid.Parse(validationResult.Claims.UserID) + if err != nil { + h.logger.Error("Failed to parse customer ID from token", map[string]interface{}{ + "error": err.Error(), + }) + return response.Unauthorized(c, "Invalid token format") + } + + // Store customer information in context for handlers to use + c.Set("customer_id", customerID) + c.Set("customer_email", validationResult.Claims.Email) + c.Set("customer_role", validationResult.Claims.Role) + c.Set("company_id", validationResult.Claims.CompanyID) + c.Set("access_token", tokenString) + + // Continue to next handler + return next(c) + } + } +} + +// GetCustomerIDFromContext extracts customer ID from Echo context +func GetCustomerIDFromContext(c echo.Context) (uuid.UUID, error) { + customerID, ok := c.Get("customer_id").(uuid.UUID) + if !ok { + return uuid.Nil, echo.NewHTTPError(http.StatusUnauthorized, "Customer ID not found in context") + } + return customerID, nil +} + +// GetAccessTokenFromContext extracts access token from Echo context +func GetAccessTokenFromContext(c echo.Context) (string, error) { + accessToken, ok := c.Get("access_token").(string) + if !ok { + return "", echo.NewHTTPError(http.StatusUnauthorized, "Access token not found in context") + } + return accessToken, nil +} diff --git a/internal/customer/repository.go b/internal/customer/repository.go new file mode 100644 index 0000000..80bbfa3 --- /dev/null +++ b/internal/customer/repository.go @@ -0,0 +1,525 @@ +package customer + +import ( + "context" + "errors" + "time" + "tm/pkg/logger" + mongopkg "tm/pkg/mongo" + + "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// Repository defines the interface for customer data operations +type Repository interface { + Create(ctx context.Context, customer *Customer) error + GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) + GetByEmail(ctx context.Context, email string) (*Customer, error) + GetByUsername(ctx context.Context, username string) (*Customer, error) + GetByCompanyName(ctx context.Context, companyName string) (*Customer, error) + GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) + GetByTaxID(ctx context.Context, taxID string) (*Customer, error) + GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) + Update(ctx context.Context, customer *Customer) error + Delete(ctx context.Context, id uuid.UUID) error + List(ctx context.Context, limit, offset int) ([]*Customer, error) + Search(ctx context.Context, search string, customerType *string, status *string, companyID *uuid.UUID, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) + CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) + CountByType(ctx context.Context, customerType CustomerType) (int64, error) + CountByStatus(ctx context.Context, status CustomerStatus) (int64, error) + UpdateStatus(ctx context.Context, id uuid.UUID, status CustomerStatus) error + UpdateVerification(ctx context.Context, id uuid.UUID, isVerified, isCompliant bool, complianceNotes *string) error +} + +// customerRepository implements the Repository interface +type customerRepository struct { + collection *mongo.Collection + logger logger.Logger +} + +// NewCustomerRepository creates a new customer repository +func NewCustomerRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository { + collection := mongoManager.GetCollection("customers") + + // Create indexes + repo := &customerRepository{ + collection: collection, + logger: logger, + } + + repo.createIndexes() + return repo +} + +// createIndexes creates necessary database indexes +func (r *customerRepository) createIndexes() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Create indexes + indexes := []mongo.IndexModel{ + { + Keys: bson.D{ + {Key: "email", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{ + {Key: "username", Value: 1}, + }, + Options: options.Index().SetUnique(true), + }, + { + Keys: bson.D{ + {Key: "company_name", Value: 1}, + }, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{ + {Key: "registration_number", Value: 1}, + }, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{ + {Key: "tax_id", Value: 1}, + }, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{ + {Key: "company_id", Value: 1}, + }, + Options: options.Index().SetSparse(true), + }, + { + Keys: bson.D{ + {Key: "type", Value: 1}, + }, + }, + { + Keys: bson.D{ + {Key: "status", Value: 1}, + }, + }, + { + Keys: bson.D{ + {Key: "industry", Value: 1}, + }, + }, + { + Keys: bson.D{ + {Key: "created_at", Value: -1}, + }, + }, + { + Keys: bson.D{ + {Key: "updated_at", Value: -1}, + }, + }, + } + + _, err := r.collection.Indexes().CreateMany(ctx, indexes) + if err != nil { + r.logger.Error("Failed to create customer indexes", map[string]interface{}{ + "error": err.Error(), + }) + } else { + r.logger.Info("Customer indexes created successfully", map[string]interface{}{}) + } +} + +// Create creates a new customer +func (r *customerRepository) Create(ctx context.Context, customer *Customer) error { + // Set timestamps + now := time.Now().Unix() + customer.CreatedAt = now + customer.UpdatedAt = now + + // Set defaults if not provided + if customer.Language == "" { + customer.Language = "en" + } + if customer.Currency == "" { + customer.Currency = "USD" + } + if customer.Timezone == "" { + customer.Timezone = "UTC" + } + + _, err := r.collection.InsertOne(ctx, customer) + if err != nil { + if mongo.IsDuplicateKeyError(err) { + return errors.New("customer with this email already exists") + } + return err + } + + r.logger.Info("Customer created successfully", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + "type": customer.Type, + }) + + return nil +} + +// GetByID retrieves a customer by ID +func (r *customerRepository) GetByID(ctx context.Context, id uuid.UUID) (*Customer, error) { + var customer Customer + err := r.collection.FindOne(ctx, bson.M{"_id": id}).Decode(&customer) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, errors.New("customer not found") + } + return nil, err + } + + return &customer, nil +} + +// GetByEmail retrieves a customer by email +func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Customer, error) { + var customer Customer + err := r.collection.FindOne(ctx, bson.M{"email": email}).Decode(&customer) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, errors.New("customer not found") + } + return nil, err + } + + return &customer, nil +} + +// GetByUsername retrieves a customer by username +func (r *customerRepository) GetByUsername(ctx context.Context, username string) (*Customer, error) { + var customer Customer + err := r.collection.FindOne(ctx, bson.M{"username": username}).Decode(&customer) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, errors.New("customer not found") + } + return nil, err + } + + return &customer, nil +} + +// GetByCompanyName retrieves a customer by company name +func (r *customerRepository) GetByCompanyName(ctx context.Context, companyName string) (*Customer, error) { + var customer Customer + err := r.collection.FindOne(ctx, bson.M{"company_name": companyName}).Decode(&customer) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, errors.New("customer not found") + } + return nil, err + } + + return &customer, nil +} + +// GetByRegistrationNumber retrieves a customer by registration number +func (r *customerRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) { + var customer Customer + err := r.collection.FindOne(ctx, bson.M{"registration_number": registrationNumber}).Decode(&customer) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, errors.New("customer not found") + } + return nil, err + } + + return &customer, nil +} + +// GetByTaxID retrieves a customer by tax ID +func (r *customerRepository) GetByTaxID(ctx context.Context, taxID string) (*Customer, error) { + var customer Customer + err := r.collection.FindOne(ctx, bson.M{"tax_id": taxID}).Decode(&customer) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, errors.New("customer not found") + } + return nil, err + } + + return &customer, nil +} + +// GetByCompanyID retrieves customers by company ID with pagination +func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*Customer, error) { + filter := bson.M{"company_id": companyID} + + opts := options.Find(). + SetLimit(int64(limit)). + SetSkip(int64(offset)). + SetSort(bson.D{{Key: "created_at", Value: -1}}) + + cursor, err := r.collection.Find(ctx, filter, opts) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + + var customers []*Customer + if err = cursor.All(ctx, &customers); err != nil { + return nil, err + } + + return customers, nil +} + +// Update updates a customer +func (r *customerRepository) Update(ctx context.Context, customer *Customer) error { + customer.UpdatedAt = time.Now().Unix() + + filter := bson.M{"_id": customer.ID} + update := bson.M{"$set": customer} + + result, err := r.collection.UpdateOne(ctx, filter, update) + if err != nil { + return err + } + + if result.MatchedCount == 0 { + return errors.New("customer not found") + } + + r.logger.Info("Customer updated successfully", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + }) + + return nil +} + +// Delete deletes a customer (soft delete by setting status to inactive) +func (r *customerRepository) Delete(ctx context.Context, id uuid.UUID) error { + filter := bson.M{"_id": id} + update := bson.M{ + "$set": bson.M{ + "status": CustomerStatusInactive, + "updated_at": time.Now().Unix(), + }, + } + + result, err := r.collection.UpdateOne(ctx, filter, update) + if err != nil { + return err + } + + if result.MatchedCount == 0 { + return errors.New("customer not found") + } + + r.logger.Info("Customer deleted successfully", map[string]interface{}{ + "customer_id": id.String(), + }) + + return nil +} + +// List retrieves customers with pagination +func (r *customerRepository) List(ctx context.Context, limit, offset int) ([]*Customer, error) { + opts := options.Find(). + SetLimit(int64(limit)). + SetSkip(int64(offset)). + SetSort(bson.D{{Key: "created_at", Value: -1}}) + + cursor, err := r.collection.Find(ctx, bson.M{}, opts) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + + var customers []*Customer + if err = cursor.All(ctx, &customers); err != nil { + return nil, err + } + + return customers, nil +} + +// Search searches customers with filters +func (r *customerRepository) Search(ctx context.Context, search string, customerType *string, status *string, companyID *uuid.UUID, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) { + filter := bson.M{} + + // Add search filter + if search != "" { + filter["$or"] = []bson.M{ + {"email": primitive.Regex{Pattern: search, Options: "i"}}, + {"company_name": primitive.Regex{Pattern: search, Options: "i"}}, + {"first_name": primitive.Regex{Pattern: search, Options: "i"}}, + {"last_name": primitive.Regex{Pattern: search, Options: "i"}}, + {"full_name": primitive.Regex{Pattern: search, Options: "i"}}, + } + } + + // Add type filter + if customerType != nil { + filter["type"] = *customerType + } + + // Add status filter + if status != nil { + filter["status"] = *status + } + + // Add company ID filter + if companyID != nil { + filter["company_id"] = *companyID + } + + // Add industry filter + if industry != nil { + filter["industry"] = *industry + } + + // Add verification filter + if isVerified != nil { + filter["is_verified"] = *isVerified + } + + // Add compliance filter + if isCompliant != nil { + filter["is_compliant"] = *isCompliant + } + + // Add language filter + if language != nil { + filter["language"] = *language + } + + // Add currency filter + if currency != nil { + filter["currency"] = *currency + } + + // Set sort options + sortDirection := 1 + if sortOrder == "desc" { + sortDirection = -1 + } + + var sortField string + switch sortBy { + case "email": + sortField = "email" + case "company_name": + sortField = "company_name" + case "updated_at": + sortField = "updated_at" + case "status": + sortField = "status" + default: + sortField = "created_at" + } + + opts := options.Find(). + SetLimit(int64(limit)). + SetSkip(int64(offset)). + SetSort(bson.D{{Key: sortField, Value: sortDirection}}) + + cursor, err := r.collection.Find(ctx, filter, opts) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + + var customers []*Customer + if err = cursor.All(ctx, &customers); err != nil { + return nil, err + } + + return customers, nil +} + +// CountByCompanyID counts customers by company ID +func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) { + filter := bson.M{"company_id": companyID} + count, err := r.collection.CountDocuments(ctx, filter) + if err != nil { + return 0, err + } + + return count, nil +} + +// CountByType counts customers by type +func (r *customerRepository) CountByType(ctx context.Context, customerType CustomerType) (int64, error) { + filter := bson.M{"type": customerType} + count, err := r.collection.CountDocuments(ctx, filter) + if err != nil { + return 0, err + } + + return count, nil +} + +// CountByStatus counts customers by status +func (r *customerRepository) CountByStatus(ctx context.Context, status CustomerStatus) (int64, error) { + filter := bson.M{"status": status} + count, err := r.collection.CountDocuments(ctx, filter) + if err != nil { + return 0, err + } + + return count, nil +} + +// UpdateStatus updates customer status +func (r *customerRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status CustomerStatus) error { + filter := bson.M{"_id": id} + update := bson.M{ + "$set": bson.M{ + "status": status, + "updated_at": time.Now().Unix(), + }, + } + + result, err := r.collection.UpdateOne(ctx, filter, update) + if err != nil { + return err + } + + if result.MatchedCount == 0 { + return errors.New("customer not found") + } + + return nil +} + +// UpdateVerification updates customer verification status +func (r *customerRepository) UpdateVerification(ctx context.Context, id uuid.UUID, isVerified, isCompliant bool, complianceNotes *string) error { + filter := bson.M{"_id": id} + update := bson.M{ + "$set": bson.M{ + "is_verified": isVerified, + "is_compliant": isCompliant, + "updated_at": time.Now().Unix(), + }, + } + + if complianceNotes != nil { + update["$set"].(bson.M)["compliance_notes"] = *complianceNotes + } + + result, err := r.collection.UpdateOne(ctx, filter, update) + if err != nil { + return err + } + + if result.MatchedCount == 0 { + return errors.New("customer not found") + } + + return nil +} diff --git a/internal/customer/service.go b/internal/customer/service.go new file mode 100644 index 0000000..a67e0e2 --- /dev/null +++ b/internal/customer/service.go @@ -0,0 +1,1016 @@ +package customer + +import ( + "context" + "errors" + "time" + + "tm/pkg/logger" + + "tm/pkg/authorization" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +// Service defines business logic for customer operations +type Service interface { + // Core customer operations + CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *uuid.UUID) (*Customer, error) + GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error) + UpdateCustomer(ctx context.Context, id uuid.UUID, form *UpdateCustomerForm, updatedBy *uuid.UUID) (*Customer, error) + DeleteCustomer(ctx context.Context, id uuid.UUID, deletedBy *uuid.UUID) error + + // Customer listing and search + ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) + GetCustomersByCompanyID(ctx context.Context, companyID uuid.UUID, 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) + + // Customer management + UpdateCustomerStatus(ctx context.Context, id uuid.UUID, form *UpdateCustomerStatusForm, updatedBy *uuid.UUID) error + UpdateCustomerVerification(ctx context.Context, id uuid.UUID, form *UpdateCustomerVerificationForm, updatedBy *uuid.UUID) error + + // Mobile-specific operations (simplified) + CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *uuid.UUID) (*Customer, error) + UpdateCustomerMobile(ctx context.Context, id uuid.UUID, form *UpdateCustomerMobileForm, updatedBy *uuid.UUID) (*Customer, error) + + // Business operations + VerifyCustomer(ctx context.Context, id uuid.UUID, verifiedBy *uuid.UUID) error + SuspendCustomer(ctx context.Context, id uuid.UUID, reason string, suspendedBy *uuid.UUID) error + ActivateCustomer(ctx context.Context, id uuid.UUID, activatedBy *uuid.UUID) error + + // Authentication operations + Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) + RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) + GetProfile(ctx context.Context, customerID uuid.UUID) (*Customer, error) + Logout(ctx context.Context, customerID uuid.UUID, accessToken string) error +} + +// customerService implements the Service interface +type customerService struct { + repository Repository + logger logger.Logger + authService authorization.AuthorizationService +} + +// NewCustomerService creates a new customer service +func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService) Service { + return &customerService{ + repository: repository, + logger: logger, + authService: authService, + } +} + +// CreateCustomer creates a new customer +func (s *customerService) CreateCustomer(ctx context.Context, form *CreateCustomerForm, createdBy *uuid.UUID) (*Customer, error) { + // Check if email already exists + existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email) + if existingCustomer != nil { + return nil, errors.New("customer with this email already exists") + } + + // Check if company name already exists (for company customers) + if form.Type == string(CustomerTypeCompany) && form.CompanyName != nil { + existingCustomer, _ = s.repository.GetByCompanyName(ctx, *form.CompanyName) + if existingCustomer != nil { + return nil, errors.New("company with this name already exists") + } + } + + // Check if registration number already exists (for company customers) + if form.Type == string(CustomerTypeCompany) && form.RegistrationNumber != nil { + existingCustomer, _ = s.repository.GetByRegistrationNumber(ctx, *form.RegistrationNumber) + if existingCustomer != nil { + return nil, errors.New("company with this registration number already exists") + } + } + + // Check if tax ID already exists (for company customers) + if form.Type == string(CustomerTypeCompany) && form.TaxID != nil { + existingCustomer, _ = s.repository.GetByTaxID(ctx, *form.TaxID) + if existingCustomer != nil { + return nil, errors.New("company with this tax ID already exists") + } + } + + // Parse company ID if provided + var companyID *uuid.UUID + if form.CompanyID != nil { + parsedID, err := uuid.Parse(*form.CompanyID) + if err != nil { + return nil, errors.New("invalid company ID") + } + companyID = &parsedID + } + + // Create customer + customer := &Customer{ + ID: uuid.New(), + Type: CustomerType(form.Type), + Status: CustomerStatusActive, + CompanyID: companyID, + FirstName: form.FirstName, + LastName: form.LastName, + FullName: form.FullName, + Username: form.Username, + Email: form.Email, + Password: "", // Will be set after hashing + Phone: form.Phone, + Mobile: form.Mobile, + CompanyName: form.CompanyName, + RegistrationNumber: form.RegistrationNumber, + TaxID: form.TaxID, + Industry: form.Industry, + Address: s.convertAddressForm(form.Address), + BusinessType: form.BusinessType, + EmployeeCount: form.EmployeeCount, + AnnualRevenue: form.AnnualRevenue, + FoundedYear: form.FoundedYear, + ContactPerson: s.convertContactPersonForm(form.ContactPerson), + IsVerified: false, + IsCompliant: false, + Language: s.getDefaultLanguage(form.Language), + Currency: s.getDefaultCurrency(form.Currency), + Timezone: s.getDefaultTimezone(form.Timezone), + CreatedAt: time.Now().Unix(), + UpdatedAt: time.Now().Unix(), + CreatedBy: createdBy, + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost) + if err != nil { + s.logger.Error("Failed to hash password", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to process password") + } + customer.Password = string(hashedPassword) + + // Save to database + err = s.repository.Create(ctx, customer) + if err != nil { + s.logger.Error("Failed to create customer", map[string]interface{}{ + "error": err.Error(), + "email": form.Email, + "type": form.Type, + "created_by": createdBy.String(), + }) + return nil, err + } + + s.logger.Info("Customer created successfully", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + "type": customer.Type, + "created_by": createdBy.String(), + }) + + return customer, nil +} + +// GetCustomerByID retrieves a customer by ID +func (s *customerService) GetCustomerByID(ctx context.Context, id uuid.UUID) (*Customer, error) { + customer, err := s.repository.GetByID(ctx, id) + if err != nil { + s.logger.Error("Failed to get customer by ID", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return nil, err + } + + s.logger.Info("Customer retrieved successfully", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + }) + + return customer, nil +} + +// UpdateCustomer updates a customer +func (s *customerService) UpdateCustomer(ctx context.Context, id uuid.UUID, form *UpdateCustomerForm, updatedBy *uuid.UUID) (*Customer, error) { + // Get existing customer + customer, err := s.repository.GetByID(ctx, id) + if err != nil { + s.logger.Error("Failed to get customer for update", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return nil, errors.New("customer not found") + } + + // Check if email already exists (if changing email) + if form.Email != nil && *form.Email != customer.Email { + existingCustomer, _ := s.repository.GetByEmail(ctx, *form.Email) + if existingCustomer != nil { + return nil, errors.New("customer with this email already exists") + } + customer.Email = *form.Email + } + + // Check if username already exists (if changing username) + if form.Username != nil && *form.Username != customer.Username { + existingCustomer, _ := s.repository.GetByUsername(ctx, *form.Username) + if existingCustomer != nil { + return nil, errors.New("customer with this username already exists") + } + customer.Username = *form.Username + } + + // Check if company name already exists (if changing company name) + if form.CompanyName != nil && *form.CompanyName != *customer.CompanyName { + existingCustomer, _ := s.repository.GetByCompanyName(ctx, *form.CompanyName) + if existingCustomer != nil { + return nil, errors.New("company with this name already exists") + } + customer.CompanyName = form.CompanyName + } + + // Check if registration number already exists (if changing) + if form.RegistrationNumber != nil && *form.RegistrationNumber != *customer.RegistrationNumber { + existingCustomer, _ := s.repository.GetByRegistrationNumber(ctx, *form.RegistrationNumber) + if existingCustomer != nil { + return nil, errors.New("company with this registration number already exists") + } + customer.RegistrationNumber = form.RegistrationNumber + } + + // Check if tax ID already exists (if changing) + if form.TaxID != nil && *form.TaxID != *customer.TaxID { + existingCustomer, _ := s.repository.GetByTaxID(ctx, *form.TaxID) + if existingCustomer != nil { + return nil, errors.New("company with this tax ID already exists") + } + customer.TaxID = form.TaxID + } + + // Update fields if provided + if form.Type != nil { + customer.Type = CustomerType(*form.Type) + } + + if form.CompanyID != nil { + parsedID, err := uuid.Parse(*form.CompanyID) + if err != nil { + return nil, errors.New("invalid company ID") + } + customer.CompanyID = &parsedID + } + + if form.FirstName != nil { + customer.FirstName = form.FirstName + } + + if form.LastName != nil { + customer.LastName = form.LastName + } + + if form.FullName != nil { + customer.FullName = form.FullName + } + + if form.Phone != nil { + customer.Phone = form.Phone + } + + if form.Mobile != nil { + customer.Mobile = form.Mobile + } + + if form.Industry != nil { + customer.Industry = form.Industry + } + + if form.Address != nil { + customer.Address = s.convertAddressForm(form.Address) + } + + if form.BusinessType != nil { + customer.BusinessType = form.BusinessType + } + + if form.EmployeeCount != nil { + customer.EmployeeCount = form.EmployeeCount + } + + if form.AnnualRevenue != nil { + customer.AnnualRevenue = form.AnnualRevenue + } + + if form.FoundedYear != nil { + customer.FoundedYear = form.FoundedYear + } + + if form.ContactPerson != nil { + customer.ContactPerson = s.convertContactPersonForm(form.ContactPerson) + } + + if form.Language != nil { + customer.Language = *form.Language + } + + if form.Currency != nil { + customer.Currency = *form.Currency + } + + if form.Timezone != nil { + customer.Timezone = *form.Timezone + } + + // Set updated by + customer.UpdatedBy = updatedBy + customer.UpdatedAt = time.Now().Unix() + + // Update in database + err = s.repository.Update(ctx, customer) + if err != nil { + s.logger.Error("Failed to update customer", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return nil, errors.New("failed to update customer") + } + + s.logger.Info("Customer updated successfully", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + "updated_by": updatedBy.String(), + }) + + return customer, nil +} + +// DeleteCustomer deletes a customer (soft delete) +func (s *customerService) DeleteCustomer(ctx context.Context, id uuid.UUID, deletedBy *uuid.UUID) error { + // Get customer to check if exists + customer, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("customer not found") + } + + // Set updated by before deletion + customer.UpdatedBy = deletedBy + + // Delete customer (soft delete) + err = s.repository.Delete(ctx, id) + if err != nil { + s.logger.Error("Failed to delete customer", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return errors.New("failed to delete customer") + } + + s.logger.Info("Customer deleted successfully", map[string]interface{}{ + "customer_id": id.String(), + "deleted_by": deletedBy.String(), + }) + + return nil +} + +// ListCustomers lists customers with search and filters +func (s *customerService) ListCustomers(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 *uuid.UUID + if form.CompanyID != nil { + parsedID, err := uuid.Parse(*form.CompanyID) + if err != nil { + return nil, errors.New("invalid company ID") + } + companyID = &parsedID + } + + // 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", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to list customers") + } + + // Convert to responses + var customerResponses []*CustomerResponse + for _, customer := range customers { + customerResponses = append(customerResponses, customer.ToResponse()) + } + + // 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 uuid.UUID, limit, offset int) ([]*Customer, int64, error) { + customers, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset) + if err != nil { + s.logger.Error("Failed to get customers by company ID", map[string]interface{}{ + "error": err.Error(), + "company_id": companyID.String(), + }) + return nil, 0, errors.New("failed to get customers by company") + } + + // Get total count + total, err := s.repository.CountByCompanyID(ctx, companyID) + if err != nil { + s.logger.Error("Failed to count customers by company ID", map[string]interface{}{ + "error": err.Error(), + "company_id": companyID.String(), + }) + return customers, 0, nil // Return customers even if count fails + } + + return customers, total, nil +} + +// GetCustomersByType retrieves customers by type with pagination +func (s *customerService) GetCustomersByType(ctx context.Context, customerType CustomerType, limit, offset int) ([]*Customer, int64, error) { + // This would need to be implemented in the repository + // For now, we'll use the search method + customerTypeStr := string(customerType) + customers, err := s.repository.Search(ctx, "", &customerTypeStr, nil, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc") + if err != nil { + s.logger.Error("Failed to get customers by type", map[string]interface{}{ + "error": err.Error(), + "customer_type": customerType, + }) + return nil, 0, errors.New("failed to get customers by type") + } + + // Get total count + total, err := s.repository.CountByType(ctx, customerType) + if err != nil { + s.logger.Error("Failed to count customers by type", map[string]interface{}{ + "error": err.Error(), + "customer_type": customerType, + }) + return customers, 0, nil + } + + return customers, total, nil +} + +// GetCustomersByStatus retrieves customers by status with pagination +func (s *customerService) GetCustomersByStatus(ctx context.Context, status CustomerStatus, limit, offset int) ([]*Customer, int64, error) { + // This would need to be implemented in the repository + // For now, we'll use the search method + statusStr := string(status) + customers, err := s.repository.Search(ctx, "", nil, &statusStr, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc") + if err != nil { + s.logger.Error("Failed to get customers by status", map[string]interface{}{ + "error": err.Error(), + "status": status, + }) + return nil, 0, errors.New("failed to get customers by status") + } + + // Get total count + total, err := s.repository.CountByStatus(ctx, status) + if err != nil { + s.logger.Error("Failed to count customers by status", map[string]interface{}{ + "error": err.Error(), + "status": status, + }) + return customers, 0, nil + } + + return customers, total, nil +} + +// UpdateCustomerStatus updates customer status +func (s *customerService) UpdateCustomerStatus(ctx context.Context, id uuid.UUID, form *UpdateCustomerStatusForm, updatedBy *uuid.UUID) error { + // Get customer to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("customer not found") + } + + // Update status + status := CustomerStatus(form.Status) + err = s.repository.UpdateStatus(ctx, id, status) + if err != nil { + s.logger.Error("Failed to update customer status", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + "status": form.Status, + }) + return errors.New("failed to update customer status") + } + + s.logger.Info("Customer status updated successfully", map[string]interface{}{ + "customer_id": id.String(), + "status": form.Status, + "updated_by": updatedBy.String(), + }) + + return nil +} + +// UpdateCustomerVerification updates customer verification status +func (s *customerService) UpdateCustomerVerification(ctx context.Context, id uuid.UUID, form *UpdateCustomerVerificationForm, updatedBy *uuid.UUID) error { + // Get customer to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("customer not found") + } + + // Update verification + err = s.repository.UpdateVerification(ctx, id, form.IsVerified, form.IsCompliant, form.ComplianceNotes) + if err != nil { + s.logger.Error("Failed to update customer verification", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return errors.New("failed to update customer verification") + } + + s.logger.Info("Customer verification updated successfully", map[string]interface{}{ + "customer_id": id.String(), + "is_verified": form.IsVerified, + "is_compliant": form.IsCompliant, + "updated_by": updatedBy.String(), + }) + + return nil +} + +// CreateCustomerMobile creates a new customer via mobile app (simplified) +func (s *customerService) CreateCustomerMobile(ctx context.Context, form *CreateCustomerMobileForm, createdBy *uuid.UUID) (*Customer, error) { + // Check if email already exists + existingCustomer, _ := s.repository.GetByEmail(ctx, form.Email) + if existingCustomer != nil { + return nil, errors.New("customer with this email already exists") + } + + // Create customer with mobile form + customer := &Customer{ + ID: uuid.New(), + Type: CustomerType(form.Type), + Status: CustomerStatusActive, + FirstName: form.FirstName, + LastName: form.LastName, + FullName: form.FullName, + Username: form.Username, + Email: form.Email, + Password: "", // Will be set after hashing + Phone: form.Phone, + Mobile: form.Mobile, + CompanyName: form.CompanyName, + Industry: form.Industry, + IsVerified: false, + IsCompliant: false, + Language: "en", + Currency: "USD", + Timezone: "UTC", + CreatedAt: time.Now().Unix(), + UpdatedAt: time.Now().Unix(), + CreatedBy: createdBy, + } + + // Hash password + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost) + if err != nil { + s.logger.Error("Failed to hash password for mobile customer", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to process password") + } + customer.Password = string(hashedPassword) + + // Save to database + err = s.repository.Create(ctx, customer) + if err != nil { + s.logger.Error("Failed to create customer via mobile", map[string]interface{}{ + "error": err.Error(), + "email": form.Email, + "type": form.Type, + "created_by": createdBy.String(), + }) + return nil, err + } + + s.logger.Info("Customer created via mobile successfully", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + "type": customer.Type, + "created_by": createdBy.String(), + }) + + return customer, nil +} + +// UpdateCustomerMobile updates a customer via mobile app (simplified) +func (s *customerService) UpdateCustomerMobile(ctx context.Context, id uuid.UUID, form *UpdateCustomerMobileForm, updatedBy *uuid.UUID) (*Customer, error) { + // Get existing customer + customer, err := s.repository.GetByID(ctx, id) + if err != nil { + return nil, errors.New("customer not found") + } + + // Update fields if provided + if form.FirstName != nil { + customer.FirstName = form.FirstName + } + + if form.LastName != nil { + customer.LastName = form.LastName + } + + if form.FullName != nil { + customer.FullName = form.FullName + } + + if form.Phone != nil { + customer.Phone = form.Phone + } + + if form.Mobile != nil { + customer.Mobile = form.Mobile + } + + if form.CompanyName != nil { + customer.CompanyName = form.CompanyName + } + + if form.Industry != nil { + customer.Industry = form.Industry + } + + // Set updated by + customer.UpdatedBy = updatedBy + + // Update in database + err = s.repository.Update(ctx, customer) + if err != nil { + s.logger.Error("Failed to update customer via mobile", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return nil, errors.New("failed to update customer") + } + + s.logger.Info("Customer updated via mobile successfully", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + "updated_by": updatedBy.String(), + }) + + return customer, nil +} + +// VerifyCustomer verifies a customer +func (s *customerService) VerifyCustomer(ctx context.Context, id uuid.UUID, verifiedBy *uuid.UUID) error { + // Get customer to check if exists + customer, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("customer not found") + } + + // Update verification + err = s.repository.UpdateVerification(ctx, id, true, customer.IsCompliant, customer.ComplianceNotes) + if err != nil { + s.logger.Error("Failed to verify customer", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return errors.New("failed to verify customer") + } + + s.logger.Info("Customer verified successfully", map[string]interface{}{ + "customer_id": id.String(), + "verified_by": verifiedBy.String(), + }) + + return nil +} + +// SuspendCustomer suspends a customer +func (s *customerService) SuspendCustomer(ctx context.Context, id uuid.UUID, reason string, suspendedBy *uuid.UUID) error { + // Get customer to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("customer not found") + } + + // Update status to suspended + err = s.repository.UpdateStatus(ctx, id, CustomerStatusSuspended) + if err != nil { + s.logger.Error("Failed to suspend customer", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return errors.New("failed to suspend customer") + } + + s.logger.Info("Customer suspended successfully", map[string]interface{}{ + "customer_id": id.String(), + "reason": reason, + "suspended_by": suspendedBy.String(), + }) + + return nil +} + +// ActivateCustomer activates a customer +func (s *customerService) ActivateCustomer(ctx context.Context, id uuid.UUID, activatedBy *uuid.UUID) error { + // Get customer to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("customer not found") + } + + // Update status to active + err = s.repository.UpdateStatus(ctx, id, CustomerStatusActive) + if err != nil { + s.logger.Error("Failed to activate customer", map[string]interface{}{ + "error": err.Error(), + "customer_id": id.String(), + }) + return errors.New("failed to activate customer") + } + + s.logger.Info("Customer activated successfully", map[string]interface{}{ + "customer_id": id.String(), + "activated_by": activatedBy.String(), + }) + + return nil +} + +// Login handles customer authentication +func (s *customerService) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) { + s.logger.Info("Customer login attempt", map[string]interface{}{ + "username": form.Username, + }) + + // Get customer by username + customer, err := s.repository.GetByUsername(ctx, form.Username) + if err != nil { + s.logger.Warn("Customer login failed - username not found", map[string]interface{}{ + "username": form.Username, + }) + return nil, errors.New("invalid credentials") + } + + // Check if customer is active + if customer.Status != CustomerStatusActive { + s.logger.Warn("Customer login failed - account not active", map[string]interface{}{ + "username": form.Username, + "customer_id": customer.ID.String(), + "status": string(customer.Status), + }) + return nil, errors.New("account is not active") + } + + // Verify password + err = bcrypt.CompareHashAndPassword([]byte(customer.Password), []byte(form.Password)) + if err != nil { + s.logger.Warn("Customer login failed - wrong password", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + }) + return nil, errors.New("invalid credentials") + } + + // Generate JWT tokens using authorization service + companyID := "" + if customer.CompanyID != nil { + companyID = customer.CompanyID.String() + } + + tokenPair, err := s.authService.GenerateTokenPair( + customer.ID.String(), + customer.Email, + "customer", // Role for customers + companyID, + ) + if err != nil { + s.logger.Error("Failed to generate JWT tokens", map[string]interface{}{ + "error": err.Error(), + "customer_id": customer.ID.String(), + }) + return nil, errors.New("failed to generate authentication tokens") + } + + s.logger.Info("Customer login successful", map[string]interface{}{ + "username": form.Username, + "customer_id": customer.ID.String(), + "customer_type": string(customer.Type), + }) + + return &AuthResponse{ + Customer: customer.ToResponse(), + AccessToken: tokenPair.AccessToken, + RefreshToken: tokenPair.RefreshToken, + ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now + }, nil +} + +// RefreshToken handles token refresh for customers +func (s *customerService) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) { + s.logger.Info("Customer token refresh attempt", map[string]interface{}{ + "refresh_token": refreshToken[:10] + "...", // Log only first 10 chars for security + }) + + // Validate refresh token and generate new access token + newAccessToken, err := s.authService.RefreshAccessToken(refreshToken) + if err != nil { + s.logger.Warn("Failed to refresh access token", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("invalid or expired refresh token") + } + + // Parse the new access token to get customer information + validationResult, err := s.authService.ValidateAccessToken(newAccessToken) + if err != nil { + s.logger.Error("Failed to validate new access token", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to generate valid access token") + } + + if !validationResult.Valid { + s.logger.Error("New access token validation failed", map[string]interface{}{ + "error": validationResult.Error, + }) + return nil, errors.New("failed to generate valid access token") + } + + // Get customer information + customerID, err := uuid.Parse(validationResult.Claims.UserID) + if err != nil { + s.logger.Error("Failed to parse customer ID from token", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("invalid token format") + } + + customer, err := s.repository.GetByID(ctx, customerID) + if err != nil { + s.logger.Error("Failed to get customer for token refresh", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID.String(), + }) + return nil, errors.New("customer not found") + } + + // Check if customer is still active + if customer.Status != CustomerStatusActive { + return nil, errors.New("customer account is inactive") + } + + s.logger.Info("Access token refreshed successfully", map[string]interface{}{ + "customer_id": customer.ID.String(), + "email": customer.Email, + "customer_type": string(customer.Type), + }) + + return &AuthResponse{ + Customer: customer.ToResponse(), + AccessToken: newAccessToken, + RefreshToken: refreshToken, // Return the same refresh token + ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now + }, nil +} + +// GetProfile retrieves customer profile information +func (s *customerService) GetProfile(ctx context.Context, customerID uuid.UUID) (*Customer, error) { + s.logger.Info("Getting customer profile", map[string]interface{}{ + "customer_id": customerID.String(), + }) + + customer, err := s.repository.GetByID(ctx, customerID) + if err != nil { + s.logger.Error("Failed to get customer profile", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID.String(), + }) + return nil, errors.New("customer not found") + } + + s.logger.Info("Customer profile retrieved successfully", map[string]interface{}{ + "customer_id": customerID.String(), + "customer_type": string(customer.Type), + }) + + return customer, nil +} + +// Logout handles customer logout +func (s *customerService) Logout(ctx context.Context, customerID uuid.UUID, accessToken string) error { + s.logger.Info("Customer logout", map[string]interface{}{ + "customer_id": customerID.String(), + "access_token": accessToken[:10] + "...", // Log only first 10 chars for security + }) + + // Invalidate the access token + err := s.authService.InvalidateToken(accessToken) + if err != nil { + s.logger.Error("Failed to invalidate access token", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID.String(), + }) + // Don't return error here as the customer should still be logged out + } + + s.logger.Info("Customer logout successful", map[string]interface{}{ + "customer_id": customerID.String(), + }) + + return nil +} + +// Helper methods for converting forms to domain objects +func (s *customerService) convertAddressForm(form *AddressForm) *Address { + if form == nil { + return nil + } + + return &Address{ + Street: form.Street, + City: form.City, + State: form.State, + PostalCode: form.PostalCode, + Country: form.Country, + AddressType: form.AddressType, + IsDefault: form.IsDefault, + } +} + +func (s *customerService) convertContactPersonForm(form *ContactPersonForm) *ContactPerson { + if form == nil { + return nil + } + + return &ContactPerson{ + FirstName: form.FirstName, + LastName: form.LastName, + FullName: form.FullName, + Position: form.Position, + Email: form.Email, + Phone: form.Phone, + Mobile: form.Mobile, + IsPrimary: form.IsPrimary, + } +} + +func (s *customerService) getDefaultLanguage(language *string) string { + if language != nil { + return *language + } + return "en" +} + +func (s *customerService) getDefaultCurrency(currency *string) string { + if currency != nil { + return *currency + } + return "USD" +} + +func (s *customerService) getDefaultTimezone(timezone *string) string { + if timezone != nil { + return *timezone + } + return "UTC" +} diff --git a/internal/user/README.md b/internal/user/README.md deleted file mode 100644 index 373c506..0000000 --- a/internal/user/README.md +++ /dev/null @@ -1,214 +0,0 @@ -# User Management Service - -This service provides comprehensive user management functionality for the Tender Management system, following Clean Architecture principles and Domain-Driven Design patterns. - -## Features - -### User Authentication -- **Login**: Authenticate users with email/username and password -- **Refresh Token**: Refresh access tokens -- **Logout**: Securely log out users -- **Password Management**: Change password and reset password functionality - -### User Management -- **Create User**: Create new users with roles and permissions -- **Update User**: Update user information and profile -- **Delete User**: Soft delete users (set status to inactive) -- **Get User**: Retrieve user information by ID -- **List Users**: Search and filter users with pagination - -### Role-Based Access Control -- **User Roles**: admin, manager, operator, viewer -- **Role Management**: Update user roles -- **Status Management**: Activate, deactivate, or suspend users - -### Company Management -- **Company Users**: Get users by company ID -- **User Count**: Count users per company - -## API Endpoints - -### Public Endpoints - -#### Authentication -``` -POST /api/v1/users/login -POST /api/v1/users/refresh-token -POST /api/v1/users/reset-password -``` - -### Protected Endpoints (Require Authentication) - -#### User Profile -``` -GET /api/v1/users/profile -PUT /api/v1/users/profile -PUT /api/v1/users/change-password -POST /api/v1/users/logout -``` - -### Admin Endpoints (Require Admin Role) - -#### User Management -``` -POST /api/v1/users/admin/ -GET /api/v1/users/admin/ -GET /api/v1/users/admin/:id -PUT /api/v1/users/admin/:id -DELETE /api/v1/users/admin/:id -``` - -#### User Status & Role Management -``` -PUT /api/v1/users/admin/:id/status -PUT /api/v1/users/admin/:id/role -``` - -#### Company & Role Queries -``` -GET /api/v1/users/admin/company/:company_id -GET /api/v1/users/admin/role/:role -``` - -## Request/Response Examples - -### Create User -```json -POST /api/v1/users/admin/ -{ - "full_name": "John Doe", - "username": "johndoe", - "email": "john.doe@example.com", - "password": "securepassword123", - "role": "manager", - "company_id": "550e8400-e29b-41d4-a716-446655440000", - "department": "Engineering", - "position": "Senior Manager", - "phone": "+1234567890", - "profile_image": "https://example.com/avatar.jpg" -} -``` - -### Login -```json -POST /api/v1/users/login -{ - "email_or_username": "john.doe@example.com", - "password": "securepassword123" -} -``` - -### Update User -```json -PUT /api/v1/users/admin/:id -{ - "full_name": "John Smith", - "department": "Product Management", - "status": "active" -} -``` - -### List Users with Filters -``` -GET /api/v1/users/admin/?search=john&role=manager&status=active&limit=20&offset=0&sort_by=created_at&sort_order=desc -``` - -## Data Models - -### User Entity -```go -type User struct { - ID uuid.UUID `bson:"_id"` - FullName string `bson:"full_name"` - Username string `bson:"username"` - Email string `bson:"email"` - Password string `bson:"password"` - Role UserRole `bson:"role"` - Status UserStatus `bson:"status"` - CompanyID *uuid.UUID `bson:"company_id,omitempty"` - Department *string `bson:"department,omitempty"` - Position *string `bson:"position,omitempty"` - Phone *string `bson:"phone,omitempty"` - ProfileImage *string `bson:"profile_image,omitempty"` - IsVerified bool `bson:"is_verified"` - LastLoginAt *int64 `bson:"last_login_at,omitempty"` - CreatedAt int64 `bson:"created_at"` - UpdatedAt int64 `bson:"updated_at"` - CreatedBy *uuid.UUID `bson:"created_by,omitempty"` - UpdatedBy *uuid.UUID `bson:"updated_by,omitempty"` -} -``` - -### User Roles -- `admin`: Full system access -- `manager`: Company and team management -- `operator`: Operational tasks -- `viewer`: Read-only access - -### User Status -- `active`: User can access the system -- `inactive`: User account is disabled -- `suspended`: User account is temporarily suspended - -## Security Features - -### Password Security -- Passwords are hashed using bcrypt -- Minimum password length: 8 characters -- Maximum password length: 128 characters - -### Authentication -- JWT-based authentication (to be implemented) -- Token refresh mechanism -- Secure logout with token invalidation - -### Authorization -- Role-based access control -- Admin-only endpoints for user management -- Company-scoped access for multi-tenant support - -## Database Indexes - -The service creates the following MongoDB indexes for optimal performance: - -- **Email Index**: Unique index on email field -- **Username Index**: Unique index on username field -- **Company ID Index**: For company-based queries -- **Role Index**: For role-based filtering -- **Status Index**: For status-based filtering -- **Created At Index**: For sorting by creation date -- **Text Search Index**: For full-text search across name, email, username, department, and position - -## Error Handling - -The service provides comprehensive error handling with: - -- **Validation Errors**: Detailed validation messages using govalidator -- **Business Logic Errors**: Clear error messages for business rule violations -- **Database Errors**: Proper handling of database constraints and connection issues -- **Security Errors**: Secure error messages that don't leak sensitive information - -## Logging - -All operations are logged with structured logging including: - -- **Operation Context**: User ID, company ID, operation type -- **Error Details**: Full error context for debugging -- **Security Events**: Login attempts, password changes, role updates -- **Performance Metrics**: Operation timing and resource usage - -## Future Enhancements - -### Planned Features -- **JWT Token Management**: Complete JWT implementation with refresh tokens -- **Email Verification**: Email verification for new user accounts -- **Password Reset**: Complete password reset flow with email -- **Audit Trail**: Comprehensive audit logging for all user operations -- **Bulk Operations**: Bulk user import/export functionality -- **Advanced Search**: Elasticsearch integration for advanced search capabilities - -### Integration Points -- **Email Service**: For password reset and verification emails -- **Notification Service**: For user activity notifications -- **Audit Service**: For comprehensive audit logging -- **Permission Service**: For fine-grained permission management \ No newline at end of file diff --git a/internal/user/handler.go b/internal/user/handler.go index c5d14fb..30aea71 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -72,7 +72,7 @@ func (h *Handler) RegisterRoutes(e *echo.Echo) { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /login [post] +// @Router /admin/v1/login [post] func (h *Handler) Login(c echo.Context) error { form, err := response.Parse[LoginForm](c) if err != nil { @@ -99,7 +99,7 @@ func (h *Handler) Login(c echo.Context) error { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /refresh-token [post] +// @Router /admin/v1/refresh-token [post] func (h *Handler) RefreshToken(c echo.Context) error { form, err := response.Parse[RefreshTokenForm](c) if err != nil { @@ -124,7 +124,7 @@ func (h *Handler) RefreshToken(c echo.Context) error { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /reset-password [post] +// @Router /admin/v1/reset-password [post] func (h *Handler) ResetPassword(c echo.Context) error { form, err := response.Parse[ResetPasswordForm](c) if err != nil { @@ -152,7 +152,7 @@ func (h *Handler) ResetPassword(c echo.Context) error { // @Failure 401 {object} response.APIResponse // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /profile [get] +// @Router /admin/v1/profile [get] func (h *Handler) GetProfile(c echo.Context) error { // Extract user ID from JWT token context userID, err := GetUserIDFromContext(c) @@ -180,7 +180,7 @@ func (h *Handler) GetProfile(c echo.Context) error { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /profile [put] +// @Router /admin/v1/profile [put] func (h *Handler) UpdateProfile(c echo.Context) error { // Extract user ID from JWT token context userID, err := GetUserIDFromContext(c) @@ -213,7 +213,7 @@ func (h *Handler) UpdateProfile(c echo.Context) error { // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /change-password [put] +// @Router /admin/v1/change-password [put] func (h *Handler) ChangePassword(c echo.Context) error { userID, err := GetUserIDFromContext(c) if err != nil { @@ -245,7 +245,7 @@ func (h *Handler) ChangePassword(c echo.Context) error { // @Success 200 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /logout [delete] +// @Router /admin/v1/logout [delete] func (h *Handler) Logout(c echo.Context) error { // Extract user ID and access token from context userID, err := GetUserIDFromContext(c) @@ -281,7 +281,7 @@ func (h *Handler) Logout(c echo.Context) error { // @Failure 401 {object} response.APIResponse // @Failure 403 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /users [post] +// @Router /admin/v1/users [post] func (h *Handler) CreateUser(c echo.Context) error { // Get current user ID from JWT token currentUserID, err := GetUserIDFromContext(c) @@ -328,7 +328,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 /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 { @@ -363,7 +363,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 /users/{id} [get] +// @Router /admin/v1/users/{id} [get] func (h *Handler) GetUserByID(c echo.Context) error { idStr := c.Param("id") userID, err := uuid.Parse(idStr) @@ -394,7 +394,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 /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) @@ -441,7 +441,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 /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) @@ -480,7 +480,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 /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 { @@ -529,7 +529,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 /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) @@ -579,7 +579,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 /users/company/{company_id} [get] +// @Router /admin/v1/users/company/{company_id} [get] func (h *Handler) GetUsersByCompanyID(c echo.Context) error { companyIDStr := c.Param("company_id") companyID, err := uuid.Parse(companyIDStr) @@ -643,7 +643,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 /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)