Implement Feedback Management System with CRUD Operations and API Enhancements
- Introduced a new feedback management system, including the creation of feedback entities, services, and repositories. - Implemented CRUD operations for feedback, allowing users to submit likes and dislikes on tenders. - Developed administrative endpoints for listing and retrieving feedback with comprehensive filtering options. - Enhanced public API to allow users to view feedback related to tenders. - Updated Swagger documentation to reflect new feedback endpoints and their functionalities. - Removed obsolete configuration file `config.yaml` as part of the transition to a modular configuration system. - Ensured adherence to Clean Architecture principles throughout the implementation.
This commit is contained in:
@@ -8,6 +8,8 @@ help:
|
|||||||
@echo " build - Build the web server"
|
@echo " build - Build the web server"
|
||||||
@echo " build-mac - Build for macOS (Intel)"
|
@echo " build-mac - Build for macOS (Intel)"
|
||||||
@echo " build-mac-arm64 - Build for macOS (Apple Silicon)"
|
@echo " build-mac-arm64 - Build for macOS (Apple Silicon)"
|
||||||
|
@echo " build-windows - Build for Windows (64-bit)"
|
||||||
|
@echo " build-windows-32 - Build for Windows (32-bit)"
|
||||||
@echo " build-all - Build for all platforms"
|
@echo " build-all - Build for all platforms"
|
||||||
@echo " run - Run the web server"
|
@echo " run - Run the web server"
|
||||||
@echo " test - Run tests"
|
@echo " test - Run tests"
|
||||||
@@ -36,8 +38,22 @@ build-mac-arm64:
|
|||||||
@GOOS=darwin GOARCH=arm64 go build -o bin/web-mac-arm64 ./cmd/web
|
@GOOS=darwin GOARCH=arm64 go build -o bin/web-mac-arm64 ./cmd/web
|
||||||
@echo "macOS ARM64 build completed successfully!"
|
@echo "macOS ARM64 build completed successfully!"
|
||||||
|
|
||||||
|
# Build for Windows (64-bit)
|
||||||
|
build-windows:
|
||||||
|
@echo "Building web server for Windows (64-bit)..."
|
||||||
|
@mkdir -p bin
|
||||||
|
@GOOS=windows GOARCH=amd64 go build -o bin/web.exe ./cmd/web
|
||||||
|
@echo "Windows 64-bit build completed successfully!"
|
||||||
|
|
||||||
|
# Build for Windows (32-bit)
|
||||||
|
build-windows-32:
|
||||||
|
@echo "Building web server for Windows (32-bit)..."
|
||||||
|
@mkdir -p bin
|
||||||
|
@GOOS=windows GOARCH=386 go build -o bin/web-32.exe ./cmd/web
|
||||||
|
@echo "Windows 32-bit build completed successfully!"
|
||||||
|
|
||||||
# Build for all platforms
|
# Build for all platforms
|
||||||
build-all: build build-mac build-mac-arm64
|
build-all: build build-mac build-mac-arm64 build-windows build-windows-32
|
||||||
@echo "All platform builds completed successfully!"
|
@echo "All platform builds completed successfully!"
|
||||||
|
|
||||||
# Run the web server
|
# Run the web server
|
||||||
|
|||||||
@@ -3431,6 +3431,436 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/v1/feedback": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for administrative use and provides comprehensive filtering options.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Admin-Feedback"
|
||||||
|
],
|
||||||
|
"summary": "List feedback with filters",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by tender ID (MongoDB ObjectID format)",
|
||||||
|
"name": "tender_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by customer ID (MongoDB ObjectID format)",
|
||||||
|
"name": "customer_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by company ID (MongoDB ObjectID format)",
|
||||||
|
"name": "company_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"like",
|
||||||
|
"dislike"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by feedback type",
|
||||||
|
"name": "feedback_type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Filter by creation date from (Unix timestamp in seconds)",
|
||||||
|
"name": "date_from",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Filter by creation date to (Unix timestamp in seconds)",
|
||||||
|
"name": "date_to",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maximum": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of items per page (default: 20, max: 100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of items to skip for pagination (default: 0)",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback list retrieved successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid query parameters",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error - Invalid filter criteria",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/v1/feedback/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve feedback details by its unique identifier with loaded tender, customer, and company information. This endpoint provides comprehensive feedback data including related entity details.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Admin-Feedback"
|
||||||
|
],
|
||||||
|
"summary": "Get feedback by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"maxLength": 24,
|
||||||
|
"minLength": 24,
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feedback ID (MongoDB ObjectID format)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "feedback retrieved successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid feedback ID format",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Feedback not found",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Mark feedback as deleted without permanently removing it from the database. The feedback will be marked as deleted and hidden from normal queries but can be restored if needed.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Admin-Feedback"
|
||||||
|
],
|
||||||
|
"summary": "Soft delete feedback",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"maxLength": 24,
|
||||||
|
"minLength": 24,
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feedback ID (MongoDB ObjectID format)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback soft deleted successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid feedback ID format",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - User not authenticated",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden - User not authorized to delete this feedback",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found - Feedback not found",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/v1/health": {
|
"/admin/v1/health": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get comprehensive server health status including system information, dependencies status, and performance metrics. This endpoint is used for monitoring and load balancer health checks.",
|
"description": "Get comprehensive server health status including system information, dependencies status, and performance metrics. This endpoint is used for monitoring and load balancer health checks.",
|
||||||
@@ -4663,6 +5093,417 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/feedback": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for public use and filters results based on the authenticated user's company.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Feedback"
|
||||||
|
],
|
||||||
|
"summary": "List feedback with filters",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by tender ID (MongoDB ObjectID format)",
|
||||||
|
"name": "tender_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by customer ID (MongoDB ObjectID format)",
|
||||||
|
"name": "customer_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by company ID (MongoDB ObjectID format)",
|
||||||
|
"name": "company_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"like",
|
||||||
|
"dislike"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by feedback type",
|
||||||
|
"name": "feedback_type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Filter by creation date from (Unix timestamp in seconds)",
|
||||||
|
"name": "date_from",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Filter by creation date to (Unix timestamp in seconds)",
|
||||||
|
"name": "date_to",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maximum": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of items per page (default: 20, max: 100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of items to skip for pagination (default: 0)",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback list retrieved successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid query parameters or missing company association",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error - Invalid filter criteria",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Toggle between like and dislike for a tender, or create a like if no feedback exists. This endpoint allows users to express their preference for tenders and automatically handles the toggle logic.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Feedback"
|
||||||
|
],
|
||||||
|
"summary": "Toggle feedback for a tender",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Feedback toggle request containing tender ID",
|
||||||
|
"name": "feedback",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/feedback.ToggleFeedbackForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback toggled successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid input data or missing customer/company association",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - User not authenticated",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error - Invalid request data format",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/feedback/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve feedback details by its unique identifier. This endpoint provides basic feedback information for public users.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Feedback"
|
||||||
|
],
|
||||||
|
"summary": "Get feedback by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"maxLength": 24,
|
||||||
|
"minLength": 24,
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feedback ID (MongoDB ObjectID format)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback retrieved successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid feedback ID format",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Feedback not found",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/profile": {
|
"/api/v1/profile": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -4903,6 +5744,11 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"/api/v1/tenders": {
|
"/api/v1/tenders": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile",
|
"description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -4980,6 +5826,11 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"/api/v1/tenders/{id}": {
|
"/api/v1/tenders/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"description": "Retrieve detailed information about a specific tender for mobile application users",
|
"description": "Retrieve detailed information about a specific tender for mobile application users",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -6156,6 +7007,80 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"feedback.FeedbackListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"feedback": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"$ref": "#/definitions/response.Meta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feedback.FeedbackResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"company_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"feedback_type": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackType"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tender": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feedback.FeedbackType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"like",
|
||||||
|
"dislike"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"FeedbackTypeLike",
|
||||||
|
"FeedbackTypeDislike"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"feedback.ToggleFeedbackForm": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"feedback_type",
|
||||||
|
"tender_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"feedback_type": {
|
||||||
|
"enum": [
|
||||||
|
"like",
|
||||||
|
"dislike"
|
||||||
|
],
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tender_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"main.HealthResponse": {
|
"main.HealthResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -7271,6 +8196,10 @@ const docTemplate = `{
|
|||||||
"description": "Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking",
|
"description": "Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking",
|
||||||
"name": "Admin-Scraping"
|
"name": "Admin-Scraping"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Administrative feedback management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination",
|
||||||
|
"name": "Admin-Feedback"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access",
|
"description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access",
|
||||||
"name": "Authorization"
|
"name": "Authorization"
|
||||||
@@ -7278,6 +8207,10 @@ const docTemplate = `{
|
|||||||
{
|
{
|
||||||
"description": "Public tender information access for mobile application including active tender listings and detailed tender information",
|
"description": "Public tender information access for mobile application including active tender listings and detailed tender information",
|
||||||
"name": "Tenders"
|
"name": "Tenders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Public feedback management operations for mobile application including like, dislike, and comment on tenders",
|
||||||
|
"name": "Feedback"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
|
|||||||
@@ -3425,6 +3425,436 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/v1/feedback": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for administrative use and provides comprehensive filtering options.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Admin-Feedback"
|
||||||
|
],
|
||||||
|
"summary": "List feedback with filters",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by tender ID (MongoDB ObjectID format)",
|
||||||
|
"name": "tender_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by customer ID (MongoDB ObjectID format)",
|
||||||
|
"name": "customer_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by company ID (MongoDB ObjectID format)",
|
||||||
|
"name": "company_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"like",
|
||||||
|
"dislike"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by feedback type",
|
||||||
|
"name": "feedback_type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Filter by creation date from (Unix timestamp in seconds)",
|
||||||
|
"name": "date_from",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Filter by creation date to (Unix timestamp in seconds)",
|
||||||
|
"name": "date_to",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maximum": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of items per page (default: 20, max: 100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of items to skip for pagination (default: 0)",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback list retrieved successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid query parameters",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error - Invalid filter criteria",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/v1/feedback/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve feedback details by its unique identifier with loaded tender, customer, and company information. This endpoint provides comprehensive feedback data including related entity details.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Admin-Feedback"
|
||||||
|
],
|
||||||
|
"summary": "Get feedback by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"maxLength": 24,
|
||||||
|
"minLength": 24,
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feedback ID (MongoDB ObjectID format)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "feedback retrieved successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid feedback ID format",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Feedback not found",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Mark feedback as deleted without permanently removing it from the database. The feedback will be marked as deleted and hidden from normal queries but can be restored if needed.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Admin-Feedback"
|
||||||
|
],
|
||||||
|
"summary": "Soft delete feedback",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"maxLength": 24,
|
||||||
|
"minLength": 24,
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feedback ID (MongoDB ObjectID format)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback soft deleted successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid feedback ID format",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - User not authenticated",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden - User not authorized to delete this feedback",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not found - Feedback not found",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/v1/health": {
|
"/admin/v1/health": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Get comprehensive server health status including system information, dependencies status, and performance metrics. This endpoint is used for monitoring and load balancer health checks.",
|
"description": "Get comprehensive server health status including system information, dependencies status, and performance metrics. This endpoint is used for monitoring and load balancer health checks.",
|
||||||
@@ -4657,6 +5087,417 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/feedback": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for public use and filters results based on the authenticated user's company.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Feedback"
|
||||||
|
],
|
||||||
|
"summary": "List feedback with filters",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by tender ID (MongoDB ObjectID format)",
|
||||||
|
"name": "tender_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by customer ID (MongoDB ObjectID format)",
|
||||||
|
"name": "customer_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by company ID (MongoDB ObjectID format)",
|
||||||
|
"name": "company_id",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enum": [
|
||||||
|
"like",
|
||||||
|
"dislike"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filter by feedback type",
|
||||||
|
"name": "feedback_type",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Filter by creation date from (Unix timestamp in seconds)",
|
||||||
|
"name": "date_from",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Filter by creation date to (Unix timestamp in seconds)",
|
||||||
|
"name": "date_to",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"maximum": 100,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of items per page (default: 20, max: 100)",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Number of items to skip for pagination (default: 0)",
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback list retrieved successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackListResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid query parameters or missing company association",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error - Invalid filter criteria",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Toggle between like and dislike for a tender, or create a like if no feedback exists. This endpoint allows users to express their preference for tenders and automatically handles the toggle logic.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Feedback"
|
||||||
|
],
|
||||||
|
"summary": "Toggle feedback for a tender",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Feedback toggle request containing tender ID",
|
||||||
|
"name": "feedback",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/feedback.ToggleFeedbackForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback toggled successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid input data or missing customer/company association",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized - User not authenticated",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation error - Invalid request data format",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/feedback/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Retrieve feedback details by its unique identifier. This endpoint provides basic feedback information for public users.",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Feedback"
|
||||||
|
],
|
||||||
|
"summary": "Get feedback by ID",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"maxLength": 24,
|
||||||
|
"minLength": 24,
|
||||||
|
"type": "string",
|
||||||
|
"description": "Feedback ID (MongoDB ObjectID format)",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Feedback retrieved successfully",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad request - Invalid feedback ID format",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Feedback not found",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/response.APIResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/profile": {
|
"/api/v1/profile": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -4897,6 +5738,11 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/tenders": {
|
"/api/v1/tenders": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile",
|
"description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -4974,6 +5820,11 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/tenders/{id}": {
|
"/api/v1/tenders/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"BearerAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"description": "Retrieve detailed information about a specific tender for mobile application users",
|
"description": "Retrieve detailed information about a specific tender for mobile application users",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -6150,6 +7001,80 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"feedback.FeedbackListResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"feedback": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"$ref": "#/definitions/response.Meta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feedback.FeedbackResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"company_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"feedback_type": {
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackType"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tender": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"feedback.FeedbackType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"like",
|
||||||
|
"dislike"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"FeedbackTypeLike",
|
||||||
|
"FeedbackTypeDislike"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"feedback.ToggleFeedbackForm": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"feedback_type",
|
||||||
|
"tender_id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"feedback_type": {
|
||||||
|
"enum": [
|
||||||
|
"like",
|
||||||
|
"dislike"
|
||||||
|
],
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/feedback.FeedbackType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tender_id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"main.HealthResponse": {
|
"main.HealthResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -7265,6 +8190,10 @@
|
|||||||
"description": "Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking",
|
"description": "Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking",
|
||||||
"name": "Admin-Scraping"
|
"name": "Admin-Scraping"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Administrative feedback management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination",
|
||||||
|
"name": "Admin-Feedback"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access",
|
"description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access",
|
||||||
"name": "Authorization"
|
"name": "Authorization"
|
||||||
@@ -7272,6 +8201,10 @@
|
|||||||
{
|
{
|
||||||
"description": "Public tender information access for mobile application including active tender listings and detailed tender information",
|
"description": "Public tender information access for mobile application including active tender listings and detailed tender information",
|
||||||
"name": "Tenders"
|
"name": "Tenders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Public feedback management operations for mobile application including like, dislike, and comment on tenders",
|
||||||
|
"name": "Feedback"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -731,6 +731,54 @@ definitions:
|
|||||||
is_verified:
|
is_verified:
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
|
feedback.FeedbackListResponse:
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/feedback.FeedbackResponse'
|
||||||
|
type: array
|
||||||
|
meta:
|
||||||
|
$ref: '#/definitions/response.Meta'
|
||||||
|
type: object
|
||||||
|
feedback.FeedbackResponse:
|
||||||
|
properties:
|
||||||
|
company_id:
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
type: integer
|
||||||
|
customer_id:
|
||||||
|
type: string
|
||||||
|
feedback_type:
|
||||||
|
$ref: '#/definitions/feedback.FeedbackType'
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
tender:
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
feedback.FeedbackType:
|
||||||
|
enum:
|
||||||
|
- like
|
||||||
|
- dislike
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- FeedbackTypeLike
|
||||||
|
- FeedbackTypeDislike
|
||||||
|
feedback.ToggleFeedbackForm:
|
||||||
|
properties:
|
||||||
|
feedback_type:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/feedback.FeedbackType'
|
||||||
|
enum:
|
||||||
|
- like
|
||||||
|
- dislike
|
||||||
|
tender_id:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- feedback_type
|
||||||
|
- tender_id
|
||||||
|
type: object
|
||||||
main.HealthResponse:
|
main.HealthResponse:
|
||||||
properties:
|
properties:
|
||||||
service:
|
service:
|
||||||
@@ -3661,6 +3709,255 @@ paths:
|
|||||||
summary: Get customers by type
|
summary: Get customers by type
|
||||||
tags:
|
tags:
|
||||||
- Admin-Customers
|
- Admin-Customers
|
||||||
|
/admin/v1/feedback:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieve paginated list of feedback with optional filtering by
|
||||||
|
various criteria. This endpoint is for administrative use and provides comprehensive
|
||||||
|
filtering options.
|
||||||
|
parameters:
|
||||||
|
- description: Filter by tender ID (MongoDB ObjectID format)
|
||||||
|
in: query
|
||||||
|
name: tender_id
|
||||||
|
type: string
|
||||||
|
- description: Filter by customer ID (MongoDB ObjectID format)
|
||||||
|
in: query
|
||||||
|
name: customer_id
|
||||||
|
type: string
|
||||||
|
- description: Filter by company ID (MongoDB ObjectID format)
|
||||||
|
in: query
|
||||||
|
name: company_id
|
||||||
|
type: string
|
||||||
|
- description: Filter by feedback type
|
||||||
|
enum:
|
||||||
|
- like
|
||||||
|
- dislike
|
||||||
|
in: query
|
||||||
|
name: feedback_type
|
||||||
|
type: string
|
||||||
|
- description: Filter by creation date from (Unix timestamp in seconds)
|
||||||
|
format: int64
|
||||||
|
in: query
|
||||||
|
name: date_from
|
||||||
|
type: integer
|
||||||
|
- description: Filter by creation date to (Unix timestamp in seconds)
|
||||||
|
format: int64
|
||||||
|
in: query
|
||||||
|
name: date_to
|
||||||
|
type: integer
|
||||||
|
- description: 'Number of items per page (default: 20, max: 100)'
|
||||||
|
in: query
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
- description: 'Number of items to skip for pagination (default: 0)'
|
||||||
|
in: query
|
||||||
|
minimum: 0
|
||||||
|
name: offset
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Feedback list retrieved successfully
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/feedback.FeedbackListResponse'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad request - Invalid query parameters
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"422":
|
||||||
|
description: Validation error - Invalid filter criteria
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: List feedback with filters
|
||||||
|
tags:
|
||||||
|
- Admin-Feedback
|
||||||
|
/admin/v1/feedback/{id}:
|
||||||
|
delete:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Mark feedback as deleted without permanently removing it from the
|
||||||
|
database. The feedback will be marked as deleted and hidden from normal queries
|
||||||
|
but can be restored if needed.
|
||||||
|
parameters:
|
||||||
|
- description: Feedback ID (MongoDB ObjectID format)
|
||||||
|
in: path
|
||||||
|
maxLength: 24
|
||||||
|
minLength: 24
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Feedback soft deleted successfully
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad request - Invalid feedback ID format
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"401":
|
||||||
|
description: Unauthorized - User not authenticated
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"403":
|
||||||
|
description: Forbidden - User not authorized to delete this feedback
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Not found - Feedback not found
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Soft delete feedback
|
||||||
|
tags:
|
||||||
|
- Admin-Feedback
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieve feedback details by its unique identifier with loaded
|
||||||
|
tender, customer, and company information. This endpoint provides comprehensive
|
||||||
|
feedback data including related entity details.
|
||||||
|
parameters:
|
||||||
|
- description: Feedback ID (MongoDB ObjectID format)
|
||||||
|
in: path
|
||||||
|
maxLength: 24
|
||||||
|
minLength: 24
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: feedback retrieved successfully
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/feedback.FeedbackResponse'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad request - Invalid feedback ID format
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Feedback not found
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Get feedback by ID
|
||||||
|
tags:
|
||||||
|
- Admin-Feedback
|
||||||
/admin/v1/health:
|
/admin/v1/health:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -4452,6 +4749,245 @@ paths:
|
|||||||
summary: Get users by role
|
summary: Get users by role
|
||||||
tags:
|
tags:
|
||||||
- Admin-Users
|
- Admin-Users
|
||||||
|
/api/v1/feedback:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieve paginated list of feedback with optional filtering by
|
||||||
|
various criteria. This endpoint is for public use and filters results based
|
||||||
|
on the authenticated user's company.
|
||||||
|
parameters:
|
||||||
|
- description: Filter by tender ID (MongoDB ObjectID format)
|
||||||
|
in: query
|
||||||
|
name: tender_id
|
||||||
|
type: string
|
||||||
|
- description: Filter by customer ID (MongoDB ObjectID format)
|
||||||
|
in: query
|
||||||
|
name: customer_id
|
||||||
|
type: string
|
||||||
|
- description: Filter by company ID (MongoDB ObjectID format)
|
||||||
|
in: query
|
||||||
|
name: company_id
|
||||||
|
type: string
|
||||||
|
- description: Filter by feedback type
|
||||||
|
enum:
|
||||||
|
- like
|
||||||
|
- dislike
|
||||||
|
in: query
|
||||||
|
name: feedback_type
|
||||||
|
type: string
|
||||||
|
- description: Filter by creation date from (Unix timestamp in seconds)
|
||||||
|
format: int64
|
||||||
|
in: query
|
||||||
|
minimum: 0
|
||||||
|
name: date_from
|
||||||
|
type: integer
|
||||||
|
- description: Filter by creation date to (Unix timestamp in seconds)
|
||||||
|
format: int64
|
||||||
|
in: query
|
||||||
|
minimum: 0
|
||||||
|
name: date_to
|
||||||
|
type: integer
|
||||||
|
- description: 'Number of items per page (default: 20, max: 100)'
|
||||||
|
in: query
|
||||||
|
maximum: 100
|
||||||
|
minimum: 1
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
- description: 'Number of items to skip for pagination (default: 0)'
|
||||||
|
in: query
|
||||||
|
minimum: 0
|
||||||
|
name: offset
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Feedback list retrieved successfully
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/feedback.FeedbackListResponse'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad request - Invalid query parameters or missing company association
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"422":
|
||||||
|
description: Validation error - Invalid filter criteria
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: List feedback with filters
|
||||||
|
tags:
|
||||||
|
- Feedback
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Toggle between like and dislike for a tender, or create a like
|
||||||
|
if no feedback exists. This endpoint allows users to express their preference
|
||||||
|
for tenders and automatically handles the toggle logic.
|
||||||
|
parameters:
|
||||||
|
- description: Feedback toggle request containing tender ID
|
||||||
|
in: body
|
||||||
|
name: feedback
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/feedback.ToggleFeedbackForm'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Feedback toggled successfully
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/feedback.FeedbackResponse'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad request - Invalid input data or missing customer/company
|
||||||
|
association
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"401":
|
||||||
|
description: Unauthorized - User not authenticated
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"422":
|
||||||
|
description: Validation error - Invalid request data format
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Toggle feedback for a tender
|
||||||
|
tags:
|
||||||
|
- Feedback
|
||||||
|
/api/v1/feedback/{id}:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Retrieve feedback details by its unique identifier. This endpoint
|
||||||
|
provides basic feedback information for public users.
|
||||||
|
parameters:
|
||||||
|
- description: Feedback ID (MongoDB ObjectID format)
|
||||||
|
in: path
|
||||||
|
maxLength: 24
|
||||||
|
minLength: 24
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Feedback retrieved successfully
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/definitions/feedback.FeedbackResponse'
|
||||||
|
type: object
|
||||||
|
"400":
|
||||||
|
description: Bad request - Invalid feedback ID format
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Feedback not found
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/response.APIResponse'
|
||||||
|
- properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
summary: Get feedback by ID
|
||||||
|
tags:
|
||||||
|
- Feedback
|
||||||
/api/v1/profile:
|
/api/v1/profile:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -4645,6 +5181,8 @@ paths:
|
|||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/response.APIResponse'
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
summary: Get public tenders
|
summary: Get public tenders
|
||||||
tags:
|
tags:
|
||||||
- Tenders
|
- Tenders
|
||||||
@@ -4679,6 +5217,8 @@ paths:
|
|||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/response.APIResponse'
|
$ref: '#/definitions/response.APIResponse'
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
summary: Get public tender details
|
summary: Get public tender details
|
||||||
tags:
|
tags:
|
||||||
- Tenders
|
- Tenders
|
||||||
@@ -4714,9 +5254,15 @@ tags:
|
|||||||
- description: Administrative scraping job management operations for TED XML data
|
- description: Administrative scraping job management operations for TED XML data
|
||||||
collection including job control, monitoring, and status tracking
|
collection including job control, monitoring, and status tracking
|
||||||
name: Admin-Scraping
|
name: Admin-Scraping
|
||||||
|
- description: Administrative feedback management operations for web panel including
|
||||||
|
CRUD operations, search, statistics, and comprehensive filtering with pagination
|
||||||
|
name: Admin-Feedback
|
||||||
- description: Customer authentication and authorization operations for mobile application
|
- description: Customer authentication and authorization operations for mobile application
|
||||||
including login, logout, token refresh, and profile access
|
including login, logout, token refresh, and profile access
|
||||||
name: Authorization
|
name: Authorization
|
||||||
- description: Public tender information access for mobile application including active
|
- description: Public tender information access for mobile application including active
|
||||||
tender listings and detailed tender information
|
tender listings and detailed tender information
|
||||||
name: Tenders
|
name: Tenders
|
||||||
|
- description: Public feedback management operations for mobile application including
|
||||||
|
like, dislike, and comment on tenders
|
||||||
|
name: Feedback
|
||||||
|
|||||||
+12
-2
@@ -41,12 +41,18 @@ package main
|
|||||||
// @tag.name Admin-Scraping
|
// @tag.name Admin-Scraping
|
||||||
// @tag.description Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking
|
// @tag.description Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking
|
||||||
|
|
||||||
|
// @tag.name Admin-Feedback
|
||||||
|
// @tag.description Administrative feedback management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination
|
||||||
|
|
||||||
// @tag.name Authorization
|
// @tag.name Authorization
|
||||||
// @tag.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access
|
// @tag.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access
|
||||||
|
|
||||||
// @tag.name Tenders
|
// @tag.name Tenders
|
||||||
// @tag.description Public tender information access for mobile application including active tender listings and detailed tender information
|
// @tag.description Public tender information access for mobile application including active tender listings and detailed tender information
|
||||||
|
|
||||||
|
// @tag.name Feedback
|
||||||
|
// @tag.description Public feedback management operations for mobile application including like, dislike, and comment on tenders
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -54,6 +60,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"tm/internal/company"
|
"tm/internal/company"
|
||||||
"tm/internal/customer"
|
"tm/internal/customer"
|
||||||
|
"tm/internal/feedback"
|
||||||
"tm/internal/tender"
|
"tm/internal/tender"
|
||||||
"tm/internal/user"
|
"tm/internal/user"
|
||||||
|
|
||||||
@@ -98,6 +105,7 @@ func main() {
|
|||||||
customerRepository := customer.NewCustomerRepository(mongoManager, logger)
|
customerRepository := customer.NewCustomerRepository(mongoManager, logger)
|
||||||
companyRepository := company.NewCompanyRepository(mongoManager, logger)
|
companyRepository := company.NewCompanyRepository(mongoManager, logger)
|
||||||
tenderRepository := tender.NewTenderRepository(mongoManager, logger)
|
tenderRepository := tender.NewTenderRepository(mongoManager, logger)
|
||||||
|
feedbackRepo := feedback.NewFeedbackRepository(mongoManager, logger)
|
||||||
|
|
||||||
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
logger.Info("Repositories initialized successfully", map[string]interface{}{
|
||||||
"repositories": []string{"customer", "user", "company", "tender"},
|
"repositories": []string{"customer", "user", "company", "tender"},
|
||||||
@@ -111,6 +119,7 @@ func main() {
|
|||||||
companyService := company.NewCompanyService(companyRepository, logger)
|
companyService := company.NewCompanyService(companyRepository, logger)
|
||||||
customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService)
|
customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService)
|
||||||
tenderService := tender.NewTenderService(tenderRepository, companyService, logger)
|
tenderService := tender.NewTenderService(tenderRepository, companyService, logger)
|
||||||
|
feedbackService := feedback.NewFeedbackService(feedbackRepo, logger)
|
||||||
|
|
||||||
logger.Info("Services initialized successfully", map[string]interface{}{
|
logger.Info("Services initialized successfully", map[string]interface{}{
|
||||||
"services": []string{"customer", "user", "company", "tender"},
|
"services": []string{"customer", "user", "company", "tender"},
|
||||||
@@ -121,6 +130,7 @@ func main() {
|
|||||||
customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger)
|
customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger)
|
||||||
companyHandler := company.NewHandler(companyService, userHandler, logger)
|
companyHandler := company.NewHandler(companyService, userHandler, logger)
|
||||||
tenderHandler := tender.NewTenderHandler(tenderService, logger)
|
tenderHandler := tender.NewTenderHandler(tenderService, logger)
|
||||||
|
feedbackHandler := feedback.NewFeedbackHandler(feedbackService, userHandler, customerHandler, logger)
|
||||||
|
|
||||||
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
logger.Info("Handlers initialized successfully", map[string]interface{}{
|
||||||
"handlers": []string{"customer", "user", "company", "tender"},
|
"handlers": []string{"customer", "user", "company", "tender"},
|
||||||
@@ -130,8 +140,8 @@ func main() {
|
|||||||
e := initHTTPServer(conf, logger)
|
e := initHTTPServer(conf, logger)
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler)
|
router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler)
|
||||||
router.RegisterPublicRoutes(e, customerHandler, tenderHandler)
|
router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler)
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
|
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ package router
|
|||||||
import (
|
import (
|
||||||
"tm/internal/company"
|
"tm/internal/company"
|
||||||
"tm/internal/customer"
|
"tm/internal/customer"
|
||||||
|
"tm/internal/feedback"
|
||||||
"tm/internal/tender"
|
"tm/internal/tender"
|
||||||
"tm/internal/user"
|
"tm/internal/user"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler) {
|
func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler *company.Handler, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler) {
|
||||||
adminV1 := e.Group("/admin/v1")
|
adminV1 := e.Group("/admin/v1")
|
||||||
|
|
||||||
// Admin Users Routes
|
// Admin Users Routes
|
||||||
@@ -111,9 +112,18 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
|
|||||||
scrapingGP.GET("/jobs/:id", tenderHandler.GetScrapingJob)
|
scrapingGP.GET("/jobs/:id", tenderHandler.GetScrapingJob)
|
||||||
scrapingGP.POST("/jobs/:id/cancel", tenderHandler.CancelScrapingJob)
|
scrapingGP.POST("/jobs/:id/cancel", tenderHandler.CancelScrapingJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admin Feedback Routes
|
||||||
|
feedbackGP := adminV1.Group("/feedback")
|
||||||
|
{
|
||||||
|
feedbackGP.Use(userHandler.AuthMiddleware())
|
||||||
|
feedbackGP.GET("", feedbackHandler.ListFeedback)
|
||||||
|
feedbackGP.GET("/:id", feedbackHandler.GetFeedback)
|
||||||
|
feedbackGP.DELETE("/:id", feedbackHandler.DeleteFeedback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler) {
|
func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tenderHandler *tender.TenderHandler, feedbackHandler *feedback.Handler) {
|
||||||
v1 := e.Group("/api/v1")
|
v1 := e.Group("/api/v1")
|
||||||
|
|
||||||
customerGP := v1.Group("/profile")
|
customerGP := v1.Group("/profile")
|
||||||
@@ -134,4 +144,13 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende
|
|||||||
tendersGP.GET("", tenderHandler.GetPublicTenders)
|
tendersGP.GET("", tenderHandler.GetPublicTenders)
|
||||||
tendersGP.GET("/:id", tenderHandler.GetPublicTenderDetails)
|
tendersGP.GET("/:id", tenderHandler.GetPublicTenderDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public Feedback Routes
|
||||||
|
feedbackGP := v1.Group("/feedback")
|
||||||
|
{
|
||||||
|
feedbackGP.Use(customerHandler.AuthMiddleware())
|
||||||
|
feedbackGP.POST("", feedbackHandler.ToggleFeedback)
|
||||||
|
feedbackGP.GET("", feedbackHandler.PublicListFeedback)
|
||||||
|
feedbackGP.GET("/:id", feedbackHandler.PublicGetFeedback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-70
@@ -1,70 +0,0 @@
|
|||||||
server:
|
|
||||||
host: "0.0.0.0"
|
|
||||||
port: 8081
|
|
||||||
timeout: 30s
|
|
||||||
read_timeout: 10s
|
|
||||||
write_timeout: 10s
|
|
||||||
|
|
||||||
database:
|
|
||||||
mongodb:
|
|
||||||
uri: "mongodb://localhost:27017"
|
|
||||||
name: "tender_management"
|
|
||||||
timeout: 10s
|
|
||||||
max_pool_size: 100
|
|
||||||
|
|
||||||
cache:
|
|
||||||
redis:
|
|
||||||
host: "localhost"
|
|
||||||
port: 6379
|
|
||||||
password: ""
|
|
||||||
db: 0
|
|
||||||
pool_size: 10
|
|
||||||
|
|
||||||
queue:
|
|
||||||
rabbitmq:
|
|
||||||
url: "amqp://guest:guest@localhost:5672/"
|
|
||||||
exchange: "tender_exchange"
|
|
||||||
queues:
|
|
||||||
scraping: "scraping_queue"
|
|
||||||
processing: "processing_queue"
|
|
||||||
notifications: "notifications_queue"
|
|
||||||
|
|
||||||
search:
|
|
||||||
elasticsearch:
|
|
||||||
urls: ["http://localhost:9200"]
|
|
||||||
username: ""
|
|
||||||
password: ""
|
|
||||||
index_prefix: "tender_"
|
|
||||||
|
|
||||||
auth:
|
|
||||||
jwt:
|
|
||||||
secret: "your-super-secret-key"
|
|
||||||
access_token_duration: "24h"
|
|
||||||
refresh_token_duration: "168h" # 7 days
|
|
||||||
|
|
||||||
ai:
|
|
||||||
openai:
|
|
||||||
api_key: ""
|
|
||||||
model: "gpt-3.5-turbo"
|
|
||||||
max_tokens: 1000
|
|
||||||
|
|
||||||
scraping:
|
|
||||||
user_agent: "TenderBot/1.0"
|
|
||||||
timeout: 30s
|
|
||||||
max_retries: 3
|
|
||||||
delay_between_requests: 1s
|
|
||||||
|
|
||||||
logging:
|
|
||||||
level: "info"
|
|
||||||
format: "json"
|
|
||||||
output: "file" # stdout, stderr, file
|
|
||||||
file:
|
|
||||||
path: "./logs/app.log"
|
|
||||||
max_size: 100 # Max size in MB before rotation
|
|
||||||
max_backups: 5 # Number of backup files to keep
|
|
||||||
max_age: 30 # Max age in days to keep log files
|
|
||||||
compress: true # Compress rotated files
|
|
||||||
|
|
||||||
rate_limiting:
|
|
||||||
requests_per_minute: 100
|
|
||||||
burst: 20
|
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
mongopkg "tm/pkg/mongo"
|
"tm/pkg/mongo"
|
||||||
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
@@ -39,36 +39,36 @@ type Repository interface {
|
|||||||
|
|
||||||
// companyRepository implements the Repository interface using the MongoDB ORM
|
// companyRepository implements the Repository interface using the MongoDB ORM
|
||||||
type companyRepository struct {
|
type companyRepository struct {
|
||||||
ormRepo mongopkg.Repository[Company]
|
ormRepo mongo.Repository[Company]
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCompanyRepository creates a new company repository
|
// NewCompanyRepository creates a new company repository
|
||||||
func NewCompanyRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository {
|
func NewCompanyRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository {
|
||||||
// Create indexes using the ORM's index management
|
// Create indexes using the ORM's index management
|
||||||
indexes := []mongopkg.Index{
|
indexes := []mongo.Index{
|
||||||
*mongopkg.CreateUniqueIndex("name_idx", bson.D{{Key: "name", Value: 1}}),
|
*mongo.CreateUniqueIndex("name_idx", bson.D{{Key: "name", Value: 1}}),
|
||||||
*mongopkg.CreateUniqueIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
|
*mongo.CreateUniqueIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}),
|
||||||
*mongopkg.CreateUniqueIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}),
|
*mongo.CreateUniqueIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}),
|
||||||
*mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}),
|
*mongo.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}),
|
||||||
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
*mongo.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
||||||
*mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}),
|
*mongo.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}),
|
||||||
*mongopkg.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}),
|
*mongo.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}),
|
||||||
*mongopkg.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}),
|
*mongo.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}),
|
||||||
*mongopkg.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}),
|
*mongo.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}),
|
||||||
*mongopkg.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}),
|
*mongo.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}),
|
||||||
*mongopkg.NewIndex("employee_count_idx", bson.D{{Key: "employee_count", Value: 1}}),
|
*mongo.NewIndex("employee_count_idx", bson.D{{Key: "employee_count", Value: 1}}),
|
||||||
*mongopkg.NewIndex("annual_revenue_idx", bson.D{{Key: "annual_revenue", Value: 1}}),
|
*mongo.NewIndex("annual_revenue_idx", bson.D{{Key: "annual_revenue", Value: 1}}),
|
||||||
*mongopkg.NewIndex("founded_year_idx", bson.D{{Key: "founded_year", Value: 1}}),
|
*mongo.NewIndex("founded_year_idx", bson.D{{Key: "founded_year", Value: 1}}),
|
||||||
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
*mongo.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
|
||||||
// Tag indexes for efficient search
|
// Tag indexes for efficient search
|
||||||
*mongopkg.NewIndex("cpv_codes_idx", bson.D{{Key: "tags.cpv_codes", Value: 1}}),
|
*mongo.NewIndex("cpv_codes_idx", bson.D{{Key: "tags.cpv_codes", Value: 1}}),
|
||||||
*mongopkg.NewIndex("categories_idx", bson.D{{Key: "tags.categories", Value: 1}}),
|
*mongo.NewIndex("categories_idx", bson.D{{Key: "tags.categories", Value: 1}}),
|
||||||
*mongopkg.NewIndex("keywords_idx", bson.D{{Key: "tags.keywords", Value: 1}}),
|
*mongo.NewIndex("keywords_idx", bson.D{{Key: "tags.keywords", Value: 1}}),
|
||||||
*mongopkg.NewIndex("specializations_idx", bson.D{{Key: "tags.specializations", Value: 1}}),
|
*mongo.NewIndex("specializations_idx", bson.D{{Key: "tags.specializations", Value: 1}}),
|
||||||
*mongopkg.NewIndex("certifications_idx", bson.D{{Key: "tags.certifications", Value: 1}}),
|
*mongo.NewIndex("certifications_idx", bson.D{{Key: "tags.certifications", Value: 1}}),
|
||||||
// Text index for search
|
// Text index for search
|
||||||
*mongopkg.CreateTextIndex("search_idx", "name", "description", "industry"),
|
*mongo.CreateTextIndex("search_idx", "name", "description", "industry"),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create indexes
|
// Create indexes
|
||||||
@@ -80,7 +80,7 @@ func NewCompanyRepository(mongoManager *mongopkg.ConnectionManager, logger logge
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create ORM repository
|
// Create ORM repository
|
||||||
ormRepo := mongopkg.NewRepository[Company](mongoManager.GetCollection("companies"), logger)
|
ormRepo := mongo.NewRepository[Company](mongoManager.GetCollection("companies"), logger)
|
||||||
|
|
||||||
return &companyRepository{
|
return &companyRepository{
|
||||||
ormRepo: ormRepo,
|
ormRepo: ormRepo,
|
||||||
@@ -119,7 +119,7 @@ func (r *companyRepository) Create(ctx context.Context, company *Company) error
|
|||||||
func (r *companyRepository) GetByID(ctx context.Context, id string) (*Company, error) {
|
func (r *companyRepository) GetByID(ctx context.Context, id string) (*Company, error) {
|
||||||
company, err := r.ormRepo.FindByID(ctx, id)
|
company, err := r.ormRepo.FindByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
||||||
return nil, errors.New("company not found")
|
return nil, errors.New("company not found")
|
||||||
}
|
}
|
||||||
r.logger.Error("Failed to get company by ID", map[string]interface{}{
|
r.logger.Error("Failed to get company by ID", map[string]interface{}{
|
||||||
@@ -137,7 +137,7 @@ func (r *companyRepository) GetByName(ctx context.Context, name string) (*Compan
|
|||||||
filter := bson.M{"name": name}
|
filter := bson.M{"name": name}
|
||||||
company, err := r.ormRepo.FindOne(ctx, filter)
|
company, err := r.ormRepo.FindOne(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
||||||
return nil, errors.New("company not found")
|
return nil, errors.New("company not found")
|
||||||
}
|
}
|
||||||
r.logger.Error("Failed to get company by name", map[string]interface{}{
|
r.logger.Error("Failed to get company by name", map[string]interface{}{
|
||||||
@@ -155,7 +155,7 @@ func (r *companyRepository) GetByRegistrationNumber(ctx context.Context, registr
|
|||||||
filter := bson.M{"registration_number": registrationNumber}
|
filter := bson.M{"registration_number": registrationNumber}
|
||||||
company, err := r.ormRepo.FindOne(ctx, filter)
|
company, err := r.ormRepo.FindOne(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
||||||
return nil, errors.New("company not found")
|
return nil, errors.New("company not found")
|
||||||
}
|
}
|
||||||
r.logger.Error("Failed to get company by registration number", map[string]interface{}{
|
r.logger.Error("Failed to get company by registration number", map[string]interface{}{
|
||||||
@@ -173,7 +173,7 @@ func (r *companyRepository) GetByTaxID(ctx context.Context, taxID string) (*Comp
|
|||||||
filter := bson.M{"tax_id": taxID}
|
filter := bson.M{"tax_id": taxID}
|
||||||
company, err := r.ormRepo.FindOne(ctx, filter)
|
company, err := r.ormRepo.FindOne(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, mongopkg.ErrDocumentNotFound) {
|
if errors.Is(err, mongo.ErrDocumentNotFound) {
|
||||||
return nil, errors.New("company not found")
|
return nil, errors.New("company not found")
|
||||||
}
|
}
|
||||||
r.logger.Error("Failed to get company by tax ID", map[string]interface{}{
|
r.logger.Error("Failed to get company by tax ID", map[string]interface{}{
|
||||||
@@ -289,7 +289,7 @@ func (r *companyRepository) Delete(ctx context.Context, id string) error {
|
|||||||
// List retrieves companies with pagination
|
// List retrieves companies with pagination
|
||||||
func (r *companyRepository) List(ctx context.Context, limit, offset int) ([]*Company, error) {
|
func (r *companyRepository) List(ctx context.Context, limit, offset int) ([]*Company, error) {
|
||||||
// Build pagination
|
// Build pagination
|
||||||
pagination := mongopkg.NewPaginationBuilder().
|
pagination := mongo.NewPaginationBuilder().
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
Skip(offset).
|
Skip(offset).
|
||||||
SortDesc("created_at").
|
SortDesc("created_at").
|
||||||
@@ -407,7 +407,7 @@ func (r *companyRepository) Search(ctx context.Context, search string, companyTy
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build pagination
|
// Build pagination
|
||||||
pagination := mongopkg.NewPaginationBuilder().
|
pagination := mongo.NewPaginationBuilder().
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
Skip(offset)
|
Skip(offset)
|
||||||
|
|
||||||
@@ -755,7 +755,7 @@ func (r *companyRepository) SearchByTags(ctx context.Context, cpvCodes []string,
|
|||||||
filter["status"] = bson.M{"$ne": CompanyStatusInactive}
|
filter["status"] = bson.M{"$ne": CompanyStatusInactive}
|
||||||
|
|
||||||
// Build pagination
|
// Build pagination
|
||||||
pagination := mongopkg.NewPaginationBuilder().
|
pagination := mongo.NewPaginationBuilder().
|
||||||
Limit(limit).
|
Limit(limit).
|
||||||
Skip(offset).
|
Skip(offset).
|
||||||
SortDesc("created_at").
|
SortDesc("created_at").
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package feedback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"tm/pkg/mongo"
|
||||||
|
"tm/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeedbackType represents the type of feedback
|
||||||
|
type FeedbackType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FeedbackTypeLike FeedbackType = "like"
|
||||||
|
FeedbackTypeDislike FeedbackType = "dislike"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Feedback represents feedback given to a tender by a user
|
||||||
|
type Feedback struct {
|
||||||
|
mongo.Model `bson:",inline"`
|
||||||
|
TenderID string `bson:"tender_id" json:"tender_id" validate:"required"`
|
||||||
|
CustomerID *string `bson:"customer_id,omitempty" json:"customer_id,omitempty"`
|
||||||
|
CompanyID *string `bson:"company_id,omitempty" json:"company_id,omitempty"`
|
||||||
|
FeedbackType FeedbackType `bson:"feedback_type" json:"feedback_type" validate:"required,oneof=like dislike"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedbackSummary represents aggregated feedback statistics for a tender
|
||||||
|
type FeedbackSummary struct {
|
||||||
|
TenderID string `bson:"tender_id" json:"tender_id"`
|
||||||
|
TotalLikes int64 `bson:"total_likes" json:"total_likes"`
|
||||||
|
TotalDislikes int64 `bson:"total_dislikes" json:"total_dislikes"`
|
||||||
|
LastUpdated int64 `bson:"last_updated" json:"last_updated"` // Unix timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedbackSearchCriteria represents search criteria for feedback
|
||||||
|
type FeedbackSearchCriteria struct {
|
||||||
|
TenderID *string `json:"tender_id,omitempty"`
|
||||||
|
CustomerID *string `json:"customer_id,omitempty"`
|
||||||
|
CompanyID *string `json:"company_id,omitempty"`
|
||||||
|
FeedbackType *FeedbackType `json:"feedback_type,omitempty"`
|
||||||
|
DateFrom *int64 `json:"date_from,omitempty"` // Unix timestamp
|
||||||
|
DateTo *int64 `json:"date_to,omitempty"` // Unix timestamp
|
||||||
|
Limit int `json:"limit,omitempty"`
|
||||||
|
Offset int `json:"offset,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedbackListResponse represents a paginated list of feedback
|
||||||
|
type FeedbackListResponse struct {
|
||||||
|
Feedback []*FeedbackResponse `json:"feedback"`
|
||||||
|
Meta *response.Meta `json:"meta"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the feedback ID
|
||||||
|
func (f *Feedback) GetID() string {
|
||||||
|
return f.ID.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLike returns true if the feedback is a like
|
||||||
|
func (f *Feedback) IsLike() bool {
|
||||||
|
return f.FeedbackType == FeedbackTypeLike
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDislike returns true if the feedback is a dislike
|
||||||
|
func (f *Feedback) IsDislike() bool {
|
||||||
|
return f.FeedbackType == FeedbackTypeDislike
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFeedbackType updates the feedback type and updated timestamp
|
||||||
|
func (f *Feedback) UpdateFeedbackType(feedbackType FeedbackType) {
|
||||||
|
f.FeedbackType = feedbackType
|
||||||
|
f.UpdatedAt = time.Now().Unix()
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package feedback
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Custom errors for feedback domain
|
||||||
|
var (
|
||||||
|
ErrFeedbackNotFound = errors.New("feedback not found")
|
||||||
|
ErrFeedbackAlreadyExists = errors.New("feedback already exists for this tender and user")
|
||||||
|
ErrInvalidFeedbackType = errors.New("invalid feedback type")
|
||||||
|
ErrInvalidRating = errors.New("invalid rating value")
|
||||||
|
ErrInvalidComment = errors.New("invalid comment")
|
||||||
|
ErrInvalidDateRange = errors.New("invalid date range: date_from must be before date_to")
|
||||||
|
ErrUnauthorizedAccess = errors.New("unauthorized access to feedback")
|
||||||
|
ErrFeedbackDeleted = errors.New("feedback has been deleted")
|
||||||
|
ErrTenderNotFound = errors.New("tender not found")
|
||||||
|
ErrUserNotFound = errors.New("user not found")
|
||||||
|
ErrCustomerNotFound = errors.New("customer not found")
|
||||||
|
ErrCompanyNotFound = errors.New("company not found")
|
||||||
|
)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package feedback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListFeedbackForm represents the form for listing feedback
|
||||||
|
type ListFeedbackForm struct {
|
||||||
|
Feedbacks []FeedbackResponse `json:"feedbacks"`
|
||||||
|
DateFrom *int64 `json:"date_from,omitempty" validate:"omitempty,min=0"`
|
||||||
|
DateTo *int64 `json:"date_to,omitempty" validate:"omitempty,min=0"`
|
||||||
|
Limit int `json:"limit" validate:"required,min=1,max=100"`
|
||||||
|
Offset int `json:"offset" validate:"required,min=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToggleFeedbackForm represents the form for toggling feedback (like/dislike)
|
||||||
|
type ToggleFeedbackForm struct {
|
||||||
|
TenderID string `json:"tender_id" validate:"required"`
|
||||||
|
FeedbackType FeedbackType `json:"feedback_type" validate:"required,oneof=like dislike"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedbackResponse represents the response for feedback operations
|
||||||
|
type FeedbackResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FeedbackType FeedbackType `json:"feedback_type"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
TenderID string `json:"tender"`
|
||||||
|
CustomerID *string `json:"customer_id"`
|
||||||
|
CompanyId *string `json:"company_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDateFromTime returns the date_from as time.Time
|
||||||
|
func (f *ListFeedbackForm) GetDateFromTime() *time.Time {
|
||||||
|
if f.DateFrom == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t := time.Unix(*f.DateFrom, 0)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDateToTime returns the date_to as time.Time
|
||||||
|
func (f *ListFeedbackForm) GetDateToTime() *time.Time {
|
||||||
|
if f.DateTo == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t := time.Unix(*f.DateTo, 0)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Feedback) ToResponse() *FeedbackResponse {
|
||||||
|
return &FeedbackResponse{
|
||||||
|
ID: f.ID.Hex(),
|
||||||
|
FeedbackType: f.FeedbackType,
|
||||||
|
TenderID: f.TenderID,
|
||||||
|
CustomerID: f.CustomerID,
|
||||||
|
CompanyId: f.CompanyID,
|
||||||
|
UpdatedAt: f.UpdatedAt,
|
||||||
|
CreatedAt: f.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
package feedback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"tm/internal/customer"
|
||||||
|
"tm/internal/user"
|
||||||
|
"tm/pkg/logger"
|
||||||
|
"tm/pkg/response"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler handles HTTP requests for feedback operations
|
||||||
|
type Handler struct {
|
||||||
|
service FeedbackService
|
||||||
|
logger logger.Logger
|
||||||
|
userHandler *user.Handler
|
||||||
|
customerHandler *customer.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFeedbackHandler creates a new feedback handler
|
||||||
|
func NewFeedbackHandler(
|
||||||
|
service FeedbackService,
|
||||||
|
userHandler *user.Handler,
|
||||||
|
customerHandler *customer.Handler,
|
||||||
|
logger logger.Logger,
|
||||||
|
) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
service: service,
|
||||||
|
logger: logger,
|
||||||
|
userHandler: userHandler,
|
||||||
|
customerHandler: customerHandler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************** ADMIN ***************
|
||||||
|
// ListFeedback retrieves feedback with pagination and filtering
|
||||||
|
// @Summary List feedback with filters
|
||||||
|
// @Description Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for administrative use and provides comprehensive filtering options.
|
||||||
|
// @Tags Admin-Feedback
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tender_id query string false "Filter by tender ID (MongoDB ObjectID format)"
|
||||||
|
// @Param customer_id query string false "Filter by customer ID (MongoDB ObjectID format)"
|
||||||
|
// @Param company_id query string false "Filter by company ID (MongoDB ObjectID format)"
|
||||||
|
// @Param feedback_type query string false "Filter by feedback type" Enums(like, dislike)
|
||||||
|
// @Param date_from query int64 false "Filter by creation date from (Unix timestamp in seconds)"
|
||||||
|
// @Param date_to query int64 false "Filter by creation date to (Unix timestamp in seconds)"
|
||||||
|
// @Param limit query int false "Number of items per page (default: 20, max: 100)" minimum(1) maximum(100)
|
||||||
|
// @Param offset query int false "Number of items to skip for pagination (default: 0)" minimum(0)
|
||||||
|
// @Success 200 {object} response.APIResponse{data=FeedbackListResponse} "Feedback list retrieved successfully"
|
||||||
|
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid query parameters"
|
||||||
|
// @Failure 422 {object} response.APIResponse{error=string,message=string} "Validation error - Invalid filter criteria"
|
||||||
|
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /admin/v1/feedback [get]
|
||||||
|
func (h *Handler) ListFeedback(c echo.Context) error {
|
||||||
|
// Parse query parameters
|
||||||
|
limitStr := c.QueryParam("limit")
|
||||||
|
offsetStr := c.QueryParam("offset")
|
||||||
|
|
||||||
|
limit := 20 // default limit
|
||||||
|
offset := 0 // default offset
|
||||||
|
|
||||||
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
||||||
|
limit = l
|
||||||
|
}
|
||||||
|
|
||||||
|
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
||||||
|
offset = o
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build search criteria
|
||||||
|
criteria := FeedbackSearchCriteria{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse optional filters
|
||||||
|
if tenderID := c.QueryParam("tender_id"); tenderID != "" {
|
||||||
|
criteria.TenderID = &tenderID
|
||||||
|
}
|
||||||
|
|
||||||
|
if customerID := c.QueryParam("customer_id"); customerID != "" {
|
||||||
|
criteria.CustomerID = &customerID
|
||||||
|
}
|
||||||
|
|
||||||
|
if companyID := c.QueryParam("company_id"); companyID != "" {
|
||||||
|
criteria.CompanyID = &companyID
|
||||||
|
}
|
||||||
|
|
||||||
|
if feedbackType := c.QueryParam("feedback_type"); feedbackType != "" {
|
||||||
|
ft := FeedbackType(feedbackType)
|
||||||
|
criteria.FeedbackType = &ft
|
||||||
|
}
|
||||||
|
|
||||||
|
if dateFrom := c.QueryParam("date_from"); dateFrom != "" {
|
||||||
|
if df, err := strconv.ParseInt(dateFrom, 10, 64); err == nil {
|
||||||
|
criteria.DateFrom = &df
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dateTo := c.QueryParam("date_to"); dateTo != "" {
|
||||||
|
if dt, err := strconv.ParseInt(dateTo, 10, 64); err == nil {
|
||||||
|
criteria.DateTo = &dt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date range
|
||||||
|
if criteria.DateFrom != nil && criteria.DateTo != nil && *criteria.DateFrom >= *criteria.DateTo {
|
||||||
|
return response.ValidationError(c, "Invalid date range", "date_from must be before date_to")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get enhanced feedback list with related data
|
||||||
|
feedbackList, err := h.service.ListFeedback(c.Request().Context(), criteria, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to retrieve feedback list")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, feedbackList.Feedback, "feedback list retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFeedbackByID retrieves feedback by ID with loaded related data
|
||||||
|
// @Summary Get feedback by ID
|
||||||
|
// @Description Retrieve feedback details by its unique identifier with loaded tender, customer, and company information. This endpoint provides comprehensive feedback data including related entity details.
|
||||||
|
// @Tags Admin-Feedback
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Feedback ID (MongoDB ObjectID format)" minlength(24) maxlength(24)
|
||||||
|
// @Success 200 {object} response.APIResponse{data=FeedbackResponse} "feedback retrieved successfully"
|
||||||
|
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid feedback ID format"
|
||||||
|
// @Failure 404 {object} response.APIResponse{error=string,message=string} "Feedback not found"
|
||||||
|
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /admin/v1/feedback/{id} [get]
|
||||||
|
func (h *Handler) GetFeedback(c echo.Context) error {
|
||||||
|
feedbackID := c.Param("id")
|
||||||
|
if feedbackID == "" {
|
||||||
|
return response.BadRequest(c, "Feedback ID is required", "Feedback ID parameter is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
feedback, err := h.service.GetFeedback(c.Request().Context(), feedbackID)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrFeedbackNotFound {
|
||||||
|
return response.NotFound(c, "Feedback not found")
|
||||||
|
}
|
||||||
|
if err == ErrFeedbackDeleted {
|
||||||
|
return response.NotFound(c, "Feedback has been deleted")
|
||||||
|
}
|
||||||
|
return response.InternalServerError(c, "Failed to retrieve feedback")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, feedback, "feedback retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SoftDeleteFeedback marks feedback as deleted without removing it
|
||||||
|
// @Summary Soft delete feedback
|
||||||
|
// @Description Mark feedback as deleted without permanently removing it from the database. The feedback will be marked as deleted and hidden from normal queries but can be restored if needed.
|
||||||
|
// @Tags Admin-Feedback
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Feedback ID (MongoDB ObjectID format)" minlength(24) maxlength(24)
|
||||||
|
// @Success 200 {object} response.APIResponse{message=string} "Feedback soft deleted successfully"
|
||||||
|
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid feedback ID format"
|
||||||
|
// @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated"
|
||||||
|
// @Failure 403 {object} response.APIResponse{error=string,message=string} "Forbidden - User not authorized to delete this feedback"
|
||||||
|
// @Failure 404 {object} response.APIResponse{error=string,message=string} "Not found - Feedback not found"
|
||||||
|
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /admin/v1/feedback/{id} [delete]
|
||||||
|
func (h *Handler) DeleteFeedback(c echo.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
if id == "" {
|
||||||
|
return response.BadRequest(c, "Feedback ID is required", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call service
|
||||||
|
err := h.service.DeleteFeedback(c.Request().Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
switch err {
|
||||||
|
case ErrFeedbackNotFound:
|
||||||
|
return response.BadRequest(c, "Feedback not found", "")
|
||||||
|
case ErrUnauthorizedAccess:
|
||||||
|
return response.Forbidden(c, "You are not authorized to delete this feedback")
|
||||||
|
default:
|
||||||
|
return response.InternalServerError(c, "Failed to soft delete feedback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, nil, "Feedback soft deleted successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************** PUBLIC ***************
|
||||||
|
// ToggleFeedback toggles between like and dislike for a tender
|
||||||
|
// @Summary Toggle feedback for a tender
|
||||||
|
// @Description Toggle between like and dislike for a tender, or create a like if no feedback exists. This endpoint allows users to express their preference for tenders and automatically handles the toggle logic.
|
||||||
|
// @Tags Feedback
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param feedback body ToggleFeedbackForm true "Feedback toggle request containing tender ID"
|
||||||
|
// @Success 200 {object} response.APIResponse{data=FeedbackResponse} "Feedback toggled successfully"
|
||||||
|
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid input data or missing customer/company association"
|
||||||
|
// @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated"
|
||||||
|
// @Failure 422 {object} response.APIResponse{error=string,message=string} "Validation error - Invalid request data format"
|
||||||
|
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/feedback [post]
|
||||||
|
func (h *Handler) ToggleFeedback(c echo.Context) error {
|
||||||
|
form, err := response.Parse[ToggleFeedbackForm](c)
|
||||||
|
if err != nil {
|
||||||
|
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate form using govalidator
|
||||||
|
if _, err := govalidator.ValidateStruct(form); err != nil {
|
||||||
|
return response.ValidationError(c, "Validation failed", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get customer ID from context
|
||||||
|
customerID, err := customer.GetCustomerIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadRequest(c, "Customer ID required", "User must be associated with a customer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get company ID from context
|
||||||
|
companyID, err := user.GetCompanyIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadRequest(c, "Company ID required", "User must be associated with a company")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call service
|
||||||
|
result, err := h.service.ToggleFeedback(c.Request().Context(), *form, &customerID, &companyID)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to toggle feedback")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, result, "Feedback toggled successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFeedback retrieves feedback with pagination and filtering
|
||||||
|
// @Summary List feedback with filters
|
||||||
|
// @Description Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for public use and filters results based on the authenticated user's company.
|
||||||
|
// @Tags Feedback
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param tender_id query string false "Filter by tender ID (MongoDB ObjectID format)"
|
||||||
|
// @Param customer_id query string false "Filter by customer ID (MongoDB ObjectID format)"
|
||||||
|
// @Param company_id query string false "Filter by company ID (MongoDB ObjectID format)"
|
||||||
|
// @Param feedback_type query string false "Filter by feedback type" Enums(like, dislike)
|
||||||
|
// @Param date_from query int64 false "Filter by creation date from (Unix timestamp in seconds)" minimum(0)
|
||||||
|
// @Param date_to query int64 false "Filter by creation date to (Unix timestamp in seconds)" minimum(0)
|
||||||
|
// @Param limit query int false "Number of items per page (default: 20, max: 100)" minimum(1) maximum(100)
|
||||||
|
// @Param offset query int false "Number of items to skip for pagination (default: 0)" minimum(0)
|
||||||
|
// @Success 200 {object} response.APIResponse{data=FeedbackListResponse} "Feedback list retrieved successfully"
|
||||||
|
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid query parameters or missing company association"
|
||||||
|
// @Failure 422 {object} response.APIResponse{error=string,message=string} "Validation error - Invalid filter criteria"
|
||||||
|
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/feedback [get]
|
||||||
|
func (h *Handler) PublicListFeedback(c echo.Context) error {
|
||||||
|
// Parse query parameters
|
||||||
|
limitStr := c.QueryParam("limit")
|
||||||
|
offsetStr := c.QueryParam("offset")
|
||||||
|
|
||||||
|
limit := 20
|
||||||
|
offset := 0
|
||||||
|
|
||||||
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
||||||
|
limit = l
|
||||||
|
}
|
||||||
|
|
||||||
|
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
||||||
|
offset = o
|
||||||
|
}
|
||||||
|
|
||||||
|
companyID, err := user.GetCompanyIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadRequest(c, "Company ID required", "User must be associated with a company")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build search criteria
|
||||||
|
criteria := FeedbackSearchCriteria{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
CompanyID: &companyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if feedbackType := c.QueryParam("feedback_type"); feedbackType != "" {
|
||||||
|
ft := FeedbackType(feedbackType)
|
||||||
|
if ft != FeedbackTypeLike && ft != FeedbackTypeDislike {
|
||||||
|
return response.BadRequest(c, "Invalid feedback type", "Must be 'like' or 'dislike'")
|
||||||
|
}
|
||||||
|
criteria.FeedbackType = &ft
|
||||||
|
}
|
||||||
|
|
||||||
|
if dateFrom := c.QueryParam("date_from"); dateFrom != "" {
|
||||||
|
if df, err := strconv.ParseInt(dateFrom, 10, 64); err == nil && df >= 0 {
|
||||||
|
criteria.DateFrom = &df
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dateTo := c.QueryParam("date_to"); dateTo != "" {
|
||||||
|
if dt, err := strconv.ParseInt(dateTo, 10, 64); err == nil && dt >= 0 {
|
||||||
|
criteria.DateTo = &dt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate date range
|
||||||
|
if criteria.DateFrom != nil && criteria.DateTo != nil && *criteria.DateFrom >= *criteria.DateTo {
|
||||||
|
return response.ValidationError(c, "Invalid date range", "date_from must be before date_to")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call service
|
||||||
|
result, err := h.service.ListFeedback(c.Request().Context(), criteria, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return response.InternalServerError(c, "Failed to list feedback")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, result, "Feedback list retrieved successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicGetFeedback retrieves feedback by ID
|
||||||
|
// @Summary Get feedback by ID
|
||||||
|
// @Description Retrieve feedback details by its unique identifier. This endpoint provides basic feedback information for public users.
|
||||||
|
// @Tags Feedback
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Feedback ID (MongoDB ObjectID format)" minlength(24) maxlength(24)
|
||||||
|
// @Success 200 {object} response.APIResponse{data=FeedbackResponse} "Feedback retrieved successfully"
|
||||||
|
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid feedback ID format"
|
||||||
|
// @Failure 404 {object} response.APIResponse{error=string,message=string} "Feedback not found"
|
||||||
|
// @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error"
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/feedback/{id} [get]
|
||||||
|
func (h *Handler) PublicGetFeedback(c echo.Context) error {
|
||||||
|
feedbackID := c.Param("id")
|
||||||
|
if feedbackID == "" {
|
||||||
|
return response.BadRequest(c, "Feedback ID is required", "Feedback ID parameter is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
feedback, err := h.service.GetFeedback(c.Request().Context(), feedbackID)
|
||||||
|
if err != nil {
|
||||||
|
return response.BadRequest(c, "Failed to get feedback", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Success(c, feedback, "Feedback retrieved successfully")
|
||||||
|
}
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
package feedback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
"tm/pkg/logger"
|
||||||
|
"tm/pkg/mongo"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
driver "go.mongodb.org/mongo-driver/mongo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeedbackRepository interface defines feedback data access methods
|
||||||
|
type Repository interface {
|
||||||
|
// Basic CRUD operations
|
||||||
|
Create(ctx context.Context, feedback *Feedback) error
|
||||||
|
Update(ctx context.Context, feedback *Feedback) error
|
||||||
|
GetByID(ctx context.Context, id string) (*Feedback, error)
|
||||||
|
GetByCompanyID(ctx context.Context, companyID string, tender string) (*Feedback, error)
|
||||||
|
Delete(ctx context.Context, id string) error
|
||||||
|
List(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) ([]*Feedback, int64, error)
|
||||||
|
Search(ctx context.Context, criteria FeedbackSearchCriteria) ([]*Feedback, error)
|
||||||
|
GetFeedbackSummary(ctx context.Context, tenderID string) (*FeedbackSummary, error)
|
||||||
|
GetFeedbackCountByType(ctx context.Context) (map[FeedbackType]int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// feedbackRepository implements FeedbackRepository interface using MongoDB ORM
|
||||||
|
type feedbackRepository struct {
|
||||||
|
repo mongo.Repository[Feedback]
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFeedbackRepository creates a new feedback repository
|
||||||
|
func NewFeedbackRepository(mongoManager *mongo.ConnectionManager, logger logger.Logger) Repository {
|
||||||
|
// Create indexes for feedback collection
|
||||||
|
feedbackIndexes := []mongo.Index{
|
||||||
|
*mongo.NewIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}),
|
||||||
|
*mongo.NewIndex("customer_id_idx", bson.D{{Key: "customer_id", Value: 1}}),
|
||||||
|
*mongo.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}),
|
||||||
|
*mongo.NewIndex("feedback_type_idx", bson.D{{Key: "feedback_type", Value: 1}}),
|
||||||
|
// Compound indexes for common queries
|
||||||
|
*mongo.NewIndex("tender_customer_idx", bson.D{
|
||||||
|
{Key: "tender_id", Value: 1},
|
||||||
|
{Key: "customer_id", Value: 1},
|
||||||
|
}),
|
||||||
|
*mongo.NewIndex("tender_type_idx", bson.D{
|
||||||
|
{Key: "tender_id", Value: 1},
|
||||||
|
{Key: "feedback_type", Value: 1},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create indexes
|
||||||
|
if err := mongoManager.CreateIndexes("feedback", feedbackIndexes); err != nil {
|
||||||
|
logger.Warn("Failed to create feedback indexes", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ORM repository
|
||||||
|
repo := mongo.NewRepository[Feedback](mongoManager.GetCollection("feedback"), logger)
|
||||||
|
|
||||||
|
return &feedbackRepository{
|
||||||
|
repo: repo,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a new feedback
|
||||||
|
func (r *feedbackRepository) Create(ctx context.Context, feedback *Feedback) error {
|
||||||
|
// Set timestamps
|
||||||
|
now := time.Now().Unix()
|
||||||
|
feedback.CreatedAt = now
|
||||||
|
feedback.UpdatedAt = now
|
||||||
|
|
||||||
|
err := r.repo.Create(ctx, feedback)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to create feedback", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
"tender_id": feedback.TenderID,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Feedback created successfully", map[string]interface{}{
|
||||||
|
"feedback_id": feedback.GetID(),
|
||||||
|
"tender_id": feedback.TenderID,
|
||||||
|
"type": feedback.FeedbackType,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update updates an existing feedback
|
||||||
|
func (r *feedbackRepository) Update(ctx context.Context, feedback *Feedback) error {
|
||||||
|
feedback.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
err := r.repo.Update(ctx, feedback)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to update feedback", map[string]interface{}{
|
||||||
|
"feedback_id": feedback.GetID(),
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Feedback updated successfully", map[string]interface{}{
|
||||||
|
"feedback_id": feedback.GetID(),
|
||||||
|
"tender_id": feedback.TenderID,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByID retrieves feedback by ID
|
||||||
|
func (r *feedbackRepository) GetByID(ctx context.Context, id string) (*Feedback, error) {
|
||||||
|
feedback, err := r.repo.FindByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get feedback by ID", map[string]interface{}{
|
||||||
|
"feedback_id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return feedback, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByCompanyID retrieves feedback by company ID and tender ID
|
||||||
|
func (r *feedbackRepository) GetByCompanyID(ctx context.Context, companyID string, tenderID string) (*Feedback, error) {
|
||||||
|
filter := bson.M{"company_id": companyID, "tender_id": tenderID}
|
||||||
|
|
||||||
|
result, err := r.repo.FindOne(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get feedback by company ID and tender ID", map[string]interface{}{
|
||||||
|
"company_id": companyID,
|
||||||
|
"tender_id": tenderID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete permanently deletes feedback by ID
|
||||||
|
func (r *feedbackRepository) Delete(ctx context.Context, id string) error {
|
||||||
|
err := r.repo.Delete(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to delete feedback", map[string]interface{}{
|
||||||
|
"feedback_id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.Info("Feedback deleted successfully", map[string]interface{}{
|
||||||
|
"feedback_id": id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List retrieves feedback with pagination and filtering
|
||||||
|
func (r *feedbackRepository) List(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) ([]*Feedback, int64, error) {
|
||||||
|
filter := r.buildSearchFilter(criteria)
|
||||||
|
|
||||||
|
pagination := mongo.Pagination{
|
||||||
|
Limit: limit,
|
||||||
|
Skip: offset,
|
||||||
|
SortField: "created_at",
|
||||||
|
SortOrder: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.repo.FindAll(ctx, filter, pagination)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to list feedback", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert []TenderFeedback to []*TenderFeedback
|
||||||
|
feedback := make([]*Feedback, len(result.Items))
|
||||||
|
for i := range result.Items {
|
||||||
|
feedback[i] = &result.Items[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return feedback, result.TotalCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByFeedbackType retrieves feedback by type
|
||||||
|
func (r *feedbackRepository) GetByFeedbackType(ctx context.Context, feedbackType FeedbackType, limit, offset int) ([]*Feedback, int64, error) {
|
||||||
|
filter := bson.M{"feedback_type": feedbackType}
|
||||||
|
|
||||||
|
pagination := mongo.Pagination{
|
||||||
|
Limit: limit,
|
||||||
|
Skip: offset,
|
||||||
|
SortField: "created_at",
|
||||||
|
SortOrder: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.repo.FindAll(ctx, filter, pagination)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get feedback by type", map[string]interface{}{
|
||||||
|
"feedback_type": feedbackType,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert []TenderFeedback to []*TenderFeedback
|
||||||
|
feedback := make([]*Feedback, len(result.Items))
|
||||||
|
for i := range result.Items {
|
||||||
|
feedback[i] = &result.Items[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return feedback, result.TotalCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search searches feedback based on criteria
|
||||||
|
func (r *feedbackRepository) Search(ctx context.Context, criteria FeedbackSearchCriteria) ([]*Feedback, error) {
|
||||||
|
filter := r.buildSearchFilter(criteria)
|
||||||
|
|
||||||
|
pagination := mongo.Pagination{
|
||||||
|
Limit: 100,
|
||||||
|
SortField: "created_at",
|
||||||
|
SortOrder: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.repo.FindAll(ctx, filter, pagination)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to search feedback", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert []TenderFeedback to []*TenderFeedback
|
||||||
|
feedback := make([]*Feedback, len(result.Items))
|
||||||
|
for i := range result.Items {
|
||||||
|
feedback[i] = &result.Items[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return feedback, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFeedbackSummary calculates feedback summary for a tender
|
||||||
|
func (r *feedbackRepository) GetFeedbackSummary(ctx context.Context, tenderID string) (*FeedbackSummary, error) {
|
||||||
|
pipeline := driver.Pipeline{
|
||||||
|
{
|
||||||
|
{Key: "$match", Value: bson.M{
|
||||||
|
"tender_id": tenderID,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{Key: "$group", Value: bson.M{
|
||||||
|
"_id": nil,
|
||||||
|
"total_likes": bson.M{
|
||||||
|
"$sum": bson.M{
|
||||||
|
"$cond": []interface{}{
|
||||||
|
bson.M{"$eq": []string{"$feedback_type", string(FeedbackTypeLike)}},
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"total_dislikes": bson.M{
|
||||||
|
"$sum": bson.M{
|
||||||
|
"$cond": []interface{}{
|
||||||
|
bson.M{"$eq": []string{"$feedback_type", string(FeedbackTypeDislike)}},
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"total_comments": bson.M{
|
||||||
|
"$sum": bson.M{
|
||||||
|
"$cond": []interface{}{
|
||||||
|
bson.M{"$ne": []interface{}{"$comment", nil}},
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"total_ratings": bson.M{
|
||||||
|
"$sum": bson.M{
|
||||||
|
"$cond": []interface{}{
|
||||||
|
bson.M{"$gt": []interface{}{"$rating", 0}},
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"rating_sum": bson.M{
|
||||||
|
"$sum": bson.M{
|
||||||
|
"$cond": []interface{}{
|
||||||
|
bson.M{"$gt": []interface{}{"$rating", 0}},
|
||||||
|
"$rating",
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"last_updated": bson.M{"$max": "$updated_at"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := r.repo.Aggregate(ctx, pipeline)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get feedback summary", map[string]interface{}{
|
||||||
|
"tender_id": tenderID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := &FeedbackSummary{
|
||||||
|
TenderID: tenderID,
|
||||||
|
TotalLikes: 0,
|
||||||
|
TotalDislikes: 0,
|
||||||
|
LastUpdated: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) > 0 {
|
||||||
|
result := results[0]
|
||||||
|
if totalLikes, ok := result["total_likes"].(int32); ok {
|
||||||
|
summary.TotalLikes = int64(totalLikes)
|
||||||
|
}
|
||||||
|
if totalDislikes, ok := result["total_dislikes"].(int32); ok {
|
||||||
|
summary.TotalDislikes = int64(totalDislikes)
|
||||||
|
}
|
||||||
|
if lastUpdated, ok := result["last_updated"].(int64); ok {
|
||||||
|
summary.LastUpdated = lastUpdated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFeedbackCountByType gets feedback count grouped by type
|
||||||
|
func (r *feedbackRepository) GetFeedbackCountByType(ctx context.Context) (map[FeedbackType]int64, error) {
|
||||||
|
pipeline := driver.Pipeline{
|
||||||
|
{
|
||||||
|
{Key: "$group", Value: bson.M{
|
||||||
|
"_id": "$feedback_type",
|
||||||
|
"count": bson.M{"$sum": 1},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := r.repo.Aggregate(ctx, pipeline)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
counts := make(map[FeedbackType]int64)
|
||||||
|
for _, result := range results {
|
||||||
|
if feedbackType, ok := result["_id"].(string); ok {
|
||||||
|
if count, ok := result["count"].(int32); ok {
|
||||||
|
counts[FeedbackType(feedbackType)] = int64(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return counts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCustomerFeedbackHistory gets the feedback history for a specific customer
|
||||||
|
func (r *feedbackRepository) GetCustomerFeedbackHistory(ctx context.Context, customerID string, limit, offset int) ([]*Feedback, int64, error) {
|
||||||
|
filter := bson.M{
|
||||||
|
"customer_id": customerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination := mongo.Pagination{
|
||||||
|
Limit: limit,
|
||||||
|
Skip: offset,
|
||||||
|
SortField: "created_at",
|
||||||
|
SortOrder: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := r.repo.FindAll(ctx, filter, pagination)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("Failed to get customer feedback history", map[string]interface{}{
|
||||||
|
"customer_id": customerID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert []TenderFeedback to []*TenderFeedback
|
||||||
|
feedback := make([]*Feedback, len(result.Items))
|
||||||
|
for i := range result.Items {
|
||||||
|
feedback[i] = &result.Items[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return feedback, result.TotalCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSearchFilter builds MongoDB filter from search criteria
|
||||||
|
func (r *feedbackRepository) buildSearchFilter(criteria FeedbackSearchCriteria) bson.M {
|
||||||
|
filter := bson.M{}
|
||||||
|
|
||||||
|
// Always exclude deleted feedback unless specifically requested
|
||||||
|
if criteria.TenderID != nil {
|
||||||
|
filter["tender_id"] = *criteria.TenderID
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.CustomerID != nil {
|
||||||
|
filter["customer_id"] = *criteria.CustomerID
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.CompanyID != nil {
|
||||||
|
filter["company_id"] = *criteria.CompanyID
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.FeedbackType != nil {
|
||||||
|
filter["feedback_type"] = *criteria.FeedbackType
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria.DateFrom != nil || criteria.DateTo != nil {
|
||||||
|
dateFilter := bson.M{}
|
||||||
|
if criteria.DateFrom != nil {
|
||||||
|
dateFilter["$gte"] = *criteria.DateFrom
|
||||||
|
}
|
||||||
|
if criteria.DateTo != nil {
|
||||||
|
dateFilter["$lte"] = *criteria.DateTo
|
||||||
|
}
|
||||||
|
filter["created_at"] = dateFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package feedback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"tm/pkg/logger"
|
||||||
|
"tm/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FeedbackService interface defines feedback business logic methods
|
||||||
|
type FeedbackService interface {
|
||||||
|
ToggleFeedback(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error)
|
||||||
|
GetFeedback(ctx context.Context, id string) (*FeedbackResponse, error)
|
||||||
|
ListFeedback(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) (*FeedbackListResponse, error)
|
||||||
|
DeleteFeedback(ctx context.Context, id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// feedbackService implements FeedbackService interface
|
||||||
|
type feedbackService struct {
|
||||||
|
feedbackRepo Repository
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFeedbackService creates a new feedback service
|
||||||
|
func NewFeedbackService(feedbackRepo Repository, logger logger.Logger) FeedbackService {
|
||||||
|
return &feedbackService{
|
||||||
|
feedbackRepo: feedbackRepo,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *feedbackService) ToggleFeedback(ctx context.Context, req ToggleFeedbackForm, customerID *string, companyID *string) (*FeedbackResponse, error) {
|
||||||
|
existing, err := s.feedbackRepo.GetByCompanyID(ctx, *companyID, req.TenderID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
feedback := &Feedback{
|
||||||
|
TenderID: req.TenderID,
|
||||||
|
FeedbackType: req.FeedbackType,
|
||||||
|
CompanyID: companyID,
|
||||||
|
CustomerID: customerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.feedbackRepo.Create(ctx, feedback)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to create new feedback", map[string]interface{}{
|
||||||
|
"tender_id": req.TenderID,
|
||||||
|
"company_id": *companyID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return feedback.ToResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing.FeedbackType == FeedbackTypeLike {
|
||||||
|
existing.FeedbackType = FeedbackTypeDislike
|
||||||
|
} else {
|
||||||
|
existing.FeedbackType = FeedbackTypeLike
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.feedbackRepo.Update(ctx, existing)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to update existing feedback", map[string]interface{}{
|
||||||
|
"feedback_id": existing.GetID(),
|
||||||
|
"tender_id": req.TenderID,
|
||||||
|
"company_id": *companyID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing.ToResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *feedbackService) GetFeedback(ctx context.Context, id string) (*FeedbackResponse, error) {
|
||||||
|
feedback, err := s.feedbackRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to get feedback", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return feedback.ToResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *feedbackService) ListFeedback(ctx context.Context, criteria FeedbackSearchCriteria, limit, offset int) (*FeedbackListResponse, error) {
|
||||||
|
feedbacks, total, err := s.feedbackRepo.List(ctx, criteria, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to list feedback", map[string]interface{}{
|
||||||
|
"criteria": criteria,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := &response.Meta{
|
||||||
|
Total: int(total),
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
Page: (offset / limit) + 1,
|
||||||
|
Pages: int((total + int64(limit) - 1) / int64(limit)),
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]*FeedbackResponse, len(feedbacks))
|
||||||
|
for i, feedback := range feedbacks {
|
||||||
|
result[i] = feedback.ToResponse()
|
||||||
|
}
|
||||||
|
return &FeedbackListResponse{
|
||||||
|
Feedback: result,
|
||||||
|
Meta: meta,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *feedbackService) DeleteFeedback(ctx context.Context, id string) error {
|
||||||
|
return s.feedbackRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
@@ -421,8 +421,8 @@ func (h *TenderHandler) GetScrapingJob(c echo.Context) error {
|
|||||||
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /admin/scraping/jobs [get]
|
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
|
// @Router /admin/scraping/jobs [get]
|
||||||
func (h *TenderHandler) ListScrapingJobs(c echo.Context) error {
|
func (h *TenderHandler) ListScrapingJobs(c echo.Context) error {
|
||||||
// Parse pagination parameters
|
// Parse pagination parameters
|
||||||
limit := 20
|
limit := 20
|
||||||
@@ -506,6 +506,7 @@ func (h *TenderHandler) CancelScrapingJob(c echo.Context) error {
|
|||||||
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Security BearerAuth
|
||||||
// @Router /api/v1/tenders [get]
|
// @Router /api/v1/tenders [get]
|
||||||
func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
|
func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
|
||||||
// Parse pagination parameters (more restrictive for public API)
|
// Parse pagination parameters (more restrictive for public API)
|
||||||
@@ -611,6 +612,7 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
|
|||||||
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
||||||
// @Failure 404 {object} response.APIResponse
|
// @Failure 404 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
// @Security BearerAuth
|
||||||
// @Router /api/v1/tenders/{id} [get]
|
// @Router /api/v1/tenders/{id} [get]
|
||||||
func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error {
|
func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
|
|||||||
Reference in New Issue
Block a user