From 21b736b55bb4cf44001e72d9a8cca17d88257df6 Mon Sep 17 00:00:00 2001 From: "n.nakhostin" Date: Sat, 16 Aug 2025 16:00:03 +0330 Subject: [PATCH] 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. --- Makefile | 18 +- cmd/web/docs/docs.go | 933 ++++++++++++++++++++++++++++++++ cmd/web/docs/swagger.json | 933 ++++++++++++++++++++++++++++++++ cmd/web/docs/swagger.yaml | 546 +++++++++++++++++++ cmd/web/main.go | 14 +- cmd/web/router/routes.go | 23 +- config.yaml | 70 --- internal/company/repository.go | 64 +-- internal/feedback/entity.go | 71 +++ internal/feedback/errors.go | 19 + internal/feedback/form.go | 61 +++ internal/feedback/handler.go | 348 ++++++++++++ internal/feedback/repository.go | 432 +++++++++++++++ internal/feedback/service.go | 120 ++++ internal/tender/handler.go | 4 +- 15 files changed, 3548 insertions(+), 108 deletions(-) delete mode 100644 config.yaml create mode 100644 internal/feedback/entity.go create mode 100644 internal/feedback/errors.go create mode 100644 internal/feedback/form.go create mode 100644 internal/feedback/handler.go create mode 100644 internal/feedback/repository.go create mode 100644 internal/feedback/service.go diff --git a/Makefile b/Makefile index 5e75d8b..661f975 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,8 @@ help: @echo " build - Build the web server" @echo " build-mac - Build for macOS (Intel)" @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 " run - Run the web server" @echo " test - Run tests" @@ -36,8 +38,22 @@ build-mac-arm64: @GOOS=darwin GOARCH=arm64 go build -o bin/web-mac-arm64 ./cmd/web @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-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!" # Run the web server diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 71c4e22..7aaae4c 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -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": { "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.", @@ -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": { "get": { "security": [ @@ -4903,6 +5744,11 @@ const docTemplate = `{ }, "/api/v1/tenders": { "get": { + "security": [ + { + "BearerAuth": [] + } + ], "description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile", "produces": [ "application/json" @@ -4980,6 +5826,11 @@ const docTemplate = `{ }, "/api/v1/tenders/{id}": { "get": { + "security": [ + { + "BearerAuth": [] + } + ], "description": "Retrieve detailed information about a specific tender for mobile application users", "produces": [ "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": { "type": "object", "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", "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", "name": "Authorization" @@ -7278,6 +8207,10 @@ const docTemplate = `{ { "description": "Public tender information access for mobile application including active tender listings and detailed tender information", "name": "Tenders" + }, + { + "description": "Public feedback management operations for mobile application including like, dislike, and comment on tenders", + "name": "Feedback" } ] }` diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index 93d5e5d..a62cba0 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -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": { "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.", @@ -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": { "get": { "security": [ @@ -4897,6 +5738,11 @@ }, "/api/v1/tenders": { "get": { + "security": [ + { + "BearerAuth": [] + } + ], "description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile", "produces": [ "application/json" @@ -4974,6 +5820,11 @@ }, "/api/v1/tenders/{id}": { "get": { + "security": [ + { + "BearerAuth": [] + } + ], "description": "Retrieve detailed information about a specific tender for mobile application users", "produces": [ "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": { "type": "object", "properties": { @@ -7265,6 +8190,10 @@ "description": "Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking", "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", "name": "Authorization" @@ -7272,6 +8201,10 @@ { "description": "Public tender information access for mobile application including active tender listings and detailed tender information", "name": "Tenders" + }, + { + "description": "Public feedback management operations for mobile application including like, dislike, and comment on tenders", + "name": "Feedback" } ] } \ No newline at end of file diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 13fed6e..77b5a89 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -731,6 +731,54 @@ definitions: is_verified: type: boolean 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: properties: service: @@ -3661,6 +3709,255 @@ paths: summary: Get customers by type tags: - 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: get: consumes: @@ -4452,6 +4749,245 @@ paths: summary: Get users by role tags: - 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: get: consumes: @@ -4645,6 +5181,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] summary: Get public tenders tags: - Tenders @@ -4679,6 +5217,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] summary: Get public tender details tags: - Tenders @@ -4714,9 +5254,15 @@ tags: - description: Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking 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 name: Authorization - description: Public tender information access for mobile application including active tender listings and detailed tender information name: Tenders +- description: Public feedback management operations for mobile application including + like, dislike, and comment on tenders + name: Feedback diff --git a/cmd/web/main.go b/cmd/web/main.go index 49975bc..dd5a1eb 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -41,12 +41,18 @@ package main // @tag.name Admin-Scraping // @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.description Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access // @tag.name Tenders // @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 ( "context" "fmt" @@ -54,6 +60,7 @@ import ( "time" "tm/internal/company" "tm/internal/customer" + "tm/internal/feedback" "tm/internal/tender" "tm/internal/user" @@ -98,6 +105,7 @@ func main() { customerRepository := customer.NewCustomerRepository(mongoManager, logger) companyRepository := company.NewCompanyRepository(mongoManager, logger) tenderRepository := tender.NewTenderRepository(mongoManager, logger) + feedbackRepo := feedback.NewFeedbackRepository(mongoManager, logger) logger.Info("Repositories initialized successfully", map[string]interface{}{ "repositories": []string{"customer", "user", "company", "tender"}, @@ -111,6 +119,7 @@ func main() { companyService := company.NewCompanyService(companyRepository, logger) customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService) tenderService := tender.NewTenderService(tenderRepository, companyService, logger) + feedbackService := feedback.NewFeedbackService(feedbackRepo, logger) logger.Info("Services initialized successfully", map[string]interface{}{ "services": []string{"customer", "user", "company", "tender"}, @@ -121,6 +130,7 @@ func main() { customerHandler := customer.NewHandler(customerService, userHandler, customerAuthService, logger) companyHandler := company.NewHandler(companyService, userHandler, logger) tenderHandler := tender.NewTenderHandler(tenderService, logger) + feedbackHandler := feedback.NewFeedbackHandler(feedbackService, userHandler, customerHandler, logger) logger.Info("Handlers initialized successfully", map[string]interface{}{ "handlers": []string{"customer", "user", "company", "tender"}, @@ -130,8 +140,8 @@ func main() { e := initHTTPServer(conf, logger) // Register routes - router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler) - router.RegisterPublicRoutes(e, customerHandler, tenderHandler) + router.RegisterAdminRoutes(e, userHandler, companyHandler, customerHandler, tenderHandler, feedbackHandler) + router.RegisterPublicRoutes(e, customerHandler, tenderHandler, feedbackHandler) // Start server serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port) diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index b22a7f6..a5e4bc0 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -3,13 +3,14 @@ package router import ( "tm/internal/company" "tm/internal/customer" + "tm/internal/feedback" "tm/internal/tender" "tm/internal/user" "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") // Admin Users Routes @@ -111,9 +112,18 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler scrapingGP.GET("/jobs/:id", tenderHandler.GetScrapingJob) 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") customerGP := v1.Group("/profile") @@ -134,4 +144,13 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende tendersGP.GET("", tenderHandler.GetPublicTenders) 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) + } } diff --git a/config.yaml b/config.yaml deleted file mode 100644 index 4d6e1a5..0000000 --- a/config.yaml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/internal/company/repository.go b/internal/company/repository.go index 06257aa..69570f0 100644 --- a/internal/company/repository.go +++ b/internal/company/repository.go @@ -5,7 +5,7 @@ import ( "errors" "time" "tm/pkg/logger" - mongopkg "tm/pkg/mongo" + "tm/pkg/mongo" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" @@ -39,36 +39,36 @@ type Repository interface { // companyRepository implements the Repository interface using the MongoDB ORM type companyRepository struct { - ormRepo mongopkg.Repository[Company] + ormRepo mongo.Repository[Company] logger logger.Logger } // 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 - indexes := []mongopkg.Index{ - *mongopkg.CreateUniqueIndex("name_idx", bson.D{{Key: "name", Value: 1}}), - *mongopkg.CreateUniqueIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}), - *mongopkg.CreateUniqueIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}), - *mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}), - *mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), - *mongopkg.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}), - *mongopkg.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}), - *mongopkg.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}), - *mongopkg.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}), - *mongopkg.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}), - *mongopkg.NewIndex("employee_count_idx", bson.D{{Key: "employee_count", Value: 1}}), - *mongopkg.NewIndex("annual_revenue_idx", bson.D{{Key: "annual_revenue", Value: 1}}), - *mongopkg.NewIndex("founded_year_idx", bson.D{{Key: "founded_year", Value: 1}}), - *mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), + indexes := []mongo.Index{ + *mongo.CreateUniqueIndex("name_idx", bson.D{{Key: "name", Value: 1}}), + *mongo.CreateUniqueIndex("registration_number_idx", bson.D{{Key: "registration_number", Value: 1}}), + *mongo.CreateUniqueIndex("tax_id_idx", bson.D{{Key: "tax_id", Value: 1}}), + *mongo.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}), + *mongo.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), + *mongo.NewIndex("industry_idx", bson.D{{Key: "industry", Value: 1}}), + *mongo.NewIndex("is_verified_idx", bson.D{{Key: "is_verified", Value: 1}}), + *mongo.NewIndex("is_compliant_idx", bson.D{{Key: "is_compliant", Value: 1}}), + *mongo.NewIndex("language_idx", bson.D{{Key: "language", Value: 1}}), + *mongo.NewIndex("currency_idx", bson.D{{Key: "currency", Value: 1}}), + *mongo.NewIndex("employee_count_idx", bson.D{{Key: "employee_count", Value: 1}}), + *mongo.NewIndex("annual_revenue_idx", bson.D{{Key: "annual_revenue", Value: 1}}), + *mongo.NewIndex("founded_year_idx", bson.D{{Key: "founded_year", Value: 1}}), + *mongo.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), // Tag indexes for efficient search - *mongopkg.NewIndex("cpv_codes_idx", bson.D{{Key: "tags.cpv_codes", Value: 1}}), - *mongopkg.NewIndex("categories_idx", bson.D{{Key: "tags.categories", Value: 1}}), - *mongopkg.NewIndex("keywords_idx", bson.D{{Key: "tags.keywords", Value: 1}}), - *mongopkg.NewIndex("specializations_idx", bson.D{{Key: "tags.specializations", Value: 1}}), - *mongopkg.NewIndex("certifications_idx", bson.D{{Key: "tags.certifications", Value: 1}}), + *mongo.NewIndex("cpv_codes_idx", bson.D{{Key: "tags.cpv_codes", Value: 1}}), + *mongo.NewIndex("categories_idx", bson.D{{Key: "tags.categories", Value: 1}}), + *mongo.NewIndex("keywords_idx", bson.D{{Key: "tags.keywords", Value: 1}}), + *mongo.NewIndex("specializations_idx", bson.D{{Key: "tags.specializations", Value: 1}}), + *mongo.NewIndex("certifications_idx", bson.D{{Key: "tags.certifications", Value: 1}}), // Text index for search - *mongopkg.CreateTextIndex("search_idx", "name", "description", "industry"), + *mongo.CreateTextIndex("search_idx", "name", "description", "industry"), } // Create indexes @@ -80,7 +80,7 @@ func NewCompanyRepository(mongoManager *mongopkg.ConnectionManager, logger logge } // Create ORM repository - ormRepo := mongopkg.NewRepository[Company](mongoManager.GetCollection("companies"), logger) + ormRepo := mongo.NewRepository[Company](mongoManager.GetCollection("companies"), logger) return &companyRepository{ 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) { company, err := r.ormRepo.FindByID(ctx, id) if err != nil { - if errors.Is(err, mongopkg.ErrDocumentNotFound) { + if errors.Is(err, mongo.ErrDocumentNotFound) { return nil, errors.New("company not found") } 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} company, err := r.ormRepo.FindOne(ctx, filter) if err != nil { - if errors.Is(err, mongopkg.ErrDocumentNotFound) { + if errors.Is(err, mongo.ErrDocumentNotFound) { return nil, errors.New("company not found") } 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} company, err := r.ormRepo.FindOne(ctx, filter) if err != nil { - if errors.Is(err, mongopkg.ErrDocumentNotFound) { + if errors.Is(err, mongo.ErrDocumentNotFound) { return nil, errors.New("company not found") } 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} company, err := r.ormRepo.FindOne(ctx, filter) if err != nil { - if errors.Is(err, mongopkg.ErrDocumentNotFound) { + if errors.Is(err, mongo.ErrDocumentNotFound) { return nil, errors.New("company not found") } 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 func (r *companyRepository) List(ctx context.Context, limit, offset int) ([]*Company, error) { // Build pagination - pagination := mongopkg.NewPaginationBuilder(). + pagination := mongo.NewPaginationBuilder(). Limit(limit). Skip(offset). SortDesc("created_at"). @@ -407,7 +407,7 @@ func (r *companyRepository) Search(ctx context.Context, search string, companyTy } // Build pagination - pagination := mongopkg.NewPaginationBuilder(). + pagination := mongo.NewPaginationBuilder(). Limit(limit). Skip(offset) @@ -755,7 +755,7 @@ func (r *companyRepository) SearchByTags(ctx context.Context, cpvCodes []string, filter["status"] = bson.M{"$ne": CompanyStatusInactive} // Build pagination - pagination := mongopkg.NewPaginationBuilder(). + pagination := mongo.NewPaginationBuilder(). Limit(limit). Skip(offset). SortDesc("created_at"). diff --git a/internal/feedback/entity.go b/internal/feedback/entity.go new file mode 100644 index 0000000..3691b5f --- /dev/null +++ b/internal/feedback/entity.go @@ -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() +} diff --git a/internal/feedback/errors.go b/internal/feedback/errors.go new file mode 100644 index 0000000..a2196cd --- /dev/null +++ b/internal/feedback/errors.go @@ -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") +) diff --git a/internal/feedback/form.go b/internal/feedback/form.go new file mode 100644 index 0000000..e2f2139 --- /dev/null +++ b/internal/feedback/form.go @@ -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, + } +} diff --git a/internal/feedback/handler.go b/internal/feedback/handler.go new file mode 100644 index 0000000..441a4d6 --- /dev/null +++ b/internal/feedback/handler.go @@ -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") +} diff --git a/internal/feedback/repository.go b/internal/feedback/repository.go new file mode 100644 index 0000000..f949541 --- /dev/null +++ b/internal/feedback/repository.go @@ -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 +} diff --git a/internal/feedback/service.go b/internal/feedback/service.go new file mode 100644 index 0000000..c1afeaf --- /dev/null +++ b/internal/feedback/service.go @@ -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) +} diff --git a/internal/tender/handler.go b/internal/tender/handler.go index 62d6253..00c2a3d 100644 --- a/internal/tender/handler.go +++ b/internal/tender/handler.go @@ -421,8 +421,8 @@ func (h *TenderHandler) GetScrapingJob(c echo.Context) error { // @Success 200 {object} response.APIResponse{data=map[string]interface{}} // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /admin/scraping/jobs [get] // @Security BearerAuth +// @Router /admin/scraping/jobs [get] func (h *TenderHandler) ListScrapingJobs(c echo.Context) error { // Parse pagination parameters limit := 20 @@ -506,6 +506,7 @@ func (h *TenderHandler) CancelScrapingJob(c echo.Context) error { // @Success 200 {object} response.APIResponse{data=map[string]interface{}} // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse +// @Security BearerAuth // @Router /api/v1/tenders [get] func (h *TenderHandler) GetPublicTenders(c echo.Context) error { // 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{}} // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse +// @Security BearerAuth // @Router /api/v1/tenders/{id} [get] func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error { id := c.Param("id")