Add Inquiry Management Functionality and Update API Documentation
- Introduced a new inquiry management feature, including CRUD operations for inquiries in both admin and public API endpoints. - Implemented inquiry entity, service, repository, and handler layers following the clean architecture principles. - Added new API routes for searching, retrieving, updating, and deleting inquiries, enhancing the overall functionality of the system. - Updated Swagger documentation to include detailed descriptions and examples for the new inquiry endpoints, ensuring clarity for API consumers. - Enhanced error handling for duplicate inquiries and validation, improving the robustness of the inquiry management process. - Updated existing routes to integrate inquiry handling, ensuring a seamless user experience across the application.
This commit is contained in:
@@ -2765,6 +2765,284 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/v1/inquiries": {
|
||||
"get": {
|
||||
"description": "Search and filter inquiries with pagination support. This endpoint allows administrators to find inquiries based on various criteria including name, company, email, status, and other filters.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin-Inquiries"
|
||||
],
|
||||
"summary": "Search inquiries",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search term for full name, company name, or email",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by status (pending, reviewed, approved, rejected)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit results (1-100)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset results",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sort field (full_name, company_name, work_email, status, created_at)",
|
||||
"name": "sort_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sort order (asc, desc)",
|
||||
"name": "sort_order",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Inquiries retrieved successfully with pagination metadata",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/inquiry.InquiryListResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid query parameters",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error - Invalid parameter values",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/v1/inquiries/{id}": {
|
||||
"get": {
|
||||
"description": "Retrieve detailed information about a specific inquiry by its unique identifier. This endpoint provides complete inquiry details including contact information, status, and status history.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin-Inquiries"
|
||||
],
|
||||
"summary": "Get inquiry by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Inquiry ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Inquiry retrieved successfully with complete details",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/inquiry.InquiryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid inquiry ID format",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found - Inquiry with specified ID not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Delete an inquiry permanently from the system. This action cannot be undone and will remove all inquiry data including contact information, status history, and timestamps.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin-Inquiries"
|
||||
],
|
||||
"summary": "Delete inquiry",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Inquiry ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Inquiry deleted successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid inquiry ID format",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found - Inquiry with specified ID not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/v1/inquiries/{id}/status": {
|
||||
"put": {
|
||||
"description": "Update the status of an inquiry with reason and optional description. This endpoint allows administrators to change inquiry status following the defined workflow: pending -\u003e reviewed -\u003e approved/rejected.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin-Inquiries"
|
||||
],
|
||||
"summary": "Update inquiry status",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Inquiry ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Status update information including new status, reason, and optional description",
|
||||
"name": "status",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/inquiry.UpdateInquiryStatusForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Inquiry status updated successfully with updated details",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/inquiry.InquiryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid inquiry ID format or invalid status transition",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found - Inquiry with specified ID not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error - Invalid data format or validation rules not met",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/v1/profile": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -4820,6 +5098,76 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/inquiries": {
|
||||
"post": {
|
||||
"description": "Create a new inquiry with contact information for potential business opportunities. This endpoint allows external users to submit inquiries about services or partnerships. Duplicate submissions are prevented based on email and phone number.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Inquiries"
|
||||
],
|
||||
"summary": "Create new inquiry",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Inquiry information including contact details and company information",
|
||||
"name": "inquiry",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/inquiry.CreateInquiryForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Inquiry created successfully with assigned ID and timestamps",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/inquiry.InquiryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid input data or missing required fields",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict - Duplicate inquiry already exists with same email or phone number",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error - Invalid data format or validation rules not met",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/profile": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -6624,6 +6972,117 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.CreateInquiryForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"company_name": {
|
||||
"description": "Company name",
|
||||
"type": "string",
|
||||
"example": "Opplens"
|
||||
},
|
||||
"full_name": {
|
||||
"description": "Full name of the inquirer",
|
||||
"type": "string",
|
||||
"example": "John Doe"
|
||||
},
|
||||
"phone_number": {
|
||||
"description": "Phone number",
|
||||
"type": "string",
|
||||
"example": "+1234567890"
|
||||
},
|
||||
"work_email": {
|
||||
"description": "Work email address",
|
||||
"type": "string",
|
||||
"example": "info@opplens.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.InquiryListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inquiries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/inquiry.InquiryResponse"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/response.Meta"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.InquiryResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"company_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"full_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"status_history": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/inquiry.StatusHistoryResponse"
|
||||
}
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"work_email": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.StatusHistoryResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"changed_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.UpdateInquiryStatusForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "Optional description",
|
||||
"type": "string",
|
||||
"example": "Additional notes about the review"
|
||||
},
|
||||
"reason": {
|
||||
"description": "Reason for status change",
|
||||
"type": "string",
|
||||
"example": "Initial review completed"
|
||||
},
|
||||
"status": {
|
||||
"description": "New status",
|
||||
"type": "string",
|
||||
"example": "reviewed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response.APIError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7305,6 +7764,10 @@ const docTemplate = `{
|
||||
"description": "Administrative tender approval management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination",
|
||||
"name": "Admin-TenderApprovals"
|
||||
},
|
||||
{
|
||||
"description": "Administrative inquiry management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination",
|
||||
"name": "Admin-Inquiries"
|
||||
},
|
||||
{
|
||||
"description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access",
|
||||
"name": "Authorization"
|
||||
@@ -7324,6 +7787,10 @@ const docTemplate = `{
|
||||
{
|
||||
"description": "Public tender approval management operations for mobile application including tender approval listing and detailed tender approval information",
|
||||
"name": "TenderApprovals"
|
||||
},
|
||||
{
|
||||
"description": "Public inquiry management operations for mobile application including inquiry submission and inquiry listing",
|
||||
"name": "Inquiries"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
@@ -2759,6 +2759,284 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/v1/inquiries": {
|
||||
"get": {
|
||||
"description": "Search and filter inquiries with pagination support. This endpoint allows administrators to find inquiries based on various criteria including name, company, email, status, and other filters.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin-Inquiries"
|
||||
],
|
||||
"summary": "Search inquiries",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search term for full name, company name, or email",
|
||||
"name": "q",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by status (pending, reviewed, approved, rejected)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Limit results (1-100)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Offset results",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sort field (full_name, company_name, work_email, status, created_at)",
|
||||
"name": "sort_by",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Sort order (asc, desc)",
|
||||
"name": "sort_order",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Inquiries retrieved successfully with pagination metadata",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/inquiry.InquiryListResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid query parameters",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error - Invalid parameter values",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/v1/inquiries/{id}": {
|
||||
"get": {
|
||||
"description": "Retrieve detailed information about a specific inquiry by its unique identifier. This endpoint provides complete inquiry details including contact information, status, and status history.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin-Inquiries"
|
||||
],
|
||||
"summary": "Get inquiry by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Inquiry ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Inquiry retrieved successfully with complete details",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/inquiry.InquiryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid inquiry ID format",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found - Inquiry with specified ID not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Delete an inquiry permanently from the system. This action cannot be undone and will remove all inquiry data including contact information, status history, and timestamps.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin-Inquiries"
|
||||
],
|
||||
"summary": "Delete inquiry",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Inquiry ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Inquiry deleted successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid inquiry ID format",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found - Inquiry with specified ID not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/v1/inquiries/{id}/status": {
|
||||
"put": {
|
||||
"description": "Update the status of an inquiry with reason and optional description. This endpoint allows administrators to change inquiry status following the defined workflow: pending -\u003e reviewed -\u003e approved/rejected.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Admin-Inquiries"
|
||||
],
|
||||
"summary": "Update inquiry status",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Inquiry ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Status update information including new status, reason, and optional description",
|
||||
"name": "status",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/inquiry.UpdateInquiryStatusForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Inquiry status updated successfully with updated details",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/inquiry.InquiryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid inquiry ID format or invalid status transition",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not found - Inquiry with specified ID not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error - Invalid data format or validation rules not met",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/v1/profile": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -4814,6 +5092,76 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/inquiries": {
|
||||
"post": {
|
||||
"description": "Create a new inquiry with contact information for potential business opportunities. This endpoint allows external users to submit inquiries about services or partnerships. Duplicate submissions are prevented based on email and phone number.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Inquiries"
|
||||
],
|
||||
"summary": "Create new inquiry",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Inquiry information including contact details and company information",
|
||||
"name": "inquiry",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/inquiry.CreateInquiryForm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Inquiry created successfully with assigned ID and timestamps",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/inquiry.InquiryResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request - Invalid input data or missing required fields",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "Conflict - Duplicate inquiry already exists with same email or phone number",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation error - Invalid data format or validation rules not met",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/response.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/profile": {
|
||||
"get": {
|
||||
"security": [
|
||||
@@ -6618,6 +6966,117 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.CreateInquiryForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"company_name": {
|
||||
"description": "Company name",
|
||||
"type": "string",
|
||||
"example": "Opplens"
|
||||
},
|
||||
"full_name": {
|
||||
"description": "Full name of the inquirer",
|
||||
"type": "string",
|
||||
"example": "John Doe"
|
||||
},
|
||||
"phone_number": {
|
||||
"description": "Phone number",
|
||||
"type": "string",
|
||||
"example": "+1234567890"
|
||||
},
|
||||
"work_email": {
|
||||
"description": "Work email address",
|
||||
"type": "string",
|
||||
"example": "info@opplens.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.InquiryListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"inquiries": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/inquiry.InquiryResponse"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/response.Meta"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.InquiryResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"company_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"full_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone_number": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"status_history": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/inquiry.StatusHistoryResponse"
|
||||
}
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"work_email": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.StatusHistoryResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"changed_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inquiry.UpdateInquiryStatusForm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"description": "Optional description",
|
||||
"type": "string",
|
||||
"example": "Additional notes about the review"
|
||||
},
|
||||
"reason": {
|
||||
"description": "Reason for status change",
|
||||
"type": "string",
|
||||
"example": "Initial review completed"
|
||||
},
|
||||
"status": {
|
||||
"description": "New status",
|
||||
"type": "string",
|
||||
"example": "reviewed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"response.APIError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7299,6 +7758,10 @@
|
||||
"description": "Administrative tender approval management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination",
|
||||
"name": "Admin-TenderApprovals"
|
||||
},
|
||||
{
|
||||
"description": "Administrative inquiry management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination",
|
||||
"name": "Admin-Inquiries"
|
||||
},
|
||||
{
|
||||
"description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access",
|
||||
"name": "Authorization"
|
||||
@@ -7318,6 +7781,10 @@
|
||||
{
|
||||
"description": "Public tender approval management operations for mobile application including tender approval listing and detailed tender approval information",
|
||||
"name": "TenderApprovals"
|
||||
},
|
||||
{
|
||||
"description": "Public inquiry management operations for mobile application including inquiry submission and inquiry listing",
|
||||
"name": "Inquiries"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -630,6 +630,83 @@ definitions:
|
||||
- feedback_type
|
||||
- tender_id
|
||||
type: object
|
||||
inquiry.CreateInquiryForm:
|
||||
properties:
|
||||
company_name:
|
||||
description: Company name
|
||||
example: Opplens
|
||||
type: string
|
||||
full_name:
|
||||
description: Full name of the inquirer
|
||||
example: John Doe
|
||||
type: string
|
||||
phone_number:
|
||||
description: Phone number
|
||||
example: "+1234567890"
|
||||
type: string
|
||||
work_email:
|
||||
description: Work email address
|
||||
example: info@opplens.com
|
||||
type: string
|
||||
type: object
|
||||
inquiry.InquiryListResponse:
|
||||
properties:
|
||||
inquiries:
|
||||
items:
|
||||
$ref: '#/definitions/inquiry.InquiryResponse'
|
||||
type: array
|
||||
meta:
|
||||
$ref: '#/definitions/response.Meta'
|
||||
type: object
|
||||
inquiry.InquiryResponse:
|
||||
properties:
|
||||
company_name:
|
||||
type: string
|
||||
created_at:
|
||||
type: integer
|
||||
full_name:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
phone_number:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
status_history:
|
||||
items:
|
||||
$ref: '#/definitions/inquiry.StatusHistoryResponse'
|
||||
type: array
|
||||
updated_at:
|
||||
type: integer
|
||||
work_email:
|
||||
type: string
|
||||
type: object
|
||||
inquiry.StatusHistoryResponse:
|
||||
properties:
|
||||
changed_at:
|
||||
type: integer
|
||||
description:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
type: object
|
||||
inquiry.UpdateInquiryStatusForm:
|
||||
properties:
|
||||
description:
|
||||
description: Optional description
|
||||
example: Additional notes about the review
|
||||
type: string
|
||||
reason:
|
||||
description: Reason for status change
|
||||
example: Initial review completed
|
||||
type: string
|
||||
status:
|
||||
description: New status
|
||||
example: reviewed
|
||||
type: string
|
||||
type: object
|
||||
response.APIError:
|
||||
properties:
|
||||
code:
|
||||
@@ -2825,6 +2902,191 @@ paths:
|
||||
summary: Health check endpoint
|
||||
tags:
|
||||
- Admin-Health
|
||||
/admin/v1/inquiries:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Search and filter inquiries with pagination support. This endpoint
|
||||
allows administrators to find inquiries based on various criteria including
|
||||
name, company, email, status, and other filters.
|
||||
parameters:
|
||||
- description: Search term for full name, company name, or email
|
||||
in: query
|
||||
name: q
|
||||
type: string
|
||||
- description: Filter by status (pending, reviewed, approved, rejected)
|
||||
in: query
|
||||
name: status
|
||||
type: string
|
||||
- description: Limit results (1-100)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
- description: Offset results
|
||||
in: query
|
||||
name: offset
|
||||
type: integer
|
||||
- description: Sort field (full_name, company_name, work_email, status, created_at)
|
||||
in: query
|
||||
name: sort_by
|
||||
type: string
|
||||
- description: Sort order (asc, desc)
|
||||
in: query
|
||||
name: sort_order
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Inquiries retrieved successfully with pagination metadata
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/response.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/inquiry.InquiryListResponse'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad request - Invalid query parameters
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"422":
|
||||
description: Validation error - Invalid parameter values
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Search inquiries
|
||||
tags:
|
||||
- Admin-Inquiries
|
||||
/admin/v1/inquiries/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Delete an inquiry permanently from the system. This action cannot
|
||||
be undone and will remove all inquiry data including contact information,
|
||||
status history, and timestamps.
|
||||
parameters:
|
||||
- description: Inquiry ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Inquiry deleted successfully
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"400":
|
||||
description: Bad request - Invalid inquiry ID format
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"404":
|
||||
description: Not found - Inquiry with specified ID not found
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Delete inquiry
|
||||
tags:
|
||||
- Admin-Inquiries
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Retrieve detailed information about a specific inquiry by its unique
|
||||
identifier. This endpoint provides complete inquiry details including contact
|
||||
information, status, and status history.
|
||||
parameters:
|
||||
- description: Inquiry ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Inquiry retrieved successfully with complete details
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/response.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/inquiry.InquiryResponse'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad request - Invalid inquiry ID format
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"404":
|
||||
description: Not found - Inquiry with specified ID not found
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Get inquiry by ID
|
||||
tags:
|
||||
- Admin-Inquiries
|
||||
/admin/v1/inquiries/{id}/status:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 'Update the status of an inquiry with reason and optional description.
|
||||
This endpoint allows administrators to change inquiry status following the
|
||||
defined workflow: pending -> reviewed -> approved/rejected.'
|
||||
parameters:
|
||||
- description: Inquiry ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Status update information including new status, reason, and optional
|
||||
description
|
||||
in: body
|
||||
name: status
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/inquiry.UpdateInquiryStatusForm'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Inquiry status updated successfully with updated details
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/response.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/inquiry.InquiryResponse'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad request - Invalid inquiry ID format or invalid status transition
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"404":
|
||||
description: Not found - Inquiry with specified ID not found
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"422":
|
||||
description: Validation error - Invalid data format or validation rules
|
||||
not met
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Update inquiry status
|
||||
tags:
|
||||
- Admin-Inquiries
|
||||
/admin/v1/profile:
|
||||
get:
|
||||
consumes:
|
||||
@@ -4096,6 +4358,54 @@ paths:
|
||||
summary: Get feedback by tender ID
|
||||
tags:
|
||||
- Feedback
|
||||
/api/v1/inquiries:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Create a new inquiry with contact information for potential business
|
||||
opportunities. This endpoint allows external users to submit inquiries about
|
||||
services or partnerships. Duplicate submissions are prevented based on email
|
||||
and phone number.
|
||||
parameters:
|
||||
- description: Inquiry information including contact details and company information
|
||||
in: body
|
||||
name: inquiry
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/inquiry.CreateInquiryForm'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Inquiry created successfully with assigned ID and timestamps
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/response.APIResponse'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/inquiry.InquiryResponse'
|
||||
type: object
|
||||
"400":
|
||||
description: Bad request - Invalid input data or missing required fields
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"409":
|
||||
description: Conflict - Duplicate inquiry already exists with same email
|
||||
or phone number
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"422":
|
||||
description: Validation error - Invalid data format or validation rules
|
||||
not met
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/response.APIResponse'
|
||||
summary: Create new inquiry
|
||||
tags:
|
||||
- Inquiries
|
||||
/api/v1/profile:
|
||||
get:
|
||||
consumes:
|
||||
@@ -4657,6 +4967,9 @@ tags:
|
||||
including CRUD operations, search, statistics, and comprehensive filtering with
|
||||
pagination
|
||||
name: Admin-TenderApprovals
|
||||
- description: Administrative inquiry management operations for web panel including
|
||||
CRUD operations, search, statistics, and comprehensive filtering with pagination
|
||||
name: Admin-Inquiries
|
||||
- description: Customer authentication and authorization operations for mobile application
|
||||
including login, logout, token refresh, and profile access
|
||||
name: Authorization
|
||||
@@ -4672,3 +4985,6 @@ tags:
|
||||
- description: Public tender approval management operations for mobile application
|
||||
including tender approval listing and detailed tender approval information
|
||||
name: TenderApprovals
|
||||
- description: Public inquiry management operations for mobile application including
|
||||
inquiry submission and inquiry listing
|
||||
name: Inquiries
|
||||
|
||||
+12
-3
@@ -44,6 +44,9 @@ package main
|
||||
// @tag.name Admin-TenderApprovals
|
||||
// @tag.description Administrative tender approval management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination
|
||||
|
||||
// @tag.name Admin-Inquiries
|
||||
// @tag.description Administrative inquiry management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination
|
||||
|
||||
// @tag.name Authorization
|
||||
// @tag.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access
|
||||
|
||||
@@ -59,6 +62,9 @@ package main
|
||||
// @tag.name TenderApprovals
|
||||
// @tag.description Public tender approval management operations for mobile application including tender approval listing and detailed tender approval information
|
||||
|
||||
// @tag.name Inquiries
|
||||
// @tag.description Public inquiry management operations for mobile application including inquiry submission and inquiry listing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
@@ -67,6 +73,7 @@ import (
|
||||
"tm/internal/company"
|
||||
"tm/internal/customer"
|
||||
"tm/internal/feedback"
|
||||
"tm/internal/inquiry"
|
||||
"tm/internal/tender"
|
||||
"tm/internal/tender_approval"
|
||||
"tm/internal/user"
|
||||
@@ -115,6 +122,7 @@ func main() {
|
||||
tenderRepository := tender.NewTenderRepository(mongoManager, logger)
|
||||
feedbackRepo := feedback.NewFeedbackRepository(mongoManager, logger)
|
||||
tenderApprovalRepository := tender_approval.NewTenderApprovalRepository(mongoManager, logger)
|
||||
inquiryRepository := inquiry.NewInquiryRepository(mongoManager, logger)
|
||||
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
||||
"repositories": []string{"customer", "user", "company", "tender", "feedback", "tender_approval"},
|
||||
})
|
||||
@@ -129,6 +137,7 @@ func main() {
|
||||
tenderService := tender.NewTenderService(tenderRepository, companyService, logger)
|
||||
feedbackService := feedback.NewFeedbackService(feedbackRepo, tenderService, logger)
|
||||
tenderApprovalService := tender_approval.NewTenderApprovalService(tenderApprovalRepository, tenderService, logger)
|
||||
inquiryService := inquiry.NewInquiryService(inquiryRepository, logger)
|
||||
logger.Info("Services initialized successfully", map[string]interface{}{
|
||||
"services": []string{"customer", "user", "company", "tender", "feedback", "tender_approval"},
|
||||
})
|
||||
@@ -140,7 +149,7 @@ func main() {
|
||||
tenderHandler := tender.NewTenderHandler(tenderService, logger)
|
||||
feedbackHandler := feedback.NewFeedbackHandler(feedbackService, userHandler, customerHandler, logger)
|
||||
tenderApprovalHandler := tender_approval.NewHandler(tenderApprovalService, logger)
|
||||
|
||||
inquiryHandler := inquiry.NewHandler(inquiryService, logger)
|
||||
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
||||
"handlers": []string{"customer", "user", "company", "tender", "feedback", "tender_approval"},
|
||||
})
|
||||
@@ -149,8 +158,8 @@ func main() {
|
||||
e := bootstrap.InitHTTPServer(conf, logger)
|
||||
|
||||
// Register routes
|
||||
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler)
|
||||
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler)
|
||||
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, inquiryHandler)
|
||||
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler, tenderApprovalHandler, companyHandler, inquiryHandler)
|
||||
|
||||
// Start server
|
||||
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"tm/internal/company"
|
||||
"tm/internal/customer"
|
||||
"tm/internal/feedback"
|
||||
"tm/internal/inquiry"
|
||||
"tm/internal/tender"
|
||||
"tm/internal/tender_approval"
|
||||
"tm/internal/user"
|
||||
@@ -11,7 +12,7 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler) {
|
||||
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, inquiryHandler *inquiry.Handler) {
|
||||
adminV1 := e.Group("/admin/v1")
|
||||
|
||||
// Admin Users Routes
|
||||
@@ -107,9 +108,19 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
|
||||
tenderApprovalGP.GET("/submission-mode/:submission_mode", tenderApprovalHandler.GetTenderApprovalsBySubmissionMode)
|
||||
tenderApprovalGP.GET("/status/:status", tenderApprovalHandler.GetTenderApprovalsByStatus)
|
||||
}
|
||||
|
||||
// Admin Inquiry Routes
|
||||
inquiryGP := adminV1.Group("/inquiries")
|
||||
{
|
||||
inquiryGP.Use(userHandler.AuthMiddleware())
|
||||
inquiryGP.GET("", inquiryHandler.SearchInquiries)
|
||||
inquiryGP.GET("/:id", inquiryHandler.GetInquiryByID)
|
||||
inquiryGP.PUT("/:id/status", inquiryHandler.UpdateInquiryStatus)
|
||||
inquiryGP.DELETE("/:id", inquiryHandler.DeleteInquiry)
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler) {
|
||||
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler, tenderApprovalHandler *tender_approval.Handler, companyHandler *company.Handler, inquiryHandler *inquiry.Handler) {
|
||||
v1 := e.Group("/api/v1")
|
||||
|
||||
customerGP := v1.Group("/profile")
|
||||
@@ -160,4 +171,10 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende
|
||||
companiesGP.Use(customerHandler.AuthMiddleware())
|
||||
companiesGP.GET("", companyHandler.MyCompany)
|
||||
}
|
||||
|
||||
// Public Inquiry Routes
|
||||
inquiryGP := v1.Group("/inquiries")
|
||||
{
|
||||
inquiryGP.POST("", inquiryHandler.CreateInquiry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package inquiry
|
||||
|
||||
import (
|
||||
"time"
|
||||
"tm/pkg/mongo"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// Inquiry status constants
|
||||
const (
|
||||
StatusPending = "pending" // درحال بررسی
|
||||
StatusReviewed = "reviewed" // بررسی شده
|
||||
StatusApproved = "approved" // تایید شده
|
||||
StatusRejected = "rejected" // رد شده
|
||||
)
|
||||
|
||||
// StatusHistory represents a status change record
|
||||
type StatusHistory struct {
|
||||
Status string `bson:"status" json:"status"` // New status
|
||||
Reason string `bson:"reason" json:"reason"` // Reason for change
|
||||
ChangedBy string `bson:"changed_by" json:"changed_by"` // User who made the change
|
||||
ChangedAt int64 `bson:"changed_at" json:"changed_at"` // Timestamp of change
|
||||
Description string `bson:"description" json:"description"` // Optional description
|
||||
}
|
||||
|
||||
type Inquiry struct {
|
||||
mongo.Model `bson:",inline"`
|
||||
FullName string `bson:"full_name"`
|
||||
CompanyName string `bson:"company_name"`
|
||||
WorkEmail string `bson:"work_email"`
|
||||
PhoneNumber string `bson:"phone_number"`
|
||||
Status string `bson:"status"` // Current status
|
||||
StatusHistory []StatusHistory `bson:"status_history"` // History of status changes
|
||||
}
|
||||
|
||||
func (i *Inquiry) SetID(id string) {
|
||||
i.ID, _ = primitive.ObjectIDFromHex(id)
|
||||
}
|
||||
|
||||
func (i *Inquiry) GetID() string {
|
||||
return i.ID.Hex()
|
||||
}
|
||||
|
||||
func (i *Inquiry) SetCreatedAt(timestamp int64) {
|
||||
i.CreatedAt = timestamp
|
||||
}
|
||||
|
||||
func (i *Inquiry) SetUpdatedAt(timestamp int64) {
|
||||
i.UpdatedAt = timestamp
|
||||
}
|
||||
|
||||
func (i *Inquiry) GetCreatedAt() int64 {
|
||||
return i.CreatedAt
|
||||
}
|
||||
|
||||
func (i *Inquiry) GetUpdatedAt() int64 {
|
||||
return i.UpdatedAt
|
||||
}
|
||||
|
||||
// AddStatusChange adds a new status change to the history
|
||||
func (i *Inquiry) AddStatusChange(status, reason, changedBy, description string) {
|
||||
statusChange := StatusHistory{
|
||||
Status: status,
|
||||
Reason: reason,
|
||||
ChangedBy: changedBy,
|
||||
ChangedAt: time.Now().Unix(),
|
||||
Description: description,
|
||||
}
|
||||
|
||||
i.StatusHistory = append(i.StatusHistory, statusChange)
|
||||
i.Status = status
|
||||
i.SetUpdatedAt(time.Now().Unix())
|
||||
}
|
||||
|
||||
// GetCurrentStatus returns the current status
|
||||
func (i *Inquiry) GetCurrentStatus() string {
|
||||
return i.Status
|
||||
}
|
||||
|
||||
// GetLastStatusChange returns the most recent status change
|
||||
func (i *Inquiry) GetLastStatusChange() *StatusHistory {
|
||||
if len(i.StatusHistory) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &i.StatusHistory[len(i.StatusHistory)-1]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package inquiry
|
||||
|
||||
import "errors"
|
||||
|
||||
// Inquiry-specific errors
|
||||
var (
|
||||
ErrDuplicateInquiryByEmail = errors.New("duplicate inquiry by email")
|
||||
ErrDuplicateInquiryByPhone = errors.New("duplicate inquiry by phone")
|
||||
ErrInquiryNotFound = errors.New("inquiry not found")
|
||||
ErrInvalidStatusTransition = errors.New("invalid status transition")
|
||||
ErrInquiryAlreadyProcessed = errors.New("inquiry already processed")
|
||||
)
|
||||
|
||||
// DuplicateInquiryError represents a duplicate inquiry error with details
|
||||
type DuplicateInquiryError struct {
|
||||
Type string `json:"type"` // "email" or "phone"
|
||||
ExistingID string `json:"existing_id"` // ID of existing inquiry
|
||||
Status string `json:"status"` // Status of existing inquiry
|
||||
CreatedAt int64 `json:"created_at"` // When existing inquiry was created
|
||||
Message string `json:"message"` // User-friendly message
|
||||
}
|
||||
|
||||
func (e *DuplicateInquiryError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// NewDuplicateInquiryError creates a new duplicate inquiry error
|
||||
func NewDuplicateInquiryError(inquiryType, existingID, status string, createdAt int64) *DuplicateInquiryError {
|
||||
var message string
|
||||
if inquiryType == "email" {
|
||||
message = "You already have a pending inquiry with this email address. Please wait for it to be processed before submitting a new one."
|
||||
} else {
|
||||
message = "A pending inquiry already exists with this phone number. Please wait for it to be processed before submitting a new one."
|
||||
}
|
||||
|
||||
return &DuplicateInquiryError{
|
||||
Type: inquiryType,
|
||||
ExistingID: existingID,
|
||||
Status: status,
|
||||
CreatedAt: createdAt,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package inquiry
|
||||
|
||||
import "tm/pkg/response"
|
||||
|
||||
// Inquiry DTOs
|
||||
|
||||
// CreateInquiryForm represents the data required to create a new inquiry
|
||||
type CreateInquiryForm struct {
|
||||
FullName string `json:"full_name" valid:"required,length(2|100)" example:"John Doe"` // Full name of the inquirer
|
||||
CompanyName string `json:"company_name" valid:"required,length(2|100)" example:"Opplens"` // Company name
|
||||
WorkEmail string `json:"work_email" valid:"required,email" example:"info@opplens.com"` // Work email address
|
||||
PhoneNumber string `json:"phone_number" valid:"required,length(10|20)" example:"+1234567890"` // Phone number
|
||||
}
|
||||
|
||||
// UpdateInquiryStatusForm represents the data required to update inquiry status
|
||||
type UpdateInquiryStatusForm struct {
|
||||
Status string `json:"status" valid:"required,in(pending|reviewed|approved|rejected)" example:"reviewed"` // New status
|
||||
Reason string `json:"reason" valid:"required,length(5|500)" example:"Initial review completed"` // Reason for status change
|
||||
Description string `json:"description" valid:"optional,length(0|1000)" example:"Additional notes about the review"` // Optional description
|
||||
}
|
||||
|
||||
// SearchInquiriesForm represents search and filter parameters for inquiries
|
||||
type SearchInquiriesForm struct {
|
||||
Search *string `query:"q" valid:"optional"` // Search term for full name, company name, or email
|
||||
Status *string `query:"status" valid:"optional,in(pending|reviewed|approved|rejected)"` // Filter by status
|
||||
Limit *int `query:"limit" valid:"optional,range(1|100)"` // Limit results
|
||||
Offset *int `query:"offset" valid:"optional,min(0)"` // Offset results
|
||||
SortBy *string `query:"sort_by" valid:"optional,in(full_name|company_name|work_email|status|created_at)"` // Sort field
|
||||
SortOrder *string `query:"sort_order" valid:"optional,in(asc|desc)"` // Sort order
|
||||
}
|
||||
|
||||
// Response DTOs
|
||||
|
||||
// StatusHistoryResponse represents status history in API responses
|
||||
type StatusHistoryResponse struct {
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
ChangedAt int64 `json:"changed_at"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// InquiryResponse represents the inquiry data in API responses
|
||||
type InquiryResponse struct {
|
||||
ID string `json:"id"`
|
||||
FullName string `json:"full_name"`
|
||||
CompanyName string `json:"company_name"`
|
||||
WorkEmail string `json:"work_email"`
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
Status string `json:"status"`
|
||||
StatusHistory []StatusHistoryResponse `json:"status_history"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
// InquiryListResponse represents a paginated list of inquiries
|
||||
type InquiryListResponse struct {
|
||||
Inquiries []*InquiryResponse `json:"inquiries"`
|
||||
Meta *response.Meta `json:"meta"`
|
||||
}
|
||||
|
||||
// Helper function to convert Inquiry to InquiryResponse
|
||||
func (i *Inquiry) ToResponse() *InquiryResponse {
|
||||
// Convert status history
|
||||
var statusHistoryResponses []StatusHistoryResponse
|
||||
for _, history := range i.StatusHistory {
|
||||
statusHistoryResponses = append(statusHistoryResponses, StatusHistoryResponse{
|
||||
Status: history.Status,
|
||||
Reason: history.Reason,
|
||||
ChangedAt: history.ChangedAt,
|
||||
Description: history.Description,
|
||||
})
|
||||
}
|
||||
|
||||
return &InquiryResponse{
|
||||
ID: i.ID.Hex(),
|
||||
FullName: i.FullName,
|
||||
CompanyName: i.CompanyName,
|
||||
WorkEmail: i.WorkEmail,
|
||||
PhoneNumber: i.PhoneNumber,
|
||||
Status: i.Status,
|
||||
StatusHistory: statusHistoryResponses,
|
||||
CreatedAt: i.CreatedAt,
|
||||
UpdatedAt: i.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package inquiry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Handler handles HTTP requests for inquiry operations
|
||||
type Handler struct {
|
||||
service Service
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewInquiryHandler creates a new inquiry handler
|
||||
func NewHandler(service Service, logger logger.Logger) *Handler {
|
||||
return &Handler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateInquiry creates a new inquiry
|
||||
// @Summary Create new inquiry
|
||||
// @Description Create a new inquiry with contact information for potential business opportunities. This endpoint allows external users to submit inquiries about services or partnerships. Duplicate submissions are prevented based on email and phone number.
|
||||
// @Tags Inquiries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param inquiry body CreateInquiryForm true "Inquiry information including contact details and company information"
|
||||
// @Success 201 {object} response.APIResponse{data=InquiryResponse} "Inquiry created successfully with assigned ID and timestamps"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data or missing required fields"
|
||||
// @Failure 409 {object} response.APIResponse "Conflict - Duplicate inquiry already exists with same email or phone number"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid data format or validation rules not met"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Router /api/v1/inquiries [post]
|
||||
func (h *Handler) CreateInquiry(c echo.Context) error {
|
||||
form, err := response.Parse[CreateInquiryForm](c)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", "")
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if _, err := govalidator.ValidateStruct(form); err != nil {
|
||||
return response.ValidationError(c, err.Error(), "")
|
||||
}
|
||||
|
||||
// Call service
|
||||
inquiry, err := h.service.Create(c.Request().Context(), form)
|
||||
if err != nil {
|
||||
// Check if it's a duplicate inquiry error
|
||||
if duplicateErr, ok := err.(*DuplicateInquiryError); ok {
|
||||
// Convert details to JSON string
|
||||
details, _ := json.Marshal(map[string]interface{}{
|
||||
"type": duplicateErr.Type,
|
||||
"existing_id": duplicateErr.ExistingID,
|
||||
"status": duplicateErr.Status,
|
||||
"created_at": duplicateErr.CreatedAt,
|
||||
})
|
||||
|
||||
// Return conflict with detailed error information
|
||||
return c.JSON(409, response.APIResponse{
|
||||
Success: false,
|
||||
Message: "Conflict",
|
||||
Error: &response.APIError{
|
||||
Code: "DUPLICATE_INQUIRY",
|
||||
Message: duplicateErr.Message,
|
||||
Details: string(details),
|
||||
},
|
||||
})
|
||||
}
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
|
||||
return response.Created(c, inquiry, "Inquiry created successfully")
|
||||
}
|
||||
|
||||
// GetInquiryByID gets an inquiry by ID
|
||||
// @Summary Get inquiry by ID
|
||||
// @Description Retrieve detailed information about a specific inquiry by its unique identifier. This endpoint provides complete inquiry details including contact information, status, and status history.
|
||||
// @Tags Admin-Inquiries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Inquiry ID"
|
||||
// @Success 200 {object} response.APIResponse{data=InquiryResponse} "Inquiry retrieved successfully with complete details"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid inquiry ID format"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Inquiry with specified ID not found"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Router /admin/v1/inquiries/{id} [get]
|
||||
func (h *Handler) GetInquiryByID(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
|
||||
inquiry, err := h.service.GetByID(c.Request().Context(), idStr)
|
||||
if err != nil {
|
||||
return response.NotFound(c, "Inquiry not found")
|
||||
}
|
||||
|
||||
return response.Success(c, inquiry, "Inquiry retrieved successfully")
|
||||
}
|
||||
|
||||
// UpdateInquiryStatus updates the status of an inquiry
|
||||
// @Summary Update inquiry status
|
||||
// @Description Update the status of an inquiry with reason and optional description. This endpoint allows administrators to change inquiry status following the defined workflow: pending -> reviewed -> approved/rejected.
|
||||
// @Tags Admin-Inquiries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Inquiry ID"
|
||||
// @Param status body UpdateInquiryStatusForm true "Status update information including new status, reason, and optional description"
|
||||
// @Success 200 {object} response.APIResponse{data=InquiryResponse} "Inquiry status updated successfully with updated details"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid inquiry ID format or invalid status transition"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Inquiry with specified ID not found"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid data format or validation rules not met"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Router /admin/v1/inquiries/{id}/status [put]
|
||||
func (h *Handler) UpdateInquiryStatus(c echo.Context) error {
|
||||
idStr := c.Param("id")
|
||||
|
||||
form, err := response.Parse[UpdateInquiryStatusForm](c)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", "")
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if _, err := govalidator.ValidateStruct(form); err != nil {
|
||||
return response.ValidationError(c, err.Error(), "")
|
||||
}
|
||||
|
||||
// Get user ID from context (assuming it's set by auth middleware)
|
||||
changedBy := "admin" // TODO: Get from JWT token or context
|
||||
// changedBy := c.Get("user_id").(string)
|
||||
|
||||
// Call service
|
||||
inquiry, err := h.service.UpdateStatus(c.Request().Context(), idStr, form, changedBy)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
|
||||
return response.Success(c, inquiry, "Inquiry status updated successfully")
|
||||
}
|
||||
|
||||
// SearchInquiries searches inquiries with filters
|
||||
// @Summary Search inquiries
|
||||
// @Description Search and filter inquiries with pagination support. This endpoint allows administrators to find inquiries based on various criteria including name, company, email, status, and other filters.
|
||||
// @Tags Admin-Inquiries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param q query string false "Search term for full name, company name, or email"
|
||||
// @Param status query string false "Filter by status (pending, reviewed, approved, rejected)"
|
||||
// @Param limit query int false "Limit results (1-100)"
|
||||
// @Param offset query int false "Offset results"
|
||||
// @Param sort_by query string false "Sort field (full_name, company_name, work_email, status, created_at)"
|
||||
// @Param sort_order query string false "Sort order (asc, desc)"
|
||||
// @Success 200 {object} response.APIResponse{data=InquiryListResponse} "Inquiries retrieved successfully with pagination metadata"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters"
|
||||
// @Failure 422 {object} response.APIResponse "Validation error - Invalid parameter values"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Router /admin/v1/inquiries [get]
|
||||
func (h *Handler) SearchInquiries(c echo.Context) error {
|
||||
form, err := response.Parse[SearchInquiriesForm](c)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", "")
|
||||
}
|
||||
|
||||
// Validate form
|
||||
if _, err := govalidator.ValidateStruct(form); err != nil {
|
||||
return response.ValidationError(c, err.Error(), "")
|
||||
}
|
||||
|
||||
// Call service
|
||||
inquiries, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to search inquiries")
|
||||
}
|
||||
|
||||
return response.Success(c, inquiries, "Inquiries retrieved successfully")
|
||||
}
|
||||
|
||||
// DeleteInquiry deletes an inquiry
|
||||
// @Summary Delete inquiry
|
||||
// @Description Delete an inquiry permanently from the system. This action cannot be undone and will remove all inquiry data including contact information, status history, and timestamps.
|
||||
// @Tags Admin-Inquiries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Inquiry ID"
|
||||
// @Success 200 {object} response.APIResponse "Inquiry deleted successfully"
|
||||
// @Failure 400 {object} response.APIResponse "Bad request - Invalid inquiry ID format"
|
||||
// @Failure 404 {object} response.APIResponse "Not found - Inquiry with specified ID not found"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Router /admin/v1/inquiries/{id} [delete]
|
||||
func (h *Handler) DeleteInquiry(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
|
||||
err := h.service.Delete(c.Request().Context(), id)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, err.Error(), "")
|
||||
}
|
||||
|
||||
return response.Success(c, map[string]interface{}{
|
||||
"message": "Inquiry deleted successfully",
|
||||
}, "Inquiry deleted successfully")
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package inquiry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
"tm/pkg/logger"
|
||||
orm "tm/pkg/mongo"
|
||||
"tm/pkg/response"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
// Repository defines methods for inquiry data access
|
||||
type Repository interface {
|
||||
Create(ctx context.Context, inquiry *Inquiry) error
|
||||
GetByID(ctx context.Context, id string) (*Inquiry, error)
|
||||
UpdateStatus(ctx context.Context, id string, status, reason, changedBy, description string) error
|
||||
Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) ([]Inquiry, int64, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
CheckPendingInquiry(ctx context.Context, email string) (*Inquiry, error)
|
||||
CheckPendingInquiryByPhone(ctx context.Context, phone string) (*Inquiry, error)
|
||||
}
|
||||
|
||||
// inquiryRepository implements the Repository interface using the MongoDB ORM
|
||||
type inquiryRepository struct {
|
||||
ormRepo orm.Repository[Inquiry]
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewInquiryRepository creates a new inquiry repository
|
||||
func NewInquiryRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
|
||||
// Create indexes using the ORM's index management
|
||||
indexes := []orm.Index{
|
||||
*orm.CreateTextIndex("search_idx", "full_name", "company_name", "work_email"),
|
||||
*orm.NewIndex("company_name_idx", bson.D{bson.E{Key: "company_name", Value: 1}}),
|
||||
*orm.NewIndex("work_email_idx", bson.D{bson.E{Key: "work_email", Value: 1}}),
|
||||
*orm.NewIndex("phone_number_idx", bson.D{bson.E{Key: "phone_number", Value: 1}}),
|
||||
*orm.NewIndex("status_idx", bson.D{bson.E{Key: "status", Value: 1}}),
|
||||
*orm.NewIndex("created_at_idx", bson.D{bson.E{Key: "created_at", Value: -1}}),
|
||||
// Compound index for duplicate prevention
|
||||
*orm.NewIndex("email_status_idx", bson.D{
|
||||
bson.E{Key: "work_email", Value: 1},
|
||||
bson.E{Key: "status", Value: 1},
|
||||
}),
|
||||
*orm.NewIndex("phone_status_idx", bson.D{
|
||||
bson.E{Key: "phone_number", Value: 1},
|
||||
bson.E{Key: "status", Value: 1},
|
||||
}),
|
||||
}
|
||||
|
||||
// Create indexes
|
||||
err := mongoManager.CreateIndexes("inquiries", indexes)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to create inquiry indexes", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Create ORM repository
|
||||
ormRepo := orm.NewRepository[Inquiry](mongoManager.GetCollection("inquiries"), logger)
|
||||
|
||||
return &inquiryRepository{
|
||||
ormRepo: ormRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new inquiry
|
||||
func (r *inquiryRepository) Create(ctx context.Context, inquiry *Inquiry) error {
|
||||
// Set created timestamp using Unix timestamp
|
||||
now := time.Now().Unix()
|
||||
inquiry.SetCreatedAt(now)
|
||||
inquiry.SetUpdatedAt(now)
|
||||
|
||||
// Set initial status to pending
|
||||
inquiry.Status = StatusPending
|
||||
|
||||
// Add initial status change to history
|
||||
inquiry.AddStatusChange(StatusPending, "Inquiry submitted", "system", "Initial inquiry submission")
|
||||
|
||||
// Use ORM to create inquiry
|
||||
err := r.ormRepo.Create(ctx, inquiry)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to create inquiry", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"work_email": inquiry.WorkEmail,
|
||||
"company_name": inquiry.CompanyName,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Inquiry created successfully", map[string]interface{}{
|
||||
"inquiry_id": inquiry.ID,
|
||||
"work_email": inquiry.WorkEmail,
|
||||
"company_name": inquiry.CompanyName,
|
||||
"status": inquiry.Status,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID retrieves an inquiry by ID
|
||||
func (r *inquiryRepository) GetByID(ctx context.Context, id string) (*Inquiry, error) {
|
||||
inquiry, err := r.ormRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, orm.ErrDocumentNotFound) {
|
||||
return nil, errors.New("inquiry not found")
|
||||
}
|
||||
r.logger.Error("Failed to get inquiry by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"inquiry_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return inquiry, nil
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status of an inquiry
|
||||
func (r *inquiryRepository) UpdateStatus(ctx context.Context, id string, status, reason, changedBy, description string) error {
|
||||
// Get inquiry first
|
||||
inquiry, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add status change to history
|
||||
inquiry.AddStatusChange(status, reason, changedBy, description)
|
||||
|
||||
// Update in database - use the correct Update method signature
|
||||
err = r.ormRepo.Update(ctx, inquiry)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to update inquiry status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"inquiry_id": id,
|
||||
"new_status": status,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Inquiry status updated successfully", map[string]interface{}{
|
||||
"inquiry_id": id,
|
||||
"old_status": inquiry.StatusHistory[len(inquiry.StatusHistory)-2].Status, // Previous status
|
||||
"new_status": status,
|
||||
"changed_by": changedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPendingInquiry checks if there's a pending inquiry for the given email
|
||||
func (r *inquiryRepository) CheckPendingInquiry(ctx context.Context, email string) (*Inquiry, error) {
|
||||
filter := bson.M{
|
||||
"work_email": email,
|
||||
"status": bson.M{"$in": []string{StatusPending, StatusReviewed}},
|
||||
}
|
||||
|
||||
pagination := orm.NewPaginationBuilder().
|
||||
Limit(1).
|
||||
SortBy("created_at", "desc").
|
||||
Build()
|
||||
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to check pending inquiry by email", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"work_email": email,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Items) > 0 {
|
||||
return &result.Items[0], nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CheckPendingInquiryByPhone checks if there's a pending inquiry for the given phone number
|
||||
func (r *inquiryRepository) CheckPendingInquiryByPhone(ctx context.Context, phone string) (*Inquiry, error) {
|
||||
filter := bson.M{
|
||||
"phone_number": phone,
|
||||
"status": bson.M{"$in": []string{StatusPending, StatusReviewed}},
|
||||
}
|
||||
|
||||
pagination := orm.NewPaginationBuilder().
|
||||
Limit(1).
|
||||
SortBy("created_at", "desc").
|
||||
Build()
|
||||
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to check pending inquiry by phone", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"phone_number": phone,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Items) > 0 {
|
||||
return &result.Items[0], nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Search retrieves inquiries with pagination and filters
|
||||
func (r *inquiryRepository) Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) ([]Inquiry, int64, error) {
|
||||
filter := bson.M{}
|
||||
|
||||
// Add search filter
|
||||
if form.Search != nil {
|
||||
filter["$or"] = bson.A{
|
||||
bson.M{"full_name": bson.M{"$regex": form.Search, "$options": "i"}},
|
||||
bson.M{"company_name": bson.M{"$regex": form.Search, "$options": "i"}},
|
||||
bson.M{"work_email": bson.M{"$regex": form.Search, "$options": "i"}},
|
||||
}
|
||||
}
|
||||
|
||||
// Add status filter
|
||||
if form.Status != nil {
|
||||
filter["status"] = *form.Status
|
||||
}
|
||||
|
||||
// Build sort
|
||||
sort := "created_at"
|
||||
if pagination.SortBy != "" {
|
||||
sort = pagination.SortBy
|
||||
}
|
||||
|
||||
sortOrder := "desc"
|
||||
if pagination.SortOrder != "" {
|
||||
sortOrder = pagination.SortOrder
|
||||
}
|
||||
|
||||
// Build pagination
|
||||
paginationBuilder := orm.NewPaginationBuilder().
|
||||
Limit(pagination.Limit).
|
||||
Skip(pagination.Offset).
|
||||
SortBy(sort, sortOrder).
|
||||
Build()
|
||||
|
||||
// Use ORM to find inquiries
|
||||
result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to search inquiries", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return result.Items, result.TotalCount, nil
|
||||
}
|
||||
|
||||
// Delete deletes an inquiry
|
||||
func (r *inquiryRepository) Delete(ctx context.Context, id string) error {
|
||||
// Get inquiry first to ensure it exists
|
||||
_, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete inquiry", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"inquiry_id": id,
|
||||
})
|
||||
if errors.Is(err, orm.ErrDocumentNotFound) {
|
||||
return errors.New("inquiry not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete in database
|
||||
err = r.ormRepo.Delete(ctx, id)
|
||||
if err != nil {
|
||||
r.logger.Error("Failed to delete inquiry", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"inquiry_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
r.logger.Info("Inquiry deleted successfully", map[string]interface{}{
|
||||
"inquiry_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package inquiry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// Service defines business logic for inquiry operations
|
||||
type Service interface {
|
||||
Create(ctx context.Context, form *CreateInquiryForm) (*InquiryResponse, error)
|
||||
GetByID(ctx context.Context, id string) (*InquiryResponse, error)
|
||||
UpdateStatus(ctx context.Context, id string, form *UpdateInquiryStatusForm, changedBy string) (*InquiryResponse, error)
|
||||
Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) (*InquiryListResponse, error)
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// inquiryService implements the Service interface
|
||||
type inquiryService struct {
|
||||
repository Repository
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewInquiryService creates a new inquiry service
|
||||
func NewInquiryService(repository Repository, logger logger.Logger) Service {
|
||||
return &inquiryService{
|
||||
repository: repository,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new inquiry
|
||||
func (s *inquiryService) Create(ctx context.Context, form *CreateInquiryForm) (*InquiryResponse, error) {
|
||||
s.logger.Info("Creating new inquiry", map[string]interface{}{
|
||||
"work_email": form.WorkEmail,
|
||||
"company_name": form.CompanyName,
|
||||
"full_name": form.FullName,
|
||||
})
|
||||
|
||||
// Check for duplicate pending inquiries by email
|
||||
existingInquiry, err := s.repository.CheckPendingInquiry(ctx, form.WorkEmail)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to check for duplicate inquiry by email", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"work_email": form.WorkEmail,
|
||||
})
|
||||
return nil, errors.New("failed to validate inquiry")
|
||||
}
|
||||
|
||||
if existingInquiry != nil {
|
||||
s.logger.Warn("Duplicate inquiry attempt by email", map[string]interface{}{
|
||||
"work_email": form.WorkEmail,
|
||||
"existing_id": existingInquiry.ID,
|
||||
"existing_status": existingInquiry.Status,
|
||||
"created_at": existingInquiry.CreatedAt,
|
||||
})
|
||||
return nil, NewDuplicateInquiryError("email", existingInquiry.ID.Hex(), existingInquiry.Status, existingInquiry.CreatedAt)
|
||||
}
|
||||
|
||||
// Check for duplicate pending inquiries by phone number
|
||||
existingInquiryByPhone, err := s.repository.CheckPendingInquiryByPhone(ctx, form.PhoneNumber)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to check for duplicate inquiry by phone", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"phone_number": form.PhoneNumber,
|
||||
})
|
||||
return nil, errors.New("failed to validate inquiry")
|
||||
}
|
||||
|
||||
if existingInquiryByPhone != nil {
|
||||
s.logger.Warn("Duplicate inquiry attempt by phone", map[string]interface{}{
|
||||
"phone_number": form.PhoneNumber,
|
||||
"existing_id": existingInquiryByPhone.ID,
|
||||
"existing_status": existingInquiryByPhone.Status,
|
||||
"created_at": existingInquiryByPhone.CreatedAt,
|
||||
})
|
||||
return nil, NewDuplicateInquiryError("phone", existingInquiryByPhone.ID.Hex(), existingInquiryByPhone.Status, existingInquiryByPhone.CreatedAt)
|
||||
}
|
||||
|
||||
// Create inquiry entity
|
||||
inquiry := &Inquiry{
|
||||
FullName: form.FullName,
|
||||
CompanyName: form.CompanyName,
|
||||
WorkEmail: form.WorkEmail,
|
||||
PhoneNumber: form.PhoneNumber,
|
||||
}
|
||||
|
||||
// Save to database
|
||||
err = s.repository.Create(ctx, inquiry)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create inquiry", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"work_email": form.WorkEmail,
|
||||
"company_name": form.CompanyName,
|
||||
})
|
||||
return nil, errors.New("failed to create inquiry")
|
||||
}
|
||||
|
||||
s.logger.Info("Inquiry created successfully", map[string]interface{}{
|
||||
"inquiry_id": inquiry.ID,
|
||||
"work_email": inquiry.WorkEmail,
|
||||
"company_name": inquiry.CompanyName,
|
||||
"status": inquiry.Status,
|
||||
})
|
||||
|
||||
return inquiry.ToResponse(), nil
|
||||
}
|
||||
|
||||
// GetByID retrieves an inquiry by ID
|
||||
func (s *inquiryService) GetByID(ctx context.Context, id string) (*InquiryResponse, error) {
|
||||
s.logger.Info("Getting inquiry by ID", map[string]interface{}{
|
||||
"inquiry_id": id,
|
||||
})
|
||||
|
||||
inquiry, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get inquiry by ID", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"inquiry_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Inquiry retrieved successfully", map[string]interface{}{
|
||||
"inquiry_id": id,
|
||||
"status": inquiry.Status,
|
||||
})
|
||||
|
||||
return inquiry.ToResponse(), nil
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status of an inquiry
|
||||
func (s *inquiryService) UpdateStatus(ctx context.Context, id string, form *UpdateInquiryStatusForm, changedBy string) (*InquiryResponse, error) {
|
||||
s.logger.Info("Updating inquiry status", map[string]interface{}{
|
||||
"inquiry_id": id,
|
||||
"new_status": form.Status,
|
||||
"changed_by": changedBy,
|
||||
})
|
||||
|
||||
// Validate status transition
|
||||
err := s.validateStatusTransition(ctx, id, form.Status)
|
||||
if err != nil {
|
||||
s.logger.Error("Invalid status transition", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"inquiry_id": id,
|
||||
"new_status": form.Status,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update status in repository
|
||||
err = s.repository.UpdateStatus(ctx, id, form.Status, form.Reason, changedBy, form.Description)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to update inquiry status", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"inquiry_id": id,
|
||||
"new_status": form.Status,
|
||||
})
|
||||
return nil, errors.New("failed to update inquiry status")
|
||||
}
|
||||
|
||||
// Get updated inquiry
|
||||
inquiry, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get updated inquiry", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"inquiry_id": id,
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Inquiry status updated successfully", map[string]interface{}{
|
||||
"inquiry_id": id,
|
||||
"new_status": form.Status,
|
||||
"changed_by": changedBy,
|
||||
})
|
||||
|
||||
return inquiry.ToResponse(), nil
|
||||
}
|
||||
|
||||
// validateStatusTransition validates if the status transition is allowed
|
||||
func (s *inquiryService) validateStatusTransition(ctx context.Context, id string, newStatus string) error {
|
||||
// Get current inquiry
|
||||
inquiry, err := s.repository.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentStatus := inquiry.GetCurrentStatus()
|
||||
|
||||
// Define allowed transitions
|
||||
allowedTransitions := map[string][]string{
|
||||
StatusPending: {StatusReviewed, StatusRejected},
|
||||
StatusReviewed: {StatusApproved, StatusRejected},
|
||||
StatusApproved: {}, // No transitions from approved
|
||||
StatusRejected: {}, // No transitions from rejected
|
||||
}
|
||||
|
||||
allowedNextStatuses, exists := allowedTransitions[currentStatus]
|
||||
if !exists {
|
||||
return errors.New("invalid current status")
|
||||
}
|
||||
|
||||
// Check if transition is allowed
|
||||
for _, allowedStatus := range allowedNextStatuses {
|
||||
if allowedStatus == newStatus {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("invalid status transition: cannot change from " + currentStatus + " to " + newStatus)
|
||||
}
|
||||
|
||||
// Search searches inquiries with search and filters
|
||||
func (s *inquiryService) Search(ctx context.Context, form *SearchInquiriesForm, pagination *response.Pagination) (*InquiryListResponse, error) {
|
||||
s.logger.Info("Searching inquiries", map[string]interface{}{
|
||||
"search": form.Search,
|
||||
"status": form.Status,
|
||||
"limit": pagination.Limit,
|
||||
"offset": pagination.Offset,
|
||||
})
|
||||
|
||||
// Get inquiries
|
||||
inquiries, total, err := s.repository.Search(ctx, form, pagination)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to search inquiries", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, errors.New("failed to search inquiries")
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
var inquiryResponses []*InquiryResponse
|
||||
for _, inquiry := range inquiries {
|
||||
inquiryResponses = append(inquiryResponses, inquiry.ToResponse())
|
||||
}
|
||||
|
||||
s.logger.Info("Inquiries search completed successfully", map[string]interface{}{
|
||||
"total_found": total,
|
||||
"returned": len(inquiryResponses),
|
||||
})
|
||||
|
||||
return &InquiryListResponse{
|
||||
Inquiries: inquiryResponses,
|
||||
Meta: pagination.Response(total),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Delete deletes an inquiry
|
||||
func (s *inquiryService) Delete(ctx context.Context, id string) error {
|
||||
s.logger.Info("Deleting inquiry", map[string]interface{}{
|
||||
"inquiry_id": id,
|
||||
})
|
||||
|
||||
err := s.repository.Delete(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to delete inquiry", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"inquiry_id": id,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Inquiry deleted successfully", map[string]interface{}{
|
||||
"inquiry_id": id,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user