diff --git a/cmd/web/docs/docs.go b/cmd/web/docs/docs.go index 7aaae4c..00c554a 100644 --- a/cmd/web/docs/docs.go +++ b/cmd/web/docs/docs.go @@ -24,238 +24,6 @@ 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": [ @@ -390,224 +158,6 @@ const docTemplate = `{ } } } - }, - "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}": { @@ -7163,432 +6713,29 @@ const docTemplate = `{ } } }, - "tender.Address": { + "tender.OrganizationResponse": { "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" + "$ref": "#/definitions/tender.OrganizationResponse" }, "country_code": { "type": "string" }, - "created_at": { - "type": "integer" - }, "currency": { "type": "string" }, "description": { "type": "string" }, - "document_uri": { - "type": "string" - }, "duration": { "type": "string" }, @@ -7598,252 +6745,29 @@ const docTemplate = `{ "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" } } }, @@ -8192,10 +7116,6 @@ const docTemplate = `{ "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": "Administrative feedback management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination", "name": "Admin-Feedback" diff --git a/cmd/web/docs/swagger.json b/cmd/web/docs/swagger.json index a62cba0..3befcfd 100644 --- a/cmd/web/docs/swagger.json +++ b/cmd/web/docs/swagger.json @@ -18,238 +18,6 @@ "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": [ @@ -384,224 +152,6 @@ } } } - }, - "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}": { @@ -7157,432 +6707,29 @@ } } }, - "tender.Address": { + "tender.OrganizationResponse": { "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" + "$ref": "#/definitions/tender.OrganizationResponse" }, "country_code": { "type": "string" }, - "created_at": { - "type": "integer" - }, "currency": { "type": "string" }, "description": { "type": "string" }, - "document_uri": { - "type": "string" - }, "duration": { "type": "string" }, @@ -7592,252 +6739,29 @@ "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" } } }, @@ -8186,10 +7110,6 @@ "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": "Administrative feedback management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination", "name": "Admin-Feedback" diff --git a/cmd/web/docs/swagger.yaml b/cmd/web/docs/swagger.yaml index 77b5a89..baba529 100644 --- a/cmd/web/docs/swagger.yaml +++ b/cmd/web/docs/swagger.yaml @@ -836,469 +836,43 @@ definitions: total: type: integer type: object - tender.Address: + tender.OrganizationResponse: 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 + $ref: '#/definitions/tender.OrganizationResponse' 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: @@ -1540,145 +1114,6 @@ 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 @@ -1767,46 +1202,6 @@ paths: 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 @@ -1912,94 +1307,6 @@ paths: 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: @@ -5251,9 +4558,6 @@ tags: - 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: Administrative feedback management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination name: Admin-Feedback diff --git a/cmd/web/main.go b/cmd/web/main.go index dd5a1eb..2250fa1 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -38,9 +38,6 @@ package main // @tag.name Admin-Tenders // @tag.description Administrative tender management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination -// @tag.name Admin-Scraping -// @tag.description Administrative scraping job management operations for TED XML data collection including job control, monitoring, and status tracking - // @tag.name Admin-Feedback // @tag.description Administrative feedback management operations for web panel including CRUD operations, search, statistics, and comprehensive filtering with pagination @@ -108,7 +105,7 @@ func main() { feedbackRepo := feedback.NewFeedbackRepository(mongoManager, logger) logger.Info("Repositories initialized successfully", map[string]interface{}{ - "repositories": []string{"customer", "user", "company", "tender"}, + "repositories": []string{"customer", "user", "company", "tender", "feedback"}, }) // Initialize validation service @@ -122,7 +119,7 @@ func main() { feedbackService := feedback.NewFeedbackService(feedbackRepo, logger) logger.Info("Services initialized successfully", map[string]interface{}{ - "services": []string{"customer", "user", "company", "tender"}, + "services": []string{"customer", "user", "company", "tender", "feedback"}, }) // Initialize handlers with services @@ -133,7 +130,7 @@ func main() { feedbackHandler := feedback.NewFeedbackHandler(feedbackService, userHandler, customerHandler, logger) logger.Info("Handlers initialized successfully", map[string]interface{}{ - "handlers": []string{"customer", "user", "company", "tender"}, + "handlers": []string{"customer", "user", "company", "tender", "feedback"}, }) // Initialize HTTP server diff --git a/cmd/web/router/routes.go b/cmd/web/router/routes.go index a5e4bc0..8a7ebcf 100644 --- a/cmd/web/router/routes.go +++ b/cmd/web/router/routes.go @@ -93,24 +93,10 @@ func RegisterAdminRoutes(e *echo.Echo, userHandler *user.Handler, companyHandler tendersGP := adminV1.Group("/tenders") { tendersGP.Use(userHandler.AuthMiddleware()) - tendersGP.POST("", tenderHandler.CreateTender) tendersGP.GET("", tenderHandler.ListTenders) tendersGP.GET("/:id", tenderHandler.GetTender) tendersGP.PUT("/:id", tenderHandler.UpdateTender) tendersGP.DELETE("/:id", tenderHandler.DeleteTender) - tendersGP.POST("/search", tenderHandler.SearchTenders) - tendersGP.GET("/statistics", tenderHandler.GetTenderStatistics) - tendersGP.GET("/dashboard", tenderHandler.GetDashboard) - } - - // Admin Scraping Routes - scrapingGP := adminV1.Group("/scraping") - { - scrapingGP.Use(userHandler.AuthMiddleware()) - scrapingGP.POST("/jobs", tenderHandler.StartScrapingJob) - scrapingGP.GET("/jobs", tenderHandler.ListScrapingJobs) - scrapingGP.GET("/jobs/:id", tenderHandler.GetScrapingJob) - scrapingGP.POST("/jobs/:id/cancel", tenderHandler.CancelScrapingJob) } // Admin Feedback Routes diff --git a/internal/scraping/scraping.go b/internal/scraping/scraping.go new file mode 100644 index 0000000..0332651 --- /dev/null +++ b/internal/scraping/scraping.go @@ -0,0 +1,153 @@ +package scraping + +import "time" + +// ScrapingJobType represents the type of scraping job +type ScrapingJobType string + +const ( + ScrapingJobTypeTEDScraper ScrapingJobType = "ted_scraper" + ScrapingJobTypeManual ScrapingJobType = "manual" + ScrapingJobTypeAPI ScrapingJobType = "api" + ScrapingJobTypeBulkImport ScrapingJobType = "bulk_import" +) + +// ScrapingJobStatus represents the status of a scraping job +type ScrapingJobStatus string + +const ( + ScrapingJobStatusPending ScrapingJobStatus = "pending" + ScrapingJobStatusRunning ScrapingJobStatus = "running" + ScrapingJobStatusCompleted ScrapingJobStatus = "completed" + ScrapingJobStatusFailed ScrapingJobStatus = "failed" + ScrapingJobStatusCancelled ScrapingJobStatus = "cancelled" +) + +// JobProgress represents the progress of a scraping job +type JobProgress struct { + ProcessedCount int `bson:"processed_count" json:"processed_count"` + TotalCount int `bson:"total_count" json:"total_count"` + SuccessCount int `bson:"success_count" json:"success_count"` + ErrorCount int `bson:"error_count" json:"error_count"` + Percentage float64 `bson:"percentage" json:"percentage"` + CurrentStep string `bson:"current_step" json:"current_step"` +} + +// JobResults represents the results of a scraping job +type JobResults struct { + TotalProcessed int `bson:"total_processed" json:"total_processed"` + TotalCreated int `bson:"total_created" json:"total_created"` + TotalUpdated int `bson:"total_updated" json:"total_updated"` + TotalSkipped int `bson:"total_skipped" json:"total_skipped"` + TotalErrors int `bson:"total_errors" json:"total_errors"` + ProcessingErrors []string `bson:"processing_errors,omitempty" json:"processing_errors,omitempty"` + Summary string `bson:"summary,omitempty" json:"summary,omitempty"` +} + +// ScrapingJob represents a scraping job entity +type ScrapingJob struct { + ID string `bson:"_id,omitempty" json:"id"` + Type ScrapingJobType `bson:"type" json:"type"` + Status ScrapingJobStatus `bson:"status" json:"status"` + StartedAt int64 `bson:"started_at" json:"started_at"` // Unix seconds + CompletedAt *int64 `bson:"completed_at,omitempty" json:"completed_at,omitempty"` // Unix seconds + Duration *int64 `bson:"duration,omitempty" json:"duration,omitempty"` // Duration in seconds + Progress JobProgress `bson:"progress" json:"progress"` + Results JobResults `bson:"results" json:"results"` + Errors []string `bson:"errors,omitempty" json:"errors,omitempty"` + Config map[string]interface{} `bson:"config,omitempty" json:"config,omitempty"` + CreatedAt int64 `bson:"created_at" json:"created_at"` // Unix seconds + UpdatedAt int64 `bson:"updated_at" json:"updated_at"` // Unix seconds +} + +// ScrapingState represents the state of the scraping process +type ScrapingState struct { + ID string `bson:"_id,omitempty" json:"id"` + LastProcessedYear int `bson:"last_processed_year" json:"last_processed_year"` + LastProcessedNumber int `bson:"last_processed_number" json:"last_processed_number"` + LastRunAt int64 `bson:"last_run_at" json:"last_run_at"` // Unix seconds + NextRunAt int64 `bson:"next_run_at" json:"next_run_at"` // Unix seconds + IsRunning bool `bson:"is_running" json:"is_running"` + CurrentJobID string `bson:"current_job_id,omitempty" json:"current_job_id,omitempty"` + CreatedAt int64 `bson:"created_at" json:"created_at"` // Unix seconds + UpdatedAt int64 `bson:"updated_at" json:"updated_at"` // Unix seconds +} + +// GetJobID returns the scraping job ID +func (j *ScrapingJob) GetJobID() string { + return j.ID +} + +// IsActive returns true if the job is actively running +func (j *ScrapingJob) IsActive() bool { + return j.Status == ScrapingJobStatusRunning || j.Status == ScrapingJobStatusPending +} + +// IsCompleted returns true if the job has completed (successfully or with failure) +func (j *ScrapingJob) IsCompleted() bool { + return j.Status == ScrapingJobStatusCompleted || j.Status == ScrapingJobStatusFailed || j.Status == ScrapingJobStatusCancelled +} + +// GetProgressPercentage calculates and returns the progress percentage +func (j *ScrapingJob) GetProgressPercentage() float64 { + if j.Progress.TotalCount == 0 { + return 0 + } + return float64(j.Progress.ProcessedCount) / float64(j.Progress.TotalCount) * 100 +} + +// UpdateProgress updates the job progress +func (j *ScrapingJob) UpdateProgress(processed, total, success, errors int, step string) { + j.Progress.ProcessedCount = processed + j.Progress.TotalCount = total + j.Progress.SuccessCount = success + j.Progress.ErrorCount = errors + j.Progress.CurrentStep = step + j.Progress.Percentage = j.GetProgressPercentage() + j.UpdatedAt = time.Now().Unix() +} + +// AddError adds an error to the job +func (j *ScrapingJob) AddError(error string) { + if j.Errors == nil { + j.Errors = make([]string, 0) + } + j.Errors = append(j.Errors, error) + j.UpdatedAt = time.Now().Unix() +} + +// MarkCompleted marks the job as completed +func (j *ScrapingJob) MarkCompleted(status ScrapingJobStatus, results JobResults) { + j.Status = status + j.Results = results + completedAt := time.Now().Unix() + j.CompletedAt = &completedAt + if j.StartedAt > 0 { + duration := completedAt - j.StartedAt + j.Duration = &duration + } + j.UpdatedAt = completedAt +} + +// IsStateRunning returns true if the scraping state indicates a job is running +func (s *ScrapingState) IsStateRunning() bool { + return s.IsRunning +} + +// MarkAsRunning marks the state as running with a job ID +func (s *ScrapingState) MarkAsRunning(jobID string) { + s.IsRunning = true + s.CurrentJobID = jobID + s.LastRunAt = time.Now().Unix() + s.UpdatedAt = time.Now().Unix() +} + +// MarkAsCompleted marks the state as completed +func (s *ScrapingState) MarkAsCompleted(year, number int) { + s.IsRunning = false + s.CurrentJobID = "" + s.LastProcessedYear = year + s.LastProcessedNumber = number + s.NextRunAt = time.Now().Add(4 * time.Hour).Unix() // Schedule next run in 4 hours + s.UpdatedAt = time.Now().Unix() +} diff --git a/internal/tender/entity.go b/internal/tender/entity.go index 14f1d8f..62d1028 100644 --- a/internal/tender/entity.go +++ b/internal/tender/entity.go @@ -128,10 +128,10 @@ type TenderSearchCriteria struct { MinEstimatedValue *float64 `json:"min_estimated_value,omitempty"` MaxEstimatedValue *float64 `json:"max_estimated_value,omitempty"` Currency string `json:"currency,omitempty"` - DeadlineFrom *int64 `json:"deadline_from,omitempty"` // Unix milliseconds - DeadlineTo *int64 `json:"deadline_to,omitempty"` // Unix milliseconds - PublicationDateFrom *int64 `json:"publication_date_from,omitempty"` // Unix milliseconds - PublicationDateTo *int64 `json:"publication_date_to,omitempty"` // Unix milliseconds + DeadlineFrom *int64 `json:"deadline_from,omitempty"` + DeadlineTo *int64 `json:"deadline_to,omitempty"` + PublicationDateFrom *int64 `json:"publication_date_from,omitempty"` + PublicationDateTo *int64 `json:"publication_date_to,omitempty"` Status []TenderStatus `json:"status,omitempty"` Source []TenderSource `json:"source,omitempty"` BuyerOrganizationID string `json:"buyer_organization_id,omitempty"` @@ -236,230 +236,6 @@ func (t *Tender) UpdateStatus(status TenderStatus) { t.UpdatedAt = time.Now().UnixMilli() } -// AddProcessingError adds a processing error to metadata -func (t *Tender) AddProcessingError(error string) { - if t.ProcessingMetadata.ParsingErrors == nil { - t.ProcessingMetadata.ParsingErrors = make([]string, 0) - } - t.ProcessingMetadata.ParsingErrors = append(t.ProcessingMetadata.ParsingErrors, error) - t.UpdatedAt = time.Now().UnixMilli() -} - -// AddValidationError adds a validation error to metadata -func (t *Tender) AddValidationError(error string) { - if t.ProcessingMetadata.ValidationErrors == nil { - t.ProcessingMetadata.ValidationErrors = make([]string, 0) - } - t.ProcessingMetadata.ValidationErrors = append(t.ProcessingMetadata.ValidationErrors, error) - t.UpdatedAt = time.Now().UnixMilli() -} - -// HasErrors returns true if the tender has processing or validation errors -func (t *Tender) HasErrors() bool { - return len(t.ProcessingMetadata.ParsingErrors) > 0 || len(t.ProcessingMetadata.ValidationErrors) > 0 -} - -// GetAllErrors returns all processing and validation errors -func (t *Tender) GetAllErrors() []string { - var allErrors []string - allErrors = append(allErrors, t.ProcessingMetadata.ParsingErrors...) - allErrors = append(allErrors, t.ProcessingMetadata.ValidationErrors...) - return allErrors -} - -// ScrapingJobType represents the type of scraping job -type ScrapingJobType string - -const ( - ScrapingJobTypeTEDScraper ScrapingJobType = "ted_scraper" - ScrapingJobTypeManual ScrapingJobType = "manual" - ScrapingJobTypeAPI ScrapingJobType = "api" - ScrapingJobTypeBulkImport ScrapingJobType = "bulk_import" -) - -// ScrapingJobStatus represents the status of a scraping job -type ScrapingJobStatus string - -const ( - ScrapingJobStatusPending ScrapingJobStatus = "pending" - ScrapingJobStatusRunning ScrapingJobStatus = "running" - ScrapingJobStatusCompleted ScrapingJobStatus = "completed" - ScrapingJobStatusFailed ScrapingJobStatus = "failed" - ScrapingJobStatusCancelled ScrapingJobStatus = "cancelled" -) - -// JobProgress represents the progress of a scraping job -type JobProgress struct { - ProcessedCount int `bson:"processed_count" json:"processed_count"` - TotalCount int `bson:"total_count" json:"total_count"` - SuccessCount int `bson:"success_count" json:"success_count"` - ErrorCount int `bson:"error_count" json:"error_count"` - Percentage float64 `bson:"percentage" json:"percentage"` - CurrentStep string `bson:"current_step" json:"current_step"` -} - -// JobResults represents the results of a scraping job -type JobResults struct { - TotalProcessed int `bson:"total_processed" json:"total_processed"` - TotalCreated int `bson:"total_created" json:"total_created"` - TotalUpdated int `bson:"total_updated" json:"total_updated"` - TotalSkipped int `bson:"total_skipped" json:"total_skipped"` - TotalErrors int `bson:"total_errors" json:"total_errors"` - ProcessingErrors []string `bson:"processing_errors,omitempty" json:"processing_errors,omitempty"` - Summary string `bson:"summary,omitempty" json:"summary,omitempty"` -} - -// ScrapingJob represents a scraping job entity -type ScrapingJob struct { - ID string `bson:"_id,omitempty" json:"id"` - Type ScrapingJobType `bson:"type" json:"type"` - Status ScrapingJobStatus `bson:"status" json:"status"` - StartedAt int64 `bson:"started_at" json:"started_at"` // Unix seconds - CompletedAt *int64 `bson:"completed_at,omitempty" json:"completed_at,omitempty"` // Unix seconds - Duration *int64 `bson:"duration,omitempty" json:"duration,omitempty"` // Duration in seconds - Progress JobProgress `bson:"progress" json:"progress"` - Results JobResults `bson:"results" json:"results"` - Errors []string `bson:"errors,omitempty" json:"errors,omitempty"` - Config map[string]interface{} `bson:"config,omitempty" json:"config,omitempty"` - CreatedAt int64 `bson:"created_at" json:"created_at"` // Unix seconds - UpdatedAt int64 `bson:"updated_at" json:"updated_at"` // Unix seconds -} - -// ScrapingState represents the state of the scraping process -type ScrapingState struct { - ID string `bson:"_id,omitempty" json:"id"` - LastProcessedYear int `bson:"last_processed_year" json:"last_processed_year"` - LastProcessedNumber int `bson:"last_processed_number" json:"last_processed_number"` - LastRunAt int64 `bson:"last_run_at" json:"last_run_at"` // Unix seconds - NextRunAt int64 `bson:"next_run_at" json:"next_run_at"` // Unix seconds - IsRunning bool `bson:"is_running" json:"is_running"` - CurrentJobID string `bson:"current_job_id,omitempty" json:"current_job_id,omitempty"` - CreatedAt int64 `bson:"created_at" json:"created_at"` // Unix seconds - UpdatedAt int64 `bson:"updated_at" json:"updated_at"` // Unix seconds -} - -// GetJobID returns the scraping job ID -func (j *ScrapingJob) GetJobID() string { - return j.ID -} - -// IsActive returns true if the job is actively running -func (j *ScrapingJob) IsActive() bool { - return j.Status == ScrapingJobStatusRunning || j.Status == ScrapingJobStatusPending -} - -// IsCompleted returns true if the job has completed (successfully or with failure) -func (j *ScrapingJob) IsCompleted() bool { - return j.Status == ScrapingJobStatusCompleted || j.Status == ScrapingJobStatusFailed || j.Status == ScrapingJobStatusCancelled -} - -// GetProgressPercentage calculates and returns the progress percentage -func (j *ScrapingJob) GetProgressPercentage() float64 { - if j.Progress.TotalCount == 0 { - return 0 - } - return float64(j.Progress.ProcessedCount) / float64(j.Progress.TotalCount) * 100 -} - -// UpdateProgress updates the job progress -func (j *ScrapingJob) UpdateProgress(processed, total, success, errors int, step string) { - j.Progress.ProcessedCount = processed - j.Progress.TotalCount = total - j.Progress.SuccessCount = success - j.Progress.ErrorCount = errors - j.Progress.CurrentStep = step - j.Progress.Percentage = j.GetProgressPercentage() - j.UpdatedAt = time.Now().Unix() -} - -// AddError adds an error to the job -func (j *ScrapingJob) AddError(error string) { - if j.Errors == nil { - j.Errors = make([]string, 0) - } - j.Errors = append(j.Errors, error) - j.UpdatedAt = time.Now().Unix() -} - -// MarkCompleted marks the job as completed -func (j *ScrapingJob) MarkCompleted(status ScrapingJobStatus, results JobResults) { - j.Status = status - j.Results = results - completedAt := time.Now().Unix() - j.CompletedAt = &completedAt - if j.StartedAt > 0 { - duration := completedAt - j.StartedAt - j.Duration = &duration - } - j.UpdatedAt = completedAt -} - -// IsStateRunning returns true if the scraping state indicates a job is running -func (s *ScrapingState) IsStateRunning() bool { - return s.IsRunning -} - -// MarkAsRunning marks the state as running with a job ID -func (s *ScrapingState) MarkAsRunning(jobID string) { - s.IsRunning = true - s.CurrentJobID = jobID - s.LastRunAt = time.Now().Unix() - s.UpdatedAt = time.Now().Unix() -} - -// MarkAsCompleted marks the state as completed -func (s *ScrapingState) MarkAsCompleted(year, number int) { - s.IsRunning = false - s.CurrentJobID = "" - s.LastProcessedYear = year - s.LastProcessedNumber = number - 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 { diff --git a/internal/tender/form.go b/internal/tender/form.go index 0398204..b3c83e8 100644 --- a/internal/tender/form.go +++ b/internal/tender/form.go @@ -1,44 +1,6 @@ package tender -// CreateTenderRequest represents a request to create a new tender -type CreateTenderRequest struct { - ContractNoticeID string `json:"contract_notice_id" valid:"required~Contract notice ID is required"` - NoticePublicationID string `json:"notice_publication_id" valid:"required~Notice publication ID is required"` - ContractFolderID string `json:"contract_folder_id" valid:"required~Contract folder ID is required"` - NoticeTypeCode string `json:"notice_type_code" valid:"required~Notice type code is required"` - NoticeSubTypeCode string `json:"notice_sub_type_code,omitempty"` - NoticeLanguageCode string `json:"notice_language_code,omitempty" valid:"stringlength(3|3)~Language code must be 3 characters"` - IssueDate int64 `json:"issue_date" valid:"required~Issue date is required"` // Unix milliseconds - IssueTime int64 `json:"issue_time,omitempty"` // Unix milliseconds - PublicationDate int64 `json:"publication_date,omitempty"` // Unix milliseconds - GazetteID string `json:"gazette_id,omitempty"` - Title string `json:"title" valid:"required~Title is required,stringlength(1|500)~Title must be between 1 and 500 characters"` - Description string `json:"description,omitempty" valid:"stringlength(0|5000)~Description must not exceed 5000 characters"` - ProcurementTypeCode string `json:"procurement_type_code,omitempty"` - ProcedureCode string `json:"procedure_code,omitempty"` - MainClassification string `json:"main_classification,omitempty"` - AdditionalClassifications []string `json:"additional_classifications,omitempty"` - EstimatedValue float64 `json:"estimated_value,omitempty" valid:"range(0|999999999999)~Estimated value must be between 0 and 999999999999"` - Currency string `json:"currency,omitempty" valid:"stringlength(3|3)~Currency must be 3 characters"` - Duration string `json:"duration,omitempty"` - DurationUnit string `json:"duration_unit,omitempty"` - TenderDeadline int64 `json:"tender_deadline,omitempty"` // Unix milliseconds - PlaceOfPerformance string `json:"place_of_performance,omitempty"` - CountryCode string `json:"country_code,omitempty" valid:"stringlength(2|2)~Country code must be 2 characters"` - RegionCode string `json:"region_code,omitempty"` - CityName string `json:"city_name,omitempty"` - PostalCode string `json:"postal_code,omitempty"` - DocumentURI string `json:"document_uri,omitempty" valid:"url~Document URI must be a valid URL"` - TenderURL string `json:"tender_url,omitempty" valid:"url~Tender URL must be a valid URL"` - BuyerOrganization *Organization `json:"buyer_organization,omitempty"` - ReviewOrganization *Organization `json:"review_organization,omitempty"` - Organizations []Organization `json:"organizations,omitempty"` - SelectionCriteria []SelectionCriterion `json:"selection_criteria,omitempty"` - OfficialLanguages []string `json:"official_languages,omitempty"` - Source TenderSource `json:"source" valid:"required~Source is required"` - SourceFileURL string `json:"source_file_url,omitempty" valid:"url~Source file URL must be a valid URL"` - SourceFileName string `json:"source_file_name,omitempty"` -} +import "tm/pkg/response" // UpdateTenderRequest represents a request to update an existing tender type UpdateTenderRequest struct { @@ -61,16 +23,8 @@ type ListTendersRequest struct { // ListTendersResponse represents the response for listing tenders type ListTendersResponse struct { - Tenders []TenderResponse `json:"tenders"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - -// SearchTendersRequest represents a request to search tenders -type SearchTendersRequest struct { - Query string `json:"query" valid:"required~Search query is required,stringlength(1|200)~Query must be between 1 and 200 characters"` - Criteria TenderSearchCriteria `json:"criteria"` + Tenders []TenderResponse `json:"tenders"` + Metadata *response.Meta `json:"metadata"` } // SearchTendersResponse represents the response for searching tenders @@ -80,229 +34,48 @@ type SearchTendersResponse struct { Total int `json:"total"` } -// StartScrapingJobRequest represents a request to start a scraping job -type StartScrapingJobRequest struct { - Type ScrapingJobType `json:"type" valid:"required~Job type is required"` - Config map[string]interface{} `json:"config,omitempty"` -} - -// ListScrapingJobsRequest represents a request to list scraping jobs -type ListScrapingJobsRequest struct { - 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"` -} - -// ListScrapingJobsResponse represents the response for listing scraping jobs -type ListScrapingJobsResponse struct { - Jobs []ScrapingJob `json:"jobs"` - Total int64 `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - -// BatchProcessingResult represents the result of batch processing -type BatchProcessingResult struct { - Total int `json:"total"` - Processed int `json:"processed"` - Errors []string `json:"errors"` - Tenders []*Tender `json:"tenders"` - SuccessRate float64 `json:"success_rate"` -} - -// DashboardData represents dashboard data -type DashboardData struct { - Statistics *TenderStatistics `json:"statistics"` - RecentTenders []Tender `json:"recent_tenders"` - ActiveJobs []ScrapingJob `json:"active_jobs"` -} - // TenderResponse represents a tender in API responses type TenderResponse struct { - ID string `json:"id"` - ContractNoticeID string `json:"contract_notice_id"` - NoticePublicationID string `json:"notice_publication_id"` - ContractFolderID string `json:"contract_folder_id"` - NoticeTypeCode string `json:"notice_type_code"` - NoticeSubTypeCode string `json:"notice_sub_type_code"` - NoticeLanguageCode string `json:"notice_language_code"` - IssueDate int64 `json:"issue_date"` - IssueTime int64 `json:"issue_time"` - PublicationDate int64 `json:"publication_date"` - GazetteID string `json:"gazette_id"` - Title string `json:"title"` - Description string `json:"description"` - ProcurementTypeCode string `json:"procurement_type_code"` - ProcedureCode string `json:"procedure_code"` - MainClassification string `json:"main_classification"` - AdditionalClassifications []string `json:"additional_classifications"` - EstimatedValue float64 `json:"estimated_value"` - Currency string `json:"currency"` - FormattedEstimatedValue string `json:"formatted_estimated_value"` - Duration string `json:"duration"` - DurationUnit string `json:"duration_unit"` - TenderDeadline int64 `json:"tender_deadline"` - PlaceOfPerformance string `json:"place_of_performance"` - CountryCode string `json:"country_code"` - RegionCode string `json:"region_code"` - CityName string `json:"city_name"` - PostalCode string `json:"postal_code"` - DocumentURI string `json:"document_uri"` - TenderURL string `json:"tender_url"` - BuyerOrganization *Organization `json:"buyer_organization"` - ReviewOrganization *Organization `json:"review_organization,omitempty"` - Organizations []Organization `json:"organizations"` - SelectionCriteria []SelectionCriterion `json:"selection_criteria"` - OfficialLanguages []string `json:"official_languages"` - Status TenderStatus `json:"status"` - Source TenderSource `json:"source"` - SourceFileURL string `json:"source_file_url"` - SourceFileName string `json:"source_file_name"` - ProcessingMetadata ProcessingMetadata `json:"processing_metadata"` - 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"` + ID string `json:"id"` + NoticePublicationID string `json:"notice_publication_id"` + PublicationDate int64 `json:"publication_date"` + Title string `json:"title"` + Description string `json:"description"` + ProcurementTypeCode string `json:"procurement_type_code"` + ProcedureCode string `json:"procedure_code"` + EstimatedValue float64 `json:"estimated_value"` + Currency string `json:"currency"` + Duration string `json:"duration"` + DurationUnit string `json:"duration_unit"` + TenderDeadline int64 `json:"tender_deadline"` + CountryCode string `json:"country_code"` + BuyerOrganization *OrganizationResponse `json:"buyer_organization"` + Status TenderStatus `json:"status"` } -// ScrapingJobResponse represents a scraping job in API responses -type ScrapingJobResponse struct { - ID string `json:"id"` - Type ScrapingJobType `json:"type"` - Status ScrapingJobStatus `json:"status"` - StartedAt int64 `json:"started_at"` - CompletedAt *int64 `json:"completed_at,omitempty"` - Duration *int64 `json:"duration,omitempty"` - Progress JobProgress `json:"progress"` - Results JobResults `json:"results"` - Errors []string `json:"errors,omitempty"` - Config map[string]interface{} `json:"config,omitempty"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -// ScrapingStateResponse represents scraping state in API responses -type ScrapingStateResponse struct { - ID string `json:"id"` - LastProcessedYear int `json:"last_processed_year"` - LastProcessedNumber int `json:"last_processed_number"` - LastRunAt int64 `json:"last_run_at"` - NextRunAt int64 `json:"next_run_at"` - IsRunning bool `json:"is_running"` - CurrentJobID string `json:"current_job_id,omitempty"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` -} - -// ValidationErrorResponse represents validation errors in API responses -type ValidationErrorResponse struct { - Field string `json:"field"` - Message string `json:"message"` - Value string `json:"value,omitempty"` -} - -// ErrorResponse represents error responses -type ErrorResponse struct { - Error string `json:"error"` - Message string `json:"message"` - ValidationErrors []ValidationErrorResponse `json:"validation_errors,omitempty"` - Timestamp int64 `json:"timestamp"` +type OrganizationResponse struct { + Name string `json:"name"` } // ToTenderResponse converts a Tender entity to TenderResponse func (t *Tender) ToTenderResponse() *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, - ContractFolderID: t.ContractFolderID, - NoticeTypeCode: t.NoticeTypeCode, - NoticeSubTypeCode: t.NoticeSubTypeCode, - NoticeLanguageCode: t.NoticeLanguageCode, - IssueDate: t.IssueDate, - IssueTime: t.IssueTime, - PublicationDate: t.PublicationDate, - GazetteID: t.GazetteID, - Title: t.Title, - Description: t.Description, - ProcurementTypeCode: t.ProcurementTypeCode, - ProcedureCode: t.ProcedureCode, - MainClassification: t.MainClassification, - AdditionalClassifications: t.AdditionalClassifications, - EstimatedValue: t.EstimatedValue, - Currency: t.Currency, - FormattedEstimatedValue: t.GetFormattedEstimatedValue(), - Duration: t.Duration, - DurationUnit: t.DurationUnit, - TenderDeadline: t.TenderDeadline, - PlaceOfPerformance: t.PlaceOfPerformance, - CountryCode: t.CountryCode, - RegionCode: t.RegionCode, - CityName: t.CityName, - PostalCode: t.PostalCode, - DocumentURI: t.DocumentURI, - TenderURL: t.TenderURL, - BuyerOrganization: t.BuyerOrganization, - ReviewOrganization: t.ReviewOrganization, - Organizations: t.Organizations, - SelectionCriteria: t.SelectionCriteria, - OfficialLanguages: t.OfficialLanguages, - Status: t.Status, - Source: t.Source, - SourceFileURL: t.SourceFileURL, - SourceFileName: t.SourceFileName, - ProcessingMetadata: t.ProcessingMetadata, - IsActive: t.IsActive(), - IsExpired: t.IsExpired(), - HasErrors: t.HasErrors(), - 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 + ID: t.ID.Hex(), + NoticePublicationID: t.NoticePublicationID, + PublicationDate: t.PublicationDate, + Title: t.Title, + Description: t.Description, + ProcurementTypeCode: t.ProcurementTypeCode, + ProcedureCode: t.ProcedureCode, + EstimatedValue: t.EstimatedValue, + Currency: t.Currency, + Duration: t.Duration, + DurationUnit: t.DurationUnit, + TenderDeadline: t.TenderDeadline, + CountryCode: t.CountryCode, + BuyerOrganization: &OrganizationResponse{Name: t.BuyerOrganization.Name}, + Status: t.Status, } return response } - -// ToScrapingJobResponse converts a ScrapingJob entity to ScrapingJobResponse -func (j *ScrapingJob) ToScrapingJobResponse() *ScrapingJobResponse { - return &ScrapingJobResponse{ - ID: j.ID, - Type: j.Type, - Status: j.Status, - StartedAt: j.StartedAt, - CompletedAt: j.CompletedAt, - Duration: j.Duration, - Progress: j.Progress, - Results: j.Results, - Errors: j.Errors, - Config: j.Config, - CreatedAt: j.CreatedAt, - UpdatedAt: j.UpdatedAt, - } -} - -// ToScrapingStateResponse converts a ScrapingState entity to ScrapingStateResponse -func (s *ScrapingState) ToScrapingStateResponse() *ScrapingStateResponse { - return &ScrapingStateResponse{ - ID: s.ID, - LastProcessedYear: s.LastProcessedYear, - LastProcessedNumber: s.LastProcessedNumber, - LastRunAt: s.LastRunAt, - NextRunAt: s.NextRunAt, - IsRunning: s.IsRunning, - CurrentJobID: s.CurrentJobID, - CreatedAt: s.CreatedAt, - UpdatedAt: s.UpdatedAt, - } -} diff --git a/internal/tender/handler.go b/internal/tender/handler.go index 00c2a3d..5f7ad16 100644 --- a/internal/tender/handler.go +++ b/internal/tender/handler.go @@ -24,40 +24,6 @@ func NewTenderHandler(service TenderService, logger logger.Logger) *TenderHandle } } -// CreateTender creates a new tender -// @Summary Create a new tender -// @Description Create a new tender with the provided information -// @Tags Admin-Tenders -// @Accept json -// @Produce json -// @Param tender body CreateTenderRequest true "Tender information" -// @Success 201 {object} response.APIResponse{data=TenderResponse} -// @Failure 400 {object} response.APIResponse -// @Failure 409 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /admin/tenders [post] -// @Security BearerAuth -func (h *TenderHandler) CreateTender(c echo.Context) error { - var req CreateTenderRequest - if err := c.Bind(&req); err != nil { - return response.ValidationError(c, "Invalid request format", err.Error()) - } - - if _, err := govalidator.ValidateStruct(req); err != nil { - return response.ValidationError(c, "Validation failed", err.Error()) - } - - tender, err := h.service.CreateTender(c.Request().Context(), req) - if err != nil { - if err.Error() == "tender with notice publication ID already exists" { - return response.Conflict(c, "Tender already exists") - } - return response.InternalServerError(c, "Failed to create tender") - } - - return response.Created(c, tender.ToTenderResponse(), "Tender created successfully") -} - // GetTender retrieves a tender by ID // @Summary Get tender by ID // @Description Retrieve a specific tender by its ID @@ -80,7 +46,7 @@ func (h *TenderHandler) GetTender(c echo.Context) error { return response.NotFound(c, "Tender not found") } - return response.Success(c, tender.ToTenderResponse(), "Tender retrieved successfully") + return response.Success(c, tender, "Tender retrieved successfully") } // UpdateTender updates an existing tender @@ -250,247 +216,7 @@ func (h *TenderHandler) ListTenders(c echo.Context) error { return response.InternalServerError(c, "Failed to retrieve tenders") } - responseData := map[string]interface{}{ - "tenders": result.Tenders, - "total": result.Total, - "limit": result.Limit, - "offset": result.Offset, - } - - return response.Success(c, responseData, "Tenders retrieved successfully") -} - -// SearchTenders searches tenders -// @Summary Search tenders -// @Description Search tenders using full-text search and filtering -// @Tags Admin-Tenders -// @Accept json -// @Produce json -// @Param search body SearchTendersRequest true "Search criteria" -// @Success 200 {object} response.APIResponse{data=map[string]interface{}} -// @Failure 400 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /admin/tenders/search [post] -// @Security BearerAuth -func (h *TenderHandler) SearchTenders(c echo.Context) error { - var req SearchTendersRequest - if err := c.Bind(&req); err != nil { - return response.ValidationError(c, "Invalid request format", err.Error()) - } - - if _, err := govalidator.ValidateStruct(req); err != nil { - return response.ValidationError(c, "Validation failed", err.Error()) - } - - result, err := h.service.SearchTenders(c.Request().Context(), req) - if err != nil { - return response.InternalServerError(c, "Failed to search 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, - "query": result.Query, - "total": result.Total, - } - - return response.Success(c, responseData, "Tenders found successfully") -} - -// GetTenderStatistics retrieves tender statistics -// @Summary Get tender statistics -// @Description Retrieve comprehensive statistics about tenders -// @Tags Admin-Tenders -// @Produce json -// @Success 200 {object} response.APIResponse{data=TenderStatistics} -// @Failure 500 {object} response.APIResponse -// @Router /admin/tenders/statistics [get] -// @Security BearerAuth -func (h *TenderHandler) GetTenderStatistics(c echo.Context) error { - stats, err := h.service.GetTenderStatistics(c.Request().Context()) - if err != nil { - return response.InternalServerError(c, "Failed to retrieve statistics") - } - - return response.Success(c, stats, "Statistics retrieved successfully") -} - -// GetDashboard retrieves dashboard data -// @Summary Get dashboard data -// @Description Retrieve dashboard data including statistics, recent tenders, and active jobs -// @Tags Admin-Tenders -// @Produce json -// @Success 200 {object} response.APIResponse{data=map[string]interface{}} -// @Failure 500 {object} response.APIResponse -// @Router /admin/tenders/dashboard [get] -// @Security BearerAuth -func (h *TenderHandler) GetDashboard(c echo.Context) error { - dashboard, err := h.service.GetDashboardData(c.Request().Context()) - if err != nil { - return response.InternalServerError(c, "Failed to retrieve dashboard data") - } - - // Convert recent tenders to response format - recentTenderResponses := make([]TenderResponse, len(dashboard.RecentTenders)) - for i, tender := range dashboard.RecentTenders { - recentTenderResponses[i] = *tender.ToTenderResponse() - } - - // Convert active jobs to response format - activeJobResponses := make([]ScrapingJobResponse, len(dashboard.ActiveJobs)) - for i, job := range dashboard.ActiveJobs { - activeJobResponses[i] = *job.ToScrapingJobResponse() - } - - responseData := map[string]interface{}{ - "statistics": dashboard.Statistics, - "recent_tenders": recentTenderResponses, - "active_jobs": activeJobResponses, - } - - return response.Success(c, responseData, "Dashboard data retrieved successfully") -} - -// StartScrapingJob starts a new scraping job -// @Summary Start scraping job -// @Description Start a new TED XML scraping job -// @Tags Admin-Scraping -// @Accept json -// @Produce json -// @Param job body StartScrapingJobRequest true "Scraping job configuration" -// @Success 201 {object} response.APIResponse{data=ScrapingJobResponse} -// @Failure 400 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /admin/scraping/jobs [post] -// @Security BearerAuth -func (h *TenderHandler) StartScrapingJob(c echo.Context) error { - var req StartScrapingJobRequest - if err := c.Bind(&req); err != nil { - return response.ValidationError(c, "Invalid request format", err.Error()) - } - - if _, err := govalidator.ValidateStruct(req); err != nil { - return response.ValidationError(c, "Validation failed", err.Error()) - } - - job, err := h.service.StartScrapingJob(c.Request().Context(), req) - if err != nil { - return response.InternalServerError(c, "Failed to start scraping job") - } - - return response.Created(c, job.ToScrapingJobResponse(), "Scraping job started successfully") -} - -// GetScrapingJob retrieves a scraping job by ID -// @Summary Get scraping job -// @Description Retrieve a specific scraping job by its ID -// @Tags Admin-Scraping -// @Produce json -// @Param id path string true "Scraping job ID" -// @Success 200 {object} response.APIResponse{data=ScrapingJobResponse} -// @Failure 404 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /admin/scraping/jobs/{id} [get] -// @Security BearerAuth -func (h *TenderHandler) GetScrapingJob(c echo.Context) error { - id := c.Param("id") - if id == "" { - return response.BadRequest(c, "Scraping job ID is required", "ID parameter cannot be empty") - } - - job, err := h.service.GetScrapingJobStatus(c.Request().Context(), id) - if err != nil { - return response.NotFound(c, "Scraping job not found") - } - - return response.Success(c, job.ToScrapingJobResponse(), "Scraping job retrieved successfully") -} - -// ListScrapingJobs retrieves scraping jobs with pagination -// @Summary List scraping jobs -// @Description Retrieve scraping jobs with pagination -// @Tags Admin-Scraping -// @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)" -// @Success 200 {object} response.APIResponse{data=map[string]interface{}} -// @Failure 400 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Security BearerAuth -// @Router /admin/scraping/jobs [get] -func (h *TenderHandler) ListScrapingJobs(c echo.Context) error { - // Parse pagination parameters - limit := 20 - if l := c.QueryParam("limit"); l != "" { - if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 { - limit = parsed - } - } - - offset := 0 - if o := c.QueryParam("offset"); o != "" { - if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { - offset = parsed - } - } - - req := ListScrapingJobsRequest{ - Limit: limit, - Offset: offset, - } - - result, err := h.service.ListScrapingJobs(c.Request().Context(), req) - if err != nil { - return response.InternalServerError(c, "Failed to retrieve scraping jobs") - } - - // Convert jobs to response format - jobResponses := make([]ScrapingJobResponse, len(result.Jobs)) - for i, job := range result.Jobs { - jobResponses[i] = *job.ToScrapingJobResponse() - } - - responseData := map[string]interface{}{ - "jobs": jobResponses, - "total": result.Total, - "limit": result.Limit, - "offset": result.Offset, - } - - return response.Success(c, responseData, "Scraping jobs retrieved successfully") -} - -// CancelScrapingJob cancels a running scraping job -// @Summary Cancel scraping job -// @Description Cancel a running or pending scraping job -// @Tags Admin-Scraping -// @Param id path string true "Scraping job ID" -// @Success 200 {object} response.APIResponse -// @Failure 400 {object} response.APIResponse -// @Failure 404 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /admin/scraping/jobs/{id}/cancel [post] -// @Security BearerAuth -func (h *TenderHandler) CancelScrapingJob(c echo.Context) error { - id := c.Param("id") - if id == "" { - return response.BadRequest(c, "Scraping job ID is required", "ID parameter cannot be empty") - } - - err := h.service.CancelScrapingJob(c.Request().Context(), id) - if err != nil { - if err.Error() == "scraping job not found" { - return response.NotFound(c, "Scraping job not found") - } - return response.InternalServerError(c, "Failed to cancel scraping job") - } - - return response.Success(c, map[string]interface{}{"cancelled": true}, "Scraping job cancelled successfully") + return response.Success(c, result, "Tenders retrieved successfully") } // GetPublicTenders retrieves public tenders for mobile app @@ -511,17 +237,13 @@ func (h *TenderHandler) CancelScrapingJob(c echo.Context) error { func (h *TenderHandler) GetPublicTenders(c echo.Context) error { // Parse pagination parameters (more restrictive for public API) limit := 20 - if l := c.QueryParam("limit"); l != "" { - if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 50 { - limit = parsed - } + if parsed, err := strconv.Atoi(c.QueryParam("limit")); err == nil && parsed > 0 && parsed <= 50 { + limit = parsed } offset := 0 - if o := c.QueryParam("offset"); o != "" { - if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { - offset = parsed - } + if parsed, err := strconv.Atoi(c.QueryParam("offset")); err == nil && parsed >= 0 { + offset = parsed } // Build search criteria for public API (only active tenders) @@ -534,21 +256,17 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { } // Parse numeric parameters - if minValue := c.QueryParam("min_estimated_value"); minValue != "" { - if parsed, err := strconv.ParseFloat(minValue, 64); err == nil { - criteria.MinEstimatedValue = &parsed - } + if parsed, err := strconv.ParseFloat(c.QueryParam("min_estimated_value"), 64); err == nil { + criteria.MinEstimatedValue = &parsed } - if maxValue := c.QueryParam("max_estimated_value"); maxValue != "" { - if parsed, err := strconv.ParseFloat(maxValue, 64); err == nil { - criteria.MaxEstimatedValue = &parsed - } + if parsed, err := strconv.ParseFloat(c.QueryParam("max_estimated_value"), 64); err == nil { + criteria.MaxEstimatedValue = &parsed } // Get company ID from customer context for automatic matching var companyID *string - if customerCompanyID, ok := c.Get("company_id").(string); ok && customerCompanyID != "" { + if customerCompanyID, ok := c.Get("company_id").(string); ok { companyID = &customerCompanyID } @@ -564,43 +282,7 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error { return response.InternalServerError(c, "Failed to retrieve tenders") } - // Convert tenders to public response format (limited fields) - publicTenders := make([]map[string]interface{}, len(result.Tenders)) - for i, tender := range result.Tenders { - tenderData := map[string]interface{}{ - "id": tender.ID, - "title": tender.Title, - "description": tender.Description, - "procurement_type_code": tender.ProcurementTypeCode, - "main_classification": tender.MainClassification, - "estimated_value": tender.EstimatedValue, - "currency": tender.Currency, - "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, - } - - // Add match percentage if available - if tender.MatchPercentage != nil { - tenderData["match_percentage"] = *tender.MatchPercentage - } - - publicTenders[i] = tenderData - } - - responseData := map[string]interface{}{ - "tenders": publicTenders, - "total": result.Total, - "limit": result.Limit, - "offset": result.Offset, - } - - return response.Success(c, responseData, "Tenders retrieved successfully") + return response.Success(c, result, "Tenders retrieved successfully") } // GetPublicTenderDetails retrieves public tender details for mobile app @@ -625,40 +307,7 @@ func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error { return response.NotFound(c, "Tender not found") } - // Return public-safe tender details - publicTender := map[string]interface{}{ - "id": tender.ID, - "notice_publication_id": tender.NoticePublicationID, - "title": tender.Title, - "description": tender.Description, - "procurement_type_code": tender.ProcurementTypeCode, - "procedure_code": tender.ProcedureCode, - "main_classification": tender.MainClassification, - "additional_classifications": tender.AdditionalClassifications, - "estimated_value": tender.EstimatedValue, - "currency": tender.Currency, - "formatted_estimated_value": tender.GetFormattedEstimatedValue(), - "duration": tender.Duration, - "duration_unit": tender.DurationUnit, - "tender_deadline": tender.TenderDeadline, - "place_of_performance": tender.PlaceOfPerformance, - "country_code": tender.CountryCode, - "region_code": tender.RegionCode, - "city_name": tender.CityName, - "postal_code": tender.PostalCode, - "document_uri": tender.DocumentURI, - "tender_url": tender.TenderURL, - "buyer_organization": tender.BuyerOrganization, - "organizations": tender.Organizations, - "selection_criteria": tender.SelectionCriteria, - "official_languages": tender.OfficialLanguages, - "publication_date": tender.PublicationDate, - "gazette_id": tender.GazetteID, - "is_active": tender.IsActive(), - "is_expired": tender.IsExpired(), - } - - return response.Success(c, publicTender, "Tender details retrieved successfully") + return response.Success(c, tender, "Tender details retrieved successfully") } // parseStringArray parses a comma-separated string into a string array diff --git a/internal/tender/repository.go b/internal/tender/repository.go index 06e0421..1c399c9 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -7,7 +7,6 @@ import ( mongopkg "tm/pkg/mongo" "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" ) @@ -38,24 +37,11 @@ type TenderRepository interface { GetTenderCountByCountry(ctx context.Context) (map[string]int64, error) GetTenderCountByType(ctx context.Context) (map[string]int64, error) GetTenderCountByClassification(ctx context.Context) (map[string]int64, error) - - // Scraping state management - GetScrapingState(ctx context.Context) (*ScrapingState, error) - SaveScrapingState(ctx context.Context, state *ScrapingState) error - - // Scraping jobs management - CreateScrapingJob(ctx context.Context, job *ScrapingJob) error - GetScrapingJob(ctx context.Context, id string) (*ScrapingJob, error) - UpdateScrapingJob(ctx context.Context, job *ScrapingJob) error - ListScrapingJobs(ctx context.Context, limit, offset int) ([]ScrapingJob, int64, error) - GetActiveScrapingJobs(ctx context.Context) ([]ScrapingJob, error) } // tenderRepository implements TenderRepository interface using MongoDB ORM type tenderRepository struct { ormRepo mongopkg.Repository[Tender] - stateOrmRepo mongopkg.Repository[ScrapingState] - jobOrmRepo mongopkg.Repository[ScrapingJob] mongoManager *mongopkg.ConnectionManager logger logger.Logger } @@ -97,17 +83,6 @@ func NewTenderRepository(mongoManager *mongopkg.ConnectionManager, logger logger *mongopkg.CreateTextIndex("search_idx", "title", "description"), } - // Create indexes for scraping jobs collection - jobIndexes := []mongopkg.Index{ - *mongopkg.NewIndex("type_idx", bson.D{{Key: "type", Value: 1}}), - *mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), - *mongopkg.NewIndex("started_at_idx", bson.D{{Key: "started_at", Value: -1}}), - *mongopkg.NewIndex("type_status_idx", bson.D{ - {Key: "type", Value: 1}, - {Key: "status", Value: 1}, - }), - } - // Create indexes if err := mongoManager.CreateIndexes("tenders", tenderIndexes); err != nil { logger.Warn("Failed to create tender indexes", map[string]interface{}{ @@ -115,21 +90,11 @@ func NewTenderRepository(mongoManager *mongopkg.ConnectionManager, logger logger }) } - if err := mongoManager.CreateIndexes("scraping_jobs", jobIndexes); err != nil { - logger.Warn("Failed to create scraping job indexes", map[string]interface{}{ - "error": err.Error(), - }) - } - // Create ORM repositories tenderOrmRepo := mongopkg.NewRepository[Tender](mongoManager.GetCollection("tenders"), logger) - stateOrmRepo := mongopkg.NewRepository[ScrapingState](mongoManager.GetCollection("scraping_state"), logger) - jobOrmRepo := mongopkg.NewRepository[ScrapingJob](mongoManager.GetCollection("scraping_jobs"), logger) return &tenderRepository{ ormRepo: tenderOrmRepo, - stateOrmRepo: stateOrmRepo, - jobOrmRepo: jobOrmRepo, mongoManager: mongoManager, logger: logger, } @@ -584,169 +549,6 @@ func (r *tenderRepository) GetTenderCountByClassification(ctx context.Context) ( return counts, nil } -// GetScrapingState retrieves the current scraping state -func (r *tenderRepository) GetScrapingState(ctx context.Context) (*ScrapingState, error) { - // Try to find existing state - filter := bson.M{} - pagination := mongopkg.Pagination{Limit: 1} - - result, err := r.stateOrmRepo.FindAll(ctx, filter, pagination) - if err != nil { - return nil, err - } - - if len(result.Items) == 0 { - // Return default state if none exists - return &ScrapingState{ - ID: "default", - LastProcessedYear: time.Now().Year(), - LastProcessedNumber: 0, - LastRunAt: 0, - NextRunAt: time.Now().Add(4 * time.Hour).Unix(), - IsRunning: false, - CreatedAt: time.Now().Unix(), - UpdatedAt: time.Now().Unix(), - }, nil - } - - return &result.Items[0], nil -} - -// SaveScrapingState saves the scraping state -func (r *tenderRepository) SaveScrapingState(ctx context.Context, state *ScrapingState) error { - if state.ID == "" { - state.ID = "default" - } - - now := time.Now().Unix() - if state.CreatedAt == 0 { - state.CreatedAt = now - } - state.UpdatedAt = now - - // Try to update existing state first - existing, err := r.GetScrapingState(ctx) - if err == nil && existing.ID != "" { - state.ID = existing.ID - state.CreatedAt = existing.CreatedAt - return r.stateOrmRepo.Update(ctx, state) - } - - // Create new state if none exists - return r.stateOrmRepo.Create(ctx, state) -} - -// CreateScrapingJob creates a new scraping job -func (r *tenderRepository) CreateScrapingJob(ctx context.Context, job *ScrapingJob) error { - if job.ID == "" { - job.ID = primitive.NewObjectID().Hex() - } - - now := time.Now().Unix() - job.CreatedAt = now - job.UpdatedAt = now - - err := r.jobOrmRepo.Create(ctx, job) - if err != nil { - r.logger.Error("Failed to create scraping job", map[string]interface{}{ - "error": err.Error(), - "type": job.Type, - }) - return err - } - - r.logger.Info("Scraping job created successfully", map[string]interface{}{ - "job_id": job.ID, - "type": job.Type, - }) - - return nil -} - -// GetScrapingJob retrieves a scraping job by ID -func (r *tenderRepository) GetScrapingJob(ctx context.Context, id string) (*ScrapingJob, error) { - job, err := r.jobOrmRepo.FindByID(ctx, id) - if err != nil { - r.logger.Error("Failed to get scraping job", map[string]interface{}{ - "job_id": id, - "error": err.Error(), - }) - return nil, err - } - - return job, nil -} - -// UpdateScrapingJob updates a scraping job -func (r *tenderRepository) UpdateScrapingJob(ctx context.Context, job *ScrapingJob) error { - job.UpdatedAt = time.Now().Unix() - - err := r.jobOrmRepo.Update(ctx, job) - if err != nil { - r.logger.Error("Failed to update scraping job", map[string]interface{}{ - "job_id": job.ID, - "error": err.Error(), - }) - return err - } - - r.logger.Info("Scraping job updated successfully", map[string]interface{}{ - "job_id": job.ID, - }) - - return nil -} - -// ListScrapingJobs retrieves scraping jobs with pagination -func (r *tenderRepository) ListScrapingJobs(ctx context.Context, limit, offset int) ([]ScrapingJob, int64, error) { - filter := bson.M{} - - pagination := mongopkg.Pagination{ - Limit: limit, - Skip: offset, - SortField: "started_at", - SortOrder: -1, - } - - result, err := r.jobOrmRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to list scraping jobs", map[string]interface{}{ - "error": err.Error(), - }) - return nil, 0, err - } - - return result.Items, result.TotalCount, nil -} - -// GetActiveScrapingJobs retrieves currently active scraping jobs -func (r *tenderRepository) GetActiveScrapingJobs(ctx context.Context) ([]ScrapingJob, error) { - filter := bson.M{ - "status": bson.M{ - "$in": []ScrapingJobStatus{ - ScrapingJobStatusPending, - ScrapingJobStatusRunning, - }, - }, - } - - pagination := mongopkg.Pagination{ - Limit: 100, - SortField: "started_at", - SortOrder: -1, - } - - result, err := r.jobOrmRepo.FindAll(ctx, filter, pagination) - if err != nil { - r.logger.Error("Failed to get active scraping jobs", map[string]interface{}{ - "error": err.Error(), - }) - return nil, err - } - - return result.Items, nil -} - // buildSearchFilter builds MongoDB filter from search criteria func (r *tenderRepository) buildSearchFilter(criteria TenderSearchCriteria) bson.M { filter := bson.M{} diff --git a/internal/tender/service.go b/internal/tender/service.go index 4b9c082..8c72d2e 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -3,33 +3,19 @@ package tender import ( "context" "fmt" - "sort" "time" "tm/internal/company" "tm/pkg/logger" - - "go.mongodb.org/mongo-driver/bson/primitive" + "tm/pkg/response" ) // TenderService interface defines tender business logic operations type TenderService interface { // Tender operations - CreateTender(ctx context.Context, req CreateTenderRequest) (*Tender, error) - GetTenderByID(ctx context.Context, id string) (*Tender, error) + GetTenderByID(ctx context.Context, id string) (*TenderResponse, error) UpdateTender(ctx context.Context, req UpdateTenderRequest) (*Tender, error) DeleteTender(ctx context.Context, id string) error ListTenders(ctx context.Context, req ListTendersRequest) (*ListTendersResponse, error) - SearchTenders(ctx context.Context, req SearchTendersRequest) (*SearchTendersResponse, error) - - // Statistics and reporting - GetTenderStatistics(ctx context.Context) (*TenderStatistics, error) - GetDashboardData(ctx context.Context) (*DashboardData, error) - - // Scraping management - StartScrapingJob(ctx context.Context, req StartScrapingJobRequest) (*ScrapingJob, error) - GetScrapingJobStatus(ctx context.Context, jobID string) (*ScrapingJob, error) - ListScrapingJobs(ctx context.Context, req ListScrapingJobsRequest) (*ListScrapingJobsResponse, error) - CancelScrapingJob(ctx context.Context, jobID string) error // Maintenance operations UpdateExpiredTenders(ctx context.Context) error @@ -52,92 +38,8 @@ func NewTenderService(repository TenderRepository, companyService company.Servic } } -// CreateTender creates a new tender -func (s *tenderService) CreateTender(ctx context.Context, req CreateTenderRequest) (*Tender, error) { - s.logger.Info("Creating new tender", map[string]interface{}{ - "notice_publication_id": req.NoticePublicationID, - "title": req.Title, - }) - - // Check if tender with same notice publication ID already exists - if req.NoticePublicationID != "" { - existing, err := s.repository.GetByNoticePublicationID(ctx, req.NoticePublicationID) - if err == nil && existing != nil { - s.logger.Warn("Tender with same notice publication ID already exists", map[string]interface{}{ - "notice_publication_id": req.NoticePublicationID, - "existing_id": existing.ID, - }) - return nil, fmt.Errorf("tender with notice publication ID %s already exists", req.NoticePublicationID) - } - } - - tender := &Tender{ - ContractNoticeID: req.ContractNoticeID, - NoticePublicationID: req.NoticePublicationID, - ContractFolderID: req.ContractFolderID, - NoticeTypeCode: req.NoticeTypeCode, - NoticeSubTypeCode: req.NoticeSubTypeCode, - NoticeLanguageCode: req.NoticeLanguageCode, - IssueDate: req.IssueDate, - IssueTime: req.IssueTime, - PublicationDate: req.PublicationDate, - GazetteID: req.GazetteID, - Title: req.Title, - Description: req.Description, - ProcurementTypeCode: req.ProcurementTypeCode, - ProcedureCode: req.ProcedureCode, - MainClassification: req.MainClassification, - AdditionalClassifications: req.AdditionalClassifications, - EstimatedValue: req.EstimatedValue, - Currency: req.Currency, - Duration: req.Duration, - DurationUnit: req.DurationUnit, - TenderDeadline: req.TenderDeadline, - PlaceOfPerformance: req.PlaceOfPerformance, - CountryCode: req.CountryCode, - RegionCode: req.RegionCode, - CityName: req.CityName, - PostalCode: req.PostalCode, - DocumentURI: req.DocumentURI, - TenderURL: req.TenderURL, - BuyerOrganization: req.BuyerOrganization, - ReviewOrganization: req.ReviewOrganization, - Organizations: req.Organizations, - SelectionCriteria: req.SelectionCriteria, - OfficialLanguages: req.OfficialLanguages, - Status: TenderStatusActive, - Source: req.Source, - SourceFileURL: req.SourceFileURL, - SourceFileName: req.SourceFileName, - ProcessingMetadata: ProcessingMetadata{ - ProcessedAt: time.Now().Unix(), - ProcessingVersion: "1.0", - }, - } - - // Generate tender URL if not provided - if tender.TenderURL == "" && tender.NoticePublicationID != "" { - tender.TenderURL = generateTenderURL(tender.NoticePublicationID) - } - - if err := s.repository.Create(ctx, tender); err != nil { - s.logger.Error("Failed to create tender", map[string]interface{}{ - "error": err.Error(), - "notice_publication_id": req.NoticePublicationID, - }) - return nil, fmt.Errorf("failed to create tender: %w", err) - } - - s.logger.Info("Tender created successfully", map[string]interface{}{ - "tender_id": tender.ID, - "notice_publication_id": tender.NoticePublicationID, - }) - - return tender, nil -} - // GetTenderByID retrieves a tender by ID -func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*Tender, error) { +func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*TenderResponse, error) { s.logger.Info("Retrieving tender by ID", map[string]interface{}{ "tender_id": id, }) @@ -151,7 +53,7 @@ func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*Tender, return nil, fmt.Errorf("failed to retrieve tender: %w", err) } - return tender, nil + return tender.ToTenderResponse(), nil } // UpdateTender updates an existing tender @@ -265,9 +167,11 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) return &ListTendersResponse{ Tenders: tenderResponses, - Total: total, - Limit: req.Limit, - Offset: req.Offset, + Metadata: &response.Meta{ + Total: int(total), + Limit: req.Limit, + Offset: req.Offset, + }, }, nil } @@ -286,9 +190,11 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) return &ListTendersResponse{ Tenders: tenderResponses, - Total: total, - Limit: req.Limit, - Offset: req.Offset, + Metadata: &response.Meta{ + Total: int(total), + Limit: req.Limit, + Offset: req.Offset, + }, }, nil } @@ -297,79 +203,24 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest) 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), + "total_tenders": len(tenders), }) + 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 -} - -// SearchTenders searches tenders based on criteria -func (s *tenderService) SearchTenders(ctx context.Context, req SearchTendersRequest) (*SearchTendersResponse, error) { - s.logger.Info("Searching tenders", map[string]interface{}{ - "query": req.Query, - }) - - tenders, err := s.repository.Search(ctx, req.Criteria) - if err != nil { - s.logger.Error("Failed to search tenders", map[string]interface{}{ - "error": err.Error(), - "query": req.Query, - }) - return nil, fmt.Errorf("failed to search tenders: %w", err) - } - - return &SearchTendersResponse{ - Tenders: tenders, - Query: req.Query, - Total: len(tenders), + Metadata: &response.Meta{ + Total: int(total), + Limit: req.Limit, + Offset: req.Offset, + }, }, nil } @@ -388,164 +239,6 @@ func (s *tenderService) GetTenderStatistics(ctx context.Context) (*TenderStatist return stats, nil } -// GetDashboardData retrieves dashboard data -func (s *tenderService) GetDashboardData(ctx context.Context) (*DashboardData, error) { - s.logger.Info("Retrieving dashboard data", map[string]interface{}{}) - - stats, err := s.GetTenderStatistics(ctx) - if err != nil { - return nil, err - } - - // Get recent tenders - recentTenders, _, err := s.repository.List(ctx, TenderSearchCriteria{}, 10, 0) - if err != nil { - s.logger.Error("Failed to retrieve recent tenders", map[string]interface{}{ - "error": err.Error(), - }) - return nil, fmt.Errorf("failed to retrieve recent tenders: %w", err) - } - - // Get active scraping jobs - activeJobs, err := s.repository.GetActiveScrapingJobs(ctx) - if err != nil { - s.logger.Error("Failed to retrieve active scraping jobs", map[string]interface{}{ - "error": err.Error(), - }) - return nil, fmt.Errorf("failed to retrieve active jobs: %w", err) - } - - return &DashboardData{ - Statistics: stats, - RecentTenders: recentTenders, - ActiveJobs: activeJobs, - }, nil -} - -// StartScrapingJob starts a new scraping job -func (s *tenderService) StartScrapingJob(ctx context.Context, req StartScrapingJobRequest) (*ScrapingJob, error) { - s.logger.Info("Starting scraping job", map[string]interface{}{ - "type": req.Type, - }) - - job := &ScrapingJob{ - ID: primitive.NewObjectID().Hex(), - Type: req.Type, - Status: ScrapingJobStatusPending, - StartedAt: time.Now().Unix(), - Progress: JobProgress{}, - Results: JobResults{}, - Config: req.Config, - } - - if err := s.repository.CreateScrapingJob(ctx, job); err != nil { - s.logger.Error("Failed to create scraping job", map[string]interface{}{ - "error": err.Error(), - "type": req.Type, - }) - return nil, fmt.Errorf("failed to create scraping job: %w", err) - } - - s.logger.Info("Scraping job created successfully", map[string]interface{}{ - "job_id": job.ID, - "type": job.Type, - }) - - return job, nil -} - -// GetScrapingJobStatus retrieves scraping job status -func (s *tenderService) GetScrapingJobStatus(ctx context.Context, jobID string) (*ScrapingJob, error) { - s.logger.Info("Retrieving scraping job status", map[string]interface{}{ - "job_id": jobID, - }) - - job, err := s.repository.GetScrapingJob(ctx, jobID) - if err != nil { - s.logger.Error("Failed to retrieve scraping job", map[string]interface{}{ - "job_id": jobID, - "error": err.Error(), - }) - return nil, fmt.Errorf("failed to retrieve scraping job: %w", err) - } - - return job, nil -} - -// ListScrapingJobs lists scraping jobs with pagination -func (s *tenderService) ListScrapingJobs(ctx context.Context, req ListScrapingJobsRequest) (*ListScrapingJobsResponse, error) { - s.logger.Info("Listing scraping jobs", map[string]interface{}{ - "limit": req.Limit, - "offset": req.Offset, - }) - - if req.Limit <= 0 { - req.Limit = 20 - } - if req.Limit > 100 { - req.Limit = 100 - } - - jobs, total, err := s.repository.ListScrapingJobs(ctx, req.Limit, req.Offset) - if err != nil { - s.logger.Error("Failed to list scraping jobs", map[string]interface{}{ - "error": err.Error(), - }) - return nil, fmt.Errorf("failed to list scraping jobs: %w", err) - } - - return &ListScrapingJobsResponse{ - Jobs: jobs, - Total: total, - Limit: req.Limit, - Offset: req.Offset, - }, nil -} - -// CancelScrapingJob cancels a running scraping job -func (s *tenderService) CancelScrapingJob(ctx context.Context, jobID string) error { - s.logger.Info("Cancelling scraping job", map[string]interface{}{ - "job_id": jobID, - }) - - job, err := s.repository.GetScrapingJob(ctx, jobID) - if err != nil { - s.logger.Error("Failed to find scraping job for cancellation", map[string]interface{}{ - "job_id": jobID, - "error": err.Error(), - }) - return fmt.Errorf("failed to find scraping job: %w", err) - } - - if job.Status != ScrapingJobStatusRunning && job.Status != ScrapingJobStatusPending { - s.logger.Warn("Cannot cancel scraping job in current status", map[string]interface{}{ - "job_id": jobID, - "status": job.Status, - }) - return fmt.Errorf("cannot cancel job in status: %s", job.Status) - } - - job.Status = ScrapingJobStatusCancelled - completedAt := time.Now().Unix() - job.CompletedAt = &completedAt - duration := completedAt - job.StartedAt - job.Duration = &duration - - if err := s.repository.UpdateScrapingJob(ctx, job); err != nil { - s.logger.Error("Failed to update cancelled scraping job", map[string]interface{}{ - "job_id": jobID, - "error": err.Error(), - }) - return fmt.Errorf("failed to update scraping job: %w", err) - } - - s.logger.Info("Scraping job cancelled successfully", map[string]interface{}{ - "job_id": jobID, - }) - - return nil -} - // UpdateExpiredTenders updates status of expired tenders func (s *tenderService) UpdateExpiredTenders(ctx context.Context) error { s.logger.Info("Updating expired tenders", map[string]interface{}{})