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.
This commit is contained in:
n.nakhostin
2025-09-08 14:20:50 +03:30
parent 3b265e567d
commit 3911180486
11 changed files with 1216 additions and 1382 deletions
+361 -309
View File
@@ -24,315 +24,6 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "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": { "/admin/v1/companies": {
"get": { "get": {
"security": [ "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": { "/admin/v1/users": {
"get": { "get": {
"security": [ "security": [
+361 -309
View File
@@ -18,315 +18,6 @@
"host": "localhost:8082", "host": "localhost:8082",
"basePath": "/", "basePath": "/",
"paths": { "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": { "/admin/v1/companies": {
"get": { "get": {
"security": [ "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": { "/admin/v1/users": {
"get": { "get": {
"security": [ "security": [
+228 -193
View File
@@ -1027,199 +1027,6 @@ info:
title: Tender Management API title: Tender Management API
version: 2.0.0 version: 2.0.0
paths: 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: /admin/v1/companies:
get: get:
consumes: consumes:
@@ -3200,6 +3007,234 @@ paths:
summary: Get tender approvals by submission mode summary: Get tender approvals by submission mode
tags: tags:
- Admin-TenderApprovals - 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: /admin/v1/users:
get: get:
consumes: consumes:
+4 -4
View File
@@ -85,10 +85,10 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler
tendersGP := adminV1.Group("/tenders") tendersGP := adminV1.Group("/tenders")
{ {
tendersGP.Use(userHandler.AuthMiddleware()) tendersGP.Use(userHandler.AuthMiddleware())
tendersGP.GET("", tenderHandler.ListTenders) tendersGP.GET("", tenderHandler.Search)
tendersGP.GET("/:id", tenderHandler.GetTender) tendersGP.GET("/:id", tenderHandler.Get)
tendersGP.PUT("/:id", tenderHandler.UpdateTender) tendersGP.PUT("/:id", tenderHandler.Update)
tendersGP.DELETE("/:id", tenderHandler.DeleteTender) tendersGP.DELETE("/:id", tenderHandler.Delete)
} }
// Admin Feedback Routes // Admin Feedback Routes
+3 -3
View File
@@ -20,12 +20,12 @@ type FeedbackService interface {
// feedbackService implements FeedbackService interface // feedbackService implements FeedbackService interface
type feedbackService struct { type feedbackService struct {
feedbackRepo Repository feedbackRepo Repository
tenderService tender.TenderService tenderService tender.Service
logger logger.Logger logger logger.Logger
} }
// NewFeedbackService creates a new feedback service // 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{ return &feedbackService{
feedbackRepo: feedbackRepo, feedbackRepo: feedbackRepo,
tenderService: tenderService, tenderService: tenderService,
@@ -133,7 +133,7 @@ func (s *feedbackService) ListFeedback(ctx context.Context, criteria FeedbackSea
result := make([]*FeedbackWithTenderResponse, len(feedbacks)) result := make([]*FeedbackWithTenderResponse, len(feedbacks))
for i, feedback := range 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 { if err != nil {
s.logger.Error("Failed to get tender", map[string]interface{}{ s.logger.Error("Failed to get tender", map[string]interface{}{
"tender_id": feedback.TenderID, "tender_id": feedback.TenderID,
-21
View File
@@ -119,27 +119,6 @@ const (
TenderSourceBulkImport TenderSource = "bulk_import" 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 // TenderStatistics represents tender statistics
type TenderStatistics struct { type TenderStatistics struct {
TotalTenders int64 `json:"total_tenders"` TotalTenders int64 `json:"total_tenders"`
+24 -12
View File
@@ -13,20 +13,32 @@ type UpdateTenderRequest struct {
TenderDeadline *int64 `json:"tender_deadline,omitempty"` // Unix milliseconds TenderDeadline *int64 `json:"tender_deadline,omitempty"` // Unix milliseconds
} }
// ListTendersRequest represents a request to list tenders with pagination and filtering // SearchForm represents a request to list tenders with pagination and filtering
type ListTendersRequest struct { type SearchForm struct {
Criteria TenderSearchCriteria `json:"criteria"` Search *string `query:"q" valid:"optional"`
Limit int `json:"limit" valid:"range(1|100)~Limit must be between 1 and 100"` NoticeType *string `query:"notice_type" valid:"optional"`
Offset int `json:"offset" valid:"range(0|999999)~Offset must be non-negative"` ProcurementType *string `query:"procurement_type" valid:"optional"`
CompanyID *string `json:"company_id,omitempty"` // Optional company ID for match percentage calculation CountryCodes []string `query:"country_codes" valid:"optional"`
From *int64 `json:"from,omitempty"` RegionCodes []string `query:"region_codes" valid:"optional"`
To *int64 `json:"to,omitempty"` 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 // SearchResponse represents the response for listing tenders
type ListTendersResponse struct { type SearchResponse struct {
Tenders []TenderResponse `json:"tenders"` Tenders []TenderResponse `json:"tenders"`
Metadata *response.Meta `json:"metadata"` Metadata *response.Meta `json:"-"`
} }
// TenderResponse represents a tender in API responses // TenderResponse represents a tender in API responses
@@ -57,7 +69,7 @@ type OrganizationResponse struct {
} }
// ToTenderResponse converts a Tender entity to TenderResponse // ToTenderResponse converts a Tender entity to TenderResponse
func (t *Tender) ToTenderResponse() *TenderResponse { func (t *Tender) ToResponse() *TenderResponse {
response := &TenderResponse{ response := &TenderResponse{
ID: t.ID.Hex(), ID: t.ID.Hex(),
NoticePublicationID: t.NoticePublicationID, NoticePublicationID: t.NoticePublicationID,
+66 -195
View File
@@ -1,8 +1,6 @@
package tender package tender
import ( import (
"strconv"
"strings"
"tm/pkg/logger" "tm/pkg/logger"
"tm/pkg/response" "tm/pkg/response"
@@ -12,18 +10,22 @@ import (
// TenderHandler handles tender-related HTTP requests // TenderHandler handles tender-related HTTP requests
type TenderHandler struct { type TenderHandler struct {
service TenderService service Service
logger logger.Logger logger logger.Logger
} }
// NewTenderHandler creates a new tender handler // NewTenderHandler creates a new tender handler
func NewTenderHandler(service TenderService, logger logger.Logger) *TenderHandler { func NewTenderHandler(service Service, logger logger.Logger) *TenderHandler {
return &TenderHandler{ return &TenderHandler{
service: service, service: service,
logger: logger, logger: logger,
} }
} }
// *****************************************************
// * ADMIN HANDLERS *
// *****************************************************
// GetTender retrieves a tender by ID // GetTender retrieves a tender by ID
// @Summary Get tender by ID // @Summary Get tender by ID
// @Description Retrieve a specific tender by its 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} // @Success 200 {object} response.APIResponse{data=TenderResponse}
// @Failure 404 {object} response.APIResponse // @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/tenders/{id} [get] // @Router /admin/v1/tenders/{id} [get]
// @Security BearerAuth // @Security BearerAuth
func (h *TenderHandler) GetTender(c echo.Context) error { func (h *TenderHandler) Get(c echo.Context) error {
id := c.Param("id") id := c.Param("id")
if id == "" { if id == "" {
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty") 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 { if err != nil {
return response.NotFound(c, "Tender not found") 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 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse // @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/tenders/{id} [put] // @Router /admin/v1/tenders/{id} [put]
// @Security BearerAuth // @Security BearerAuth
func (h *TenderHandler) UpdateTender(c echo.Context) error { func (h *TenderHandler) Update(c echo.Context) error {
id := c.Param("id") id := c.Param("id")
if id == "" { if id == "" {
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty") 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()) 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 != nil {
if err.Error() == "tender not found" { if err.Error() == "tender not found" {
return response.NotFound(c, "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.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 // DeleteTender deletes a tender
@@ -99,15 +101,15 @@ func (h *TenderHandler) UpdateTender(c echo.Context) error {
// @Success 200 {object} response.APIResponse // @Success 200 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse // @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/tenders/{id} [delete] // @Router /admin/v1/tenders/{id} [delete]
// @Security BearerAuth // @Security BearerAuth
func (h *TenderHandler) DeleteTender(c echo.Context) error { func (h *TenderHandler) Delete(c echo.Context) error {
id := c.Param("id") id := c.Param("id")
if id == "" { if id == "" {
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty") 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 != nil {
if err.Error() == "tender not found" { if err.Error() == "tender not found" {
return response.NotFound(c, "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 // @Description Retrieve tenders with pagination, filtering, and search capabilities with optional company-based matching
// @Tags Admin-Tenders // @Tags Admin-Tenders
// @Produce json // @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 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 notice_type query string false "Filter by notice type code"
// @Param procurement_type query string false "Filter by procurement 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)" // @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 min_estimated_value query number false "Minimum estimated value"
// @Param max_estimated_value query number false "Maximum estimated value" // @Param max_estimated_value query number false "Maximum estimated value"
// @Param currency query string false "Filter by currency" // @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{}} // @Success 200 {object} response.APIResponse{data=map[string]interface{}}
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /admin/tenders [get] // @Router /admin/v1/tenders [get]
// @Security BearerAuth // @Security BearerAuth
func (h *TenderHandler) ListTenders(c echo.Context) error { func (h *TenderHandler) Search(c echo.Context) error {
// Parse pagination parameters form, err := response.Parse[SearchForm](c)
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)
if err != nil { 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 // GetPublicTenders retrieves public tenders for mobile app
// @Summary Get public tenders // @Summary Get public tenders
// @Description Retrieve active tenders for mobile application users with automatic company-based matching from customer profile // @Description Retrieve active tenders for mobile application users with automatic company-based matching from customer profile
@@ -237,64 +192,26 @@ func (h *TenderHandler) ListTenders(c echo.Context) error {
// @Security BearerAuth // @Security BearerAuth
// @Router /api/v1/tenders [get] // @Router /api/v1/tenders [get]
func (h *TenderHandler) GetPublicTenders(c echo.Context) error { func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
// Parse pagination parameters (more restrictive for public API)
limit := 20 form, err := response.Parse[SearchForm](c)
if parsed, err := strconv.Atoi(c.QueryParam("limit")); err == nil && parsed > 0 && parsed <= 50 { if err != nil {
limit = parsed return response.BadRequest(c, "Invalid request format", "")
} }
offset := 0 if _, err := govalidator.ValidateStruct(form); err != nil {
if parsed, err := strconv.Atoi(c.QueryParam("offset")); err == nil && parsed >= 0 { return response.ValidationError(c, err.Error(), "")
offset = parsed
} }
// Build search criteria for public API (only active tenders) form.Status = []string{string(TenderStatusActive)}
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
}
// Get company ID from customer context for automatic matching // Get company ID from customer context for automatic matching
var companyID *string var companyID *string
if customerCompanyID, ok := c.Get("company_id").(string); ok { if customerCompanyID, ok := c.Get("company_id").(string); ok {
companyID = &customerCompanyID companyID = &customerCompanyID
} }
form.CompanyID = companyID
var from *int64 result, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
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)
if err != nil { if err != nil {
return response.InternalServerError(c, "Failed to retrieve tenders") return response.InternalServerError(c, "Failed to retrieve tenders")
} }
@@ -320,56 +237,26 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
// @Security BearerAuth // @Security BearerAuth
// @Router /api/v1/tenders/recommend [get] // @Router /api/v1/tenders/recommend [get]
func (h *TenderHandler) RecommendTenders(c echo.Context) error { func (h *TenderHandler) RecommendTenders(c echo.Context) error {
// Parse pagination parameters (more restrictive for public API)
limit := 20 form, err := response.Parse[SearchForm](c)
if parsed, err := strconv.Atoi(c.QueryParam("limit")); err == nil && parsed > 0 && parsed <= 50 { if err != nil {
limit = parsed return response.BadRequest(c, "Invalid request format", "")
} }
offset := 0 if _, err := govalidator.ValidateStruct(form); err != nil {
if parsed, err := strconv.Atoi(c.QueryParam("offset")); err == nil && parsed >= 0 { return response.ValidationError(c, err.Error(), "")
offset = parsed
} }
// Build search criteria for public API (only active tenders) form.Status = []string{string(TenderStatusActive)}
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
}
// Get company ID from customer context for automatic matching
var companyID *string var companyID *string
if customerCompanyID, ok := c.Get("company_id").(string); ok { if customerCompanyID, ok := c.Get("company_id").(string); ok {
companyID = &customerCompanyID companyID = &customerCompanyID
} }
var from *int64 form.CompanyID = companyID
if fromParam := c.QueryParam("publication_from"); fromParam != "" {
if parsed, err := strconv.ParseInt(fromParam, 10, 64); err == nil {
from = &parsed
}
}
var to *int64 result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c))
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)
if err != nil { if err != nil {
return response.InternalServerError(c, "Failed to retrieve tenders") 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") 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 { if err != nil {
return response.NotFound(c, "Tender not found") return response.NotFound(c, "Tender not found")
} }
return response.Success(c, tender, "Tender details retrieved successfully") 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
}
+100 -240
View File
@@ -4,7 +4,8 @@ import (
"context" "context"
"time" "time"
"tm/pkg/logger" "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/bson"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
@@ -17,81 +18,73 @@ type TenderRepository interface {
GetByID(ctx context.Context, id string) (*Tender, error) GetByID(ctx context.Context, id string) (*Tender, error)
GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error)
GetByNoticePublicationID(ctx context.Context, noticePublicationID 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) GetTenderCountByCountry(ctx context.Context) (map[string]int64, error)
GetTenderCountByType(ctx context.Context) (map[string]int64, error) GetTenderCountByType(ctx context.Context) (map[string]int64, error)
GetTenderCountByClassification(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 // tenderRepository implements TenderRepository interface using MongoDB ORM
type tenderRepository struct { type tenderRepository struct {
ormRepo mongopkg.Repository[Tender] ormRepo orm.Repository[Tender]
mongoManager *mongopkg.ConnectionManager mongoManager *orm.ConnectionManager
logger logger.Logger logger logger.Logger
} }
// NewTenderRepository creates a new tender repository // 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 // Create indexes for tenders collection
tenderIndexes := []mongopkg.Index{ tenderIndexes := []orm.Index{
*mongopkg.CreateUniqueIndex("notice_publication_id_idx", bson.D{{Key: "notice_publication_id", Value: 1}}), *orm.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}}), *orm.NewIndex("contract_notice_id_idx", bson.D{{Key: "contract_notice_id", Value: 1}}),
*mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), *orm.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
*mongopkg.NewIndex("source_idx", bson.D{{Key: "source", Value: 1}}), *orm.NewIndex("source_idx", bson.D{{Key: "source", Value: 1}}),
*mongopkg.NewIndex("country_code_idx", bson.D{{Key: "country_code", Value: 1}}), *orm.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}}), *orm.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}}), *orm.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}}), *orm.NewIndex("main_classification_idx", bson.D{{Key: "main_classification", Value: 1}}),
*mongopkg.NewIndex("publication_date_idx", bson.D{{Key: "publication_date", Value: -1}}), *orm.NewIndex("publication_date_idx", bson.D{{Key: "publication_date", Value: -1}}),
*mongopkg.NewIndex("tender_deadline_idx", bson.D{{Key: "tender_deadline", Value: 1}}), *orm.NewIndex("tender_deadline_idx", bson.D{{Key: "tender_deadline", Value: 1}}),
*mongopkg.NewIndex("estimated_value_idx", bson.D{{Key: "estimated_value", Value: -1}}), *orm.NewIndex("estimated_value_idx", bson.D{{Key: "estimated_value", Value: -1}}),
*mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), *orm.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}),
*mongopkg.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}), *orm.NewIndex("updated_at_idx", bson.D{{Key: "updated_at", Value: -1}}),
// Compound indexes for common queries // Compound indexes for common queries
*mongopkg.NewIndex("status_country_idx", bson.D{ *orm.NewIndex("status_country_idx", bson.D{
{Key: "status", Value: 1}, {Key: "status", Value: 1},
{Key: "country_code", 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: "notice_type_code", Value: 1},
{Key: "status", Value: 1}, {Key: "status", Value: 1},
}), }),
// Index for CPV matching queries // Index for CPV matching queries
*mongopkg.NewIndex("cpv_matching_idx", bson.D{ *orm.NewIndex("cpv_matching_idx", bson.D{
{Key: "status", Value: 1}, {Key: "status", Value: 1},
{Key: "main_classification", Value: 1}, {Key: "main_classification", Value: 1},
{Key: "additional_classifications", Value: 1}, {Key: "additional_classifications", Value: 1},
{Key: "publication_date", Value: -1}, {Key: "publication_date", Value: -1},
}), }),
// Text index for search // Text index for search
*mongopkg.CreateTextIndex("search_idx", "title", "description"), *orm.CreateTextIndex("search_idx", "title", "description"),
} }
// Create indexes // 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{}{ logger.Warn("Failed to create tender indexes", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
}) })
} }
// Create ORM repositories // Create ORM repositories
tenderOrmRepo := mongopkg.NewRepository[Tender](mongoManager.GetCollection("tenders"), logger) tenderOrmRepo := orm.NewRepository[Tender](mongoManager.GetCollection(collectionName()), logger)
return &tenderRepository{ return &tenderRepository{
ormRepo: tenderOrmRepo, ormRepo: tenderOrmRepo,
@@ -102,6 +95,9 @@ func NewTenderRepository(mongoManager *mongopkg.ConnectionManager, logger logger
// Create creates a new tender // Create creates a new tender
func (r *tenderRepository) Create(ctx context.Context, tender *Tender) error { func (r *tenderRepository) Create(ctx context.Context, tender *Tender) error {
now := time.Now().Unix()
tender.SetCreatedAt(now)
err := r.ormRepo.Create(ctx, tender) err := r.ormRepo.Create(ctx, tender)
if err != nil { if err != nil {
r.logger.Error("Failed to create tender", map[string]interface{}{ 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 // GetByContractNoticeID retrieves a tender by contract notice ID
func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) { func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) {
filter := bson.M{"contract_notice_id": contractNoticeID} 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) result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil { if err != nil {
@@ -148,7 +144,7 @@ func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNo
} }
if len(result.Items) == 0 { if len(result.Items) == 0 {
return nil, mongopkg.ErrDocumentNotFound return nil, orm.ErrDocumentNotFound
} }
return &result.Items[0], nil 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 // GetByNoticePublicationID retrieves a tender by notice publication ID
func (r *tenderRepository) GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error) { func (r *tenderRepository) GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error) {
filter := bson.M{"notice_publication_id": noticePublicationID} 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) result, err := r.ormRepo.FindAll(ctx, filter, pagination)
if err != nil { if err != nil {
@@ -169,7 +165,7 @@ func (r *tenderRepository) GetByNoticePublicationID(ctx context.Context, noticeP
} }
if len(result.Items) == 0 { if len(result.Items) == 0 {
return nil, mongopkg.ErrDocumentNotFound return nil, orm.ErrDocumentNotFound
} }
return &result.Items[0], nil return &result.Items[0], nil
@@ -213,28 +209,31 @@ func (r *tenderRepository) Delete(ctx context.Context, id string) error {
return nil return nil
} }
// List retrieves tenders with pagination and filtering // Search searches tenders based on criteria
func (r *tenderRepository) List(ctx context.Context, criteria TenderSearchCriteria, limit, offset int, from *int64, to *int64) ([]Tender, int64, error) { func (r *tenderRepository) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) ([]Tender, int64, error) {
filter := r.buildSearchFilter(criteria) filter := r.buildSearchFilter(form)
pagination := mongopkg.Pagination{ // Build sort
Limit: limit, sort := "created_at"
Skip: offset, if pagination.SortBy != "" {
SortField: "publication_date", sort = pagination.SortBy
SortOrder: -1,
} }
if from != nil { sortOrder := "desc"
filter["publication_date"] = bson.M{"$gte": *from} if pagination.SortOrder != "" {
sortOrder = pagination.SortOrder
} }
if to != nil { // Build pagination
filter["publication_date"] = bson.M{"$lte": *to} 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 { 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(), "error": err.Error(),
}) })
return nil, 0, err return nil, 0, err
@@ -243,123 +242,6 @@ func (r *tenderRepository) List(ctx context.Context, criteria TenderSearchCriter
return result.Items, result.TotalCount, nil 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 // GetExpiredTenders retrieves tenders that are expired
func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error) { func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int64) ([]Tender, error) {
filter := bson.M{ 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 Limit: 1000, // Large limit for maintenance operations
SortField: "tender_deadline", SortField: "tender_deadline",
SortOrder: 1, SortOrder: 1,
@@ -392,28 +274,6 @@ func (r *tenderRepository) GetExpiredTenders(ctx context.Context, beforeDate int
return result.Items, nil 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 // GetStatistics calculates tender statistics using aggregation
func (r *tenderRepository) GetStatistics(ctx context.Context) (*TenderStatistics, error) { func (r *tenderRepository) GetStatistics(ctx context.Context) (*TenderStatistics, error) {
stats := &TenderStatistics{ stats := &TenderStatistics{
@@ -557,88 +417,88 @@ func (r *tenderRepository) GetTenderCountByClassification(ctx context.Context) (
return counts, nil return counts, nil
} }
// buildSearchFilter builds MongoDB filter from search criteria // buildSearchFilter builds MongoDB filter from search vvvform
func (r *tenderRepository) buildSearchFilter(criteria TenderSearchCriteria) bson.M { func (r *tenderRepository) buildSearchFilter(form *SearchForm) bson.M {
filter := bson.M{} filter := bson.M{}
if criteria.Query != "" { if form.Search != nil {
filter["$text"] = bson.M{"$search": criteria.Query} filter["$text"] = bson.M{"$search": *form.Search}
} }
if criteria.NoticeType != "" { if form.NoticeType != nil {
filter["notice_type_code"] = criteria.NoticeType filter["notice_type_code"] = form.NoticeType
} }
if criteria.ProcurementType != "" { if form.ProcurementType != nil {
filter["procurement_type_code"] = criteria.ProcurementType filter["procurement_type_code"] = form.ProcurementType
} }
if len(criteria.CountryCodes) > 0 { if len(form.CountryCodes) > 0 {
filter["country_code"] = bson.M{"$in": criteria.CountryCodes} filter["country_code"] = bson.M{"$in": form.CountryCodes}
} }
if len(criteria.RegionCodes) > 0 { if len(form.RegionCodes) > 0 {
filter["region_code"] = bson.M{"$in": criteria.RegionCodes} filter["region_code"] = bson.M{"$in": form.RegionCodes}
} }
if len(criteria.Classifications) > 0 { if len(form.Classifications) > 0 {
filter["$or"] = []bson.M{ filter["$or"] = []bson.M{
{"main_classification": bson.M{"$in": criteria.Classifications}}, {"main_classification": bson.M{"$in": form.Classifications}},
{"additional_classifications": bson.M{"$in": criteria.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{} valueFilter := bson.M{}
if criteria.MinEstimatedValue != nil { if form.MinEstimatedValue != nil {
valueFilter["$gte"] = *criteria.MinEstimatedValue valueFilter["$gte"] = *form.MinEstimatedValue
} }
if criteria.MaxEstimatedValue != nil { if form.MaxEstimatedValue != nil {
valueFilter["$lte"] = *criteria.MaxEstimatedValue valueFilter["$lte"] = *form.MaxEstimatedValue
} }
filter["estimated_value"] = valueFilter filter["estimated_value"] = valueFilter
} }
if criteria.Currency != "" { if form.Currency != "" {
filter["currency"] = criteria.Currency filter["currency"] = form.Currency
} }
if criteria.DeadlineFrom != nil || criteria.DeadlineTo != nil { if form.DeadlineFrom != nil || form.DeadlineTo != nil {
deadlineFilter := bson.M{} deadlineFilter := bson.M{}
if criteria.DeadlineFrom != nil { if form.DeadlineFrom != nil {
deadlineFilter["$gte"] = *criteria.DeadlineFrom deadlineFilter["$gte"] = *form.DeadlineFrom
} }
if criteria.DeadlineTo != nil { if form.DeadlineTo != nil {
deadlineFilter["$lte"] = *criteria.DeadlineTo deadlineFilter["$lte"] = *form.DeadlineTo
} }
filter["tender_deadline"] = deadlineFilter filter["tender_deadline"] = deadlineFilter
} }
if criteria.PublicationDateFrom != nil || criteria.PublicationDateTo != nil { if form.PublicationDateFrom != nil || form.PublicationDateTo != nil {
pubDateFilter := bson.M{} pubDateFilter := bson.M{}
if criteria.PublicationDateFrom != nil { if form.PublicationDateFrom != nil {
pubDateFilter["$gte"] = *criteria.PublicationDateFrom pubDateFilter["$gte"] = *form.PublicationDateFrom
} }
if criteria.PublicationDateTo != nil { if form.PublicationDateTo != nil {
pubDateFilter["$lte"] = *criteria.PublicationDateTo pubDateFilter["$lte"] = *form.PublicationDateTo
} }
filter["publication_date"] = pubDateFilter filter["publication_date"] = pubDateFilter
} }
if len(criteria.Status) > 0 { if len(form.Status) > 0 {
filter["status"] = bson.M{"$in": criteria.Status} filter["status"] = bson.M{"$in": form.Status}
} }
if len(criteria.Source) > 0 { if len(form.Source) > 0 {
filter["source"] = bson.M{"$in": criteria.Source} filter["source"] = bson.M{"$in": form.Source}
} }
if criteria.BuyerOrganizationID != "" { if form.BuyerOrganizationID != nil {
filter["buyer_organization.id"] = criteria.BuyerOrganizationID filter["buyer_organization.id"] = *form.BuyerOrganizationID
} }
if len(criteria.Languages) > 0 { if len(form.Languages) > 0 {
filter["notice_language_code"] = bson.M{"$in": criteria.Languages} filter["notice_language_code"] = bson.M{"$in": form.Languages}
} }
return filter return filter
+56 -83
View File
@@ -3,7 +3,6 @@ package tender
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"time" "time"
"tm/internal/company" "tm/internal/company"
"tm/pkg/logger" "tm/pkg/logger"
@@ -11,17 +10,17 @@ import (
) )
// TenderService interface defines tender business logic operations // TenderService interface defines tender business logic operations
type TenderService interface { type Service interface {
// Tender operations // Tender operations
GetTenderByID(ctx context.Context, id string) (*TenderResponse, error) GetByID(ctx context.Context, id string) (*TenderResponse, error)
UpdateTender(ctx context.Context, req UpdateTenderRequest) (*Tender, error) Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error)
DeleteTender(ctx context.Context, id string) error Delete(ctx context.Context, id string) error
ListTenders(ctx context.Context, req ListTendersRequest) (*ListTendersResponse, error) Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
RecommendTenders(ctx context.Context, companyID *string, from *int64, to *int64, limit, offset int) (*ListTendersResponse, error) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
// Maintenance operations // Maintenance operations
UpdateExpiredTenders(ctx context.Context) error 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 // tenderService implements TenderService interface
@@ -32,7 +31,7 @@ type tenderService struct {
} }
// NewTenderService creates a new tender service // 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{ return &tenderService{
repository: repository, repository: repository,
companyService: companyService, companyService: companyService,
@@ -41,7 +40,7 @@ func NewTenderService(repository TenderRepository, companyService company.Servic
} }
// GetTenderByID retrieves a tender by ID // 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{}{ s.logger.Info("Retrieving tender by ID", map[string]interface{}{
"tender_id": id, "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 nil, fmt.Errorf("failed to retrieve tender: %w", err)
} }
return tender.ToTenderResponse(), nil return tender.ToResponse(), nil
} }
// UpdateTender updates an existing tender // 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{}{ s.logger.Info("Updating tender", map[string]interface{}{
"tender_id": req.ID, "tender_id": req.ID,
}) })
@@ -106,11 +105,11 @@ func (s *tenderService) UpdateTender(ctx context.Context, req UpdateTenderReques
"tender_id": tender.ID, "tender_id": tender.ID,
}) })
return tender, nil return tender.ToResponse(), nil
} }
// DeleteTender deletes a tender // Delete deletes a tender
func (s *tenderService) DeleteTender(ctx context.Context, id string) error { func (s *tenderService) Delete(ctx context.Context, id string) error {
s.logger.Info("Deleting tender", map[string]interface{}{ s.logger.Info("Deleting tender", map[string]interface{}{
"tender_id": id, "tender_id": id,
}) })
@@ -131,23 +130,16 @@ func (s *tenderService) DeleteTender(ctx context.Context, id string) error {
} }
// ListTenders retrieves tenders with pagination and filtering // 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{}{ s.logger.Info("Listing tenders", map[string]interface{}{
"limit": req.Limit, "limit": pagination.Limit,
"offset": req.Offset, "offset": pagination.Offset,
"company_id": req.CompanyID, "company_id": form.CompanyID,
"from": req.From, "from": form.DeadlineFrom,
"to": req.To, "to": form.DeadlineTo,
}) })
if req.Limit <= 0 { tenders, total, err := s.repository.Search(ctx, form, pagination)
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)
if err != nil { if err != nil {
s.logger.Error("Failed to list tenders", map[string]interface{}{ s.logger.Error("Failed to list tenders", map[string]interface{}{
"error": err.Error(), "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 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)) tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders { for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) tenderResponses = append(tenderResponses, *tender.ToResponse())
} }
return &ListTendersResponse{ return &SearchResponse{
Tenders: tenderResponses, Tenders: tenderResponses,
Metadata: &response.Meta{ Metadata: pagination.Response(total),
Total: int(total),
Limit: req.Limit,
Offset: req.Offset,
},
}, nil }, nil
} }
// Get company CPV codes for match calculation // 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 { if err != nil {
s.logger.Error("Failed to get company for match calculation", map[string]interface{}{ s.logger.Error("Failed to get company for match calculation", map[string]interface{}{
"company_id": *req.CompanyID, "company_id": *form.CompanyID,
"error": err.Error(), "error": err.Error(),
}) })
// Continue without match percentage if company not found // Continue without match percentage if company not found
tenderResponses := make([]TenderResponse, 0, len(tenders)) tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders { for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) tenderResponses = append(tenderResponses, *tender.ToResponse())
} }
return &ListTendersResponse{ return &SearchResponse{
Tenders: tenderResponses, Tenders: tenderResponses,
Metadata: &response.Meta{ Metadata: pagination.Response(total),
Total: int(total),
Limit: req.Limit,
Offset: req.Offset,
}, },
}, nil nil
} }
var companyCPVCodes []string 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{}{ s.logger.Info("Tenders sorted by match percentage", map[string]interface{}{
"company_id": *req.CompanyID, "company_id": *form.CompanyID,
"company_cpv_codes": len(companyCPVCodes), "company_cpv_codes": len(companyCPVCodes),
"total_tenders": len(tenders), "total_tenders": len(tenders),
}) })
tenderResponses := make([]TenderResponse, 0, len(tenders)) tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders { for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) tenderResponses = append(tenderResponses, *tender.ToResponse())
} }
return &ListTendersResponse{ return &SearchResponse{
Tenders: tenderResponses, Tenders: tenderResponses,
Metadata: &response.Meta{ Metadata: pagination.Response(total),
Total: int(total),
Limit: req.Limit,
Offset: req.Offset,
}, },
}, nil nil
} }
// ListTenders retrieves tenders with pagination and filtering // Recommend retrieves tenders with pagination and filtering
func (s *tenderService) RecommendTenders(ctx context.Context, companyID *string, from *int64, to *int64, limit, offset int) (*ListTendersResponse, error) { func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
s.logger.Info("Recommend tenders", map[string]interface{}{ s.logger.Info("Recommend tenders", map[string]interface{}{
"company_id": companyID, "company_id": form.CompanyID,
}) })
if limit <= 0 { company, err := s.companyService.GetByID(ctx, *form.CompanyID)
limit = 20
}
if limit > 100 {
limit = 100
}
company, err := s.companyService.GetByID(ctx, *companyID)
if err != nil { if err != nil {
s.logger.Error("Failed to get company", map[string]interface{}{ s.logger.Error("Failed to get company", map[string]interface{}{
"company_id": *companyID, "company_id": form.CompanyID,
"error": err.Error(), "error": err.Error(),
}) })
return nil, fmt.Errorf("failed to get company: %w", err) return nil, fmt.Errorf("failed to get company: %w", err)
} }
tenders, total, err := s.repository.List(ctx, TenderSearchCriteria{ form.Status = []string{string(TenderStatusActive)}
Status: []TenderStatus{TenderStatusActive}, form.Classifications = company.Tags.CPVCodes
Classifications: company.Tags.CPVCodes,
}, limit, offset, from, to) tenders, total, err := s.repository.Search(ctx, form, pagination)
if err != nil { if err != nil {
s.logger.Error("Failed to list tenders", map[string]interface{}{ s.logger.Error("Failed to list tenders", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
@@ -256,21 +231,17 @@ func (s *tenderService) RecommendTenders(ctx context.Context, companyID *string,
tenderResponses := make([]TenderResponse, 0, len(tenders)) tenderResponses := make([]TenderResponse, 0, len(tenders))
for _, tender := range tenders { for _, tender := range tenders {
tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) tenderResponses = append(tenderResponses, *tender.ToResponse())
} }
return &ListTendersResponse{ return &SearchResponse{
Tenders: tenderResponses, Tenders: tenderResponses,
Metadata: &response.Meta{ Metadata: pagination.Response(total),
Total: int(total),
Limit: limit,
Offset: offset,
Pages: int(math.Ceil(float64(total) / float64(limit))),
Page: int(math.Ceil(float64(offset) / float64(limit))),
},
}, nil }, nil
} }
// Delete deletes a tender
// GetTenderStatistics retrieves tender statistics // GetTenderStatistics retrieves tender statistics
func (s *tenderService) GetTenderStatistics(ctx context.Context) (*TenderStatistics, error) { func (s *tenderService) GetTenderStatistics(ctx context.Context) (*TenderStatistics, error) {
s.logger.Info("Retrieving tender statistics", map[string]interface{}{}) 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 // 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{}{ s.logger.Info("Cleaning up old data", map[string]interface{}{
"older_than": olderThan.String(), "older_than": olderThan.String(),
"limit": pagination.Limit,
"offset": pagination.Offset,
}) })
cutoffTime := time.Now().Add(-olderThan).Unix() cutoffTime := time.Now().Add(-olderThan).Unix()
// For now, we'll just log what would be cleaned up // For now, we'll just log what would be cleaned up
// In production, you might want to actually delete old data // In production, you might want to actually delete old data
criteria := TenderSearchCriteria{ form := &SearchForm{
Status: []TenderStatus{TenderStatusExpired, TenderStatusCancelled, TenderStatusAwarded}, 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 { if err != nil {
s.logger.Error("Failed to find old tenders for cleanup", map[string]interface{}{ s.logger.Error("Failed to find old tenders for cleanup", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
+9 -9
View File
@@ -51,12 +51,12 @@ type Service interface {
// tenderApprovalService implements the Service interface // tenderApprovalService implements the Service interface
type tenderApprovalService struct { type tenderApprovalService struct {
repository Repository repository Repository
tenderService tender.TenderService tenderService tender.Service
logger logger.Logger logger logger.Logger
} }
// NewTenderApprovalService creates a new tender approval service // 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{ return &tenderApprovalService{
repository: repository, repository: repository,
tenderService: tenderService, tenderService: tenderService,
@@ -134,7 +134,7 @@ func (s *tenderApprovalService) GetTenderApprovalByIDWithTender(ctx context.Cont
} }
// Get tender details // Get tender details
tenderResponse, err := s.tenderService.GetTenderByID(ctx, tenderApproval.TenderID) tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID)
if err != nil { if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{ s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": tenderApproval.TenderID, "tender_id": tenderApproval.TenderID,
@@ -205,7 +205,7 @@ func (s *tenderApprovalService) GetTenderApprovalByTenderAndCompanyWithTender(ct
} }
// Get tender details // Get tender details
tenderResponse, err := s.tenderService.GetTenderByID(ctx, tenderApproval.TenderID) tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID)
if err != nil { if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{ s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": tenderApproval.TenderID, "tender_id": tenderApproval.TenderID,
@@ -299,7 +299,7 @@ func (s *tenderApprovalService) GetTenderApprovalsByCompanyIDWithTender(ctx cont
} }
for _, approval := range tenderApprovals { for _, approval := range tenderApprovals {
// Get tender details // Get tender details
tenderResponse, err := s.tenderService.GetTenderByID(ctx, approval.TenderID) tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
if err != nil { if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{ s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": approval.TenderID, "tender_id": approval.TenderID,
@@ -444,7 +444,7 @@ func (s *tenderApprovalService) UpdateTenderApprovalWithTender(ctx context.Conte
} }
// Get tender details // Get tender details
tenderResponse, err := s.tenderService.GetTenderByID(ctx, tenderApproval.TenderID) tenderResponse, err := s.tenderService.GetByID(ctx, tenderApproval.TenderID)
if err != nil { if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{ s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": tenderApproval.TenderID, "tender_id": tenderApproval.TenderID,
@@ -784,7 +784,7 @@ func (s *tenderApprovalService) GetTenderApprovalsByStatusWithTender(ctx context
var responses []*TenderApprovalWithTenderResponse var responses []*TenderApprovalWithTenderResponse
for _, approval := range tenderApprovals { for _, approval := range tenderApprovals {
// Get tender details // Get tender details
tenderResponse, err := s.tenderService.GetTenderByID(ctx, approval.TenderID) tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
if err != nil { if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{ s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": approval.TenderID, "tender_id": approval.TenderID,
@@ -832,7 +832,7 @@ func (s *tenderApprovalService) GetTenderApprovalsBySubmissionModeWithTender(ctx
var responses []*TenderApprovalWithTenderResponse var responses []*TenderApprovalWithTenderResponse
for _, approval := range tenderApprovals { for _, approval := range tenderApprovals {
// Get tender details // Get tender details
tenderResponse, err := s.tenderService.GetTenderByID(ctx, approval.TenderID) tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
if err != nil { if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{ s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": approval.TenderID, "tender_id": approval.TenderID,
@@ -930,7 +930,7 @@ func (s *tenderApprovalService) ListTenderApprovalsWithTender(ctx context.Contex
var responses []*TenderApprovalWithTenderResponse var responses []*TenderApprovalWithTenderResponse
for _, approval := range tenderApprovals { for _, approval := range tenderApprovals {
// Get tender details // Get tender details
tenderResponse, err := s.tenderService.GetTenderByID(ctx, approval.TenderID) tenderResponse, err := s.tenderService.GetByID(ctx, approval.TenderID)
if err != nil { if err != nil {
s.logger.Warn("Failed to get tender details", map[string]interface{}{ s.logger.Warn("Failed to get tender details", map[string]interface{}{
"tender_id": approval.TenderID, "tender_id": approval.TenderID,