From 0a23ff985abb6862eb3966396bd05b603de82c23 Mon Sep 17 00:00:00 2001 From: "n.nakhostin" Date: Wed, 13 Aug 2025 19:47:58 +0330 Subject: [PATCH] Enhance Tender Management with Company-Based Matching and API Updates - Updated the tender service to include company-based matching for tenders, allowing for more relevant results based on company CPV codes. - Modified the ListTenders endpoint to accept a company ID parameter for calculating match percentages and sorting tenders accordingly. - Refactored the tender response structure to include match percentage and days until deadline for better client-side handling. - Updated API documentation to reflect changes in the tender listing functionality and added new endpoints for public tender access. - Removed deprecated customer-related methods and streamlined customer listing functionality for improved clarity and performance. --- cmd/web/docs/docs.go | 1798 ++++++++++++++++++++++++++++--- cmd/web/docs/swagger.json | 1798 ++++++++++++++++++++++++++++--- cmd/web/docs/swagger.yaml | 1163 ++++++++++++++++++-- cmd/web/main.go | 2 +- cmd/web/router/routes.go | 2 +- internal/customer/handler.go | 41 +- internal/customer/repository.go | 65 -- internal/customer/service.go | 74 -- internal/tender/entity.go | 59 + internal/tender/form.go | 31 +- internal/tender/handler.go | 56 +- internal/tender/repository.go | 7 + internal/tender/service.go | 115 +- 13 files changed, 4543 insertions(+), 668 deletions(-) diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 8ce73fc..58266af 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -24,6 +24,765 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/admin/scraping/jobs": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve scraping jobs with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Scraping" + ], + "summary": "List scraping jobs", + "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" + } + ], + "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" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Start a new TED XML scraping job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Scraping" + ], + "summary": "Start scraping job", + "parameters": [ + { + "description": "Scraping job configuration", + "name": "job", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.StartScrapingJobRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.ScrapingJobResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/scraping/jobs/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a specific scraping job by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Scraping" + ], + "summary": "Get scraping job", + "parameters": [ + { + "type": "string", + "description": "Scraping job ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.ScrapingJobResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/scraping/jobs/{id}/cancel": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Cancel a running or pending scraping job", + "tags": [ + "Admin-Scraping" + ], + "summary": "Cancel scraping job", + "parameters": [ + { + "type": "string", + "description": "Scraping job ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "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" + } + } + } + } + }, + "/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" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new tender with the provided information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Create a new tender", + "parameters": [ + { + "description": "Tender information", + "name": "tender", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.CreateTenderRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/tenders/dashboard": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve dashboard data including statistics, recent tenders, and active jobs", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Get dashboard data", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/tenders/search": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Search tenders using full-text search and filtering", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Search tenders", + "parameters": [ + { + "description": "Search criteria", + "name": "search", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.SearchTendersRequest" + } + } + ], + "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/statistics": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve comprehensive statistics about tenders", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Get tender statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderStatistics" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/tenders/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a specific tender by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Get tender by ID", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update an existing tender with the provided information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Update tender", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated tender information", + "name": "tender", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.UpdateTenderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a tender by its ID", + "tags": [ + "Admin-Tenders" + ], + "summary": "Delete tender", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/companies": { "get": { "security": [ @@ -2006,174 +2765,6 @@ const docTemplate = `{ } } }, - "/admin/v1/customers/with-companies": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve a paginated list of customers with their assigned companies and advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Customers" - ], - "summary": "List customers with companies and filters", - "parameters": [ - { - "type": "string", - "description": "Search term to filter customers by name, email, or company name", - "name": "search", - "in": "query" - }, - { - "enum": [ - "individual", - "company", - "government" - ], - "type": "string", - "description": "Filter by customer type", - "name": "type", - "in": "query" - }, - { - "enum": [ - "active", - "inactive", - "suspended", - "pending" - ], - "type": "string", - "description": "Filter by customer status", - "name": "status", - "in": "query" - }, - { - "type": "string", - "format": "uuid", - "description": "Filter by company ID", - "name": "company_id", - "in": "query" - }, - { - "type": "string", - "description": "Filter by industry", - "name": "industry", - "in": "query" - }, - { - "type": "boolean", - "description": "Filter by verification status", - "name": "is_verified", - "in": "query" - }, - { - "type": "boolean", - "description": "Filter by compliance status", - "name": "is_compliant", - "in": "query" - }, - { - "type": "string", - "description": "Filter by preferred language", - "name": "language", - "in": "query" - }, - { - "type": "string", - "description": "Filter by preferred currency", - "name": "currency", - "in": "query" - }, - { - "maximum": 100, - "minimum": 1, - "type": "integer", - "default": 20, - "description": "Number of customers per page (1-100)", - "name": "limit", - "in": "query" - }, - { - "minimum": 0, - "type": "integer", - "default": 0, - "description": "Number of customers to skip for pagination", - "name": "offset", - "in": "query" - }, - { - "enum": [ - "email", - "company_name", - "created_at", - "updated_at", - "status" - ], - "type": "string", - "default": "created_at", - "description": "Field to sort by", - "name": "sort_by", - "in": "query" - }, - { - "enum": [ - "asc", - "desc" - ], - "type": "string", - "default": "desc", - "description": "Sort order", - "name": "sort_order", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Customers with companies retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.CustomerListResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request - Invalid query parameters", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "422": { - "description": "Validation error - Invalid query parameters", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/customers/{id}": { "get": { "security": [ @@ -4309,6 +4900,137 @@ const docTemplate = `{ } } } + }, + "/api/v1/tenders": { + "get": { + "description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile", + "produces": [ + "application/json" + ], + "tags": [ + "Tenders" + ], + "summary": "Get public tenders", + "parameters": [ + { + "type": "integer", + "description": "Number of items per page (default: 20, max: 50)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of items to skip (default: 0)", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Filter by country code", + "name": "country_code", + "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" + } + ], + "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" + } + } + } + } + }, + "/api/v1/tenders/{id}": { + "get": { + "description": "Retrieve detailed information about a specific tender for mobile application users", + "produces": [ + "application/json" + ], + "tags": [ + "Tenders" + ], + "summary": "Get public tender details", + "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": { + "type": "object", + "additionalProperties": true + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } } }, "definitions": { @@ -5516,6 +6238,734 @@ const docTemplate = `{ } } }, + "tender.Address": { + "type": "object", + "properties": { + "city_name": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "country_subentity_code": { + "type": "string" + }, + "department": { + "type": "string" + }, + "postal_zone": { + "type": "string" + }, + "region": { + "type": "string" + }, + "street_name": { + "type": "string" + } + } + }, + "tender.CreateTenderRequest": { + "type": "object", + "properties": { + "additional_classifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "buyer_organization": { + "$ref": "#/definitions/tender.Organization" + }, + "city_name": { + "type": "string" + }, + "contract_folder_id": { + "type": "string" + }, + "contract_notice_id": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "document_uri": { + "type": "string" + }, + "duration": { + "type": "string" + }, + "duration_unit": { + "type": "string" + }, + "estimated_value": { + "type": "number" + }, + "gazette_id": { + "type": "string" + }, + "issue_date": { + "description": "Unix milliseconds", + "type": "integer" + }, + "issue_time": { + "description": "Unix milliseconds", + "type": "integer" + }, + "main_classification": { + "type": "string" + }, + "notice_language_code": { + "type": "string" + }, + "notice_publication_id": { + "type": "string" + }, + "notice_sub_type_code": { + "type": "string" + }, + "notice_type_code": { + "type": "string" + }, + "official_languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "organizations": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.Organization" + } + }, + "place_of_performance": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "procedure_code": { + "type": "string" + }, + "procurement_type_code": { + "type": "string" + }, + "publication_date": { + "description": "Unix milliseconds", + "type": "integer" + }, + "region_code": { + "type": "string" + }, + "review_organization": { + "$ref": "#/definitions/tender.Organization" + }, + "selection_criteria": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.SelectionCriterion" + } + }, + "source": { + "$ref": "#/definitions/tender.TenderSource" + }, + "source_file_name": { + "type": "string" + }, + "source_file_url": { + "type": "string" + }, + "tender_deadline": { + "description": "Unix milliseconds", + "type": "integer" + }, + "tender_url": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "tender.JobProgress": { + "type": "object", + "properties": { + "current_step": { + "type": "string" + }, + "error_count": { + "type": "integer" + }, + "percentage": { + "type": "number" + }, + "processed_count": { + "type": "integer" + }, + "success_count": { + "type": "integer" + }, + "total_count": { + "type": "integer" + } + } + }, + "tender.JobResults": { + "type": "object", + "properties": { + "processing_errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "total_created": { + "type": "integer" + }, + "total_errors": { + "type": "integer" + }, + "total_processed": { + "type": "integer" + }, + "total_skipped": { + "type": "integer" + }, + "total_updated": { + "type": "integer" + } + } + }, + "tender.Organization": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/tender.Address" + }, + "company_id": { + "type": "string" + }, + "contact_email": { + "type": "string" + }, + "contact_fax": { + "type": "string" + }, + "contact_name": { + "type": "string" + }, + "contact_telephone": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "role": { + "type": "string" + }, + "website_uri": { + "type": "string" + } + } + }, + "tender.ProcessingMetadata": { + "type": "object", + "properties": { + "enrichment_data": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "parsing_errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "processed_at": { + "description": "Unix milliseconds", + "type": "integer" + }, + "processing_version": { + "type": "string" + }, + "scraped_at": { + "description": "Unix milliseconds", + "type": "integer" + }, + "validation_errors": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "tender.ScrapingJobResponse": { + "type": "object", + "properties": { + "completed_at": { + "type": "integer" + }, + "config": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "integer" + }, + "duration": { + "type": "integer" + }, + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "progress": { + "$ref": "#/definitions/tender.JobProgress" + }, + "results": { + "$ref": "#/definitions/tender.JobResults" + }, + "started_at": { + "type": "integer" + }, + "status": { + "$ref": "#/definitions/tender.ScrapingJobStatus" + }, + "type": { + "$ref": "#/definitions/tender.ScrapingJobType" + }, + "updated_at": { + "type": "integer" + } + } + }, + "tender.ScrapingJobStatus": { + "type": "string", + "enum": [ + "pending", + "running", + "completed", + "failed", + "cancelled" + ], + "x-enum-varnames": [ + "ScrapingJobStatusPending", + "ScrapingJobStatusRunning", + "ScrapingJobStatusCompleted", + "ScrapingJobStatusFailed", + "ScrapingJobStatusCancelled" + ] + }, + "tender.ScrapingJobType": { + "type": "string", + "enum": [ + "ted_scraper", + "manual", + "api", + "bulk_import" + ], + "x-enum-varnames": [ + "ScrapingJobTypeTEDScraper", + "ScrapingJobTypeManual", + "ScrapingJobTypeAPI", + "ScrapingJobTypeBulkImport" + ] + }, + "tender.SearchTendersRequest": { + "type": "object", + "properties": { + "criteria": { + "$ref": "#/definitions/tender.TenderSearchCriteria" + }, + "query": { + "type": "string" + } + } + }, + "tender.SelectionCriterion": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "language_id": { + "type": "string" + }, + "type_code": { + "type": "string" + } + } + }, + "tender.StartScrapingJobRequest": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": true + }, + "type": { + "$ref": "#/definitions/tender.ScrapingJobType" + } + } + }, + "tender.TenderResponse": { + "type": "object", + "properties": { + "additional_classifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "buyer_organization": { + "$ref": "#/definitions/tender.Organization" + }, + "city_name": { + "type": "string" + }, + "contract_folder_id": { + "type": "string" + }, + "contract_notice_id": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "document_uri": { + "type": "string" + }, + "duration": { + "type": "string" + }, + "duration_unit": { + "type": "string" + }, + "estimated_value": { + "type": "number" + }, + "formatted_estimated_value": { + "type": "string" + }, + "gazette_id": { + "type": "string" + }, + "has_errors": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "is_expired": { + "type": "boolean" + }, + "issue_date": { + "type": "integer" + }, + "issue_time": { + "type": "integer" + }, + "main_classification": { + "type": "string" + }, + "match_percentage": { + "description": "Match percentage for company (0-100)", + "type": "integer" + }, + "notice_language_code": { + "type": "string" + }, + "notice_publication_id": { + "type": "string" + }, + "notice_sub_type_code": { + "type": "string" + }, + "notice_type_code": { + "type": "string" + }, + "official_languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "organizations": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.Organization" + } + }, + "place_of_performance": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "procedure_code": { + "type": "string" + }, + "processing_metadata": { + "$ref": "#/definitions/tender.ProcessingMetadata" + }, + "procurement_type_code": { + "type": "string" + }, + "publication_date": { + "type": "integer" + }, + "region_code": { + "type": "string" + }, + "review_organization": { + "$ref": "#/definitions/tender.Organization" + }, + "selection_criteria": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.SelectionCriterion" + } + }, + "source": { + "$ref": "#/definitions/tender.TenderSource" + }, + "source_file_name": { + "type": "string" + }, + "source_file_url": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/tender.TenderStatus" + }, + "tender_deadline": { + "type": "integer" + }, + "tender_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "integer" + } + } + }, + "tender.TenderSearchCriteria": { + "type": "object", + "properties": { + "buyer_organization_id": { + "type": "string" + }, + "classifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "country_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "currency": { + "type": "string" + }, + "deadline_from": { + "description": "Unix milliseconds", + "type": "integer" + }, + "deadline_to": { + "description": "Unix milliseconds", + "type": "integer" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "max_estimated_value": { + "type": "number" + }, + "min_estimated_value": { + "type": "number" + }, + "notice_type": { + "type": "string" + }, + "procurement_type": { + "type": "string" + }, + "publication_date_from": { + "description": "Unix milliseconds", + "type": "integer" + }, + "publication_date_to": { + "description": "Unix milliseconds", + "type": "integer" + }, + "query": { + "type": "string" + }, + "region_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.TenderSource" + } + }, + "status": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.TenderStatus" + } + } + } + }, + "tender.TenderSource": { + "type": "string", + "enum": [ + "ted_scraper", + "manual", + "api", + "bulk_import" + ], + "x-enum-varnames": [ + "TenderSourceTEDScraper", + "TenderSourceManual", + "TenderSourceAPI", + "TenderSourceBulkImport" + ] + }, + "tender.TenderStatistics": { + "type": "object", + "properties": { + "active_tenders": { + "type": "integer" + }, + "average_estimated_value": { + "type": "number" + }, + "expired_tenders": { + "type": "integer" + }, + "last_updated": { + "description": "Unix milliseconds", + "type": "integer" + }, + "tenders_by_classification": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "tenders_by_country": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "tenders_by_type": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "total_estimated_value": { + "type": "number" + }, + "total_tenders": { + "type": "integer" + } + } + }, + "tender.TenderStatus": { + "type": "string", + "enum": [ + "active", + "expired", + "cancelled", + "awarded", + "draft" + ], + "x-enum-varnames": [ + "TenderStatusActive", + "TenderStatusExpired", + "TenderStatusCancelled", + "TenderStatusAwarded", + "TenderStatusDraft" + ] + }, + "tender.UpdateTenderRequest": { + "type": "object", + "properties": { + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "estimated_value": { + "type": "number" + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/tender.TenderStatus" + }, + "tender_deadline": { + "description": "Unix milliseconds", + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, "user.AuthResponse": { "type": "object", "properties": { @@ -5818,9 +7268,21 @@ const docTemplate = `{ "description": "Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting", "name": "Admin-Companies" }, + { + "description": "Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination", + "name": "Admin-Tenders" + }, + { + "description": "Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking", + "name": "Admin-Scraping" + }, { "description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access", "name": "Authorization" + }, + { + "description": "Public tender information access for mobile application including active tender listings and detailed tender information", + "name": "Tenders" } ] }` diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index c4d2695..48876c5 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -18,6 +18,765 @@ "host": "localhost:8081", "basePath": "/", "paths": { + "/admin/scraping/jobs": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve scraping jobs with pagination", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Scraping" + ], + "summary": "List scraping jobs", + "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" + } + ], + "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" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Start a new TED XML scraping job", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Scraping" + ], + "summary": "Start scraping job", + "parameters": [ + { + "description": "Scraping job configuration", + "name": "job", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.StartScrapingJobRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.ScrapingJobResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/scraping/jobs/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a specific scraping job by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Scraping" + ], + "summary": "Get scraping job", + "parameters": [ + { + "type": "string", + "description": "Scraping job ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.ScrapingJobResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/scraping/jobs/{id}/cancel": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Cancel a running or pending scraping job", + "tags": [ + "Admin-Scraping" + ], + "summary": "Cancel scraping job", + "parameters": [ + { + "type": "string", + "description": "Scraping job ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "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" + } + } + } + } + }, + "/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" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new tender with the provided information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Create a new tender", + "parameters": [ + { + "description": "Tender information", + "name": "tender", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.CreateTenderRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/tenders/dashboard": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve dashboard data including statistics, recent tenders, and active jobs", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Get dashboard data", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/tenders/search": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Search tenders using full-text search and filtering", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Search tenders", + "parameters": [ + { + "description": "Search criteria", + "name": "search", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.SearchTendersRequest" + } + } + ], + "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/statistics": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve comprehensive statistics about tenders", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Get tender statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderStatistics" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/admin/tenders/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve a specific tender by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Get tender by ID", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update an existing tender with the provided information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Admin-Tenders" + ], + "summary": "Update tender", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated tender information", + "name": "tender", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tender.UpdateTenderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/response.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/tender.TenderResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a tender by its ID", + "tags": [ + "Admin-Tenders" + ], + "summary": "Delete tender", + "parameters": [ + { + "type": "string", + "description": "Tender ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/admin/v1/companies": { "get": { "security": [ @@ -2000,174 +2759,6 @@ } } }, - "/admin/v1/customers/with-companies": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Retrieve a paginated list of customers with their assigned companies and advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Admin-Customers" - ], - "summary": "List customers with companies and filters", - "parameters": [ - { - "type": "string", - "description": "Search term to filter customers by name, email, or company name", - "name": "search", - "in": "query" - }, - { - "enum": [ - "individual", - "company", - "government" - ], - "type": "string", - "description": "Filter by customer type", - "name": "type", - "in": "query" - }, - { - "enum": [ - "active", - "inactive", - "suspended", - "pending" - ], - "type": "string", - "description": "Filter by customer status", - "name": "status", - "in": "query" - }, - { - "type": "string", - "format": "uuid", - "description": "Filter by company ID", - "name": "company_id", - "in": "query" - }, - { - "type": "string", - "description": "Filter by industry", - "name": "industry", - "in": "query" - }, - { - "type": "boolean", - "description": "Filter by verification status", - "name": "is_verified", - "in": "query" - }, - { - "type": "boolean", - "description": "Filter by compliance status", - "name": "is_compliant", - "in": "query" - }, - { - "type": "string", - "description": "Filter by preferred language", - "name": "language", - "in": "query" - }, - { - "type": "string", - "description": "Filter by preferred currency", - "name": "currency", - "in": "query" - }, - { - "maximum": 100, - "minimum": 1, - "type": "integer", - "default": 20, - "description": "Number of customers per page (1-100)", - "name": "limit", - "in": "query" - }, - { - "minimum": 0, - "type": "integer", - "default": 0, - "description": "Number of customers to skip for pagination", - "name": "offset", - "in": "query" - }, - { - "enum": [ - "email", - "company_name", - "created_at", - "updated_at", - "status" - ], - "type": "string", - "default": "created_at", - "description": "Field to sort by", - "name": "sort_by", - "in": "query" - }, - { - "enum": [ - "asc", - "desc" - ], - "type": "string", - "default": "desc", - "description": "Sort order", - "name": "sort_order", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Customers with companies retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/response.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/customer.CustomerListResponse" - } - } - } - ] - } - }, - "400": { - "description": "Bad request - Invalid query parameters", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "422": { - "description": "Validation error - Invalid query parameters", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/response.APIResponse" - } - } - } - } - }, "/admin/v1/customers/{id}": { "get": { "security": [ @@ -4303,6 +4894,137 @@ } } } + }, + "/api/v1/tenders": { + "get": { + "description": "Retrieve active tenders for mobile application users with automatic company-based matching from customer profile", + "produces": [ + "application/json" + ], + "tags": [ + "Tenders" + ], + "summary": "Get public tenders", + "parameters": [ + { + "type": "integer", + "description": "Number of items per page (default: 20, max: 50)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Number of items to skip (default: 0)", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Filter by country code", + "name": "country_code", + "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" + } + ], + "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" + } + } + } + } + }, + "/api/v1/tenders/{id}": { + "get": { + "description": "Retrieve detailed information about a specific tender for mobile application users", + "produces": [ + "application/json" + ], + "tags": [ + "Tenders" + ], + "summary": "Get public tender details", + "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": { + "type": "object", + "additionalProperties": true + } + } + } + ] + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } } }, "definitions": { @@ -5510,6 +6232,734 @@ } } }, + "tender.Address": { + "type": "object", + "properties": { + "city_name": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "country_subentity_code": { + "type": "string" + }, + "department": { + "type": "string" + }, + "postal_zone": { + "type": "string" + }, + "region": { + "type": "string" + }, + "street_name": { + "type": "string" + } + } + }, + "tender.CreateTenderRequest": { + "type": "object", + "properties": { + "additional_classifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "buyer_organization": { + "$ref": "#/definitions/tender.Organization" + }, + "city_name": { + "type": "string" + }, + "contract_folder_id": { + "type": "string" + }, + "contract_notice_id": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "document_uri": { + "type": "string" + }, + "duration": { + "type": "string" + }, + "duration_unit": { + "type": "string" + }, + "estimated_value": { + "type": "number" + }, + "gazette_id": { + "type": "string" + }, + "issue_date": { + "description": "Unix milliseconds", + "type": "integer" + }, + "issue_time": { + "description": "Unix milliseconds", + "type": "integer" + }, + "main_classification": { + "type": "string" + }, + "notice_language_code": { + "type": "string" + }, + "notice_publication_id": { + "type": "string" + }, + "notice_sub_type_code": { + "type": "string" + }, + "notice_type_code": { + "type": "string" + }, + "official_languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "organizations": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.Organization" + } + }, + "place_of_performance": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "procedure_code": { + "type": "string" + }, + "procurement_type_code": { + "type": "string" + }, + "publication_date": { + "description": "Unix milliseconds", + "type": "integer" + }, + "region_code": { + "type": "string" + }, + "review_organization": { + "$ref": "#/definitions/tender.Organization" + }, + "selection_criteria": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.SelectionCriterion" + } + }, + "source": { + "$ref": "#/definitions/tender.TenderSource" + }, + "source_file_name": { + "type": "string" + }, + "source_file_url": { + "type": "string" + }, + "tender_deadline": { + "description": "Unix milliseconds", + "type": "integer" + }, + "tender_url": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "tender.JobProgress": { + "type": "object", + "properties": { + "current_step": { + "type": "string" + }, + "error_count": { + "type": "integer" + }, + "percentage": { + "type": "number" + }, + "processed_count": { + "type": "integer" + }, + "success_count": { + "type": "integer" + }, + "total_count": { + "type": "integer" + } + } + }, + "tender.JobResults": { + "type": "object", + "properties": { + "processing_errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "summary": { + "type": "string" + }, + "total_created": { + "type": "integer" + }, + "total_errors": { + "type": "integer" + }, + "total_processed": { + "type": "integer" + }, + "total_skipped": { + "type": "integer" + }, + "total_updated": { + "type": "integer" + } + } + }, + "tender.Organization": { + "type": "object", + "properties": { + "address": { + "$ref": "#/definitions/tender.Address" + }, + "company_id": { + "type": "string" + }, + "contact_email": { + "type": "string" + }, + "contact_fax": { + "type": "string" + }, + "contact_name": { + "type": "string" + }, + "contact_telephone": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "role": { + "type": "string" + }, + "website_uri": { + "type": "string" + } + } + }, + "tender.ProcessingMetadata": { + "type": "object", + "properties": { + "enrichment_data": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "parsing_errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "processed_at": { + "description": "Unix milliseconds", + "type": "integer" + }, + "processing_version": { + "type": "string" + }, + "scraped_at": { + "description": "Unix milliseconds", + "type": "integer" + }, + "validation_errors": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "tender.ScrapingJobResponse": { + "type": "object", + "properties": { + "completed_at": { + "type": "integer" + }, + "config": { + "type": "object", + "additionalProperties": true + }, + "created_at": { + "type": "integer" + }, + "duration": { + "type": "integer" + }, + "errors": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "progress": { + "$ref": "#/definitions/tender.JobProgress" + }, + "results": { + "$ref": "#/definitions/tender.JobResults" + }, + "started_at": { + "type": "integer" + }, + "status": { + "$ref": "#/definitions/tender.ScrapingJobStatus" + }, + "type": { + "$ref": "#/definitions/tender.ScrapingJobType" + }, + "updated_at": { + "type": "integer" + } + } + }, + "tender.ScrapingJobStatus": { + "type": "string", + "enum": [ + "pending", + "running", + "completed", + "failed", + "cancelled" + ], + "x-enum-varnames": [ + "ScrapingJobStatusPending", + "ScrapingJobStatusRunning", + "ScrapingJobStatusCompleted", + "ScrapingJobStatusFailed", + "ScrapingJobStatusCancelled" + ] + }, + "tender.ScrapingJobType": { + "type": "string", + "enum": [ + "ted_scraper", + "manual", + "api", + "bulk_import" + ], + "x-enum-varnames": [ + "ScrapingJobTypeTEDScraper", + "ScrapingJobTypeManual", + "ScrapingJobTypeAPI", + "ScrapingJobTypeBulkImport" + ] + }, + "tender.SearchTendersRequest": { + "type": "object", + "properties": { + "criteria": { + "$ref": "#/definitions/tender.TenderSearchCriteria" + }, + "query": { + "type": "string" + } + } + }, + "tender.SelectionCriterion": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "language_id": { + "type": "string" + }, + "type_code": { + "type": "string" + } + } + }, + "tender.StartScrapingJobRequest": { + "type": "object", + "properties": { + "config": { + "type": "object", + "additionalProperties": true + }, + "type": { + "$ref": "#/definitions/tender.ScrapingJobType" + } + } + }, + "tender.TenderResponse": { + "type": "object", + "properties": { + "additional_classifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "buyer_organization": { + "$ref": "#/definitions/tender.Organization" + }, + "city_name": { + "type": "string" + }, + "contract_folder_id": { + "type": "string" + }, + "contract_notice_id": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "created_at": { + "type": "integer" + }, + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "document_uri": { + "type": "string" + }, + "duration": { + "type": "string" + }, + "duration_unit": { + "type": "string" + }, + "estimated_value": { + "type": "number" + }, + "formatted_estimated_value": { + "type": "string" + }, + "gazette_id": { + "type": "string" + }, + "has_errors": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "is_expired": { + "type": "boolean" + }, + "issue_date": { + "type": "integer" + }, + "issue_time": { + "type": "integer" + }, + "main_classification": { + "type": "string" + }, + "match_percentage": { + "description": "Match percentage for company (0-100)", + "type": "integer" + }, + "notice_language_code": { + "type": "string" + }, + "notice_publication_id": { + "type": "string" + }, + "notice_sub_type_code": { + "type": "string" + }, + "notice_type_code": { + "type": "string" + }, + "official_languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "organizations": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.Organization" + } + }, + "place_of_performance": { + "type": "string" + }, + "postal_code": { + "type": "string" + }, + "procedure_code": { + "type": "string" + }, + "processing_metadata": { + "$ref": "#/definitions/tender.ProcessingMetadata" + }, + "procurement_type_code": { + "type": "string" + }, + "publication_date": { + "type": "integer" + }, + "region_code": { + "type": "string" + }, + "review_organization": { + "$ref": "#/definitions/tender.Organization" + }, + "selection_criteria": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.SelectionCriterion" + } + }, + "source": { + "$ref": "#/definitions/tender.TenderSource" + }, + "source_file_name": { + "type": "string" + }, + "source_file_url": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/tender.TenderStatus" + }, + "tender_deadline": { + "type": "integer" + }, + "tender_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "integer" + } + } + }, + "tender.TenderSearchCriteria": { + "type": "object", + "properties": { + "buyer_organization_id": { + "type": "string" + }, + "classifications": { + "type": "array", + "items": { + "type": "string" + } + }, + "country_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "currency": { + "type": "string" + }, + "deadline_from": { + "description": "Unix milliseconds", + "type": "integer" + }, + "deadline_to": { + "description": "Unix milliseconds", + "type": "integer" + }, + "languages": { + "type": "array", + "items": { + "type": "string" + } + }, + "max_estimated_value": { + "type": "number" + }, + "min_estimated_value": { + "type": "number" + }, + "notice_type": { + "type": "string" + }, + "procurement_type": { + "type": "string" + }, + "publication_date_from": { + "description": "Unix milliseconds", + "type": "integer" + }, + "publication_date_to": { + "description": "Unix milliseconds", + "type": "integer" + }, + "query": { + "type": "string" + }, + "region_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.TenderSource" + } + }, + "status": { + "type": "array", + "items": { + "$ref": "#/definitions/tender.TenderStatus" + } + } + } + }, + "tender.TenderSource": { + "type": "string", + "enum": [ + "ted_scraper", + "manual", + "api", + "bulk_import" + ], + "x-enum-varnames": [ + "TenderSourceTEDScraper", + "TenderSourceManual", + "TenderSourceAPI", + "TenderSourceBulkImport" + ] + }, + "tender.TenderStatistics": { + "type": "object", + "properties": { + "active_tenders": { + "type": "integer" + }, + "average_estimated_value": { + "type": "number" + }, + "expired_tenders": { + "type": "integer" + }, + "last_updated": { + "description": "Unix milliseconds", + "type": "integer" + }, + "tenders_by_classification": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "tenders_by_country": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "tenders_by_type": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "total_estimated_value": { + "type": "number" + }, + "total_tenders": { + "type": "integer" + } + } + }, + "tender.TenderStatus": { + "type": "string", + "enum": [ + "active", + "expired", + "cancelled", + "awarded", + "draft" + ], + "x-enum-varnames": [ + "TenderStatusActive", + "TenderStatusExpired", + "TenderStatusCancelled", + "TenderStatusAwarded", + "TenderStatusDraft" + ] + }, + "tender.UpdateTenderRequest": { + "type": "object", + "properties": { + "currency": { + "type": "string" + }, + "description": { + "type": "string" + }, + "estimated_value": { + "type": "number" + }, + "id": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/tender.TenderStatus" + }, + "tender_deadline": { + "description": "Unix milliseconds", + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, "user.AuthResponse": { "type": "object", "properties": { @@ -5812,9 +7262,21 @@ "description": "Administrative company management operations for web panel including CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting", "name": "Admin-Companies" }, + { + "description": "Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination", + "name": "Admin-Tenders" + }, + { + "description": "Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking", + "name": "Admin-Scraping" + }, { "description": "Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access", "name": "Authorization" + }, + { + "description": "Public tender information access for mobile application including active tender listings and detailed tender information", + "name": "Tenders" } ] } \ No newline at end of file diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 4000560..285d115 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -788,6 +788,502 @@ definitions: total: type: integer type: object + tender.Address: + properties: + city_name: + type: string + country_code: + type: string + country_subentity_code: + type: string + department: + type: string + postal_zone: + type: string + region: + type: string + street_name: + type: string + type: object + tender.CreateTenderRequest: + properties: + additional_classifications: + items: + type: string + type: array + buyer_organization: + $ref: '#/definitions/tender.Organization' + city_name: + type: string + contract_folder_id: + type: string + contract_notice_id: + type: string + country_code: + type: string + currency: + type: string + description: + type: string + document_uri: + type: string + duration: + type: string + duration_unit: + type: string + estimated_value: + type: number + gazette_id: + type: string + issue_date: + description: Unix milliseconds + type: integer + issue_time: + description: Unix milliseconds + type: integer + main_classification: + type: string + notice_language_code: + type: string + notice_publication_id: + type: string + notice_sub_type_code: + type: string + notice_type_code: + type: string + official_languages: + items: + type: string + type: array + organizations: + items: + $ref: '#/definitions/tender.Organization' + type: array + place_of_performance: + type: string + postal_code: + type: string + procedure_code: + type: string + procurement_type_code: + type: string + publication_date: + description: Unix milliseconds + type: integer + region_code: + type: string + review_organization: + $ref: '#/definitions/tender.Organization' + selection_criteria: + items: + $ref: '#/definitions/tender.SelectionCriterion' + type: array + source: + $ref: '#/definitions/tender.TenderSource' + source_file_name: + type: string + source_file_url: + type: string + tender_deadline: + description: Unix milliseconds + type: integer + tender_url: + type: string + title: + type: string + type: object + tender.JobProgress: + properties: + current_step: + type: string + error_count: + type: integer + percentage: + type: number + processed_count: + type: integer + success_count: + type: integer + total_count: + type: integer + type: object + tender.JobResults: + properties: + processing_errors: + items: + type: string + type: array + summary: + type: string + total_created: + type: integer + total_errors: + type: integer + total_processed: + type: integer + total_skipped: + type: integer + total_updated: + type: integer + type: object + tender.Organization: + properties: + address: + $ref: '#/definitions/tender.Address' + company_id: + type: string + contact_email: + type: string + contact_fax: + type: string + contact_name: + type: string + contact_telephone: + type: string + id: + type: string + name: + type: string + role: + type: string + website_uri: + type: string + type: object + tender.ProcessingMetadata: + properties: + enrichment_data: + additionalProperties: + type: string + type: object + parsing_errors: + items: + type: string + type: array + processed_at: + description: Unix milliseconds + type: integer + processing_version: + type: string + scraped_at: + description: Unix milliseconds + type: integer + validation_errors: + items: + type: string + type: array + type: object + tender.ScrapingJobResponse: + properties: + completed_at: + type: integer + config: + additionalProperties: true + type: object + created_at: + type: integer + duration: + type: integer + errors: + items: + type: string + type: array + id: + type: string + progress: + $ref: '#/definitions/tender.JobProgress' + results: + $ref: '#/definitions/tender.JobResults' + started_at: + type: integer + status: + $ref: '#/definitions/tender.ScrapingJobStatus' + type: + $ref: '#/definitions/tender.ScrapingJobType' + updated_at: + type: integer + type: object + tender.ScrapingJobStatus: + enum: + - pending + - running + - completed + - failed + - cancelled + type: string + x-enum-varnames: + - ScrapingJobStatusPending + - ScrapingJobStatusRunning + - ScrapingJobStatusCompleted + - ScrapingJobStatusFailed + - ScrapingJobStatusCancelled + tender.ScrapingJobType: + enum: + - ted_scraper + - manual + - api + - bulk_import + type: string + x-enum-varnames: + - ScrapingJobTypeTEDScraper + - ScrapingJobTypeManual + - ScrapingJobTypeAPI + - ScrapingJobTypeBulkImport + tender.SearchTendersRequest: + properties: + criteria: + $ref: '#/definitions/tender.TenderSearchCriteria' + query: + type: string + type: object + tender.SelectionCriterion: + properties: + description: + type: string + language_id: + type: string + type_code: + type: string + type: object + tender.StartScrapingJobRequest: + properties: + config: + additionalProperties: true + type: object + type: + $ref: '#/definitions/tender.ScrapingJobType' + type: object + tender.TenderResponse: + properties: + additional_classifications: + items: + type: string + type: array + buyer_organization: + $ref: '#/definitions/tender.Organization' + city_name: + type: string + contract_folder_id: + type: string + contract_notice_id: + type: string + country_code: + type: string + created_at: + type: integer + currency: + type: string + description: + type: string + document_uri: + type: string + duration: + type: string + duration_unit: + type: string + estimated_value: + type: number + formatted_estimated_value: + type: string + gazette_id: + type: string + has_errors: + type: boolean + id: + type: string + is_active: + type: boolean + is_expired: + type: boolean + issue_date: + type: integer + issue_time: + type: integer + main_classification: + type: string + match_percentage: + description: Match percentage for company (0-100) + type: integer + notice_language_code: + type: string + notice_publication_id: + type: string + notice_sub_type_code: + type: string + notice_type_code: + type: string + official_languages: + items: + type: string + type: array + organizations: + items: + $ref: '#/definitions/tender.Organization' + type: array + place_of_performance: + type: string + postal_code: + type: string + procedure_code: + type: string + processing_metadata: + $ref: '#/definitions/tender.ProcessingMetadata' + procurement_type_code: + type: string + publication_date: + type: integer + region_code: + type: string + review_organization: + $ref: '#/definitions/tender.Organization' + selection_criteria: + items: + $ref: '#/definitions/tender.SelectionCriterion' + type: array + source: + $ref: '#/definitions/tender.TenderSource' + source_file_name: + type: string + source_file_url: + type: string + status: + $ref: '#/definitions/tender.TenderStatus' + tender_deadline: + type: integer + tender_url: + type: string + title: + type: string + updated_at: + type: integer + type: object + tender.TenderSearchCriteria: + properties: + buyer_organization_id: + type: string + classifications: + items: + type: string + type: array + country_codes: + items: + type: string + type: array + currency: + type: string + deadline_from: + description: Unix milliseconds + type: integer + deadline_to: + description: Unix milliseconds + type: integer + languages: + items: + type: string + type: array + max_estimated_value: + type: number + min_estimated_value: + type: number + notice_type: + type: string + procurement_type: + type: string + publication_date_from: + description: Unix milliseconds + type: integer + publication_date_to: + description: Unix milliseconds + type: integer + query: + type: string + region_codes: + items: + type: string + type: array + source: + items: + $ref: '#/definitions/tender.TenderSource' + type: array + status: + items: + $ref: '#/definitions/tender.TenderStatus' + type: array + type: object + tender.TenderSource: + enum: + - ted_scraper + - manual + - api + - bulk_import + type: string + x-enum-varnames: + - TenderSourceTEDScraper + - TenderSourceManual + - TenderSourceAPI + - TenderSourceBulkImport + tender.TenderStatistics: + properties: + active_tenders: + type: integer + average_estimated_value: + type: number + expired_tenders: + type: integer + last_updated: + description: Unix milliseconds + type: integer + tenders_by_classification: + additionalProperties: + format: int64 + type: integer + type: object + tenders_by_country: + additionalProperties: + format: int64 + type: integer + type: object + tenders_by_type: + additionalProperties: + format: int64 + type: integer + type: object + total_estimated_value: + type: number + total_tenders: + type: integer + type: object + tender.TenderStatus: + enum: + - active + - expired + - cancelled + - awarded + - draft + type: string + x-enum-varnames: + - TenderStatusActive + - TenderStatusExpired + - TenderStatusCancelled + - TenderStatusAwarded + - TenderStatusDraft + tender.UpdateTenderRequest: + properties: + currency: + type: string + description: + type: string + estimated_value: + type: number + id: + type: string + status: + $ref: '#/definitions/tender.TenderStatus' + tender_deadline: + description: Unix milliseconds + type: integer + title: + type: string + type: object user.AuthResponse: properties: access_token: @@ -1000,6 +1496,466 @@ info: title: Tender Management API version: 2.0.0 paths: + /admin/scraping/jobs: + get: + description: Retrieve scraping jobs with pagination + 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 + 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 scraping jobs + tags: + - Admin-Scraping + post: + consumes: + - application/json + description: Start a new TED XML scraping job + parameters: + - description: Scraping job configuration + in: body + name: job + required: true + schema: + $ref: '#/definitions/tender.StartScrapingJobRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/tender.ScrapingJobResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Start scraping job + tags: + - Admin-Scraping + /admin/scraping/jobs/{id}: + get: + description: Retrieve a specific scraping job by its ID + parameters: + - description: Scraping job 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.ScrapingJobResponse' + 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 scraping job + tags: + - Admin-Scraping + /admin/scraping/jobs/{id}/cancel: + post: + description: Cancel a running or pending scraping job + parameters: + - description: Scraping job ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/response.APIResponse' + "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: Cancel scraping job + tags: + - Admin-Scraping + /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 + post: + consumes: + - application/json + description: Create a new tender with the provided information + parameters: + - description: Tender information + in: body + name: tender + required: true + schema: + $ref: '#/definitions/tender.CreateTenderRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/tender.TenderResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Create a new tender + 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/tenders/dashboard: + get: + description: Retrieve dashboard data including statistics, recent tenders, and + active jobs + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + additionalProperties: true + type: object + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get dashboard data + tags: + - Admin-Tenders + /admin/tenders/search: + post: + consumes: + - application/json + description: Search tenders using full-text search and filtering + parameters: + - description: Search criteria + in: body + name: search + required: true + schema: + $ref: '#/definitions/tender.SearchTendersRequest' + 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: Search tenders + tags: + - Admin-Tenders + /admin/tenders/statistics: + get: + description: Retrieve comprehensive statistics about tenders + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/response.APIResponse' + - properties: + data: + $ref: '#/definitions/tender.TenderStatistics' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + security: + - BearerAuth: [] + summary: Get tender statistics + tags: + - Admin-Tenders /admin/v1/companies: get: consumes: @@ -2709,121 +3665,6 @@ paths: summary: Get customers by type tags: - Admin-Customers - /admin/v1/customers/with-companies: - get: - consumes: - - application/json - description: Retrieve a paginated list of customers with their assigned companies - and advanced filtering options including search, type, status, company, industry, - verification status, and compliance status. Supports sorting and pagination. - parameters: - - description: Search term to filter customers by name, email, or company name - in: query - name: search - type: string - - description: Filter by customer type - enum: - - individual - - company - - government - in: query - name: type - type: string - - description: Filter by customer status - enum: - - active - - inactive - - suspended - - pending - in: query - name: status - type: string - - description: Filter by company ID - format: uuid - in: query - name: company_id - type: string - - description: Filter by industry - in: query - name: industry - type: string - - description: Filter by verification status - in: query - name: is_verified - type: boolean - - description: Filter by compliance status - in: query - name: is_compliant - type: boolean - - description: Filter by preferred language - in: query - name: language - type: string - - description: Filter by preferred currency - in: query - name: currency - type: string - - default: 20 - description: Number of customers per page (1-100) - in: query - maximum: 100 - minimum: 1 - name: limit - type: integer - - default: 0 - description: Number of customers to skip for pagination - in: query - minimum: 0 - name: offset - type: integer - - default: created_at - description: Field to sort by - enum: - - email - - company_name - - created_at - - updated_at - - status - in: query - name: sort_by - type: string - - default: desc - description: Sort order - enum: - - asc - - desc - in: query - name: sort_order - type: string - produces: - - application/json - responses: - "200": - description: Customers with companies retrieved successfully - schema: - allOf: - - $ref: '#/definitions/response.APIResponse' - - properties: - data: - $ref: '#/definitions/customer.CustomerListResponse' - type: object - "400": - description: Bad request - Invalid query parameters - schema: - $ref: '#/definitions/response.APIResponse' - "422": - description: Validation error - Invalid query parameters - schema: - $ref: '#/definitions/response.APIResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/response.APIResponse' - security: - - BearerAuth: [] - summary: List customers with companies and filters - tags: - - Admin-Customers /admin/v1/health: get: consumes: @@ -3762,6 +4603,89 @@ paths: summary: Refresh customer access token tags: - Authorization + /api/v1/tenders: + get: + description: Retrieve active tenders for mobile application users with automatic + company-based matching from customer profile + parameters: + - description: 'Number of items per page (default: 20, max: 50)' + in: query + name: limit + type: integer + - description: 'Number of items to skip (default: 0)' + in: query + name: offset + type: integer + - description: Filter by country code + in: query + name: country_code + type: string + - description: Minimum estimated value + in: query + name: min_estimated_value + type: number + - description: Maximum estimated value + in: query + name: max_estimated_value + type: number + 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' + summary: Get public tenders + tags: + - Tenders + /api/v1/tenders/{id}: + get: + description: Retrieve detailed information about a specific tender for mobile + application users + 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: + additionalProperties: true + type: object + type: object + "404": + description: Not Found + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get public tender details + tags: + - Tenders securityDefinitions: BearerAuth: description: 'Type "Bearer" followed by a space and JWT token. Example: "Bearer @@ -3788,6 +4712,15 @@ tags: CRUD operations, tag management, verification, status updates, comprehensive search and filtering, and statistical reporting name: Admin-Companies +- description: Administrative tender management operations for web panel including + CRUD operations, search, statistics, and comprehensive filtering with pagination + name: Admin-Tenders +- description: Administrative scraping job management operations for TED XML data + collection including job control, monitoring, and status tracking + name: Admin-Scraping - description: Customer authentication and authorization operations for mobile application including login, logout, token refresh, and profile access name: Authorization +- description: Public tender information access for mobile application including active + tender listings and detailed tender information + name: Tenders diff --git a/cmd/web/main.go b/cmd/web/main.go index 6236bd1..49975bc 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -110,7 +110,7 @@ func main() { userService := user.NewUserService(userRepository, logger, userAuthService, userValidator) companyService := company.NewCompanyService(companyRepository, logger) customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService) - tenderService := tender.NewTenderService(tenderRepository, logger) + tenderService := tender.NewTenderService(tenderRepository, companyService, logger) logger.Info("Services initialized successfully", map[string]interface{}{ "services": []string{"customer", "user", "company", "tender"}, diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index 7b1693f..b22a7f6 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -68,7 +68,6 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler } // Admin Customers Routes - customersGP := adminV1.Group("/customers") { customersGP.Use(userHandler.AuthMiddleware()) @@ -130,6 +129,7 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende // Public Tenders Routes tendersGP := v1.Group("/tenders") + tendersGP.Use(customerHandler.AuthMiddleware()) { tendersGP.GET("", tenderHandler.GetPublicTenders) tendersGP.GET("/:id", tenderHandler.GetPublicTenderDetails) diff --git a/internal/customer/handler.go b/internal/customer/handler.go index f6def24..23a15df 100644 --- a/internal/customer/handler.go +++ b/internal/customer/handler.go @@ -207,7 +207,7 @@ func (h *Handler) ListCustomers(c echo.Context) error { return response.ValidationError(c, "Invalid request data", err.Error()) } - result, err := h.service.ListCustomersWithCompanies(c.Request().Context(), form) + result, err := h.service.ListCustomers(c.Request().Context(), form) if err != nil { return response.InternalServerError(c, "Failed to list customers") } @@ -215,45 +215,6 @@ func (h *Handler) ListCustomers(c echo.Context) error { return response.Success(c, result, "Customers retrieved successfully") } -// ListCustomersWithCompanies lists customers with companies and filters -// @Summary List customers with companies and filters -// @Description Retrieve a paginated list of customers with their assigned companies and advanced filtering options including search, type, status, company, industry, verification status, and compliance status. Supports sorting and pagination. -// @Tags Admin-Customers -// @Accept json -// @Produce json -// @Param search query string false "Search term to filter customers by name, email, or company name" -// @Param type query string false "Filter by customer type" Enums(individual, company, government) -// @Param status query string false "Filter by customer status" Enums(active, inactive, suspended, pending) -// @Param company_id query string false "Filter by company ID" format(uuid) -// @Param industry query string false "Filter by industry" -// @Param is_verified query boolean false "Filter by verification status" -// @Param is_compliant query boolean false "Filter by compliance status" -// @Param language query string false "Filter by preferred language" -// @Param currency query string false "Filter by preferred currency" -// @Param limit query integer false "Number of customers per page (1-100)" minimum(1) maximum(100) default(20) -// @Param offset query integer false "Number of customers to skip for pagination" minimum(0) default(0) -// @Param sort_by query string false "Field to sort by" Enums(email, company_name, created_at, updated_at, status) default(created_at) -// @Param sort_order query string false "Sort order" Enums(asc, desc) default(desc) -// @Success 200 {object} response.APIResponse{data=CustomerListResponse} "Customers with companies retrieved successfully" -// @Failure 400 {object} response.APIResponse "Bad request - Invalid query parameters" -// @Failure 422 {object} response.APIResponse "Validation error - Invalid query parameters" -// @Failure 500 {object} response.APIResponse "Internal server error" -// @Security BearerAuth -// @Router /admin/v1/customers/with-companies [get] -func (h *Handler) ListCustomersWithCompanies(c echo.Context) error { - form, err := response.Parse[ListCustomersForm](c) - if err != nil { - return response.ValidationError(c, "Invalid request data", err.Error()) - } - - result, err := h.service.ListCustomersWithCompanies(c.Request().Context(), form) - if err != nil { - return response.InternalServerError(c, "Failed to list customers with companies") - } - - return response.Success(c, result, "Customers with companies retrieved successfully") -} - // UpdateCustomerStatus updates customer status (Web Panel) // @Summary Update customer status // @Description Update customer account status (active, inactive, suspended, pending) diff --git a/internal/customer/repository.go b/internal/customer/repository.go index de4e1f0..99409e4 100644 --- a/internal/customer/repository.go +++ b/internal/customer/repository.go @@ -20,13 +20,11 @@ type Repository interface { GetByRegistrationNumber(ctx context.Context, registrationNumber string) (*Customer, error) GetByTaxID(ctx context.Context, taxID string) (*Customer, error) GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, error) - GetByCompanyIDs(ctx context.Context, companyIDs []string, limit, offset int) ([]*Customer, error) Update(ctx context.Context, customer *Customer) error Delete(ctx context.Context, id string) error List(ctx context.Context, limit, offset int) ([]*Customer, error) Search(ctx context.Context, search string, customerType *string, status *string, companyID *string, industry *string, isVerified *bool, isCompliant *bool, language *string, currency *string, limit, offset int, sortBy, sortOrder string) ([]*Customer, error) CountByCompanyID(ctx context.Context, companyID string) (int64, error) - CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error) CountByType(ctx context.Context, customerType CustomerType) (int64, error) CountByStatus(ctx context.Context, status CustomerStatus) (int64, error) UpdateStatus(ctx context.Context, id string, status CustomerStatus) error @@ -247,46 +245,6 @@ func (r *customerRepository) GetByCompanyID(ctx context.Context, companyID strin return customers, nil } -// GetByCompanyIDs retrieves customers by multiple company IDs with pagination -func (r *customerRepository) GetByCompanyIDs(ctx context.Context, companyIDs []string, limit, offset int) ([]*Customer, error) { - if len(companyIDs) == 0 { - return nil, errors.New("no company IDs provided") - } - - // Build pagination - pagination := mongopkg.NewPaginationBuilder(). - Limit(limit). - Skip(offset). - SortDesc("created_at"). - Build() - - // Filter by company IDs and active status - filter := bson.M{ - "company_ids": bson.M{"$in": companyIDs}, - "status": bson.M{"$ne": CustomerStatusInactive}, - } - - // Use ORM to find customers - result, err := r.ormRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to get customers by multiple company IDs", map[string]interface{}{ - "error": err.Error(), - "company_ids": companyIDs, - "limit": limit, - "offset": offset, - }) - return nil, err - } - - // Convert []Customer to []*Customer - customers := make([]*Customer, len(result.Items)) - for i := range result.Items { - customers[i] = &result.Items[i] - } - - return customers, nil -} - // Update updates a customer func (r *customerRepository) Update(ctx context.Context, customer *Customer) error { // Set updated timestamp using Unix timestamp @@ -466,29 +424,6 @@ func (r *customerRepository) CountByCompanyID(ctx context.Context, companyID str return count, nil } -// CountByCompanyIDs counts customers by multiple company IDs -func (r *customerRepository) CountByCompanyIDs(ctx context.Context, companyIDs []string) (int64, error) { - if len(companyIDs) == 0 { - return 0, errors.New("no company IDs provided") - } - - filter := bson.M{ - "company_ids": bson.M{"$in": companyIDs}, - "status": bson.M{"$ne": CustomerStatusInactive}, - } - - count, err := r.ormRepo.Count(ctx, filter) - if err != nil { - r.logger.Error("Failed to count customers by multiple company IDs", map[string]interface{}{ - "error": err.Error(), - "company_ids": companyIDs, - }) - return 0, err - } - - return count, nil -} - // CountByType counts customers by type func (r *customerRepository) CountByType(ctx context.Context, customerType CustomerType) (int64, error) { filter := bson.M{ diff --git a/internal/customer/service.go b/internal/customer/service.go index ebf1710..43e9fee 100644 --- a/internal/customer/service.go +++ b/internal/customer/service.go @@ -25,7 +25,6 @@ type Service interface { // Customer listing and search ListCustomers(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) - ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) GetCustomersByType(ctx context.Context, customerType CustomerType, limit, offset int) ([]*Customer, int64, error) GetCustomersByStatus(ctx context.Context, status CustomerStatus, limit, offset int) ([]*Customer, int64, error) @@ -471,79 +470,6 @@ func (s *customerService) ListCustomers(ctx context.Context, form *ListCustomers }, nil } -// ListCustomersWithCompanies lists customers with search and filters, including companies -func (s *customerService) ListCustomersWithCompanies(ctx context.Context, form *ListCustomersForm) (*CustomerListResponse, error) { - // Set defaults - limit := 20 - if form.Limit != nil { - limit = *form.Limit - } - - offset := 0 - if form.Offset != nil { - offset = *form.Offset - } - - search := "" - if form.Search != nil { - search = *form.Search - } - - sortBy := "created_at" - if form.SortBy != nil { - sortBy = *form.SortBy - } - - sortOrder := "desc" - if form.SortOrder != nil { - sortOrder = *form.SortOrder - } - - // Parse company ID if provided - var companyID *string - if form.CompanyID != nil { - companyID = form.CompanyID - } - - // Get customers - customers, err := s.repository.Search(ctx, search, form.Type, form.Status, companyID, form.Industry, form.IsVerified, form.IsCompliant, form.Language, form.Currency, limit, offset, sortBy, sortOrder) - if err != nil { - s.logger.Error("Failed to list customers with companies", map[string]interface{}{ - "error": err.Error(), - }) - return nil, errors.New("failed to list customers with companies") - } - - // Convert to responses - var customerResponses []*CustomerResponse - for _, customer := range customers { - // Load companies for the customer - companies, err := s.loadCompaniesForCustomer(ctx, customer) - if err != nil { - s.logger.Error("Failed to load companies for customer in list", map[string]interface{}{ - "error": err.Error(), - "customer_id": customer.ID, - }) - // Continue without companies if loading fails - companies = []*CompanySummary{} - } - customerResponse := customer.ToResponseWithCompanies(companies) - customerResponses = append(customerResponses, customerResponse) - } - - // TODO: Implement proper count for pagination metadata - total := int64(len(customers)) // This should be a separate count query - totalPages := (total + int64(limit) - 1) / int64(limit) - - return &CustomerListResponse{ - Customers: customerResponses, - Total: total, - Limit: limit, - Offset: offset, - TotalPages: int(totalPages), - }, nil -} - // GetCustomersByCompanyID retrieves customers by company ID with pagination func (s *customerService) GetCustomersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*Customer, int64, error) { customers, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset) diff --git a/internal/tender/entity.go b/internal/tender/entity.go index 132218e..14f1d8f 100644 --- a/internal/tender/entity.go +++ b/internal/tender/entity.go @@ -416,3 +416,62 @@ func (s *ScrapingState) MarkAsCompleted(year, number int) { s.NextRunAt = time.Now().Add(4 * time.Hour).Unix() // Schedule next run in 4 hours s.UpdatedAt = time.Now().Unix() } + +// CalculateMatchPercentage calculates the match percentage between tender classifications and company CPV codes +func (t *Tender) CalculateMatchPercentage(companyCPVCodes []string) int { + if len(companyCPVCodes) == 0 { + return 0 + } + + mainMatch := false + additionalMatch := false + + // Check if main classification matches any company CPV codes + if t.MainClassification != "" { + for _, companyCode := range companyCPVCodes { + if companyCode == t.MainClassification { + mainMatch = true + break + } + } + } + + // Check if any additional classification matches any company CPV codes + if len(t.AdditionalClassifications) > 0 { + for _, additionalCode := range t.AdditionalClassifications { + for _, companyCode := range companyCPVCodes { + if companyCode == additionalCode { + additionalMatch = true + break + } + } + if additionalMatch { + break + } + } + } + + // Calculate percentage based on matches + if mainMatch && additionalMatch { + return 100 + } else if mainMatch || additionalMatch { + return 50 + } + return 0 +} + +// GetDaysUntilDeadline returns the number of days until the tender deadline +func (t *Tender) GetDaysUntilDeadline() int { + if t.TenderDeadline == 0 { + return 0 + } + + now := time.Now().UnixMilli() + if t.TenderDeadline <= now { + return 0 // Already expired + } + + diffMillis := t.TenderDeadline - now + diffDays := diffMillis / (24 * 60 * 60 * 1000) + return int(diffDays) +} diff --git a/internal/tender/form.go b/internal/tender/form.go index ebf2c0c..0398204 100644 --- a/internal/tender/form.go +++ b/internal/tender/form.go @@ -53,17 +53,18 @@ type UpdateTenderRequest struct { // ListTendersRequest represents a request to list tenders with pagination and filtering type ListTendersRequest struct { - Criteria TenderSearchCriteria `json:"criteria"` - Limit int `json:"limit" valid:"range(1|100)~Limit must be between 1 and 100"` - Offset int `json:"offset" valid:"range(0|999999)~Offset must be non-negative"` + Criteria TenderSearchCriteria `json:"criteria"` + Limit int `json:"limit" valid:"range(1|100)~Limit must be between 1 and 100"` + Offset int `json:"offset" valid:"range(0|999999)~Offset must be non-negative"` + CompanyID *string `json:"company_id,omitempty"` // Optional company ID for match percentage calculation } // ListTendersResponse represents the response for listing tenders type ListTendersResponse struct { - Tenders []Tender `json:"tenders"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` + Tenders []TenderResponse `json:"tenders"` + Total int64 `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` } // SearchTendersRequest represents a request to search tenders @@ -160,6 +161,7 @@ type TenderResponse struct { IsActive bool `json:"is_active"` IsExpired bool `json:"is_expired"` HasErrors bool `json:"has_errors"` + MatchPercentage *int `json:"match_percentage,omitempty"` // Match percentage for company (0-100) CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -210,7 +212,12 @@ type ErrorResponse struct { // ToTenderResponse converts a Tender entity to TenderResponse func (t *Tender) ToTenderResponse() *TenderResponse { - return &TenderResponse{ + return t.ToTenderResponseWithMatch(nil) +} + +// ToTenderResponseWithMatch converts a Tender entity to TenderResponse with optional match percentage +func (t *Tender) ToTenderResponseWithMatch(companyCPVCodes []string) *TenderResponse { + response := &TenderResponse{ ID: t.ID.Hex(), ContractNoticeID: t.ContractNoticeID, NoticePublicationID: t.NoticePublicationID, @@ -257,6 +264,14 @@ func (t *Tender) ToTenderResponse() *TenderResponse { CreatedAt: t.CreatedAt, UpdatedAt: t.UpdatedAt, } + + // Calculate match percentage if company CPV codes are provided + if companyCPVCodes != nil { + matchPercentage := t.CalculateMatchPercentage(companyCPVCodes) + response.MatchPercentage = &matchPercentage + } + + return response } // ToScrapingJobResponse converts a ScrapingJob entity to ScrapingJobResponse diff --git a/internal/tender/handler.go b/internal/tender/handler.go index 3c7d279..62d6253 100644 --- a/internal/tender/handler.go +++ b/internal/tender/handler.go @@ -154,11 +154,12 @@ func (h *TenderHandler) DeleteTender(c echo.Context) error { // ListTenders retrieves tenders with pagination and filtering // @Summary List tenders -// @Description Retrieve tenders with pagination, filtering, and search capabilities +// @Description Retrieve tenders with pagination, filtering, and search capabilities with optional company-based matching // @Tags Admin-Tenders // @Produce json // @Param limit query int false "Number of items per page (default: 20, max: 100)" // @Param offset query int false "Number of items to skip (default: 0)" +// @Param company_id query string false "Company ID for match percentage calculation and sorting" // @Param query query string false "Search query for title and description" // @Param notice_type query string false "Filter by notice type code" // @Param procurement_type query string false "Filter by procurement type code" @@ -231,10 +232,17 @@ func (h *TenderHandler) ListTenders(c echo.Context) error { } } + // 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, + Criteria: criteria, + Limit: limit, + Offset: offset, + CompanyID: companyID, } result, err := h.service.ListTenders(c.Request().Context(), req) @@ -242,14 +250,8 @@ func (h *TenderHandler) ListTenders(c echo.Context) error { return response.InternalServerError(c, "Failed to retrieve tenders") } - // Convert tenders to response format - tenderResponses := make([]TenderResponse, len(result.Tenders)) - for i, tender := range result.Tenders { - tenderResponses[i] = *tender.ToTenderResponse() - } - responseData := map[string]interface{}{ - "tenders": tenderResponses, + "tenders": result.Tenders, "total": result.Total, "limit": result.Limit, "offset": result.Offset, @@ -493,7 +495,7 @@ func (h *TenderHandler) CancelScrapingJob(c echo.Context) error { // GetPublicTenders retrieves public tenders for mobile app // @Summary Get public tenders -// @Description Retrieve active tenders for mobile application users +// @Description Retrieve active tenders for mobile application users with automatic company-based matching from customer profile // @Tags Tenders // @Produce json // @Param limit query int false "Number of items per page (default: 20, max: 50)" @@ -504,7 +506,7 @@ func (h *TenderHandler) CancelScrapingJob(c echo.Context) error { // @Success 200 {object} response.APIResponse{data=map[string]interface{}} // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /tenders [get] +// @Router /api/v1/tenders [get] func (h *TenderHandler) GetPublicTenders(c echo.Context) error { // Parse pagination parameters (more restrictive for public API) limit := 20 @@ -543,10 +545,17 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { } } + // Get company ID from customer context for automatic matching + var companyID *string + if customerCompanyID, ok := c.Get("company_id").(string); ok && customerCompanyID != "" { + companyID = &customerCompanyID + } + req := ListTendersRequest{ - Criteria: criteria, - Limit: limit, - Offset: offset, + Criteria: criteria, + Limit: limit, + Offset: offset, + CompanyID: companyID, } result, err := h.service.ListTenders(c.Request().Context(), req) @@ -557,7 +566,7 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { // Convert tenders to public response format (limited fields) publicTenders := make([]map[string]interface{}, len(result.Tenders)) for i, tender := range result.Tenders { - publicTenders[i] = map[string]interface{}{ + tenderData := map[string]interface{}{ "id": tender.ID, "title": tender.Title, "description": tender.Description, @@ -565,15 +574,22 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { "main_classification": tender.MainClassification, "estimated_value": tender.EstimatedValue, "currency": tender.Currency, - "formatted_estimated_value": tender.GetFormattedEstimatedValue(), + "formatted_estimated_value": tender.FormattedEstimatedValue, "tender_deadline": tender.TenderDeadline, "country_code": tender.CountryCode, "city_name": tender.CityName, "tender_url": tender.TenderURL, "publication_date": tender.PublicationDate, "buyer_organization": tender.BuyerOrganization, - "is_expired": tender.IsExpired(), + "is_expired": tender.IsExpired, } + + // Add match percentage if available + if tender.MatchPercentage != nil { + tenderData["match_percentage"] = *tender.MatchPercentage + } + + publicTenders[i] = tenderData } responseData := map[string]interface{}{ @@ -595,7 +611,7 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { // @Success 200 {object} response.APIResponse{data=map[string]interface{}} // @Failure 404 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /tenders/{id} [get] +// @Router /api/v1/tenders/{id} [get] func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error { id := c.Param("id") if id == "" { diff --git a/internal/tender/repository.go b/internal/tender/repository.go index 67deff9..06e0421 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -86,6 +86,13 @@ func NewTenderRepository(mongoManager *mongopkg.ConnectionManager, logger logger {Key: "notice_type_code", Value: 1}, {Key: "status", Value: 1}, }), + // Index for CPV matching queries + *mongopkg.NewIndex("cpv_matching_idx", bson.D{ + {Key: "status", Value: 1}, + {Key: "main_classification", Value: 1}, + {Key: "additional_classifications", Value: 1}, + {Key: "publication_date", Value: -1}, + }), // Text index for search *mongopkg.CreateTextIndex("search_idx", "title", "description"), } diff --git a/internal/tender/service.go b/internal/tender/service.go index 5986b37..4b9c082 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -3,7 +3,9 @@ package tender import ( "context" "fmt" + "sort" "time" + "tm/internal/company" "tm/pkg/logger" "go.mongodb.org/mongo-driver/bson/primitive" @@ -36,15 +38,17 @@ type TenderService interface { // tenderService implements TenderService interface type tenderService struct { - repository TenderRepository - logger logger.Logger + repository TenderRepository + companyService company.Service + logger logger.Logger } // NewTenderService creates a new tender service -func NewTenderService(repository TenderRepository, logger logger.Logger) TenderService { +func NewTenderService(repository TenderRepository, companyService company.Service, logger logger.Logger) TenderService { return &tenderService{ - repository: repository, - logger: logger, + repository: repository, + companyService: companyService, + logger: logger, } } @@ -222,11 +226,19 @@ func (s *tenderService) DeleteTender(ctx context.Context, id string) error { return nil } +// TenderWithMatch represents a tender with calculated match percentage for sorting +type TenderWithMatch struct { + Tender *Tender + MatchPercentage int + DaysUntilDeadline int +} + // ListTenders retrieves tenders with pagination and filtering func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) (*ListTendersResponse, error) { s.logger.Info("Listing tenders", map[string]interface{}{ - "limit": req.Limit, - "offset": req.Offset, + "limit": req.Limit, + "offset": req.Offset, + "company_id": req.CompanyID, }) if req.Limit <= 0 { @@ -244,8 +256,95 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) return nil, fmt.Errorf("failed to list tenders: %w", err) } + // If no company ID provided, return tenders without match percentage + if req.CompanyID == nil || *req.CompanyID == "" { + tenderResponses := make([]TenderResponse, 0, len(tenders)) + for _, tender := range tenders { + tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) + } + + return &ListTendersResponse{ + Tenders: tenderResponses, + Total: total, + Limit: req.Limit, + Offset: req.Offset, + }, nil + } + + // Get company CPV codes for match calculation + comp, err := s.companyService.GetCompanyByID(ctx, *req.CompanyID) + if err != nil { + s.logger.Error("Failed to get company for match calculation", map[string]interface{}{ + "company_id": *req.CompanyID, + "error": err.Error(), + }) + // Continue without match percentage if company not found + tenderResponses := make([]TenderResponse, 0, len(tenders)) + for _, tender := range tenders { + tenderResponses = append(tenderResponses, *tender.ToTenderResponse()) + } + + return &ListTendersResponse{ + Tenders: tenderResponses, + Total: total, + Limit: req.Limit, + Offset: req.Offset, + }, nil + } + + var companyCPVCodes []string + if comp.Tags != nil && comp.Tags.CPVCodes != nil { + companyCPVCodes = comp.Tags.CPVCodes + } + + // Calculate match percentages and prepare for sorting + tenderMatches := make([]TenderWithMatch, 0, len(tenders)) + for _, tender := range tenders { + tenderMatch := TenderWithMatch{ + Tender: &tender, + MatchPercentage: tender.CalculateMatchPercentage(companyCPVCodes), + DaysUntilDeadline: tender.GetDaysUntilDeadline(), + } + tenderMatches = append(tenderMatches, tenderMatch) + } + + // Sort by match percentage (descending) and then by deadline preference + sort.Slice(tenderMatches, func(i, j int) bool { + matchI := tenderMatches[i].MatchPercentage + matchJ := tenderMatches[j].MatchPercentage + + // First, sort by match percentage (descending) + if matchI != matchJ { + return matchI > matchJ + } + + // For equal match percentages, prioritize tenders with >30 days deadline + daysI := tenderMatches[i].DaysUntilDeadline + daysJ := tenderMatches[j].DaysUntilDeadline + + // Both have >30 days or both have <=30 days, sort by deadline descending (more time is better) + if (daysI > 30 && daysJ > 30) || (daysI <= 30 && daysJ <= 30) { + return daysI > daysJ + } + + // One has >30 days, one has <=30 days - prioritize the one with >30 days + return daysI > 30 + }) + + // Convert to TenderResponse objects with match percentages + tenderResponses := make([]TenderResponse, 0, len(tenderMatches)) + for _, tenderMatch := range tenderMatches { + tenderResponses = append(tenderResponses, *tenderMatch.Tender.ToTenderResponseWithMatch(companyCPVCodes)) + } + + s.logger.Info("Tenders sorted by match percentage", map[string]interface{}{ + "company_id": *req.CompanyID, + "company_cpv_codes": len(companyCPVCodes), + "total_tenders": len(tenderResponses), + }) + return &ListTendersResponse{ - Tenders: tenders, + Tenders: tenderResponses, Total: total, Limit: req.Limit, Offset: req.Offset,