Implement TED Scraper and Configuration Management

- Introduced a new TED scraper in `cmd/scraper` to handle downloading and parsing TED XML files.
- Added configuration management for the scraper, including a new `Config` struct and YAML configuration file.
- Created necessary handler, service, and repository layers for tender management, adhering to Clean Architecture principles.
- Implemented comprehensive logging and error handling throughout the scraper functionality.
- Updated API routes to include tender management operations, enhancing the overall system capabilities.
This commit is contained in:
n.nakhostin
2025-08-13 16:52:24 +03:30
parent b7fa012d13
commit 96c21b8b78
16 changed files with 6970 additions and 7 deletions
+293
View File
@@ -0,0 +1,293 @@
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"`
}
// UpdateTenderRequest represents a request to update an existing tender
type UpdateTenderRequest struct {
ID string `json:"id" valid:"required~Tender ID is required"`
Title *string `json:"title,omitempty" valid:"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"`
Status *TenderStatus `json:"status,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"`
TenderDeadline *int64 `json:"tender_deadline,omitempty"` // Unix milliseconds
}
// ListTendersRequest represents a request to list tenders with pagination and filtering
type ListTendersRequest struct {
Criteria TenderSearchCriteria `json:"criteria"`
Limit int `json:"limit" valid:"range(1|100)~Limit must be between 1 and 100"`
Offset int `json:"offset" valid:"range(0|999999)~Offset must be non-negative"`
}
// ListTendersResponse represents the response for listing tenders
type ListTendersResponse struct {
Tenders []Tender `json:"tenders"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
// 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"`
}
// SearchTendersResponse represents the response for searching tenders
type SearchTendersResponse struct {
Tenders []Tender `json:"tenders"`
Query string `json:"query"`
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"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
// 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"`
}
// ToTenderResponse converts a Tender entity to TenderResponse
func (t *Tender) ToTenderResponse() *TenderResponse {
return &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,
}
}
// 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,
}
}