From 3911180486cbbef98fd8dd3cf9f9dd77e30f3e34 Mon Sep 17 00:00:00 2001 From: "n.nakhostin" Date: Mon, 8 Sep 2025 14:20:50 +0330 Subject: [PATCH] Refactor Tender Management API Endpoints and Update Documentation - Renamed and restructured tender-related API endpoints for improved clarity and consistency, including changing the route from `/admin/tenders` to `/admin/v1/companies` and updating the associated methods. - Introduced a new SearchForm structure for listing tenders with advanced filtering capabilities, enhancing the API's search functionality. - Updated the service and repository layers to align with the new endpoint structure, ensuring proper handling of tender data. - Enhanced Swagger and YAML documentation to reflect the changes in endpoint structure, including detailed descriptions and examples for the new search functionality. - Improved error handling and response structures to provide clearer feedback to API consumers, ensuring a more robust and user-friendly experience. --- cmd/web/docs/docs.go | 670 +++++++++++++++------------- cmd/web/docs/swagger.json | 670 +++++++++++++++------------- cmd/web/docs/swagger.yaml | 421 +++++++++-------- cmd/web/router/routes.go | 8 +- internal/feedback/service.go | 6 +- internal/tender/entity.go | 21 - internal/tender/form.go | 36 +- internal/tender/handler.go | 261 +++-------- internal/tender/repository.go | 340 +++++--------- internal/tender/service.go | 147 +++--- internal/tender_approval/service.go | 18 +- 11 files changed, 1216 insertions(+), 1382 deletions(-) diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index e1ea1fd..daf5f09 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -24,315 +24,6 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/admin/tenders": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve tenders with pagination, filtering, and search capabilities with optional company-based matching", - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Tenders" - ], - "summary": "List tenders", - "parameters": [ - { - "type": "integer", - "description": "Number of items per page (default: 20, max: 100)", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Number of items to skip (default: 0)", - "name": "offset", - "in": "query" - }, - { - "type": "string", - "description": "Company ID for match percentage calculation and sorting", - "name": "company_id", - "in": "query" - }, - { - "type": "string", - "description": "Search query for title and description", - "name": "query", - "in": "query" - }, - { - "type": "string", - "description": "Filter by notice type code", - "name": "notice_type", - "in": "query" - }, - { - "type": "string", - "description": "Filter by procurement type code", - "name": "procurement_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "csv", - "description": "Filter by country codes (comma-separated)", - "name": "country_codes", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "csv", - "description": "Filter by status (comma-separated)", - "name": "status", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "csv", - "description": "Filter by source (comma-separated)", - "name": "source", - "in": "query" - }, - { - "type": "number", - "description": "Minimum estimated value", - "name": "min_estimated_value", - "in": "query" - }, - { - "type": "number", - "description": "Maximum estimated value", - "name": "max_estimated_value", - "in": "query" - }, - { - "type": "string", - "description": "Filter by currency", - "name": "currency", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": true - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/tenders/{id}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve a specific tender by its ID", - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Tenders" - ], - "summary": "Get tender by ID", - "parameters": [ - { - "type": "string", - "description": "Tender ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/tender.TenderResponse" - } - } - } - ] - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update an existing tender with the provided information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Tenders" - ], - "summary": "Update tender", - "parameters": [ - { - "type": "string", - "description": "Tender ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated tender information", - "name": "tender", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/tender.UpdateTenderRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/tender.TenderResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Delete a tender by its ID", - "tags": [ - "Admin-Tenders" - ], - "summary": "Delete tender", - "parameters": [ - { - "type": "string", - "description": "Tender ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/companies": { "get": { "security": [ @@ -3452,6 +3143,367 @@ const docTemplate = `{ } } }, + "/admin/v1/tenders": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve tenders with pagination, filtering, and search capabilities with optional company-based matching", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "List tenders", + "parameters": [ + { + "type": "string", + "description": "Company ID for match percentage calculation and sorting", + "name": "company_id", + "in": "query" + }, + { + "type": "string", + "description": "Search query for title and description", + "name": "q", + "in": "query" + }, + { + "type": "string", + "description": "Filter by notice type code", + "name": "notice_type", + "in": "query" + }, + { + "type": "string", + "description": "Filter by procurement type code", + "name": "procurement_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "Filter by country codes (comma-separated)", + "name": "country_codes", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "Filter by status (comma-separated)", + "name": "status", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "Filter by source (comma-separated)", + "name": "source", + "in": "query" + }, + { + "type": "number", + "description": "Minimum estimated value", + "name": "min_estimated_value", + "in": "query" + }, + { + "type": "number", + "description": "Maximum estimated value", + "name": "max_estimated_value", + "in": "query" + }, + { + "type": "string", + "description": "Filter by currency", + "name": "currency", + "in": "query" + }, + { + "type": "number", + "description": "Deadline from", + "name": "deadline_from", + "in": "query" + }, + { + "type": "number", + "description": "Deadline to", + "name": "deadline_to", + "in": "query" + }, + { + "type": "number", + "description": "Publication date from", + "name": "publication_date_from", + "in": "query" + }, + { + "type": "number", + "description": "Publication date to", + "name": "publication_date_to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "Filter by languages (comma-separated)", + "name": "languages", + "in": "query" + }, + { + "type": "string", + "description": "Filter by buyer organization ID", + "name": "buyer_organization_id", + "in": "query" + }, + { + "type": "integer", + "description": "Number of items per page (default: 20, max: 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of items to skip (default: 0)", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Sort by field", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Sort order (asc or desc)", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/tenders/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a specific tender by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Get tender by ID", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update an existing tender with the provided information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Update tender", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated tender information", + "name": "tender", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.UpdateTenderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a tender by its ID", + "tags": [ + "Admin-Tenders" + ], + "summary": "Delete tender", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/users": { "get": { "security": [ diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index 707c5ec..84f9cc7 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -18,315 +18,6 @@ "host": "localhost:8082", "basePath": "/", "paths": { - "/admin/tenders": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve tenders with pagination, filtering, and search capabilities with optional company-based matching", - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Tenders" - ], - "summary": "List tenders", - "parameters": [ - { - "type": "integer", - "description": "Number of items per page (default: 20, max: 100)", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Number of items to skip (default: 0)", - "name": "offset", - "in": "query" - }, - { - "type": "string", - "description": "Company ID for match percentage calculation and sorting", - "name": "company_id", - "in": "query" - }, - { - "type": "string", - "description": "Search query for title and description", - "name": "query", - "in": "query" - }, - { - "type": "string", - "description": "Filter by notice type code", - "name": "notice_type", - "in": "query" - }, - { - "type": "string", - "description": "Filter by procurement type code", - "name": "procurement_type", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "csv", - "description": "Filter by country codes (comma-separated)", - "name": "country_codes", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "csv", - "description": "Filter by status (comma-separated)", - "name": "status", - "in": "query" - }, - { - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "csv", - "description": "Filter by source (comma-separated)", - "name": "source", - "in": "query" - }, - { - "type": "number", - "description": "Minimum estimated value", - "name": "min_estimated_value", - "in": "query" - }, - { - "type": "number", - "description": "Maximum estimated value", - "name": "max_estimated_value", - "in": "query" - }, - { - "type": "string", - "description": "Filter by currency", - "name": "currency", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "additionalProperties": true - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, - "/admin/tenders/{id}": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve a specific tender by its ID", - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Tenders" - ], - "summary": "Get tender by ID", - "parameters": [ - { - "type": "string", - "description": "Tender ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/tender.TenderResponse" - } - } - } - ] - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "put": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update an existing tender with the provided information", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Tenders" - ], - "summary": "Update tender", - "parameters": [ - { - "type": "string", - "description": "Tender ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Updated tender information", - "name": "tender", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/tender.UpdateTenderRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/tender.TenderResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - }, - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Delete a tender by its ID", - "tags": [ - "Admin-Tenders" - ], - "summary": "Delete tender", - "parameters": [ - { - "type": "string", - "description": "Tender ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/companies": { "get": { "security": [ @@ -3446,6 +3137,367 @@ } } }, + "/admin/v1/tenders": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve tenders with pagination, filtering, and search capabilities with optional company-based matching", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "List tenders", + "parameters": [ + { + "type": "string", + "description": "Company ID for match percentage calculation and sorting", + "name": "company_id", + "in": "query" + }, + { + "type": "string", + "description": "Search query for title and description", + "name": "q", + "in": "query" + }, + { + "type": "string", + "description": "Filter by notice type code", + "name": "notice_type", + "in": "query" + }, + { + "type": "string", + "description": "Filter by procurement type code", + "name": "procurement_type", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "Filter by country codes (comma-separated)", + "name": "country_codes", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "Filter by status (comma-separated)", + "name": "status", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "Filter by source (comma-separated)", + "name": "source", + "in": "query" + }, + { + "type": "number", + "description": "Minimum estimated value", + "name": "min_estimated_value", + "in": "query" + }, + { + "type": "number", + "description": "Maximum estimated value", + "name": "max_estimated_value", + "in": "query" + }, + { + "type": "string", + "description": "Filter by currency", + "name": "currency", + "in": "query" + }, + { + "type": "number", + "description": "Deadline from", + "name": "deadline_from", + "in": "query" + }, + { + "type": "number", + "description": "Deadline to", + "name": "deadline_to", + "in": "query" + }, + { + "type": "number", + "description": "Publication date from", + "name": "publication_date_from", + "in": "query" + }, + { + "type": "number", + "description": "Publication date to", + "name": "publication_date_to", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "Filter by languages (comma-separated)", + "name": "languages", + "in": "query" + }, + { + "type": "string", + "description": "Filter by buyer organization ID", + "name": "buyer_organization_id", + "in": "query" + }, + { + "type": "integer", + "description": "Number of items per page (default: 20, max: 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of items to skip (default: 0)", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Sort by field", + "name": "sort_by", + "in": "query" + }, + { + "type": "string", + "description": "Sort order (asc or desc)", + "name": "sort_order", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/v1/tenders/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a specific tender by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Get tender by ID", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update an existing tender with the provided information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Update tender", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated tender information", + "name": "tender", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.UpdateTenderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a tender by its ID", + "tags": [ + "Admin-Tenders" + ], + "summary": "Delete tender", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/users": { "get": { "security": [ diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 8e83d80..d14a663 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -1027,199 +1027,6 @@ info: title: Tender Management API version: 2.0.0 paths: - /admin/tenders: - get: - description: Retrieve tenders with pagination, filtering, and search capabilities - with optional company-based matching - parameters: - - description: 'Number of items per page (default: 20, max: 100)' - in: query - name: limit - type: integer - - description: 'Number of items to skip (default: 0)' - in: query - name: offset - type: integer - - description: Company ID for match percentage calculation and sorting - in: query - name: company_id - type: string - - description: Search query for title and description - in: query - name: query - type: string - - description: Filter by notice type code - in: query - name: notice_type - type: string - - description: Filter by procurement type code - in: query - name: procurement_type - type: string - - collectionFormat: csv - description: Filter by country codes (comma-separated) - in: query - items: - type: string - name: country_codes - type: array - - collectionFormat: csv - description: Filter by status (comma-separated) - in: query - items: - type: string - name: status - type: array - - collectionFormat: csv - description: Filter by source (comma-separated) - in: query - items: - type: string - name: source - type: array - - description: Minimum estimated value - in: query - name: min_estimated_value - type: number - - description: Maximum estimated value - in: query - name: max_estimated_value - type: number - - description: Filter by currency - in: query - name: currency - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - additionalProperties: true - type: object - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: List tenders - tags: - - Admin-Tenders - /admin/tenders/{id}: - delete: - description: Delete a tender by its ID - parameters: - - description: Tender ID - in: path - name: id - required: true - type: string - responses: - "200": - description: OK - schema: - $ref: '#/definitions/response.APIResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Delete tender - tags: - - Admin-Tenders - get: - description: Retrieve a specific tender by its ID - parameters: - - description: Tender ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/tender.TenderResponse' - type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Get tender by ID - tags: - - Admin-Tenders - put: - consumes: - - application/json - description: Update an existing tender with the provided information - parameters: - - description: Tender ID - in: path - name: id - required: true - type: string - - description: Updated tender information - in: body - name: tender - required: true - schema: - $ref: '#/definitions/tender.UpdateTenderRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/tender.TenderResponse' - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/response.APIResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: Update tender - tags: - - Admin-Tenders /admin/v1/companies: get: consumes: @@ -3200,6 +3007,234 @@ paths: summary: Get tender approvals by submission mode tags: - Admin-TenderApprovals + /admin/v1/tenders: + get: + description: Retrieve tenders with pagination, filtering, and search capabilities + with optional company-based matching + parameters: + - description: Company ID for match percentage calculation and sorting + in: query + name: company_id + type: string + - description: Search query for title and description + in: query + name: q + type: string + - description: Filter by notice type code + in: query + name: notice_type + type: string + - description: Filter by procurement type code + in: query + name: procurement_type + type: string + - collectionFormat: csv + description: Filter by country codes (comma-separated) + in: query + items: + type: string + name: country_codes + type: array + - collectionFormat: csv + description: Filter by status (comma-separated) + in: query + items: + type: string + name: status + type: array + - collectionFormat: csv + description: Filter by source (comma-separated) + in: query + items: + type: string + name: source + type: array + - description: Minimum estimated value + in: query + name: min_estimated_value + type: number + - description: Maximum estimated value + in: query + name: max_estimated_value + type: number + - description: Filter by currency + in: query + name: currency + type: string + - description: Deadline from + in: query + name: deadline_from + type: number + - description: Deadline to + in: query + name: deadline_to + type: number + - description: Publication date from + in: query + name: publication_date_from + type: number + - description: Publication date to + in: query + name: publication_date_to + type: number + - collectionFormat: csv + description: Filter by languages (comma-separated) + in: query + items: + type: string + name: languages + type: array + - description: Filter by buyer organization ID + in: query + name: buyer_organization_id + type: string + - description: 'Number of items per page (default: 20, max: 100)' + in: query + name: limit + type: integer + - description: 'Number of items to skip (default: 0)' + in: query + name: offset + type: integer + - description: Sort by field + in: query + name: sort_by + type: string + - description: Sort order (asc or desc) + in: query + name: sort_order + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + additionalProperties: true + type: object + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: List tenders + tags: + - Admin-Tenders + /admin/v1/tenders/{id}: + delete: + description: Delete a tender by its ID + parameters: + - description: Tender ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Delete tender + tags: + - Admin-Tenders + get: + description: Retrieve a specific tender by its ID + parameters: + - description: Tender ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/tender.TenderResponse' + type: object + "404": + description: Not Found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get tender by ID + tags: + - Admin-Tenders + put: + consumes: + - application/json + description: Update an existing tender with the provided information + parameters: + - description: Tender ID + in: path + name: id + required: true + type: string + - description: Updated tender information + in: body + name: tender + required: true + schema: + $ref: '#/definitions/tender.UpdateTenderRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/tender.TenderResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Update tender + tags: + - Admin-Tenders /admin/v1/users: get: consumes: diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 5bd7700..4bbd6e9 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -85,10 +85,10 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler tendersGP := adminV1.Group("/tenders") { tendersGP.Use(userHandler.AuthMiddleware()) - tendersGP.GET("", tenderHandler.ListTenders) - tendersGP.GET("/:id", tenderHandler.GetTender) - tendersGP.PUT("/:id", tenderHandler.UpdateTender) - tendersGP.DELETE("/:id", tenderHandler.DeleteTender) + tendersGP.GET("", tenderHandler.Search) + tendersGP.GET("/:id", tenderHandler.Get) + tendersGP.PUT("/:id", tenderHandler.Update) + tendersGP.DELETE("/:id", tenderHandler.Delete) } // Admin Feedback Routes diff --git a/internal/feedback/service.go b/internal/feedback/service.go index 10045f3..8a3bb84 100644 --- a/internal/feedback/service.go +++ b/internal/feedback/service.go @@ -20,12 +20,12 @@ type FeedbackService interface { // feedbackService implements FeedbackService interface type feedbackService struct { feedbackRepo Repository - tenderService tender.TenderService + tenderService tender.Service logger logger.Logger } // NewFeedbackService creates a new feedback service -func NewFeedbackService(feedbackRepo Repository, tenderService tender.TenderService, logger logger.Logger) FeedbackService { +func NewFeedbackService(feedbackRepo Repository, tenderService tender.Service, logger logger.Logger) FeedbackService { return &feedbackService{ feedbackRepo: feedbackRepo, tenderService: tenderService, @@ -133,7 +133,7 @@ func (s *feedbackService) ListFeedback(ctx context.Context, criteria FeedbackSea result := make([]*FeedbackWithTenderResponse, len(feedbacks)) for i, feedback := range feedbacks { - tender, err := s.tenderService.GetTenderByID(ctx, feedback.TenderID) + tender, err := s.tenderService.GetByID(ctx, feedback.TenderID) if err != nil { s.logger.Error("Failed to get tender", map[string]interface{}{ "tender_id": feedback.TenderID, diff --git a/internal/tender/entity.go b/internal/tender/entity.go index 926bfdc..e72488c 100644 --- a/internal/tender/entity.go +++ b/internal/tender/entity.go @@ -119,27 +119,6 @@ const ( TenderSourceBulkImport TenderSource = "bulk_import" ) -// TenderSearchCriteria represents search criteria for tenders -type TenderSearchCriteria struct { - Query string `json:"query,omitempty"` - NoticeType string `json:"notice_type,omitempty"` - ProcurementType string `json:"procurement_type,omitempty"` - CountryCodes []string `json:"country_codes,omitempty"` - RegionCodes []string `json:"region_codes,omitempty"` - Classifications []string `json:"classifications,omitempty"` - MinEstimatedValue *float64 `json:"min_estimated_value,omitempty"` - MaxEstimatedValue *float64 `json:"max_estimated_value,omitempty"` - Currency string `json:"currency,omitempty"` - DeadlineFrom *int64 `json:"deadline_from,omitempty"` - DeadlineTo *int64 `json:"deadline_to,omitempty"` - PublicationDateFrom *int64 `json:"publication_date_from,omitempty"` - PublicationDateTo *int64 `json:"publication_date_to,omitempty"` - Status []TenderStatus `json:"status,omitempty"` - Source []TenderSource `json:"source,omitempty"` - BuyerOrganizationID string `json:"buyer_organization_id,omitempty"` - Languages []string `json:"languages,omitempty"` -} - // TenderStatistics represents tender statistics type TenderStatistics struct { TotalTenders int64 `json:"total_tenders"` diff --git a/internal/tender/form.go b/internal/tender/form.go index 2e294b5..256148d 100644 --- a/internal/tender/form.go +++ b/internal/tender/form.go @@ -13,20 +13,32 @@ type UpdateTenderRequest struct { TenderDeadline *int64 `json:"tender_deadline,omitempty"` // Unix milliseconds } -// ListTendersRequest represents a request to list tenders with pagination and filtering -type ListTendersRequest struct { - Criteria TenderSearchCriteria `json:"criteria"` - Limit int `json:"limit" valid:"range(1|100)~Limit must be between 1 and 100"` - Offset int `json:"offset" valid:"range(0|999999)~Offset must be non-negative"` - CompanyID *string `json:"company_id,omitempty"` // Optional company ID for match percentage calculation - From *int64 `json:"from,omitempty"` - To *int64 `json:"to,omitempty"` +// SearchForm represents a request to list tenders with pagination and filtering +type SearchForm struct { + Search *string `query:"q" valid:"optional"` + NoticeType *string `query:"notice_type" valid:"optional"` + ProcurementType *string `query:"procurement_type" valid:"optional"` + CountryCodes []string `query:"country_codes" valid:"optional"` + RegionCodes []string `query:"region_codes" valid:"optional"` + CompanyID *string `query:"company_id" valid:"optional"` + Status []string `query:"status" valid:"optional"` + Classifications []string `query:"classifications" valid:"optional"` + MinEstimatedValue *float64 `query:"min_estimated_value" valid:"optional"` + MaxEstimatedValue *float64 `query:"max_estimated_value" valid:"optional"` + Currency string `query:"currency" valid:"optional"` + DeadlineFrom *int64 `query:"deadline_from" valid:"optional"` + DeadlineTo *int64 `query:"deadline_to" valid:"optional"` + PublicationDateFrom *int64 `query:"publication_date_from" valid:"optional"` + PublicationDateTo *int64 `query:"publication_date_to" valid:"optional"` + Source []string `query:"source" valid:"optional"` + BuyerOrganizationID *string `query:"buyer_organization_id" valid:"optional"` + Languages []string `query:"languages" valid:"optional"` } -// ListTendersResponse represents the response for listing tenders -type ListTendersResponse struct { +// SearchResponse represents the response for listing tenders +type SearchResponse struct { Tenders []TenderResponse `json:"tenders"` - Metadata *response.Meta `json:"metadata"` + Metadata *response.Meta `json:"-"` } // TenderResponse represents a tender in API responses @@ -57,7 +69,7 @@ type OrganizationResponse struct { } // ToTenderResponse converts a Tender entity to TenderResponse -func (t *Tender) ToTenderResponse() *TenderResponse { +func (t *Tender) ToResponse() *TenderResponse { response := &TenderResponse{ ID: t.ID.Hex(), NoticePublicationID: t.NoticePublicationID, diff --git a/internal/tender/handler.go b/internal/tender/handler.go index 9d65c79..abf890c 100644 --- a/internal/tender/handler.go +++ b/internal/tender/handler.go @@ -1,8 +1,6 @@ package tender import ( - "strconv" - "strings" "tm/pkg/logger" "tm/pkg/response" @@ -12,18 +10,22 @@ import ( // TenderHandler handles tender-related HTTP requests type TenderHandler struct { - service TenderService + service Service logger logger.Logger } // NewTenderHandler creates a new tender handler -func NewTenderHandler(service TenderService, logger logger.Logger) *TenderHandler { +func NewTenderHandler(service Service, logger logger.Logger) *TenderHandler { return &TenderHandler{ service: service, logger: logger, } } +// ***************************************************** +// * ADMIN HANDLERS * +// ***************************************************** + // GetTender retrieves a tender by ID // @Summary Get tender by ID // @Description Retrieve a specific tender by its ID @@ -33,15 +35,15 @@ func NewTenderHandler(service TenderService, logger logger.Logger) *TenderHandle // @Success 200 {object} response.APIResponse{data=TenderResponse} // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /admin/tenders/{id} [get] +// @Router /admin/v1/tenders/{id} [get] // @Security BearerAuth -func (h *TenderHandler) GetTender(c echo.Context) error { +func (h *TenderHandler) Get(c echo.Context) error { id := c.Param("id") if id == "" { return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty") } - tender, err := h.service.GetTenderByID(c.Request().Context(), id) + tender, err := h.service.GetByID(c.Request().Context(), id) if err != nil { return response.NotFound(c, "Tender not found") } @@ -61,9 +63,9 @@ func (h *TenderHandler) GetTender(c echo.Context) error { // @Failure 400 {object} response.APIResponse // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /admin/tenders/{id} [put] +// @Router /admin/v1/tenders/{id} [put] // @Security BearerAuth -func (h *TenderHandler) UpdateTender(c echo.Context) error { +func (h *TenderHandler) Update(c echo.Context) error { id := c.Param("id") if id == "" { return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty") @@ -80,7 +82,7 @@ func (h *TenderHandler) UpdateTender(c echo.Context) error { return response.ValidationError(c, "Validation failed", err.Error()) } - tender, err := h.service.UpdateTender(c.Request().Context(), req) + tender, err := h.service.Update(c.Request().Context(), req) if err != nil { if err.Error() == "tender not found" { return response.NotFound(c, "Tender not found") @@ -88,7 +90,7 @@ func (h *TenderHandler) UpdateTender(c echo.Context) error { return response.InternalServerError(c, "Failed to update tender") } - return response.Success(c, tender.ToTenderResponse(), "Tender updated successfully") + return response.Success(c, tender, "Tender updated successfully") } // DeleteTender deletes a tender @@ -99,15 +101,15 @@ func (h *TenderHandler) UpdateTender(c echo.Context) error { // @Success 200 {object} response.APIResponse // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /admin/tenders/{id} [delete] +// @Router /admin/v1/tenders/{id} [delete] // @Security BearerAuth -func (h *TenderHandler) DeleteTender(c echo.Context) error { +func (h *TenderHandler) Delete(c echo.Context) error { id := c.Param("id") if id == "" { return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty") } - err := h.service.DeleteTender(c.Request().Context(), id) + err := h.service.Delete(c.Request().Context(), id) if err != nil { if err.Error() == "tender not found" { return response.NotFound(c, "Tender not found") @@ -123,10 +125,8 @@ func (h *TenderHandler) DeleteTender(c echo.Context) error { // @Description Retrieve tenders with pagination, filtering, and search capabilities with optional company-based matching // @Tags Admin-Tenders // @Produce json -// @Param limit query int false "Number of items per page (default: 20, max: 100)" -// @Param offset query int false "Number of items to skip (default: 0)" // @Param company_id query string false "Company ID for match percentage calculation and sorting" -// @Param query query string false "Search query for title and description" +// @Param q query string false "Search query for title and description" // @Param notice_type query string false "Filter by notice type code" // @Param procurement_type query string false "Filter by procurement type code" // @Param country_codes query []string false "Filter by country codes (comma-separated)" @@ -135,90 +135,45 @@ func (h *TenderHandler) DeleteTender(c echo.Context) error { // @Param min_estimated_value query number false "Minimum estimated value" // @Param max_estimated_value query number false "Maximum estimated value" // @Param currency query string false "Filter by currency" +// @Param deadline_from query number false "Deadline from" +// @Param deadline_to query number false "Deadline to" +// @Param publication_date_from query number false "Publication date from" +// @Param publication_date_to query number false "Publication date to" +// @Param languages query []string false "Filter by languages (comma-separated)" +// @Param buyer_organization_id query string false "Filter by buyer organization ID" +// @Param limit query int false "Number of items per page (default: 20, max: 100)" +// @Param offset query int false "Number of items to skip (default: 0)" +// @Param sort_by query string false "Sort by field" +// @Param sort_order query string false "Sort order (asc or desc)" // @Success 200 {object} response.APIResponse{data=map[string]interface{}} // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /admin/tenders [get] +// @Router /admin/v1/tenders [get] // @Security BearerAuth -func (h *TenderHandler) ListTenders(c echo.Context) error { - // Parse pagination parameters - limit := 20 - if l := c.QueryParam("limit"); l != "" { - if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 { - limit = parsed - } - } - - offset := 0 - if o := c.QueryParam("offset"); o != "" { - if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { - offset = parsed - } - } - - // Build search criteria - criteria := TenderSearchCriteria{ - Query: c.QueryParam("query"), - NoticeType: c.QueryParam("notice_type"), - ProcurementType: c.QueryParam("procurement_type"), - Currency: c.QueryParam("currency"), - } - - // Parse array parameters - if countryCodes := c.QueryParam("country_codes"); countryCodes != "" { - criteria.CountryCodes = parseStringArray(countryCodes) - } - - if statuses := c.QueryParam("status"); statuses != "" { - statusStrings := parseStringArray(statuses) - criteria.Status = make([]TenderStatus, len(statusStrings)) - for i, s := range statusStrings { - criteria.Status[i] = TenderStatus(s) - } - } - - if sources := c.QueryParam("source"); sources != "" { - sourceStrings := parseStringArray(sources) - criteria.Source = make([]TenderSource, len(sourceStrings)) - for i, s := range sourceStrings { - criteria.Source[i] = TenderSource(s) - } - } - - // Parse numeric parameters - if minValue := c.QueryParam("min_estimated_value"); minValue != "" { - if parsed, err := strconv.ParseFloat(minValue, 64); err == nil { - criteria.MinEstimatedValue = &parsed - } - } - - if maxValue := c.QueryParam("max_estimated_value"); maxValue != "" { - if parsed, err := strconv.ParseFloat(maxValue, 64); err == nil { - criteria.MaxEstimatedValue = &parsed - } - } - - // Parse company_id parameter for matching - var companyID *string - if companyIDParam := c.QueryParam("company_id"); companyIDParam != "" { - companyID = &companyIDParam - } - - req := ListTendersRequest{ - Criteria: criteria, - Limit: limit, - Offset: offset, - CompanyID: companyID, - } - - result, err := h.service.ListTenders(c.Request().Context(), req) +func (h *TenderHandler) Search(c echo.Context) error { + form, err := response.Parse[SearchForm](c) if err != nil { - return response.InternalServerError(c, "Failed to retrieve tenders") + return response.ValidationError(c, "Invalid request data", err.Error()) } - return response.Success(c, result, "Tenders retrieved successfully") + // Validate form + if _, err := govalidator.ValidateStruct(form); err != nil { + return response.ValidationError(c, "Validation failed", err.Error()) + } + + // Call service + tenders, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c)) + if err != nil { + return response.InternalServerError(c, "Failed to list tenders") + } + + return response.SuccessWithMeta(c, tenders, tenders.Metadata, "Tenders retrieved successfully") } +// ***************************************************** +// * PUBLIC HANDLERS * +// ***************************************************** + // GetPublicTenders retrieves public tenders for mobile app // @Summary Get public tenders // @Description Retrieve active tenders for mobile application users with automatic company-based matching from customer profile @@ -237,64 +192,26 @@ func (h *TenderHandler) ListTenders(c echo.Context) error { // @Security BearerAuth // @Router /api/v1/tenders [get] func (h *TenderHandler) GetPublicTenders(c echo.Context) error { - // Parse pagination parameters (more restrictive for public API) - limit := 20 - if parsed, err := strconv.Atoi(c.QueryParam("limit")); err == nil && parsed > 0 && parsed <= 50 { - limit = parsed + + form, err := response.Parse[SearchForm](c) + if err != nil { + return response.BadRequest(c, "Invalid request format", "") } - offset := 0 - if parsed, err := strconv.Atoi(c.QueryParam("offset")); err == nil && parsed >= 0 { - offset = parsed + if _, err := govalidator.ValidateStruct(form); err != nil { + return response.ValidationError(c, err.Error(), "") } - // Build search criteria for public API (only active tenders) - criteria := TenderSearchCriteria{ - Status: []TenderStatus{TenderStatusActive}, - } - - if countryCode := c.QueryParam("country_code"); countryCode != "" { - criteria.CountryCodes = []string{countryCode} - } - - // Parse numeric parameters - if parsed, err := strconv.ParseFloat(c.QueryParam("min_estimated_value"), 64); err == nil { - criteria.MinEstimatedValue = &parsed - } - - if parsed, err := strconv.ParseFloat(c.QueryParam("max_estimated_value"), 64); err == nil { - criteria.MaxEstimatedValue = &parsed - } + form.Status = []string{string(TenderStatusActive)} // Get company ID from customer context for automatic matching var companyID *string if customerCompanyID, ok := c.Get("company_id").(string); ok { companyID = &customerCompanyID } + form.CompanyID = companyID - var from *int64 - if fromParam := c.QueryParam("publication_from"); fromParam != "" { - if parsed, err := strconv.ParseInt(fromParam, 10, 64); err == nil { - from = &parsed - } - } - - var to *int64 - if toParam := c.QueryParam("publication_to"); toParam != "" { - if parsed, err := strconv.ParseInt(toParam, 10, 64); err == nil { - to = &parsed - } - } - - req := ListTendersRequest{ - Criteria: criteria, - Limit: limit, - Offset: offset, - CompanyID: companyID, - From: from, - To: to, - } - result, err := h.service.ListTenders(c.Request().Context(), req) + result, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c)) if err != nil { return response.InternalServerError(c, "Failed to retrieve tenders") } @@ -320,56 +237,26 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { // @Security BearerAuth // @Router /api/v1/tenders/recommend [get] func (h *TenderHandler) RecommendTenders(c echo.Context) error { - // Parse pagination parameters (more restrictive for public API) - limit := 20 - if parsed, err := strconv.Atoi(c.QueryParam("limit")); err == nil && parsed > 0 && parsed <= 50 { - limit = parsed + + form, err := response.Parse[SearchForm](c) + if err != nil { + return response.BadRequest(c, "Invalid request format", "") } - offset := 0 - if parsed, err := strconv.Atoi(c.QueryParam("offset")); err == nil && parsed >= 0 { - offset = parsed + if _, err := govalidator.ValidateStruct(form); err != nil { + return response.ValidationError(c, err.Error(), "") } - // Build search criteria for public API (only active tenders) - criteria := TenderSearchCriteria{ - Status: []TenderStatus{TenderStatusActive}, - } + form.Status = []string{string(TenderStatusActive)} - if countryCode := c.QueryParam("country_code"); countryCode != "" { - criteria.CountryCodes = []string{countryCode} - } - - // Parse numeric parameters - if parsed, err := strconv.ParseFloat(c.QueryParam("min_estimated_value"), 64); err == nil { - criteria.MinEstimatedValue = &parsed - } - - if parsed, err := strconv.ParseFloat(c.QueryParam("max_estimated_value"), 64); err == nil { - criteria.MaxEstimatedValue = &parsed - } - - // Get company ID from customer context for automatic matching var companyID *string if customerCompanyID, ok := c.Get("company_id").(string); ok { companyID = &customerCompanyID } - var from *int64 - if fromParam := c.QueryParam("publication_from"); fromParam != "" { - if parsed, err := strconv.ParseInt(fromParam, 10, 64); err == nil { - from = &parsed - } - } + form.CompanyID = companyID - var to *int64 - if toParam := c.QueryParam("publication_to"); toParam != "" { - if parsed, err := strconv.ParseInt(toParam, 10, 64); err == nil { - to = &parsed - } - } - - result, err := h.service.RecommendTenders(c.Request().Context(), companyID, from, to, limit, offset) + result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c)) if err != nil { return response.InternalServerError(c, "Failed to retrieve tenders") } @@ -394,26 +281,10 @@ func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error { return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty") } - tender, err := h.service.GetTenderByID(c.Request().Context(), id) + tender, err := h.service.GetByID(c.Request().Context(), id) if err != nil { return response.NotFound(c, "Tender not found") } return response.Success(c, tender, "Tender details retrieved successfully") } - -// parseStringArray parses a comma-separated string into a string array -func parseStringArray(s string) []string { - if s == "" { - return nil - } - - result := make([]string, 0) - for _, item := range strings.Split(s, ",") { - if trimmed := strings.TrimSpace(item); trimmed != "" { - result = append(result, trimmed) - } - } - - return result -} diff --git a/internal/tender/repository.go b/internal/tender/repository.go index f801907..05b83de 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -4,7 +4,8 @@ import ( "context" "time" "tm/pkg/logger" - mongopkg "tm/pkg/mongo" + orm "tm/pkg/mongo" + "tm/pkg/response" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" @@ -17,81 +18,73 @@ type TenderRepository interface { GetByID(ctx context.Context, id string) (*Tender, error) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error) - Update(ctx context.Context, tender *Tender) error - Delete(ctx context.Context, id string) error - List(ctx context.Context, criteria TenderSearchCriteria, limit, offset int, from *int64, to *int64) ([]Tender, int64, error) - - // Bulk operations - CreateBatch(ctx context.Context, tenders []Tender) error - UpdateBatch(ctx context.Context, tenders []Tender) error - - // Search and filtering - Search(ctx context.Context, criteria TenderSearchCriteria) ([]Tender, error) - GetByCountryCode(ctx context.Context, countryCode string, limit, offset int) ([]Tender, error) - GetByStatus(ctx context.Context, status TenderStatus, limit, offset int) ([]Tender, error) - GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error) - GetActiveTenders(ctx context.Context, limit, offset int) ([]Tender, error) - - // Statistics and aggregations - GetStatistics(ctx context.Context) (*TenderStatistics, error) GetTenderCountByCountry(ctx context.Context) (map[string]int64, error) GetTenderCountByType(ctx context.Context) (map[string]int64, error) GetTenderCountByClassification(ctx context.Context) (map[string]int64, error) + Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Tender, int64, error) + GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error) + Update(ctx context.Context, tender *Tender) error + Delete(ctx context.Context, id string) error + GetStatistics(ctx context.Context) (*TenderStatistics, error) +} + +func collectionName() string { + return "tenders" } // tenderRepository implements TenderRepository interface using MongoDB ORM type tenderRepository struct { - ormRepo mongopkg.Repository[Tender] - mongoManager *mongopkg.ConnectionManager + ormRepo orm.Repository[Tender] + mongoManager *orm.ConnectionManager logger logger.Logger } // NewTenderRepository creates a new tender repository -func NewTenderRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) TenderRepository { +func NewTenderRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) TenderRepository { // Create indexes for tenders collection - tenderIndexes := []mongopkg.Index{ - *mongopkg.CreateUniqueIndex("notice_publication_id_idx", bson.D{{Key: "notice_publication_id", Value: 1}}), - *mongopkg.NewIndex("contract_notice_id_idx", bson.D{{Key: "contract_notice_id", Value: 1}}), - *mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), - *mongopkg.NewIndex("source_idx", bson.D{{Key: "source", Value: 1}}), - *mongopkg.NewIndex("country_code_idx", bson.D{{Key: "country_code", Value: 1}}), - *mongopkg.NewIndex("notice_type_code_idx", bson.D{{Key: "notice_type_code", Value: 1}}), - *mongopkg.NewIndex("procurement_type_code_idx", bson.D{{Key: "procurement_type_code", Value: 1}}), - *mongopkg.NewIndex("main_classification_idx", bson.D{{Key: "main_classification", Value: 1}}), - *mongopkg.NewIndex("publication_date_idx", bson.D{{Key: "publication_date", Value: -1}}), - *mongopkg.NewIndex("tender_deadline_idx", bson.D{{Key: "tender_deadline", Value: 1}}), - *mongopkg.NewIndex("estimated_value_idx", bson.D{{Key: "estimated_value", Value: -1}}), - *mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), - *mongopkg.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}), + tenderIndexes := []orm.Index{ + *orm.CreateUniqueIndex("notice_publication_id_idx", bson.D{{Key: "notice_publication_id", Value: 1}}), + *orm.NewIndex("contract_notice_id_idx", bson.D{{Key: "contract_notice_id", Value: 1}}), + *orm.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), + *orm.NewIndex("source_idx", bson.D{{Key: "source", Value: 1}}), + *orm.NewIndex("country_code_idx", bson.D{{Key: "country_code", Value: 1}}), + *orm.NewIndex("notice_type_code_idx", bson.D{{Key: "notice_type_code", Value: 1}}), + *orm.NewIndex("procurement_type_code_idx", bson.D{{Key: "procurement_type_code", Value: 1}}), + *orm.NewIndex("main_classification_idx", bson.D{{Key: "main_classification", Value: 1}}), + *orm.NewIndex("publication_date_idx", bson.D{{Key: "publication_date", Value: -1}}), + *orm.NewIndex("tender_deadline_idx", bson.D{{Key: "tender_deadline", Value: 1}}), + *orm.NewIndex("estimated_value_idx", bson.D{{Key: "estimated_value", Value: -1}}), + *orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), + *orm.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}), // Compound indexes for common queries - *mongopkg.NewIndex("status_country_idx", bson.D{ + *orm.NewIndex("status_country_idx", bson.D{ {Key: "status", Value: 1}, {Key: "country_code", Value: 1}, }), - *mongopkg.NewIndex("notice_type_status_idx", bson.D{ + *orm.NewIndex("notice_type_status_idx", bson.D{ {Key: "notice_type_code", Value: 1}, {Key: "status", Value: 1}, }), // Index for CPV matching queries - *mongopkg.NewIndex("cpv_matching_idx", bson.D{ + *orm.NewIndex("cpv_matching_idx", bson.D{ {Key: "status", Value: 1}, {Key: "main_classification", Value: 1}, {Key: "additional_classifications", Value: 1}, {Key: "publication_date", Value: -1}, }), // Text index for search - *mongopkg.CreateTextIndex("search_idx", "title", "description"), + *orm.CreateTextIndex("search_idx", "title", "description"), } // Create indexes - if err := mongoManager.CreateIndexes("tenders", tenderIndexes); err != nil { + if err := mongoManager.CreateIndexes(collectionName(), tenderIndexes); err != nil { logger.Warn("Failed to create tender indexes", map[string]interface{}{ "error": err.Error(), }) } // Create ORM repositories - tenderOrmRepo := mongopkg.NewRepository[Tender](mongoManager.GetCollection("tenders"), logger) + tenderOrmRepo := orm.NewRepository[Tender](mongoManager.GetCollection(collectionName()), logger) return &tenderRepository{ ormRepo: tenderOrmRepo, @@ -102,6 +95,9 @@ func NewTenderRepository(mongoManager *mongopkg.ConnectionManager, logger logger // Create creates a new tender func (r *tenderRepository) Create(ctx context.Context, tender *Tender) error { + now := time.Now().Unix() + tender.SetCreatedAt(now) + err := r.ormRepo.Create(ctx, tender) if err != nil { r.logger.Error("Failed to create tender", map[string]interface{}{ @@ -136,7 +132,7 @@ func (r *tenderRepository) GetByID(ctx context.Context, id string) (*Tender, err // GetByContractNoticeID retrieves a tender by contract notice ID func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) { filter := bson.M{"contract_notice_id": contractNoticeID} - pagination := mongopkg.Pagination{Limit: 1} + pagination := orm.Pagination{Limit: 1} result, err := r.ormRepo.FindAll(ctx, filter, pagination) if err != nil { @@ -148,7 +144,7 @@ func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNo } if len(result.Items) == 0 { - return nil, mongopkg.ErrDocumentNotFound + return nil, orm.ErrDocumentNotFound } return &result.Items[0], nil @@ -157,7 +153,7 @@ func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNo // GetByNoticePublicationID retrieves a tender by notice publication ID func (r *tenderRepository) GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error) { filter := bson.M{"notice_publication_id": noticePublicationID} - pagination := mongopkg.Pagination{Limit: 1} + pagination := orm.Pagination{Limit: 1} result, err := r.ormRepo.FindAll(ctx, filter, pagination) if err != nil { @@ -169,7 +165,7 @@ func (r *tenderRepository) GetByNoticePublicationID(ctx context.Context, noticeP } if len(result.Items) == 0 { - return nil, mongopkg.ErrDocumentNotFound + return nil, orm.ErrDocumentNotFound } return &result.Items[0], nil @@ -213,28 +209,31 @@ func (r *tenderRepository) Delete(ctx context.Context, id string) error { return nil } -// List retrieves tenders with pagination and filtering -func (r *tenderRepository) List(ctx context.Context, criteria TenderSearchCriteria, limit, offset int, from *int64, to *int64) ([]Tender, int64, error) { - filter := r.buildSearchFilter(criteria) +// Search searches tenders based on criteria +func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Tender, int64, error) { + filter := r.buildSearchFilter(form) - pagination := mongopkg.Pagination{ - Limit: limit, - Skip: offset, - SortField: "publication_date", - SortOrder: -1, + // Build sort + sort := "created_at" + if pagination.SortBy != "" { + sort = pagination.SortBy } - if from != nil { - filter["publication_date"] = bson.M{"$gte": *from} + sortOrder := "desc" + if pagination.SortOrder != "" { + sortOrder = pagination.SortOrder } - if to != nil { - filter["publication_date"] = bson.M{"$lte": *to} - } + // Build pagination + paginationBuilder := orm.NewPaginationBuilder(). + Limit(pagination.Limit). + Skip(pagination.Offset). + SortBy(sort, sortOrder). + Build() - result, err := r.ormRepo.FindAll(ctx, filter, pagination) + result, err := r.ormRepo.FindAll(ctx, filter, paginationBuilder) if err != nil { - r.logger.Error("Failed to list tenders", map[string]interface{}{ + r.logger.Error("Failed to search tenders", map[string]interface{}{ "error": err.Error(), }) return nil, 0, err @@ -243,123 +242,6 @@ func (r *tenderRepository) List(ctx context.Context, criteria TenderSearchCriter return result.Items, result.TotalCount, nil } -// CreateBatch creates multiple tenders in a single operation -func (r *tenderRepository) CreateBatch(ctx context.Context, tenders []Tender) error { - if len(tenders) == 0 { - return nil - } - - for i := range tenders { - if err := r.ormRepo.Create(ctx, &tenders[i]); err != nil { - r.logger.Error("Failed to create tender in batch", map[string]interface{}{ - "tender_id": tenders[i].ID, - "error": err.Error(), - }) - return err - } - } - - r.logger.Info("Tender batch created successfully", map[string]interface{}{ - "count": len(tenders), - }) - - return nil -} - -// UpdateBatch updates multiple tenders -func (r *tenderRepository) UpdateBatch(ctx context.Context, tenders []Tender) error { - if len(tenders) == 0 { - return nil - } - - now := time.Now().Unix() - - for i := range tenders { - tenders[i].UpdatedAt = now - - if err := r.ormRepo.Update(ctx, &tenders[i]); err != nil { - r.logger.Error("Failed to update tender in batch", map[string]interface{}{ - "tender_id": tenders[i].ID, - "error": err.Error(), - }) - return err - } - } - - r.logger.Info("Tender batch updated successfully", map[string]interface{}{ - "count": len(tenders), - }) - - return nil -} - -// Search searches tenders based on criteria -func (r *tenderRepository) Search(ctx context.Context, criteria TenderSearchCriteria) ([]Tender, error) { - filter := r.buildSearchFilter(criteria) - - pagination := mongopkg.Pagination{ - Limit: 100, - SortField: "created_at", - SortOrder: -1, - } - - result, err := r.ormRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to search tenders", map[string]interface{}{ - "error": err.Error(), - }) - return nil, err - } - - return result.Items, nil -} - -// GetByCountryCode retrieves tenders by country code -func (r *tenderRepository) GetByCountryCode(ctx context.Context, countryCode string, limit, offset int) ([]Tender, error) { - filter := bson.M{"country_code": countryCode} - - pagination := mongopkg.Pagination{ - Limit: limit, - Skip: offset, - SortField: "created_at", - SortOrder: -1, - } - - result, err := r.ormRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to get tenders by country code", map[string]interface{}{ - "country_code": countryCode, - "error": err.Error(), - }) - return nil, err - } - - return result.Items, nil -} - -// GetByStatus retrieves tenders by status -func (r *tenderRepository) GetByStatus(ctx context.Context, status TenderStatus, limit, offset int) ([]Tender, error) { - filter := bson.M{"status": status} - - pagination := mongopkg.Pagination{ - Limit: limit, - Skip: offset, - SortField: "created_at", - SortOrder: -1, - } - - result, err := r.ormRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to get tenders by status", map[string]interface{}{ - "status": status, - "error": err.Error(), - }) - return nil, err - } - - return result.Items, nil -} - // GetExpiredTenders retrieves tenders that are expired func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error) { filter := bson.M{ @@ -374,7 +256,7 @@ func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int }, } - pagination := mongopkg.Pagination{ + pagination := orm.Pagination{ Limit: 1000, // Large limit for maintenance operations SortField: "tender_deadline", SortOrder: 1, @@ -392,28 +274,6 @@ func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int return result.Items, nil } -// GetActiveTenders retrieves active tenders -func (r *tenderRepository) GetActiveTenders(ctx context.Context, limit, offset int) ([]Tender, error) { - filter := bson.M{"status": TenderStatusActive} - - pagination := mongopkg.Pagination{ - Limit: limit, - Skip: offset, - SortField: "tender_deadline", - SortOrder: 1, - } - - result, err := r.ormRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to get active tenders", map[string]interface{}{ - "error": err.Error(), - }) - return nil, err - } - - return result.Items, nil -} - // GetStatistics calculates tender statistics using aggregation func (r *tenderRepository) GetStatistics(ctx context.Context) (*TenderStatistics, error) { stats := &TenderStatistics{ @@ -557,88 +417,88 @@ func (r *tenderRepository) GetTenderCountByClassification(ctx context.Context) ( return counts, nil } -// buildSearchFilter builds MongoDB filter from search criteria -func (r *tenderRepository) buildSearchFilter(criteria TenderSearchCriteria) bson.M { +// buildSearchFilter builds MongoDB filter from search vvvform +func (r *tenderRepository) buildSearchFilter(form *SearchForm) bson.M { filter := bson.M{} - if criteria.Query != "" { - filter["$text"] = bson.M{"$search": criteria.Query} + if form.Search != nil { + filter["$text"] = bson.M{"$search": *form.Search} } - if criteria.NoticeType != "" { - filter["notice_type_code"] = criteria.NoticeType + if form.NoticeType != nil { + filter["notice_type_code"] = form.NoticeType } - if criteria.ProcurementType != "" { - filter["procurement_type_code"] = criteria.ProcurementType + if form.ProcurementType != nil { + filter["procurement_type_code"] = form.ProcurementType } - if len(criteria.CountryCodes) > 0 { - filter["country_code"] = bson.M{"$in": criteria.CountryCodes} + if len(form.CountryCodes) > 0 { + filter["country_code"] = bson.M{"$in": form.CountryCodes} } - if len(criteria.RegionCodes) > 0 { - filter["region_code"] = bson.M{"$in": criteria.RegionCodes} + if len(form.RegionCodes) > 0 { + filter["region_code"] = bson.M{"$in": form.RegionCodes} } - if len(criteria.Classifications) > 0 { + if len(form.Classifications) > 0 { filter["$or"] = []bson.M{ - {"main_classification": bson.M{"$in": criteria.Classifications}}, - {"additional_classifications": bson.M{"$in": criteria.Classifications}}, + {"main_classification": bson.M{"$in": form.Classifications}}, + {"additional_classifications": bson.M{"$in": form.Classifications}}, } } - if criteria.MinEstimatedValue != nil || criteria.MaxEstimatedValue != nil { + if form.MinEstimatedValue != nil || form.MaxEstimatedValue != nil { valueFilter := bson.M{} - if criteria.MinEstimatedValue != nil { - valueFilter["$gte"] = *criteria.MinEstimatedValue + if form.MinEstimatedValue != nil { + valueFilter["$gte"] = *form.MinEstimatedValue } - if criteria.MaxEstimatedValue != nil { - valueFilter["$lte"] = *criteria.MaxEstimatedValue + if form.MaxEstimatedValue != nil { + valueFilter["$lte"] = *form.MaxEstimatedValue } filter["estimated_value"] = valueFilter } - if criteria.Currency != "" { - filter["currency"] = criteria.Currency + if form.Currency != "" { + filter["currency"] = form.Currency } - if criteria.DeadlineFrom != nil || criteria.DeadlineTo != nil { + if form.DeadlineFrom != nil || form.DeadlineTo != nil { deadlineFilter := bson.M{} - if criteria.DeadlineFrom != nil { - deadlineFilter["$gte"] = *criteria.DeadlineFrom + if form.DeadlineFrom != nil { + deadlineFilter["$gte"] = *form.DeadlineFrom } - if criteria.DeadlineTo != nil { - deadlineFilter["$lte"] = *criteria.DeadlineTo + if form.DeadlineTo != nil { + deadlineFilter["$lte"] = *form.DeadlineTo } filter["tender_deadline"] = deadlineFilter } - if criteria.PublicationDateFrom != nil || criteria.PublicationDateTo != nil { + if form.PublicationDateFrom != nil || form.PublicationDateTo != nil { pubDateFilter := bson.M{} - if criteria.PublicationDateFrom != nil { - pubDateFilter["$gte"] = *criteria.PublicationDateFrom + if form.PublicationDateFrom != nil { + pubDateFilter["$gte"] = *form.PublicationDateFrom } - if criteria.PublicationDateTo != nil { - pubDateFilter["$lte"] = *criteria.PublicationDateTo + if form.PublicationDateTo != nil { + pubDateFilter["$lte"] = *form.PublicationDateTo } filter["publication_date"] = pubDateFilter } - if len(criteria.Status) > 0 { - filter["status"] = bson.M{"$in": criteria.Status} + if len(form.Status) > 0 { + filter["status"] = bson.M{"$in": form.Status} } - if len(criteria.Source) > 0 { - filter["source"] = bson.M{"$in": criteria.Source} + if len(form.Source) > 0 { + filter["source"] = bson.M{"$in": form.Source} } - if criteria.BuyerOrganizationID != "" { - filter["buyer_organization.id"] = criteria.BuyerOrganizationID + if form.BuyerOrganizationID != nil { + filter["buyer_organization.id"] = *form.BuyerOrganizationID } - if len(criteria.Languages) > 0 { - filter["notice_language_code"] = bson.M{"$in": criteria.Languages} + if len(form.Languages) > 0 { + filter["notice_language_code"] = bson.M{"$in": form.Languages} } return filter diff --git a/internal/tender/service.go b/internal/tender/service.go index 3c7c273..0eed84e 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -3,7 +3,6 @@ package tender import ( "context" "fmt" - "math" "time" "tm/internal/company" "tm/pkg/logger" @@ -11,17 +10,17 @@ import ( ) // TenderService interface defines tender business logic operations -type TenderService interface { +type Service interface { // Tender operations - GetTenderByID(ctx context.Context, id string) (*TenderResponse, error) - UpdateTender(ctx context.Context, req UpdateTenderRequest) (*Tender, error) - DeleteTender(ctx context.Context, id string) error - ListTenders(ctx context.Context, req ListTendersRequest) (*ListTendersResponse, error) - RecommendTenders(ctx context.Context, companyID *string, from *int64, to *int64, limit, offset int) (*ListTendersResponse, error) + GetByID(ctx context.Context, id string) (*TenderResponse, error) + Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error) + Delete(ctx context.Context, id string) error + Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error) + Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) // Maintenance operations UpdateExpiredTenders(ctx context.Context) error - CleanupOldData(ctx context.Context, olderThan time.Duration) error + CleanupOldData(ctx context.Context, olderThan time.Duration, pagination *response.Pagination) error } // tenderService implements TenderService interface @@ -32,7 +31,7 @@ type tenderService struct { } // NewTenderService creates a new tender service -func NewTenderService(repository TenderRepository, companyService company.Service, logger logger.Logger) TenderService { +func NewTenderService(repository TenderRepository, companyService company.Service, logger logger.Logger) Service { return &tenderService{ repository: repository, companyService: companyService, @@ -41,7 +40,7 @@ func NewTenderService(repository TenderRepository, companyService company.Servic } // GetTenderByID retrieves a tender by ID -func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*TenderResponse, error) { +func (s *tenderService) GetByID(ctx context.Context, id string) (*TenderResponse, error) { s.logger.Info("Retrieving tender by ID", map[string]interface{}{ "tender_id": id, }) @@ -55,11 +54,11 @@ func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*TenderRe return nil, fmt.Errorf("failed to retrieve tender: %w", err) } - return tender.ToTenderResponse(), nil + return tender.ToResponse(), nil } // UpdateTender updates an existing tender -func (s *tenderService) UpdateTender(ctx context.Context, req UpdateTenderRequest) (*Tender, error) { +func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error) { s.logger.Info("Updating tender", map[string]interface{}{ "tender_id": req.ID, }) @@ -106,11 +105,11 @@ func (s *tenderService) UpdateTender(ctx context.Context, req UpdateTenderReques "tender_id": tender.ID, }) - return tender, nil + return tender.ToResponse(), nil } -// DeleteTender deletes a tender -func (s *tenderService) DeleteTender(ctx context.Context, id string) error { +// Delete deletes a tender +func (s *tenderService) Delete(ctx context.Context, id string) error { s.logger.Info("Deleting tender", map[string]interface{}{ "tender_id": id, }) @@ -131,23 +130,16 @@ func (s *tenderService) DeleteTender(ctx context.Context, id string) error { } // ListTenders retrieves tenders with pagination and filtering -func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) (*ListTendersResponse, error) { +func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) { s.logger.Info("Listing tenders", map[string]interface{}{ - "limit": req.Limit, - "offset": req.Offset, - "company_id": req.CompanyID, - "from": req.From, - "to": req.To, + "limit": pagination.Limit, + "offset": pagination.Offset, + "company_id": form.CompanyID, + "from": form.DeadlineFrom, + "to": form.DeadlineTo, }) - if req.Limit <= 0 { - req.Limit = 20 - } - if req.Limit > 100 { - req.Limit = 100 - } - - tenders, total, err := s.repository.List(ctx, req.Criteria, req.Limit, req.Offset, req.From, req.To) + tenders, total, err := s.repository.Search(ctx, form, pagination) if err != nil { s.logger.Error("Failed to list tenders", map[string]interface{}{ "error": err.Error(), @@ -156,43 +148,36 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) } // If no company ID provided, return tenders without match percentage - if req.CompanyID == nil || *req.CompanyID == "" { + if form.CompanyID == nil || *form.CompanyID == "" { tenderResponses := make([]TenderResponse, 0, len(tenders)) for _, tender := range tenders { - tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) + tenderResponses = append(tenderResponses, *tender.ToResponse()) } - return &ListTendersResponse{ - Tenders: tenderResponses, - Metadata: &response.Meta{ - Total: int(total), - Limit: req.Limit, - Offset: req.Offset, - }, + return &SearchResponse{ + Tenders: tenderResponses, + Metadata: pagination.Response(total), }, nil } // Get company CPV codes for match calculation - comp, err := s.companyService.GetByID(ctx, *req.CompanyID) + comp, err := s.companyService.GetByID(ctx, *form.CompanyID) if err != nil { s.logger.Error("Failed to get company for match calculation", map[string]interface{}{ - "company_id": *req.CompanyID, + "company_id": *form.CompanyID, "error": err.Error(), }) // Continue without match percentage if company not found tenderResponses := make([]TenderResponse, 0, len(tenders)) for _, tender := range tenders { - tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) + tenderResponses = append(tenderResponses, *tender.ToResponse()) } - return &ListTendersResponse{ - Tenders: tenderResponses, - Metadata: &response.Meta{ - Total: int(total), - Limit: req.Limit, - Offset: req.Offset, + return &SearchResponse{ + Tenders: tenderResponses, + Metadata: pagination.Response(total), }, - }, nil + nil } var companyCPVCodes []string @@ -201,52 +186,42 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) } s.logger.Info("Tenders sorted by match percentage", map[string]interface{}{ - "company_id": *req.CompanyID, + "company_id": *form.CompanyID, "company_cpv_codes": len(companyCPVCodes), "total_tenders": len(tenders), }) tenderResponses := make([]TenderResponse, 0, len(tenders)) for _, tender := range tenders { - tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) + tenderResponses = append(tenderResponses, *tender.ToResponse()) } - return &ListTendersResponse{ - Tenders: tenderResponses, - Metadata: &response.Meta{ - Total: int(total), - Limit: req.Limit, - Offset: req.Offset, + return &SearchResponse{ + Tenders: tenderResponses, + Metadata: pagination.Response(total), }, - }, nil + nil } -// ListTenders retrieves tenders with pagination and filtering -func (s *tenderService) RecommendTenders(ctx context.Context, companyID *string, from *int64, to *int64, limit, offset int) (*ListTendersResponse, error) { +// Recommend retrieves tenders with pagination and filtering +func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) { s.logger.Info("Recommend tenders", map[string]interface{}{ - "company_id": companyID, + "company_id": form.CompanyID, }) - if limit <= 0 { - limit = 20 - } - if limit > 100 { - limit = 100 - } - - company, err := s.companyService.GetByID(ctx, *companyID) + company, err := s.companyService.GetByID(ctx, *form.CompanyID) if err != nil { s.logger.Error("Failed to get company", map[string]interface{}{ - "company_id": *companyID, + "company_id": form.CompanyID, "error": err.Error(), }) return nil, fmt.Errorf("failed to get company: %w", err) } - tenders, total, err := s.repository.List(ctx, TenderSearchCriteria{ - Status: []TenderStatus{TenderStatusActive}, - Classifications: company.Tags.CPVCodes, - }, limit, offset, from, to) + form.Status = []string{string(TenderStatusActive)} + form.Classifications = company.Tags.CPVCodes + + tenders, total, err := s.repository.Search(ctx, form, pagination) if err != nil { s.logger.Error("Failed to list tenders", map[string]interface{}{ "error": err.Error(), @@ -256,21 +231,17 @@ func (s *tenderService) RecommendTenders(ctx context.Context, companyID *string, tenderResponses := make([]TenderResponse, 0, len(tenders)) for _, tender := range tenders { - tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) + tenderResponses = append(tenderResponses, *tender.ToResponse()) } - return &ListTendersResponse{ - Tenders: tenderResponses, - Metadata: &response.Meta{ - Total: int(total), - Limit: limit, - Offset: offset, - Pages: int(math.Ceil(float64(total) / float64(limit))), - Page: int(math.Ceil(float64(offset) / float64(limit))), - }, + return &SearchResponse{ + Tenders: tenderResponses, + Metadata: pagination.Response(total), }, nil } +// Delete deletes a tender + // GetTenderStatistics retrieves tender statistics func (s *tenderService) GetTenderStatistics(ctx context.Context) (*TenderStatistics, error) { s.logger.Info("Retrieving tender statistics", map[string]interface{}{}) @@ -323,20 +294,22 @@ func (s *tenderService) UpdateExpiredTenders(ctx context.Context) error { } // CleanupOldData removes old data based on retention policy -func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Duration) error { +func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Duration, pagination *response.Pagination) error { s.logger.Info("Cleaning up old data", map[string]interface{}{ "older_than": olderThan.String(), + "limit": pagination.Limit, + "offset": pagination.Offset, }) cutoffTime := time.Now().Add(-olderThan).Unix() // For now, we'll just log what would be cleaned up // In production, you might want to actually delete old data - criteria := TenderSearchCriteria{ - Status: []TenderStatus{TenderStatusExpired, TenderStatusCancelled, TenderStatusAwarded}, + form := &SearchForm{ + Status: []string{string(TenderStatusExpired), string(TenderStatusCancelled), string(TenderStatusAwarded)}, } - oldTenders, err := s.repository.Search(ctx, criteria) + oldTenders, _, err := s.repository.Search(ctx, form, pagination) if err != nil { s.logger.Error("Failed to find old tenders for cleanup", map[string]interface{}{ "error": err.Error(), diff --git a/internal/tender_approval/service.go b/internal/tender_approval/service.go index 5147242..a09783e 100644 --- a/internal/tender_approval/service.go +++ b/internal/tender_approval/service.go @@ -51,12 +51,12 @@ type Service interface { // tenderApprovalService implements the Service interface type tenderApprovalService struct { repository Repository - tenderService tender.TenderService + tenderService tender.Service logger logger.Logger } // NewTenderApprovalService creates a new tender approval service -func NewTenderApprovalService(repository Repository, tenderService tender.TenderService, logger logger.Logger) Service { +func NewTenderApprovalService(repository Repository, tenderService tender.Service, logger logger.Logger) Service { return &tenderApprovalService{ repository: repository, tenderService: tenderService, @@ -134,7 +134,7 @@ func (s *tenderApprovalService) GetTenderApprovalByIDWithTender(ctx context.Cont } // Get tender details - tenderResponse, err := s.tenderService.GetTenderByID(ctx, tenderApproval.TenderID) + tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID) if err != nil { s.logger.Warn("Failed to get tender details", map[string]interface{}{ "tender_id": tenderApproval.TenderID, @@ -205,7 +205,7 @@ func (s *tenderApprovalService) GetTenderApprovalByTenderAndCompanyWithTender(ct } // Get tender details - tenderResponse, err := s.tenderService.GetTenderByID(ctx, tenderApproval.TenderID) + tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID) if err != nil { s.logger.Warn("Failed to get tender details", map[string]interface{}{ "tender_id": tenderApproval.TenderID, @@ -299,7 +299,7 @@ func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx cont } for _, approval := range tenderApprovals { // Get tender details - tenderResponse, err := s.tenderService.GetTenderByID(ctx, approval.TenderID) + tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID) if err != nil { s.logger.Warn("Failed to get tender details", map[string]interface{}{ "tender_id": approval.TenderID, @@ -444,7 +444,7 @@ func (s *tenderApprovalService) UpdateTenderApprovalWithTender(ctx context.Conte } // Get tender details - tenderResponse, err := s.tenderService.GetTenderByID(ctx, tenderApproval.TenderID) + tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID) if err != nil { s.logger.Warn("Failed to get tender details", map[string]interface{}{ "tender_id": tenderApproval.TenderID, @@ -784,7 +784,7 @@ func (s *tenderApprovalService) GetTenderApprovalsByStatusWithTender(ctx context var responses []*TenderApprovalWithTenderResponse for _, approval := range tenderApprovals { // Get tender details - tenderResponse, err := s.tenderService.GetTenderByID(ctx, approval.TenderID) + tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID) if err != nil { s.logger.Warn("Failed to get tender details", map[string]interface{}{ "tender_id": approval.TenderID, @@ -832,7 +832,7 @@ func (s *tenderApprovalService) GetTenderApprovalsBySubmissionModeWithTender(ctx var responses []*TenderApprovalWithTenderResponse for _, approval := range tenderApprovals { // Get tender details - tenderResponse, err := s.tenderService.GetTenderByID(ctx, approval.TenderID) + tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID) if err != nil { s.logger.Warn("Failed to get tender details", map[string]interface{}{ "tender_id": approval.TenderID, @@ -930,7 +930,7 @@ func (s *tenderApprovalService) ListTenderApprovalsWithTender(ctx context.Contex var responses []*TenderApprovalWithTenderResponse for _, approval := range tenderApprovals { // Get tender details - tenderResponse, err := s.tenderService.GetTenderByID(ctx, approval.TenderID) + tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID) if err != nil { s.logger.Warn("Failed to get tender details", map[string]interface{}{ "tender_id": approval.TenderID,