diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 977972d..d9580e5 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -81,6 +81,1629 @@ const docTemplate = `{ } } }, + "/admin/v1/companies": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a paginated list of companies with advanced filtering options including search, type, status, industry, verification status, tags, and business criteria. Supports sorting and pagination.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "List companies with filters and pagination", + "parameters": [ + { + "type": "string", + "description": "Search term to filter companies by name, description, or industry", + "name": "search", + "in": "query" + }, + { + "enum": [ + "private", + "public", + "government", + "ngo", + "startup" + ], + "type": "string", + "description": "Filter by company type", + "name": "type", + "in": "query" + }, + { + "enum": [ + "active", + "inactive", + "suspended", + "pending" + ], + "type": "string", + "description": "Filter by company status", + "name": "status", + "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": "boolean", + "description": "Filter by customer assignment status", + "name": "has_customer", + "in": "query" + }, + { + "type": "array", + "description": "Filter by CPV codes", + "name": "cpv_codes", + "in": "query" + }, + { + "type": "array", + "description": "Filter by categories", + "name": "categories", + "in": "query" + }, + { + "type": "array", + "description": "Filter by keywords", + "name": "keywords", + "in": "query" + }, + { + "type": "array", + "description": "Filter by specializations", + "name": "specializations", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum employee count", + "name": "employee_count_min", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum employee count", + "name": "employee_count_max", + "in": "query" + }, + { + "type": "number", + "description": "Minimum annual revenue", + "name": "annual_revenue_min", + "in": "query" + }, + { + "type": "number", + "description": "Maximum annual revenue", + "name": "annual_revenue_max", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum founded year", + "name": "founded_year_min", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum founded year", + "name": "founded_year_max", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of companies per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of companies to skip for pagination", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "name", + "type", + "industry", + "created_at", + "updated_at", + "status", + "employee_count", + "annual_revenue" + ], + "type": "string", + "default": "created_at", + "description": "Field to sort by", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "default": "desc", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyListResponse" + } + } + } + ] + } + }, + "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 company with comprehensive information including business details, address, tags, and customer assignment. This endpoint is used by the web panel for full company registration.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Create a new company", + "parameters": [ + { + "description": "Company information including type, business details, address, tags, and optional customer assignment", + "name": "company", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.CreateCompanyForm" + } + } + ], + "responses": { + "201": { + "description": "Company created successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Company with this name/registration/tax ID 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" + } + } + } + } + }, + "/admin/v1/companies/industry/{industry}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve companies filtered by their industry", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get companies by industry", + "parameters": [ + { + "type": "string", + "description": "Industry", + "name": "industry", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of companies per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of companies to skip for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CompanyResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid industry", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/search": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Search companies with advanced filtering capabilities including tags, business criteria, and location", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Advanced search for companies", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "q", + "in": "query" + }, + { + "type": "array", + "description": "CPV codes filter", + "name": "cpv_codes", + "in": "query" + }, + { + "type": "array", + "description": "Categories filter", + "name": "categories", + "in": "query" + }, + { + "type": "array", + "description": "Keywords filter", + "name": "keywords", + "in": "query" + }, + { + "type": "array", + "description": "Specializations filter", + "name": "specializations", + "in": "query" + }, + { + "type": "string", + "description": "Industry filter", + "name": "industry", + "in": "query" + }, + { + "type": "string", + "description": "Location filter", + "name": "location", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum employees", + "name": "min_employees", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum employees", + "name": "max_employees", + "in": "query" + }, + { + "type": "number", + "description": "Minimum revenue", + "name": "min_revenue", + "in": "query" + }, + { + "type": "number", + "description": "Maximum revenue", + "name": "max_revenue", + "in": "query" + }, + { + "type": "boolean", + "description": "Verification status", + "name": "is_verified", + "in": "query" + }, + { + "type": "boolean", + "description": "Compliance status", + "name": "is_compliant", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies search completed", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyListResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/stats": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get comprehensive company statistics for dashboard", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get company statistics", + "responses": { + "200": { + "description": "Company statistics retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyStatsResponse" + } + } + } + ] + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/status/{status}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve companies filtered by their status (active, inactive, suspended, pending)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get companies by status", + "parameters": [ + { + "enum": [ + "active", + "inactive", + "suspended", + "pending" + ], + "type": "string", + "description": "Company status", + "name": "status", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of companies per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of companies to skip for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CompanyResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid company status", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/type/{type}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve companies filtered by their type (private, public, government, ngo, startup)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get companies by type", + "parameters": [ + { + "enum": [ + "private", + "public", + "government", + "ngo", + "startup" + ], + "type": "string", + "description": "Company type", + "name": "type", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of companies per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of companies to skip for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CompanyResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid company type", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve detailed company information by company ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get company by ID", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Company retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update company information including business details, address, tags, and customer assignment", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Update company information", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Company update information", + "name": "company", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.UpdateCompanyForm" + } + } + ], + "responses": { + "200": { + "description": "Company updated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Company with this name/registration/tax ID 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 company by setting status to inactive", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Delete company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Company deleted successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/activate": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Activate a suspended company account", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Activate company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Company activated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/assign-customer": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Assign a customer to a company for login credentials", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Assign customer to company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Customer assignment information", + "name": "assignment", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.AssignCustomerForm" + } + } + ], + "responses": { + "200": { + "description": "Customer assigned successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Customer already assigned", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/status": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update company account status (active, inactive, suspended, pending)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Update company status", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "New company status", + "name": "status", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.UpdateCompanyStatusForm" + } + } + ], + "responses": { + "200": { + "description": "Company status updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/suspend": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Suspend a company account with reason", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Suspend company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Suspension information", + "name": "suspension", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.SuspendCompanyForm" + } + } + ], + "responses": { + "200": { + "description": "Company suspended successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/tags": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update company tags (CPV, categories, keywords, specializations, certifications)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Update company tags", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Tags update information", + "name": "tags", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.UpdateCompanyTagsForm" + } + } + ], + "responses": { + "200": { + "description": "Company tags updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/tags/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Add specific tags to a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Add tags to company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Tags to add", + "name": "tags", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.AddTagsForm" + } + } + ], + "responses": { + "200": { + "description": "Tags added successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/tags/remove": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove specific tags from a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Remove tags from company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Tags to remove", + "name": "tags", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.RemoveTagsForm" + } + } + ], + "responses": { + "200": { + "description": "Tags removed successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/unassign-customer": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove customer assignment from a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Unassign customer from company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer unassigned successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/verification": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update company verification and compliance status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Update company verification status", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Verification status update", + "name": "verification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.UpdateCompanyVerificationForm" + } + } + ], + "responses": { + "200": { + "description": "Company verification updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/verify": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a company as verified", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Verify company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Company verified successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers": { "get": { "security": [ @@ -329,7 +1952,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Retrieve all customers associated with a specific company. This is useful for company administrators to see all users within their organization.", + "description": "Retrieve all customers associated with a specific company", "consumes": [ "application/json" ], @@ -343,26 +1966,20 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Company UUID", + "description": "Company ID", "name": "companyId", "in": "path", "required": true }, { - "maximum": 100, - "minimum": 1, "type": "integer", - "default": 20, - "description": "Number of customers per page (1-100)", + "description": "Number of customers to return (default: 10, max: 100)", "name": "limit", "in": "query" }, { - "minimum": 0, "type": "integer", - "default": 0, - "description": "Number of customers to skip for pagination", + "description": "Number of customers to skip (default: 0)", "name": "offset", "in": "query" } @@ -383,9 +2000,6 @@ const docTemplate = `{ "items": { "$ref": "#/definitions/customer.CustomerResponse" } - }, - "meta": { - "$ref": "#/definitions/response.Meta" } } } @@ -393,7 +2007,7 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request - Invalid company ID format", + "description": "Bad request - Invalid company ID", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -593,7 +2207,7 @@ const docTemplate = `{ "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.", + "description": "Retrieve detailed customer information by customer ID", "consumes": [ "application/json" ], @@ -607,8 +2221,7 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true @@ -634,13 +2247,13 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request - Invalid customer ID format", + "description": "Bad request - Invalid customer ID", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -659,7 +2272,7 @@ const docTemplate = `{ "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.", + "description": "Update customer information including personal details, company information, address, and business details", "consumes": [ "application/json" ], @@ -673,14 +2286,13 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true }, { - "description": "Customer update information - all fields are optional", + "description": "Updated customer information", "name": "customer", "in": "body", "required": true, @@ -709,13 +2321,13 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request - Invalid customer ID or input data", + "description": "Bad request - Invalid input data", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -746,7 +2358,7 @@ const docTemplate = `{ "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.", + "description": "Soft delete a customer by setting status to inactive", "consumes": [ "application/json" ], @@ -760,8 +2372,7 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true @@ -775,13 +2386,13 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request - Invalid customer ID format", + "description": "Bad request - Invalid customer ID", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -802,7 +2413,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Reactivate a suspended customer account by setting their status back to active. This reverses the suspension action.", + "description": "Activate a suspended customer account", "consumes": [ "application/json" ], @@ -812,12 +2423,11 @@ const docTemplate = `{ "tags": [ "Customers-Admin" ], - "summary": "Activate suspended customer account", + "summary": "Activate customer", "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true @@ -837,7 +2447,7 @@ const docTemplate = `{ } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -858,7 +2468,7 @@ const docTemplate = `{ "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.", + "description": "Update customer account status (active, inactive, suspended, pending)", "consumes": [ "application/json" ], @@ -872,14 +2482,13 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true }, { - "description": "Status update information", + "description": "New customer status", "name": "status", "in": "body", "required": true, @@ -896,19 +2505,19 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request - Invalid customer ID or status", + "description": "Bad request - Invalid input data", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "422": { - "description": "Validation error - Invalid status value", + "description": "Validation error - Invalid request data", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -929,7 +2538,7 @@ const docTemplate = `{ "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.", + "description": "Suspend a customer account with optional reason", "consumes": [ "application/json" ], @@ -939,23 +2548,22 @@ const docTemplate = `{ "tags": [ "Customers-Admin" ], - "summary": "Suspend customer account", + "summary": "Suspend customer", "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true }, { - "description": "Suspension reason", - "name": "reason", + "description": "Suspension information", + "name": "suspension", "in": "body", "required": true, "schema": { - "type": "object" + "$ref": "#/definitions/customer.SuspendCustomerForm" } } ], @@ -967,13 +2575,19 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request - Invalid customer ID or missing reason", + "description": "Bad request - Invalid input data", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -994,7 +2608,7 @@ const docTemplate = `{ "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.", + "description": "Update customer verification and compliance status", "consumes": [ "application/json" ], @@ -1004,18 +2618,17 @@ const docTemplate = `{ "tags": [ "Customers-Admin" ], - "summary": "Update customer verification and compliance status", + "summary": "Update customer verification status", "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true }, { - "description": "Verification and compliance update information", + "description": "Verification status update", "name": "verification", "in": "body", "required": true, @@ -1032,19 +2645,19 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request - Invalid customer ID", + "description": "Bad request - Invalid input data", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "422": { - "description": "Validation error - Invalid verification data", + "description": "Validation error - Invalid request data", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -1065,7 +2678,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Mark a customer as verified. This is a simple verification action that sets the customer's verification status to true.", + "description": "Mark a customer as verified", "consumes": [ "application/json" ], @@ -1079,8 +2692,7 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true @@ -1100,7 +2712,7 @@ const docTemplate = `{ } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -1114,6 +2726,29 @@ const docTemplate = `{ } } }, + "/admin/v1/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" + } + } + } + } + }, "/admin/v1/login": { "post": { "description": "Authenticate user with username and password", @@ -2271,7 +3906,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Logout customer and invalidate access token. This ensures the token cannot be used for future requests.", + "description": "Logout customer and invalidate access token", "consumes": [ "application/json" ], @@ -2279,7 +3914,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Customers-Authorization" + "Customers-Mobile" ], "summary": "Customer logout", "responses": { @@ -2290,7 +3925,7 @@ const docTemplate = `{ } }, "401": { - "description": "Unauthorized - Invalid or missing access token", + "description": "Unauthorized - Invalid or missing token", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2311,7 +3946,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Retrieve authenticated customer's profile information. This endpoint requires a valid access token and returns the customer's own profile data.", + "description": "Retrieve current customer profile information", "consumes": [ "application/json" ], @@ -2319,7 +3954,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Customers-Authorization" + "Customers-Mobile" ], "summary": "Get customer profile", "responses": { @@ -2342,13 +3977,13 @@ const docTemplate = `{ } }, "401": { - "description": "Unauthorized - Invalid or missing access token", + "description": "Unauthorized - Invalid or missing token", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer profile not found", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2431,36 +4066,646 @@ const docTemplate = `{ } } } - }, - "/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": { + "company.AddTagsForm": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "certifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "company.Address": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + } + } + }, + "company.AddressForm": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + } + } + }, + "company.AssignCustomerForm": { + "type": "object", + "properties": { + "customer_id": { + "type": "string" + } + } + }, + "company.CPVCodeCount": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "cpv_code": { + "type": "string" + } + } + }, + "company.CategoryCount": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + }, + "company.CompanyListResponse": { + "type": "object", + "properties": { + "companies": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CompanyResponse" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "company.CompanyResponse": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/company.Address" + }, + "annual_revenue": { + "type": "number" + }, + "compliance_notes": { + "type": "string" + }, + "contact_person": { + "$ref": "#/definitions/company.ContactPerson" + }, + "created_at": { + "type": "integer" + }, + "created_by": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "customer_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "type": "integer" + }, + "founded_year": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "industry": { + "type": "string" + }, + "is_compliant": { + "type": "boolean" + }, + "is_verified": { + "type": "boolean" + }, + "language": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tags": { + "$ref": "#/definitions/company.CompanyTags" + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "integer" + }, + "updated_by": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "company.CompanyStatsResponse": { + "type": "object", + "properties": { + "active_companies": { + "type": "integer" + }, + "companies_by_industry": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "companies_by_status": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "companies_by_type": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "companies_with_customer": { + "type": "integer" + }, + "top_categories": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CategoryCount" + } + }, + "top_cpv_codes": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CPVCodeCount" + } + }, + "total_companies": { + "type": "integer" + }, + "verified_companies": { + "type": "integer" + } + } + }, + "company.CompanyTags": { + "type": "object", + "properties": { + "categories": { + "description": "Categories", + "type": "array", + "items": { + "type": "string" + } + }, + "certifications": { + "description": "Certifications", + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "description": "CPV codes (Common Procurement Vocabulary)", + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "description": "Keywords for search and matching", + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "description": "Specializations", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "company.CompanyTagsForm": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "certifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "company.ContactPerson": { + "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" + } + } + }, + "company.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" + } + } + }, + "company.CreateCompanyForm": { + "type": "object", + "properties": { + "address": { + "description": "Address information", + "allOf": [ + { + "$ref": "#/definitions/company.AddressForm" + } + ] + }, + "annual_revenue": { + "type": "number" + }, + "contact_person": { + "description": "Contact information", + "allOf": [ + { + "$ref": "#/definitions/company.ContactPersonForm" + } + ] + }, + "currency": { + "type": "string" + }, + "customer_id": { + "description": "Customer assignment (for login credentials)", + "type": "string" + }, + "description": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "description": "Business information", + "type": "integer" + }, + "founded_year": { + "type": "integer" + }, + "industry": { + "type": "string" + }, + "language": { + "description": "Settings", + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "tags": { + "description": "Tags for categorization and search", + "allOf": [ + { + "$ref": "#/definitions/company.CompanyTagsForm" + } + ] + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "company.RemoveTagsForm": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "certifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "company.SuspendCompanyForm": { + "type": "object", + "properties": { + "reason": { + "type": "string" + } + } + }, + "company.UpdateCompanyForm": { + "type": "object", + "properties": { + "address": { + "description": "Address information", + "allOf": [ + { + "$ref": "#/definitions/company.AddressForm" + } + ] + }, + "annual_revenue": { + "type": "number" + }, + "contact_person": { + "description": "Contact information", + "allOf": [ + { + "$ref": "#/definitions/company.ContactPersonForm" + } + ] + }, + "currency": { + "type": "string" + }, + "customer_id": { + "description": "Customer assignment (for login credentials)", + "type": "string" + }, + "description": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "description": "Business information", + "type": "integer" + }, + "founded_year": { + "type": "integer" + }, + "industry": { + "type": "string" + }, + "language": { + "description": "Settings", + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "tags": { + "description": "Tags for categorization and search", + "allOf": [ + { + "$ref": "#/definitions/company.CompanyTagsForm" + } + ] + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "company.UpdateCompanyStatusForm": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "company.UpdateCompanyTagsForm": { + "type": "object", + "properties": { + "tags": { + "$ref": "#/definitions/company.CompanyTagsForm" + } + } + }, + "company.UpdateCompanyVerificationForm": { + "type": "object", + "properties": { + "compliance_notes": { + "type": "string" + }, + "is_compliant": { + "type": "boolean" + }, + "is_verified": { + "type": "boolean" + } + } + }, "customer.Address": { "type": "object", "properties": { - "addressType": { + "address_type": { "description": "billing, shipping, etc.", "type": "string" }, @@ -2470,10 +4715,10 @@ const docTemplate = `{ "country": { "type": "string" }, - "isDefault": { + "is_default": { "type": "boolean" }, - "postalCode": { + "postal_code": { "type": "string" }, "state": { @@ -2533,16 +4778,16 @@ const docTemplate = `{ "email": { "type": "string" }, - "firstName": { + "first_name": { "type": "string" }, - "fullName": { + "full_name": { "type": "string" }, - "isPrimary": { + "is_primary": { "type": "boolean" }, - "lastName": { + "last_name": { "type": "string" }, "mobile": { @@ -2813,6 +5058,14 @@ const docTemplate = `{ } } }, + "customer.SuspendCustomerForm": { + "type": "object", + "properties": { + "reason": { + "type": "string" + } + } + }, "customer.UpdateCustomerForm": { "type": "object", "properties": { @@ -3232,6 +5485,10 @@ const docTemplate = `{ "description": "Customer authentication operations including login, logout, and token management", "name": "Customers-Authorization" }, + { + "description": "Company management operations for web panel including CRUD, customer assignment, and tag management", + "name": "Companies-Admin" + }, { "description": "Health check operations", "name": "Health" diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index d4af547..8d5e8cf 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -74,6 +74,1629 @@ } } }, + "/admin/v1/companies": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a paginated list of companies with advanced filtering options including search, type, status, industry, verification status, tags, and business criteria. Supports sorting and pagination.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "List companies with filters and pagination", + "parameters": [ + { + "type": "string", + "description": "Search term to filter companies by name, description, or industry", + "name": "search", + "in": "query" + }, + { + "enum": [ + "private", + "public", + "government", + "ngo", + "startup" + ], + "type": "string", + "description": "Filter by company type", + "name": "type", + "in": "query" + }, + { + "enum": [ + "active", + "inactive", + "suspended", + "pending" + ], + "type": "string", + "description": "Filter by company status", + "name": "status", + "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": "boolean", + "description": "Filter by customer assignment status", + "name": "has_customer", + "in": "query" + }, + { + "type": "array", + "description": "Filter by CPV codes", + "name": "cpv_codes", + "in": "query" + }, + { + "type": "array", + "description": "Filter by categories", + "name": "categories", + "in": "query" + }, + { + "type": "array", + "description": "Filter by keywords", + "name": "keywords", + "in": "query" + }, + { + "type": "array", + "description": "Filter by specializations", + "name": "specializations", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum employee count", + "name": "employee_count_min", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum employee count", + "name": "employee_count_max", + "in": "query" + }, + { + "type": "number", + "description": "Minimum annual revenue", + "name": "annual_revenue_min", + "in": "query" + }, + { + "type": "number", + "description": "Maximum annual revenue", + "name": "annual_revenue_max", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum founded year", + "name": "founded_year_min", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum founded year", + "name": "founded_year_max", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of companies per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of companies to skip for pagination", + "name": "offset", + "in": "query" + }, + { + "enum": [ + "name", + "type", + "industry", + "created_at", + "updated_at", + "status", + "employee_count", + "annual_revenue" + ], + "type": "string", + "default": "created_at", + "description": "Field to sort by", + "name": "sort_by", + "in": "query" + }, + { + "enum": [ + "asc", + "desc" + ], + "type": "string", + "default": "desc", + "description": "Sort order", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyListResponse" + } + } + } + ] + } + }, + "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 company with comprehensive information including business details, address, tags, and customer assignment. This endpoint is used by the web panel for full company registration.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Create a new company", + "parameters": [ + { + "description": "Company information including type, business details, address, tags, and optional customer assignment", + "name": "company", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.CreateCompanyForm" + } + } + ], + "responses": { + "201": { + "description": "Company created successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Company with this name/registration/tax ID 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" + } + } + } + } + }, + "/admin/v1/companies/industry/{industry}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve companies filtered by their industry", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get companies by industry", + "parameters": [ + { + "type": "string", + "description": "Industry", + "name": "industry", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of companies per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of companies to skip for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CompanyResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid industry", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/search": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Search companies with advanced filtering capabilities including tags, business criteria, and location", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Advanced search for companies", + "parameters": [ + { + "type": "string", + "description": "Search query", + "name": "q", + "in": "query" + }, + { + "type": "array", + "description": "CPV codes filter", + "name": "cpv_codes", + "in": "query" + }, + { + "type": "array", + "description": "Categories filter", + "name": "categories", + "in": "query" + }, + { + "type": "array", + "description": "Keywords filter", + "name": "keywords", + "in": "query" + }, + { + "type": "array", + "description": "Specializations filter", + "name": "specializations", + "in": "query" + }, + { + "type": "string", + "description": "Industry filter", + "name": "industry", + "in": "query" + }, + { + "type": "string", + "description": "Location filter", + "name": "location", + "in": "query" + }, + { + "type": "integer", + "description": "Minimum employees", + "name": "min_employees", + "in": "query" + }, + { + "type": "integer", + "description": "Maximum employees", + "name": "max_employees", + "in": "query" + }, + { + "type": "number", + "description": "Minimum revenue", + "name": "min_revenue", + "in": "query" + }, + { + "type": "number", + "description": "Maximum revenue", + "name": "max_revenue", + "in": "query" + }, + { + "type": "boolean", + "description": "Verification status", + "name": "is_verified", + "in": "query" + }, + { + "type": "boolean", + "description": "Compliance status", + "name": "is_compliant", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies search completed", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyListResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid query parameters", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/stats": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get comprehensive company statistics for dashboard", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get company statistics", + "responses": { + "200": { + "description": "Company statistics retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyStatsResponse" + } + } + } + ] + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/status/{status}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve companies filtered by their status (active, inactive, suspended, pending)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get companies by status", + "parameters": [ + { + "enum": [ + "active", + "inactive", + "suspended", + "pending" + ], + "type": "string", + "description": "Company status", + "name": "status", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of companies per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of companies to skip for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CompanyResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid company status", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/type/{type}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve companies filtered by their type (private, public, government, ngo, startup)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get companies by type", + "parameters": [ + { + "enum": [ + "private", + "public", + "government", + "ngo", + "startup" + ], + "type": "string", + "description": "Company type", + "name": "type", + "in": "path", + "required": true + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "default": 20, + "description": "Number of companies per page (1-100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Number of companies to skip for pagination", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Companies retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CompanyResponse" + } + }, + "meta": { + "$ref": "#/definitions/response.Meta" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid company type", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve detailed company information by company ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Get company by ID", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Company retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update company information including business details, address, tags, and customer assignment", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Update company information", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Company update information", + "name": "company", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.UpdateCompanyForm" + } + } + ], + "responses": { + "200": { + "description": "Company updated successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/company.CompanyResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Company with this name/registration/tax ID 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 company by setting status to inactive", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Delete company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Company deleted successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/activate": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Activate a suspended company account", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Activate company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Company activated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/assign-customer": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Assign a customer to a company for login credentials", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Assign customer to company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Customer assignment information", + "name": "assignment", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.AssignCustomerForm" + } + } + ], + "responses": { + "200": { + "description": "Customer assigned successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict - Customer already assigned", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/status": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update company account status (active, inactive, suspended, pending)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Update company status", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "New company status", + "name": "status", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.UpdateCompanyStatusForm" + } + } + ], + "responses": { + "200": { + "description": "Company status updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/suspend": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Suspend a company account with reason", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Suspend company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Suspension information", + "name": "suspension", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.SuspendCompanyForm" + } + } + ], + "responses": { + "200": { + "description": "Company suspended successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/tags": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update company tags (CPV, categories, keywords, specializations, certifications)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Update company tags", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Tags update information", + "name": "tags", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.UpdateCompanyTagsForm" + } + } + ], + "responses": { + "200": { + "description": "Company tags updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/tags/add": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Add specific tags to a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Add tags to company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Tags to add", + "name": "tags", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.AddTagsForm" + } + } + ], + "responses": { + "200": { + "description": "Tags added successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/tags/remove": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove specific tags from a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Remove tags from company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Tags to remove", + "name": "tags", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.RemoveTagsForm" + } + } + ], + "responses": { + "200": { + "description": "Tags removed successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/unassign-customer": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove customer assignment from a company", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Unassign customer from company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Customer unassigned successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/verification": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update company verification and compliance status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Update company verification status", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Verification status update", + "name": "verification", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/company.UpdateCompanyVerificationForm" + } + } + ], + "responses": { + "200": { + "description": "Company verification updated successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid input data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/companies/{id}/verify": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a company as verified", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Companies-Admin" + ], + "summary": "Verify company", + "parameters": [ + { + "type": "string", + "description": "Company ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Company verified successfully", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "400": { + "description": "Bad request - Invalid company ID", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not found - Company not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/customers": { "get": { "security": [ @@ -322,7 +1945,7 @@ "BearerAuth": [] } ], - "description": "Retrieve all customers associated with a specific company. This is useful for company administrators to see all users within their organization.", + "description": "Retrieve all customers associated with a specific company", "consumes": [ "application/json" ], @@ -336,26 +1959,20 @@ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Company UUID", + "description": "Company ID", "name": "companyId", "in": "path", "required": true }, { - "maximum": 100, - "minimum": 1, "type": "integer", - "default": 20, - "description": "Number of customers per page (1-100)", + "description": "Number of customers to return (default: 10, max: 100)", "name": "limit", "in": "query" }, { - "minimum": 0, "type": "integer", - "default": 0, - "description": "Number of customers to skip for pagination", + "description": "Number of customers to skip (default: 0)", "name": "offset", "in": "query" } @@ -376,9 +1993,6 @@ "items": { "$ref": "#/definitions/customer.CustomerResponse" } - }, - "meta": { - "$ref": "#/definitions/response.Meta" } } } @@ -386,7 +2000,7 @@ } }, "400": { - "description": "Bad request - Invalid company ID format", + "description": "Bad request - Invalid company ID", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -586,7 +2200,7 @@ "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.", + "description": "Retrieve detailed customer information by customer ID", "consumes": [ "application/json" ], @@ -600,8 +2214,7 @@ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true @@ -627,13 +2240,13 @@ } }, "400": { - "description": "Bad request - Invalid customer ID format", + "description": "Bad request - Invalid customer ID", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -652,7 +2265,7 @@ "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.", + "description": "Update customer information including personal details, company information, address, and business details", "consumes": [ "application/json" ], @@ -666,14 +2279,13 @@ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true }, { - "description": "Customer update information - all fields are optional", + "description": "Updated customer information", "name": "customer", "in": "body", "required": true, @@ -702,13 +2314,13 @@ } }, "400": { - "description": "Bad request - Invalid customer ID or input data", + "description": "Bad request - Invalid input data", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -739,7 +2351,7 @@ "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.", + "description": "Soft delete a customer by setting status to inactive", "consumes": [ "application/json" ], @@ -753,8 +2365,7 @@ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true @@ -768,13 +2379,13 @@ } }, "400": { - "description": "Bad request - Invalid customer ID format", + "description": "Bad request - Invalid customer ID", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -795,7 +2406,7 @@ "BearerAuth": [] } ], - "description": "Reactivate a suspended customer account by setting their status back to active. This reverses the suspension action.", + "description": "Activate a suspended customer account", "consumes": [ "application/json" ], @@ -805,12 +2416,11 @@ "tags": [ "Customers-Admin" ], - "summary": "Activate suspended customer account", + "summary": "Activate customer", "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true @@ -830,7 +2440,7 @@ } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -851,7 +2461,7 @@ "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.", + "description": "Update customer account status (active, inactive, suspended, pending)", "consumes": [ "application/json" ], @@ -865,14 +2475,13 @@ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true }, { - "description": "Status update information", + "description": "New customer status", "name": "status", "in": "body", "required": true, @@ -889,19 +2498,19 @@ } }, "400": { - "description": "Bad request - Invalid customer ID or status", + "description": "Bad request - Invalid input data", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "422": { - "description": "Validation error - Invalid status value", + "description": "Validation error - Invalid request data", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -922,7 +2531,7 @@ "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.", + "description": "Suspend a customer account with optional reason", "consumes": [ "application/json" ], @@ -932,23 +2541,22 @@ "tags": [ "Customers-Admin" ], - "summary": "Suspend customer account", + "summary": "Suspend customer", "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true }, { - "description": "Suspension reason", - "name": "reason", + "description": "Suspension information", + "name": "suspension", "in": "body", "required": true, "schema": { - "type": "object" + "$ref": "#/definitions/customer.SuspendCustomerForm" } } ], @@ -960,13 +2568,19 @@ } }, "400": { - "description": "Bad request - Invalid customer ID or missing reason", + "description": "Bad request - Invalid input data", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "422": { + "description": "Validation error - Invalid request data", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -987,7 +2601,7 @@ "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.", + "description": "Update customer verification and compliance status", "consumes": [ "application/json" ], @@ -997,18 +2611,17 @@ "tags": [ "Customers-Admin" ], - "summary": "Update customer verification and compliance status", + "summary": "Update customer verification status", "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true }, { - "description": "Verification and compliance update information", + "description": "Verification status update", "name": "verification", "in": "body", "required": true, @@ -1025,19 +2638,19 @@ } }, "400": { - "description": "Bad request - Invalid customer ID", + "description": "Bad request - Invalid input data", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "422": { - "description": "Validation error - Invalid verification data", + "description": "Validation error - Invalid request data", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -1058,7 +2671,7 @@ "BearerAuth": [] } ], - "description": "Mark a customer as verified. This is a simple verification action that sets the customer's verification status to true.", + "description": "Mark a customer as verified", "consumes": [ "application/json" ], @@ -1072,8 +2685,7 @@ "parameters": [ { "type": "string", - "format": "uuid", - "description": "Customer UUID", + "description": "Customer ID", "name": "id", "in": "path", "required": true @@ -1093,7 +2705,7 @@ } }, "404": { - "description": "Not found - Customer does not exist", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -1107,6 +2719,29 @@ } } }, + "/admin/v1/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" + } + } + } + } + }, "/admin/v1/login": { "post": { "description": "Authenticate user with username and password", @@ -2264,7 +3899,7 @@ "BearerAuth": [] } ], - "description": "Logout customer and invalidate access token. This ensures the token cannot be used for future requests.", + "description": "Logout customer and invalidate access token", "consumes": [ "application/json" ], @@ -2272,7 +3907,7 @@ "application/json" ], "tags": [ - "Customers-Authorization" + "Customers-Mobile" ], "summary": "Customer logout", "responses": { @@ -2283,7 +3918,7 @@ } }, "401": { - "description": "Unauthorized - Invalid or missing access token", + "description": "Unauthorized - Invalid or missing token", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2304,7 +3939,7 @@ "BearerAuth": [] } ], - "description": "Retrieve authenticated customer's profile information. This endpoint requires a valid access token and returns the customer's own profile data.", + "description": "Retrieve current customer profile information", "consumes": [ "application/json" ], @@ -2312,7 +3947,7 @@ "application/json" ], "tags": [ - "Customers-Authorization" + "Customers-Mobile" ], "summary": "Get customer profile", "responses": { @@ -2335,13 +3970,13 @@ } }, "401": { - "description": "Unauthorized - Invalid or missing access token", + "description": "Unauthorized - Invalid or missing token", "schema": { "$ref": "#/definitions/response.APIResponse" } }, "404": { - "description": "Not found - Customer profile not found", + "description": "Not found - Customer not found", "schema": { "$ref": "#/definitions/response.APIResponse" } @@ -2424,36 +4059,646 @@ } } } - }, - "/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": { + "company.AddTagsForm": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "certifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "company.Address": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + } + } + }, + "company.AddressForm": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "state": { + "type": "string" + }, + "street": { + "type": "string" + } + } + }, + "company.AssignCustomerForm": { + "type": "object", + "properties": { + "customer_id": { + "type": "string" + } + } + }, + "company.CPVCodeCount": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "cpv_code": { + "type": "string" + } + } + }, + "company.CategoryCount": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + }, + "company.CompanyListResponse": { + "type": "object", + "properties": { + "companies": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CompanyResponse" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "total_pages": { + "type": "integer" + } + } + }, + "company.CompanyResponse": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/company.Address" + }, + "annual_revenue": { + "type": "number" + }, + "compliance_notes": { + "type": "string" + }, + "contact_person": { + "$ref": "#/definitions/company.ContactPerson" + }, + "created_at": { + "type": "integer" + }, + "created_by": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "customer_id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "type": "integer" + }, + "founded_year": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "industry": { + "type": "string" + }, + "is_compliant": { + "type": "boolean" + }, + "is_verified": { + "type": "boolean" + }, + "language": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tags": { + "$ref": "#/definitions/company.CompanyTags" + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updated_at": { + "type": "integer" + }, + "updated_by": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "company.CompanyStatsResponse": { + "type": "object", + "properties": { + "active_companies": { + "type": "integer" + }, + "companies_by_industry": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "companies_by_status": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "companies_by_type": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "companies_with_customer": { + "type": "integer" + }, + "top_categories": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CategoryCount" + } + }, + "top_cpv_codes": { + "type": "array", + "items": { + "$ref": "#/definitions/company.CPVCodeCount" + } + }, + "total_companies": { + "type": "integer" + }, + "verified_companies": { + "type": "integer" + } + } + }, + "company.CompanyTags": { + "type": "object", + "properties": { + "categories": { + "description": "Categories", + "type": "array", + "items": { + "type": "string" + } + }, + "certifications": { + "description": "Certifications", + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "description": "CPV codes (Common Procurement Vocabulary)", + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "description": "Keywords for search and matching", + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "description": "Specializations", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "company.CompanyTagsForm": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "certifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "company.ContactPerson": { + "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" + } + } + }, + "company.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" + } + } + }, + "company.CreateCompanyForm": { + "type": "object", + "properties": { + "address": { + "description": "Address information", + "allOf": [ + { + "$ref": "#/definitions/company.AddressForm" + } + ] + }, + "annual_revenue": { + "type": "number" + }, + "contact_person": { + "description": "Contact information", + "allOf": [ + { + "$ref": "#/definitions/company.ContactPersonForm" + } + ] + }, + "currency": { + "type": "string" + }, + "customer_id": { + "description": "Customer assignment (for login credentials)", + "type": "string" + }, + "description": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "description": "Business information", + "type": "integer" + }, + "founded_year": { + "type": "integer" + }, + "industry": { + "type": "string" + }, + "language": { + "description": "Settings", + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "tags": { + "description": "Tags for categorization and search", + "allOf": [ + { + "$ref": "#/definitions/company.CompanyTagsForm" + } + ] + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "company.RemoveTagsForm": { + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "type": "string" + } + }, + "certifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "cpv_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "specializations": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "company.SuspendCompanyForm": { + "type": "object", + "properties": { + "reason": { + "type": "string" + } + } + }, + "company.UpdateCompanyForm": { + "type": "object", + "properties": { + "address": { + "description": "Address information", + "allOf": [ + { + "$ref": "#/definitions/company.AddressForm" + } + ] + }, + "annual_revenue": { + "type": "number" + }, + "contact_person": { + "description": "Contact information", + "allOf": [ + { + "$ref": "#/definitions/company.ContactPersonForm" + } + ] + }, + "currency": { + "type": "string" + }, + "customer_id": { + "description": "Customer assignment (for login credentials)", + "type": "string" + }, + "description": { + "type": "string" + }, + "email": { + "type": "string" + }, + "employee_count": { + "description": "Business information", + "type": "integer" + }, + "founded_year": { + "type": "integer" + }, + "industry": { + "type": "string" + }, + "language": { + "description": "Settings", + "type": "string" + }, + "name": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "registration_number": { + "type": "string" + }, + "tags": { + "description": "Tags for categorization and search", + "allOf": [ + { + "$ref": "#/definitions/company.CompanyTagsForm" + } + ] + }, + "tax_id": { + "type": "string" + }, + "timezone": { + "type": "string" + }, + "type": { + "type": "string" + }, + "website": { + "type": "string" + } + } + }, + "company.UpdateCompanyStatusForm": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "company.UpdateCompanyTagsForm": { + "type": "object", + "properties": { + "tags": { + "$ref": "#/definitions/company.CompanyTagsForm" + } + } + }, + "company.UpdateCompanyVerificationForm": { + "type": "object", + "properties": { + "compliance_notes": { + "type": "string" + }, + "is_compliant": { + "type": "boolean" + }, + "is_verified": { + "type": "boolean" + } + } + }, "customer.Address": { "type": "object", "properties": { - "addressType": { + "address_type": { "description": "billing, shipping, etc.", "type": "string" }, @@ -2463,10 +4708,10 @@ "country": { "type": "string" }, - "isDefault": { + "is_default": { "type": "boolean" }, - "postalCode": { + "postal_code": { "type": "string" }, "state": { @@ -2526,16 +4771,16 @@ "email": { "type": "string" }, - "firstName": { + "first_name": { "type": "string" }, - "fullName": { + "full_name": { "type": "string" }, - "isPrimary": { + "is_primary": { "type": "boolean" }, - "lastName": { + "last_name": { "type": "string" }, "mobile": { @@ -2806,6 +5051,14 @@ } } }, + "customer.SuspendCustomerForm": { + "type": "object", + "properties": { + "reason": { + "type": "string" + } + } + }, "customer.UpdateCustomerForm": { "type": "object", "properties": { @@ -3225,6 +5478,10 @@ "description": "Customer authentication operations including login, logout, and token management", "name": "Customers-Authorization" }, + { + "description": "Company management operations for web panel including CRUD, customer assignment, and tag management", + "name": "Companies-Admin" + }, { "description": "Health check operations", "name": "Health" diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 183b5a3..58c3e45 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -1,16 +1,428 @@ definitions: + company.AddTagsForm: + properties: + categories: + items: + type: string + type: array + certifications: + items: + type: string + type: array + cpv_codes: + items: + type: string + type: array + keywords: + items: + type: string + type: array + specializations: + items: + type: string + type: array + type: object + company.Address: + properties: + city: + type: string + country: + type: string + postal_code: + type: string + state: + type: string + street: + type: string + type: object + company.AddressForm: + properties: + city: + type: string + country: + type: string + postal_code: + type: string + state: + type: string + street: + type: string + type: object + company.AssignCustomerForm: + properties: + customer_id: + type: string + type: object + company.CPVCodeCount: + properties: + count: + type: integer + cpv_code: + type: string + type: object + company.CategoryCount: + properties: + category: + type: string + count: + type: integer + type: object + company.CompanyListResponse: + properties: + companies: + items: + $ref: '#/definitions/company.CompanyResponse' + type: array + limit: + type: integer + offset: + type: integer + total: + type: integer + total_pages: + type: integer + type: object + company.CompanyResponse: + properties: + address: + $ref: '#/definitions/company.Address' + annual_revenue: + type: number + compliance_notes: + type: string + contact_person: + $ref: '#/definitions/company.ContactPerson' + created_at: + type: integer + created_by: + type: string + currency: + type: string + customer_id: + type: string + description: + type: string + email: + type: string + employee_count: + type: integer + founded_year: + type: integer + id: + type: string + industry: + type: string + is_compliant: + type: boolean + is_verified: + type: boolean + language: + type: string + name: + type: string + phone: + type: string + registration_number: + type: string + status: + type: string + tags: + $ref: '#/definitions/company.CompanyTags' + tax_id: + type: string + timezone: + type: string + type: + type: string + updated_at: + type: integer + updated_by: + type: string + website: + type: string + type: object + company.CompanyStatsResponse: + properties: + active_companies: + type: integer + companies_by_industry: + additionalProperties: + format: int64 + type: integer + type: object + companies_by_status: + additionalProperties: + format: int64 + type: integer + type: object + companies_by_type: + additionalProperties: + format: int64 + type: integer + type: object + companies_with_customer: + type: integer + top_categories: + items: + $ref: '#/definitions/company.CategoryCount' + type: array + top_cpv_codes: + items: + $ref: '#/definitions/company.CPVCodeCount' + type: array + total_companies: + type: integer + verified_companies: + type: integer + type: object + company.CompanyTags: + properties: + categories: + description: Categories + items: + type: string + type: array + certifications: + description: Certifications + items: + type: string + type: array + cpv_codes: + description: CPV codes (Common Procurement Vocabulary) + items: + type: string + type: array + keywords: + description: Keywords for search and matching + items: + type: string + type: array + specializations: + description: Specializations + items: + type: string + type: array + type: object + company.CompanyTagsForm: + properties: + categories: + items: + type: string + type: array + certifications: + items: + type: string + type: array + cpv_codes: + items: + type: string + type: array + keywords: + items: + type: string + type: array + specializations: + items: + type: string + type: array + type: object + company.ContactPerson: + 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 + company.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 + company.CreateCompanyForm: + properties: + address: + allOf: + - $ref: '#/definitions/company.AddressForm' + description: Address information + annual_revenue: + type: number + contact_person: + allOf: + - $ref: '#/definitions/company.ContactPersonForm' + description: Contact information + currency: + type: string + customer_id: + description: Customer assignment (for login credentials) + type: string + description: + type: string + email: + type: string + employee_count: + description: Business information + type: integer + founded_year: + type: integer + industry: + type: string + language: + description: Settings + type: string + name: + type: string + phone: + type: string + registration_number: + type: string + tags: + allOf: + - $ref: '#/definitions/company.CompanyTagsForm' + description: Tags for categorization and search + tax_id: + type: string + timezone: + type: string + type: + type: string + website: + type: string + type: object + company.RemoveTagsForm: + properties: + categories: + items: + type: string + type: array + certifications: + items: + type: string + type: array + cpv_codes: + items: + type: string + type: array + keywords: + items: + type: string + type: array + specializations: + items: + type: string + type: array + type: object + company.SuspendCompanyForm: + properties: + reason: + type: string + type: object + company.UpdateCompanyForm: + properties: + address: + allOf: + - $ref: '#/definitions/company.AddressForm' + description: Address information + annual_revenue: + type: number + contact_person: + allOf: + - $ref: '#/definitions/company.ContactPersonForm' + description: Contact information + currency: + type: string + customer_id: + description: Customer assignment (for login credentials) + type: string + description: + type: string + email: + type: string + employee_count: + description: Business information + type: integer + founded_year: + type: integer + industry: + type: string + language: + description: Settings + type: string + name: + type: string + phone: + type: string + registration_number: + type: string + tags: + allOf: + - $ref: '#/definitions/company.CompanyTagsForm' + description: Tags for categorization and search + tax_id: + type: string + timezone: + type: string + type: + type: string + website: + type: string + type: object + company.UpdateCompanyStatusForm: + properties: + status: + type: string + type: object + company.UpdateCompanyTagsForm: + properties: + tags: + $ref: '#/definitions/company.CompanyTagsForm' + type: object + company.UpdateCompanyVerificationForm: + properties: + compliance_notes: + type: string + is_compliant: + type: boolean + is_verified: + type: boolean + type: object customer.Address: properties: - addressType: + address_type: description: billing, shipping, etc. type: string city: type: string country: type: string - isDefault: + is_default: type: boolean - postalCode: + postal_code: type: string state: type: string @@ -49,13 +461,13 @@ definitions: properties: email: type: string - firstName: + first_name: type: string - fullName: + full_name: type: string - isPrimary: + is_primary: type: boolean - lastName: + last_name: type: string mobile: type: string @@ -232,6 +644,11 @@ definitions: refresh_token: type: string type: object + customer.SuspendCustomerForm: + properties: + reason: + type: string + type: object customer.UpdateCustomerForm: properties: address: @@ -537,6 +954,1055 @@ paths: summary: Change password tags: - Users + /admin/v1/companies: + get: + consumes: + - application/json + description: Retrieve a paginated list of companies with advanced filtering + options including search, type, status, industry, verification status, tags, + and business criteria. Supports sorting and pagination. + parameters: + - description: Search term to filter companies by name, description, or industry + in: query + name: search + type: string + - description: Filter by company type + enum: + - private + - public + - government + - ngo + - startup + in: query + name: type + type: string + - description: Filter by company status + enum: + - active + - inactive + - suspended + - pending + in: query + name: status + 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 customer assignment status + in: query + name: has_customer + type: boolean + - description: Filter by CPV codes + in: query + name: cpv_codes + type: array + - description: Filter by categories + in: query + name: categories + type: array + - description: Filter by keywords + in: query + name: keywords + type: array + - description: Filter by specializations + in: query + name: specializations + type: array + - description: Minimum employee count + in: query + name: employee_count_min + type: integer + - description: Maximum employee count + in: query + name: employee_count_max + type: integer + - description: Minimum annual revenue + in: query + name: annual_revenue_min + type: number + - description: Maximum annual revenue + in: query + name: annual_revenue_max + type: number + - description: Minimum founded year + in: query + name: founded_year_min + type: integer + - description: Maximum founded year + in: query + name: founded_year_max + type: integer + - default: 20 + description: Number of companies per page (1-100) + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + - default: 0 + description: Number of companies to skip for pagination + in: query + minimum: 0 + name: offset + type: integer + - default: created_at + description: Field to sort by + enum: + - name + - type + - industry + - created_at + - updated_at + - status + - employee_count + - annual_revenue + 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: Companies retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company.CompanyListResponse' + 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 companies with filters and pagination + tags: + - Companies-Admin + post: + consumes: + - application/json + description: Create a new company with comprehensive information including business + details, address, tags, and customer assignment. This endpoint is used by + the web panel for full company registration. + parameters: + - description: Company information including type, business details, address, + tags, and optional customer assignment + in: body + name: company + required: true + schema: + $ref: '#/definitions/company.CreateCompanyForm' + produces: + - application/json + responses: + "201": + description: Company created successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company.CompanyResponse' + type: object + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict - Company with this name/registration/tax ID 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 company + tags: + - Companies-Admin + /admin/v1/companies/{id}: + delete: + consumes: + - application/json + description: Soft delete a company by setting status to inactive + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Company deleted successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid company ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Delete company + tags: + - Companies-Admin + get: + consumes: + - application/json + description: Retrieve detailed company information by company ID + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Company retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company.CompanyResponse' + type: object + "400": + description: Bad request - Invalid company ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get company by ID + tags: + - Companies-Admin + put: + consumes: + - application/json + description: Update company information including business details, address, + tags, and customer assignment + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + - description: Company update information + in: body + name: company + required: true + schema: + $ref: '#/definitions/company.UpdateCompanyForm' + produces: + - application/json + responses: + "200": + description: Company updated successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company.CompanyResponse' + type: object + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict - Company with this name/registration/tax ID 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 company information + tags: + - Companies-Admin + /admin/v1/companies/{id}/activate: + post: + consumes: + - application/json + description: Activate a suspended company account + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Company activated successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid company ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Activate company + tags: + - Companies-Admin + /admin/v1/companies/{id}/assign-customer: + post: + consumes: + - application/json + description: Assign a customer to a company for login credentials + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + - description: Customer assignment information + in: body + name: assignment + required: true + schema: + $ref: '#/definitions/company.AssignCustomerForm' + produces: + - application/json + responses: + "200": + description: Customer assigned successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict - Customer already assigned + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid request data + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Assign customer to company + tags: + - Companies-Admin + /admin/v1/companies/{id}/status: + patch: + consumes: + - application/json + description: Update company account status (active, inactive, suspended, pending) + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + - description: New company status + in: body + name: status + required: true + schema: + $ref: '#/definitions/company.UpdateCompanyStatusForm' + produces: + - application/json + responses: + "200": + description: Company status updated successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "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 company status + tags: + - Companies-Admin + /admin/v1/companies/{id}/suspend: + post: + consumes: + - application/json + description: Suspend a company account with reason + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + - description: Suspension information + in: body + name: suspension + required: true + schema: + $ref: '#/definitions/company.SuspendCompanyForm' + produces: + - application/json + responses: + "200": + description: Company suspended successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "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: Suspend company + tags: + - Companies-Admin + /admin/v1/companies/{id}/tags: + put: + consumes: + - application/json + description: Update company tags (CPV, categories, keywords, specializations, + certifications) + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + - description: Tags update information + in: body + name: tags + required: true + schema: + $ref: '#/definitions/company.UpdateCompanyTagsForm' + produces: + - application/json + responses: + "200": + description: Company tags updated successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "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 company tags + tags: + - Companies-Admin + /admin/v1/companies/{id}/tags/add: + post: + consumes: + - application/json + description: Add specific tags to a company + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + - description: Tags to add + in: body + name: tags + required: true + schema: + $ref: '#/definitions/company.AddTagsForm' + produces: + - application/json + responses: + "200": + description: Tags added successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "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: Add tags to company + tags: + - Companies-Admin + /admin/v1/companies/{id}/tags/remove: + post: + consumes: + - application/json + description: Remove specific tags from a company + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + - description: Tags to remove + in: body + name: tags + required: true + schema: + $ref: '#/definitions/company.RemoveTagsForm' + produces: + - application/json + responses: + "200": + description: Tags removed successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid request data + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Remove tags from company + tags: + - Companies-Admin + /admin/v1/companies/{id}/unassign-customer: + post: + consumes: + - application/json + description: Remove customer assignment from a company + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Customer unassigned successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid company ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Unassign customer from company + tags: + - Companies-Admin + /admin/v1/companies/{id}/verification: + patch: + consumes: + - application/json + description: Update company verification and compliance status + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + - description: Verification status update + in: body + name: verification + required: true + schema: + $ref: '#/definitions/company.UpdateCompanyVerificationForm' + produces: + - application/json + responses: + "200": + description: Company verification updated successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid input data + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "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 company verification status + tags: + - Companies-Admin + /admin/v1/companies/{id}/verify: + post: + consumes: + - application/json + description: Mark a company as verified + parameters: + - description: Company ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Company verified successfully + schema: + $ref: '#/definitions/response.APIResponse' + "400": + description: Bad request - Invalid company ID + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not found - Company not found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Verify company + tags: + - Companies-Admin + /admin/v1/companies/industry/{industry}: + get: + consumes: + - application/json + description: Retrieve companies filtered by their industry + parameters: + - description: Industry + in: path + name: industry + required: true + type: string + - default: 20 + description: Number of companies per page (1-100) + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + - default: 0 + description: Number of companies to skip for pagination + in: query + minimum: 0 + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Companies retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + items: + $ref: '#/definitions/company.CompanyResponse' + type: array + meta: + $ref: '#/definitions/response.Meta' + type: object + "400": + description: Bad request - Invalid industry + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get companies by industry + tags: + - Companies-Admin + /admin/v1/companies/search: + get: + consumes: + - application/json + description: Search companies with advanced filtering capabilities including + tags, business criteria, and location + parameters: + - description: Search query + in: query + name: q + type: string + - description: CPV codes filter + in: query + name: cpv_codes + type: array + - description: Categories filter + in: query + name: categories + type: array + - description: Keywords filter + in: query + name: keywords + type: array + - description: Specializations filter + in: query + name: specializations + type: array + - description: Industry filter + in: query + name: industry + type: string + - description: Location filter + in: query + name: location + type: string + - description: Minimum employees + in: query + name: min_employees + type: integer + - description: Maximum employees + in: query + name: max_employees + type: integer + - description: Minimum revenue + in: query + name: min_revenue + type: number + - description: Maximum revenue + in: query + name: max_revenue + type: number + - description: Verification status + in: query + name: is_verified + type: boolean + - description: Compliance status + in: query + name: is_compliant + type: boolean + - default: 20 + description: Limit + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Companies search completed + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company.CompanyListResponse' + 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: Advanced search for companies + tags: + - Companies-Admin + /admin/v1/companies/stats: + get: + consumes: + - application/json + description: Get comprehensive company statistics for dashboard + produces: + - application/json + responses: + "200": + description: Company statistics retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/company.CompanyStatsResponse' + type: object + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get company statistics + tags: + - Companies-Admin + /admin/v1/companies/status/{status}: + get: + consumes: + - application/json + description: Retrieve companies filtered by their status (active, inactive, + suspended, pending) + parameters: + - description: Company status + enum: + - active + - inactive + - suspended + - pending + in: path + name: status + required: true + type: string + - default: 20 + description: Number of companies per page (1-100) + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + - default: 0 + description: Number of companies to skip for pagination + in: query + minimum: 0 + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Companies retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + items: + $ref: '#/definitions/company.CompanyResponse' + type: array + meta: + $ref: '#/definitions/response.Meta' + type: object + "400": + description: Bad request - Invalid company status + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get companies by status + tags: + - Companies-Admin + /admin/v1/companies/type/{type}: + get: + consumes: + - application/json + description: Retrieve companies filtered by their type (private, public, government, + ngo, startup) + parameters: + - description: Company type + enum: + - private + - public + - government + - ngo + - startup + in: path + name: type + required: true + type: string + - default: 20 + description: Number of companies per page (1-100) + in: query + maximum: 100 + minimum: 1 + name: limit + type: integer + - default: 0 + description: Number of companies to skip for pagination + in: query + minimum: 0 + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Companies retrieved successfully + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + items: + $ref: '#/definitions/company.CompanyResponse' + type: array + meta: + $ref: '#/definitions/response.Meta' + type: object + "400": + description: Bad request - Invalid company type + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get companies by type + tags: + - Companies-Admin /admin/v1/customers: get: consumes: @@ -703,11 +2169,9 @@ paths: 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. + description: Soft delete a customer by setting status to inactive parameters: - - description: Customer UUID - format: uuid + - description: Customer ID in: path name: id required: true @@ -720,11 +2184,11 @@ paths: schema: $ref: '#/definitions/response.APIResponse' "400": - description: Bad request - Invalid customer ID format + description: Bad request - Invalid customer ID schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not found - Customer does not exist + description: Not found - Customer not found schema: $ref: '#/definitions/response.APIResponse' "500": @@ -739,12 +2203,9 @@ paths: 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. + description: Retrieve detailed customer information by customer ID parameters: - - description: Customer UUID - format: uuid + - description: Customer ID in: path name: id required: true @@ -762,11 +2223,11 @@ paths: $ref: '#/definitions/customer.CustomerResponse' type: object "400": - description: Bad request - Invalid customer ID format + description: Bad request - Invalid customer ID schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not found - Customer does not exist + description: Not found - Customer not found schema: $ref: '#/definitions/response.APIResponse' "500": @@ -781,17 +2242,15 @@ paths: 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. + description: Update customer information including personal details, company + information, address, and business details parameters: - - description: Customer UUID - format: uuid + - description: Customer ID in: path name: id required: true type: string - - description: Customer update information - all fields are optional + - description: Updated customer information in: body name: customer required: true @@ -810,11 +2269,11 @@ paths: $ref: '#/definitions/customer.CustomerResponse' type: object "400": - description: Bad request - Invalid customer ID or input data + description: Bad request - Invalid input data schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not found - Customer does not exist + description: Not found - Customer not found schema: $ref: '#/definitions/response.APIResponse' "409": @@ -838,11 +2297,9 @@ paths: post: consumes: - application/json - description: Reactivate a suspended customer account by setting their status - back to active. This reverses the suspension action. + description: Activate a suspended customer account parameters: - - description: Customer UUID - format: uuid + - description: Customer ID in: path name: id required: true @@ -859,7 +2316,7 @@ paths: schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not found - Customer does not exist + description: Not found - Customer not found schema: $ref: '#/definitions/response.APIResponse' "500": @@ -868,24 +2325,21 @@ paths: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Activate suspended customer account + summary: Activate customer 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. + description: Update customer account status (active, inactive, suspended, pending) parameters: - - description: Customer UUID - format: uuid + - description: Customer ID in: path name: id required: true type: string - - description: Status update information + - description: New customer status in: body name: status required: true @@ -899,15 +2353,15 @@ paths: schema: $ref: '#/definitions/response.APIResponse' "400": - description: Bad request - Invalid customer ID or status + description: Bad request - Invalid input data schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not found - Customer does not exist + description: Not found - Customer not found schema: $ref: '#/definitions/response.APIResponse' "422": - description: Validation error - Invalid status value + description: Validation error - Invalid request data schema: $ref: '#/definitions/response.APIResponse' "500": @@ -923,22 +2377,19 @@ paths: 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. + description: Suspend a customer account with optional reason parameters: - - description: Customer UUID - format: uuid + - description: Customer ID in: path name: id required: true type: string - - description: Suspension reason + - description: Suspension information in: body - name: reason + name: suspension required: true schema: - type: object + $ref: '#/definitions/customer.SuspendCustomerForm' produces: - application/json responses: @@ -947,11 +2398,15 @@ paths: schema: $ref: '#/definitions/response.APIResponse' "400": - description: Bad request - Invalid customer ID or missing reason + description: Bad request - Invalid input data schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not found - Customer does not exist + description: Not found - Customer not found + schema: + $ref: '#/definitions/response.APIResponse' + "422": + description: Validation error - Invalid request data schema: $ref: '#/definitions/response.APIResponse' "500": @@ -960,24 +2415,21 @@ paths: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Suspend customer account + summary: Suspend customer 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. + description: Update customer verification and compliance status parameters: - - description: Customer UUID - format: uuid + - description: Customer ID in: path name: id required: true type: string - - description: Verification and compliance update information + - description: Verification status update in: body name: verification required: true @@ -991,15 +2443,15 @@ paths: schema: $ref: '#/definitions/response.APIResponse' "400": - description: Bad request - Invalid customer ID + description: Bad request - Invalid input data schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not found - Customer does not exist + description: Not found - Customer not found schema: $ref: '#/definitions/response.APIResponse' "422": - description: Validation error - Invalid verification data + description: Validation error - Invalid request data schema: $ref: '#/definitions/response.APIResponse' "500": @@ -1008,18 +2460,16 @@ paths: $ref: '#/definitions/response.APIResponse' security: - BearerAuth: [] - summary: Update customer verification and compliance status + summary: Update customer verification 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. + description: Mark a customer as verified parameters: - - description: Customer UUID - format: uuid + - description: Customer ID in: path name: id required: true @@ -1036,7 +2486,7 @@ paths: schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not found - Customer does not exist + description: Not found - Customer not found schema: $ref: '#/definitions/response.APIResponse' "500": @@ -1052,26 +2502,19 @@ paths: 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. + description: Retrieve all customers associated with a specific company parameters: - - description: Company UUID - format: uuid + - description: Company ID in: path name: companyId required: true type: string - - default: 20 - description: Number of customers per page (1-100) + - description: 'Number of customers to return (default: 10, max: 100)' in: query - maximum: 100 - minimum: 1 name: limit type: integer - - default: 0 - description: Number of customers to skip for pagination + - description: 'Number of customers to skip (default: 0)' in: query - minimum: 0 name: offset type: integer produces: @@ -1087,11 +2530,9 @@ paths: items: $ref: '#/definitions/customer.CustomerResponse' type: array - meta: - $ref: '#/definitions/response.Meta' type: object "400": - description: Bad request - Invalid company ID format + description: Bad request - Invalid company ID schema: $ref: '#/definitions/response.APIResponse' "500": @@ -1220,6 +2661,21 @@ paths: summary: Get customers by type tags: - Customers-Admin + /admin/v1/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 /admin/v1/login: post: consumes: @@ -1937,8 +3393,7 @@ paths: delete: consumes: - application/json - description: Logout customer and invalidate access token. This ensures the token - cannot be used for future requests. + description: Logout customer and invalidate access token produces: - application/json responses: @@ -1947,7 +3402,7 @@ paths: schema: $ref: '#/definitions/response.APIResponse' "401": - description: Unauthorized - Invalid or missing access token + description: Unauthorized - Invalid or missing token schema: $ref: '#/definitions/response.APIResponse' "500": @@ -1958,13 +3413,12 @@ paths: - BearerAuth: [] summary: Customer logout tags: - - Customers-Authorization + - Customers-Mobile /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. + description: Retrieve current customer profile information produces: - application/json responses: @@ -1978,11 +3432,11 @@ paths: $ref: '#/definitions/customer.CustomerResponse' type: object "401": - description: Unauthorized - Invalid or missing access token + description: Unauthorized - Invalid or missing token schema: $ref: '#/definitions/response.APIResponse' "404": - description: Not found - Customer profile not found + description: Not found - Customer not found schema: $ref: '#/definitions/response.APIResponse' "500": @@ -1993,7 +3447,7 @@ paths: - BearerAuth: [] summary: Get customer profile tags: - - Customers-Authorization + - Customers-Mobile /api/v1/refresh-token: post: consumes: @@ -2038,21 +3492,6 @@ paths: 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. @@ -2072,5 +3511,8 @@ tags: - description: Customer authentication operations including login, logout, and token management name: Customers-Authorization +- description: Company management operations for web panel including CRUD, customer + assignment, and tag management + name: Companies-Admin - description: Health check operations name: Health diff --git a/cmd/web/main.go b/cmd/web/main.go index 8467dae..0829cc7 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -31,6 +31,9 @@ package main // @tag.name Customers-Authorization // @tag.description Customer authentication operations including login, logout, and token management +// @tag.name Companies-Admin +// @tag.description Company management operations for web panel including CRUD, customer assignment, and tag management + // @tag.name Health // @tag.description Health check operations @@ -39,6 +42,7 @@ import ( "fmt" "net/http" "time" + "tm/internal/company" "tm/internal/customer" "tm/internal/user" @@ -80,9 +84,10 @@ func main() { // Initialize repositories with MongoDB connection manager userRepository := user.NewUserRepository(mongoManager, logger) customerRepository := customer.NewCustomerRepository(mongoManager, logger) + companyRepository := company.NewCompanyRepository(mongoManager, logger) logger.Info("Repositories initialized successfully", map[string]interface{}{ - "repositories": []string{"customer", "user"}, + "repositories": []string{"customer", "user", "company"}, }) // Initialize validation service @@ -91,17 +96,19 @@ func main() { // Initialize services with repositories userService := user.NewUserService(userRepository, logger, userAuthService, userValidator) customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService) + companyService := company.NewCompanyService(companyRepository, logger) logger.Info("Services initialized successfully", map[string]interface{}{ - "services": []string{"customer", "user"}, + "services": []string{"customer", "user", "company"}, }) // Initialize handlers with services userHandler := user.NewUserHandler(userService, logger, userValidator, userAuthService) customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger) + companyHandler := company.NewHandler(companyService, userHandler, logger) logger.Info("Handlers initialized successfully", map[string]interface{}{ - "handlers": []string{"customer", "user"}, + "handlers": []string{"customer", "user", "company"}, }) // Initialize HTTP server @@ -110,6 +117,7 @@ func main() { // Register routes userHandler.RegisterRoutes(e) customerHandler.RegisterRoutes(e) + companyHandler.RegisterRoutes(e) // Start server serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port) diff --git a/internal/company/entity.go b/internal/company/entity.go new file mode 100644 index 0000000..268a173 --- /dev/null +++ b/internal/company/entity.go @@ -0,0 +1,207 @@ +package company + +import ( + "tm/pkg/mongo" +) + +// CompanyStatus represents company account status +type CompanyStatus string + +const ( + CompanyStatusActive CompanyStatus = "active" + CompanyStatusInactive CompanyStatus = "inactive" + CompanyStatusSuspended CompanyStatus = "suspended" + CompanyStatusPending CompanyStatus = "pending" +) + +// CompanyType represents the type of company +type CompanyType string + +const ( + CompanyTypePrivate CompanyType = "private" + CompanyTypePublic CompanyType = "public" + CompanyTypeGovernment CompanyType = "government" + CompanyTypeNgo CompanyType = "ngo" + CompanyTypeStartup CompanyType = "startup" +) + +// Company represents a company in the tender management system +type Company struct { + mongo.Model + Name string `bson:"name" json:"name"` + Type CompanyType `bson:"type" json:"type"` + Status CompanyStatus `bson:"status" json:"status"` + RegistrationNumber string `bson:"registration_number" json:"registration_number"` + TaxID string `bson:"tax_id" json:"tax_id"` + Industry string `bson:"industry" json:"industry"` + Description *string `bson:"description,omitempty" json:"description,omitempty"` + Website *string `bson:"website,omitempty" json:"website,omitempty"` + + // Business information + EmployeeCount *int `bson:"employee_count,omitempty" json:"employee_count,omitempty"` + AnnualRevenue *float64 `bson:"annual_revenue,omitempty" json:"annual_revenue,omitempty"` + FoundedYear *int `bson:"founded_year,omitempty" json:"founded_year,omitempty"` + + // Address information + Address *Address `bson:"address,omitempty" json:"address,omitempty"` + + // Contact information + ContactPerson *ContactPerson `bson:"contact_person,omitempty" json:"contact_person,omitempty"` + Phone *string `bson:"phone,omitempty" json:"phone,omitempty"` + Email *string `bson:"email,omitempty" json:"email,omitempty"` + + // Tags for categorization and search + Tags *CompanyTags `bson:"tags,omitempty" json:"tags,omitempty"` + + // Verification and compliance + IsVerified bool `bson:"is_verified" json:"is_verified"` + IsCompliant bool `bson:"is_compliant" json:"is_compliant"` + ComplianceNotes *string `bson:"compliance_notes,omitempty" json:"compliance_notes,omitempty"` + + // Settings + Language string `bson:"language" json:"language"` // Default: "en" + Currency string `bson:"currency" json:"currency"` // Default: "USD" + Timezone string `bson:"timezone" json:"timezone"` // Default: "UTC" + + // Customer assignment (for login credentials) + CustomerID *string `bson:"customer_id,omitempty" json:"customer_id,omitempty"` + + // Audit fields + CreatedBy *string `bson:"created_by,omitempty" json:"created_by,omitempty"` + UpdatedBy *string `bson:"updated_by,omitempty" json:"updated_by,omitempty"` +} + +// SetID sets the company ID (implements IDSetter interface) +func (c *Company) SetID(id string) { + c.ID = id +} + +// GetID returns the company ID (implements IDGetter interface) +func (c *Company) GetID() string { + return c.ID +} + +// SetCreatedAt sets the created timestamp (implements Timestampable interface) +func (c *Company) SetCreatedAt(timestamp int64) { + c.CreatedAt = timestamp +} + +// SetUpdatedAt sets the updated timestamp (implements Timestampable interface) +func (c *Company) SetUpdatedAt(timestamp int64) { + c.UpdatedAt = timestamp +} + +// GetCreatedAt returns the created timestamp (implements Timestampable interface) +func (c *Company) GetCreatedAt() int64 { + return c.CreatedAt +} + +// GetUpdatedAt returns the updated timestamp (implements Timestampable interface) +func (c *Company) GetUpdatedAt() int64 { + return c.UpdatedAt +} + +// Address represents a company's address +type Address struct { + Street string `bson:"street" json:"street"` + City string `bson:"city" json:"city"` + State string `bson:"state" json:"state"` + PostalCode string `bson:"postal_code" json:"postal_code"` + Country string `bson:"country" json:"country"` +} + +// ContactPerson represents a contact person for the company +type ContactPerson struct { + FirstName string `bson:"first_name" json:"first_name"` + LastName string `bson:"last_name" json:"last_name"` + FullName string `bson:"full_name" json:"full_name"` + Position string `bson:"position" json:"position"` + Email string `bson:"email" json:"email"` + Phone string `bson:"phone" json:"phone"` + Mobile *string `bson:"mobile,omitempty" json:"mobile,omitempty"` + IsPrimary bool `bson:"is_primary" json:"is_primary"` +} + +// CompanyTags represents tags for company categorization and search +type CompanyTags struct { + // CPV codes (Common Procurement Vocabulary) + CPVCodes []string `bson:"cpv_codes,omitempty" json:"cpv_codes,omitempty"` + + // Categories + Categories []string `bson:"categories,omitempty" json:"categories,omitempty"` + + // Keywords for search and matching + Keywords []string `bson:"keywords,omitempty" json:"keywords,omitempty"` + + // Specializations + Specializations []string `bson:"specializations,omitempty" json:"specializations,omitempty"` + + // Certifications + Certifications []string `bson:"certifications,omitempty" json:"certifications,omitempty"` +} + +// CompanyResponse represents the company data sent in API responses +type CompanyResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Status string `json:"status"` + RegistrationNumber string `json:"registration_number"` + TaxID string `json:"tax_id"` + Industry string `json:"industry"` + Description *string `json:"description,omitempty"` + Website *string `json:"website,omitempty"` + EmployeeCount *int `json:"employee_count,omitempty"` + AnnualRevenue *float64 `json:"annual_revenue,omitempty"` + FoundedYear *int `json:"founded_year,omitempty"` + Address *Address `json:"address,omitempty"` + ContactPerson *ContactPerson `json:"contact_person,omitempty"` + Phone *string `json:"phone,omitempty"` + Email *string `json:"email,omitempty"` + Tags *CompanyTags `json:"tags,omitempty"` + IsVerified bool `json:"is_verified"` + IsCompliant bool `json:"is_compliant"` + ComplianceNotes *string `json:"compliance_notes,omitempty"` + Language string `json:"language"` + Currency string `json:"currency"` + Timezone string `json:"timezone"` + CustomerID *string `json:"customer_id,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + CreatedBy *string `json:"created_by,omitempty"` + UpdatedBy *string `json:"updated_by,omitempty"` +} + +// ToResponse converts Company to CompanyResponse +func (c *Company) ToResponse() *CompanyResponse { + return &CompanyResponse{ + ID: c.ID, + Name: c.Name, + Type: string(c.Type), + Status: string(c.Status), + RegistrationNumber: c.RegistrationNumber, + TaxID: c.TaxID, + Industry: c.Industry, + Description: c.Description, + Website: c.Website, + EmployeeCount: c.EmployeeCount, + AnnualRevenue: c.AnnualRevenue, + FoundedYear: c.FoundedYear, + Address: c.Address, + ContactPerson: c.ContactPerson, + Phone: c.Phone, + Email: c.Email, + Tags: c.Tags, + IsVerified: c.IsVerified, + IsCompliant: c.IsCompliant, + ComplianceNotes: c.ComplianceNotes, + Language: c.Language, + Currency: c.Currency, + Timezone: c.Timezone, + CustomerID: c.CustomerID, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + CreatedBy: c.CreatedBy, + UpdatedBy: c.UpdatedBy, + } +} diff --git a/internal/company/form.go b/internal/company/form.go new file mode 100644 index 0000000..4b2a534 --- /dev/null +++ b/internal/company/form.go @@ -0,0 +1,224 @@ +package company + +// CreateCompanyForm represents the form for creating a new company +type CreateCompanyForm struct { + Name string `json:"name" valid:"required,length(2|200)"` + Type string `json:"type" valid:"required,in(private|public|government|ngo|startup)"` + RegistrationNumber string `json:"registration_number" valid:"required,length(5|50)"` + TaxID string `json:"tax_id" valid:"required,length(5|50)"` + Industry string `json:"industry" valid:"required,length(2|100)"` + Description *string `json:"description,omitempty" valid:"optional,length(10|1000)"` + Website *string `json:"website,omitempty" valid:"optional,url"` + + // Business information + EmployeeCount *int `json:"employee_count,omitempty" valid:"optional,range(1|100000)"` + AnnualRevenue *float64 `json:"annual_revenue,omitempty" valid:"optional,range(0|999999999999)"` + FoundedYear *int `json:"founded_year,omitempty" valid:"optional,range(1800|2100)"` + + // Address information + Address *AddressForm `json:"address,omitempty"` + + // Contact information + ContactPerson *ContactPersonForm `json:"contact_person,omitempty"` + Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` + Email *string `json:"email,omitempty" valid:"optional,email"` + + // Tags for categorization and search + Tags *CompanyTagsForm `json:"tags,omitempty"` + + // Customer assignment (for login credentials) + CustomerID *string `json:"customer_id,omitempty" valid:"optional,uuid"` + + // Settings + Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"` + Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"` + Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"` +} + +// UpdateCompanyForm represents the form for updating a company +type UpdateCompanyForm struct { + Name *string `json:"name,omitempty" valid:"optional,length(2|200)"` + Type *string `json:"type,omitempty" valid:"optional,in(private|public|government|ngo|startup)"` + RegistrationNumber *string `json:"registration_number,omitempty" valid:"optional,length(5|50)"` + TaxID *string `json:"tax_id,omitempty" valid:"optional,length(5|50)"` + Industry *string `json:"industry,omitempty" valid:"optional,length(2|100)"` + Description *string `json:"description,omitempty" valid:"optional,length(10|1000)"` + Website *string `json:"website,omitempty" valid:"optional,url"` + + // Business information + EmployeeCount *int `json:"employee_count,omitempty" valid:"optional,range(1|100000)"` + AnnualRevenue *float64 `json:"annual_revenue,omitempty" valid:"optional,range(0|999999999999)"` + FoundedYear *int `json:"founded_year,omitempty" valid:"optional,range(1800|2100)"` + + // Address information + Address *AddressForm `json:"address,omitempty"` + + // Contact information + ContactPerson *ContactPersonForm `json:"contact_person,omitempty"` + Phone *string `json:"phone,omitempty" valid:"optional,length(10|20)"` + Email *string `json:"email,omitempty" valid:"optional,email"` + + // Tags for categorization and search + Tags *CompanyTagsForm `json:"tags,omitempty"` + + // Customer assignment (for login credentials) + CustomerID *string `json:"customer_id,omitempty" valid:"optional,uuid"` + + // Settings + Language *string `json:"language,omitempty" valid:"optional,in(en|ar|fr|es|de|zh|ja|ko)"` + Currency *string `json:"currency,omitempty" valid:"optional,in(USD|EUR|GBP|JPY|CAD|AUD|CHF|CNY|INR|BRL)"` + Timezone *string `json:"timezone,omitempty" valid:"optional,length(3|50)"` +} + +// ListCompaniesForm represents the form for listing companies with filters +type ListCompaniesForm struct { + Search *string `query:"search" valid:"optional"` + Type *string `query:"type" valid:"optional,in(private|public|government|ngo|startup)"` + Status *string `query:"status" valid:"optional,in(active|inactive|suspended|pending)"` + Industry *string `query:"industry" valid:"optional"` + IsVerified *bool `query:"is_verified" valid:"optional"` + IsCompliant *bool `query:"is_compliant" valid:"optional"` + Language *string `query:"language" valid:"optional"` + Currency *string `query:"currency" valid:"optional"` + CPVCodes []string `query:"cpv_codes" valid:"optional"` + Categories []string `query:"categories" valid:"optional"` + Keywords []string `query:"keywords" valid:"optional"` + Specializations []string `query:"specializations" valid:"optional"` + HasCustomer *bool `query:"has_customer" valid:"optional"` + EmployeeCountMin *int `query:"employee_count_min" valid:"optional,min(1)"` + EmployeeCountMax *int `query:"employee_count_max" valid:"optional,min(1)"` + AnnualRevenueMin *float64 `query:"annual_revenue_min" valid:"optional,min(0)"` + AnnualRevenueMax *float64 `query:"annual_revenue_max" valid:"optional,min(0)"` + FoundedYearMin *int `query:"founded_year_min" valid:"optional,range(1800|2100)"` + FoundedYearMax *int `query:"founded_year_max" valid:"optional,range(1800|2100)"` + Limit *int `query:"limit" valid:"optional,range(1|100)"` + Offset *int `query:"offset" valid:"optional,min(0)"` + SortBy *string `query:"sort_by" valid:"optional,in(name|type|industry|created_at|updated_at|status|employee_count|annual_revenue)"` + SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"` +} + +// UpdateCompanyStatusForm represents the form for updating company status +type UpdateCompanyStatusForm struct { + Status string `json:"status" valid:"required,in(active|inactive|suspended|pending)"` +} + +// UpdateCompanyVerificationForm represents the form for updating company verification status +type UpdateCompanyVerificationForm struct { + IsVerified bool `json:"is_verified"` + IsCompliant bool `json:"is_compliant"` + ComplianceNotes *string `json:"compliance_notes,omitempty" valid:"optional,length(0|1000)"` +} + +// AssignCustomerForm represents the form for assigning a customer to a company +type AssignCustomerForm struct { + CustomerID string `json:"customer_id" valid:"required,uuid"` +} + +// UpdateCompanyTagsForm represents the form for updating company tags +type UpdateCompanyTagsForm struct { + Tags *CompanyTagsForm `json:"tags" valid:"required"` +} + +// AddTagsForm represents the form for adding tags to a company +type AddTagsForm struct { + CPVCodes []string `json:"cpv_codes,omitempty" valid:"optional"` + Categories []string `json:"categories,omitempty" valid:"optional"` + Keywords []string `json:"keywords,omitempty" valid:"optional"` + Specializations []string `json:"specializations,omitempty" valid:"optional"` + Certifications []string `json:"certifications,omitempty" valid:"optional"` +} + +// RemoveTagsForm represents the form for removing tags from a company +type RemoveTagsForm struct { + CPVCodes []string `json:"cpv_codes,omitempty" valid:"optional"` + Categories []string `json:"categories,omitempty" valid:"optional"` + Keywords []string `json:"keywords,omitempty" valid:"optional"` + Specializations []string `json:"specializations,omitempty" valid:"optional"` + Certifications []string `json:"certifications,omitempty" valid:"optional"` +} + +// SuspendCompanyForm represents the form for suspending a company +type SuspendCompanyForm struct { + Reason string `json:"reason" valid:"required,length(10|500)"` +} + +// AddressForm represents the form for company address +type AddressForm struct { + Street string `json:"street" valid:"required,length(5|200)"` + City string `json:"city" valid:"required,length(2|100)"` + State string `json:"state" valid:"required,length(2|100)"` + PostalCode string `json:"postal_code" valid:"required,length(3|20)"` + Country string `json:"country" valid:"required,length(2|100)"` +} + +// ContactPersonForm represents the form for contact person +type ContactPersonForm struct { + FirstName string `json:"first_name" valid:"required,length(2|50)"` + LastName string `json:"last_name" valid:"required,length(2|50)"` + FullName string `json:"full_name" valid:"required,length(2|100)"` + Position string `json:"position" valid:"required,length(2|100)"` + Email string `json:"email" valid:"required,email"` + Phone string `json:"phone" valid:"required,length(10|20)"` + Mobile *string `json:"mobile,omitempty" valid:"optional,length(10|20)"` + IsPrimary bool `json:"is_primary"` +} + +// CompanyTagsForm represents the form for company tags +type CompanyTagsForm struct { + CPVCodes []string `json:"cpv_codes,omitempty" valid:"optional"` + Categories []string `json:"categories,omitempty" valid:"optional"` + Keywords []string `json:"keywords,omitempty" valid:"optional"` + Specializations []string `json:"specializations,omitempty" valid:"optional"` + Certifications []string `json:"certifications,omitempty" valid:"optional"` +} + +// CompanyListResponse represents the response for listing companies +type CompanyListResponse struct { + Companies []*CompanyResponse `json:"companies"` + Total int64 `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` + TotalPages int `json:"total_pages"` +} + +// Company search and matching DTOs +type CompanySearchForm struct { + Query *string `query:"q" valid:"optional"` + CPVCodes []string `query:"cpv_codes" valid:"optional"` + Categories []string `query:"categories" valid:"optional"` + Keywords []string `query:"keywords" valid:"optional"` + Specializations []string `query:"specializations" valid:"optional"` + Industry *string `query:"industry" valid:"optional"` + Location *string `query:"location" valid:"optional"` + MinEmployees *int `query:"min_employees" valid:"optional,min(1)"` + MaxEmployees *int `query:"max_employees" valid:"optional,min(1)"` + MinRevenue *float64 `query:"min_revenue" valid:"optional,min(0)"` + MaxRevenue *float64 `query:"max_revenue" valid:"optional,min(0)"` + IsVerified *bool `query:"is_verified" valid:"optional"` + IsCompliant *bool `query:"is_compliant" valid:"optional"` + Limit *int `query:"limit" valid:"optional,range(1|100)"` + Offset *int `query:"offset" valid:"optional,min(0)"` +} + +// Company statistics DTOs +type CompanyStatsResponse struct { + TotalCompanies int64 `json:"total_companies"` + ActiveCompanies int64 `json:"active_companies"` + VerifiedCompanies int64 `json:"verified_companies"` + CompaniesWithCustomer int64 `json:"companies_with_customer"` + CompaniesByType map[string]int64 `json:"companies_by_type"` + CompaniesByIndustry map[string]int64 `json:"companies_by_industry"` + CompaniesByStatus map[string]int64 `json:"companies_by_status"` + TopCategories []CategoryCount `json:"top_categories"` + TopCPVCodes []CPVCodeCount `json:"top_cpv_codes"` +} + +type CategoryCount struct { + Category string `json:"category"` + Count int64 `json:"count"` +} + +type CPVCodeCount struct { + CPVCode string `json:"cpv_code"` + Count int64 `json:"count"` +} diff --git a/internal/company/handler.go b/internal/company/handler.go new file mode 100644 index 0000000..ec73f71 --- /dev/null +++ b/internal/company/handler.go @@ -0,0 +1,869 @@ +package company + +import ( + "strconv" + "tm/internal/user" + "tm/pkg/logger" + "tm/pkg/response" + + "github.com/labstack/echo/v4" +) + +// Handler handles HTTP requests for company operations +type Handler struct { + service Service + userHandler *user.Handler + logger logger.Logger +} + +// NewHandler creates a new company handler +func NewHandler(service Service, userHandler *user.Handler, logger logger.Logger) *Handler { + return &Handler{ + service: service, + userHandler: userHandler, + logger: logger, + } +} + +// RegisterRoutes registers company routes +func (h *Handler) RegisterRoutes(e *echo.Echo) { + // Admin routes for web panel + adminV1 := e.Group("/admin/v1/companies") + adminV1.Use(h.userHandler.AuthMiddleware()) + adminV1.POST("", h.CreateCompany) + adminV1.GET("", h.ListCompanies) + adminV1.GET("/:id", h.GetCompany) + adminV1.PUT("/:id", h.UpdateCompany) + adminV1.DELETE("/:id", h.DeleteCompany) + adminV1.GET("/search", h.SearchCompanies) + adminV1.PATCH("/:id/status", h.UpdateCompanyStatus) + adminV1.PATCH("/:id/verification", h.UpdateCompanyVerification) + adminV1.POST("/:id/assign-customer", h.AssignCustomer) + adminV1.POST("/:id/unassign-customer", h.UnassignCustomer) + adminV1.PUT("/:id/tags", h.UpdateCompanyTags) + adminV1.POST("/:id/tags/add", h.AddTags) + adminV1.POST("/:id/tags/remove", h.RemoveTags) + adminV1.POST("/:id/verify", h.VerifyCompany) + adminV1.POST("/:id/suspend", h.SuspendCompany) + adminV1.POST("/:id/activate", h.ActivateCompany) + adminV1.GET("/stats", h.GetCompanyStats) + adminV1.GET("/type/:type", h.GetCompaniesByType) + adminV1.GET("/status/:status", h.GetCompaniesByStatus) + adminV1.GET("/industry/:industry", h.GetCompaniesByIndustry) +} + +// Web Panel Endpoints + +// CreateCompany creates a new company (Web Panel) +// @Summary Create a new company +// @Description Create a new company with comprehensive information including business details, address, tags, and customer assignment. This endpoint is used by the web panel for full company registration. +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param company body CreateCompanyForm true "Company information including type, business details, address, tags, and optional customer assignment" +// @Success 201 {object} response.APIResponse{data=CompanyResponse} "Company created successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 409 {object} response.APIResponse "Conflict - Company with this name/registration/tax ID already exists" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies [post] +func (h *Handler) CreateCompany(c echo.Context) error { + form, err := response.Parse[CreateCompanyForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + createdBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + company, err := h.service.CreateCompany(c.Request().Context(), form, &createdBy) + if err != nil { + if err.Error() == "company with this name already exists" || + err.Error() == "company with this registration number already exists" || + err.Error() == "company with this tax ID already exists" { + return response.Conflict(c, err.Error()) + } + return response.InternalServerError(c, "Failed to create company") + } + + return response.Created(c, company.ToResponse(), "Company created successfully") +} + +// GetCompany retrieves a company by ID (Web Panel) +// @Summary Get company by ID +// @Description Retrieve detailed company information by company ID +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Success 200 {object} response.APIResponse{data=CompanyResponse} "Company retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id} [get] +func (h *Handler) GetCompany(c echo.Context) error { + id := c.Param("id") + + company, err := h.service.GetCompanyByID(c.Request().Context(), id) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to get company") + } + + return response.Success(c, company.ToResponse(), "Company retrieved successfully") +} + +// UpdateCompany updates a company (Web Panel) +// @Summary Update company information +// @Description Update company information including business details, address, tags, and customer assignment +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Param company body UpdateCompanyForm true "Company update information" +// @Success 200 {object} response.APIResponse{data=CompanyResponse} "Company updated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 409 {object} response.APIResponse "Conflict - Company with this name/registration/tax ID already exists" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id} [put] +func (h *Handler) UpdateCompany(c echo.Context) error { + id := c.Param("id") + form, err := response.Parse[UpdateCompanyForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + updatedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + company, err := h.service.UpdateCompany(c.Request().Context(), id, form, &updatedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + if err.Error() == "company with this name already exists" || + err.Error() == "company with this registration number already exists" || + err.Error() == "company with this tax ID already exists" { + return response.Conflict(c, err.Error()) + } + return response.InternalServerError(c, "Failed to update company") + } + + return response.Success(c, company.ToResponse(), "Company updated successfully") +} + +// DeleteCompany deletes a company (Web Panel) +// @Summary Delete company +// @Description Soft delete a company by setting status to inactive +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Success 200 {object} response.APIResponse "Company deleted successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id} [delete] +func (h *Handler) DeleteCompany(c echo.Context) error { + id := c.Param("id") + + // Get user ID from JWT token context + deletedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.DeleteCompany(c.Request().Context(), id, &deletedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to delete company") + } + + return response.Success(c, map[string]interface{}{ + "message": "Company deleted successfully", + }, "Company deleted successfully") +} + +// ListCompanies lists companies with filters and pagination (Web Panel) +// @Summary List companies with filters and pagination +// @Description Retrieve a paginated list of companies with advanced filtering options including search, type, status, industry, verification status, tags, and business criteria. Supports sorting and pagination. +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param search query string false "Search term to filter companies by name, description, or industry" +// @Param type query string false "Filter by company type" Enums(private, public, government, ngo, startup) +// @Param status query string false "Filter by company status" Enums(active, inactive, suspended, pending) +// @Param industry query string false "Filter by industry" +// @Param is_verified query boolean false "Filter by verification status" +// @Param is_compliant query boolean false "Filter by compliance status" +// @Param has_customer query boolean false "Filter by customer assignment status" +// @Param cpv_codes query array false "Filter by CPV codes" +// @Param categories query array false "Filter by categories" +// @Param keywords query array false "Filter by keywords" +// @Param specializations query array false "Filter by specializations" +// @Param employee_count_min query integer false "Minimum employee count" +// @Param employee_count_max query integer false "Maximum employee count" +// @Param annual_revenue_min query number false "Minimum annual revenue" +// @Param annual_revenue_max query number false "Maximum annual revenue" +// @Param founded_year_min query integer false "Minimum founded year" +// @Param founded_year_max query integer false "Maximum founded year" +// @Param limit query integer false "Number of companies per page (1-100)" minimum(1) maximum(100) default(20) +// @Param offset query integer false "Number of companies to skip for pagination" minimum(0) default(0) +// @Param sort_by query string false "Field to sort by" Enums(name, type, industry, created_at, updated_at, status, employee_count, annual_revenue) default(created_at) +// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc) +// @Success 200 {object} response.APIResponse{data=CompanyListResponse} "Companies retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies [get] +func (h *Handler) ListCompanies(c echo.Context) error { + form, err := response.Parse[ListCompaniesForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + result, err := h.service.ListCompanies(c.Request().Context(), form) + if err != nil { + return response.InternalServerError(c, "Failed to list companies") + } + + return response.Success(c, result, "Companies retrieved successfully") +} + +// SearchCompanies searches companies with advanced filters (Web Panel) +// @Summary Advanced search for companies +// @Description Search companies with advanced filtering capabilities including tags, business criteria, and location +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param q query string false "Search query" +// @Param cpv_codes query array false "CPV codes filter" +// @Param categories query array false "Categories filter" +// @Param keywords query array false "Keywords filter" +// @Param specializations query array false "Specializations filter" +// @Param industry query string false "Industry filter" +// @Param location query string false "Location filter" +// @Param min_employees query integer false "Minimum employees" +// @Param max_employees query integer false "Maximum employees" +// @Param min_revenue query number false "Minimum revenue" +// @Param max_revenue query number false "Maximum revenue" +// @Param is_verified query boolean false "Verification status" +// @Param is_compliant query boolean false "Compliance status" +// @Param limit query integer false "Limit" default(20) +// @Param offset query integer false "Offset" default(0) +// @Success 200 {object} response.APIResponse{data=CompanyListResponse} "Companies search completed" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/search [get] +func (h *Handler) SearchCompanies(c echo.Context) error { + form, err := response.Parse[CompanySearchForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + result, err := h.service.SearchCompanies(c.Request().Context(), form) + if err != nil { + return response.InternalServerError(c, "Failed to search companies") + } + + return response.Success(c, result, "Companies search completed") +} + +// UpdateCompanyStatus updates company status (Web Panel) +// @Summary Update company status +// @Description Update company account status (active, inactive, suspended, pending) +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Param status body UpdateCompanyStatusForm true "New company status" +// @Success 200 {object} response.APIResponse "Company status updated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/status [patch] +func (h *Handler) UpdateCompanyStatus(c echo.Context) error { + id := c.Param("id") + form, err := response.Parse[UpdateCompanyStatusForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + updatedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.UpdateCompanyStatus(c.Request().Context(), id, form, &updatedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to update company status") + } + + return response.Success(c, map[string]interface{}{ + "message": "Company status updated successfully", + }, "Company status updated successfully") +} + +// UpdateCompanyVerification updates company verification status (Web Panel) +// @Summary Update company verification status +// @Description Update company verification and compliance status +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Param verification body UpdateCompanyVerificationForm true "Verification status update" +// @Success 200 {object} response.APIResponse "Company verification updated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/verification [patch] +func (h *Handler) UpdateCompanyVerification(c echo.Context) error { + id := c.Param("id") + form, err := response.Parse[UpdateCompanyVerificationForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + updatedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.UpdateCompanyVerification(c.Request().Context(), id, form, &updatedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to update company verification") + } + + return response.Success(c, map[string]interface{}{ + "message": "Company verification updated successfully", + }, "Company verification updated successfully") +} + +// AssignCustomer assigns a customer to a company (Web Panel) +// @Summary Assign customer to company +// @Description Assign a customer to a company for login credentials +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Param assignment body AssignCustomerForm true "Customer assignment information" +// @Success 200 {object} response.APIResponse "Customer assigned successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 409 {object} response.APIResponse "Conflict - Customer already assigned" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/assign-customer [post] +func (h *Handler) AssignCustomer(c echo.Context) error { + id := c.Param("id") + form, err := response.Parse[AssignCustomerForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + assignedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.AssignCustomer(c.Request().Context(), id, form, &assignedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + if err.Error() == "customer is already assigned to another company" { + return response.Conflict(c, err.Error()) + } + return response.InternalServerError(c, "Failed to assign customer") + } + + return response.Success(c, map[string]interface{}{ + "message": "Customer assigned successfully", + }, "Customer assigned successfully") +} + +// UnassignCustomer unassigns a customer from a company (Web Panel) +// @Summary Unassign customer from company +// @Description Remove customer assignment from a company +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Success 200 {object} response.APIResponse "Customer unassigned successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/unassign-customer [post] +func (h *Handler) UnassignCustomer(c echo.Context) error { + id := c.Param("id") + + // Get user ID from JWT token context + unassignedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.UnassignCustomer(c.Request().Context(), id, &unassignedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to unassign customer") + } + + return response.Success(c, map[string]interface{}{ + "message": "Customer unassigned successfully", + }, "Customer unassigned successfully") +} + +// UpdateCompanyTags updates company tags (Web Panel) +// @Summary Update company tags +// @Description Update company tags (CPV, categories, keywords, specializations, certifications) +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Param tags body UpdateCompanyTagsForm true "Tags update information" +// @Success 200 {object} response.APIResponse "Company tags updated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/tags [put] +func (h *Handler) UpdateCompanyTags(c echo.Context) error { + id := c.Param("id") + form, err := response.Parse[UpdateCompanyTagsForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + updatedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.UpdateCompanyTags(c.Request().Context(), id, form, &updatedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to update company tags") + } + + return response.Success(c, map[string]interface{}{ + "message": "Company tags updated successfully", + }, "Company tags updated successfully") +} + +// AddTags adds tags to a company (Web Panel) +// @Summary Add tags to company +// @Description Add specific tags to a company +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Param tags body AddTagsForm true "Tags to add" +// @Success 200 {object} response.APIResponse "Tags added successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/tags/add [post] +func (h *Handler) AddTags(c echo.Context) error { + id := c.Param("id") + form, err := response.Parse[AddTagsForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + updatedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.AddTags(c.Request().Context(), id, form, &updatedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to add tags") + } + + return response.Success(c, map[string]interface{}{ + "message": "Tags added successfully", + }, "Tags added successfully") +} + +// RemoveTags removes tags from a company (Web Panel) +// @Summary Remove tags from company +// @Description Remove specific tags from a company +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Param tags body RemoveTagsForm true "Tags to remove" +// @Success 200 {object} response.APIResponse "Tags removed successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/tags/remove [post] +func (h *Handler) RemoveTags(c echo.Context) error { + id := c.Param("id") + form, err := response.Parse[RemoveTagsForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + updatedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.RemoveTags(c.Request().Context(), id, form, &updatedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to remove tags") + } + + return response.Success(c, map[string]interface{}{ + "message": "Tags removed successfully", + }, "Tags removed successfully") +} + +// VerifyCompany verifies a company (Web Panel) +// @Summary Verify company +// @Description Mark a company as verified +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Success 200 {object} response.APIResponse "Company verified successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/verify [post] +func (h *Handler) VerifyCompany(c echo.Context) error { + id := c.Param("id") + + // Get user ID from JWT token context + verifiedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.VerifyCompany(c.Request().Context(), id, &verifiedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to verify company") + } + + return response.Success(c, map[string]interface{}{ + "message": "Company verified successfully", + }, "Company verified successfully") +} + +// SuspendCompany suspends a company (Web Panel) +// @Summary Suspend company +// @Description Suspend a company account with reason +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Param suspension body SuspendCompanyForm true "Suspension information" +// @Success 200 {object} response.APIResponse "Company suspended successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/suspend [post] +func (h *Handler) SuspendCompany(c echo.Context) error { + id := c.Param("id") + form, err := response.Parse[SuspendCompanyForm](c) + if err != nil { + return response.ValidationError(c, "Invalid request data", err.Error()) + } + + // Get user ID from JWT token context + suspendedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.SuspendCompany(c.Request().Context(), id, form.Reason, &suspendedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to suspend company") + } + + return response.Success(c, map[string]interface{}{ + "message": "Company suspended successfully", + }, "Company suspended successfully") +} + +// ActivateCompany activates a company (Web Panel) +// @Summary Activate company +// @Description Activate a suspended company account +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param id path string true "Company ID" +// @Success 200 {object} response.APIResponse "Company activated successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid company ID" +// @Failure 404 {object} response.APIResponse "Not found - Company not found" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/{id}/activate [post] +func (h *Handler) ActivateCompany(c echo.Context) error { + id := c.Param("id") + + // Get user ID from JWT token context + activatedBy, err := user.GetUserIDFromContext(c) + if err != nil { + return response.Unauthorized(c, "User not authenticated") + } + + err = h.service.ActivateCompany(c.Request().Context(), id, &activatedBy) + if err != nil { + if err.Error() == "company not found" { + return response.NotFound(c, "Company not found") + } + return response.InternalServerError(c, "Failed to activate company") + } + + return response.Success(c, map[string]interface{}{ + "message": "Company activated successfully", + }, "Company activated successfully") +} + +// GetCompanyStats returns company statistics (Web Panel) +// @Summary Get company statistics +// @Description Get comprehensive company statistics for dashboard +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Success 200 {object} response.APIResponse{data=CompanyStatsResponse} "Company statistics retrieved successfully" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/stats [get] +func (h *Handler) GetCompanyStats(c echo.Context) error { + stats, err := h.service.GetCompanyStats(c.Request().Context()) + if err != nil { + return response.InternalServerError(c, "Failed to get company statistics") + } + + return response.Success(c, stats, "Company statistics retrieved successfully") +} + +// GetCompaniesByType retrieves companies by type (Web Panel) +// @Summary Get companies by type +// @Description Retrieve companies filtered by their type (private, public, government, ngo, startup) +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param type path string true "Company type" Enums(private, public, government, ngo, startup) +// @Param limit query integer false "Number of companies per page (1-100)" minimum(1) maximum(100) default(20) +// @Param offset query integer false "Number of companies to skip for pagination" minimum(0) default(0) +// @Success 200 {object} response.APIResponse{data=[]CompanyResponse,meta=response.Meta} "Companies retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid company type" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/type/{type} [get] +func (h *Handler) GetCompaniesByType(c echo.Context) error { + companyTypeStr := c.Param("type") + companyType := CompanyType(companyTypeStr) + + // Validate company type + if companyType != CompanyTypePrivate && companyType != CompanyTypePublic && + companyType != CompanyTypeGovernment && companyType != CompanyTypeNgo && + companyType != CompanyTypeStartup { + return response.BadRequest(c, "Invalid company type", "Must be private, public, government, ngo, or startup") + } + + limit := 20 + if limitStr := c.QueryParam("limit"); limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 { + limit = parsedLimit + } + } + + offset := 0 + if offsetStr := c.QueryParam("offset"); offsetStr != "" { + if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 { + offset = parsedOffset + } + } + + companies, total, err := h.service.GetCompaniesByType(c.Request().Context(), companyType, limit, offset) + if err != nil { + return response.InternalServerError(c, "Failed to retrieve companies by type") + } + + var companyResponses []*CompanyResponse + for _, company := range companies { + companyResponses = append(companyResponses, company.ToResponse()) + } + + meta := &response.Meta{ + Total: int(total), + Limit: limit, + Offset: offset, + } + + return response.SuccessWithMeta(c, companyResponses, meta, "Companies retrieved successfully") +} + +// GetCompaniesByStatus retrieves companies by status (Web Panel) +// @Summary Get companies by status +// @Description Retrieve companies filtered by their status (active, inactive, suspended, pending) +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param status path string true "Company status" Enums(active, inactive, suspended, pending) +// @Param limit query integer false "Number of companies per page (1-100)" minimum(1) maximum(100) default(20) +// @Param offset query integer false "Number of companies to skip for pagination" minimum(0) default(0) +// @Success 200 {object} response.APIResponse{data=[]CompanyResponse,meta=response.Meta} "Companies retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid company status" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/status/{status} [get] +func (h *Handler) GetCompaniesByStatus(c echo.Context) error { + statusStr := c.Param("status") + status := CompanyStatus(statusStr) + + // Validate company status + if status != CompanyStatusActive && status != CompanyStatusInactive && + status != CompanyStatusSuspended && status != CompanyStatusPending { + return response.BadRequest(c, "Invalid company status", "Must be active, inactive, suspended, or pending") + } + + limit := 20 + if limitStr := c.QueryParam("limit"); limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 { + limit = parsedLimit + } + } + + offset := 0 + if offsetStr := c.QueryParam("offset"); offsetStr != "" { + if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 { + offset = parsedOffset + } + } + + companies, total, err := h.service.GetCompaniesByStatus(c.Request().Context(), status, limit, offset) + if err != nil { + return response.InternalServerError(c, "Failed to retrieve companies by status") + } + + var companyResponses []*CompanyResponse + for _, company := range companies { + companyResponses = append(companyResponses, company.ToResponse()) + } + + meta := &response.Meta{ + Total: int(total), + Limit: limit, + Offset: offset, + } + + return response.SuccessWithMeta(c, companyResponses, meta, "Companies retrieved successfully") +} + +// GetCompaniesByIndustry retrieves companies by industry (Web Panel) +// @Summary Get companies by industry +// @Description Retrieve companies filtered by their industry +// @Tags Companies-Admin +// @Accept json +// @Produce json +// @Param industry path string true "Industry" +// @Param limit query integer false "Number of companies per page (1-100)" minimum(1) maximum(100) default(20) +// @Param offset query integer false "Number of companies to skip for pagination" minimum(0) default(0) +// @Success 200 {object} response.APIResponse{data=[]CompanyResponse,meta=response.Meta} "Companies retrieved successfully" +// @Failure 400 {object} response.APIResponse "Bad request - Invalid industry" +// @Failure 500 {object} response.APIResponse "Internal server error" +// @Security BearerAuth +// @Router /admin/v1/companies/industry/{industry} [get] +func (h *Handler) GetCompaniesByIndustry(c echo.Context) error { + industry := c.Param("industry") + + limit := 20 + if limitStr := c.QueryParam("limit"); limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 && parsedLimit <= 100 { + limit = parsedLimit + } + } + + offset := 0 + if offsetStr := c.QueryParam("offset"); offsetStr != "" { + if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 { + offset = parsedOffset + } + } + + companies, total, err := h.service.GetCompaniesByIndustry(c.Request().Context(), industry, limit, offset) + if err != nil { + return response.InternalServerError(c, "Failed to retrieve companies by industry") + } + + var companyResponses []*CompanyResponse + for _, company := range companies { + companyResponses = append(companyResponses, company.ToResponse()) + } + + meta := &response.Meta{ + Total: int(total), + Limit: limit, + Offset: offset, + } + + return response.SuccessWithMeta(c, companyResponses, meta, "Companies retrieved successfully") +} diff --git a/internal/company/repository.go b/internal/company/repository.go new file mode 100644 index 0000000..9ac1188 --- /dev/null +++ b/internal/company/repository.go @@ -0,0 +1,877 @@ +package company + +import ( + "context" + "errors" + "time" + "tm/pkg/logger" + mongopkg "tm/pkg/mongo" + + "go.mongodb.org/mongo-driver/bson" +) + +// Repository defines the interface for company data operations +type Repository interface { + Create(ctx context.Context, company *Company) error + GetByID(ctx context.Context, id string) (*Company, error) + GetByName(ctx context.Context, name string) (*Company, error) + GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Company, error) + GetByTaxID(ctx context.Context, taxID string) (*Company, error) + GetByCustomerID(ctx context.Context, customerID string) (*Company, error) + Update(ctx context.Context, company *Company) error + Delete(ctx context.Context, id string) error + List(ctx context.Context, limit, offset int) ([]*Company, error) + Search(ctx context.Context, search string, companyType *string, status *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, hasCustomer *bool, cpvCodes []string, categories []string, keywords []string, specializations []string, employeeCountMin *int, employeeCountMax *int, annualRevenueMin *float64, annualRevenueMax *float64, foundedYearMin *int, foundedYearMax *int, limit, offset int, sortBy, sortOrder string) ([]*Company, error) + Count(ctx context.Context) (int64, error) + CountByType(ctx context.Context, companyType CompanyType) (int64, error) + CountByStatus(ctx context.Context, status CompanyStatus) (int64, error) + CountByIndustry(ctx context.Context, industry string) (int64, error) + CountVerified(ctx context.Context) (int64, error) + CountWithCustomer(ctx context.Context) (int64, error) + UpdateStatus(ctx context.Context, id string, status CompanyStatus) error + UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error + AssignCustomer(ctx context.Context, id string, customerID string) error + UnassignCustomer(ctx context.Context, id string) error + UpdateTags(ctx context.Context, id string, tags *CompanyTags) error + AddTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error + RemoveTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error + GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error) + SearchByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error) +} + +// companyRepository implements the Repository interface using the MongoDB ORM +type companyRepository struct { + ormRepo mongopkg.Repository[Company] + logger logger.Logger +} + +// NewCompanyRepository creates a new company repository +func NewCompanyRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository { + // Create indexes using the ORM's index management + indexes := []mongopkg.Index{ + *mongopkg.CreateUniqueIndex("name_idx", bson.D{{Key: "name", Value: 1}}), + *mongopkg.CreateUniqueIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}), + *mongopkg.CreateUniqueIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}), + *mongopkg.NewIndex("customer_id_idx", bson.D{{Key: "customer_id", Value: 1}}), + *mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}), + *mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), + *mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}), + *mongopkg.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}), + *mongopkg.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}), + *mongopkg.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}), + *mongopkg.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}), + *mongopkg.NewIndex("employee_count_idx", bson.D{{Key: "employee_count", Value: 1}}), + *mongopkg.NewIndex("annual_revenue_idx", bson.D{{Key: "annual_revenue", Value: 1}}), + *mongopkg.NewIndex("founded_year_idx", bson.D{{Key: "founded_year", Value: 1}}), + *mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), + // Tag indexes for efficient search + *mongopkg.NewIndex("cpv_codes_idx", bson.D{{Key: "tags.cpv_codes", Value: 1}}), + *mongopkg.NewIndex("categories_idx", bson.D{{Key: "tags.categories", Value: 1}}), + *mongopkg.NewIndex("keywords_idx", bson.D{{Key: "tags.keywords", Value: 1}}), + *mongopkg.NewIndex("specializations_idx", bson.D{{Key: "tags.specializations", Value: 1}}), + *mongopkg.NewIndex("certifications_idx", bson.D{{Key: "tags.certifications", Value: 1}}), + // Text index for search + *mongopkg.CreateTextIndex("search_idx", "name", "description", "industry"), + } + + // Create indexes + err := mongoManager.CreateIndexes("companies", indexes) + if err != nil { + logger.Warn("Failed to create company indexes", map[string]interface{}{ + "error": err.Error(), + }) + } + + // Create ORM repository + ormRepo := mongopkg.NewRepository[Company](mongoManager.GetCollection("companies"), logger) + + return &companyRepository{ + ormRepo: ormRepo, + logger: logger, + } +} + +// Create creates a new company +func (r *companyRepository) Create(ctx context.Context, company *Company) error { + // Set created/updated timestamps using Unix timestamps + now := time.Now().Unix() + company.SetCreatedAt(now) + company.SetUpdatedAt(now) + + // Use ORM to create company + err := r.ormRepo.Create(ctx, company) + if err != nil { + r.logger.Error("Failed to create company", map[string]interface{}{ + "error": err.Error(), + "name": company.Name, + "registration_number": company.RegistrationNumber, + }) + return err + } + + r.logger.Info("Company created successfully", map[string]interface{}{ + "company_id": company.ID, + "name": company.Name, + "type": company.Type, + }) + + return nil +} + +// GetByID retrieves a company by ID +func (r *companyRepository) GetByID(ctx context.Context, id string) (*Company, error) { + company, err := r.ormRepo.FindByID(ctx, id) + if err != nil { + if errors.Is(err, mongopkg.ErrDocumentNotFound) { + return nil, errors.New("company not found") + } + r.logger.Error("Failed to get company by ID", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return nil, err + } + + return company, nil +} + +// GetByName retrieves a company by name +func (r *companyRepository) GetByName(ctx context.Context, name string) (*Company, error) { + filter := bson.M{"name": name} + company, err := r.ormRepo.FindOne(ctx, filter) + if err != nil { + if errors.Is(err, mongopkg.ErrDocumentNotFound) { + return nil, errors.New("company not found") + } + r.logger.Error("Failed to get company by name", map[string]interface{}{ + "error": err.Error(), + "name": name, + }) + return nil, err + } + + return company, nil +} + +// GetByRegistrationNumber retrieves a company by registration number +func (r *companyRepository) GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Company, error) { + filter := bson.M{"registration_number": registrationNumber} + company, err := r.ormRepo.FindOne(ctx, filter) + if err != nil { + if errors.Is(err, mongopkg.ErrDocumentNotFound) { + return nil, errors.New("company not found") + } + r.logger.Error("Failed to get company by registration number", map[string]interface{}{ + "error": err.Error(), + "registration_number": registrationNumber, + }) + return nil, err + } + + return company, nil +} + +// GetByTaxID retrieves a company by tax ID +func (r *companyRepository) GetByTaxID(ctx context.Context, taxID string) (*Company, error) { + filter := bson.M{"tax_id": taxID} + company, err := r.ormRepo.FindOne(ctx, filter) + if err != nil { + if errors.Is(err, mongopkg.ErrDocumentNotFound) { + return nil, errors.New("company not found") + } + r.logger.Error("Failed to get company by tax ID", map[string]interface{}{ + "error": err.Error(), + "tax_id": taxID, + }) + return nil, err + } + + return company, nil +} + +// GetByCustomerID retrieves a company by customer ID +func (r *companyRepository) GetByCustomerID(ctx context.Context, customerID string) (*Company, error) { + filter := bson.M{"customer_id": customerID} + company, err := r.ormRepo.FindOne(ctx, filter) + if err != nil { + if errors.Is(err, mongopkg.ErrDocumentNotFound) { + return nil, errors.New("company not found") + } + r.logger.Error("Failed to get company by customer ID", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + return nil, err + } + + return company, nil +} + +// Update updates a company +func (r *companyRepository) Update(ctx context.Context, company *Company) error { + // Set updated timestamp using Unix timestamp + company.SetUpdatedAt(time.Now().Unix()) + + // Use ORM to update company + err := r.ormRepo.Update(ctx, company) + if err != nil { + r.logger.Error("Failed to update company", map[string]interface{}{ + "error": err.Error(), + "company_id": company.ID, + }) + return err + } + + r.logger.Info("Company updated successfully", map[string]interface{}{ + "company_id": company.ID, + "name": company.Name, + }) + + return nil +} + +// Delete deletes a company (soft delete by setting status to inactive) +func (r *companyRepository) Delete(ctx context.Context, id string) error { + // Get company first + company, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // Update status to inactive + company.Status = CompanyStatusInactive + company.SetUpdatedAt(time.Now().Unix()) + + // Update in database + err = r.ormRepo.Update(ctx, company) + if err != nil { + r.logger.Error("Failed to delete company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return err + } + + r.logger.Info("Company deleted successfully", map[string]interface{}{ + "company_id": id, + }) + + return nil +} + +// List retrieves companies with pagination +func (r *companyRepository) List(ctx context.Context, limit, offset int) ([]*Company, error) { + // Build pagination + pagination := mongopkg.NewPaginationBuilder(). + Limit(limit). + Skip(offset). + SortDesc("created_at"). + Build() + + // Only active companies by default + filter := bson.M{"status": bson.M{"$ne": CompanyStatusInactive}} + + // Use ORM to find companies + result, err := r.ormRepo.FindAll(ctx, filter, pagination) + if err != nil { + r.logger.Error("Failed to list companies", map[string]interface{}{ + "error": err.Error(), + "limit": limit, + "offset": offset, + }) + return nil, err + } + + // Convert []Company to []*Company + companies := make([]*Company, len(result.Items)) + for i := range result.Items { + companies[i] = &result.Items[i] + } + + return companies, nil +} + +// Search retrieves companies with search and filters +func (r *companyRepository) Search(ctx context.Context, search string, companyType *string, status *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, hasCustomer *bool, cpvCodes []string, categories []string, keywords []string, specializations []string, employeeCountMin *int, employeeCountMax *int, annualRevenueMin *float64, annualRevenueMax *float64, foundedYearMin *int, foundedYearMax *int, limit, offset int, sortBy, sortOrder string) ([]*Company, error) { + // Build filter + filter := bson.M{} + + if search != "" { + filter["$text"] = bson.M{"$search": search} + } + + if companyType != nil { + filter["type"] = *companyType + } + + if status != nil { + filter["status"] = *status + } + + if industry != nil { + filter["industry"] = *industry + } + + if isVerified != nil { + filter["is_verified"] = *isVerified + } + + if isCompliant != nil { + filter["is_compliant"] = *isCompliant + } + + if language != nil { + filter["language"] = *language + } + + if currency != nil { + filter["currency"] = *currency + } + + if hasCustomer != nil { + if *hasCustomer { + filter["customer_id"] = bson.M{"$ne": nil} + } else { + filter["customer_id"] = nil + } + } + + // Tag filters + if len(cpvCodes) > 0 { + filter["tags.cpv_codes"] = bson.M{"$in": cpvCodes} + } + + if len(categories) > 0 { + filter["tags.categories"] = bson.M{"$in": categories} + } + + if len(keywords) > 0 { + filter["tags.keywords"] = bson.M{"$in": keywords} + } + + if len(specializations) > 0 { + filter["tags.specializations"] = bson.M{"$in": specializations} + } + + // Range filters + if employeeCountMin != nil || employeeCountMax != nil { + rangeFilter := bson.M{} + if employeeCountMin != nil { + rangeFilter["$gte"] = *employeeCountMin + } + if employeeCountMax != nil { + rangeFilter["$lte"] = *employeeCountMax + } + filter["employee_count"] = rangeFilter + } + + if annualRevenueMin != nil || annualRevenueMax != nil { + rangeFilter := bson.M{} + if annualRevenueMin != nil { + rangeFilter["$gte"] = *annualRevenueMin + } + if annualRevenueMax != nil { + rangeFilter["$lte"] = *annualRevenueMax + } + filter["annual_revenue"] = rangeFilter + } + + if foundedYearMin != nil || foundedYearMax != nil { + rangeFilter := bson.M{} + if foundedYearMin != nil { + rangeFilter["$gte"] = *foundedYearMin + } + if foundedYearMax != nil { + rangeFilter["$lte"] = *foundedYearMax + } + filter["founded_year"] = rangeFilter + } + + // Build pagination + pagination := mongopkg.NewPaginationBuilder(). + Limit(limit). + Skip(offset) + + // Set sort + if sortBy != "" { + if sortOrder == "desc" { + pagination.SortDesc(sortBy) + } else { + pagination.SortAsc(sortBy) + } + } else { + pagination.SortDesc("created_at") // Default sort + } + + // Use ORM to find companies + result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build()) + if err != nil { + r.logger.Error("Failed to search companies", map[string]interface{}{ + "error": err.Error(), + "search": search, + }) + return nil, err + } + + // Convert []Company to []*Company + companies := make([]*Company, len(result.Items)) + for i := range result.Items { + companies[i] = &result.Items[i] + } + + return companies, nil +} + +// Count counts all companies +func (r *companyRepository) Count(ctx context.Context) (int64, error) { + filter := bson.M{"status": bson.M{"$ne": CompanyStatusInactive}} + + count, err := r.ormRepo.Count(ctx, filter) + if err != nil { + r.logger.Error("Failed to count companies", map[string]interface{}{ + "error": err.Error(), + }) + return 0, err + } + + return count, nil +} + +// CountByType counts companies by type +func (r *companyRepository) CountByType(ctx context.Context, companyType CompanyType) (int64, error) { + filter := bson.M{ + "type": companyType, + "status": bson.M{"$ne": CompanyStatusInactive}, + } + + count, err := r.ormRepo.Count(ctx, filter) + if err != nil { + r.logger.Error("Failed to count companies by type", map[string]interface{}{ + "error": err.Error(), + "company_type": companyType, + }) + return 0, err + } + + return count, nil +} + +// CountByStatus counts companies by status +func (r *companyRepository) CountByStatus(ctx context.Context, status CompanyStatus) (int64, error) { + filter := bson.M{"status": status} + + count, err := r.ormRepo.Count(ctx, filter) + if err != nil { + r.logger.Error("Failed to count companies by status", map[string]interface{}{ + "error": err.Error(), + "status": status, + }) + return 0, err + } + + return count, nil +} + +// CountByIndustry counts companies by industry +func (r *companyRepository) CountByIndustry(ctx context.Context, industry string) (int64, error) { + filter := bson.M{ + "industry": industry, + "status": bson.M{"$ne": CompanyStatusInactive}, + } + + count, err := r.ormRepo.Count(ctx, filter) + if err != nil { + r.logger.Error("Failed to count companies by industry", map[string]interface{}{ + "error": err.Error(), + "industry": industry, + }) + return 0, err + } + + return count, nil +} + +// CountVerified counts verified companies +func (r *companyRepository) CountVerified(ctx context.Context) (int64, error) { + filter := bson.M{ + "is_verified": true, + "status": bson.M{"$ne": CompanyStatusInactive}, + } + + count, err := r.ormRepo.Count(ctx, filter) + if err != nil { + r.logger.Error("Failed to count verified companies", map[string]interface{}{ + "error": err.Error(), + }) + return 0, err + } + + return count, nil +} + +// CountWithCustomer counts companies with assigned customers +func (r *companyRepository) CountWithCustomer(ctx context.Context) (int64, error) { + filter := bson.M{ + "customer_id": bson.M{"$ne": nil}, + "status": bson.M{"$ne": CompanyStatusInactive}, + } + + count, err := r.ormRepo.Count(ctx, filter) + if err != nil { + r.logger.Error("Failed to count companies with customer", map[string]interface{}{ + "error": err.Error(), + }) + return 0, err + } + + return count, nil +} + +// UpdateStatus updates company status +func (r *companyRepository) UpdateStatus(ctx context.Context, id string, status CompanyStatus) error { + // Get company first + company, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // Update status + company.Status = status + company.SetUpdatedAt(time.Now().Unix()) + + // Update in database + err = r.ormRepo.Update(ctx, company) + if err != nil { + r.logger.Error("Failed to update company status", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + "status": status, + }) + return err + } + + r.logger.Info("Company status updated successfully", map[string]interface{}{ + "company_id": id, + "status": status, + }) + + return nil +} + +// UpdateVerification updates company verification status +func (r *companyRepository) UpdateVerification(ctx context.Context, id string, isVerified, isCompliant bool, complianceNotes *string) error { + // Get company first + company, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // Update verification fields + company.IsVerified = isVerified + company.IsCompliant = isCompliant + company.ComplianceNotes = complianceNotes + company.SetUpdatedAt(time.Now().Unix()) + + // Update in database + err = r.ormRepo.Update(ctx, company) + if err != nil { + r.logger.Error("Failed to update company verification", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return err + } + + r.logger.Info("Company verification updated successfully", map[string]interface{}{ + "company_id": id, + "is_verified": isVerified, + "is_compliant": isCompliant, + }) + + return nil +} + +// AssignCustomer assigns a customer to a company +func (r *companyRepository) AssignCustomer(ctx context.Context, id string, customerID string) error { + // Get company first + company, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // Assign customer + company.CustomerID = &customerID + company.SetUpdatedAt(time.Now().Unix()) + + // Update in database + err = r.ormRepo.Update(ctx, company) + if err != nil { + r.logger.Error("Failed to assign customer to company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + "customer_id": customerID, + }) + return err + } + + r.logger.Info("Customer assigned to company successfully", map[string]interface{}{ + "company_id": id, + "customer_id": customerID, + }) + + return nil +} + +// UnassignCustomer unassigns a customer from a company +func (r *companyRepository) UnassignCustomer(ctx context.Context, id string) error { + // Get company first + company, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // Unassign customer + company.CustomerID = nil + company.SetUpdatedAt(time.Now().Unix()) + + // Update in database + err = r.ormRepo.Update(ctx, company) + if err != nil { + r.logger.Error("Failed to unassign customer from company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return err + } + + r.logger.Info("Customer unassigned from company successfully", map[string]interface{}{ + "company_id": id, + }) + + return nil +} + +// UpdateTags updates company tags +func (r *companyRepository) UpdateTags(ctx context.Context, id string, tags *CompanyTags) error { + // Get company first + company, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // Update tags + company.Tags = tags + company.SetUpdatedAt(time.Now().Unix()) + + // Update in database + err = r.ormRepo.Update(ctx, company) + if err != nil { + r.logger.Error("Failed to update company tags", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return err + } + + r.logger.Info("Company tags updated successfully", map[string]interface{}{ + "company_id": id, + }) + + return nil +} + +// AddTags adds tags to a company +func (r *companyRepository) AddTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error { + // Get company first + company, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // Initialize tags if nil + if company.Tags == nil { + company.Tags = &CompanyTags{} + } + + // Add tags (avoiding duplicates) + company.Tags.CPVCodes = r.addUniqueStrings(company.Tags.CPVCodes, cpvCodes) + company.Tags.Categories = r.addUniqueStrings(company.Tags.Categories, categories) + company.Tags.Keywords = r.addUniqueStrings(company.Tags.Keywords, keywords) + company.Tags.Specializations = r.addUniqueStrings(company.Tags.Specializations, specializations) + company.Tags.Certifications = r.addUniqueStrings(company.Tags.Certifications, certifications) + + company.SetUpdatedAt(time.Now().Unix()) + + // Update in database + err = r.ormRepo.Update(ctx, company) + if err != nil { + r.logger.Error("Failed to add tags to company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return err + } + + r.logger.Info("Tags added to company successfully", map[string]interface{}{ + "company_id": id, + }) + + return nil +} + +// RemoveTags removes tags from a company +func (r *companyRepository) RemoveTags(ctx context.Context, id string, cpvCodes []string, categories []string, keywords []string, specializations []string, certifications []string) error { + // Get company first + company, err := r.GetByID(ctx, id) + if err != nil { + return err + } + + // Initialize tags if nil + if company.Tags == nil { + company.Tags = &CompanyTags{} + } + + // Remove tags + company.Tags.CPVCodes = r.removeStrings(company.Tags.CPVCodes, cpvCodes) + company.Tags.Categories = r.removeStrings(company.Tags.Categories, categories) + company.Tags.Keywords = r.removeStrings(company.Tags.Keywords, keywords) + company.Tags.Specializations = r.removeStrings(company.Tags.Specializations, specializations) + company.Tags.Certifications = r.removeStrings(company.Tags.Certifications, certifications) + + company.SetUpdatedAt(time.Now().Unix()) + + // Update in database + err = r.ormRepo.Update(ctx, company) + if err != nil { + r.logger.Error("Failed to remove tags from company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return err + } + + r.logger.Info("Tags removed from company successfully", map[string]interface{}{ + "company_id": id, + }) + + return nil +} + +// GetCompanyStats returns company statistics +func (r *companyRepository) GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error) { + // This would typically use aggregation pipelines for better performance + // For now, we'll use basic counts + + totalCompanies, _ := r.Count(ctx) + activeCompanies, _ := r.CountByStatus(ctx, CompanyStatusActive) + verifiedCompanies, _ := r.CountVerified(ctx) + companiesWithCustomer, _ := r.CountWithCustomer(ctx) + + // For this implementation, we'll return basic stats + // In a real implementation, you'd use MongoDB aggregation pipelines + stats := &CompanyStatsResponse{ + TotalCompanies: totalCompanies, + ActiveCompanies: activeCompanies, + VerifiedCompanies: verifiedCompanies, + CompaniesWithCustomer: companiesWithCustomer, + CompaniesByType: make(map[string]int64), + CompaniesByIndustry: make(map[string]int64), + CompaniesByStatus: make(map[string]int64), + TopCategories: []CategoryCount{}, + TopCPVCodes: []CPVCodeCount{}, + } + + return stats, nil +} + +// SearchByTags searches companies by tags +func (r *companyRepository) SearchByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error) { + filter := bson.M{} + + // Build tag filters + var tagFilters []bson.M + + if len(cpvCodes) > 0 { + tagFilters = append(tagFilters, bson.M{"tags.cpv_codes": bson.M{"$in": cpvCodes}}) + } + + if len(categories) > 0 { + tagFilters = append(tagFilters, bson.M{"tags.categories": bson.M{"$in": categories}}) + } + + if len(keywords) > 0 { + tagFilters = append(tagFilters, bson.M{"tags.keywords": bson.M{"$in": keywords}}) + } + + if len(specializations) > 0 { + tagFilters = append(tagFilters, bson.M{"tags.specializations": bson.M{"$in": specializations}}) + } + + if len(tagFilters) > 0 { + filter["$or"] = tagFilters + } + + // Only active companies + filter["status"] = bson.M{"$ne": CompanyStatusInactive} + + // Build pagination + pagination := mongopkg.NewPaginationBuilder(). + Limit(limit). + Skip(offset). + SortDesc("created_at"). + Build() + + // Use ORM to find companies + result, err := r.ormRepo.FindAll(ctx, filter, pagination) + if err != nil { + r.logger.Error("Failed to search companies by tags", map[string]interface{}{ + "error": err.Error(), + }) + return nil, err + } + + // Convert []Company to []*Company + companies := make([]*Company, len(result.Items)) + for i := range result.Items { + companies[i] = &result.Items[i] + } + + return companies, nil +} + +// Helper functions for tag management +func (r *companyRepository) addUniqueStrings(existing []string, new []string) []string { + existingMap := make(map[string]bool) + for _, s := range existing { + existingMap[s] = true + } + + result := make([]string, len(existing)) + copy(result, existing) + + for _, s := range new { + if !existingMap[s] { + result = append(result, s) + existingMap[s] = true + } + } + + return result +} + +func (r *companyRepository) removeStrings(existing []string, toRemove []string) []string { + removeMap := make(map[string]bool) + for _, s := range toRemove { + removeMap[s] = true + } + + var result []string + for _, s := range existing { + if !removeMap[s] { + result = append(result, s) + } + } + + return result +} diff --git a/internal/company/service.go b/internal/company/service.go new file mode 100644 index 0000000..95f46fb --- /dev/null +++ b/internal/company/service.go @@ -0,0 +1,874 @@ +package company + +import ( + "context" + "errors" + "time" + + "tm/pkg/logger" +) + +// Service defines business logic for company operations +type Service interface { + // Core company operations + CreateCompany(ctx context.Context, form *CreateCompanyForm, createdBy *string) (*Company, error) + GetCompanyByID(ctx context.Context, id string) (*Company, error) + UpdateCompany(ctx context.Context, id string, form *UpdateCompanyForm, updatedBy *string) (*Company, error) + DeleteCompany(ctx context.Context, id string, deletedBy *string) error + + // Company listing and search + ListCompanies(ctx context.Context, form *ListCompaniesForm) (*CompanyListResponse, error) + SearchCompanies(ctx context.Context, form *CompanySearchForm) (*CompanyListResponse, error) + SearchCompaniesByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error) + + // Company management + UpdateCompanyStatus(ctx context.Context, id string, form *UpdateCompanyStatusForm, updatedBy *string) error + UpdateCompanyVerification(ctx context.Context, id string, form *UpdateCompanyVerificationForm, updatedBy *string) error + + // Customer assignment + AssignCustomer(ctx context.Context, id string, form *AssignCustomerForm, assignedBy *string) error + UnassignCustomer(ctx context.Context, id string, unassignedBy *string) error + GetCompanyByCustomerID(ctx context.Context, customerID string) (*Company, error) + + // Tag management + UpdateCompanyTags(ctx context.Context, id string, form *UpdateCompanyTagsForm, updatedBy *string) error + AddTags(ctx context.Context, id string, form *AddTagsForm, updatedBy *string) error + RemoveTags(ctx context.Context, id string, form *RemoveTagsForm, updatedBy *string) error + + // Business operations + VerifyCompany(ctx context.Context, id string, verifiedBy *string) error + SuspendCompany(ctx context.Context, id string, reason string, suspendedBy *string) error + ActivateCompany(ctx context.Context, id string, activatedBy *string) error + + // Statistics and analytics + GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error) + + // Search and filtering helpers + GetCompaniesByType(ctx context.Context, companyType CompanyType, limit, offset int) ([]*Company, int64, error) + GetCompaniesByStatus(ctx context.Context, status CompanyStatus, limit, offset int) ([]*Company, int64, error) + GetCompaniesByIndustry(ctx context.Context, industry string, limit, offset int) ([]*Company, int64, error) +} + +// companyService implements the Service interface +type companyService struct { + repository Repository + logger logger.Logger +} + +// NewCompanyService creates a new company service +func NewCompanyService(repository Repository, logger logger.Logger) Service { + return &companyService{ + repository: repository, + logger: logger, + } +} + +// CreateCompany creates a new company +func (s *companyService) CreateCompany(ctx context.Context, form *CreateCompanyForm, createdBy *string) (*Company, error) { + // Check if company name already exists + existingCompany, _ := s.repository.GetByName(ctx, form.Name) + if existingCompany != nil { + return nil, errors.New("company with this name already exists") + } + + // Check if registration number already exists + existingCompany, _ = s.repository.GetByRegistrationNumber(ctx, form.RegistrationNumber) + if existingCompany != nil { + return nil, errors.New("company with this registration number already exists") + } + + // Check if tax ID already exists + existingCompany, _ = s.repository.GetByTaxID(ctx, form.TaxID) + if existingCompany != nil { + return nil, errors.New("company with this tax ID already exists") + } + + // Create company + company := &Company{ + Name: form.Name, + Type: CompanyType(form.Type), + Status: CompanyStatusPending, + RegistrationNumber: form.RegistrationNumber, + TaxID: form.TaxID, + Industry: form.Industry, + Description: form.Description, + Website: form.Website, + EmployeeCount: form.EmployeeCount, + AnnualRevenue: form.AnnualRevenue, + FoundedYear: form.FoundedYear, + Address: s.convertAddressForm(form.Address), + ContactPerson: s.convertContactPersonForm(form.ContactPerson), + Phone: form.Phone, + Email: form.Email, + Tags: s.convertCompanyTagsForm(form.Tags), + CustomerID: form.CustomerID, + IsVerified: false, + IsCompliant: false, + Language: s.getDefaultLanguage(form.Language), + Currency: s.getDefaultCurrency(form.Currency), + Timezone: s.getDefaultTimezone(form.Timezone), + CreatedBy: createdBy, + } + + // Save to database + err := s.repository.Create(ctx, company) + if err != nil { + s.logger.Error("Failed to create company", map[string]interface{}{ + "error": err.Error(), + "name": form.Name, + "registration_number": form.RegistrationNumber, + }) + return nil, err + } + + s.logger.Info("Company created successfully", map[string]interface{}{ + "company_id": company.ID, + "name": company.Name, + "type": company.Type, + "created_by": createdBy, + }) + + return company, nil +} + +// GetCompanyByID retrieves a company by ID +func (s *companyService) GetCompanyByID(ctx context.Context, id string) (*Company, error) { + company, err := s.repository.GetByID(ctx, id) + if err != nil { + s.logger.Error("Failed to get company by ID", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return nil, err + } + + s.logger.Info("Company retrieved successfully", map[string]interface{}{ + "company_id": company.ID, + "name": company.Name, + }) + + return company, nil +} + +// UpdateCompany updates a company +func (s *companyService) UpdateCompany(ctx context.Context, id string, form *UpdateCompanyForm, updatedBy *string) (*Company, error) { + // Get existing company + company, err := s.repository.GetByID(ctx, id) + if err != nil { + s.logger.Error("Failed to get company for update", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return nil, errors.New("company not found") + } + + // Check if name already exists (if changing name) + if form.Name != nil && *form.Name != company.Name { + existingCompany, _ := s.repository.GetByName(ctx, *form.Name) + if existingCompany != nil { + return nil, errors.New("company with this name already exists") + } + company.Name = *form.Name + } + + // Check if registration number already exists (if changing) + if form.RegistrationNumber != nil && *form.RegistrationNumber != company.RegistrationNumber { + existingCompany, _ := s.repository.GetByRegistrationNumber(ctx, *form.RegistrationNumber) + if existingCompany != nil { + return nil, errors.New("company with this registration number already exists") + } + company.RegistrationNumber = *form.RegistrationNumber + } + + // Check if tax ID already exists (if changing) + if form.TaxID != nil && *form.TaxID != company.TaxID { + existingCompany, _ := s.repository.GetByTaxID(ctx, *form.TaxID) + if existingCompany != nil { + return nil, errors.New("company with this tax ID already exists") + } + company.TaxID = *form.TaxID + } + + // Update fields if provided + if form.Type != nil { + company.Type = CompanyType(*form.Type) + } + + if form.Industry != nil { + company.Industry = *form.Industry + } + + if form.Description != nil { + company.Description = form.Description + } + + if form.Website != nil { + company.Website = form.Website + } + + if form.EmployeeCount != nil { + company.EmployeeCount = form.EmployeeCount + } + + if form.AnnualRevenue != nil { + company.AnnualRevenue = form.AnnualRevenue + } + + if form.FoundedYear != nil { + company.FoundedYear = form.FoundedYear + } + + if form.Address != nil { + company.Address = s.convertAddressForm(form.Address) + } + + if form.ContactPerson != nil { + company.ContactPerson = s.convertContactPersonForm(form.ContactPerson) + } + + if form.Phone != nil { + company.Phone = form.Phone + } + + if form.Email != nil { + company.Email = form.Email + } + + if form.Tags != nil { + company.Tags = s.convertCompanyTagsForm(form.Tags) + } + + if form.CustomerID != nil { + company.CustomerID = form.CustomerID + } + + if form.Language != nil { + company.Language = *form.Language + } + + if form.Currency != nil { + company.Currency = *form.Currency + } + + if form.Timezone != nil { + company.Timezone = *form.Timezone + } + + // Set updated by + company.UpdatedBy = updatedBy + company.UpdatedAt = time.Now().Unix() + + // Update in database + err = s.repository.Update(ctx, company) + if err != nil { + s.logger.Error("Failed to update company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return nil, errors.New("failed to update company") + } + + s.logger.Info("Company updated successfully", map[string]interface{}{ + "company_id": company.ID, + "name": company.Name, + "updated_by": updatedBy, + }) + + return company, nil +} + +// DeleteCompany deletes a company (soft delete) +func (s *companyService) DeleteCompany(ctx context.Context, id string, deletedBy *string) error { + // Get company to check if exists + company, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Set updated by before deletion + company.UpdatedBy = deletedBy + + // Delete company (soft delete) + err = s.repository.Delete(ctx, id) + if err != nil { + s.logger.Error("Failed to delete company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return errors.New("failed to delete company") + } + + s.logger.Info("Company deleted successfully", map[string]interface{}{ + "company_id": id, + "deleted_by": deletedBy, + }) + + return nil +} + +// ListCompanies lists companies with search and filters +func (s *companyService) ListCompanies(ctx context.Context, form *ListCompaniesForm) (*CompanyListResponse, error) { + // Set defaults + limit := 20 + if form.Limit != nil { + limit = *form.Limit + } + + offset := 0 + if form.Offset != nil { + offset = *form.Offset + } + + search := "" + if form.Search != nil { + search = *form.Search + } + + sortBy := "created_at" + if form.SortBy != nil { + sortBy = *form.SortBy + } + + sortOrder := "desc" + if form.SortOrder != nil { + sortOrder = *form.SortOrder + } + + // Get companies + companies, err := s.repository.Search(ctx, search, form.Type, form.Status, form.Industry, form.IsVerified, form.IsCompliant, form.Language, form.Currency, form.HasCustomer, form.CPVCodes, form.Categories, form.Keywords, form.Specializations, form.EmployeeCountMin, form.EmployeeCountMax, form.AnnualRevenueMin, form.AnnualRevenueMax, form.FoundedYearMin, form.FoundedYearMax, limit, offset, sortBy, sortOrder) + if err != nil { + s.logger.Error("Failed to list companies", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to list companies") + } + + // Convert to responses + var companyResponses []*CompanyResponse + for _, company := range companies { + companyResponses = append(companyResponses, company.ToResponse()) + } + + // TODO: Implement proper count for pagination metadata + total := int64(len(companies)) // This should be a separate count query + totalPages := (total + int64(limit) - 1) / int64(limit) + + return &CompanyListResponse{ + Companies: companyResponses, + Total: total, + Limit: limit, + Offset: offset, + TotalPages: int(totalPages), + }, nil +} + +// SearchCompanies searches companies with advanced filters +func (s *companyService) SearchCompanies(ctx context.Context, form *CompanySearchForm) (*CompanyListResponse, error) { + // Set defaults + limit := 20 + if form.Limit != nil { + limit = *form.Limit + } + + offset := 0 + if form.Offset != nil { + offset = *form.Offset + } + + query := "" + if form.Query != nil { + query = *form.Query + } + + // Get companies using search + companies, err := s.repository.Search(ctx, query, nil, nil, form.Industry, form.IsVerified, form.IsCompliant, nil, nil, nil, form.CPVCodes, form.Categories, form.Keywords, form.Specializations, form.MinEmployees, form.MaxEmployees, form.MinRevenue, form.MaxRevenue, nil, nil, limit, offset, "created_at", "desc") + if err != nil { + s.logger.Error("Failed to search companies", map[string]interface{}{ + "error": err.Error(), + "query": query, + }) + return nil, errors.New("failed to search companies") + } + + // Convert to responses + var companyResponses []*CompanyResponse + for _, company := range companies { + companyResponses = append(companyResponses, company.ToResponse()) + } + + // TODO: Implement proper count for pagination metadata + total := int64(len(companies)) // This should be a separate count query + totalPages := (total + int64(limit) - 1) / int64(limit) + + return &CompanyListResponse{ + Companies: companyResponses, + Total: total, + Limit: limit, + Offset: offset, + TotalPages: int(totalPages), + }, nil +} + +// SearchCompaniesByTags searches companies by tags +func (s *companyService) SearchCompaniesByTags(ctx context.Context, cpvCodes []string, categories []string, keywords []string, specializations []string, limit, offset int) ([]*Company, error) { + companies, err := s.repository.SearchByTags(ctx, cpvCodes, categories, keywords, specializations, limit, offset) + if err != nil { + s.logger.Error("Failed to search companies by tags", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to search companies by tags") + } + + return companies, nil +} + +// UpdateCompanyStatus updates company status +func (s *companyService) UpdateCompanyStatus(ctx context.Context, id string, form *UpdateCompanyStatusForm, updatedBy *string) error { + // Get company to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Update status + status := CompanyStatus(form.Status) + err = s.repository.UpdateStatus(ctx, id, status) + if err != nil { + s.logger.Error("Failed to update company status", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + "status": form.Status, + }) + return errors.New("failed to update company status") + } + + s.logger.Info("Company status updated successfully", map[string]interface{}{ + "company_id": id, + "status": form.Status, + "updated_by": updatedBy, + }) + + return nil +} + +// UpdateCompanyVerification updates company verification status +func (s *companyService) UpdateCompanyVerification(ctx context.Context, id string, form *UpdateCompanyVerificationForm, updatedBy *string) error { + // Get company to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Update verification + err = s.repository.UpdateVerification(ctx, id, form.IsVerified, form.IsCompliant, form.ComplianceNotes) + if err != nil { + s.logger.Error("Failed to update company verification", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return errors.New("failed to update company verification") + } + + s.logger.Info("Company verification updated successfully", map[string]interface{}{ + "company_id": id, + "is_verified": form.IsVerified, + "is_compliant": form.IsCompliant, + "updated_by": updatedBy, + }) + + return nil +} + +// AssignCustomer assigns a customer to a company +func (s *companyService) AssignCustomer(ctx context.Context, id string, form *AssignCustomerForm, assignedBy *string) error { + // Get company to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Check if customer is already assigned to another company + existingCompany, _ := s.repository.GetByCustomerID(ctx, form.CustomerID) + if existingCompany != nil { + return errors.New("customer is already assigned to another company") + } + + // Assign customer + err = s.repository.AssignCustomer(ctx, id, form.CustomerID) + if err != nil { + s.logger.Error("Failed to assign customer to company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + "customer_id": form.CustomerID, + }) + return errors.New("failed to assign customer to company") + } + + s.logger.Info("Customer assigned to company successfully", map[string]interface{}{ + "company_id": id, + "customer_id": form.CustomerID, + "assigned_by": assignedBy, + }) + + return nil +} + +// UnassignCustomer unassigns a customer from a company +func (s *companyService) UnassignCustomer(ctx context.Context, id string, unassignedBy *string) error { + // Get company to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Unassign customer + err = s.repository.UnassignCustomer(ctx, id) + if err != nil { + s.logger.Error("Failed to unassign customer from company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return errors.New("failed to unassign customer from company") + } + + s.logger.Info("Customer unassigned from company successfully", map[string]interface{}{ + "company_id": id, + "unassigned_by": unassignedBy, + }) + + return nil +} + +// GetCompanyByCustomerID retrieves a company by customer ID +func (s *companyService) GetCompanyByCustomerID(ctx context.Context, customerID string) (*Company, error) { + company, err := s.repository.GetByCustomerID(ctx, customerID) + if err != nil { + s.logger.Error("Failed to get company by customer ID", map[string]interface{}{ + "error": err.Error(), + "customer_id": customerID, + }) + return nil, err + } + + s.logger.Info("Company retrieved by customer ID successfully", map[string]interface{}{ + "company_id": company.ID, + "customer_id": customerID, + }) + + return company, nil +} + +// UpdateCompanyTags updates company tags +func (s *companyService) UpdateCompanyTags(ctx context.Context, id string, form *UpdateCompanyTagsForm, updatedBy *string) error { + // Get company to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Update tags + tags := s.convertCompanyTagsForm(form.Tags) + err = s.repository.UpdateTags(ctx, id, tags) + if err != nil { + s.logger.Error("Failed to update company tags", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return errors.New("failed to update company tags") + } + + s.logger.Info("Company tags updated successfully", map[string]interface{}{ + "company_id": id, + "updated_by": updatedBy, + }) + + return nil +} + +// AddTags adds tags to a company +func (s *companyService) AddTags(ctx context.Context, id string, form *AddTagsForm, updatedBy *string) error { + // Get company to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Add tags + err = s.repository.AddTags(ctx, id, form.CPVCodes, form.Categories, form.Keywords, form.Specializations, form.Certifications) + if err != nil { + s.logger.Error("Failed to add tags to company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return errors.New("failed to add tags to company") + } + + s.logger.Info("Tags added to company successfully", map[string]interface{}{ + "company_id": id, + "updated_by": updatedBy, + }) + + return nil +} + +// RemoveTags removes tags from a company +func (s *companyService) RemoveTags(ctx context.Context, id string, form *RemoveTagsForm, updatedBy *string) error { + // Get company to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Remove tags + err = s.repository.RemoveTags(ctx, id, form.CPVCodes, form.Categories, form.Keywords, form.Specializations, form.Certifications) + if err != nil { + s.logger.Error("Failed to remove tags from company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return errors.New("failed to remove tags from company") + } + + s.logger.Info("Tags removed from company successfully", map[string]interface{}{ + "company_id": id, + "updated_by": updatedBy, + }) + + return nil +} + +// VerifyCompany verifies a company +func (s *companyService) VerifyCompany(ctx context.Context, id string, verifiedBy *string) error { + // Get company to check if exists + company, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Update verification + err = s.repository.UpdateVerification(ctx, id, true, company.IsCompliant, company.ComplianceNotes) + if err != nil { + s.logger.Error("Failed to verify company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return errors.New("failed to verify company") + } + + s.logger.Info("Company verified successfully", map[string]interface{}{ + "company_id": id, + "verified_by": verifiedBy, + }) + + return nil +} + +// SuspendCompany suspends a company +func (s *companyService) SuspendCompany(ctx context.Context, id string, reason string, suspendedBy *string) error { + // Get company to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Update status to suspended + err = s.repository.UpdateStatus(ctx, id, CompanyStatusSuspended) + if err != nil { + s.logger.Error("Failed to suspend company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return errors.New("failed to suspend company") + } + + s.logger.Info("Company suspended successfully", map[string]interface{}{ + "company_id": id, + "reason": reason, + "suspended_by": suspendedBy, + }) + + return nil +} + +// ActivateCompany activates a company +func (s *companyService) ActivateCompany(ctx context.Context, id string, activatedBy *string) error { + // Get company to check if exists + _, err := s.repository.GetByID(ctx, id) + if err != nil { + return errors.New("company not found") + } + + // Update status to active + err = s.repository.UpdateStatus(ctx, id, CompanyStatusActive) + if err != nil { + s.logger.Error("Failed to activate company", map[string]interface{}{ + "error": err.Error(), + "company_id": id, + }) + return errors.New("failed to activate company") + } + + s.logger.Info("Company activated successfully", map[string]interface{}{ + "company_id": id, + "activated_by": activatedBy, + }) + + return nil +} + +// GetCompanyStats returns company statistics +func (s *companyService) GetCompanyStats(ctx context.Context) (*CompanyStatsResponse, error) { + stats, err := s.repository.GetCompanyStats(ctx) + if err != nil { + s.logger.Error("Failed to get company stats", map[string]interface{}{ + "error": err.Error(), + }) + return nil, errors.New("failed to get company statistics") + } + + return stats, nil +} + +// GetCompaniesByType retrieves companies by type with pagination +func (s *companyService) GetCompaniesByType(ctx context.Context, companyType CompanyType, limit, offset int) ([]*Company, int64, error) { + // Use search method with type filter + typeStr := string(companyType) + companies, err := s.repository.Search(ctx, "", &typeStr, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc") + if err != nil { + s.logger.Error("Failed to get companies by type", map[string]interface{}{ + "error": err.Error(), + "company_type": companyType, + }) + return nil, 0, errors.New("failed to get companies by type") + } + + // Get total count + total, err := s.repository.CountByType(ctx, companyType) + if err != nil { + s.logger.Error("Failed to count companies by type", map[string]interface{}{ + "error": err.Error(), + "company_type": companyType, + }) + return companies, 0, nil // Return companies even if count fails + } + + return companies, total, nil +} + +// GetCompaniesByStatus retrieves companies by status with pagination +func (s *companyService) GetCompaniesByStatus(ctx context.Context, status CompanyStatus, limit, offset int) ([]*Company, int64, error) { + // Use search method with status filter + statusStr := string(status) + companies, err := s.repository.Search(ctx, "", nil, &statusStr, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc") + if err != nil { + s.logger.Error("Failed to get companies by status", map[string]interface{}{ + "error": err.Error(), + "status": status, + }) + return nil, 0, errors.New("failed to get companies by status") + } + + // Get total count + total, err := s.repository.CountByStatus(ctx, status) + if err != nil { + s.logger.Error("Failed to count companies by status", map[string]interface{}{ + "error": err.Error(), + "status": status, + }) + return companies, 0, nil + } + + return companies, total, nil +} + +// GetCompaniesByIndustry retrieves companies by industry with pagination +func (s *companyService) GetCompaniesByIndustry(ctx context.Context, industry string, limit, offset int) ([]*Company, int64, error) { + // Use search method with industry filter + companies, err := s.repository.Search(ctx, "", nil, nil, &industry, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, limit, offset, "created_at", "desc") + if err != nil { + s.logger.Error("Failed to get companies by industry", map[string]interface{}{ + "error": err.Error(), + "industry": industry, + }) + return nil, 0, errors.New("failed to get companies by industry") + } + + // Get total count + total, err := s.repository.CountByIndustry(ctx, industry) + if err != nil { + s.logger.Error("Failed to count companies by industry", map[string]interface{}{ + "error": err.Error(), + "industry": industry, + }) + return companies, 0, nil + } + + return companies, total, nil +} + +// Helper methods for converting forms to domain objects +func (s *companyService) convertAddressForm(form *AddressForm) *Address { + if form == nil { + return nil + } + + return &Address{ + Street: form.Street, + City: form.City, + State: form.State, + PostalCode: form.PostalCode, + Country: form.Country, + } +} + +func (s *companyService) convertContactPersonForm(form *ContactPersonForm) *ContactPerson { + if form == nil { + return nil + } + + return &ContactPerson{ + FirstName: form.FirstName, + LastName: form.LastName, + FullName: form.FullName, + Position: form.Position, + Email: form.Email, + Phone: form.Phone, + Mobile: form.Mobile, + IsPrimary: form.IsPrimary, + } +} + +func (s *companyService) convertCompanyTagsForm(form *CompanyTagsForm) *CompanyTags { + if form == nil { + return nil + } + + return &CompanyTags{ + CPVCodes: form.CPVCodes, + Categories: form.Categories, + Keywords: form.Keywords, + Specializations: form.Specializations, + Certifications: form.Certifications, + } +} + +func (s *companyService) getDefaultLanguage(language *string) string { + if language != nil { + return *language + } + return "en" +} + +func (s *companyService) getDefaultCurrency(currency *string) string { + if currency != nil { + return *currency + } + return "USD" +} + +func (s *companyService) getDefaultTimezone(timezone *string) string { + if timezone != nil { + return *timezone + } + return "UTC" +}