diff --git a/cmd/scraper/bootstrap/bootstrap.go b/cmd/scraper/bootstrap/bootstrap.go index c1fc2e5..dfa875f 100644 --- a/cmd/scraper/bootstrap/bootstrap.go +++ b/cmd/scraper/bootstrap/bootstrap.go @@ -1,140 +1,140 @@ package bootstrap -import ( - "context" - "fmt" - "os" - "os/signal" - "syscall" - "time" - "tm/internal/tender" - "tm/pkg/config" - "tm/pkg/logger" - "tm/pkg/mongo" - "tm/ted" -) +// import ( +// "context" +// "fmt" +// "os" +// "os/signal" +// "syscall" +// "time" +// "tm/internal/tender" +// "tm/pkg/config" +// "tm/pkg/logger" +// "tm/pkg/mongo" +// "tm/ted" +// ) -// Init Application Configuration -func InitConfig() (*Config, error) { - conf, err := config.LoadConfig(".", &Config{}) - if err != nil { - return nil, fmt.Errorf("failed to load config: %v", err) - } +// // Init Application Configuration +// func InitConfig() (*Config, error) { +// conf, err := config.LoadConfig(".", &Config{}) +// if err != nil { +// return nil, fmt.Errorf("failed to load config: %v", err) +// } - return conf, nil -} +// return conf, nil +// } -// Init Logger Service -func InitLogger(conf config.LoggingConfig) logger.Logger { - return logger.NewLogger(&logger.Config{ - Level: conf.Level, - Format: conf.Format, - Output: conf.Output, - File: logger.FileConfig{ - Path: conf.File.Path, - MaxSize: conf.File.MaxSize, - MaxBackups: conf.File.MaxBackups, - MaxAge: conf.File.MaxAge, - Compress: conf.File.Compress, - }, - }) -} +// // Init Logger Service +// func InitLogger(conf config.LoggingConfig) logger.Logger { +// return logger.NewLogger(&logger.Config{ +// Level: conf.Level, +// Format: conf.Format, +// Output: conf.Output, +// File: logger.FileConfig{ +// Path: conf.File.Path, +// MaxSize: conf.File.MaxSize, +// MaxBackups: conf.File.MaxBackups, +// MaxAge: conf.File.MaxAge, +// Compress: conf.File.Compress, +// }, +// }) +// } -// Init MongoDB Connection Manager -func InitMongoDB(conf config.MongoConfig, log logger.Logger) *mongo.ConnectionManager { - // Convert infra.MongoConfig to mongo.ConnectionConfig - connectionConfig := mongo.ConnectionConfig{ - URI: conf.URI, - Database: conf.Name, - MaxPoolSize: uint64(conf.MaxPoolSize), - MinPoolSize: 5, // Default minimum pool size - MaxConnIdleTime: 30 * time.Minute, // Default idle time - ConnectTimeout: conf.Timeout, - ServerSelectionTimeout: 5 * time.Second, // Default server selection timeout - } +// // Init MongoDB Connection Manager +// func InitMongoDB(conf config.MongoConfig, log logger.Logger) *mongo.ConnectionManager { +// // Convert infra.MongoConfig to mongo.ConnectionConfig +// connectionConfig := mongo.ConnectionConfig{ +// URI: conf.URI, +// Database: conf.Name, +// MaxPoolSize: uint64(conf.MaxPoolSize), +// MinPoolSize: 5, // Default minimum pool size +// MaxConnIdleTime: 30 * time.Minute, // Default idle time +// ConnectTimeout: conf.Timeout, +// ServerSelectionTimeout: 5 * time.Second, // Default server selection timeout +// } - // Create MongoDB connection manager - connectionManager, err := mongo.NewConnectionManager(connectionConfig, log) - if err != nil { - log.Error("Failed to initialize MongoDB connection", map[string]interface{}{ - "error": err.Error(), - "uri": conf.URI, - "db": conf.Name, - }) - panic(fmt.Sprintf("Failed to initialize MongoDB connection: %v", err)) - } +// // Create MongoDB connection manager +// connectionManager, err := mongo.NewConnectionManager(connectionConfig, log) +// if err != nil { +// log.Error("Failed to initialize MongoDB connection", map[string]interface{}{ +// "error": err.Error(), +// "uri": conf.URI, +// "db": conf.Name, +// }) +// panic(fmt.Sprintf("Failed to initialize MongoDB connection: %v", err)) +// } - log.Info("MongoDB connection manager initialized successfully", map[string]interface{}{ - "database": conf.Name, - "uri": conf.URI, - "pool_size": conf.MaxPoolSize, - }) +// log.Info("MongoDB connection manager initialized successfully", map[string]interface{}{ +// "database": conf.Name, +// "uri": conf.URI, +// "pool_size": conf.MaxPoolSize, +// }) - return connectionManager -} +// return connectionManager +// } -// init TED scraper -func InitTEDScraper(config Config, mongoManager *mongo.ConnectionManager, appLogger logger.Logger) { - // Initialize tender repository - tenderRepo := tender.NewRepository(mongoManager, appLogger) +// // init TED scraper +// func InitTEDScraper(config Config, mongoManager *mongo.ConnectionManager, appLogger logger.Logger) { +// // Initialize tender repository +// tenderRepo := tender.NewRepository(mongoManager, appLogger) - // Initialize TED scraper - tedScraper := ted.NewTEDScraper( - &ted.Config{ - BaseURL: config.TED.BaseURL, - Timeout: config.TED.Timeout, - MaxRetries: config.TED.MaxRetries, - RetryDelay: config.TED.RetryDelay, - UserAgent: config.TED.UserAgent, - MaxConcurrency: config.TED.MaxConcurrency, - DownloadDir: config.TED.DownloadDir, - CleanupAfter: config.TED.CleanupAfter, - ScrapingInterval: config.TED.ScrapingInterval, - }, - appLogger, - mongoManager, - tenderRepo, - ) +// // Initialize TED scraper +// tedScraper := ted.NewTEDScraper( +// &ted.Config{ +// BaseURL: config.TED.BaseURL, +// Timeout: config.TED.Timeout, +// MaxRetries: config.TED.MaxRetries, +// RetryDelay: config.TED.RetryDelay, +// UserAgent: config.TED.UserAgent, +// MaxConcurrency: config.TED.MaxConcurrency, +// DownloadDir: config.TED.DownloadDir, +// CleanupAfter: config.TED.CleanupAfter, +// ScrapingInterval: config.TED.ScrapingInterval, +// }, +// appLogger, +// mongoManager, +// tenderRepo, +// ) - // Create context with cancellation - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() +// // Create context with cancellation +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() - // Set up signal handling for graceful shutdown - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) +// // Set up signal handling for graceful shutdown +// signalChan := make(chan os.Signal, 1) +// signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) - // Start the scraper in a goroutine - scraperDone := make(chan error, 1) - go func() { - appLogger.Info("Starting TED scraper", map[string]interface{}{ - "interval": config.TED.ScrapingInterval.String(), - }) - scraperDone <- tedScraper.RunPeriodicScraping(ctx) - }() +// // Start the scraper in a goroutine +// scraperDone := make(chan error, 1) +// go func() { +// appLogger.Info("Starting TED scraper", map[string]interface{}{ +// "interval": config.TED.ScrapingInterval.String(), +// }) +// scraperDone <- tedScraper.RunPeriodicScraping(ctx) +// }() - // Wait for shutdown signal or scraper completion - select { - case <-signalChan: - appLogger.Info("Received shutdown signal, stopping scraper...", map[string]interface{}{}) - cancel() +// // Wait for shutdown signal or scraper completion +// select { +// case <-signalChan: +// appLogger.Info("Received shutdown signal, stopping scraper...", map[string]interface{}{}) +// cancel() - // Wait for scraper to finish gracefully with timeout - shutdownTimeout := time.NewTimer(30 * time.Second) - select { - case <-scraperDone: - appLogger.Info("Scraper stopped gracefully", map[string]interface{}{}) - case <-shutdownTimeout.C: - appLogger.Warn("Scraper shutdown timeout, forcing exit", map[string]interface{}{}) - } +// // Wait for scraper to finish gracefully with timeout +// shutdownTimeout := time.NewTimer(30 * time.Second) +// select { +// case <-scraperDone: +// appLogger.Info("Scraper stopped gracefully", map[string]interface{}{}) +// case <-shutdownTimeout.C: +// appLogger.Warn("Scraper shutdown timeout, forcing exit", map[string]interface{}{}) +// } - case err := <-scraperDone: - if err != nil { - appLogger.Error("Scraper finished with error", map[string]interface{}{ - "error": err.Error(), - }) - os.Exit(1) - } - appLogger.Info("Scraper finished successfully", map[string]interface{}{}) - } -} +// case err := <-scraperDone: +// if err != nil { +// appLogger.Error("Scraper finished with error", map[string]interface{}{ +// "error": err.Error(), +// }) +// os.Exit(1) +// } +// appLogger.Info("Scraper finished successfully", map[string]interface{}{}) +// } +// } diff --git a/cmd/scraper/main.go b/cmd/scraper/main.go index ecf08bf..e8bb8ba 100644 --- a/cmd/scraper/main.go +++ b/cmd/scraper/main.go @@ -1,37 +1,31 @@ package main -import ( - "context" - "time" - "tm/cmd/scraper/bootstrap" -) - func main() { - // Load configuration - config, err := bootstrap.InitConfig() - if err != nil { - panic(err) - } + // // Load configuration + // config, err := bootstrap.InitConfig() + // if err != nil { + // panic(err) + // } - // Initialize logger - appLogger := bootstrap.InitLogger(config.Logging) - appLogger.Info("Starting TED scraper application", map[string]interface{}{ - "version": "1.0.0", - }) + // // Initialize logger + // appLogger := bootstrap.InitLogger(config.Logging) + // appLogger.Info("Starting TED scraper application", map[string]interface{}{ + // "version": "1.0.0", + // }) - // Initialize MongoDB connection - mongoManager := bootstrap.InitMongoDB(config.Database.MongoDB, appLogger) - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - if err := mongoManager.Close(ctx); err != nil { - appLogger.Error("Failed to close MongoDB connection", map[string]interface{}{ - "error": err.Error(), - }) - } - }() + // // Initialize MongoDB connection + // mongoManager := bootstrap.InitMongoDB(config.Database.MongoDB, appLogger) + // defer func() { + // ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + // defer cancel() + // if err := mongoManager.Close(ctx); err != nil { + // appLogger.Error("Failed to close MongoDB connection", map[string]interface{}{ + // "error": err.Error(), + // }) + // } + // }() - bootstrap.InitTEDScraper(*config, mongoManager, appLogger) + // bootstrap.InitTEDScraper(*config, mongoManager, appLogger) - appLogger.Info("TED scraper application stopped", map[string]interface{}{}) + // appLogger.Info("TED scraper application stopped", map[string]interface{}{}) } diff --git a/ted/README.md b/ted/README.md deleted file mode 100644 index 7b5383b..0000000 --- a/ted/README.md +++ /dev/null @@ -1,396 +0,0 @@ -# TED XML Parser - Complete Model - -This package provides comprehensive Go structures for parsing all TED (Tenders Electronic Daily) XML documents, supporting the complete range of eForms notice types and legacy TED XML formats. - -## ๐Ÿš€ Features - -- โœ… **Complete TED XML Support**: All notice subtypes from eForms SDK 1.3 to 1.13 -- โœ… **Automatic Document Detection**: Intelligent parsing with type detection -- โœ… **Full Field Coverage**: Comprehensive mapping of all TED XML fields -- โœ… **eForms Compliance**: Full support for eForms extensions and organizations -- โœ… **Legacy Compatibility**: Backward compatibility with old TED XML formats -- โœ… **Validation & Utility Methods**: Built-in validation and data extraction helpers -- โœ… **Award Results**: Complete lot tender and award information -- โœ… **Multi-language Support**: Proper handling of multilingual content -- โœ… **High Performance**: Optimized parsing with 50MB file support - -## ๐Ÿ“‹ Supported Document Types - -### Core Notice Types -- **ContractNotice** - Standard tender notices for procurement opportunities -- **ContractAwardNotice** - Award notices announcing procurement results -- **PriorInformationNotice** - Early market engagement and planning notices -- **ResultNotice** - eForms-specific result notices - -### Specialized Notice Types -- **DesignContest** - Design competition notices with jury and reward information -- **QualificationSystemNotice** - Qualification system establishment notices -- **ConcessionNotice** - Concession contract notices with revenue terms -- **PlanningNotice** - Procurement planning and budget notices -- **CompetitionNotice** - Competition procedure notices -- **ChangeNotice** - Notice modifications and corrections (eForms) -- **SubcontractNotice** - Subcontracting opportunity notices - -### Legacy Document Types -- **Corrigendum** - Corrections (parsed as ContractNotice) -- **ModificationNotice** - Modifications (parsed as ContractNotice) - -## ๐Ÿ—๏ธ Complete Data Model - -### Business Groups Covered - -#### **BG-1: Notice** -- Complete notice metadata (ID, version, dates, language) -- Notice type codes and regulatory domains -- Publication information and gazette references -- UBL extensions with eForms data - -#### **BG-2: Competition** -- Procedure codes and submission methods -- Tender submission deadlines and periods -- Open tender events and auction terms -- Process justification and government agreements - -#### **BG-3: Tender** -- Tender reference and identification -- Tender variants and ranking information -- Legal monetary totals and payable amounts -- Subcontracting terms and descriptions - -#### **BG-4: Lot** -- Lot identification and titles -- Procurement project details per lot -- Tendering terms and processes per lot -- Award criteria and selection requirements - -#### **BG-5: Part** -- Part identification and classification -- Place of performance and location details -- Contract duration and planned periods -- Additional commodity classifications - -#### **BG-6: Contracting Entity** -- Contracting party identification -- Buyer profile URIs and contact information -- Contracting party types and activities -- Service provider party details - -#### **BG-7: Procedure** -- Procurement procedures and frameworks -- Procedure identifiers (UUID v4) -- Framework agreement terms -- Cross-border procurement indicators - -#### **BG-8: Organization** -- Complete organization details and contacts -- Party identification and legal entities -- Postal addresses with country codes -- Natural person indicators - -#### **BG-9: Document** -- Document references and attachments -- External references with URIs and hashes -- Document types and status codes -- Validity periods and language specifications - -#### **BG-10: Lot Result** -- Tender result codes and award decisions -- Received submission statistics -- Winner identification and tender details -- Contract references and settlement information - -#### **BG-11: Winner** -- Winning tenderer identification -- Winner organization details -- SME indicator and natural person flags -- Country of origin information - -#### **BG-12: Settled Contract** -- Contract identification and titles -- Issue dates and signatory parties -- Contract values and currency information -- Performance period details - -#### **BG-13: Subcontracting** -- Subcontracting terms and percentages -- Subcontractor identification -- Value thresholds and descriptions -- Unknown price indicators - -### Extended Structures - -#### **Design Contest Specific** -- **ContestDesignJury**: Jury decisions and member information -- **ContestDesignReward**: Reward structures and benefit values -- **JudgingCriteria**: Evaluation criteria and descriptions - -#### **Qualification System Specific** -- **QualificationApplication**: Application procedures and periods -- **TendererQualificationRequest**: Qualification requirements -- **ApplicationPeriod**: Application start/end dates and times - -#### **Concession Specific** -- **ConcessionRevenue**: Revenue types and amounts -- **ConcessionTerm**: Concession values and duration -- **FrameworkAgreement**: Framework terms and participants - -#### **Planning Specific** -- **BudgetAccountLine**: Budget codes and amounts -- **BudgetAccount**: Account classification schemes -- **PlannedPeriod**: Planning timeline information - -#### **Award Results Enhanced** -- **EnhancedTenderResult**: Comprehensive award information -- **AwardedTenderedProject**: Project award details -- **WinningTenderLot**: Lot-specific winners -- **WinningTender**: Individual tender results -- **ReceivedSubmissionsStatistics**: Submission counts and statistics - -## ๐Ÿ”ง Usage Examples - -### Basic Document Parsing - -```go -package main - -import ( - "fmt" - "io/ioutil" - "log" - - "tm/ted" -) - -func main() { - // Create comprehensive TED parser - parser := ted.NewTEDParser() - - // Read any TED XML file - xmlData, err := ioutil.ReadFile("ted_document.xml") - if err != nil { - log.Fatal(err) - } - - // Automatic detection and parsing - parsedDoc, err := parser.ParseXML(xmlData) - if err != nil { - log.Fatal(err) - } - - // Get document information - document := parsedDoc.GetDocument() - fmt.Printf("Document Type: %s\n", parsedDoc.Type) - fmt.Printf("Document ID: %s\n", document.GetID()) - fmt.Printf("Notice Type: %s\n", document.GetNoticeType()) - fmt.Printf("Language: %s\n", document.GetLanguage()) - - // Validate document - if err := document.Validate(); err != nil { - log.Printf("Validation error: %v", err) - } -} -``` - -### Advanced Usage by Document Type - -```go -// Handle specific document types -switch parsedDoc.Type { -case ted.DocumentTypeContractNotice: - cn := parsedDoc.ContractNotice - fmt.Printf("Tender Deadline: %s\n", cn.GetTenderDeadline()) - amount, currency := cn.GetBudget() - fmt.Printf("Estimated Value: %s %s\n", amount, currency) - -case ted.DocumentTypeContractAwardNotice: - can := parsedDoc.ContractAwardNotice - fmt.Printf("Award Date: %s\n", can.GetAwardDate()) - amount, currency := can.GetTotalAwardValue() - fmt.Printf("Total Award: %s %s\n", amount, currency) - -case ted.DocumentTypePriorInformationNotice: - pin := parsedDoc.PriorInformationNotice - fmt.Printf("Prior Information ID: %s\n", pin.GetID()) - -case ted.DocumentTypeDesignContest: - dc := parsedDoc.DesignContest - fmt.Printf("Design Contest ID: %s\n", dc.GetID()) - if dc.ContestDesignJury != nil { - fmt.Printf("Jury Decision: %s\n", dc.ContestDesignJury.JuryDecision) - } - -case ted.DocumentTypeQualificationSystemNotice: - qsn := parsedDoc.QualificationSystemNotice - fmt.Printf("Qualification System ID: %s\n", qsn.GetID()) - -case ted.DocumentTypeConcessionNotice: - cn := parsedDoc.ConcessionNotice - fmt.Printf("Concession ID: %s\n", cn.GetID()) - if cn.ConcessionRevenue != nil { - fmt.Printf("Revenue Type: %s\n", cn.ConcessionRevenue.RevenueType) - } - -case ted.DocumentTypePlanningNotice: - pn := parsedDoc.PlanningNotice - fmt.Printf("Planning ID: %s\n", pn.GetID()) - fmt.Printf("Budget Lines: %d\n", len(pn.BudgetAccountLine)) - -case ted.DocumentTypeResultNotice: - rn := parsedDoc.ResultNotice - fmt.Printf("Result ID: %s\n", rn.GetID()) - fmt.Printf("Tender Results: %d\n", len(rn.TenderResult)) - -case ted.DocumentTypeChangeNotice: - cn := parsedDoc.ChangeNotice - fmt.Printf("Change ID: %s\n", cn.GetID()) - fmt.Printf("Change Reasons: %d\n", len(cn.ChangeReason)) - -case ted.DocumentTypeSubcontractNotice: - sn := parsedDoc.SubcontractNotice - fmt.Printf("Subcontract ID: %s\n", sn.GetID()) - fmt.Printf("Subcontract Calls: %d\n", len(sn.SubcontractCall)) -} -``` - -### Working with Organizations and Extensions - -```go -// Extract organization information (works for all notice types) -if doc := parsedDoc.GetDocument(); doc != nil { - // Type assertion for specific methods - if notice, ok := doc.(*ted.ContractNotice); ok { - orgs := notice.GetOrganizations() - if orgs != nil { - fmt.Printf("Organizations: %d\n", len(orgs.Organization)) - for _, org := range orgs.Organization { - if org.Company != nil && org.Company.PartyName != nil { - fmt.Printf("- %s\n", org.Company.PartyName.Name) - } - } - } - - // Get publication information - pub := notice.GetPublicationInfo() - if pub != nil { - fmt.Printf("Publication ID: %s\n", pub.NoticePublicationID) - fmt.Printf("OJS ID: %s\n", pub.GazetteID) - fmt.Printf("Publication Date: %s\n", pub.PublicationDate) - } - } -} -``` - -## ๐Ÿ” Document Type Detection - -The parser includes intelligent document type detection: - -```go -// Detect document type without parsing -docType, err := parser.DetectDocumentType(xmlData) -if err != nil { - log.Fatal(err) -} - -fmt.Printf("Detected document type: %s\n", docType) - -// Parse specific type if known -switch docType { -case ted.DocumentTypeContractNotice: - notice, err := parser.ParseXMLAsContractNotice(xmlData) -case ted.DocumentTypePriorInformationNotice: - notice, err := parser.ParseXMLAsPriorInformationNotice(xmlData) -case ted.DocumentTypeDesignContest: - notice, err := parser.ParseXMLAsDesignContest(xmlData) -// ... other specific parsers available -} -``` - -## โœ… Comprehensive Validation - -All notice types include thorough validation: - -```go -// Validate any notice type -err := notice.Validate() -if err != nil { - log.Printf("Validation failed: %v", err) -} -``` - -### Validation Coverage -- Required fields (ID, ContractFolderID, IssueDate) -- Date format validation (multiple ISO 8601 formats) -- Notice type code validation -- Language code validation (3-letter ISO codes) -- Currency code validation (3-letter ISO codes) -- Duration unit code validation -- Business rule validation per notice type - -## ๐ŸŽฏ Complete Field Coverage - -### Core Metadata Fields -- UBLVersionID, CustomizationID, ID, ContractFolderID -- IssueDate, IssueTime, VersionID, RegulatoryDomain -- NoticeTypeCode, NoticeLanguageCode -- All namespace declarations and XML attributes - -### Business Information -- Contracting party details and buyer profiles -- Tendering terms, processes, and submission requirements -- Procurement projects with lots and parts -- Award criteria and selection requirements -- Legal monetary totals with currency support -- Duration measures with unit codes - -### eForms Extensions -- Complete eForms extension support -- Organizations with party identification -- Publication information and gazette details -- Notice results with lot awards -- Selection criteria and official languages -- Award criterion parameters - -### Enhanced Structures -- Subcontracting terms with enhanced details -- Framework agreements and parent contracts -- Document references with validity periods -- Address information with country codes -- Contact details and electronic communication - -## ๐Ÿงช Tested Compatibility - -This comprehensive parser has been tested with: -- โœ… All eForms SDK versions 1.3 to 1.13 -- โœ… Legacy TED XML formats (R2.0.8, R2.0.9) -- โœ… Multiple notice subtypes (20+ tested) -- โœ… Complex organization structures (10+ organizations) -- โœ… Large procurement lots (50+ lots tested) -- โœ… Multiple currencies and value formats -- โœ… Multilingual content and character encoding -- โœ… Large XML files (up to 50MB) -- โœ… Real TED database documents - -## ๐Ÿ“Š Performance Characteristics - -- **Memory Efficient**: Streaming XML parsing with configurable limits -- **Fast Detection**: String-based detection with XML fallback -- **Scalable**: Handles large files and complex structures -- **Concurrent Safe**: Thread-safe parser instances -- **Error Resilient**: Graceful handling of malformed or incomplete XML - -## ๐Ÿ”— Dependencies - -- Go 1.19+ -- Standard library: `encoding/xml`, `time`, `fmt` -- Custom XML parser: `tm/pkg/xmlparser` - -## ๐Ÿ“ˆ Future Extensions - -This model is designed to be forward-compatible with: -- New eForms SDK versions -- Additional notice subtypes -- Extended business groups -- Enhanced validation rules -- Performance optimizations - -The comprehensive nature of this TED XML model ensures complete coverage of all procurement notice types and supports the full lifecycle of European public procurement processes. \ No newline at end of file diff --git a/ted/parser.go b/ted/parser.go deleted file mode 100644 index c3c7d37..0000000 --- a/ted/parser.go +++ /dev/null @@ -1,436 +0,0 @@ -package ted - -import ( - "bytes" - "encoding/xml" - "fmt" - "strings" - "tm/pkg/xmlparser" -) - -// TEDParser handles parsing of TED XML files -type TEDParser struct { - contractNoticeParser xmlparser.Parser[ContractNotice] - contractAwardNoticeParser xmlparser.Parser[ContractAwardNotice] - priorInformationNoticeParser xmlparser.Parser[PriorInformationNotice] - designContestParser xmlparser.Parser[DesignContest] - qualificationSystemNoticeParser xmlparser.Parser[QualificationSystemNotice] - concessionNoticeParser xmlparser.Parser[ConcessionNotice] - planningNoticeParser xmlparser.Parser[PlanningNotice] - competitionNoticeParser xmlparser.Parser[CompetitionNotice] - resultNoticeParser xmlparser.Parser[ResultNotice] - changeNoticeParser xmlparser.Parser[ChangeNotice] - subcontractNoticeParser xmlparser.Parser[SubcontractNotice] -} - -// NewTEDParser creates a new TED parser -func NewTEDParser() *TEDParser { - options := xmlparser.DefaultParserOptions() - options.MaxSize = 50 * 1024 * 1024 // 50MB limit for TED XML files - options.IgnoreUnknownElements = true // TED XML has many optional fields - - return &TEDParser{ - contractNoticeParser: xmlparser.NewParser[ContractNotice](options), - contractAwardNoticeParser: xmlparser.NewParser[ContractAwardNotice](options), - priorInformationNoticeParser: xmlparser.NewParser[PriorInformationNotice](options), - designContestParser: xmlparser.NewParser[DesignContest](options), - qualificationSystemNoticeParser: xmlparser.NewParser[QualificationSystemNotice](options), - concessionNoticeParser: xmlparser.NewParser[ConcessionNotice](options), - planningNoticeParser: xmlparser.NewParser[PlanningNotice](options), - competitionNoticeParser: xmlparser.NewParser[CompetitionNotice](options), - resultNoticeParser: xmlparser.NewParser[ResultNotice](options), - changeNoticeParser: xmlparser.NewParser[ChangeNotice](options), - subcontractNoticeParser: xmlparser.NewParser[SubcontractNotice](options), - } -} - -// DetectDocumentType detects the type of TED document from XML data -func (p *TEDParser) DetectDocumentType(xmlData []byte) (DocumentType, error) { - // Fast string-based detection for common patterns - xmlStr := string(xmlData) - - if strings.Contains(xmlStr, "", "", 1) - - return []byte(xmlStr) -} - -// Legacy methods for backward compatibility -// ParseXMLAsContractNotice parses XML specifically as ContractNotice (legacy method) -func (p *TEDParser) ParseXMLAsContractNotice(xmlData []byte) (*ContractNotice, error) { - return p.contractNoticeParser.ParseBytes(xmlData) -} - -// ParseXMLAsContractAwardNotice parses XML specifically as ContractAwardNotice -func (p *TEDParser) ParseXMLAsContractAwardNotice(xmlData []byte) (*ContractAwardNotice, error) { - return p.contractAwardNoticeParser.ParseBytes(xmlData) -} - -// Specific parser methods for all notice types -func (p *TEDParser) ParseXMLAsPriorInformationNotice(xmlData []byte) (*PriorInformationNotice, error) { - return p.priorInformationNoticeParser.ParseBytes(xmlData) -} - -func (p *TEDParser) ParseXMLAsDesignContest(xmlData []byte) (*DesignContest, error) { - return p.designContestParser.ParseBytes(xmlData) -} - -func (p *TEDParser) ParseXMLAsQualificationSystemNotice(xmlData []byte) (*QualificationSystemNotice, error) { - return p.qualificationSystemNoticeParser.ParseBytes(xmlData) -} - -func (p *TEDParser) ParseXMLAsConcessionNotice(xmlData []byte) (*ConcessionNotice, error) { - return p.concessionNoticeParser.ParseBytes(xmlData) -} - -func (p *TEDParser) ParseXMLAsPlanningNotice(xmlData []byte) (*PlanningNotice, error) { - return p.planningNoticeParser.ParseBytes(xmlData) -} - -func (p *TEDParser) ParseXMLAsCompetitionNotice(xmlData []byte) (*CompetitionNotice, error) { - return p.competitionNoticeParser.ParseBytes(xmlData) -} - -func (p *TEDParser) ParseXMLAsResultNotice(xmlData []byte) (*ResultNotice, error) { - return p.resultNoticeParser.ParseBytes(xmlData) -} - -func (p *TEDParser) ParseXMLAsChangeNotice(xmlData []byte) (*ChangeNotice, error) { - return p.changeNoticeParser.ParseBytes(xmlData) -} - -func (p *TEDParser) ParseXMLAsSubcontractNotice(xmlData []byte) (*SubcontractNotice, error) { - return p.subcontractNoticeParser.ParseBytes(xmlData) -} diff --git a/ted/scraper.go b/ted/scraper.go deleted file mode 100644 index bb01e7f..0000000 --- a/ted/scraper.go +++ /dev/null @@ -1,2067 +0,0 @@ -package ted - -import ( - "archive/tar" - "compress/gzip" - "context" - "fmt" - "io" - "net/http" - "path/filepath" - "strconv" - "strings" - "time" - "tm/internal/tender" - "tm/pkg/logger" - "tm/pkg/mongo" - - "go.mongodb.org/mongo-driver/v2/bson" - mongoDriver "go.mongodb.org/mongo-driver/v2/mongo" - "go.mongodb.org/mongo-driver/v2/mongo/options" -) - -// TEDScraper handles downloading and parsing TED XML files -type TEDScraper struct { - config *Config - logger logger.Logger - httpClient *http.Client - xmlParser *TEDParser - mongoManager *mongo.ConnectionManager - tenderRepo tender.TenderRepository -} - -// ScrapingJob represents a scraping job (simplified version) -type ScrapingJob struct { - mongo.Model - Type string `bson:"type" json:"type"` - Status string `bson:"status" json:"status"` - StartedAt int64 `bson:"started_at" json:"started_at"` - CompletedAt *int64 `bson:"completed_at,omitempty" json:"completed_at,omitempty"` - Progress JobProgress `bson:"progress" json:"progress"` - Results JobResults `bson:"results" json:"results"` - Config map[string]interface{} `bson:"config,omitempty" json:"config,omitempty"` -} - -// JobProgress represents the progress of a scraping job -type JobProgress struct { - TotalFiles int `bson:"total_files" json:"total_files"` - ProcessedFiles int `bson:"processed_files" json:"processed_files"` - FailedFiles int `bson:"failed_files" json:"failed_files"` - PercentComplete float64 `bson:"percent_complete" json:"percent_complete"` - CurrentFile string `bson:"current_file,omitempty" json:"current_file,omitempty"` -} - -// JobResults represents the results of a scraping job -type JobResults struct { - TotalTenders int `bson:"total_tenders" json:"total_tenders"` - NewTenders int `bson:"new_tenders" json:"new_tenders"` - UpdatedTenders int `bson:"updated_tenders" json:"updated_tenders"` - ErrorCount int `bson:"error_count" json:"error_count"` - SuccessRate float64 `bson:"success_rate" json:"success_rate"` - ProcessingTime int64 `bson:"processing_time" json:"processing_time"` -} - -// ScrapingState represents the current state of scraping -type ScrapingState struct { - mongo.Model - 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"` - NextRunAt int64 `bson:"next_run_at" json:"next_run_at"` - IsRunning bool `bson:"is_running" json:"is_running"` - CurrentJobID bson.ObjectID `bson:"current_job_id,omitempty" json:"current_job_id,omitempty"` -} - -// ScraperConfig holds configuration for the TED scraper -type Config struct { - BaseURL string `mapstructure:"base_url"` - Timeout time.Duration `mapstructure:"timeout"` - MaxRetries int `mapstructure:"max_retries"` - RetryDelay time.Duration `mapstructure:"retry_delay"` - UserAgent string `mapstructure:"user_agent"` - MaxConcurrency int `mapstructure:"max_concurrency"` - DownloadDir string `mapstructure:"download_dir"` - CleanupAfter time.Duration `mapstructure:"cleanup_after"` - ScrapingInterval time.Duration `mapstructure:"scraping_interval"` -} - -// ScrapingMode represents the type of scraping operation -type ScrapingMode string - -const ( - ScrapingModeDaily ScrapingMode = "daily" // Daily bulk fetch mode - ScrapingModeManual ScrapingMode = "manual" // Manual date range mode - ScrapingModeRealtime ScrapingMode = "realtime" // Real-time processing mode -) - -// DateRangeConfig holds configuration for manual date range scraping -type DateRangeConfig struct { - StartDate time.Time `json:"start_date"` - EndDate time.Time `json:"end_date"` - Mode ScrapingMode `json:"mode"` -} - -// NewTEDScraper creates a new TED scraper instance -func NewTEDScraper( - config *Config, - logger logger.Logger, - mongoManager *mongo.ConnectionManager, - tenderRepo tender.TenderRepository, -) *TEDScraper { - httpClient := &http.Client{ - Timeout: config.Timeout, - } - - return &TEDScraper{ - config: config, - logger: logger, - httpClient: httpClient, - xmlParser: NewTEDParser(), - mongoManager: mongoManager, - tenderRepo: tenderRepo, - } -} - -// RunPeriodicScraping runs the scraper at configured intervals -func (s *TEDScraper) RunPeriodicScraping(ctx context.Context) error { - ticker := time.NewTicker(s.config.ScrapingInterval) - defer ticker.Stop() - - s.logger.Info("Starting periodic TED XML scraping", map[string]interface{}{ - "interval": s.config.ScrapingInterval.String(), - "base_url": s.config.BaseURL, - }) - - // Run initial scraping - if err := s.ScrapeLatest(ctx); err != nil { - s.logger.Error("Initial scraping failed", map[string]interface{}{ - "error": err.Error(), - }) - } - - for { - select { - case <-ctx.Done(): - s.logger.Info("Periodic scraping stopped", map[string]interface{}{ - "reason": ctx.Err().Error(), - }) - return ctx.Err() - case <-ticker.C: - if err := s.ScrapeLatest(ctx); err != nil { - s.logger.Error("Periodic scraping failed", map[string]interface{}{ - "error": err.Error(), - }) - } - } - } -} - -// ScrapeLatest scrapes the latest TED XML files -func (s *TEDScraper) ScrapeLatest(ctx context.Context) error { - startTime := time.Now() - - s.logger.Info("Starting TED XML scraping", map[string]interface{}{ - "timestamp": startTime.Unix(), - }) - - // Get current state - state, err := s.getScrapingState(ctx) - if err != nil { - s.logger.Error("Failed to get scraping state", map[string]interface{}{ - "error": err.Error(), - }) - // Initialize with current date if no state exists - state = &ScrapingState{ - LastProcessedYear: time.Now().Year(), - LastProcessedNumber: 0, - LastRunAt: startTime.Unix(), - NextRunAt: startTime.Add(s.config.ScrapingInterval).Unix(), - IsRunning: false, - } - } - - // Handle case where state is nil (no document found) - if state == nil { - s.logger.Info("No scraping state found, initializing new state", map[string]interface{}{}) - state = &ScrapingState{ - LastProcessedYear: time.Now().Year(), - LastProcessedNumber: 0, - LastRunAt: startTime.Unix(), - NextRunAt: startTime.Add(s.config.ScrapingInterval).Unix(), - IsRunning: false, - } - } - - if state.IsRunning { - s.logger.Info("Scraping already running, skipping", map[string]interface{}{ - "current_job_id": state.CurrentJobID, - }) - return nil - } - - // Create scraping job - job := &ScrapingJob{ - Type: "ted_daily", - Status: "running", - StartedAt: startTime.Unix(), - Progress: JobProgress{}, - Results: JobResults{}, - Config: map[string]interface{}{ - "base_url": s.config.BaseURL, - "max_retries": s.config.MaxRetries, - "scraper_type": "ted_daily", - }, - } - - // Save job - if err := s.saveScrapingJob(ctx, job); err != nil { - s.logger.Error("Failed to save scraping job", map[string]interface{}{ - "error": err.Error(), - }) - } - - // Update state - state.IsRunning = true - state.CurrentJobID = job.ID - state.UpdatedAt = time.Now().Unix() - s.saveScrapingState(ctx, state) - - // Calculate files to download - filesToDownload := s.calculateFilesToDownload(state) - if len(filesToDownload) == 0 { - s.logger.Info("No new files to download", map[string]interface{}{ - "last_processed": fmt.Sprintf("%d%05d", state.LastProcessedYear, state.LastProcessedNumber), - }) - - // Complete the job - s.completeJob(ctx, job, state, nil) - return nil - } - - // Update job progress - job.Progress.TotalFiles = len(filesToDownload) - s.saveScrapingJob(ctx, job) - - var lastSuccessfulFile *FileInfo - - // Download and process files - for i, fileInfo := range filesToDownload { - job.Progress.CurrentFile = fmt.Sprintf("%d%05d", fileInfo.Year, fileInfo.Number) - s.saveScrapingJob(ctx, job) - - fileResult, err := s.downloadAndProcess(ctx, fileInfo) - if err != nil { - s.logger.Error("Failed to download and process file", map[string]interface{}{ - "year": fileInfo.Year, - "number": fileInfo.Number, - "error": err.Error(), - }) - job.Progress.FailedFiles++ - job.Results.ErrorCount++ - } else { - job.Progress.ProcessedFiles++ - job.Results.TotalTenders += fileResult.ProcessedCount - job.Results.NewTenders += fileResult.NewTenders - job.Results.UpdatedTenders += fileResult.UpdatedTenders - if len(fileResult.Errors) > 0 { - job.Results.ErrorCount += len(fileResult.Errors) - } - lastSuccessfulFile = &fileInfo - } - - // Update progress percentage - job.Progress.PercentComplete = float64(i+1) / float64(len(filesToDownload)) * 100 - s.saveScrapingJob(ctx, job) - - // Check context for cancellation - select { - case <-ctx.Done(): - s.completeJob(ctx, job, state, lastSuccessfulFile) - return ctx.Err() - default: - // Continue processing - } - } - - // Complete the job - s.completeJob(ctx, job, state, lastSuccessfulFile) - - s.logger.Info("TED XML scraping completed", map[string]interface{}{ - "files_processed": job.Progress.ProcessedFiles, - "tenders_created": job.Results.NewTenders, - "tenders_updated": job.Results.UpdatedTenders, - "total_tenders": job.Results.TotalTenders, - "error_count": job.Results.ErrorCount, - "success_rate": job.Results.SuccessRate, - }) - - return nil -} - -// FileInfo represents information about a TED file to download -type FileInfo struct { - Year int - Number int - URL string - OJS string -} - -// calculateFilesToDownload determines which files need to be downloaded -func (s *TEDScraper) calculateFilesToDownload(state *ScrapingState) []FileInfo { - var files []FileInfo - now := time.Now() - currentYear := now.Year() - currentMonth := now.Month() - - // Only process current year and month - year := currentYear - - // Start from the last processed file if it's from current year, otherwise start from beginning of current month - startNumber := 1 - if state.LastProcessedYear == currentYear { - startNumber = state.LastProcessedNumber + 1 - } else { - // Calculate approximate file number for start of current month - // Assuming roughly 22 working days per month (weekdays only) - startNumber = int(currentMonth-1)*22 + 1 - } - - // Calculate end number for current month - // Estimate based on current day of month and weekdays - daysInCurrentMonth := now.Day() - // Rough estimate: multiply by 0.7 to account for weekdays only - maxNumber := int(currentMonth-1)*22 + int(float64(daysInCurrentMonth)*0.7) - - // Add some buffer for potential files - maxNumber += 5 - - s.logger.Info("Calculating files to download for current month", map[string]interface{}{ - "current_year": currentYear, - "current_month": int(currentMonth), - "start_number": startNumber, - "max_number": maxNumber, - "last_processed": fmt.Sprintf("%d%05d", state.LastProcessedYear, state.LastProcessedNumber), - }) - - startNumber = 146 - - for number := startNumber; number <= maxNumber && len(files) < 10; number++ { // Limit to 10 files per run - fileURL := fmt.Sprintf("%s/%d%05d", s.config.BaseURL, year, number) - ojsID := fmt.Sprintf("S %03d / %d", number, year) - - files = append(files, FileInfo{ - Year: year, - Number: number, - URL: fileURL, - OJS: ojsID, - }) - } - - return files -} - -// FileProcessingResult represents the result of processing a single file -type FileProcessingResult struct { - Year int `json:"year"` - Number int `json:"number"` - ProcessedCount int `json:"processed_count"` - SuccessCount int `json:"success_count"` - NewTenders int `json:"new_tenders"` - UpdatedTenders int `json:"updated_tenders"` - Errors []string `json:"errors"` - ProcessedAt int64 `json:"processed_at"` -} - -// downloadAndProcess downloads and processes a single TED file -func (s *TEDScraper) downloadAndProcess(ctx context.Context, fileInfo FileInfo) (*FileProcessingResult, error) { - url := fmt.Sprintf("%s/%d%05d", s.config.BaseURL, fileInfo.Year, fileInfo.Number) - - s.logger.Info("Downloading TED file", map[string]interface{}{ - "url": url, - "year": fileInfo.Year, - "number": fileInfo.Number, - }) - - var result *FileProcessingResult - var err error - - // Retry mechanism - for attempt := 1; attempt <= s.config.MaxRetries; attempt++ { - result, err = s.downloadAndProcessWithRetry(ctx, url, fileInfo) - if err == nil { - break - } - - if attempt < s.config.MaxRetries { - s.logger.Warn("Download attempt failed, retrying", map[string]interface{}{ - "attempt": attempt, - "error": err.Error(), - "url": url, - }) - - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-time.After(s.config.RetryDelay): - // Continue to next attempt - } - } - } - - if err != nil { - s.logger.Error("Failed to download and process file after retries", map[string]interface{}{ - "url": url, - "attempts": s.config.MaxRetries, - "error": err.Error(), - }) - return nil, fmt.Errorf("failed to download %s after %d attempts: %w", url, s.config.MaxRetries, err) - } - - return result, nil -} - -// downloadAndProcessWithRetry performs a single download and process attempt -func (s *TEDScraper) downloadAndProcessWithRetry(ctx context.Context, url string, fileInfo FileInfo) (*FileProcessingResult, error) { - // Create a longer timeout context for processing large files - processCtx, cancel := context.WithTimeout(ctx, s.config.Timeout+time.Minute*5) - defer cancel() - - // Create HTTP request - req, err := http.NewRequestWithContext(processCtx, "GET", url, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("User-Agent", s.config.UserAgent) - - s.logger.Info("Starting download", map[string]interface{}{ - "url": url, - "year": fileInfo.Year, - "number": fileInfo.Number, - }) - - // Make request - resp, err := s.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to download file: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP error: %d %s", resp.StatusCode, resp.Status) - } - - s.logger.Info("Download completed, starting processing", map[string]interface{}{ - "url": url, - "content_length": resp.Header.Get("Content-Length"), - }) - - // Process tar.gz file directly from response body - return s.processTarGzFile(processCtx, resp.Body, fileInfo) -} - -// processTarGzFile extracts and processes XML files from a tar.gz archive -func (s *TEDScraper) processTarGzFile(ctx context.Context, reader io.Reader, fileInfo FileInfo) (*FileProcessingResult, error) { - result := &FileProcessingResult{ - Year: fileInfo.Year, - Number: fileInfo.Number, - ProcessedCount: 0, - SuccessCount: 0, - Errors: make([]string, 0), - ProcessedAt: time.Now().Unix(), - } - - // Create gzip reader - gzipReader, err := gzip.NewReader(reader) - if err != nil { - return nil, fmt.Errorf("failed to create gzip reader: %w", err) - } - defer gzipReader.Close() - - // Create tar reader - tarReader := tar.NewReader(gzipReader) - - // Process each file in the tar archive - fileCount := 0 - for { - // Check context for cancellation before each tar entry - select { - case <-ctx.Done(): - s.logger.Warn("Context cancelled during tar processing", map[string]interface{}{ - "processed_files": fileCount, - "error": ctx.Err().Error(), - }) - return result, ctx.Err() - default: - } - - header, err := tarReader.Next() - if err == io.EOF { - break // End of archive - } - if err != nil { - s.logger.Warn("Error reading tar entry", map[string]interface{}{ - "error": err.Error(), - "processed_files": fileCount, - }) - // If it's a timeout error, return it immediately - if ctx.Err() != nil { - return result, fmt.Errorf("timeout during tar processing: %w", err) - } - continue - } - fileCount++ - - // Skip directories and non-XML files - if header.Typeflag != tar.TypeReg { - continue - } - - fileName := filepath.Base(header.Name) - if !strings.HasSuffix(strings.ToLower(fileName), ".xml") { - continue - } - - // Log progress every 10 files or for first file - if result.ProcessedCount == 0 || result.ProcessedCount%10 == 0 { - s.logger.Info("Processing XML file from tar.gz", map[string]interface{}{ - "file_name": fileName, - "size": header.Size, - "processed_count": result.ProcessedCount, - "total_files_seen": fileCount, - "year": fileInfo.Year, - "number": fileInfo.Number, - }) - } else { - s.logger.Debug("Processing XML file from tar.gz", map[string]interface{}{ - "file_name": fileName, - "processed_count": result.ProcessedCount, - }) - } - - // Read file content - content, err := io.ReadAll(tarReader) - if err != nil { - errMsg := fmt.Sprintf("failed to read XML file %s: %v", fileName, err) - result.Errors = append(result.Errors, errMsg) - continue - } - - // Parse and process XML content - processResult, err := s.processXMLContent(ctx, content, fileName, fileInfo) - if err != nil { - errMsg := fmt.Sprintf("failed to process XML file %s: %v", fileName, err) - result.Errors = append(result.Errors, errMsg) - } else { - result.SuccessCount++ - if processResult.IsNew { - result.NewTenders++ - } else { - result.UpdatedTenders++ - } - } - - result.ProcessedCount++ - - // Check context for cancellation - select { - case <-ctx.Done(): - return result, ctx.Err() - default: - // Continue processing - } - } - - s.logger.Info("Completed processing tar.gz file", map[string]interface{}{ - "year": fileInfo.Year, - "number": fileInfo.Number, - "total_files": fileCount, - "processed_count": result.ProcessedCount, - "success_count": result.SuccessCount, - "error_count": len(result.Errors), - "processing_time": time.Now().Unix() - result.ProcessedAt, - }) - - return result, nil -} - -// XMLProcessResult represents the result of processing a single XML file -type XMLProcessResult struct { - IsNew bool `json:"is_new"` - TenderID string `json:"tender_id"` - ContractID string `json:"contract_id"` -} - -// processXMLContent parses XML content and creates/updates tender -func (s *TEDScraper) processXMLContent(ctx context.Context, content []byte, fileName string, fileInfo FileInfo) (*XMLProcessResult, error) { - s.logger.Info("Starting XML processing", map[string]interface{}{ - "file_name": fileName, - "file_size": len(content), - }) - - // Parse XML using the TED parser - parsedDoc, err := s.xmlParser.ParseXML(content) - if err != nil { - s.logger.Error("XML parsing failed", map[string]interface{}{ - "file_name": fileName, - "error": err.Error(), - }) - return nil, fmt.Errorf("failed to parse XML: %w", err) - } - - // Get the underlying document for ID and validation - document := parsedDoc.GetDocument() - documentID := "" - noticeTypeCode := "" - if document != nil { - documentID = document.GetID() - noticeTypeCode = document.GetNoticeType() - } - - s.logger.Info("XML parsed successfully", map[string]interface{}{ - "file_name": fileName, - "document_type": string(parsedDoc.Type), - "document_id": documentID, - "notice_type_code": noticeTypeCode, - }) - - // Validate parsed data - var validationErr error - if document != nil { - validationErr = document.Validate() - } - if validationErr != nil { - s.logger.Warn("XML validation failed, continuing anyway", map[string]interface{}{ - "file_name": fileName, - "document_type": string(parsedDoc.Type), - "error": validationErr.Error(), - }) - // Don't return error, continue processing - } else { - s.logger.Info("XML validation passed", map[string]interface{}{ - "file_name": fileName, - "document_type": string(parsedDoc.Type), - }) - } - - // Map to tender entity - sourceURL := fmt.Sprintf("%s/%d%05d", s.config.BaseURL, fileInfo.Year, fileInfo.Number) - tenderEntity := s.mapToTenderFromParsedDoc(parsedDoc, sourceURL, fileName) - - // Handle status-specific information and award logic - s.applyStatusSpecificInformation(tenderEntity, parsedDoc) - s.applyAwardLogic(tenderEntity, parsedDoc) - - s.logger.Info("Mapped to tender entity", map[string]interface{}{ - "file_name": fileName, - "tender_id": tenderEntity.TenderID, - "contract_notice_id": tenderEntity.ContractNoticeID, - "title": tenderEntity.Title, - "status": string(tenderEntity.Status), - }) - - // Try to find existing tender by contract notice ID (ContractID) - s.logger.Info("Checking for existing tender", map[string]interface{}{ - "contract_notice_id": tenderEntity.ContractNoticeID, - }) - - existingTender, err := s.findTenderByContractNoticeID(ctx, tenderEntity.ContractNoticeID) - if err != nil && err.Error() != "document not found" { - s.logger.Warn("Error checking for existing tender", map[string]interface{}{ - "contract_notice_id": tenderEntity.ContractNoticeID, - "error": err.Error(), - }) - } - - var processResult *XMLProcessResult - - if err == nil && existingTender != nil { - // Update existing tender - s.logger.Info("Found existing tender, updating", map[string]interface{}{ - "existing_tender_id": existingTender.ID, - "contract_notice_id": tenderEntity.ContractNoticeID, - "old_status": string(existingTender.Status), - "new_status": string(tenderEntity.Status), - }) - - // Merge updates with existing data - s.mergeExistingTenderData(existingTender, tenderEntity) - - tenderEntity.ID = existingTender.ID - tenderEntity.CreatedAt = existingTender.CreatedAt - tenderEntity.UpdatedAt = time.Now().Unix() - - err = s.tenderRepo.Update(ctx, tenderEntity) - if err != nil { - s.logger.Error("Failed to update existing tender", map[string]interface{}{ - "tender_id": tenderEntity.ID, - "error": err.Error(), - }) - return nil, fmt.Errorf("failed to update existing tender: %w", err) - } - - s.logger.Info("Successfully updated tender", map[string]interface{}{ - "tender_id": tenderEntity.ID, - }) - - processResult = &XMLProcessResult{ - IsNew: false, - TenderID: tenderEntity.TenderID, - ContractID: tenderEntity.ContractNoticeID, - } - } else { - // Create new tender - s.logger.Info("Creating new tender", map[string]interface{}{ - "contract_notice_id": tenderEntity.ContractNoticeID, - "tender_id": tenderEntity.TenderID, - "status": string(tenderEntity.Status), - }) - - tenderEntity.CreatedAt = time.Now().Unix() - tenderEntity.UpdatedAt = tenderEntity.CreatedAt - - err = s.tenderRepo.Create(ctx, tenderEntity) - if err != nil { - s.logger.Error("Failed to create new tender", map[string]interface{}{ - "contract_notice_id": tenderEntity.ContractNoticeID, - "tender_id": tenderEntity.TenderID, - "error": err.Error(), - }) - return nil, fmt.Errorf("failed to create tender: %w", err) - } - - s.logger.Info("Successfully created new tender", map[string]interface{}{ - "tender_id": tenderEntity.TenderID, - "contract_notice_id": tenderEntity.ContractNoticeID, - }) - - processResult = &XMLProcessResult{ - IsNew: true, - TenderID: tenderEntity.TenderID, - ContractID: tenderEntity.ContractNoticeID, - } - } - - s.logger.Debug("Successfully processed XML file", map[string]interface{}{ - "file_name": fileName, - "document_type": string(parsedDoc.Type), - "document_id": documentID, - "tender_id": tenderEntity.TenderID, - "is_new": processResult.IsNew, - }) - - return processResult, nil -} - -// Helper methods for database operations -func (s *TEDScraper) getScrapingState(ctx context.Context) (*ScrapingState, error) { - collection := s.mongoManager.GetCollection("scraping_state") - var state ScrapingState - err := collection.FindOne(ctx, bson.M{"_id": "ted_scraper"}).Decode(&state) - if err != nil { - if err == mongoDriver.ErrNoDocuments { - return nil, nil - } - return nil, err - } - return &state, nil -} - -func (s *TEDScraper) saveScrapingState(ctx context.Context, state *ScrapingState) error { - collection := s.mongoManager.GetCollection("scraping_state") - state.UpdatedAt = time.Now().Unix() - opts := options.Replace().SetUpsert(true) - _, err := collection.ReplaceOne(ctx, bson.M{"_id": state.ID}, state, opts) - return err -} - -func (s *TEDScraper) saveScrapingJob(ctx context.Context, job *ScrapingJob) error { - collection := s.mongoManager.GetCollection("scraping_jobs") - job.UpdatedAt = time.Now().Unix() - opts := options.Replace().SetUpsert(true) - _, err := collection.ReplaceOne(ctx, bson.M{"_id": job.ID}, job, opts) - return err -} - -func (s *TEDScraper) findTenderByContractNoticeID(ctx context.Context, contractNoticeID string) (*tender.Tender, error) { - // Use repository's specific method for finding by contract notice ID - existingTender, err := s.tenderRepo.GetByContractNoticeID(ctx, contractNoticeID) - if err != nil { - // If document not found, return nil without error - if err.Error() == "document not found" { - return nil, nil - } - return nil, err - } - - return existingTender, nil -} - -func (s *TEDScraper) completeJob(ctx context.Context, job *ScrapingJob, state *ScrapingState, lastFile *FileInfo) { - // Update state with last successful file - if lastFile != nil { - state.LastProcessedYear = lastFile.Year - state.LastProcessedNumber = lastFile.Number - } - - // Complete the job - endTime := time.Now() - completedAt := endTime.Unix() - job.Status = "completed" - job.CompletedAt = &completedAt - job.Results.ProcessingTime = completedAt - job.StartedAt - if job.Results.TotalTenders > 0 { - job.Results.SuccessRate = float64(job.Results.NewTenders) / float64(job.Results.TotalTenders) - } - s.saveScrapingJob(ctx, job) - - // Update state - state.IsRunning = false - state.CurrentJobID = bson.NilObjectID - state.LastRunAt = completedAt - state.NextRunAt = endTime.Add(s.config.ScrapingInterval).Unix() - state.UpdatedAt = completedAt - s.saveScrapingState(ctx, state) -} - -// mapToTenderFromParsedDoc maps a parsed TED document to tender entity -func (s *TEDScraper) mapToTenderFromParsedDoc(parsedDoc *ParsedDocument, sourceFileURL, sourceFileName string) *tender.Tender { - switch parsedDoc.Type { - case DocumentTypeContractNotice: - if parsedDoc.ContractNotice == nil { - s.logger.Error("ContractNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapToTender(parsedDoc.ContractNotice, sourceFileURL, sourceFileName) - - case DocumentTypeContractAwardNotice: - if parsedDoc.ContractAwardNotice == nil { - s.logger.Error("ContractAwardNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapContractAwardNoticeToTender(parsedDoc.ContractAwardNotice, sourceFileURL, sourceFileName) - - case DocumentTypePriorInformationNotice: - if parsedDoc.PriorInformationNotice == nil { - s.logger.Error("PriorInformationNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapGenericNoticeToTender(parsedDoc.PriorInformationNotice, sourceFileURL, sourceFileName) - - case DocumentTypeDesignContest: - if parsedDoc.DesignContest == nil { - s.logger.Error("DesignContest is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapGenericNoticeToTender(parsedDoc.DesignContest, sourceFileURL, sourceFileName) - - case DocumentTypeQualificationSystemNotice: - if parsedDoc.QualificationSystemNotice == nil { - s.logger.Error("QualificationSystemNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapGenericNoticeToTender(parsedDoc.QualificationSystemNotice, sourceFileURL, sourceFileName) - - case DocumentTypeConcessionNotice: - if parsedDoc.ConcessionNotice == nil { - s.logger.Error("ConcessionNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapGenericNoticeToTender(parsedDoc.ConcessionNotice, sourceFileURL, sourceFileName) - - case DocumentTypePlanningNotice: - if parsedDoc.PlanningNotice == nil { - s.logger.Error("PlanningNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapGenericNoticeToTender(parsedDoc.PlanningNotice, sourceFileURL, sourceFileName) - - case DocumentTypeCompetitionNotice: - if parsedDoc.CompetitionNotice == nil { - s.logger.Error("CompetitionNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapGenericNoticeToTender(parsedDoc.CompetitionNotice, sourceFileURL, sourceFileName) - - case DocumentTypeResultNotice: - if parsedDoc.ResultNotice == nil { - s.logger.Error("ResultNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapGenericNoticeToTender(parsedDoc.ResultNotice, sourceFileURL, sourceFileName) - - case DocumentTypeChangeNotice: - if parsedDoc.ChangeNotice == nil { - s.logger.Error("ChangeNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapGenericNoticeToTender(parsedDoc.ChangeNotice, sourceFileURL, sourceFileName) - - case DocumentTypeSubcontractNotice: - if parsedDoc.SubcontractNotice == nil { - s.logger.Error("SubcontractNotice is nil", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } - return s.mapGenericNoticeToTender(parsedDoc.SubcontractNotice, sourceFileURL, sourceFileName) - - // Legacy document types that should be parsed as ContractNotice but may still be in the old flow - case DocumentTypeCorrigendum, DocumentTypeModificationNotice: - if parsedDoc.ContractNotice != nil { - return s.mapToTender(parsedDoc.ContractNotice, sourceFileURL, sourceFileName) - } - // Fallback: try to get the document from the generic interface - document := parsedDoc.GetDocument() - if document != nil { - return s.mapGenericNoticeToTender(document, sourceFileURL, sourceFileName) - } - s.logger.Error("No valid document found for legacy type", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - - default: - s.logger.Error("Unknown document type for mapping", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - }) - return nil - } -} - -// mapGenericNoticeToTender maps any TED document type to tender entity using the common interface -func (s *TEDScraper) mapGenericNoticeToTender(doc TEDDocument, sourceFileURL, sourceFileName string) *tender.Tender { - if doc == nil { - s.logger.Error("Document is nil in mapGenericNoticeToTender", map[string]interface{}{}) - return nil - } - - now := time.Now().Unix() - - t := &tender.Tender{ - ContractNoticeID: doc.GetID(), - ContractFolderID: "", // Not available in generic interface - NoticeTypeCode: doc.GetNoticeType(), - NoticeSubTypeCode: "", // Not available in generic interface - NoticeLanguageCode: doc.GetLanguage(), - IssueDate: now, // TODO: Parse actual date from document - IssueTime: now, // TODO: Parse actual time from document - GazetteID: "", // Not available in generic interface - NoticePublicationID: "", // Not available in generic interface - PublicationDate: now, // TODO: Parse actual publication date - - // Basic tender information - Title: "Generic Notice " + doc.GetNoticeType(), - Description: "Processed from " + doc.GetNoticeType() + " document", - Status: tender.TenderStatusActive, - Source: tender.TenderSourceTEDScraper, - SourceFileURL: sourceFileURL, - SourceFileName: sourceFileName, - ProcessingMetadata: tender.ProcessingMetadata{ - ScrapedAt: now, - ProcessedAt: now, - ProcessingVersion: "1.0", - }, - } - - // Generate the tender ID after creating the tender - t.TenderID = s.generateTenderID(t) - - return t -} - -// mapToTender maps a TED ContractNotice to tender entity -func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileName string) *tender.Tender { - now := time.Now().Unix() - - t := &tender.Tender{ - ContractNoticeID: cn.ID, - ContractFolderID: cn.ContractFolderID, - NoticeTypeCode: cn.GetNoticeSubType(), - NoticeSubTypeCode: cn.GetNoticeSubType(), - NoticeLanguageCode: cn.NoticeLanguageCode, - IssueDate: s.parseDateToUnix(cn.IssueDate), - IssueTime: s.parseTimeToUnix(cn.IssueTime), - GazetteID: cn.GetOJSID(), - Title: cn.GetProcurementTitle(), - Description: cn.GetProcurementDescription(), - ProcurementTypeCode: cn.GetContractNature(), - ProcedureCode: cn.GetProcedureCode(), - MainClassification: cn.GetMainClassification(), - PlaceOfPerformance: cn.GetPlaceOfPerformance(), - DocumentURI: cn.GetDocumentURI(), - Status: tender.TenderStatusActive, - Source: tender.TenderSourceTEDScraper, - SourceFileURL: sourceFileURL, - SourceFileName: sourceFileName, - ProcessingMetadata: tender.ProcessingMetadata{ - ScrapedAt: now, - ProcessedAt: now, - ProcessingVersion: "1.0", - }, - } - - // Set publication information - if pubInfo := cn.GetPublicationInfo(); pubInfo != nil { - t.NoticePublicationID = pubInfo.NoticePublicationID - t.PublicationDate = s.parseDateToUnix(pubInfo.PublicationDate) - t.TenderURL = s.generateTenderURL(pubInfo.NoticePublicationID) - } - - // Set additional classifications - if cn.ProcurementProject != nil && cn.ProcurementProject.AdditionalCommodityClassification != nil { - for _, additionalClass := range cn.ProcurementProject.AdditionalCommodityClassification { - if additionalClass.ItemClassificationCode != "" { - t.AdditionalClassifications = append(t.AdditionalClassifications, additionalClass.ItemClassificationCode) - } - } - } - - // Set estimated value and currency - if amount, currency := cn.GetBudget(); amount != "" { - if value, err := strconv.ParseFloat(amount, 64); err == nil { - t.EstimatedValue = value - t.Currency = currency - } - } - - // Set duration information - if duration, unit := cn.GetDuration(); duration != "" { - t.Duration = duration - t.DurationUnit = unit - } - - // Set tender deadline - if deadline := cn.GetTenderDeadline(); deadline != "" { - t.TenderDeadline = s.parseDateToUnix(deadline) - - if deadlineTime, err := s.parseDate(deadline); err == nil { - applicationDeadline := s.calculateApplicationDeadline(deadlineTime) - t.ApplicationDeadline = applicationDeadline.Unix() - } - } - - if pubDate, err := s.parseDate(cn.GetPublicationDate()); err == nil { - submissionDeadline := s.calculateSubmissionDeadline(pubDate) - t.SubmissionDeadline = submissionDeadline.Unix() - } - - // Set SubmissionURL from TenderingTerms - if cn.ProcurementProjectLot != nil && cn.ProcurementProjectLot.TenderingTerms != nil { - if tenderingTerms := cn.ProcurementProjectLot.TenderingTerms; tenderingTerms.TenderRecipientParty != nil { - if endpointID := tenderingTerms.TenderRecipientParty.EndpointID; endpointID != "" { - if strings.HasPrefix(endpointID, "http") { - t.SubmissionURL = endpointID - } else { - t.SubmissionURL = "https://" + endpointID - } - } - } - } - - // Set location information - if cn.ProcurementProject != nil && cn.ProcurementProject.RealizedLocation != nil { - if addr := cn.ProcurementProject.RealizedLocation.Address; addr != nil { - t.CityName = addr.CityName - t.PostalCode = addr.PostalZone - t.RegionCode = addr.CountrySubentityCode - if addr.Country != nil { - t.CountryCode = addr.Country.IdentificationCode - } - } - } - - // Set buyer organization - buyerID := cn.GetBuyerID() - if buyerID != "" { - if buyerOrg := cn.GetOrganizationByID(buyerID); buyerOrg != nil { - t.BuyerOrganization = s.mapOrganization(buyerOrg, "buyer") - } - } - - // Set review organization - reviewID := cn.GetReviewOrganizationID() - if reviewID != "" { - if reviewOrg := cn.GetOrganizationByID(reviewID); reviewOrg != nil { - t.ReviewOrganization = s.mapOrganization(reviewOrg, "review") - } - } - - // Set all organizations - if orgs := cn.GetOrganizations(); orgs != nil { - for _, org := range orgs.Organization { - mappedOrg := s.mapOrganization(&org, "") - if mappedOrg != nil { - t.Organizations = append(t.Organizations, *mappedOrg) - } - } - } - - // Set selection criteria - if criteria := cn.GetSelectionCriteria(); criteria != nil { - for _, criterion := range criteria { - t.SelectionCriteria = append(t.SelectionCriteria, tender.SelectionCriterion{ - TypeCode: criterion.CriterionTypeCode, - Description: criterion.Description, - LanguageID: criterion.LanguageID, - }) - } - } - - // Set official languages - if languages := cn.GetOfficialLanguages(); languages != nil { - t.OfficialLanguages = languages - } - - // Generate unique tender ID - t.TenderID = s.generateTenderID(t) - - return t -} - -// mapContractAwardNoticeToTender maps a TED ContractAwardNotice to tender entity -func (s *TEDScraper) mapContractAwardNoticeToTender(can *ContractAwardNotice, sourceFileURL, sourceFileName string) *tender.Tender { - now := time.Now().Unix() - - t := &tender.Tender{ - ContractNoticeID: can.ID, - ContractFolderID: can.ContractFolderID, - NoticeTypeCode: can.NoticeTypeCode, - NoticeSubTypeCode: can.GetNoticeSubType(), - NoticeLanguageCode: can.NoticeLanguageCode, - IssueDate: s.parseDateToUnix(can.IssueDate), - IssueTime: s.parseTimeToUnix(can.IssueTime), - GazetteID: can.GetOJSID(), - Title: can.GetProcurementTitle(), - Description: can.GetProcurementDescription(), - ProcurementTypeCode: can.GetContractNature(), - ProcedureCode: can.GetProcedureCode(), - MainClassification: can.GetMainClassification(), - Status: tender.TenderStatusAwarded, // Award notices are for completed tenders - Source: tender.TenderSourceTEDScraper, - SourceFileURL: sourceFileURL, - SourceFileName: sourceFileName, - ProcessingMetadata: tender.ProcessingMetadata{ - ScrapedAt: now, - ProcessedAt: now, - ProcessingVersion: "1.0", - }, - } - - // Set publication information - if pubInfo := can.GetPublicationInfo(); pubInfo != nil { - t.NoticePublicationID = pubInfo.NoticePublicationID - t.PublicationDate = s.parseDateToUnix(pubInfo.PublicationDate) - t.TenderURL = s.generateTenderURL(pubInfo.NoticePublicationID) - } - - // Set total award value - if amount, currency := can.GetTotalAwardValue(); amount != "" { - if parsedAmount, err := strconv.ParseFloat(amount, 64); err == nil { - t.EstimatedValue = parsedAmount - } - t.Currency = currency - } - - // Set location information from main procurement project - if can.ProcurementProject != nil && can.ProcurementProject.RealizedLocation != nil { - if addr := can.ProcurementProject.RealizedLocation.Address; addr != nil { - t.CityName = addr.CityName - t.PostalCode = addr.PostalZone - t.RegionCode = addr.CountrySubentityCode - if addr.Country != nil { - t.CountryCode = addr.Country.IdentificationCode - } - } - } - - // Set buyer organization - buyerID := can.GetBuyerID() - if buyerID != "" { - if buyerOrg := can.GetOrganizationByID(buyerID); buyerOrg != nil { - t.BuyerOrganization = s.mapOrganization(buyerOrg, "buyer") - } - } - - // Set all organizations - if orgs := can.GetOrganizations(); orgs != nil { - for _, org := range orgs.Organization { - mappedOrg := s.mapOrganization(&org, "") - if mappedOrg != nil { - t.Organizations = append(t.Organizations, *mappedOrg) - } - } - } - - // Generate unique tender ID - t.TenderID = s.generateTenderID(t) - - return t -} - -// mapOrganization maps a TED Organization to tender Organization -func (s *TEDScraper) mapOrganization(tedOrg *Organization, role string) *tender.Organization { - if tedOrg == nil || tedOrg.Company == nil { - return nil - } - - company := tedOrg.Company - org := &tender.Organization{ - Role: role, - } - - // Set organization ID and name - if company.PartyIdentification != nil { - org.ID = company.PartyIdentification.ID - } - if company.PartyName != nil { - org.Name = company.PartyName.Name - } - - // Set company ID - if company.PartyLegalEntity != nil { - org.CompanyID = company.PartyLegalEntity.CompanyID - } - - // Set website - org.WebsiteURI = company.WebsiteURI - - // Set contact information - if company.Contact != nil { - org.ContactName = company.Contact.Name - org.ContactTelephone = company.Contact.Telephone - org.ContactEmail = company.Contact.ElectronicMail - org.ContactFax = company.Contact.Telefax - } - - // Set address - if company.PostalAddress != nil { - org.Address = tender.Address{ - StreetName: company.PostalAddress.StreetName, - CityName: company.PostalAddress.CityName, - PostalZone: company.PostalAddress.PostalZone, - CountrySubentityCode: company.PostalAddress.CountrySubentityCode, - Department: company.PostalAddress.Department, - } - if company.PostalAddress.Country != nil { - org.Address.CountryCode = company.PostalAddress.Country.IdentificationCode - } - } - - return org -} - -// generateTenderID generates a unique tender ID using the format: -// {Source}{BUYER_CODE}{TED_CODE}{DATE}{LOCATION_CODE} -func (s *TEDScraper) generateTenderID(t *tender.Tender) string { - var parts []string - - // Source (TED) - parts = append(parts, "1") - - // TED code (Notice Publication ID, first 8 chars) - tedCode := "00000000" - if t.NoticePublicationID != "" { - tedCode = s.padOrTruncate(t.NoticePublicationID, 8) - } - parts = append(parts, tedCode) - - // Buyer code (first 8 chars of buyer organization ID, or 8 zeros if not available) - // buyerCode := "00000000" - // if t.BuyerOrganization != nil && t.BuyerOrganization.ID != "" { - // buyerCode = s.padOrTruncate(t.BuyerOrganization.ID, 8) - // } - // parts = append(parts, buyerCode) - - // Date (YYYYMMDD format from issue date) - parts = append(parts, s.padOrTruncate(s.UnixToDateString(t.IssueDate), 4)) - - // Location code (country code + region code, padded to 6 chars) - locationCode := "EU" - if t.CountryCode != "" { - locationCode = t.CountryCode - } - parts = append(parts, locationCode) - - return strings.Join(parts, "") -} - -// padOrTruncate pads a string with zeros or truncates it to the specified length -func (s *TEDScraper) padOrTruncate(str string, length int) string { - // Remove any non-alphanumeric characters and convert to uppercase - clean := strings.ToUpper(strings.ReplaceAll(str, "-", "")) - clean = strings.ReplaceAll(clean, " ", "") - - if len(clean) >= length { - return clean[:length] - } - - // Pad with zeros - return clean + strings.Repeat("0", length-len(clean)) -} - -// generateTenderURL generates the TED tender URL from NoticePublicationID -func (s *TEDScraper) generateTenderURL(noticePublicationID string) string { - if noticePublicationID == "" { - return "" - } - - // Remove leading zeros and format for URL - // Example: 00522942-2025 -> https://ted.europa.eu/en/notice/-/detail/522942-2025 - parts := strings.Split(noticePublicationID, "-") - if len(parts) != 2 { - return "" - } - - // Convert to int and back to string to remove leading zeros - if num, err := strconv.Atoi(parts[0]); err == nil { - cleanID := fmt.Sprintf("%d-%s", num, parts[1]) - return fmt.Sprintf("https://ted.europa.eu/en/notice/-/detail/%s", cleanID) - } - - return "" -} - -// parseDate parses various date formats used in TED XML -func (s *TEDScraper) parseDate(dateStr string) (time.Time, error) { - if dateStr == "" { - return time.Time{}, fmt.Errorf("empty date string") - } - - // Common TED date formats - formats := []string{ - "2006-01-02+07:00", - "2006-01-02-07:00", - "2006-01-02T15:04:05+07:00", - "2006-01-02T15:04:05-07:00", - "2006-01-02T15:04:05Z", - "2006-01-02Z", - "2006-01-02", - time.RFC3339, - time.RFC3339Nano, - } - - for _, format := range formats { - if parsedTime, err := time.Parse(format, dateStr); err == nil { - return parsedTime, nil - } - } - - return time.Time{}, fmt.Errorf("unable to parse date: %s", dateStr) -} - -// parseDateToUnix converts string date to Unix milliseconds (int64) -func (s *TEDScraper) parseDateToUnix(dateStr string) int64 { - if dateStr == "" { - return 0 - } - - if parsedTime, err := s.parseDate(dateStr); err == nil { - return parsedTime.Unix() - } - - return 0 -} - -// parseTimeToUnix converts string time to Unix milliseconds (int64) -func (s *TEDScraper) parseTimeToUnix(timeStr string) int64 { - if timeStr == "" { - return 0 - } - - // If time string is just time without date, use today's date - if !strings.Contains(timeStr, "T") && !strings.Contains(timeStr, " ") { - // Assume it's just a time like "15:04:05" - today := time.Now().Format("2006-01-02") - timeStr = fmt.Sprintf("%sT%s", today, timeStr) - } - - if parsedTime, err := s.parseDate(timeStr); err == nil { - return parsedTime.Unix() - } - - return 0 -} - -// UnixToDateString converts Unix milliseconds to date string for TenderID generation -func (s *TEDScraper) UnixToDateString(u int64) string { - if u == 0 { - return "00000000" - } - - return time.Unix(u, 0).Format("20060102") -} - -func (s *TEDScraper) calculateSubmissionDeadline(publicationDate time.Time) time.Time { - deadline := publicationDate - workingHoursAdded := 0 - - for workingHoursAdded < 48 { - deadline = deadline.Add(1 * time.Hour) - - if deadline.Weekday() != time.Saturday && deadline.Weekday() != time.Sunday { - workingHoursAdded++ - } - } - - return deadline -} - -func (s *TEDScraper) calculateApplicationDeadline(tenderDeadline time.Time) time.Time { - applicationDeadline := tenderDeadline - workingDaysSubtracted := 0 - - for workingDaysSubtracted < 7 { - applicationDeadline = applicationDeadline.AddDate(0, 0, -1) - - if applicationDeadline.Weekday() != time.Saturday && applicationDeadline.Weekday() != time.Sunday { - workingDaysSubtracted++ - } - } - - return applicationDeadline -} - -func (s *TEDScraper) adjustToWorkingDay(targetDate time.Time, forward bool) time.Time { - current := targetDate - - for current.Weekday() == time.Saturday || current.Weekday() == time.Sunday { - if forward { - current = current.AddDate(0, 0, 1) - } else { - current = current.AddDate(0, 0, -1) - } - } - - return current -} - -// mapTEDStatusToTenderStatus maps TED notice status to tender status -func (s *TEDScraper) mapTEDStatusToTenderStatus(tedStatus NoticeStatus) tender.TenderStatus { - switch tedStatus { - case NoticeStatusDraft: - return tender.TenderStatusDraft - case NoticeStatusPublished: - return tender.TenderStatusPublished - case NoticeStatusCancelled: - return tender.TenderStatusCancelled - case NoticeStatusAwarded: - return tender.TenderStatusAwarded - case NoticeStatusClosed: - return tender.TenderStatusClosed - case NoticeStatusModified: - return tender.TenderStatusModified - case NoticeStatusSuspended: - return tender.TenderStatusSuspended - default: - // Default to active for unknown statuses, but log a warning - s.logger.Warn("Unknown TED notice status, defaulting to active", map[string]interface{}{ - "ted_status": string(tedStatus), - }) - return tender.TenderStatusActive - } -} - -// applyStatusSpecificInformation applies status-specific information to the tender entity -func (s *TEDScraper) applyStatusSpecificInformation(tenderEntity *tender.Tender, parsedDoc *ParsedDocument) { - if tenderEntity == nil || parsedDoc == nil { - return - } - - // Map TED status to tender status - tenderEntity.Status = s.mapTEDStatusToTenderStatus(parsedDoc.Status) - - // Get the document to access status-specific methods - document := parsedDoc.GetDocument() - if document == nil { - return - } - - // Apply status-specific information based on document type - switch parsedDoc.Type { - case DocumentTypeContractNotice: - if cn, ok := document.(*ContractNotice); ok { - s.applyContractNoticeStatusInfo(tenderEntity, cn, parsedDoc.Status) - } - case DocumentTypeContractAwardNotice: - if can, ok := document.(*ContractAwardNotice); ok { - s.applyContractAwardNoticeStatusInfo(tenderEntity, can, parsedDoc.Status) - } - default: - // For other document types, we rely on the parsed status - s.logger.Debug("Applied basic status information for document type", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - "status": string(parsedDoc.Status), - }) - } -} - -// applyContractNoticeStatusInfo applies status-specific information from ContractNotice -func (s *TEDScraper) applyContractNoticeStatusInfo(tenderEntity *tender.Tender, cn *ContractNotice, status NoticeStatus) { - switch status { - case NoticeStatusCancelled: - if cancellation := cn.GetCancellationReason(); cancellation != nil { - tenderEntity.CancellationReason = cancellation.Description - if cancellation.ReasonCode != "" { - tenderEntity.CancellationReason = fmt.Sprintf("%s: %s", cancellation.ReasonCode, cancellation.Description) - } - // Note: TED XML doesn't typically have cancellation date, so we use current time - tenderEntity.CancellationDate = time.Now().Unix() - } - - case NoticeStatusAwarded: - if award := cn.GetAwardInformation(); award != nil { - if award.AwardDate != "" { - tenderEntity.AwardDate = s.parseDateToUnix(award.AwardDate) - } - if award.AwardedValue.Text != "" { - if value, err := strconv.ParseFloat(award.AwardedValue.Text, 64); err == nil { - tenderEntity.AwardedValue = value - } - } - tenderEntity.ContractNumber = award.ContractNumber - if award.WinningTenderer != nil && award.WinningTenderer.Company != nil { - tenderEntity.WinningTenderer = s.mapOrganization(&Organization{ - Company: award.WinningTenderer.Company, - }, "winner") - } - } - - case NoticeStatusSuspended: - if suspension := cn.GetSuspensionReason(); suspension != nil { - tenderEntity.SuspensionReason = suspension.Description - if suspension.ReasonCode != "" { - tenderEntity.SuspensionReason = fmt.Sprintf("%s: %s", suspension.ReasonCode, suspension.Description) - } - tenderEntity.SuspensionDate = time.Now().Unix() - } - - case NoticeStatusModified: - if modifications := cn.GetModifications(); modifications != nil { - for _, mod := range modifications { - tenderMod := tender.TenderModification{ - ModificationDate: s.parseDateToUnix(mod.ModificationDate), - ModificationReason: mod.ModificationReason, - Description: mod.Description, - LanguageID: mod.LanguageID, - } - tenderEntity.Modifications = append(tenderEntity.Modifications, tenderMod) - } - } - } -} - -// applyContractAwardNoticeStatusInfo applies status-specific information from ContractAwardNotice -func (s *TEDScraper) applyContractAwardNoticeStatusInfo(tenderEntity *tender.Tender, can *ContractAwardNotice, status NoticeStatus) { - // Contract award notices are typically awarded status - tenderEntity.Status = tender.TenderStatusAwarded - - // Set award date from tender result - if can.TenderResult != nil && can.TenderResult.AwardDate != "" { - tenderEntity.AwardDate = s.parseDateToUnix(can.TenderResult.AwardDate) - } - - // Get award information from extensions if available - if award := can.GetAwardInformation(); award != nil { - if award.AwardedValue.Text != "" { - if value, err := strconv.ParseFloat(award.AwardedValue.Text, 64); err == nil { - tenderEntity.AwardedValue = value - } - } - tenderEntity.ContractNumber = award.ContractNumber - } - - // Get total award value from notice result - if amount, currency := can.GetTotalAwardValue(); amount != "" { - if parsedAmount, err := strconv.ParseFloat(amount, 64); err == nil { - tenderEntity.AwardedValue = parsedAmount - tenderEntity.Currency = currency - } - } - - // Extract detailed awarded contractors information - s.extractAwardedEntities(tenderEntity, can) - - // Handle other statuses if applicable - switch status { - case NoticeStatusCancelled: - if cancellation := can.GetCancellationReason(); cancellation != nil { - tenderEntity.Status = tender.TenderStatusCancelled - tenderEntity.CancellationReason = cancellation.Description - if cancellation.ReasonCode != "" { - tenderEntity.CancellationReason = fmt.Sprintf("%s: %s", cancellation.ReasonCode, cancellation.Description) - } - tenderEntity.CancellationDate = time.Now().Unix() - } - - case NoticeStatusModified: - if modifications := can.GetModifications(); modifications != nil { - tenderEntity.Status = tender.TenderStatusModified - for _, mod := range modifications { - tenderMod := tender.TenderModification{ - ModificationDate: s.parseDateToUnix(mod.ModificationDate), - ModificationReason: mod.ModificationReason, - Description: mod.Description, - LanguageID: mod.LanguageID, - } - tenderEntity.Modifications = append(tenderEntity.Modifications, tenderMod) - } - } - } -} - -// extractAwardedEntities extracts detailed information about awarded entities from a contract award notice -func (s *TEDScraper) extractAwardedEntities(tenderEntity *tender.Tender, can *ContractAwardNotice) { - awardedContractors := can.GetAwardedContractors() - if len(awardedContractors) == 0 { - return - } - - s.logger.Info("Extracting awarded entities", map[string]interface{}{ - "contract_notice_id": can.ID, - "num_contractors": len(awardedContractors), - }) - - for _, contractor := range awardedContractors { - entity := tender.Awarded{ - Name: contractor.Name, - Address: contractor.Address, - Country: contractor.Country, - Currency: contractor.Currency, - ContractID: contractor.ContractID, - TenderID: contractor.TenderID, - LotID: contractor.LotID, - CompanyID: contractor.CompanyID, - OrganizationID: contractor.OrganizationID, - } - - // Parse amount - if contractor.Amount != "" { - if amount, err := strconv.ParseFloat(contractor.Amount, 64); err == nil { - entity.Amount = amount - } else { - s.logger.Warn("Failed to parse award amount", map[string]interface{}{ - "amount_string": contractor.Amount, - "contractor": contractor.Name, - "error": err.Error(), - }) - } - } - - // Parse award date - if contractor.AwardDate != "" { - entity.AwardDate = s.parseDateToUnix(contractor.AwardDate) - } else if contractor.IssueDate != "" { - entity.AwardDate = s.parseDateToUnix(contractor.IssueDate) - } - - // Calculate share percentage if there are multiple contractors - totalAmount := tenderEntity.AwardedValue - if totalAmount > 0 && entity.Amount > 0 { - entity.Share = (entity.Amount / totalAmount) * 100 - } - - tenderEntity.AwardedEntities = append(tenderEntity.AwardedEntities, entity) - - s.logger.Debug("Added awarded entity", map[string]interface{}{ - "name": entity.Name, - "amount": entity.Amount, - "currency": entity.Currency, - "share": entity.Share, - "contract_id": entity.ContractID, - }) - } - - s.logger.Info("Successfully extracted awarded entities", map[string]interface{}{ - "total_entities": len(tenderEntity.AwardedEntities), - "total_value": tenderEntity.AwardedValue, - "currency": tenderEntity.Currency, - }) -} - -// ScrapeByDateRange scrapes TED XML files within a specified date range -func (s *TEDScraper) ScrapeByDateRange(ctx context.Context, config DateRangeConfig) error { - startTime := time.Now() - - s.logger.Info("Starting TED XML scraping by date range", map[string]interface{}{ - "start_date": config.StartDate.Format("2006-01-02"), - "end_date": config.EndDate.Format("2006-01-02"), - "mode": string(config.Mode), - "timestamp": startTime.Unix(), - }) - - // Validate date range - if config.EndDate.Before(config.StartDate) { - return fmt.Errorf("end date cannot be before start date") - } - - // Calculate the date range in days - daysInRange := int(config.EndDate.Sub(config.StartDate).Hours()/24) + 1 - if daysInRange > 365 { - return fmt.Errorf("date range too large: maximum 365 days allowed") - } - - // Create scraping job - job := &ScrapingJob{ - Type: fmt.Sprintf("ted_%s", config.Mode), - Status: "running", - StartedAt: startTime.Unix(), - Progress: JobProgress{}, - Results: JobResults{}, - Config: map[string]interface{}{ - "base_url": s.config.BaseURL, - "start_date": config.StartDate.Format("2006-01-02"), - "end_date": config.EndDate.Format("2006-01-02"), - "mode": string(config.Mode), - }, - } - - // Save job - if err := s.saveScrapingJob(ctx, job); err != nil { - s.logger.Error("Failed to save scraping job", map[string]interface{}{ - "error": err.Error(), - }) - } - - // Calculate files to download based on date range - filesToDownload := s.calculateFilesInDateRange(config.StartDate, config.EndDate) - if len(filesToDownload) == 0 { - s.logger.Info("No files to download in date range", map[string]interface{}{ - "start_date": config.StartDate.Format("2006-01-02"), - "end_date": config.EndDate.Format("2006-01-02"), - }) - - // Complete the job - s.completeJob(ctx, job, nil, nil) - return nil - } - - // Update job progress - job.Progress.TotalFiles = len(filesToDownload) - s.saveScrapingJob(ctx, job) - - var lastSuccessfulFile *FileInfo - - // Download and process files - for i, fileInfo := range filesToDownload { - job.Progress.CurrentFile = fmt.Sprintf("%d%05d", fileInfo.Year, fileInfo.Number) - s.saveScrapingJob(ctx, job) - - fileResult, err := s.downloadAndProcess(ctx, fileInfo) - if err != nil { - s.logger.Error("Failed to download and process file", map[string]interface{}{ - "year": fileInfo.Year, - "number": fileInfo.Number, - "error": err.Error(), - }) - job.Progress.FailedFiles++ - job.Results.ErrorCount++ - } else { - job.Progress.ProcessedFiles++ - job.Results.TotalTenders += fileResult.ProcessedCount - job.Results.NewTenders += fileResult.NewTenders - job.Results.UpdatedTenders += fileResult.UpdatedTenders - if len(fileResult.Errors) > 0 { - job.Results.ErrorCount += len(fileResult.Errors) - } - lastSuccessfulFile = &fileInfo - } - - // Update progress percentage - job.Progress.PercentComplete = float64(i+1) / float64(len(filesToDownload)) * 100 - s.saveScrapingJob(ctx, job) - - // Check context for cancellation - select { - case <-ctx.Done(): - s.completeJob(ctx, job, nil, lastSuccessfulFile) - return ctx.Err() - default: - // Continue processing - } - } - - // Complete the job - s.completeJob(ctx, job, nil, lastSuccessfulFile) - - s.logger.Info("TED XML scraping by date range completed", map[string]interface{}{ - "files_processed": job.Progress.ProcessedFiles, - "tenders_created": job.Results.NewTenders, - "tenders_updated": job.Results.UpdatedTenders, - "error_count": job.Results.ErrorCount, - "success_rate": job.Results.SuccessRate, - }) - - return nil -} - -// calculateFilesInDateRange calculates which files need to be downloaded for a date range -func (s *TEDScraper) calculateFilesInDateRange(startDate, endDate time.Time) []FileInfo { - var files []FileInfo - - // TED files are published roughly once per working day - // We estimate file numbers based on working days from start of year - - for year := startDate.Year(); year <= endDate.Year(); year++ { - yearStart := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) - yearEnd := time.Date(year, 12, 31, 23, 59, 59, 0, time.UTC) - - // Determine the effective start and end dates for this year - effectiveStart := startDate - if yearStart.After(startDate) { - effectiveStart = yearStart - } - - effectiveEnd := endDate - if yearEnd.Before(endDate) { - effectiveEnd = yearEnd - } - - // Skip if no overlap with this year - if effectiveStart.After(effectiveEnd) { - continue - } - - // Calculate approximate file range for this year - startDayOfYear := effectiveStart.YearDay() - endDayOfYear := effectiveEnd.YearDay() - - // Estimate file numbers (roughly 0.7 files per day, accounting for weekends) - startFileNum := int(float64(startDayOfYear) * 0.7) - endFileNum := int(float64(endDayOfYear) * 0.7) - - // Ensure minimum range - if startFileNum < 1 { - startFileNum = 1 - } - if endFileNum < startFileNum { - endFileNum = startFileNum - } - - // Add some buffer - startFileNum = max(1, startFileNum-2) - endFileNum = min(366, endFileNum+2) // Max ~366 files per year - - s.logger.Info("Calculating file range for year", map[string]interface{}{ - "year": year, - "start_file_num": startFileNum, - "end_file_num": endFileNum, - "start_date": effectiveStart.Format("2006-01-02"), - "end_date": effectiveEnd.Format("2006-01-02"), - }) - - for number := startFileNum; number <= endFileNum; number++ { - fileURL := fmt.Sprintf("%s/%d%05d", s.config.BaseURL, year, number) - ojsID := fmt.Sprintf("S %03d / %d", number, year) - - files = append(files, FileInfo{ - Year: year, - Number: number, - URL: fileURL, - OJS: ojsID, - }) - } - } - - s.logger.Info("Calculated files for date range", map[string]interface{}{ - "total_files": len(files), - "start_date": startDate.Format("2006-01-02"), - "end_date": endDate.Format("2006-01-02"), - }) - - return files -} - -// Helper function for max -func max(a, b int) int { - if a > b { - return a - } - return b -} - -// Helper function for min -func min(a, b int) int { - if a < b { - return a - } - return b -} - -// applyAwardLogic applies award-specific logic based on document type and content -func (s *TEDScraper) applyAwardLogic(tenderEntity *tender.Tender, parsedDoc *ParsedDocument) { - if tenderEntity == nil || parsedDoc == nil { - return - } - - // Only set award information if the tender has a confirmed winner - hasConfirmedWinner := s.hasConfirmedWinner(parsedDoc) - - s.logger.Debug("Applying award logic", map[string]interface{}{ - "document_type": string(parsedDoc.Type), - "status": string(parsedDoc.Status), - "has_confirmed_winner": hasConfirmedWinner, - }) - - if !hasConfirmedWinner { - // Clear any award date and winner information for tenders without confirmed winners - tenderEntity.AwardDate = 0 - tenderEntity.AwardedValue = 0 - tenderEntity.WinningTenderer = nil - tenderEntity.AwardedEntities = nil - tenderEntity.ContractNumber = "" - - s.logger.Debug("Cleared award information for tender without confirmed winner", map[string]interface{}{ - "contract_notice_id": tenderEntity.ContractNoticeID, - }) - return - } - - // Process award information for tenders with confirmed winners - s.logger.Info("Processing award information for tender with confirmed winner", map[string]interface{}{ - "contract_notice_id": tenderEntity.ContractNoticeID, - "document_type": string(parsedDoc.Type), - }) -} - -// hasConfirmedWinner checks if the tender has a confirmed winner based on the document content -func (s *TEDScraper) hasConfirmedWinner(parsedDoc *ParsedDocument) bool { - if parsedDoc == nil { - return false - } - - // Check for award-specific document types - switch parsedDoc.Type { - case DocumentTypeContractAwardNotice, DocumentTypeResultNotice: - // These document types typically indicate awarded tenders - if parsedDoc.Status == NoticeStatusAwarded { - return true - } - - // Check for specific award indicators in the document - document := parsedDoc.GetDocument() - if document != nil { - return s.checkForAwardIndicators(document, parsedDoc.Type) - } - - case DocumentTypeContractNotice: - // Contract notices may have award information if they're updated with results - if parsedDoc.Status == NoticeStatusAwarded { - if cn := parsedDoc.ContractNotice; cn != nil { - return s.checkForAwardIndicators(cn, parsedDoc.Type) - } - } - } - - return false -} - -// checkForAwardIndicators checks for specific award indicators in the document -func (s *TEDScraper) checkForAwardIndicators(document TEDDocument, docType DocumentType) bool { - switch docType { - case DocumentTypeContractAwardNotice: - if can, ok := document.(*ContractAwardNotice); ok { - // Check for tender result with award date - if can.TenderResult != nil && can.TenderResult.AwardDate != "" { - return true - } - - // Check for awarded contractors - awardedContractors := can.GetAwardedContractors() - if len(awardedContractors) > 0 { - return true - } - - // Check for award information in extensions - if award := can.GetAwardInformation(); award != nil && award.AwardDate != "" { - return true - } - } - - case DocumentTypeContractNotice: - if cn, ok := document.(*ContractNotice); ok { - // Check for award information - if award := cn.GetAwardInformation(); award != nil && award.AwardDate != "" { - return true - } - } - - case DocumentTypeResultNotice: - // Result notices typically contain award information - return true - } - - return false -} - -// mergeExistingTenderData merges new data with existing tender data, preserving important existing information -func (s *TEDScraper) mergeExistingTenderData(existing *tender.Tender, updated *tender.Tender) { - // Preserve creation timestamp and original metadata - updated.CreatedAt = existing.CreatedAt - - // Merge processing metadata - if existing.ProcessingMetadata.ScrapedAt != 0 { - // Keep original scraped time, update processed time - updated.ProcessingMetadata.ScrapedAt = existing.ProcessingMetadata.ScrapedAt - } - - // Preserve existing award information if the new document doesn't have confirmed winners - // but existing data does (to avoid losing award data on subsequent updates) - if existing.AwardDate != 0 && updated.AwardDate == 0 && len(existing.AwardedEntities) > 0 { - s.logger.Info("Preserving existing award information", map[string]interface{}{ - "contract_notice_id": existing.ContractNoticeID, - "existing_award_date": existing.AwardDate, - "awarded_entities_count": len(existing.AwardedEntities), - }) - - updated.AwardDate = existing.AwardDate - updated.AwardedValue = existing.AwardedValue - updated.WinningTenderer = existing.WinningTenderer - updated.AwardedEntities = existing.AwardedEntities - updated.ContractNumber = existing.ContractNumber - } - - // Merge modifications (append new modifications to existing ones) - if len(existing.Modifications) > 0 { - // Create a map to avoid duplicate modifications - modMap := make(map[string]tender.TenderModification) - - // Add existing modifications - for _, mod := range existing.Modifications { - key := fmt.Sprintf("%d_%s", mod.ModificationDate, mod.ModificationReason) - modMap[key] = mod - } - - // Add new modifications - for _, mod := range updated.Modifications { - key := fmt.Sprintf("%d_%s", mod.ModificationDate, mod.ModificationReason) - modMap[key] = mod - } - - // Convert back to slice - updated.Modifications = make([]tender.TenderModification, 0, len(modMap)) - for _, mod := range modMap { - updated.Modifications = append(updated.Modifications, mod) - } - } -} diff --git a/ted/ted.go b/ted/ted.go deleted file mode 100644 index 70e9232..0000000 --- a/ted/ted.go +++ /dev/null @@ -1,2241 +0,0 @@ -package ted - -import ( - "encoding/xml" - "fmt" - "strings" - "time" -) - -// DocumentType represents the type of TED document -type DocumentType string - -// Document type constants for all supported TED XML notice types -const ( - DocumentTypeContractNotice DocumentType = "ContractNotice" - DocumentTypeContractAwardNotice DocumentType = "ContractAwardNotice" - DocumentTypePriorInformationNotice DocumentType = "PriorInformationNotice" - DocumentTypeDesignContest DocumentType = "DesignContest" - DocumentTypeCorrigendum DocumentType = "Corrigendum" - DocumentTypeModificationNotice DocumentType = "ModificationNotice" - DocumentTypeConcessionNotice DocumentType = "ConcessionNotice" - DocumentTypeQualificationSystemNotice DocumentType = "QualificationSystemNotice" - DocumentTypePlanningNotice DocumentType = "PlanningNotice" - DocumentTypeCompetitionNotice DocumentType = "CompetitionNotice" - DocumentTypeResultNotice DocumentType = "ResultNotice" - DocumentTypeChangeNotice DocumentType = "ChangeNotice" - DocumentTypeVoluntaryExAnte DocumentType = "VoluntaryExAnte" - DocumentTypeSubcontractNotice DocumentType = "SubcontractNotice" -) - -// NoticeStatus represents the status of a TED notice -type NoticeStatus string - -// Notice status constants for all possible TED notice statuses -const ( - NoticeStatusDraft NoticeStatus = "Draft" - NoticeStatusPublished NoticeStatus = "Published" - NoticeStatusCancelled NoticeStatus = "Cancelled" - NoticeStatusAwarded NoticeStatus = "Awarded" - NoticeStatusClosed NoticeStatus = "Closed" - NoticeStatusModified NoticeStatus = "Modified" - NoticeStatusSuspended NoticeStatus = "Suspended" -) - -// ParsedDocument wraps any TED document with type information -type ParsedDocument struct { - Type DocumentType `json:"type"` - Status NoticeStatus `json:"status"` - ContractNotice *ContractNotice `json:"contract_notice,omitempty"` - ContractAwardNotice *ContractAwardNotice `json:"contract_award_notice,omitempty"` - PriorInformationNotice *PriorInformationNotice `json:"prior_information_notice,omitempty"` - DesignContest *DesignContest `json:"design_contest,omitempty"` - QualificationSystemNotice *QualificationSystemNotice `json:"qualification_system_notice,omitempty"` - ConcessionNotice *ConcessionNotice `json:"concession_notice,omitempty"` - PlanningNotice *PlanningNotice `json:"planning_notice,omitempty"` - CompetitionNotice *CompetitionNotice `json:"competition_notice,omitempty"` - ResultNotice *ResultNotice `json:"result_notice,omitempty"` - ChangeNotice *ChangeNotice `json:"change_notice,omitempty"` - SubcontractNotice *SubcontractNotice `json:"subcontract_notice,omitempty"` -} - -// GetDocument returns the underlying document interface -func (pd *ParsedDocument) GetDocument() TEDDocument { - switch pd.Type { - case DocumentTypeContractNotice: - return pd.ContractNotice - case DocumentTypeContractAwardNotice: - return pd.ContractAwardNotice - case DocumentTypePriorInformationNotice: - return pd.PriorInformationNotice - case DocumentTypeDesignContest: - return pd.DesignContest - case DocumentTypeQualificationSystemNotice: - return pd.QualificationSystemNotice - case DocumentTypeConcessionNotice: - return pd.ConcessionNotice - case DocumentTypePlanningNotice: - return pd.PlanningNotice - case DocumentTypeCompetitionNotice: - return pd.CompetitionNotice - case DocumentTypeResultNotice: - return pd.ResultNotice - case DocumentTypeChangeNotice: - return pd.ChangeNotice - case DocumentTypeSubcontractNotice: - return pd.SubcontractNotice - default: - return nil - } -} - -// TEDDocument represents the common interface for all TED document types -type TEDDocument interface { - GetID() string - GetNoticeType() string - GetLanguage() string - GetNoticeStatus() NoticeStatus - Validate() error -} - -// PriorInformationNotice represents a prior information notice -type PriorInformationNotice struct { - XMLName xml.Name `xml:"PriorInformationNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - ProcurementProjectLot []ProcurementProjectLot `xml:"ProcurementProjectLot,omitempty"` - AdditionalDocumentReference []AdditionalDocumentReference `xml:"AdditionalDocumentReference,omitempty"` -} - -// Interface methods for PriorInformationNotice -func (pin PriorInformationNotice) GetID() string { - return pin.ID -} - -func (pin PriorInformationNotice) GetNoticeType() string { - return pin.NoticeTypeCode -} - -func (pin PriorInformationNotice) GetLanguage() string { - return pin.NoticeLanguageCode -} - -func (pin PriorInformationNotice) GetNoticeStatus() NoticeStatus { - // Prior information notices are typically published - return NoticeStatusPublished -} - -func (pin PriorInformationNotice) Validate() error { - if pin.ID == "" { - return fmt.Errorf("prior information notice ID is required") - } - if pin.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - return nil -} - -// DesignContest represents a design contest notice -type DesignContest struct { - XMLName xml.Name `xml:"DesignContest"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - ContestDesignJury *ContestDesignJury `xml:"ContestDesignJury,omitempty"` - ContestDesignReward []ContestDesignReward `xml:"ContestDesignReward,omitempty"` -} - -// Interface methods for DesignContest -func (dc DesignContest) GetID() string { return dc.ID } -func (dc DesignContest) GetNoticeType() string { return dc.NoticeTypeCode } -func (dc DesignContest) GetLanguage() string { return dc.NoticeLanguageCode } -func (dc DesignContest) GetNoticeStatus() NoticeStatus { return NoticeStatusPublished } -func (dc DesignContest) Validate() error { - if dc.ID == "" { - return fmt.Errorf("design contest ID is required") - } - if dc.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - return nil -} - -// QualificationSystemNotice represents a qualification system notice -type QualificationSystemNotice struct { - XMLName xml.Name `xml:"QualificationSystemNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - QualificationApplication []QualificationApplication `xml:"QualificationApplication,omitempty"` -} - -// Interface methods for QualificationSystemNotice -func (qsn QualificationSystemNotice) GetID() string { return qsn.ID } -func (qsn QualificationSystemNotice) GetNoticeType() string { return qsn.NoticeTypeCode } -func (qsn QualificationSystemNotice) GetLanguage() string { return qsn.NoticeLanguageCode } -func (qsn QualificationSystemNotice) GetNoticeStatus() NoticeStatus { return NoticeStatusPublished } -func (qsn QualificationSystemNotice) Validate() error { - if qsn.ID == "" { - return fmt.Errorf("qualification system notice ID is required") - } - if qsn.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - return nil -} - -// ConcessionNotice represents a concession notice -type ConcessionNotice struct { - XMLName xml.Name `xml:"ConcessionNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - ProcurementProjectLot []ProcurementProjectLot `xml:"ProcurementProjectLot,omitempty"` - ConcessionRevenue *ConcessionRevenue `xml:"ConcessionRevenue,omitempty"` - ConcessionTerm *ConcessionTerm `xml:"ConcessionTerm,omitempty"` -} - -// Interface methods for ConcessionNotice -func (cn ConcessionNotice) GetID() string { return cn.ID } -func (cn ConcessionNotice) GetNoticeType() string { return cn.NoticeTypeCode } -func (cn ConcessionNotice) GetLanguage() string { return cn.NoticeLanguageCode } -func (cn ConcessionNotice) GetNoticeStatus() NoticeStatus { return NoticeStatusPublished } -func (cn ConcessionNotice) Validate() error { - if cn.ID == "" { - return fmt.Errorf("concession notice ID is required") - } - if cn.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - return nil -} - -// PlanningNotice represents a planning notice -type PlanningNotice struct { - XMLName xml.Name `xml:"PlanningNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - ProcurementProjectLot []ProcurementProjectLot `xml:"ProcurementProjectLot,omitempty"` - BudgetAccountLine []BudgetAccountLine `xml:"BudgetAccountLine,omitempty"` - PlannedPeriod *PlannedPeriod `xml:"PlannedPeriod,omitempty"` -} - -// Interface methods for PlanningNotice -func (pn PlanningNotice) GetID() string { return pn.ID } -func (pn PlanningNotice) GetNoticeType() string { return pn.NoticeTypeCode } -func (pn PlanningNotice) GetLanguage() string { return pn.NoticeLanguageCode } -func (pn PlanningNotice) GetNoticeStatus() NoticeStatus { return NoticeStatusPublished } -func (pn PlanningNotice) Validate() error { - if pn.ID == "" { - return fmt.Errorf("planning notice ID is required") - } - if pn.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - return nil -} - -// CompetitionNotice represents a competition notice -type CompetitionNotice struct { - XMLName xml.Name `xml:"CompetitionNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - ProcurementProjectLot []ProcurementProjectLot `xml:"ProcurementProjectLot,omitempty"` -} - -// Interface methods for CompetitionNotice -func (cn CompetitionNotice) GetID() string { return cn.ID } -func (cn CompetitionNotice) GetNoticeType() string { return cn.NoticeTypeCode } -func (cn CompetitionNotice) GetLanguage() string { return cn.NoticeLanguageCode } -func (cn CompetitionNotice) GetNoticeStatus() NoticeStatus { return NoticeStatusPublished } -func (cn CompetitionNotice) Validate() error { - if cn.ID == "" { - return fmt.Errorf("competition notice ID is required") - } - if cn.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - return nil -} - -// ResultNotice represents a result notice (eForms specific) -type ResultNotice struct { - XMLName xml.Name `xml:"ResultNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - ProcurementProjectLot []ProcurementProjectLot `xml:"ProcurementProjectLot,omitempty"` - TenderResult []TenderResult `xml:"TenderResult,omitempty"` - SettledContract []SettledContract `xml:"SettledContract,omitempty"` -} - -// Interface methods for ResultNotice -func (rn ResultNotice) GetID() string { return rn.ID } -func (rn ResultNotice) GetNoticeType() string { return rn.NoticeTypeCode } -func (rn ResultNotice) GetLanguage() string { return rn.NoticeLanguageCode } -func (rn ResultNotice) GetNoticeStatus() NoticeStatus { return NoticeStatusAwarded } -func (rn ResultNotice) Validate() error { - if rn.ID == "" { - return fmt.Errorf("result notice ID is required") - } - if rn.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - return nil -} - -// ChangeNotice represents a change notice (eForms specific) -type ChangeNotice struct { - XMLName xml.Name `xml:"ChangeNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - ProcurementProjectLot []ProcurementProjectLot `xml:"ProcurementProjectLot,omitempty"` - NoticeVersionIdentifier string `xml:"NoticeVersionIdentifier,omitempty"` - ChangeReason []ChangeReason `xml:"ChangeReason,omitempty"` -} - -// Interface methods for ChangeNotice -func (cn ChangeNotice) GetID() string { return cn.ID } -func (cn ChangeNotice) GetNoticeType() string { return cn.NoticeTypeCode } -func (cn ChangeNotice) GetLanguage() string { return cn.NoticeLanguageCode } -func (cn ChangeNotice) GetNoticeStatus() NoticeStatus { return NoticeStatusModified } -func (cn ChangeNotice) Validate() error { - if cn.ID == "" { - return fmt.Errorf("change notice ID is required") - } - if cn.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - return nil -} - -// SubcontractNotice represents a subcontract notice -type SubcontractNotice struct { - XMLName xml.Name `xml:"SubcontractNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - SubcontractCall []SubcontractCall `xml:"SubcontractCall,omitempty"` - ParentContract *ParentContract `xml:"ParentContract,omitempty"` -} - -// Interface methods for SubcontractNotice -func (sn SubcontractNotice) GetID() string { return sn.ID } -func (sn SubcontractNotice) GetNoticeType() string { return sn.NoticeTypeCode } -func (sn SubcontractNotice) GetLanguage() string { return sn.NoticeLanguageCode } -func (sn SubcontractNotice) GetNoticeStatus() NoticeStatus { return NoticeStatusPublished } -func (sn SubcontractNotice) Validate() error { - if sn.ID == "" { - return fmt.Errorf("subcontract notice ID is required") - } - if sn.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - return nil -} - -// Additional structures for new notice types - -// ContestDesignJury contains design contest jury information -type ContestDesignJury struct { - XMLName xml.Name `xml:"ContestDesignJury"` - JuryDecisionDate string `xml:"JuryDecisionDate,omitempty"` - JuryDecision string `xml:"JuryDecision,omitempty"` - JuryMember []JuryMember `xml:"JuryMember,omitempty"` - JudgingCriteria *JudgingCriteria `xml:"JudgingCriteria,omitempty"` -} - -// ContestDesignReward contains design contest reward information -type ContestDesignReward struct { - XMLName xml.Name `xml:"ContestDesignReward"` - RewardCode string `xml:"RewardCode,omitempty"` - Benefit *Benefit `xml:"Benefit,omitempty"` -} - -// JuryMember contains jury member information -type JuryMember struct { - XMLName xml.Name `xml:"JuryMember"` - Party *Party `xml:"Party,omitempty"` -} - -// JudgingCriteria contains judging criteria information -type JudgingCriteria struct { - XMLName xml.Name `xml:"JudgingCriteria"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// Benefit contains benefit information -type Benefit struct { - XMLName xml.Name `xml:"Benefit"` - BenefitValue CurrencyText `xml:"BenefitValue,omitempty"` -} - -// QualificationApplication contains qualification application information -type QualificationApplication struct { - XMLName xml.Name `xml:"QualificationApplication"` - QualificationTypeCode string `xml:"QualificationTypeCode,omitempty"` - ApplicationPeriod *ApplicationPeriod `xml:"ApplicationPeriod,omitempty"` - TendererQualificationRequest *TendererQualificationRequest `xml:"TendererQualificationRequest,omitempty"` -} - -// ApplicationPeriod contains application period information -type ApplicationPeriod struct { - XMLName xml.Name `xml:"ApplicationPeriod"` - StartDate string `xml:"StartDate,omitempty"` - EndDate string `xml:"EndDate,omitempty"` - StartTime string `xml:"StartTime,omitempty"` - EndTime string `xml:"EndTime,omitempty"` -} - -// ConcessionRevenue contains concession revenue information -type ConcessionRevenue struct { - XMLName xml.Name `xml:"ConcessionRevenue"` - RevenueType string `xml:"RevenueType,omitempty"` - RevenueAmount CurrencyText `xml:"RevenueAmount,omitempty"` -} - -// ConcessionTerm contains concession term information -type ConcessionTerm struct { - XMLName xml.Name `xml:"ConcessionTerm"` - ConcessionValue CurrencyText `xml:"ConcessionValue,omitempty"` - Duration *DurationMeasure `xml:"DurationMeasure,omitempty"` -} - -// BudgetAccountLine contains budget account line information -type BudgetAccountLine struct { - XMLName xml.Name `xml:"BudgetAccountLine"` - ID string `xml:"ID,omitempty"` - BudgetYear string `xml:"BudgetYear,omitempty"` - TotalAmount CurrencyText `xml:"TotalAmount,omitempty"` - BudgetAccount *BudgetAccount `xml:"BudgetAccount,omitempty"` -} - -// BudgetAccount contains budget account information -type BudgetAccount struct { - XMLName xml.Name `xml:"BudgetAccount"` - BudgetCode string `xml:"BudgetCode,omitempty"` - RequiredClassificationScheme *RequiredClassificationScheme `xml:"RequiredClassificationScheme,omitempty"` -} - -// RequiredClassificationScheme contains classification scheme information -type RequiredClassificationScheme struct { - XMLName xml.Name `xml:"RequiredClassificationScheme"` - ItemClassificationCode string `xml:"ItemClassificationCode,omitempty"` -} - -// SubcontractCall contains subcontract call information -type SubcontractCall struct { - XMLName xml.Name `xml:"SubcontractCall"` - SubcontractingTypeCode string `xml:"SubcontractingTypeCode,omitempty"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` -} - -// ParentContract contains parent contract information -type ParentContract struct { - XMLName xml.Name `xml:"ParentContract"` - ContractReference *ContractReference `xml:"ContractReference,omitempty"` - FrameworkAgreement *FrameworkAgreement `xml:"FrameworkAgreement,omitempty"` -} - -// FrameworkAgreement contains framework agreement information -type FrameworkAgreement struct { - XMLName xml.Name `xml:"FrameworkAgreement"` - EstimatedMaximumValue CurrencyText `xml:"EstimatedMaximumValue,omitempty"` - MaximumParticipants string `xml:"MaximumParticipants,omitempty"` - Duration *DurationMeasure `xml:"DurationMeasure,omitempty"` - SubsequentProcedureIndicator string `xml:"SubsequentProcedureIndicator,omitempty"` -} - -// AdditionalDocumentReference contains additional document reference information -type AdditionalDocumentReference struct { - XMLName xml.Name `xml:"AdditionalDocumentReference"` - ID string `xml:"ID,omitempty"` - DocumentType string `xml:"DocumentType,omitempty"` - DocumentDescription string `xml:"DocumentDescription,omitempty"` - LanguageID string `xml:"LanguageID,omitempty"` - Attachment *Attachment `xml:"Attachment,omitempty"` - ValidityPeriod *ValidityPeriod `xml:"ValidityPeriod,omitempty"` - UBLExtensions *UBLExtensions `xml:"UBLExtensions,omitempty"` -} - -// ValidityPeriod contains validity period information -type ValidityPeriod struct { - XMLName xml.Name `xml:"ValidityPeriod"` - StartDate string `xml:"StartDate,omitempty"` - EndDate string `xml:"EndDate,omitempty"` - StartTime string `xml:"StartTime,omitempty"` - EndTime string `xml:"EndTime,omitempty"` -} - -// EnhancedTenderResult structure with more comprehensive information -type EnhancedTenderResult struct { - XMLName xml.Name `xml:"TenderResult"` - AwardDate string `xml:"AwardDate"` - AwardTime string `xml:"AwardTime,omitempty"` - ReceivedTenderQuantity string `xml:"ReceivedTenderQuantity,omitempty"` - LowerTenderAmount CurrencyText `xml:"LowerTenderAmount,omitempty"` - HigherTenderAmount CurrencyText `xml:"HigherTenderAmount,omitempty"` - AwardedTenderedProject *AwardedTenderedProject `xml:"AwardedTenderedProject,omitempty"` - ContractAwardNotice *ContractAwardNotice `xml:"ContractAwardNotice,omitempty"` - SubcontractingTerm []SubcontractingTerm `xml:"SubcontractingTerm,omitempty"` - WinningTenderLot []WinningTenderLot `xml:"WinningTenderLot,omitempty"` - ReceivedSubmissionsStatistics []ReceivedSubmissionsStatistics `xml:"ReceivedSubmissionsStatistics,omitempty"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` -} - -// AwardedTenderedProject contains awarded tendered project information -type AwardedTenderedProject struct { - XMLName xml.Name `xml:"AwardedTenderedProject"` - ProcurementTypeCode string `xml:"ProcurementTypeCode,omitempty"` - TotalAmount CurrencyText `xml:"TotalAmount,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - LegalMonetaryTotal *LegalMonetaryTotal `xml:"LegalMonetaryTotal,omitempty"` - TenderLot []TenderLot `xml:"TenderLot,omitempty"` -} - -// WinningTenderLot contains winning tender lot information -type WinningTenderLot struct { - XMLName xml.Name `xml:"WinningTenderLot"` - TenderLot *TenderLot `xml:"TenderLot,omitempty"` - WinningTender []WinningTender `xml:"WinningTender,omitempty"` -} - -// WinningTender contains winning tender information -type WinningTender struct { - XMLName xml.Name `xml:"WinningTender"` - ID string `xml:"ID"` - TenderVariantIndicator string `xml:"TenderVariantIndicator,omitempty"` - LegalMonetaryTotal *LegalMonetaryTotal `xml:"LegalMonetaryTotal,omitempty"` - TenderingParty *TenderingParty `xml:"TenderingParty,omitempty"` - SubcontractingTerm []SubcontractingTerm `xml:"SubcontractingTerm,omitempty"` -} - -// Enhanced SubcontractingTerm structure -type SubcontractingTerm struct { - XMLName xml.Name `xml:"SubcontractingTerm"` - TermCode string `xml:"TermCode"` - Amount CurrencyText `xml:"Amount,omitempty"` - Rate string `xml:"Rate,omitempty"` - Description string `xml:"Description,omitempty"` - UnknownPriceIndicator string `xml:"UnknownPriceIndicator,omitempty"` - SubcontractingDescription string `xml:"SubcontractingDescription,omitempty"` -} - -// NoticeStatusInformation contains status-specific fields -type NoticeStatusInformation struct { - Status NoticeStatus `xml:"NoticeStatus,omitempty"` - StatusCode string `xml:"StatusCode,omitempty"` - CancellationReason *CancellationReason `xml:"CancellationReason,omitempty"` - AwardInformation *AwardInformation `xml:"AwardInformation,omitempty"` - Modifications []NoticeModification `xml:"Modifications>Modification,omitempty"` - SuspensionReason *SuspensionReason `xml:"SuspensionReason,omitempty"` -} - -// CancellationReason contains cancellation information -type CancellationReason struct { - XMLName xml.Name `xml:"CancellationReason"` - ReasonCode string `xml:"ReasonCode"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// AwardInformation contains award-specific information -type AwardInformation struct { - XMLName xml.Name `xml:"AwardInformation"` - AwardDate string `xml:"AwardDate"` - AwardedValue CurrencyText `xml:"AwardedValue,omitempty"` - WinningTenderer *WinningTenderer `xml:"WinningTenderer,omitempty"` - ContractNumber string `xml:"ContractNumber,omitempty"` -} - -// WinningTenderer contains information about the winning tenderer -type WinningTenderer struct { - XMLName xml.Name `xml:"WinningTenderer"` - NaturalPersonIndicator string `xml:"NaturalPersonIndicator,omitempty"` - Company *Company `xml:"Company,omitempty"` -} - -// NoticeModification contains modification information -type NoticeModification struct { - XMLName xml.Name `xml:"Modification"` - ModificationDate string `xml:"ModificationDate"` - ModificationReason string `xml:"ModificationReason"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// SuspensionReason contains suspension information -type SuspensionReason struct { - XMLName xml.Name `xml:"SuspensionReason"` - ReasonCode string `xml:"ReasonCode"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// ContractNotice represents the main TED XML contract notice structure -type ContractNotice struct { - XMLName xml.Name `xml:"ContractNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - StatusInformation *NoticeStatusInformation `xml:"StatusInformation,omitempty"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - ProcurementProjectLot *ProcurementProjectLot `xml:"ProcurementProjectLot,omitempty"` -} - -// ContractAwardNotice represents the main TED XML contract award notice structure -type ContractAwardNotice struct { - XMLName xml.Name `xml:"ContractAwardNotice"` - Xmlns string `xml:"xmlns,attr,omitempty"` - XmlnsCac string `xml:"xmlns:cac,attr,omitempty"` - XmlnsCbc string `xml:"xmlns:cbc,attr,omitempty"` - XmlnsEfac string `xml:"xmlns:efac,attr,omitempty"` - XmlnsEfbc string `xml:"xmlns:efbc,attr,omitempty"` - XmlnsEfext string `xml:"xmlns:efext,attr,omitempty"` - XmlnsExt string `xml:"xmlns:ext,attr,omitempty"` - Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"` - UBLVersionID string `xml:"UBLVersionID"` - CustomizationID string `xml:"CustomizationID"` - ID string `xml:"ID"` - ContractFolderID string `xml:"ContractFolderID"` - IssueDate string `xml:"IssueDate"` - IssueTime string `xml:"IssueTime"` - VersionID string `xml:"VersionID"` - RegulatoryDomain string `xml:"RegulatoryDomain"` - NoticeTypeCode string `xml:"NoticeTypeCode"` - NoticeLanguageCode string `xml:"NoticeLanguageCode"` - StatusInformation *NoticeStatusInformation `xml:"StatusInformation,omitempty"` - ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` - ProcurementProjectLot []ProcurementProjectLot `xml:"ProcurementProjectLot,omitempty"` - TenderResult *TenderResult `xml:"TenderResult,omitempty"` -} - -// NoticeResult contains notice result information from extensions -type NoticeResult struct { - XMLName xml.Name `xml:"NoticeResult"` - TotalAmount *CurrencyText `xml:"TotalAmount,omitempty"` - LotResult []LotResult `xml:"LotResult,omitempty"` - LotTender []LotTender `xml:"LotTender,omitempty"` - SettledContract []SettledContract `xml:"SettledContract,omitempty"` - TenderingParty []TenderingParty `xml:"TenderingParty,omitempty"` -} - -// LotResult contains lot result information -type LotResult struct { - XMLName xml.Name `xml:"LotResult"` - ID string `xml:"ID"` - TenderResultCode string `xml:"TenderResultCode"` - LotTender []LotTender `xml:"LotTender,omitempty"` - ReceivedSubmissionsStatistics []ReceivedSubmissionsStatistics `xml:"ReceivedSubmissionsStatistics,omitempty"` - SettledContract []SettledContract `xml:"SettledContract,omitempty"` - TenderLot *TenderLot `xml:"TenderLot,omitempty"` -} - -// LotTender contains lot tender information -type LotTender struct { - XMLName xml.Name `xml:"LotTender"` - ID string `xml:"ID"` - RankCode string `xml:"RankCode,omitempty"` - TenderRankedIndicator string `xml:"TenderRankedIndicator,omitempty"` - TenderVariantIndicator string `xml:"TenderVariantIndicator,omitempty"` - LegalMonetaryTotal *LegalMonetaryTotal `xml:"LegalMonetaryTotal,omitempty"` - SubcontractingTerm *SubcontractingTerm `xml:"SubcontractingTerm,omitempty"` - TenderingParty *TenderingParty `xml:"TenderingParty,omitempty"` - TenderLot *TenderLot `xml:"TenderLot,omitempty"` - TenderReference *TenderReference `xml:"TenderReference,omitempty"` -} - -// LegalMonetaryTotal contains monetary total information -type LegalMonetaryTotal struct { - XMLName xml.Name `xml:"LegalMonetaryTotal"` - PayableAmount CurrencyText `xml:"PayableAmount"` -} - -// TenderingParty contains tendering party information -type TenderingParty struct { - XMLName xml.Name `xml:"TenderingParty"` - ID string `xml:"ID"` - Tenderer []Tenderer `xml:"Tenderer,omitempty"` -} - -// Tenderer contains tenderer information -type Tenderer struct { - XMLName xml.Name `xml:"Tenderer"` - ID string `xml:"ID"` -} - -// TenderLot contains tender lot information -type TenderLot struct { - XMLName xml.Name `xml:"TenderLot"` - ID string `xml:"ID"` -} - -// TenderReference contains tender reference information -type TenderReference struct { - XMLName xml.Name `xml:"TenderReference"` - ID string `xml:"ID"` -} - -// ReceivedSubmissionsStatistics contains received submissions statistics -type ReceivedSubmissionsStatistics struct { - XMLName xml.Name `xml:"ReceivedSubmissionsStatistics"` - StatisticsCode string `xml:"StatisticsCode"` - StatisticsNumeric string `xml:"StatisticsNumeric"` -} - -// SettledContract contains settled contract information -type SettledContract struct { - XMLName xml.Name `xml:"SettledContract"` - ID string `xml:"ID"` - IssueDate string `xml:"IssueDate,omitempty"` - AwardDate string `xml:"AwardDate,omitempty"` - Title string `xml:"Title,omitempty"` - ContractFrameworkIndicator string `xml:"ContractFrameworkIndicator,omitempty"` - SignatoryParty *SignatoryParty `xml:"SignatoryParty,omitempty"` - ContractReference *ContractReference `xml:"ContractReference,omitempty"` - LotTender *LotTender `xml:"LotTender,omitempty"` - NoticeDocumentReference *NoticeDocumentReference `xml:"NoticeDocumentReference,omitempty"` -} - -// SignatoryParty contains signatory party information -type SignatoryParty struct { - XMLName xml.Name `xml:"SignatoryParty"` - PartyIdentification *PartyIdentification `xml:"PartyIdentification,omitempty"` -} - -// ContractReference contains contract reference information -type ContractReference struct { - XMLName xml.Name `xml:"ContractReference"` - ID string `xml:"ID"` -} - -// TenderResult contains tender result information -type TenderResult struct { - XMLName xml.Name `xml:"TenderResult"` - AwardDate string `xml:"AwardDate"` -} - -// NoticeDocumentReference contains notice document reference information -type NoticeDocumentReference struct { - XMLName xml.Name `xml:"NoticeDocumentReference"` - ID string `xml:"ID"` -} - -// PresentationPeriod contains presentation period information -type PresentationPeriod struct { - XMLName xml.Name `xml:"PresentationPeriod"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// AppealInformationParty contains appeal information party -type AppealInformationParty struct { - XMLName xml.Name `xml:"AppealInformationParty"` - PartyIdentification *PartyIdentification `xml:"PartyIdentification,omitempty"` -} - -// MediationParty contains mediation party information -type MediationParty struct { - XMLName xml.Name `xml:"MediationParty"` - PartyIdentification *PartyIdentification `xml:"PartyIdentification,omitempty"` -} - -// UBLExtensions contains eForms extensions -type UBLExtensions struct { - XMLName xml.Name `xml:"UBLExtensions"` - UBLExtension *UBLExtension `xml:"UBLExtension,omitempty"` -} - -// UBLExtension contains extension content -type UBLExtension struct { - XMLName xml.Name `xml:"UBLExtension"` - ExtensionContent *ExtensionContent `xml:"ExtensionContent,omitempty"` -} - -// ExtensionContent contains eForms extension data -type ExtensionContent struct { - XMLName xml.Name `xml:"ExtensionContent"` - EformsExtension *EformsExtension `xml:"EformsExtension,omitempty"` -} - -// EformsExtension contains eForms specific data -type EformsExtension struct { - XMLName xml.Name `xml:"EformsExtension"` - NoticeResult *NoticeResult `xml:"NoticeResult,omitempty"` - NoticeSubType *NoticeSubType `xml:"NoticeSubType,omitempty"` - Organizations *Organizations `xml:"Organizations,omitempty"` - Publication *Publication `xml:"Publication,omitempty"` - Changes *Changes `xml:"Changes,omitempty"` - SelectionCriteria []SelectionCriteria `xml:"SelectionCriteria,omitempty"` - OfficialLanguages *OfficialLanguages `xml:"OfficialLanguages,omitempty"` - AwardCriterionParameter *AwardCriterionParameter `xml:"AwardCriterionParameter,omitempty"` - AccessToolName string `xml:"AccessToolName,omitempty"` -} - -// NoticeSubType contains notice subtype information -type NoticeSubType struct { - XMLName xml.Name `xml:"NoticeSubType"` - SubTypeCode string `xml:"SubTypeCode"` -} - -// SelectionCriteria contains tenderer requirement information -type SelectionCriteria struct { - XMLName xml.Name `xml:"SelectionCriteria"` - CriterionTypeCode string `xml:"CriterionTypeCode"` - TendererRequirementTypeCode string `xml:"TendererRequirementTypeCode"` - Name string `xml:"Name,omitempty"` - Description string `xml:"Description,omitempty"` - CalculationExpressionCode string `xml:"CalculationExpressionCode,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// OfficialLanguages contains official language information -type OfficialLanguages struct { - XMLName xml.Name `xml:"OfficialLanguages"` - Language []Language `xml:"Language"` -} - -// Language represents a language entry -type Language struct { - XMLName xml.Name `xml:"Language"` - ID string `xml:"ID"` -} - -// Organizations contains organization information -type Organizations struct { - XMLName xml.Name `xml:"Organizations"` - Organization []Organization `xml:"Organization"` -} - -// Organization represents an organization entity -type Organization struct { - XMLName xml.Name `xml:"Organization"` - NaturalPersonIndicator string `xml:"NaturalPersonIndicator,omitempty"` - Company *Company `xml:"Company,omitempty"` -} - -// Company represents company informationf -type Company struct { - XMLName xml.Name `xml:"Company"` - WebsiteURI string `xml:"WebsiteURI,omitempty"` - PartyIdentification *PartyIdentification `xml:"PartyIdentification,omitempty"` - PartyName *PartyName `xml:"PartyName,omitempty"` - PostalAddress *PostalAddress `xml:"PostalAddress,omitempty"` - PartyLegalEntity *PartyLegalEntity `xml:"PartyLegalEntity,omitempty"` - Contact *Contact `xml:"Contact,omitempty"` -} - -// PartyIdentification contains party ID information -type PartyIdentification struct { - XMLName xml.Name `xml:"PartyIdentification"` - ID string `xml:"ID"` -} - -// PartyName contains party name -type PartyName struct { - XMLName xml.Name `xml:"PartyName"` - Name string `xml:"Name"` -} - -// PostalAddress contains address information -type PostalAddress struct { - XMLName xml.Name `xml:"PostalAddress"` - StreetName string `xml:"StreetName,omitempty"` - CityName string `xml:"CityName,omitempty"` - PostalZone string `xml:"PostalZone,omitempty"` - CountrySubentityCode string `xml:"CountrySubentityCode,omitempty"` - Department string `xml:"Department,omitempty"` - Country *Country `xml:"Country,omitempty"` -} - -// Country contains country information -type Country struct { - XMLName xml.Name `xml:"Country"` - IdentificationCode string `xml:"IdentificationCode"` -} - -// PartyLegalEntity contains legal entity information -type PartyLegalEntity struct { - XMLName xml.Name `xml:"PartyLegalEntity"` - CompanyID string `xml:"CompanyID"` -} - -// Contact contains contact information -type Contact struct { - XMLName xml.Name `xml:"Contact"` - Name string `xml:"Name,omitempty"` - Telephone string `xml:"Telephone,omitempty"` - Telefax string `xml:"Telefax,omitempty"` - ElectronicMail string `xml:"ElectronicMail,omitempty"` -} - -// Publication contains publication information -type Publication struct { - XMLName xml.Name `xml:"Publication"` - NoticePublicationID string `xml:"NoticePublicationID"` - GazetteID string `xml:"GazetteID"` - PublicationDate string `xml:"PublicationDate"` -} - -// Changes contains notice change information -type Changes struct { - XMLName xml.Name `xml:"Changes"` - ChangedNoticeIdentifier string `xml:"ChangedNoticeIdentifier"` - ChangeReason *ChangeReason `xml:"ChangeReason,omitempty"` -} - -// ChangeReason contains change reason information -type ChangeReason struct { - XMLName xml.Name `xml:"ChangeReason"` - ReasonCode string `xml:"ReasonCode"` -} - -// ContractingParty contains contracting party information -type ContractingParty struct { - XMLName xml.Name `xml:"ContractingParty"` - BuyerProfileURI string `xml:"BuyerProfileURI,omitempty"` - ContractingPartyType *ContractingPartyType `xml:"ContractingPartyType,omitempty"` - ContractingActivity *ContractingActivity `xml:"ContractingActivity,omitempty"` - Party *Party `xml:"Party,omitempty"` -} - -// ContractingPartyType contains party type information -type ContractingPartyType struct { - XMLName xml.Name `xml:"ContractingPartyType"` - PartyTypeCode string `xml:"PartyTypeCode"` -} - -// ContractingActivity contains contracting activity information -type ContractingActivity struct { - XMLName xml.Name `xml:"ContractingActivity"` - ActivityTypeCode string `xml:"ActivityTypeCode"` -} - -// Party contains party information -type Party struct { - XMLName xml.Name `xml:"Party"` - PartyIdentification *PartyIdentification `xml:"PartyIdentification,omitempty"` - ServiceProviderParty *ServiceProviderParty `xml:"ServiceProviderParty,omitempty"` -} - -// ServiceProviderParty contains service provider information -type ServiceProviderParty struct { - XMLName xml.Name `xml:"ServiceProviderParty"` - ServiceTypeCode string `xml:"ServiceTypeCode,omitempty"` - Party *Party `xml:"Party,omitempty"` -} - -// TenderingTerms contains tendering terms -type TenderingTerms struct { - XMLName xml.Name `xml:"TenderingTerms"` - VariantConstraintCode string `xml:"VariantConstraintCode,omitempty"` - FundingProgramCode string `xml:"FundingProgramCode,omitempty"` - RecurringProcurementIndicator string `xml:"RecurringProcurementIndicator,omitempty"` - MultipleTendersCode string `xml:"MultipleTendersCode,omitempty"` - TendererQualificationRequest []TendererQualificationRequest `xml:"TendererQualificationRequest,omitempty"` - AppealTerms *AppealTerms `xml:"AppealTerms,omitempty"` - CallForTendersDocumentReference []CallForTendersDocumentReference `xml:"CallForTendersDocumentReference,omitempty"` - RequiredFinancialGuarantee *RequiredFinancialGuarantee `xml:"RequiredFinancialGuarantee,omitempty"` - PaymentTerms *PaymentTerms `xml:"PaymentTerms,omitempty"` - AwardingTerms *AwardingTerms `xml:"AwardingTerms,omitempty"` - AdditionalInformationParty *AdditionalInformationParty `xml:"AdditionalInformationParty,omitempty"` - TenderRecipientParty *TenderRecipientParty `xml:"TenderRecipientParty,omitempty"` - TenderValidityPeriod *TenderValidityPeriod `xml:"TenderValidityPeriod,omitempty"` - Language *Language `xml:"Language,omitempty"` - PostAwardProcess *PostAwardProcess `xml:"PostAwardProcess,omitempty"` - ContractExecutionRequirement []ContractExecutionRequirement `xml:"ContractExecutionRequirement,omitempty"` - UBLExtensions *UBLExtensions `xml:"UBLExtensions,omitempty"` -} - -// RequiredFinancialGuarantee contains financial guarantee information -type RequiredFinancialGuarantee struct { - XMLName xml.Name `xml:"RequiredFinancialGuarantee"` - GuaranteeTypeCode string `xml:"GuaranteeTypeCode,omitempty"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// PaymentTerms contains payment terms information -type PaymentTerms struct { - XMLName xml.Name `xml:"PaymentTerms"` - Note string `xml:"Note,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// AwardingTerms contains awarding terms information -type AwardingTerms struct { - XMLName xml.Name `xml:"AwardingTerms"` - AwardingCriterion []AwardingCriterion `xml:"AwardingCriterion,omitempty"` -} - -// AwardingCriterion contains awarding criteria information -type AwardingCriterion struct { - XMLName xml.Name `xml:"AwardingCriterion"` - SubordinateAwardingCriterion []SubordinateAwardingCriterion `xml:"SubordinateAwardingCriterion,omitempty"` -} - -// SubordinateAwardingCriterion contains subordinate awarding criteria -type SubordinateAwardingCriterion struct { - XMLName xml.Name `xml:"SubordinateAwardingCriterion"` - AwardingCriterionTypeCode string `xml:"AwardingCriterionTypeCode,omitempty"` - Name string `xml:"Name,omitempty"` - Description string `xml:"Description,omitempty"` - UBLExtensions *UBLExtensions `xml:"UBLExtensions,omitempty"` -} - -// AwardCriterionParameter contains award criterion parameter information -type AwardCriterionParameter struct { - XMLName xml.Name `xml:"AwardCriterionParameter"` - ParameterCode string `xml:"ParameterCode,omitempty"` - ParameterNumeric string `xml:"ParameterNumeric,omitempty"` -} - -// AdditionalInformationParty contains additional information party -type AdditionalInformationParty struct { - XMLName xml.Name `xml:"AdditionalInformationParty"` - PartyIdentification *PartyIdentification `xml:"PartyIdentification,omitempty"` -} - -// TenderRecipientParty contains tender recipient party information -type TenderRecipientParty struct { - XMLName xml.Name `xml:"TenderRecipientParty"` - EndpointID string `xml:"EndpointID,omitempty"` -} - -// TenderValidityPeriod contains tender validity period -type TenderValidityPeriod struct { - XMLName xml.Name `xml:"TenderValidityPeriod"` - DurationMeasure *DurationMeasure `xml:"DurationMeasure,omitempty"` -} - -// PostAwardProcess contains post award process information -type PostAwardProcess struct { - XMLName xml.Name `xml:"PostAwardProcess"` - ElectronicCatalogueUsageIndicator string `xml:"ElectronicCatalogueUsageIndicator,omitempty"` - ElectronicOrderUsageIndicator string `xml:"ElectronicOrderUsageIndicator,omitempty"` - ElectronicPaymentUsageIndicator string `xml:"ElectronicPaymentUsageIndicator,omitempty"` -} - -// ContractExecutionRequirement contains contract execution requirements -type ContractExecutionRequirement struct { - XMLName xml.Name `xml:"ContractExecutionRequirement"` - ExecutionRequirementCode string `xml:"ExecutionRequirementCode,omitempty"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// AppealTerms contains appeal terms information -type AppealTerms struct { - XMLName xml.Name `xml:"AppealTerms"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` - PresentationPeriod *PresentationPeriod `xml:"PresentationPeriod,omitempty"` - AppealInformationParty *AppealInformationParty `xml:"AppealInformationParty,omitempty"` - AppealReceiverParty *AppealReceiverParty `xml:"AppealReceiverParty,omitempty"` - MediationParty *MediationParty `xml:"MediationParty,omitempty"` -} - -// AppealReceiverParty contains appeal receiver party information -type AppealReceiverParty struct { - XMLName xml.Name `xml:"AppealReceiverParty"` - PartyIdentification *PartyIdentification `xml:"PartyIdentification,omitempty"` -} - -// CallForTendersDocumentReference contains document reference information -type CallForTendersDocumentReference struct { - XMLName xml.Name `xml:"CallForTendersDocumentReference"` - ID string `xml:"ID,omitempty"` - DocumentType string `xml:"DocumentType,omitempty"` - LanguageID string `xml:"LanguageID,omitempty"` - DocumentStatusCode string `xml:"DocumentStatusCode,omitempty"` - Attachment *Attachment `xml:"Attachment,omitempty"` - UBLExtensions *UBLExtensions `xml:"UBLExtensions,omitempty"` -} - -// Attachment contains attachment information -type Attachment struct { - XMLName xml.Name `xml:"Attachment"` - ExternalReference *ExternalReference `xml:"ExternalReference,omitempty"` -} - -// ExternalReference contains external reference information -type ExternalReference struct { - XMLName xml.Name `xml:"ExternalReference"` - URI string `xml:"URI"` - DocumentHash string `xml:"DocumentHash,omitempty"` - FileName string `xml:"FileName,omitempty"` -} - -// TendererQualificationRequest contains qualification request information -type TendererQualificationRequest struct { - XMLName xml.Name `xml:"TendererQualificationRequest"` - CompanyLegalFormCode string `xml:"CompanyLegalFormCode,omitempty"` - SpecificTendererRequirement []SpecificTendererRequirement `xml:"SpecificTendererRequirement,omitempty"` -} - -// SpecificTendererRequirement contains specific requirement information -type SpecificTendererRequirement struct { - XMLName xml.Name `xml:"SpecificTendererRequirement"` - TendererRequirementTypeCode string `xml:"TendererRequirementTypeCode"` - Description string `xml:"Description,omitempty"` -} - -// TenderingProcess contains tendering process information -type TenderingProcess struct { - XMLName xml.Name `xml:"TenderingProcess"` - Description string `xml:"Description,omitempty"` - ProcedureCode string `xml:"ProcedureCode"` - SubmissionMethodCode string `xml:"SubmissionMethodCode,omitempty"` - GovernmentAgreementConstraintIndicator string `xml:"GovernmentAgreementConstraintIndicator,omitempty"` - ProcessJustification *ProcessJustification `xml:"ProcessJustification,omitempty"` - TenderSubmissionDeadlinePeriod *TenderSubmissionDeadlinePeriod `xml:"TenderSubmissionDeadlinePeriod,omitempty"` - OpenTenderEvent *OpenTenderEvent `xml:"OpenTenderEvent,omitempty"` - AuctionTerms *AuctionTerms `xml:"AuctionTerms,omitempty"` - ContractingSystem []ContractingSystem `xml:"ContractingSystem,omitempty"` - NoticeDocumentReference []NoticeDocumentReference `xml:"NoticeDocumentReference,omitempty"` - UBLExtensions *UBLExtensions `xml:"UBLExtensions,omitempty"` -} - -// TenderSubmissionDeadlinePeriod contains tender submission deadline information -type TenderSubmissionDeadlinePeriod struct { - XMLName xml.Name `xml:"TenderSubmissionDeadlinePeriod"` - EndDate string `xml:"EndDate"` - EndTime string `xml:"EndTime,omitempty"` -} - -// OpenTenderEvent contains open tender event information -type OpenTenderEvent struct { - XMLName xml.Name `xml:"OpenTenderEvent"` - OccurrenceDate string `xml:"OccurrenceDate,omitempty"` - OccurrenceTime string `xml:"OccurrenceTime,omitempty"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` - OccurenceLocation *OccurenceLocation `xml:"OccurenceLocation,omitempty"` -} - -// OccurenceLocation contains occurrence location information -type OccurenceLocation struct { - XMLName xml.Name `xml:"OccurenceLocation"` - Description string `xml:"Description,omitempty"` - LanguageID string `xml:"languageID,attr,omitempty"` -} - -// AuctionTerms contains auction terms information -type AuctionTerms struct { - XMLName xml.Name `xml:"AuctionTerms"` - AuctionConstraintIndicator string `xml:"AuctionConstraintIndicator,omitempty"` -} - -// ContractingSystem contains contracting system information -type ContractingSystem struct { - XMLName xml.Name `xml:"ContractingSystem"` - ContractingSystemTypeCode string `xml:"ContractingSystemTypeCode,omitempty"` -} - -// ProcurementProject contains procurement project information -type ProcurementProject struct { - XMLName xml.Name `xml:"ProcurementProject"` - ID string `xml:"ID"` - Name string `xml:"Name"` - Description string `xml:"Description"` - ProcurementTypeCode string `xml:"ProcurementTypeCode"` - SMESuitableIndicator string `xml:"SMESuitableIndicator,omitempty"` - Note string `xml:"Note,omitempty"` - - RequestedTenderTotal *RequestedTenderTotal `xml:"RequestedTenderTotal,omitempty"` - MainCommodityClassification *MainCommodityClassification `xml:"MainCommodityClassification,omitempty"` - AdditionalCommodityClassification []AdditionalCommodityClassification `xml:"AdditionalCommodityClassification,omitempty"` - ProcurementAdditionalType *ProcurementAdditionalType `xml:"ProcurementAdditionalType,omitempty"` - RealizedLocation *RealizedLocation `xml:"RealizedLocation,omitempty"` - PlannedPeriod *PlannedPeriod `xml:"PlannedPeriod,omitempty"` - ContractExtension *ContractExtension `xml:"ContractExtension,omitempty"` -} - -// ProcurementAdditionalType contains additional procurement type information -type ProcurementAdditionalType struct { - XMLName xml.Name `xml:"ProcurementAdditionalType"` - ProcurementTypeCode string `xml:"ProcurementTypeCode,omitempty"` -} - -// ContractExtension contains contract extension information -type ContractExtension struct { - XMLName xml.Name `xml:"ContractExtension"` - MaximumNumberNumeric string `xml:"MaximumNumberNumeric,omitempty"` -} - -// ProcessJustification contains process justification -type ProcessJustification struct { - XMLName xml.Name `xml:"ProcessJustification"` - ProcessReasonCode string `xml:"ProcessReasonCode"` -} - -// RequestedTenderTotal contains tender total information -type RequestedTenderTotal struct { - XMLName xml.Name `xml:"RequestedTenderTotal"` - EstimatedOverallContractAmount CurrencyText `xml:"EstimatedOverallContractAmount"` -} - -// CurrencyText represents text with currency attribute -type CurrencyText struct { - CurrencyID string `xml:"currencyID,attr,omitempty"` - Text string `xml:",chardata"` -} - -// MainCommodityClassification contains main commodity classification -type MainCommodityClassification struct { - XMLName xml.Name `xml:"MainCommodityClassification"` - ItemClassificationCode string `xml:"ItemClassificationCode"` -} - -// AdditionalCommodityClassification contains additional commodity classification -type AdditionalCommodityClassification struct { - XMLName xml.Name `xml:"AdditionalCommodityClassification"` - ItemClassificationCode string `xml:"ItemClassificationCode"` -} - -// RealizedLocation contains location information -type RealizedLocation struct { - XMLName xml.Name `xml:"RealizedLocation"` - Description string `xml:"Description,omitempty"` - Address *Address `xml:"Address,omitempty"` -} - -// Address contains address information (different from PostalAddress) -type Address struct { - XMLName xml.Name `xml:"Address"` - StreetName string `xml:"StreetName,omitempty"` - AdditionalStreetName string `xml:"AdditionalStreetName,omitempty"` - CityName string `xml:"CityName,omitempty"` - PostalZone string `xml:"PostalZone,omitempty"` - CountrySubentityCode string `xml:"CountrySubentityCode,omitempty"` - Region string `xml:"Region,omitempty"` - Country *Country `xml:"Country,omitempty"` -} - -// PlannedPeriod contains planned period information -type PlannedPeriod struct { - XMLName xml.Name `xml:"PlannedPeriod"` - StartDate string `xml:"StartDate,omitempty"` - EndDate string `xml:"EndDate,omitempty"` - DurationMeasure *DurationMeasure `xml:"DurationMeasure,omitempty"` -} - -// DurationMeasure contains duration with unit information -type DurationMeasure struct { - XMLName xml.Name `xml:"DurationMeasure"` - UnitCode string `xml:"unitCode,attr,omitempty"` - Text string `xml:",chardata"` -} - -// ProcurementProjectLot contains lot information -type ProcurementProjectLot struct { - XMLName xml.Name `xml:"ProcurementProjectLot"` - ID string `xml:"ID"` - TenderingTerms *TenderingTerms `xml:"TenderingTerms,omitempty"` - TenderingProcess *TenderingProcess `xml:"TenderingProcess,omitempty"` - ProcurementProject *ProcurementProject `xml:"ProcurementProject,omitempty"` -} - -// Validate validates the ContractNotice structure -func (cn ContractNotice) Validate() error { - if cn.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - - // Validate date format - TED XML uses ISO 8601 format with timezone - validFormats := []string{ - "2006-01-02+07:00", - "2006-01-02-07:00", - "2006-01-02T15:04:05+07:00", - "2006-01-02T15:04:05-07:00", - "2006-01-02T15:04:05Z", - "2006-01-02Z", - "2006-01-02", - time.RFC3339, - time.RFC3339Nano, - } - - var validDate bool - for _, format := range validFormats { - if _, err := time.Parse(format, cn.IssueDate); err == nil { - validDate = true - break - } - } - - if !validDate { - return fmt.Errorf("invalid issue date format: %s (expected ISO 8601 format)", cn.IssueDate) - } - - // Validate language code if provided (should be 3-letter ISO code) - if cn.NoticeLanguageCode != "" && len(cn.NoticeLanguageCode) != 3 { - return fmt.Errorf("invalid notice language code: %s (expected 3-letter ISO code)", cn.NoticeLanguageCode) - } - - // Validate tender submission deadline format if present - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.TenderingProcess != nil && - cn.ProcurementProjectLot.TenderingProcess.TenderSubmissionDeadlinePeriod != nil { - - deadline := cn.ProcurementProjectLot.TenderingProcess.TenderSubmissionDeadlinePeriod.EndDate - if deadline != "" { - var validDeadline bool - for _, format := range validFormats { - if _, err := time.Parse(format, deadline); err == nil { - validDeadline = true - break - } - } - if !validDeadline { - return fmt.Errorf("invalid tender submission deadline format: %s (expected ISO 8601 format)", deadline) - } - } - } - - // Validate currency code if provided in budget - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.ProcurementProject != nil && - cn.ProcurementProjectLot.ProcurementProject.RequestedTenderTotal != nil { - - currency := cn.ProcurementProjectLot.ProcurementProject.RequestedTenderTotal.EstimatedOverallContractAmount.CurrencyID - if currency != "" && len(currency) != 3 { - return fmt.Errorf("invalid currency code: %s (expected 3-letter ISO code)", currency) - } - } - - // Validate duration unit code if provided - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.ProcurementProject != nil && - cn.ProcurementProjectLot.ProcurementProject.PlannedPeriod != nil && - cn.ProcurementProjectLot.ProcurementProject.PlannedPeriod.DurationMeasure != nil { - - unitCode := cn.ProcurementProjectLot.ProcurementProject.PlannedPeriod.DurationMeasure.UnitCode - if unitCode != "" { - validUnits := map[string]bool{ - "MONTH": true, - "DAY": true, - "WEEK": true, - "YEAR": true, - } - if !validUnits[unitCode] { - return fmt.Errorf("invalid duration unit code: %s (expected MONTH, DAY, WEEK, or YEAR)", unitCode) - } - } - } - - return nil -} - -// GetID returns the contract notice ID -func (cn ContractNotice) GetID() string { - return cn.ID -} - -// GetNoticeType returns the notice type -func (cn ContractNotice) GetNoticeType() string { - return cn.NoticeTypeCode -} - -// GetLanguage returns the notice language -func (cn ContractNotice) GetLanguage() string { - return cn.NoticeLanguageCode -} - -// GetNoticeStatus returns the notice status -func (cn ContractNotice) GetNoticeStatus() NoticeStatus { - if cn.StatusInformation != nil { - if cn.StatusInformation.Status != "" { - return cn.StatusInformation.Status - } - // Check for specific status indicators - if cn.StatusInformation.CancellationReason != nil { - return NoticeStatusCancelled - } - if cn.StatusInformation.AwardInformation != nil { - return NoticeStatusAwarded - } - if cn.StatusInformation.SuspensionReason != nil { - return NoticeStatusSuspended - } - if len(cn.StatusInformation.Modifications) > 0 { - return NoticeStatusModified - } - } - // Default to Published if no status information is available - return NoticeStatusPublished -} - -// GetCancellationReason returns the cancellation reason if notice is cancelled -func (cn ContractNotice) GetCancellationReason() *CancellationReason { - if cn.StatusInformation != nil { - return cn.StatusInformation.CancellationReason - } - return nil -} - -// GetAwardInformation returns the award information if notice is awarded -func (cn ContractNotice) GetAwardInformation() *AwardInformation { - if cn.StatusInformation != nil { - return cn.StatusInformation.AwardInformation - } - return nil -} - -// GetModifications returns the list of modifications if notice is modified -func (cn ContractNotice) GetModifications() []NoticeModification { - if cn.StatusInformation != nil { - return cn.StatusInformation.Modifications - } - return nil -} - -// GetSuspensionReason returns the suspension reason if notice is suspended -func (cn ContractNotice) GetSuspensionReason() *SuspensionReason { - if cn.StatusInformation != nil { - return cn.StatusInformation.SuspensionReason - } - return nil -} - -// GetPublicationInfo returns publication information if available -func (cn ContractNotice) GetPublicationInfo() *Publication { - if cn.Extensions != nil && - cn.Extensions.UBLExtension != nil && - cn.Extensions.UBLExtension.ExtensionContent != nil && - cn.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil { - return cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.Publication - } - return nil -} - -// GetOrganizations returns organizations from extensions -func (cn ContractNotice) GetOrganizations() *Organizations { - if cn.Extensions != nil && - cn.Extensions.UBLExtension != nil && - cn.Extensions.UBLExtension.ExtensionContent != nil && - cn.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil { - return cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.Organizations - } - return nil -} - -// GetNoticeSubType returns the notice subtype code from extensions -func (cn ContractNotice) GetNoticeSubType() string { - if cn.Extensions != nil && - cn.Extensions.UBLExtension != nil && - cn.Extensions.UBLExtension.ExtensionContent != nil && - cn.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil && - cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.NoticeSubType != nil { - return cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.NoticeSubType.SubTypeCode - } - return "" -} - -// GetNoticePublicationID returns the notice publication ID from extensions -func (cn ContractNotice) GetNoticePublicationID() string { - pub := cn.GetPublicationInfo() - if pub != nil { - return pub.NoticePublicationID - } - return "" -} - -// GetOJSID returns the OJS gazette ID from extensions -func (cn ContractNotice) GetOJSID() string { - pub := cn.GetPublicationInfo() - if pub != nil { - return pub.GazetteID - } - return "" -} - -// GetPublicationDate returns the publication date from extensions -func (cn ContractNotice) GetPublicationDate() string { - pub := cn.GetPublicationInfo() - if pub != nil { - return pub.PublicationDate - } - return "" -} - -// GetOrganizationByID returns an organization by its ID -func (cn ContractNotice) GetOrganizationByID(orgID string) *Organization { - orgs := cn.GetOrganizations() - if orgs == nil { - return nil - } - - for _, org := range orgs.Organization { - if org.Company != nil && - org.Company.PartyIdentification != nil && - org.Company.PartyIdentification.ID == orgID { - return &org - } - } - return nil -} - -// GetBuyerID returns the buyer organization ID from contracting party -func (cn ContractNotice) GetBuyerID() string { - if cn.ContractingParty != nil && - cn.ContractingParty.Party != nil && - cn.ContractingParty.Party.PartyIdentification != nil { - return cn.ContractingParty.Party.PartyIdentification.ID - } - return "" -} - -// GetReviewOrganizationID returns the review organization ID from appeal terms -func (cn ContractNotice) GetReviewOrganizationID() string { - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.TenderingTerms != nil && - cn.ProcurementProjectLot.TenderingTerms.AppealTerms != nil && - cn.ProcurementProjectLot.TenderingTerms.AppealTerms.AppealReceiverParty != nil && - cn.ProcurementProjectLot.TenderingTerms.AppealTerms.AppealReceiverParty.PartyIdentification != nil { - return cn.ProcurementProjectLot.TenderingTerms.AppealTerms.AppealReceiverParty.PartyIdentification.ID - } - return "" -} - -// GetProcedureCode returns the type of procedure -func (cn ContractNotice) GetProcedureCode() string { - if cn.TenderingProcess != nil { - return cn.TenderingProcess.ProcedureCode - } - return "" -} - -// GetProcurementTitle returns the procurement project title -func (cn ContractNotice) GetProcurementTitle() string { - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.ProcurementProject != nil { - return cn.ProcurementProjectLot.ProcurementProject.Name - } - return "" -} - -// GetProcurementDescription returns the procurement project description -func (cn ContractNotice) GetProcurementDescription() string { - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.ProcurementProject != nil { - return cn.ProcurementProjectLot.ProcurementProject.Description - } - return "" -} - -// GetContractNature returns the main nature of the contract -func (cn ContractNotice) GetContractNature() string { - if cn.ProcurementProject != nil { - return cn.ProcurementProject.ProcurementTypeCode - } - return "" -} - -// GetMainClassification returns the main CPV classification code -func (cn ContractNotice) GetMainClassification() string { - if cn.ProcurementProject != nil && - cn.ProcurementProject.MainCommodityClassification != nil { - return cn.ProcurementProject.MainCommodityClassification.ItemClassificationCode - } - return "" -} - -// GetPlaceOfPerformance returns the place of performance -func (cn ContractNotice) GetPlaceOfPerformance() string { - if cn.ProcurementProject != nil && - cn.ProcurementProject.RealizedLocation != nil && - cn.ProcurementProject.RealizedLocation.Address != nil { - return cn.ProcurementProject.RealizedLocation.Address.Region - } - return "" -} - -// GetLotID returns the procurement lot ID -func (cn ContractNotice) GetLotID() string { - if cn.ProcurementProjectLot != nil { - return cn.ProcurementProjectLot.ID - } - return "" -} - -// GetSelectionCriteria returns selection criteria from extensions -func (cn ContractNotice) GetSelectionCriteria() []SelectionCriteria { - if cn.Extensions != nil && - cn.Extensions.UBLExtension != nil && - cn.Extensions.UBLExtension.ExtensionContent != nil && - cn.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil { - return cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.SelectionCriteria - } - return nil -} - -// GetOfficialLanguages returns official languages from tender terms extensions -func (cn ContractNotice) GetOfficialLanguages() []string { - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.TenderingTerms != nil && - len(cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference) > 0 && - cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions != nil && - cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension != nil && - cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension.ExtensionContent != nil && - cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension.ExtensionContent.EformsExtension != nil && - cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension.ExtensionContent.EformsExtension.OfficialLanguages != nil { - - languages := make([]string, len(cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension.ExtensionContent.EformsExtension.OfficialLanguages.Language)) - for i, lang := range cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension.ExtensionContent.EformsExtension.OfficialLanguages.Language { - languages[i] = lang.ID - } - return languages - } - return nil -} - -// GetDocumentURI returns the document URI from call for tenders document reference -func (cn ContractNotice) GetDocumentURI() string { - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.TenderingTerms != nil && - len(cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference) > 0 && - cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].Attachment != nil && - cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].Attachment.ExternalReference != nil { - return cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference[0].Attachment.ExternalReference.URI - } - return "" -} - -// GetAllDocumentURIs returns all document URIs from call for tenders document references -func (cn ContractNotice) GetAllDocumentURIs() []string { - var uris []string - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.TenderingTerms != nil { - - for _, docRef := range cn.ProcurementProjectLot.TenderingTerms.CallForTendersDocumentReference { - if docRef.Attachment != nil && - docRef.Attachment.ExternalReference != nil && - docRef.Attachment.ExternalReference.URI != "" { - uris = append(uris, docRef.Attachment.ExternalReference.URI) - } - } - } - return uris -} - -// GetTenderDeadline returns the tender submission deadline -func (cn ContractNotice) GetTenderDeadline() string { - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.TenderingProcess != nil && - cn.ProcurementProjectLot.TenderingProcess.TenderSubmissionDeadlinePeriod != nil { - return cn.ProcurementProjectLot.TenderingProcess.TenderSubmissionDeadlinePeriod.EndDate - } - return "" -} - -// GetBudget returns the estimated contract amount and currency -func (cn ContractNotice) GetBudget() (amount string, currency string) { - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.ProcurementProject != nil && - cn.ProcurementProjectLot.ProcurementProject.RequestedTenderTotal != nil { - - contractAmount := cn.ProcurementProjectLot.ProcurementProject.RequestedTenderTotal.EstimatedOverallContractAmount - return contractAmount.Text, contractAmount.CurrencyID - } - return "", "" -} - -// GetDuration returns the contract duration and unit -func (cn ContractNotice) GetDuration() (duration string, unit string) { - if cn.ProcurementProjectLot != nil && - cn.ProcurementProjectLot.ProcurementProject != nil && - cn.ProcurementProjectLot.ProcurementProject.PlannedPeriod != nil && - cn.ProcurementProjectLot.ProcurementProject.PlannedPeriod.DurationMeasure != nil { - - durationMeasure := cn.ProcurementProjectLot.ProcurementProject.PlannedPeriod.DurationMeasure - return durationMeasure.Text, durationMeasure.UnitCode - } - return "", "" -} - -// ==================== ContractAwardNotice Methods ==================== - -// Validate validates the ContractAwardNotice structure -func (can ContractAwardNotice) Validate() error { - if can.ID == "" { - return fmt.Errorf("contract award notice ID is required") - } - - if can.ContractFolderID == "" { - return fmt.Errorf("contract folder ID is required") - } - - if can.IssueDate == "" { - return fmt.Errorf("issue date is required") - } - - // Validate date format - TED XML uses ISO 8601 format with timezone - validFormats := []string{ - "2006-01-02+07:00", - "2006-01-02-07:00", - "2006-01-02T15:04:05+07:00", - "2006-01-02T15:04:05-07:00", - "2006-01-02T15:04:05Z", - "2006-01-02Z", - "2006-01-02", - time.RFC3339, - time.RFC3339Nano, - } - - var validDate bool - for _, format := range validFormats { - if _, err := time.Parse(format, can.IssueDate); err == nil { - validDate = true - break - } - } - - if !validDate { - return fmt.Errorf("invalid issue date format: %s (expected ISO 8601 format)", can.IssueDate) - } - - // Validate language code if provided (should be 3-letter ISO code) - if can.NoticeLanguageCode != "" && len(can.NoticeLanguageCode) != 3 { - return fmt.Errorf("invalid notice language code: %s (expected 3-letter ISO code)", can.NoticeLanguageCode) - } - - return nil -} - -// GetID returns the contract award notice ID -func (can ContractAwardNotice) GetID() string { - return can.ID -} - -// GetNoticeType returns the notice type -func (can ContractAwardNotice) GetNoticeType() string { - return can.NoticeTypeCode -} - -// GetLanguage returns the notice language -func (can ContractAwardNotice) GetLanguage() string { - return can.NoticeLanguageCode -} - -// GetNoticeStatus returns the notice status -func (can ContractAwardNotice) GetNoticeStatus() NoticeStatus { - if can.StatusInformation != nil && can.StatusInformation.Status != "" { - return can.StatusInformation.Status - } - // Award notices are typically awarded status - return NoticeStatusAwarded -} - -// GetCancellationReason returns the cancellation reason if notice is cancelled -func (can ContractAwardNotice) GetCancellationReason() *CancellationReason { - if can.StatusInformation != nil { - return can.StatusInformation.CancellationReason - } - return nil -} - -// GetAwardInformation returns the award information if notice is awarded -func (can ContractAwardNotice) GetAwardInformation() *AwardInformation { - if can.StatusInformation != nil { - return can.StatusInformation.AwardInformation - } - return nil -} - -// GetModifications returns the list of modifications if notice is modified -func (can ContractAwardNotice) GetModifications() []NoticeModification { - if can.StatusInformation != nil { - return can.StatusInformation.Modifications - } - return nil -} - -// GetSuspensionReason returns the suspension reason if notice is suspended -func (can ContractAwardNotice) GetSuspensionReason() *SuspensionReason { - if can.StatusInformation != nil { - return can.StatusInformation.SuspensionReason - } - return nil -} - -// GetPublicationInfo returns publication information if available -func (can ContractAwardNotice) GetPublicationInfo() *Publication { - if can.Extensions != nil && - can.Extensions.UBLExtension != nil && - can.Extensions.UBLExtension.ExtensionContent != nil && - can.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil { - return can.Extensions.UBLExtension.ExtensionContent.EformsExtension.Publication - } - return nil -} - -// GetOrganizations returns organizations from extensions -func (can ContractAwardNotice) GetOrganizations() *Organizations { - if can.Extensions != nil && - can.Extensions.UBLExtension != nil && - can.Extensions.UBLExtension.ExtensionContent != nil && - can.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil { - return can.Extensions.UBLExtension.ExtensionContent.EformsExtension.Organizations - } - return nil -} - -// GetNoticeResult returns notice result from extensions -func (can ContractAwardNotice) GetNoticeResult() *NoticeResult { - if can.Extensions != nil && - can.Extensions.UBLExtension != nil && - can.Extensions.UBLExtension.ExtensionContent != nil && - can.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil { - return can.Extensions.UBLExtension.ExtensionContent.EformsExtension.NoticeResult - } - return nil -} - -// GetNoticeSubType returns the notice subtype code from extensions -func (can ContractAwardNotice) GetNoticeSubType() string { - if can.Extensions != nil && - can.Extensions.UBLExtension != nil && - can.Extensions.UBLExtension.ExtensionContent != nil && - can.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil && - can.Extensions.UBLExtension.ExtensionContent.EformsExtension.NoticeSubType != nil { - return can.Extensions.UBLExtension.ExtensionContent.EformsExtension.NoticeSubType.SubTypeCode - } - return "" -} - -// GetNoticePublicationID returns the notice publication ID from extensions -func (can ContractAwardNotice) GetNoticePublicationID() string { - pub := can.GetPublicationInfo() - if pub != nil { - return pub.NoticePublicationID - } - return "" -} - -// GetOJSID returns the OJS gazette ID from extensions -func (can ContractAwardNotice) GetOJSID() string { - pub := can.GetPublicationInfo() - if pub != nil { - return pub.GazetteID - } - return "" -} - -// GetPublicationDate returns the publication date from extensions -func (can ContractAwardNotice) GetPublicationDate() string { - pub := can.GetPublicationInfo() - if pub != nil { - return pub.PublicationDate - } - return "" -} - -// GetBuyerID returns the buyer organization ID from contracting party -func (can ContractAwardNotice) GetBuyerID() string { - if can.ContractingParty != nil && - can.ContractingParty.Party != nil && - can.ContractingParty.Party.PartyIdentification != nil { - return can.ContractingParty.Party.PartyIdentification.ID - } - return "" -} - -// GetProcedureCode returns the type of procedure -func (can ContractAwardNotice) GetProcedureCode() string { - if can.TenderingProcess != nil { - return can.TenderingProcess.ProcedureCode - } - return "" -} - -// GetProcurementTitle returns the procurement project title -func (can ContractAwardNotice) GetProcurementTitle() string { - if can.ProcurementProject != nil { - return can.ProcurementProject.Name - } - return "" -} - -// GetProcurementDescription returns the procurement project description -func (can ContractAwardNotice) GetProcurementDescription() string { - if can.ProcurementProject != nil { - return can.ProcurementProject.Description - } - return "" -} - -// GetContractNature returns the main nature of the contract -func (can ContractAwardNotice) GetContractNature() string { - if can.ProcurementProject != nil { - return can.ProcurementProject.ProcurementTypeCode - } - return "" -} - -// GetMainClassification returns the main CPV classification code -func (can ContractAwardNotice) GetMainClassification() string { - if can.ProcurementProject != nil && - can.ProcurementProject.MainCommodityClassification != nil { - return can.ProcurementProject.MainCommodityClassification.ItemClassificationCode - } - return "" -} - -// GetTotalAwardValue returns the total award amount and currency -func (can ContractAwardNotice) GetTotalAwardValue() (amount string, currency string) { - noticeResult := can.GetNoticeResult() - if noticeResult != nil && noticeResult.TotalAmount != nil { - return noticeResult.TotalAmount.Text, noticeResult.TotalAmount.CurrencyID - } - return "", "" -} - -// GetAwardDate returns the tender award date -func (can ContractAwardNotice) GetAwardDate() string { - if can.TenderResult != nil { - return can.TenderResult.AwardDate - } - return "" -} - -// GetOrganizationByID returns an organization by its ID -func (can ContractAwardNotice) GetOrganizationByID(orgID string) *Organization { - orgs := can.GetOrganizations() - if orgs == nil { - return nil - } - - for _, org := range orgs.Organization { - if org.Company != nil && - org.Company.PartyIdentification != nil && - org.Company.PartyIdentification.ID == orgID { - return &org - } - } - return nil -} - -// GetAwardedContractors returns detailed information about awarded contractors -func (can ContractAwardNotice) GetAwardedContractors() []AwardedContractorInfo { - var contractors []AwardedContractorInfo - - noticeResult := can.GetNoticeResult() - if noticeResult == nil { - return contractors - } - - // Create a map of tender ID to tender info for quick lookup - tenderMap := make(map[string]*LotTender) - for _, lotTender := range noticeResult.LotTender { - tenderMap[lotTender.ID] = &lotTender - } - - // Create a map of tendering party ID to organization ID - partyToOrgMap := make(map[string]string) - for _, tenderingParty := range noticeResult.TenderingParty { - for _, tenderer := range tenderingParty.Tenderer { - partyToOrgMap[tenderingParty.ID] = tenderer.ID - } - } - - // Process settled contracts to extract award information - for _, contract := range noticeResult.SettledContract { - if contract.LotTender != nil { - // Get tender information - tender := tenderMap[contract.LotTender.ID] - if tender == nil { - continue - } - - // Get organization information - var org *Organization - if tender.TenderingParty != nil { - orgID := partyToOrgMap[tender.TenderingParty.ID] - if orgID != "" { - org = can.GetOrganizationByID(orgID) - } - } - - // Create awarded contractor info - contractor := AwardedContractorInfo{ - ContractID: contract.ID, - TenderID: contract.LotTender.ID, - AwardDate: contract.AwardDate, - IssueDate: contract.IssueDate, - } - - // Set contract reference if available - if contract.ContractReference != nil { - contractor.ContractReference = contract.ContractReference.ID - } - - // Set amount and currency from tender - if tender.LegalMonetaryTotal != nil { - contractor.Amount = tender.LegalMonetaryTotal.PayableAmount.Text - contractor.Currency = tender.LegalMonetaryTotal.PayableAmount.CurrencyID - } - - // Set organization information - if org != nil && org.Company != nil { - if org.Company.PartyName != nil { - contractor.Name = org.Company.PartyName.Name - } - if org.Company.PartyLegalEntity != nil { - contractor.CompanyID = org.Company.PartyLegalEntity.CompanyID - } - if org.Company.PartyIdentification != nil { - contractor.OrganizationID = org.Company.PartyIdentification.ID - } - - // Set address information - if org.Company.PostalAddress != nil { - addressParts := []string{} - if org.Company.PostalAddress.StreetName != "" { - addressParts = append(addressParts, org.Company.PostalAddress.StreetName) - } - if org.Company.PostalAddress.CityName != "" { - addressParts = append(addressParts, org.Company.PostalAddress.CityName) - } - if org.Company.PostalAddress.PostalZone != "" { - addressParts = append(addressParts, org.Company.PostalAddress.PostalZone) - } - contractor.Address = strings.Join(addressParts, ", ") - - if org.Company.PostalAddress.Country != nil { - contractor.Country = org.Company.PostalAddress.Country.IdentificationCode - } - } - - // Set contact information - if org.Company.Contact != nil { - contractor.ContactName = org.Company.Contact.Name - contractor.ContactEmail = org.Company.Contact.ElectronicMail - contractor.ContactPhone = org.Company.Contact.Telephone - } - } - - // Set lot information - if tender.TenderLot != nil { - contractor.LotID = tender.TenderLot.ID - } - - contractors = append(contractors, contractor) - } - } - - return contractors -} - -// AwardedContractorInfo contains detailed information about an awarded contractor -type AwardedContractorInfo struct { - ContractID string `json:"contract_id"` - TenderID string `json:"tender_id"` - LotID string `json:"lot_id,omitempty"` - Name string `json:"name"` - CompanyID string `json:"company_id,omitempty"` - OrganizationID string `json:"organization_id,omitempty"` - Address string `json:"address,omitempty"` - Country string `json:"country,omitempty"` - Amount string `json:"amount"` - Currency string `json:"currency,omitempty"` - AwardDate string `json:"award_date,omitempty"` - IssueDate string `json:"issue_date,omitempty"` - ContractReference string `json:"contract_reference,omitempty"` - ContactName string `json:"contact_name,omitempty"` - ContactEmail string `json:"contact_email,omitempty"` - ContactPhone string `json:"contact_phone,omitempty"` -}