Refactor TED Repository and Introduce TED Mapping Functionality

- Updated the tender repository to create a unique index on tender_id, enhancing database query performance.
- Introduced new TED mapping functions to convert parsed TED documents into tender entities, improving integration with the tender management system.
- Added error handling for contract notice processing in the TED scraper, ensuring robust logging and error management.
- Removed deprecated eform structures to streamline the codebase and focus on essential components.
This commit is contained in:
n.nakhostin
2025-10-04 12:34:37 +03:30
parent 366f7290a7
commit 0dd877a46d
32 changed files with 2386 additions and 1817 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ type tenderRepository struct {
func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) TenderRepository {
// Create indexes for tenders collection
tenderIndexes := []orm.Index{
*orm.CreateUniqueIndex("notice_publication_id_idx", bson.D{{Key: "notice_publication_id", Value: 1}}),
*orm.CreateUniqueIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}),
*orm.NewIndex("contract_notice_id_idx", bson.D{{Key: "contract_notice_id", Value: 1}}),
*orm.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
*orm.NewIndex("source_idx", bson.D{{Key: "source", Value: 1}}),
-54
View File
@@ -1,54 +0,0 @@
package eform
// =====================================================
// AWARD CRITERIA
// =====================================================
// AwardCriteria defines how tenders will be evaluated
type AwardCriteria struct {
Type AwardCriteriaType `json:"type" xml:"type" validate:"required"`
PriceWeight *float64 `json:"price_weight,omitempty" xml:"price_weight,omitempty" validate:"omitempty,min=0,max=100"`
CriteriaStatement string `json:"criteria_statement,omitempty" xml:"criteria_statement,omitempty" validate:"max=10000"`
Criteria []AwardCriterion `json:"criteria,omitempty" xml:"criteria,omitempty" validate:"dive"`
CostCalculation string `json:"cost_calculation,omitempty" xml:"cost_calculation,omitempty" validate:"max=10000"`
}
// AwardCriteriaType represents the evaluation approach
type AwardCriteriaType string
const (
AwardTypePrice AwardCriteriaType = "price"
AwardTypeCost AwardCriteriaType = "cost"
AwardTypeQualPrice AwardCriteriaType = "qual-price"
)
// AwardCriterion represents an individual evaluation criterion
type AwardCriterion struct {
ID string `json:"id" xml:"id" validate:"required,acIDFormat"`
Name string `json:"name" xml:"name" validate:"required,max=200"`
Description string `json:"description,omitempty" xml:"description,omitempty" validate:"max=2000"`
Weight float64 `json:"weight" xml:"weight" validate:"required,min=0,max=100"`
Type AwardCriterionType `json:"type" xml:"type" validate:"required"`
SubCriteria []AwardCriterion `json:"sub_criteria,omitempty" xml:"sub_criteria,omitempty" validate:"dive"`
}
// AwardCriterionType represents the type of criterion
type AwardCriterionType string
const (
CriterionQuality AwardCriterionType = "quality"
CriterionCost AwardCriterionType = "cost"
CriterionPrice AwardCriterionType = "price"
CriterionSocial AwardCriterionType = "social"
CriterionEnvironmental AwardCriterionType = "environmental"
CriterionInnovative AwardCriterionType = "innovative"
)
// CalculateTotalAwardCriteriaWeight validates criteria weights sum to 100
func (ac *AwardCriteria) CalculateTotalAwardCriteriaWeight() float64 {
var total float64
for _, criterion := range ac.Criteria {
total += criterion.Weight
}
return total
}
-77
View File
@@ -1,77 +0,0 @@
package eform
import "encoding/xml"
// =====================================================
// BUSINESS REGISTRATION INFORMATION NOTICE
// =====================================================
// AdditionalNoticeLanguage represents additional notice language
type AdditionalNoticeLanguage struct {
XMLName xml.Name `xml:"AdditionalNoticeLanguage"`
ID string `json:"id" xml:"ID"`
}
// SenderParty represents the sender party
type SenderParty struct {
XMLName xml.Name `xml:"SenderParty"`
Contact Contact `json:"contact,omitempty" xml:"Contact,omitempty"`
}
// BusinessParty represents the business party
type BusinessParty struct {
XMLName xml.Name `xml:"BusinessParty"`
PostalAddress PostalAddress `json:"postal_address,omitempty" xml:"PostalAddress,omitempty"`
PartyLegalEntity []PartyLegalEntityEnhanced `json:"party_legal_entity,omitempty" xml:"PartyLegalEntity,omitempty"`
Contact Contact `json:"contact,omitempty" xml:"Contact,omitempty"`
WebsiteURI string `json:"website_uri,omitempty" xml:"WebsiteURI,omitempty"`
}
// AdditionalDocumentReference represents additional document reference
type AdditionalDocumentReference struct {
XMLName xml.Name `xml:"AdditionalDocumentReference"`
ID string `json:"id" xml:"ID"`
IssueDate string `json:"issue_date,omitempty" xml:"IssueDate,omitempty"`
ReferencedDocumentInternalAddress string `json:"referenced_document_internal_address,omitempty" xml:"ReferencedDocumentInternalAddress,omitempty"`
DocumentDescription string `json:"document_description,omitempty" xml:"DocumentDescription,omitempty"`
Attachment Attachment `json:"attachment,omitempty" xml:"Attachment,omitempty"`
}
// BusinessCapability represents business capability
type BusinessCapability struct {
XMLName xml.Name `xml:"BusinessCapability"`
CapabilityTypeCode string `json:"capability_type_code" xml:"CapabilityTypeCode"`
}
// NoticePurpose represents notice purpose
type NoticePurpose struct {
XMLName xml.Name `xml:"NoticePurpose"`
PurposeCode string `json:"purpose_code" xml:"PurposeCode"`
}
// =====================================================
// ENHANCED PARTY LEGAL ENTITY FOR BUSINESS REGISTRATION
// =====================================================
// PartyLegalEntityEnhanced represents enhanced party legal entity for business registration
type PartyLegalEntityEnhanced struct {
XMLName xml.Name `xml:"PartyLegalEntity"`
RegistrationName string `json:"registration_name,omitempty" xml:"RegistrationName,omitempty"`
CompanyID string `json:"company_id,omitempty" xml:"CompanyID,omitempty"`
RegistrationDate string `json:"registration_date,omitempty" xml:"RegistrationDate,omitempty"`
CorporateRegistrationScheme CorporateRegistrationScheme `json:"corporate_registration_scheme,omitempty" xml:"CorporateRegistrationScheme,omitempty"`
}
// CorporateRegistrationScheme represents corporate registration scheme
type CorporateRegistrationScheme struct {
XMLName xml.Name `xml:"CorporateRegistrationScheme"`
JurisdictionRegionAddress JurisdictionRegionAddress `json:"jurisdiction_region_address,omitempty" xml:"JurisdictionRegionAddress,omitempty"`
}
// JurisdictionRegionAddress represents jurisdiction region address
type JurisdictionRegionAddress struct {
XMLName xml.Name `xml:"JurisdictionRegionAddress"`
CityName string `json:"city_name,omitempty" xml:"CityName,omitempty"`
PostalZone string `json:"postal_zone,omitempty" xml:"PostalZone,omitempty"`
Country Country `json:"country,omitempty" xml:"Country,omitempty"`
}
@@ -1,44 +0,0 @@
package business_registration_information
import (
"encoding/xml"
"tm/internal/tender"
"tm/ted/eform"
)
// BusinessRegistrationInformationNotice represents the UBL BusinessRegistrationInformationNotice structure
type BusinessRegistrationInformationNotice struct {
XMLName xml.Name `xml:"BusinessRegistrationInformationNotice"`
UBLVersionID string `json:"ubl_version_id" xml:"UBLVersionID"`
CustomizationID string `json:"customization_id" xml:"CustomizationID"`
ID string `json:"id" xml:"ID"`
IssueDate string `json:"issue_date" xml:"IssueDate"`
IssueTime string `json:"issue_time,omitempty" xml:"IssueTime,omitempty"`
VersionID string `json:"version_id,omitempty" xml:"VersionID,omitempty"`
RegulatoryDomain string `json:"regulatory_domain,omitempty" xml:"RegulatoryDomain,omitempty"`
NoticeTypeCode string `json:"notice_type_code" xml:"NoticeTypeCode"`
NoticeLanguageCode string `json:"notice_language_code" xml:"NoticeLanguageCode"`
UBLExtensions eform.UBLExtensions `json:"ubl_extensions,omitempty" xml:"UBLExtensions,omitempty"`
AdditionalNoticeLanguage []eform.AdditionalNoticeLanguage `json:"additional_notice_language,omitempty" xml:"AdditionalNoticeLanguage,omitempty"`
SenderParty eform.SenderParty `json:"sender_party,omitempty" xml:"SenderParty,omitempty"`
BusinessParty eform.BusinessParty `json:"business_party" xml:"BusinessParty"`
AdditionalDocumentReference []eform.AdditionalDocumentReference `json:"additional_document_reference,omitempty" xml:"AdditionalDocumentReference,omitempty"`
BusinessCapability []eform.BusinessCapability `json:"business_capability,omitempty" xml:"BusinessCapability,omitempty"`
NoticePurpose eform.NoticePurpose `json:"notice_purpose,omitempty" xml:"NoticePurpose,omitempty"`
}
// GetID returns the business registration information notice ID
func (brin BusinessRegistrationInformationNotice) GetID() string {
return brin.ID
}
// Validate validates the parsed model
func (brin BusinessRegistrationInformationNotice) Validate() error {
return nil
}
func (notice BusinessRegistrationInformationNotice) MapToTender() *tender.Tender {
t := new(tender.Tender)
t.ContractNoticeID = notice.GetID()
return t
}
-96
View File
@@ -1,96 +0,0 @@
package eform
// =====================================================
// ENUMERATIONS AND CONSTANTS
// =====================================================
// Common CPV Code Prefixes (for validation)
const (
CPVWorks = "45" // Construction works
CPVServices = "50" // Services (general range 50-99 with exceptions)
)
// Common NUTS Code Structure
// NUTS codes are hierarchical: CC1234
// CC = Country code (2 chars)
// 1 = NUTS 1 level
// 12 = NUTS 2 level
// 123 = NUTS 3 level
// ISO 4217 Currency Codes (common ones)
const (
CurrencyEUR = "EUR" // Euro
CurrencyUSD = "USD" // US Dollar
CurrencyGBP = "GBP" // British Pound
CurrencySEK = "SEK" // Swedish Krona
CurrencyDKK = "DKK" // Danish Krone
CurrencyPLN = "PLN" // Polish Zloty
CurrencyCZK = "CZK" // Czech Koruna
CurrencyHUF = "HUF" // Hungarian Forint
CurrencyRON = "RON" // Romanian Leu
CurrencyBGN = "BGN" // Bulgarian Lev
CurrencyHRK = "HRK" // Croatian Kuna
)
// ISO 3166-1 Alpha-2 Country Codes (EU/EEA)
const (
CountryAT = "AT" // Austria
CountryBE = "BE" // Belgium
CountryBG = "BG" // Bulgaria
CountryCY = "CY" // Cyprus
CountryCZ = "CZ" // Czech Republic
CountryDE = "DE" // Germany
CountryDK = "DK" // Denmark
CountryEE = "EE" // Estonia
CountryES = "ES" // Spain
CountryFI = "FI" // Finland
CountryFR = "FR" // France
CountryGR = "GR" // Greece
CountryHR = "HR" // Croatia
CountryHU = "HU" // Hungary
CountryIE = "IE" // Ireland
CountryIT = "IT" // Italy
CountryLT = "LT" // Lithuania
CountryLU = "LU" // Luxembourg
CountryLV = "LV" // Latvia
CountryMT = "MT" // Malta
CountryNL = "NL" // Netherlands
CountryPL = "PL" // Poland
CountryPT = "PT" // Portugal
CountryRO = "RO" // Romania
CountrySE = "SE" // Sweden
CountrySI = "SI" // Slovenia
CountrySK = "SK" // Slovakia
CountryIS = "IS" // Iceland (EEA)
CountryLI = "LI" // Liechtenstein (EEA)
CountryNO = "NO" // Norway (EEA)
CountryGB = "GB" // United Kingdom
)
// ISO 639-1 Language Codes (EU official)
const (
LangBG = "BG" // Bulgarian
LangCS = "CS" // Czech
LangDA = "DA" // Danish
LangDE = "DE" // German
LangEL = "EL" // Greek
LangEN = "EN" // English
LangES = "ES" // Spanish
LangET = "ET" // Estonian
LangFI = "FI" // Finnish
LangFR = "FR" // French
LangGA = "GA" // Irish
LangHR = "HR" // Croatian
LangHU = "HU" // Hungarian
LangIT = "IT" // Italian
LangLT = "LT" // Lithuanian
LangLV = "LV" // Latvian
LangMT = "MT" // Maltese
LangNL = "NL" // Dutch
LangPL = "PL" // Polish
LangPT = "PT" // Portuguese
LangRO = "RO" // Romanian
LangSK = "SK" // Slovak
LangSL = "SL" // Slovenian
LangSV = "SV" // Swedish
)
-213
View File
@@ -1,213 +0,0 @@
package contract
import (
"encoding/xml"
"time"
"tm/internal/tender"
"tm/ted/eform"
)
// Notice represents the UBL ContractNotice structure used by TED
type ContractNotice struct {
XMLName xml.Name `xml:"ContractNotice"`
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"`
UBLExtensions eform.UBLExtensions `xml:"UBLExtensions,omitempty"`
ContractingParty eform.ContractingParty `xml:"ContractingParty"`
TenderingTerms eform.TenderingTerms `xml:"TenderingTerms,omitempty"`
TenderingProcess eform.TenderingProcess `xml:"TenderingProcess,omitempty"`
ProcurementProject eform.ProcurementProject `xml:"ProcurementProject"`
ProcurementProjectLot []eform.ProcurementProjectLot `xml:"ProcurementProjectLot,omitempty"`
}
func (notice ContractNotice) MapToTender() *tender.Tender {
now := time.Now().Unix()
t := &tender.Tender{
ContractNoticeID: notice.ID,
ContractFolderID: notice.ContractFolderID,
NoticeTypeCode: notice.NoticeTypeCode,
// NoticeSubTypeCode: notice.NoticeSubTypeCode,
NoticeLanguageCode: notice.NoticeLanguageCode,
// IssueDate: parseDateToUnixMilli(notice.IssueDate),
// IssueTime: parseTimeToUnixMilli(notice.IssueTime),
// GazetteID: notice.GetOJSID(),
// Title: notice.GetProcurementTitle(),
// Description: notice.GetProcurementDescription(),
// ProcurementTypeCode: notice.GetContractNature(),
// ProcedureCode: notice.GetProcedureCode(),
// MainClassification: notice.GetMainClassification(),
// PlaceOfPerformance: notice.GetPlaceOfPerformance(),
// DocumentURI: notice.GetDocumentURI(),
Status: tender.TenderStatusActive,
Source: tender.TenderSourceTEDScraper,
ProcessingMetadata: tender.ProcessingMetadata{
ScrapedAt: now,
ProcessedAt: now,
ProcessingVersion: "1.0",
},
}
// Set publication information
// if pubInfo := notice.GetPublicationInfo(); pubInfo != nil {
// t.NoticePublicationID = pubInfo.NoticePublicationID
// t.PublicationDate = s.parseDateToUnixMilli(pubInfo.PublicationDate)
// t.TenderURL = s.generateTenderURL(pubInfo.NoticePublicationID)
// }
// Set additional classifications
// if notice.ProcurementProject != nil && notice.ProcurementProject.AdditionalCommodityClassification != nil {
// for _, additionalClass := range notice.ProcurementProject.AdditionalCommodityClassification {
// if additionalClass.ItemClassificationCode != "" {
// t.AdditionalClassifications = append(t.AdditionalClassifications, additionalClass.ItemClassificationCode)
// }
// }
// }
// Set estimated value and currency
// if amount, currency := notice.GetBudget(); amount != "" {
// if value, err := strconv.ParseFloat(amount, 64); err == nil {
// t.EstimatedValue = value
// t.Currency = currency
// }
// }
// Set duration information
// if duration, unit := notice.GetDuration(); duration != "" {
// t.Duration = duration
// t.DurationUnit = unit
// }
// Set tender deadline
// if deadline := notice.GetTenderDeadline(); deadline != "" {
// t.TenderDeadline = parseDateToUnixMilli(deadline)
// if deadlineTime, err := parseDate(deadline); err == nil {
// applicationDeadline := calculateApplicationDeadline(deadlineTime)
// t.ApplicationDeadline = applicationDeadline.UnixMilli()
// }
// }
// if pubDate, err := parseDate(notice.GetPublicationDate()); err == nil {
// submissionDeadline := calculateSubmissionDeadline(pubDate)
// t.SubmissionDeadline = submissionDeadline.UnixMilli()
// }
// Set SubmissionURL from TenderingTerms
// if notice.ProcurementProjectLot != nil && notice.ProcurementProjectLot.TenderingTerms != nil {
// if tenderingTerms := notice.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 notice.ProcurementProject != nil && notice.ProcurementProject.RealizedLocation != nil {
// if addr := notice.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 := notice.GetBuyerID()
// if buyerID != "" {
// if buyerOrg := notice.GetOrganizationByID(buyerID); buyerOrg != nil {
// t.BuyerOrganization = s.mapOrganization(buyerOrg, "buyer")
// }
// }
// Set review organization
// reviewID := notice.GetReviewOrganizationID()
// if reviewID != "" {
// if reviewOrg := notice.GetOrganizationByID(reviewID); reviewOrg != nil {
// t.ReviewOrganization = s.mapOrganization(reviewOrg, "review")
// }
// }
// Set all organizations
// if orgs := notice.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 := notice.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 := notice.GetOfficialLanguages(); languages != nil {
// t.OfficialLanguages = languages
// }
// Generate unique tender ID
// t.TenderID = generateTenderID(t)
return t
}
// GetID returns the contract notice ID
func (cn ContractNotice) GetID() string {
return cn.ID
}
// Validate validates the parsed model
func (cn ContractNotice) Validate() error {
return nil
}
// GetMainBuyer returns the lead/main buyer organization
func (n *ContractNotice) GetMainBuyer() *eform.Organization {
if len(n.UBLExtensions.UBLExtension) > 0 {
orgs := n.UBLExtensions.UBLExtension[0].ExtensionContent.EformsExtension.Organizations.Organization
for i := range orgs {
for _, role := range orgs[i].Roles {
if role == eform.RoleBuyer || role == eform.RoleLeadBuyer {
return &orgs[i]
}
}
}
}
return nil
}
// GetOrganizationByID retrieves organization by ID
func (n *ContractNotice) GetOrganizationByID(id string) *eform.Organization {
if len(n.UBLExtensions.UBLExtension) > 0 {
orgs := n.UBLExtensions.UBLExtension[0].ExtensionContent.EformsExtension.Organizations.Organization
for i := range orgs {
if orgs[i].ID == id {
return &orgs[i]
}
}
}
return nil
}
@@ -1,44 +0,0 @@
package contract_award
import (
"encoding/xml"
"tm/internal/tender"
"tm/ted/eform"
)
// ContractAwardNotice represents the UBL ContractAwardNotice structure
type ContractAwardNotice struct {
XMLName xml.Name `xml:"ContractAwardNotice"`
UBLVersionID string `json:"ubl_version_id" xml:"UBLVersionID"`
CustomizationID string `json:"customization_id" xml:"CustomizationID"`
ID string `json:"id" xml:"ID"`
ContractFolderID string `json:"contract_folder_id" xml:"ContractFolderID"`
IssueDate string `json:"issue_date" xml:"IssueDate"`
IssueTime string `json:"issue_time,omitempty" xml:"IssueTime,omitempty"`
VersionID string `json:"version_id,omitempty" xml:"VersionID,omitempty"`
RegulatoryDomain string `json:"regulatory_domain,omitempty" xml:"RegulatoryDomain,omitempty"`
NoticeTypeCode string `json:"notice_type_code" xml:"NoticeTypeCode"`
NoticeLanguageCode string `json:"notice_language_code" xml:"NoticeLanguageCode"`
ContractingParty eform.ContractingParty `json:"contracting_party" xml:"ContractingParty"`
TenderingTerms eform.TenderingTerms `json:"tendering_terms,omitempty" xml:"TenderingTerms,omitempty"`
TenderingProcess eform.TenderingProcess `json:"tendering_process,omitempty" xml:"TenderingProcess,omitempty"`
ProcurementProject eform.ProcurementProject `json:"procurement_project" xml:"ProcurementProject"`
ProcurementProjectLot []eform.ProcurementProjectLot `json:"procurement_project_lots,omitempty" xml:"ProcurementProjectLot,omitempty"`
UBLExtensions eform.UBLExtensions `json:"ubl_extensions,omitempty" xml:"UBLExtensions,omitempty"`
}
// GetID returns the contract award notice ID
func (can ContractAwardNotice) GetID() string {
return can.ID
}
// Validate validates the parsed model
func (can ContractAwardNotice) Validate() error {
return nil
}
func (notice ContractAwardNotice) MapToTender() *tender.Tender {
t := new(tender.Tender)
t.ContractNoticeID = notice.GetID()
return t
}
-82
View File
@@ -1,82 +0,0 @@
package eform
import "encoding/xml"
// =====================================================
// CONTRACTING PARTY
// =====================================================
// ContractingParty represents the contracting party
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 represents the contracting activity
type ContractingActivity struct {
XMLName xml.Name `xml:"ContractingActivity"`
ActivityTypeCode string `xml:"ActivityTypeCode"`
}
// Party represents a party
type Party struct {
XMLName xml.Name `xml:"Party"`
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"`
WebsiteURI string `xml:"WebsiteURI,omitempty"`
}
// PartyIdentification represents party identification
type PartyIdentification struct {
XMLName xml.Name `xml:"PartyIdentification"`
ID string `xml:"ID"`
}
// PartyName represents party name
type PartyName struct {
XMLName xml.Name `xml:"PartyName"`
Name string `xml:"Name"`
}
// PostalAddress represents postal address
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"`
Country Country `xml:"Country,omitempty"`
}
// Country represents country information
type Country struct {
XMLName xml.Name `xml:"Country"`
IdentificationCode string `xml:"IdentificationCode"`
}
// PartyLegalEntity represents party legal entity
type PartyLegalEntity struct {
XMLName xml.Name `xml:"PartyLegalEntity"`
CompanyID string `xml:"CompanyID"`
}
// Contact represents 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"`
}
-31
View File
@@ -1,31 +0,0 @@
package eform
// =====================================================
// DOCUMENT STRUCTURE
// =====================================================
// Document represents attached documentation
type Document struct {
ID string `json:"id" xml:"id" validate:"required"`
Type DocumentType `json:"type" xml:"type" validate:"required"`
Title string `json:"title" xml:"title" validate:"required,max=500"`
Description string `json:"description,omitempty" xml:"description,omitempty" validate:"max=2000"`
URL string `json:"url,omitempty" xml:"url,omitempty" validate:"omitempty,url"`
RestrictedAccess bool `json:"restricted_access" xml:"restricted_access"`
Language LanguageCode `json:"language" xml:"language" validate:"required,iso639_1"`
}
// DocumentType represents the type of document
type DocumentType string
const (
DocTypeProcurementDocs DocumentType = "procurement-docs"
DocTypeAdditionalInfo DocumentType = "additional-info"
DocTypeTenderSpecs DocumentType = "tender-specs"
DocTypeTenderForm DocumentType = "tender-form"
DocTypeContractDraft DocumentType = "contract-draft"
DocTypeEvaluationCriteria DocumentType = "evaluation-criteria"
DocTypeBillsQuantities DocumentType = "bills-quantities"
DocTypeIllustrations DocumentType = "illustrations"
DocTypeOther DocumentType = "other"
)
-62
View File
@@ -1,62 +0,0 @@
package eform
// =====================================================
// LOT STRUCTURE
// =====================================================
// Lot represents an individual procurement lot
type Lot struct {
ID string `json:"id" xml:"id" validate:"required,lotIDFormat"`
InternalID string `json:"internal_id,omitempty" xml:"internal_id,omitempty" validate:"max=100"`
Title string `json:"title" xml:"title" validate:"required,max=500"`
Description string `json:"description" xml:"description" validate:"required,max=10000"`
MainCPVCode string `json:"main_cpv_code" xml:"main_cpv_code" validate:"required,cpvCode"`
AdditionalCPVCodes []string `json:"additional_cpv_codes,omitempty" xml:"additional_cpv_codes,omitempty" validate:"dive,cpvCode"`
ContractNature ContractNature `json:"contract_nature" xml:"contract_nature" validate:"required"`
EstimatedValue *Money `json:"estimated_value,omitempty" xml:"estimated_value,omitempty"`
PlacesOfPerformance []Place `json:"places_of_performance" xml:"places_of_performance" validate:"required,min=1,dive"`
Duration *Duration `json:"duration,omitempty" xml:"duration,omitempty"`
Renewable bool `json:"renewable" xml:"renewable"`
RenewalDescription string `json:"renewal_description,omitempty" xml:"renewal_description,omitempty" validate:"required_if=Renewable true"`
OptionsDescription string `json:"options_description,omitempty" xml:"options_description,omitempty" validate:"max=10000"`
VariantsAccepted bool `json:"variants_accepted" xml:"variants_accepted"`
MaxLotsPerTenderer *int `json:"max_lots_per_tenderer,omitempty" xml:"max_lots_per_tenderer,omitempty" validate:"omitempty,min=1"`
MaxLotsAwarded *int `json:"max_lots_awarded,omitempty" xml:"max_lots_awarded,omitempty" validate:"omitempty,min=1"`
LotCombinations []string `json:"lot_combinations,omitempty" xml:"lot_combinations,omitempty"`
SubcontractingObligation SubcontractingObligation `json:"subcontracting_obligation" xml:"subcontracting_obligation"`
SubcontractingPercentage *float64 `json:"subcontracting_percentage,omitempty" xml:"subcontracting_percentage,omitempty" validate:"omitempty,min=0,max=100"`
TechnicalSpecifications TechnicalSpecifications `json:"technical_specifications" xml:"technical_specifications" validate:"required"`
AwardCriteria AwardCriteria `json:"award_criteria" xml:"award_criteria" validate:"required"`
TenderingTerms TenderingTerms `json:"tendering_terms" xml:"tendering_terms" validate:"required"`
Result *LotResult `json:"result,omitempty" xml:"result,omitempty"`
}
// SubcontractingObligation represents subcontracting requirements
type SubcontractingObligation string
const (
SubcontractingNone SubcontractingObligation = "none"
SubcontractingMinimum SubcontractingObligation = "minimum"
SubcontractingRequired SubcontractingObligation = "required"
)
// Place represents a place of performance
type Place struct {
StreetAddress string `json:"street_address,omitempty" xml:"street_address,omitempty" validate:"max=200"`
Locality string `json:"locality,omitempty" xml:"locality,omitempty" validate:"max=100"`
PostalCode string `json:"postal_code,omitempty" xml:"postal_code,omitempty" validate:"max=20"`
CountrySubdivision string `json:"country_subdivision,omitempty" xml:"country_subdivision,omitempty" validate:"max=100"`
CountryCode string `json:"country_code" xml:"country_code" validate:"required,iso3166_1_alpha2"`
NUTSCodes []string `json:"nuts_codes" xml:"nuts_codes" validate:"required,min=1,dive,nutsCode"`
Description string `json:"description,omitempty" xml:"description,omitempty" validate:"max=1000"`
}
// HasSubcontractingObligation checks if subcontracting is required
func (l *Lot) HasSubcontractingObligation() bool {
return l.SubcontractingObligation != SubcontractingNone
}
// IsReservedContract checks if lot is reserved for specific participants
func (l *Lot) IsReservedContract() bool {
return l.TechnicalSpecifications.ReservedExecution != ReservedNone
}
-54
View File
@@ -1,54 +0,0 @@
package eform
import "time"
// =====================================================
// LOT RESULT STRUCTURE
// =====================================================
// LotResult represents the outcome of lot procurement
type LotResult struct {
ID string `json:"id" xml:"id" validate:"required,resIDFormat"`
Status ResultStatus `json:"status" xml:"status" validate:"required"`
StatusDescription string `json:"status_description,omitempty" xml:"status_description,omitempty" validate:"max=10000"`
DecisionDate *time.Time `json:"decision_date,omitempty" xml:"decision_date,omitempty"`
TendersReceived int `json:"tenders_received" xml:"tenders_received" validate:"min=0"`
TendersSME int `json:"tenders_sme" xml:"tenders_sme" validate:"min=0"`
TendersElectronic int `json:"tenders_electronic" xml:"tenders_electronic" validate:"min=0"`
TendersOtherEU int `json:"tenders_other_eu" xml:"tenders_other_eu" validate:"min=0"`
TendersNonEU int `json:"tenders_non_eu" xml:"tenders_non_eu" validate:"min=0"`
AwardedValue *Money `json:"awarded_value,omitempty" xml:"awarded_value,omitempty"`
SubcontractingShare *float64 `json:"subcontracting_share,omitempty" xml:"subcontracting_share,omitempty" validate:"omitempty,min=0,max=100"`
SubcontractingValue *Money `json:"subcontracting_value,omitempty" xml:"subcontracting_value,omitempty"`
ConcessionRevenue *Money `json:"concession_revenue,omitempty" xml:"concession_revenue,omitempty"`
Tenders []Tender `json:"tenders,omitempty" xml:"tenders,omitempty" validate:"dive"`
SettledContract *SettledContract `json:"settled_contract,omitempty" xml:"settled_contract,omitempty"`
NonAwardJustification string `json:"non_award_justification,omitempty" xml:"non_award_justification,omitempty" validate:"max=10000"`
}
// ResultStatus represents the lot result status
type ResultStatus string
const (
ResultOpen ResultStatus = "open"
ResultClose ResultStatus = "close"
ResultWinner ResultStatus = "winn"
ResultNoWinner ResultStatus = "no-winn"
ResultSelected ResultStatus = "selec"
ResultClosedNoWin ResultStatus = "clos-nw"
)
// HasWinner checks if lot has a winning tender
func (lr *LotResult) HasWinner() bool {
return lr.Status == ResultWinner
}
// GetWinningTender returns the winning tender if exists
func (lr *LotResult) GetWinningTender() *Tender {
for i := range lr.Tenders {
if lr.Tenders[i].IsWinner {
return &lr.Tenders[i]
}
}
return nil
}
-53
View File
@@ -1,53 +0,0 @@
package eform
import "time"
// =====================================================
// MODIFICATION STRUCTURE
// =====================================================
// Modification represents changes to existing contracts
type Modification struct {
ID string `json:"id" xml:"id" validate:"required,modIDFormat"`
OriginalContractID string `json:"original_contract_id" xml:"original_contract_id" validate:"required,conIDFormat"`
Reason ModificationReason `json:"reason" xml:"reason" validate:"required"`
ReasonDescription string `json:"reason_description" xml:"reason_description" validate:"required,max=10000"`
ValueBefore Money `json:"value_before" xml:"value_before" validate:"required"`
ValueAfter Money `json:"value_after" xml:"value_after" validate:"required"`
ValueChange Money `json:"value_change" xml:"value_change"`
PercentageChange float64 `json:"percentage_change" xml:"percentage_change"`
DurationBefore *Duration `json:"duration_before,omitempty" xml:"duration_before,omitempty"`
DurationAfter *Duration `json:"duration_after,omitempty" xml:"duration_after,omitempty"`
ModificationDescription string `json:"modification_description" xml:"modification_description" validate:"required,max=10000"`
ModificationDate time.Time `json:"modification_date" xml:"modification_date" validate:"required"`
ContractorChange bool `json:"contractor_change" xml:"contractor_change"`
NewContractorIDs []string `json:"new_contractor_ids,omitempty" xml:"new_contractor_ids,omitempty"`
}
// ModificationReason represents the reason for contract modification
type ModificationReason string
const (
ModReasonAdditionalWorks ModificationReason = "add-wks"
ModReasonUnforeseen ModificationReason = "unforeseen"
ModReasonDesignDefect ModificationReason = "design-defect"
ModReasonAdditionalNeed ModificationReason = "additional-need"
ModReasonRepeatSimilar ModificationReason = "repeat-similar"
ModReasonRatePriceUpdate ModificationReason = "rate-price-update"
ModReasonSuspendCall ModificationReason = "suspend-call"
ModReasonOther ModificationReason = "other"
)
// CalculateModificationChange calculates value change for modifications
func (m *Modification) CalculateModificationChange() {
if m.ValueBefore.Currency == m.ValueAfter.Currency {
change := m.ValueAfter.Amount - m.ValueBefore.Amount
m.ValueChange = Money{
Amount: change,
Currency: m.ValueBefore.Currency,
}
if m.ValueBefore.Amount != 0 {
m.PercentageChange = (change / m.ValueBefore.Amount) * 100
}
}
}
-16
View File
@@ -1,16 +0,0 @@
package eform
// =====================================================
// CHANGE/CORRECTION STRUCTURE
// =====================================================
// NoticeChange represents changes or corrections to notices
type NoticeChange struct {
ChangeID string `json:"change_id" xml:"change_id" validate:"required"`
SectionReference string `json:"section_reference" xml:"section_reference" validate:"required"`
LotReference string `json:"lot_reference,omitempty" xml:"lot_reference,omitempty"`
Label string `json:"label" xml:"label" validate:"required,max=200"`
OldValue string `json:"old_value,omitempty" xml:"old_value,omitempty"`
NewValue string `json:"new_value" xml:"new_value" validate:"required"`
ChangeDescription string `json:"change_description,omitempty" xml:"change_description,omitempty" validate:"max=2000"`
}
-144
View File
@@ -1,144 +0,0 @@
package eform
import "time"
// =====================================================
// NOTICE METADATA (Legacy - keeping for compatibility)
// =====================================================
// NoticeMetadata contains identification and publication information
type NoticeMetadata struct {
NoticeID string `json:"notice_id" xml:"notice_id" validate:"required,noticeIDFormat"`
NoticeType NoticeType `json:"notice_type" xml:"notice_type" validate:"required"`
NoticeVersion string `json:"notice_version" xml:"notice_version" validate:"required,semver"`
PublicationDate *time.Time `json:"publication_date,omitempty" xml:"publication_date,omitempty"`
DispatchDate time.Time `json:"dispatch_date" xml:"dispatch_date" validate:"required"`
Language LanguageCode `json:"language" xml:"language" validate:"required,iso639_1"`
TranslationLanguages []LanguageCode `json:"translation_languages,omitempty" xml:"translation_languages,omitempty" validate:"dive,iso639_1"`
LegalBasis LegalBasis `json:"legal_basis" xml:"legal_basis" validate:"required"`
PreviousNoticeID string `json:"previous_notice_id,omitempty" xml:"previous_notice_id,omitempty" validate:"omitempty,noticeIDFormat"`
ChangedNoticeID string `json:"changed_notice_id,omitempty" xml:"changed_notice_id,omitempty" validate:"omitempty,noticeIDFormat"`
CorrectionPrevious bool `json:"correction_previous" xml:"correction_previous"`
}
// NoticeType represents the eForms subtype identifier (1-40, E1-E5)
type NoticeType string
const (
// Planning notices (1-6)
NoticeTypePIN1 NoticeType = "1" // Prior Information Notice - standard
NoticeTypePIN2 NoticeType = "2" // Prior Information Notice - utilities
NoticeTypePIN3 NoticeType = "3" // Prior Information Notice - social
NoticeTypePINCallComp4 NoticeType = "4" // PIN as call for competition - standard
NoticeTypePINCallComp5 NoticeType = "5" // PIN as call for competition - utilities
NoticeTypePINCallComp6 NoticeType = "6" // PIN as call for competition - social
// Competition notices (7-14)
NoticeTypeCN7 NoticeType = "7" // Contract Notice - open/restricted
NoticeTypeCN8 NoticeType = "8" // Contract Notice - utilities
NoticeTypeCN9 NoticeType = "9" // Contract Notice - negotiated
NoticeTypeCN10 NoticeType = "10" // Contract Notice - competitive dialogue
NoticeTypeCN11 NoticeType = "11" // Contract Notice - innovation partnership
NoticeTypeCN12 NoticeType = "12" // Contract Notice - social services
NoticeTypeCN13 NoticeType = "13" // Contract Notice - concessions
NoticeTypeCN14 NoticeType = "14" // Contract Notice - design contest
// Direct award notices (15-24)
NoticeTypeCANDirect15 NoticeType = "15" // CAN without prior - direct award
NoticeTypeCANDirect16 NoticeType = "16" // CAN without prior - utilities
NoticeTypeCANDirect17 NoticeType = "17" // CAN without prior - social
NoticeTypeCANDirect18 NoticeType = "18" // CAN without prior - negotiated
NoticeTypeCANDirect19 NoticeType = "19" // CAN without prior - concessions
NoticeTypeCANDirect20 NoticeType = "20" // Voluntary ex-ante transparency - direct award
NoticeTypeCANDirect21 NoticeType = "21" // Voluntary ex-ante transparency - utilities
NoticeTypeCANDirect22 NoticeType = "22" // Voluntary ex-ante transparency - social
NoticeTypeCANDirect23 NoticeType = "23" // Voluntary ex-ante transparency - concessions
NoticeTypeCANDirect24 NoticeType = "24" // Voluntary ex-ante transparency - defence
// Result notices (25-35)
NoticeTypeCAN25 NoticeType = "25" // Contract Award Notice - standard
NoticeTypeCAN26 NoticeType = "26" // Contract Award Notice - utilities
NoticeTypeCAN27 NoticeType = "27" // Contract Award Notice - social
NoticeTypeCAN28 NoticeType = "28" // Contract Award Notice - concessions
NoticeTypeCAN29 NoticeType = "29" // Contract Award Notice - design contest results
NoticeTypeCAN30 NoticeType = "30" // Contract Award Notice - defence
NoticeTypeCAN31 NoticeType = "31" // Contract Award Notice - framework single
NoticeTypeCAN32 NoticeType = "32" // Contract Award Notice - DPS award
NoticeTypeCAN33 NoticeType = "33" // Contract Award Notice - modification
NoticeTypeCAN34 NoticeType = "34" // Contract Award Notice - completion
NoticeTypeCAN35 NoticeType = "35" // Contract Award Notice - quarterly DPS
// Modification notices (36-40)
NoticeTypeMod36 NoticeType = "36" // Contract Modification Notice - standard
NoticeTypeMod37 NoticeType = "37" // Contract Modification Notice - utilities
NoticeTypeMod38 NoticeType = "38" // Contract Modification Notice - social
NoticeTypeMod39 NoticeType = "39" // Contract Modification Notice - concessions
NoticeTypeMod40 NoticeType = "40" // Contract Modification Notice - defence
// Voluntary notices (E1-E5)
NoticeTypeVoluntaryE1 NoticeType = "E1" // Voluntary notice - below threshold
NoticeTypeVoluntaryE2 NoticeType = "E2" // Voluntary notice - modification
NoticeTypeVoluntaryE3 NoticeType = "E3" // Voluntary notice - contract completion
NoticeTypeVoluntaryE4 NoticeType = "E4" // Voluntary notice - concession award
NoticeTypeVoluntaryE5 NoticeType = "E5" // Voluntary notice - qualification system
)
// LegalBasis represents applicable EU directives
type LegalBasis string
const (
LegalBasisClassicDir LegalBasis = "32014L0024" // Directive 2014/24/EU (classic)
LegalBasisUtilitiesDir LegalBasis = "32014L0025" // Directive 2014/25/EU (utilities)
LegalBasisConcessionsDir LegalBasis = "32014L0023" // Directive 2014/23/EU (concessions)
LegalBasisDefenceDir LegalBasis = "32009L0081" // Directive 2009/81/EC (defence)
LegalBasisFinancialReg LegalBasis = "32018R1046" // Financial Regulation (EU institutions)
)
// LanguageCode represents ISO 639-1 language codes
type LanguageCode string
// IsPlanningNotice checks if notice is a planning/PIN notice
func (nt NoticeType) IsPlanningNotice() bool {
planningTypes := []NoticeType{
NoticeTypePIN1, NoticeTypePIN2, NoticeTypePIN3,
NoticeTypePINCallComp4, NoticeTypePINCallComp5, NoticeTypePINCallComp6,
}
for _, t := range planningTypes {
if nt == t {
return true
}
}
return false
}
// IsCompetitionNotice checks if notice is a competition/CN notice
func (nt NoticeType) IsCompetitionNotice() bool {
return nt >= NoticeTypeCN7 && nt <= NoticeTypeCN14
}
// IsResultNotice checks if notice is a result/CAN notice
func (nt NoticeType) IsResultNotice() bool {
return (nt >= NoticeTypeCANDirect15 && nt <= NoticeTypeCANDirect24) ||
(nt >= NoticeTypeCAN25 && nt <= NoticeTypeCAN35)
}
// IsModificationNotice checks if notice is a modification notice
func (nt NoticeType) IsModificationNotice() bool {
return nt >= NoticeTypeMod36 && nt <= NoticeTypeMod40
}
// IsVoluntaryNotice checks if notice is voluntary
func (nt NoticeType) IsVoluntaryNotice() bool {
voluntaryTypes := []NoticeType{
NoticeTypeVoluntaryE1, NoticeTypeVoluntaryE2, NoticeTypeVoluntaryE3,
NoticeTypeVoluntaryE4, NoticeTypeVoluntaryE5,
NoticeTypeCANDirect20, NoticeTypeCANDirect21, NoticeTypeCANDirect22,
NoticeTypeCANDirect23, NoticeTypeCANDirect24,
}
for _, t := range voluntaryTypes {
if nt == t {
return true
}
}
return false
}
-97
View File
@@ -1,97 +0,0 @@
package eform
// =====================================================
// ORGANIZATION STRUCTURE
// =====================================================
// Organization represents any party involved in the procurement
type Organization struct {
ID string `json:"id" xml:"id" validate:"required,orgIDFormat"`
Roles []OrganizationRole `json:"roles" xml:"roles" validate:"required,min=1,dive"`
LegalName string `json:"legal_name" xml:"legal_name" validate:"required,max=500"`
LegalType *OrganizationType `json:"legal_type,omitempty" xml:"legal_type,omitempty"`
RegistrationID string `json:"registration_id,omitempty" xml:"registration_id,omitempty"`
VATNumber string `json:"vat_number,omitempty" xml:"vat_number,omitempty" validate:"omitempty,vatFormat"`
NationalID string `json:"national_id,omitempty" xml:"national_id,omitempty"`
Address Address `json:"address" xml:"address" validate:"required"`
ContactPoint ContactPoint `json:"contact_point" xml:"contact_point" validate:"required"`
BuyerProfile string `json:"buyer_profile,omitempty" xml:"buyer_profile,omitempty" validate:"omitempty,url"`
ProcurementActivity *ActivityType `json:"procurement_activity,omitempty" xml:"procurement_activity,omitempty"`
SME bool `json:"sme" xml:"sme"`
}
// OrganizationRole defines the role of an organization in the procurement
type OrganizationRole string
const (
RoleBuyer OrganizationRole = "buyer"
RoleServiceProvider OrganizationRole = "service-provider"
RoleTEDeSender OrganizationRole = "ted-esen"
RoleWinner OrganizationRole = "winner"
RoleSubcontractor OrganizationRole = "subcontractor"
RoleReviewBody OrganizationRole = "review-body"
RoleReviewOrg OrganizationRole = "review-org"
RoleMediationBody OrganizationRole = "mediation-body"
RoleLeadBuyer OrganizationRole = "lead-buyer"
RoleCentralBuyer OrganizationRole = "central-buyer"
RoleAcquirer OrganizationRole = "acquirer"
)
// OrganizationType represents the legal type of organization
type OrganizationType string
const (
OrgTypeBodyPublicLaw OrganizationType = "body-pl"
OrgTypeCentralGov OrganizationType = "cga"
OrgTypeLocalAuthority OrganizationType = "la"
OrgTypeSubsidized OrganizationType = "org-sub"
OrgTypePublicUndertaking OrganizationType = "pub-undert"
OrgTypeEUInstitution OrganizationType = "eu-ins-bod-ag"
OrgTypeOther OrganizationType = "org-other"
)
// ActivityType represents main procurement activity
type ActivityType string
const (
ActivityAuthority ActivityType = "auth-oth"
ActivityDefence ActivityType = "def"
ActivityEconomicAffairs ActivityType = "econ-aff"
ActivityEducation ActivityType = "edu"
ActivityEnvironment ActivityType = "env"
ActivityGeneralPublic ActivityType = "gen-pub"
ActivityHealth ActivityType = "health"
ActivityHousingCommunity ActivityType = "hous-com"
ActivityPublicOrderSafety ActivityType = "pub-os"
ActivityRecreationCulture ActivityType = "rec-com"
ActivitySocialProtection ActivityType = "soc-pro"
ActivityWater ActivityType = "water"
ActivityAirport ActivityType = "airport"
ActivityElectricity ActivityType = "electricity"
ActivityExplorationExtract ActivityType = "exploration-extraction"
ActivityGasHeat ActivityType = "gas-heat"
ActivityPort ActivityType = "port"
ActivityPost ActivityType = "post"
ActivityRailway ActivityType = "rail"
ActivityUrbanRailway ActivityType = "urban-railway"
ActivityWaterUtility ActivityType = "water"
)
// Address represents physical address information
type Address struct {
StreetAddress string `json:"street_address" xml:"street_address" validate:"required,max=200"`
Locality string `json:"locality" xml:"locality" validate:"required,max=100"`
PostalCode string `json:"postal_code" xml:"postal_code" validate:"required,max=20"`
CountrySubdivision string `json:"country_subdivision,omitempty" xml:"country_subdivision,omitempty" validate:"max=100"`
CountryCode string `json:"country_code" xml:"country_code" validate:"required,iso3166_1_alpha2"`
NUTSCodes []string `json:"nuts_codes,omitempty" xml:"nuts_codes,omitempty" validate:"dive,nutsCode"`
}
// ContactPoint represents contact information
type ContactPoint struct {
Name string `json:"name,omitempty" xml:"name,omitempty" validate:"max=200"`
Email string `json:"email" xml:"email" validate:"required,email"`
Telephone string `json:"telephone,omitempty" xml:"telephone,omitempty" validate:"max=30"`
Fax string `json:"fax,omitempty" xml:"fax,omitempty" validate:"max=30"`
URL string `json:"url,omitempty" xml:"url,omitempty" validate:"omitempty,url"`
}
@@ -1,44 +0,0 @@
package prior_information
import (
"encoding/xml"
"tm/internal/tender"
"tm/ted/eform"
)
// PriorInformationNotice represents the UBL PriorInformationNotice structure
type PriorInformationNotice struct {
XMLName xml.Name `xml:"PriorInformationNotice"`
UBLVersionID string `json:"ubl_version_id" xml:"UBLVersionID"`
CustomizationID string `json:"customization_id" xml:"CustomizationID"`
ID string `json:"id" xml:"ID"`
ContractFolderID string `json:"contract_folder_id" xml:"ContractFolderID"`
IssueDate string `json:"issue_date" xml:"IssueDate"`
IssueTime string `json:"issue_time,omitempty" xml:"IssueTime,omitempty"`
VersionID string `json:"version_id,omitempty" xml:"VersionID,omitempty"`
RegulatoryDomain string `json:"regulatory_domain,omitempty" xml:"RegulatoryDomain,omitempty"`
NoticeTypeCode string `json:"notice_type_code" xml:"NoticeTypeCode"`
NoticeLanguageCode string `json:"notice_language_code" xml:"NoticeLanguageCode"`
ContractingParty eform.ContractingParty `json:"contracting_party" xml:"ContractingParty"`
TenderingTerms eform.TenderingTerms `json:"tendering_terms,omitempty" xml:"TenderingTerms,omitempty"`
TenderingProcess eform.TenderingProcess `json:"tendering_process,omitempty" xml:"TenderingProcess,omitempty"`
ProcurementProject eform.ProcurementProject `json:"procurement_project" xml:"ProcurementProject"`
ProcurementProjectLot []eform.ProcurementProjectLot `json:"procurement_project_lots,omitempty" xml:"ProcurementProjectLot,omitempty"`
UBLExtensions eform.UBLExtensions `json:"ubl_extensions,omitempty" xml:"UBLExtensions,omitempty"`
}
// GetID returns the prior information notice ID
func (pin PriorInformationNotice) GetID() string {
return pin.ID
}
// Validate validates the parsed model
func (pin PriorInformationNotice) Validate() error {
return nil
}
func (pin PriorInformationNotice) MapToTender() *tender.Tender {
t := new(tender.Tender)
t.ContractNoticeID = pin.GetID()
return t
}
-244
View File
@@ -1,244 +0,0 @@
package eform
// =====================================================
// PROCEDURE STRUCTURE
// =====================================================
// Procedure represents the procurement procedure
type Procedure struct {
ID string `json:"id" xml:"id" validate:"required,procIDFormat"`
Title string `json:"title" xml:"title" validate:"required,max=500"`
Description string `json:"description" xml:"description" validate:"required,max=10000"`
ProcedureType ProcedureType `json:"procedure_type" xml:"procedure_type" validate:"required"`
AcceleratedProcedure bool `json:"accelerated_procedure" xml:"accelerated_procedure"`
AcceleratedJustification string `json:"accelerated_justification,omitempty" xml:"accelerated_justification,omitempty" validate:"required_if=AcceleratedProcedure true"`
FrameworkAgreement bool `json:"framework_agreement" xml:"framework_agreement"`
FAMaxParticipants *int `json:"fa_max_participants,omitempty" xml:"fa_max_participants,omitempty" validate:"omitempty,min=1"`
FADuration *Duration `json:"fa_duration,omitempty" xml:"fa_duration,omitempty"`
DynamicPurchasingSystem bool `json:"dynamic_purchasing_system" xml:"dynamic_purchasing_system"`
ElectronicAuction bool `json:"electronic_auction" xml:"electronic_auction"`
JointProcurement bool `json:"joint_procurement" xml:"joint_procurement"`
CentralPurchasing bool `json:"central_purchasing" xml:"central_purchasing"`
GPA bool `json:"gpa" xml:"gpa"`
InternationalAgreements []string `json:"international_agreements,omitempty" xml:"international_agreements,omitempty"`
MainCPVCode string `json:"main_cpv_code" xml:"main_cpv_code" validate:"required,cpvCode"`
AdditionalCPVCodes []string `json:"additional_cpv_codes,omitempty" xml:"additional_cpv_codes,omitempty" validate:"dive,cpvCode"`
ContractNature ContractNature `json:"contract_nature" xml:"contract_nature" validate:"required"`
EstimatedValue *Money `json:"estimated_value,omitempty" xml:"estimated_value,omitempty"`
EstimatedValueRange *ValueRange `json:"estimated_value_range,omitempty" xml:"estimated_value_range,omitempty"`
ProcurementDocumentsURL string `json:"procurement_documents_url,omitempty" xml:"procurement_documents_url,omitempty" validate:"omitempty,url"`
ProcurementDocsRestricted bool `json:"procurement_docs_restricted" xml:"procurement_docs_restricted"`
SubmissionElectronic SubmissionMethod `json:"submission_electronic" xml:"submission_electronic" validate:"required"`
SubmissionLanguages []LanguageCode `json:"submission_languages" xml:"submission_languages" validate:"required,min=1,dive,iso639_1"`
TenderValidityDuration *Duration `json:"tender_validity_duration,omitempty" xml:"tender_validity_duration,omitempty"`
Lots []Lot `json:"lots,omitempty" xml:"lots,omitempty" validate:"dive"`
SecondStage *SecondStage `json:"second_stage,omitempty" xml:"second_stage,omitempty"`
Recurrence bool `json:"recurrence" xml:"recurrence"`
RecurrenceTiming string `json:"recurrence_timing,omitempty" xml:"recurrence_timing,omitempty" validate:"max=1000"`
}
// ProcedureType represents the type of procurement procedure
type ProcedureType string
const (
ProcTypeOpen ProcedureType = "open"
ProcTypeRestricted ProcedureType = "restricted"
ProcTypeCompetitiveDialogue ProcedureType = "comp-dial"
ProcTypeInnovation ProcedureType = "innovation"
ProcTypeNegotiatedWithCall ProcedureType = "neg-w-call"
ProcTypeNegotiatedWoCall ProcedureType = "neg-wo-call"
ProcTypeCompWithNeg ProcedureType = "comp-w-n"
ProcTypeAwardWoCall ProcedureType = "pt-award-wo-call"
ProcTypeOtherSingle ProcedureType = "oth-single"
ProcTypeOtherMultiple ProcedureType = "oth-mult"
)
// ContractNature represents the type of contract
type ContractNature string
const (
ContractNatureWorks ContractNature = "works"
ContractNatureSupplies ContractNature = "supplies"
ContractNatureServices ContractNature = "services"
ContractNatureCombined ContractNature = "combined"
)
// SubmissionMethod represents how tenders can be submitted
type SubmissionMethod string
const (
SubmissionAllowed SubmissionMethod = "allowed"
SubmissionRequired SubmissionMethod = "required"
SubmissionNotAllowed SubmissionMethod = "not-allowed"
)
// Duration represents a time duration
type Duration struct {
Type DurationType `json:"type" xml:"type" validate:"required"`
Value *int `json:"value,omitempty" xml:"value,omitempty" validate:"omitempty,min=1"`
}
// DurationType represents the unit of duration
type DurationType string
const (
DurationMonths DurationType = "months"
DurationDays DurationType = "days"
DurationYears DurationType = "years"
DurationUnlimited DurationType = "unlimited"
)
// Money represents a monetary value
type Money struct {
Amount float64 `json:"amount" xml:"amount" validate:"required,min=0"`
Currency string `json:"currency" xml:"currency" validate:"required,iso4217"`
}
// ValueRange represents a range of monetary values
type ValueRange struct {
MinValue Money `json:"min_value" xml:"min_value" validate:"required"`
MaxValue Money `json:"max_value" xml:"max_value" validate:"required,gtfield=MinValue.Amount"`
}
// SecondStage represents second stage of two-stage procedures
type SecondStage struct {
MinCandidates *int `json:"min_candidates,omitempty" xml:"min_candidates,omitempty" validate:"omitempty,min=1"`
MaxCandidates *int `json:"max_candidates,omitempty" xml:"max_candidates,omitempty" validate:"omitempty,min=1"`
ObjectiveCriteria string `json:"objective_criteria,omitempty" xml:"objective_criteria,omitempty" validate:"max=10000"`
}
// IsAboveThreshold checks if procedure value is above EU thresholds
func (p *Procedure) IsAboveThreshold() bool {
if p.EstimatedValue == nil {
return false
}
// EU thresholds 2024-2025 (in EUR)
thresholds := map[ContractNature]float64{
ContractNatureWorks: 5382000,
ContractNatureSupplies: 143000, // central government
ContractNatureServices: 143000, // central government
}
threshold, exists := thresholds[p.ContractNature]
if !exists {
return false
}
// Convert to EUR if needed
valueEUR := p.EstimatedValue.Amount
if p.EstimatedValue.Currency != CurrencyEUR {
// In real implementation, would use exchange rates
// This is simplified
return true // Assume above threshold for non-EUR
}
return valueEUR >= threshold
}
// GetTotalEstimatedValue calculates total value from lots
func (p *Procedure) GetTotalEstimatedValue() *Money {
if len(p.Lots) == 0 {
return p.EstimatedValue
}
var total float64
var currency string
for _, lot := range p.Lots {
if lot.EstimatedValue != nil {
if currency == "" {
currency = lot.EstimatedValue.Currency
}
if lot.EstimatedValue.Currency == currency {
total += lot.EstimatedValue.Amount
}
}
}
if currency != "" {
return &Money{
Amount: total,
Currency: currency,
}
}
return p.EstimatedValue
}
// IsDividedIntoLots checks if procedure has lots
func (p *Procedure) IsDividedIntoLots() bool {
return len(p.Lots) > 0
}
// GetLotByID retrieves lot by ID
func (p *Procedure) GetLotByID(id string) *Lot {
for i := range p.Lots {
if p.Lots[i].ID == id {
return &p.Lots[i]
}
}
return nil
}
// IsFrameworkAgreement checks if this is a framework agreement
func (p *Procedure) IsFrameworkAgreement() bool {
return p.FrameworkAgreement
}
// IsDynamicPurchasingSystem checks if this is a DPS
func (p *Procedure) IsDynamicPurchasingSystem() bool {
return p.DynamicPurchasingSystem
}
// RequiresTwoStage checks if procedure is two-stage
func (p *Procedure) RequiresTwoStage() bool {
twoStageProcs := []ProcedureType{
ProcTypeRestricted,
ProcTypeCompetitiveDialogue,
ProcTypeInnovation,
ProcTypeNegotiatedWithCall,
}
for _, t := range twoStageProcs {
if p.ProcedureType == t {
return true
}
}
return false
}
// GetAwardedContracts returns all awarded contracts from procedure
func (p *Procedure) GetAwardedContracts() []SettledContract {
var contracts []SettledContract
for _, lot := range p.Lots {
if lot.Result != nil && lot.Result.SettledContract != nil {
contracts = append(contracts, *lot.Result.SettledContract)
}
}
return contracts
}
// GetTotalAwardedValue calculates total value of all awarded contracts
func (p *Procedure) GetTotalAwardedValue() *Money {
contracts := p.GetAwardedContracts()
if len(contracts) == 0 {
return nil
}
var total float64
currency := contracts[0].ContractValue.Currency
for _, contract := range contracts {
if contract.ContractValue.Currency == currency {
total += contract.ContractValue.Amount
}
}
return &Money{
Amount: total,
Currency: currency,
}
}
-49
View File
@@ -1,49 +0,0 @@
package eform
import "encoding/xml"
// =====================================================
// PROCUREMENT PROJECT
// =====================================================
// ProcurementProject represents the procurement project
type ProcurementProject struct {
XMLName xml.Name `xml:"ProcurementProject"`
ID string `xml:"ID"`
Name string `xml:"Name"`
Description string `xml:"Description"`
ProcurementTypeCode string `xml:"ProcurementTypeCode"`
Note string `xml:"Note,omitempty"`
RequestedTenderTotal RequestedTenderTotal `xml:"RequestedTenderTotal,omitempty"`
MainCommodityClassification MainCommodityClassification `xml:"MainCommodityClassification,omitempty"`
RealizedLocation RealizedLocation `xml:"RealizedLocation,omitempty"`
PlannedPeriod PlannedPeriod `xml:"PlannedPeriod,omitempty"`
}
// RequestedTenderTotal represents the estimated contract amount
type RequestedTenderTotal struct {
XMLName xml.Name `xml:"RequestedTenderTotal"`
EstimatedOverallContractAmount string `xml:"EstimatedOverallContractAmount"`
}
// MainCommodityClassification represents the CPV code
type MainCommodityClassification struct {
ItemClassificationCode string `json:"item_classification_code" xml:"ItemClassificationCode"`
}
// RealizedLocation represents the location where work will be performed
type RealizedLocation struct {
Address PostalAddress `xml:"PostalAddress"`
Region string `xml:"Region,omitempty"`
}
// PlannedPeriod represents the planned duration of the contract
type PlannedPeriod struct {
DurationMeasure DurationMeasure `json:"duration_measure" xml:"DurationMeasure"`
}
// DurationMeasure represents a duration with unit
type DurationMeasure struct {
UnitCode string `json:"unit_code" xml:"unitCode,attr"`
Value string `json:"value" xml:",chardata"`
}
-13
View File
@@ -1,13 +0,0 @@
package eform
// =====================================================
// PROCUREMENT PROJECT LOT
// =====================================================
// ProcurementProjectLot represents a procurement project lot
type ProcurementProjectLot struct {
ID string `json:"id" xml:"ID"`
TenderingTerms TenderingTerms `json:"tendering_terms,omitempty" xml:"TenderingTerms,omitempty"`
TenderingProcess TenderingProcess `json:"tendering_process,omitempty" xml:"TenderingProcess,omitempty"`
ProcurementProject ProcurementProject `json:"procurement_project,omitempty" xml:"ProcurementProject,omitempty"`
}
-17
View File
@@ -1,17 +0,0 @@
package eform
// =====================================================
// REVIEW/APPEAL INFORMATION
// =====================================================
// ReviewInfo represents information about review procedures
type ReviewInfo struct {
ReviewBody string `json:"review_body" xml:"review_body" validate:"required"`
ReviewBodyID string `json:"review_body_id" xml:"review_body_id" validate:"required,orgIDFormat"`
ReviewProcedure string `json:"review_procedure" xml:"review_procedure" validate:"required,max=10000"`
ReviewDeadline string `json:"review_deadline,omitempty" xml:"review_deadline,omitempty" validate:"max=1000"`
MediationBody string `json:"mediation_body,omitempty" xml:"mediation_body,omitempty"`
MediationBodyID string `json:"mediation_body_id,omitempty" xml:"mediation_body_id,omitempty" validate:"omitempty,orgIDFormat"`
ReviewInfoURL string `json:"review_info_url,omitempty" xml:"review_info_url,omitempty" validate:"omitempty,url"`
LodgingOfAppeals string `json:"lodging_of_appeals,omitempty" xml:"lodging_of_appeals,omitempty" validate:"max=10000"`
}
-26
View File
@@ -1,26 +0,0 @@
package eform
import "time"
// =====================================================
// SETTLED CONTRACT STRUCTURE
// =====================================================
// SettledContract represents awarded contract details
type SettledContract struct {
ID string `json:"id" xml:"id" validate:"required,conIDFormat"`
ContractNumber string `json:"contract_number,omitempty" xml:"contract_number,omitempty" validate:"max=100"`
Title string `json:"title" xml:"title" validate:"required,max=500"`
ConclusionDate time.Time `json:"conclusion_date" xml:"conclusion_date" validate:"required"`
ContractValue Money `json:"contract_value" xml:"contract_value" validate:"required"`
InitialValue *Money `json:"initial_value,omitempty" xml:"initial_value,omitempty"`
FrameworkMaxValue *Money `json:"framework_max_value,omitempty" xml:"framework_max_value,omitempty"`
StartDate *time.Time `json:"start_date,omitempty" xml:"start_date,omitempty"`
EndDate *time.Time `json:"end_date,omitempty" xml:"end_date,omitempty"`
Duration *Duration `json:"duration,omitempty" xml:"duration,omitempty"`
ContractorIDs []string `json:"contractor_ids" xml:"contractor_ids" validate:"required,min=1"`
Modifications []Modification `json:"modifications,omitempty" xml:"modifications,omitempty" validate:"dive"`
FinalValue *Money `json:"final_value,omitempty" xml:"final_value,omitempty"`
CompletionDate *time.Time `json:"completion_date,omitempty" xml:"completion_date,omitempty"`
GroupID string `json:"group_id,omitempty" xml:"group_id,omitempty"`
}
-43
View File
@@ -1,43 +0,0 @@
package eform
// =====================================================
// TECHNICAL SPECIFICATIONS
// =====================================================
// TechnicalSpecifications contains technical and eligibility requirements
type TechnicalSpecifications struct {
Description string `json:"description" xml:"description" validate:"required,max=10000"`
EligibilityCriteria string `json:"eligibility_criteria,omitempty" xml:"eligibility_criteria,omitempty" validate:"max=10000"`
TechnicalCapability string `json:"technical_capability,omitempty" xml:"technical_capability,omitempty" validate:"max=10000"`
EconomicCapability string `json:"economic_capability,omitempty" xml:"economic_capability,omitempty" validate:"max=10000"`
References bool `json:"references" xml:"references"`
ReferencesDescription string `json:"references_description,omitempty" xml:"references_description,omitempty" validate:"required_if=References true"`
SecurityClearance bool `json:"security_clearance" xml:"security_clearance"`
SecurityClearanceDescription string `json:"security_clearance_description,omitempty" xml:"security_clearance_description,omitempty" validate:"required_if=SecurityClearance true"`
ReservedExecution ReservedExecution `json:"reserved_execution" xml:"reserved_execution"`
StrategicProcurement []StrategicType `json:"strategic_procurement,omitempty" xml:"strategic_procurement,omitempty"`
AccessibilityCriteria bool `json:"accessibility_criteria" xml:"accessibility_criteria"`
AccessibilityDescription string `json:"accessibility_description,omitempty" xml:"accessibility_description,omitempty" validate:"max=10000"`
GreenProcurement bool `json:"green_procurement" xml:"green_procurement"`
SocialProcurement bool `json:"social_procurement" xml:"social_procurement"`
InnovativeProcurement bool `json:"innovative_procurement" xml:"innovative_procurement"`
}
// ReservedExecution represents reserved participation types
type ReservedExecution string
const (
ReservedNone ReservedExecution = "none"
ReservedShelteredWS ReservedExecution = "sheltered-workshop"
ReservedPublicService ReservedExecution = "public-service-mission"
)
// StrategicType represents strategic procurement objectives
type StrategicType string
const (
StrategicEnvironmental StrategicType = "environmental"
StrategicSocial StrategicType = "social"
StrategicInnovation StrategicType = "innovation"
StrategicOther StrategicType = "other"
)
-24
View File
@@ -1,24 +0,0 @@
package eform
// =====================================================
// TENDER STRUCTURE
// =====================================================
// Tender represents information about individual bids
type Tender struct {
ID string `json:"id" xml:"id" validate:"required,tenIDFormat"`
TenderRank *int `json:"tender_rank,omitempty" xml:"tender_rank,omitempty" validate:"omitempty,min=1"`
IsWinner bool `json:"is_winner" xml:"is_winner"`
TendererIDs []string `json:"tenderer_ids" xml:"tenderer_ids" validate:"required,min=1"`
IsConsortium bool `json:"is_consortium" xml:"is_consortium"`
VariantTender bool `json:"variant_tender" xml:"variant_tender"`
ExcludedTender bool `json:"excluded_tender" xml:"excluded_tender"`
ExclusionReason string `json:"exclusion_reason,omitempty" xml:"exclusion_reason,omitempty" validate:"required_if=ExcludedTender true"`
TenderValue *Money `json:"tender_value,omitempty" xml:"tender_value,omitempty"`
SubcontractingPercentage *float64 `json:"subcontracting_percentage,omitempty" xml:"subcontracting_percentage,omitempty" validate:"omitempty,min=0,max=100"`
SMETender bool `json:"sme_tender" xml:"sme_tender"`
ConcessionValue *Money `json:"concession_value,omitempty" xml:"concession_value,omitempty"`
SubcontractorIDs []string `json:"subcontractor_ids,omitempty" xml:"subcontractor_ids,omitempty"`
AbnormallyLow bool `json:"abnormally_low" xml:"abnormally_low"`
AbnormallyLowExplanation string `json:"abnormally_low_explanation,omitempty" xml:"abnormally_low_explanation,omitempty" validate:"max=10000"`
}
-28
View File
@@ -1,28 +0,0 @@
package eform
import "encoding/xml"
// =====================================================
// TENDERING PROCESS
// =====================================================
// TenderingProcess represents the tendering process
type TenderingProcess struct {
XMLName xml.Name `xml:"TenderingProcess"`
ProcedureCode string `xml:"ProcedureCode"`
ProcessJustification ProcessJustification `xml:"ProcessJustification,omitempty"`
TenderSubmissionDeadlinePeriod TenderSubmissionDeadlinePeriod `xml:"TenderSubmissionDeadlinePeriod,omitempty"`
}
// ProcessJustification represents process justification
type ProcessJustification struct {
XMLName xml.Name `xml:"ProcessJustification"`
ProcessReasonCode string `xml:"ProcessReasonCode"`
}
// TenderSubmissionDeadlinePeriod represents the deadline for tender submission
type TenderSubmissionDeadlinePeriod struct {
XMLName xml.Name `xml:"TenderSubmissionDeadlinePeriod"`
EndDate string `xml:"EndDate"`
EndTime string `xml:"EndTime,omitempty"`
}
-85
View File
@@ -1,85 +0,0 @@
package eform
import (
"encoding/xml"
"time"
)
// =====================================================
// TENDERING TERMS
// =====================================================
// TenderingTerms represents tendering terms
type TenderingTerms struct {
TendererQualificationRequest []TendererQualificationRequest `json:"tenderer_qualification_request,omitempty" xml:"TendererQualificationRequest,omitempty"`
AppealTerms AppealTerms `json:"appeal_terms,omitempty" xml:"AppealTerms,omitempty"`
CallForTendersDocumentReference CallForTendersDocumentReference `json:"call_for_tenders_document_reference,omitempty" xml:"CallForTendersDocumentReference,omitempty"`
TenderRecipientParty TenderRecipientParty `json:"tender_recipient_party,omitempty" xml:"TenderRecipientParty,omitempty"`
UBLExtensions UBLExtensions `json:"ubl_extensions,omitempty" xml:"UBLExtensions,omitempty"`
}
// TendererQualificationRequest represents qualification requirements
type TendererQualificationRequest struct {
SpecificTendererRequirement []SpecificTendererRequirement `json:"specific_tenderer_requirement,omitempty" xml:"SpecificTendererRequirement,omitempty"`
}
// SpecificTendererRequirement represents specific requirements
type SpecificTendererRequirement struct {
XMLName xml.Name `xml:"SpecificTendererRequirement"`
TendererRequirementTypeCode string `xml:"TendererRequirementTypeCode"`
Description string `xml:"Description,omitempty"`
}
// AppealTerms represents appeal terms for review organization
type AppealTerms struct {
XMLName xml.Name `xml:"AppealTerms"`
AppealReceiverParty AppealReceiverParty `xml:"AppealReceiverParty"`
}
// AppealReceiverParty represents the party that receives appeals
type AppealReceiverParty struct {
XMLName xml.Name `xml:"AppealReceiverParty"`
PartyIdentification PartyIdentification `xml:"PartyIdentification"`
}
// CallForTendersDocumentReference represents document reference
type CallForTendersDocumentReference struct {
XMLName xml.Name `xml:"CallForTendersDocumentReference"`
ID string `xml:"ID"`
DocumentType string `xml:"DocumentType,omitempty"`
Attachment Attachment `xml:"Attachment"`
UBLExtensions UBLExtensions `xml:"UBLExtensions,omitempty"`
}
// Attachment represents document attachment
type Attachment struct {
ExternalReference ExternalReference `json:"external_reference" xml:"ExternalReference"`
}
// ExternalReference represents external document reference
type ExternalReference struct {
XMLName xml.Name `xml:"ExternalReference"`
URI string `xml:"URI"`
}
// TenderRecipientParty represents the party that receives tenders
type TenderRecipientParty struct {
XMLName xml.Name `xml:"TenderRecipientParty"`
EndpointID string `xml:"EndpointID"`
}
// EOSize represents economic operator size restrictions
type EOSize string
const (
EOSizeNone EOSize = "none"
EOSizeSME EOSize = "sme"
EOSizeNotSME EOSize = "not-sme"
)
// TenderOpening represents tender opening information
type TenderOpening struct {
DateTime time.Time `json:"date_time" xml:"date_time" validate:"required"`
Place string `json:"place,omitempty" xml:"place,omitempty" validate:"max=500"`
Description string `json:"description,omitempty" xml:"description,omitempty" validate:"max=2000"`
}
-71
View File
@@ -1,71 +0,0 @@
package eform
import "encoding/xml"
// =====================================================
// UBL EXTENSIONS
// =====================================================
// UBLExtensions represents the UBL extensions element
type UBLExtensions struct {
UBLExtension []UBLExtension `json:"ubl_extension,omitempty" xml:"UBLExtension,omitempty"`
}
// UBLExtension represents a single UBL extension
type UBLExtension struct {
ExtensionContent ExtensionContent `json:"extension_content,omitempty" xml:"ExtensionContent,omitempty"`
}
// ExtensionContent represents the extension content
type ExtensionContent struct {
EformsExtension EformsExtension `json:"eforms_extension,omitempty" xml:"EformsExtension,omitempty"`
}
// EformsExtension represents the eforms extension data
type EformsExtension struct {
NoticeSubType NoticeSubType `json:"notice_sub_type,omitempty" xml:"NoticeSubType,omitempty"`
Organizations Organizations `json:"organizations,omitempty" xml:"Organizations,omitempty"`
Publication Publication `json:"publication,omitempty" xml:"Publication,omitempty"`
SelectionCriteria []SelectionCriteria `json:"selection_criteria,omitempty" xml:"SelectionCriteria,omitempty"`
OfficialLanguages OfficialLanguages `json:"official_languages,omitempty" xml:"OfficialLanguages,omitempty"`
}
// NoticeSubType represents the notice subtype
type NoticeSubType struct {
XMLName xml.Name `xml:"NoticeSubType"`
SubTypeCode string `xml:"SubTypeCode"`
}
// Organizations represents the organizations element
type Organizations struct {
XMLName xml.Name `xml:"Organizations"`
Organization []Organization `xml:"Organization,omitempty"`
}
// Publication represents the publication information
type Publication struct {
XMLName xml.Name `xml:"Publication"`
NoticePublicationID string `xml:"NoticePublicationID"`
GazetteID string `xml:"GazetteID"`
PublicationDate string `xml:"PublicationDate"`
}
// SelectionCriteria represents selection criteria for tenderers
type SelectionCriteria struct {
XMLName xml.Name `xml:"SelectionCriteria"`
TendererRequirementTypeCode string `xml:"TendererRequirementTypeCode"`
Description string `xml:"Description,omitempty"`
SecondStageIndicator bool `xml:"SecondStageIndicator,omitempty"`
}
// OfficialLanguages represents official languages for the tender
type OfficialLanguages struct {
XMLName xml.Name `xml:"OfficialLanguages"`
Language []Language `xml:"Language,omitempty"`
}
// Language represents a language code
type Language struct {
XMLName xml.Name `xml:"Language"`
ID string `xml:"ID"`
}
-19
View File
@@ -1,19 +0,0 @@
package eform
// =====================================================
// VALIDATION HELPER TYPES
// =====================================================
// ValidationError represents a validation error
type ValidationError struct {
Field string `json:"field"`
Tag string `json:"tag"`
Value string `json:"value"`
Message string `json:"message"`
}
// ValidationResult represents the result of validation
type ValidationResult struct {
Valid bool `json:"valid"`
Errors []ValidationError `json:"errors,omitempty"`
}
+367
View File
@@ -0,0 +1,367 @@
package ted
import (
"strconv"
"strings"
"time"
"tm/internal/tender"
)
// 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)
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 = 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: ParseDateToUnix(cn.IssueDate),
IssueTime: 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 = ParseDateToUnix(pubInfo.PublicationDate)
t.TenderURL = 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 = ParseDateToUnix(deadline)
if deadlineTime, err := ParseDate(deadline); err == nil {
applicationDeadline := CalculateApplicationDeadline(deadlineTime)
t.ApplicationDeadline = applicationDeadline.Unix()
}
}
if pubDate, err := ParseDate(cn.GetPublicationDate()); err == nil {
submissionDeadline := 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 = 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: ParseDateToUnix(can.IssueDate),
IssueTime: 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 = ParseDateToUnix(pubInfo.PublicationDate)
t.TenderURL = 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 = 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
}
+1884
View File
File diff suppressed because it is too large Load Diff
+39 -40
View File
@@ -5,26 +5,13 @@ import (
"fmt"
"strings"
"tm/pkg/xmlparser"
"tm/ted/eform/business_registration_information"
"tm/ted/eform/contract"
"tm/ted/eform/contract_award"
"tm/ted/eform/prior_information"
)
type ParserType string
const (
ParserTypeContractNotice ParserType = "ContractNotice"
ParserTypeContractAwardNotice ParserType = "ContractAwardNotice"
ParserTypePriorInformationNotice ParserType = "PriorInformationNotice"
ParserTypeBusinessRegistrationInformationNotice ParserType = "BusinessRegistrationInformationNotice"
)
type TEDParser struct {
ContractNotice xmlparser.Parser[contract.ContractNotice]
ContractAwardNotice xmlparser.Parser[contract_award.ContractAwardNotice]
PriorInformationNotice xmlparser.Parser[prior_information.PriorInformationNotice]
BusinessRegistrationInformationNotice xmlparser.Parser[business_registration_information.BusinessRegistrationInformationNotice]
ContractNotice xmlparser.Parser[ContractNotice]
ContractAwardNotice xmlparser.Parser[ContractAwardNotice]
PriorInformationNotice xmlparser.Parser[PriorInformationNotice]
// BusinessRegistrationInformationNotice xmlparser.Parser[BusinessRegistrationInformationNotice]
}
// NewTEDParser creates a new TED parser
@@ -33,43 +20,55 @@ func NewTEDParser() *TEDParser {
options.MaxSize = 50 * 1024 * 1024 // 50MB limit for TED XML files
options.IgnoreUnknownElements = true // TED XML has many optional fields
contractNoticeParser := xmlparser.NewParser[contract.ContractNotice](options)
contractAwardNoticeParser := xmlparser.NewParser[contract_award.ContractAwardNotice](options)
priorInformationNoticeParser := xmlparser.NewParser[prior_information.PriorInformationNotice](options)
businessRegistrationInformationNoticeParser := xmlparser.NewParser[business_registration_information.BusinessRegistrationInformationNotice](options)
contractNoticeParser := xmlparser.NewParser[ContractNotice](options)
contractAwardNoticeParser := xmlparser.NewParser[ContractAwardNotice](options)
priorInformationNoticeParser := xmlparser.NewParser[PriorInformationNotice](options)
// businessRegistrationInformationNoticeParser := xmlparser.NewParser[BusinessRegistrationInformationNotice](options)
return &TEDParser{
ContractNotice: contractNoticeParser,
ContractAwardNotice: contractAwardNoticeParser,
PriorInformationNotice: priorInformationNoticeParser,
BusinessRegistrationInformationNotice: businessRegistrationInformationNoticeParser,
// BusinessRegistrationInformationNotice: businessRegistrationInformationNoticeParser,
}
}
// ParseAnyNotice parses XML content and detects the notice type automatically
func (tp *TEDParser) Parse(content []byte) (interface{}, string, ParserType, error) {
// ParseXML automatically detects and parses any supported TED XML document type
func (p *TEDParser) ParseXML(content []byte) (*ParsedDocument, string, DocumentType, error) {
// First, detect the root element type
rootElement, err := detectRootElement(content)
if err != nil {
return nil, "", ParserType(""), fmt.Errorf("failed to detect root element: %w", err)
return nil, "", DocumentType(""), fmt.Errorf("failed to detect root element: %w", err)
}
// Parse based on the detected type
switch ParserType(rootElement) {
case ParserTypeContractNotice:
notice, err := tp.ContractNotice.ParseBytes(content)
return notice, notice.GetID(), ParserTypeContractNotice, err
case ParserTypeContractAwardNotice:
notice, err := tp.ContractAwardNotice.ParseBytes(content)
return notice, notice.GetID(), ParserTypeContractAwardNotice, err
case ParserTypePriorInformationNotice:
notice, err := tp.PriorInformationNotice.ParseBytes(content)
return notice, notice.GetID(), ParserTypePriorInformationNotice, err
case ParserTypeBusinessRegistrationInformationNotice:
notice, err := tp.BusinessRegistrationInformationNotice.ParseBytes(content)
return notice, notice.GetID(), ParserTypeBusinessRegistrationInformationNotice, err
parsedDoc := &ParsedDocument{Type: DocumentType(rootElement)}
switch parsedDoc.Type {
case DocumentTypeContractNotice:
notice, err := p.ContractNotice.ParseBytes(content)
if err != nil {
return nil, "", DocumentType(""), fmt.Errorf("failed to parse contract notice: %w", err)
}
parsedDoc.ContractNotice = notice
return parsedDoc, notice.GetID(), parsedDoc.Type, nil
case DocumentTypeContractAwardNotice:
notice, err := p.ContractAwardNotice.ParseBytes(content)
if err != nil {
return nil, "", DocumentType(""), fmt.Errorf("failed to parse contract award notice: %w", err)
}
parsedDoc.ContractAwardNotice = notice
return parsedDoc, notice.GetID(), parsedDoc.Type, nil
case DocumentTypePriorInformationNotice:
notice, err := p.PriorInformationNotice.ParseBytes(content)
if err != nil {
return nil, "", DocumentType(""), fmt.Errorf("failed to parse prior information notice: %w", err)
}
parsedDoc.PriorInformationNotice = notice
return parsedDoc, notice.GetID(), parsedDoc.Type, nil
default:
return nil, "", ParserType(""), fmt.Errorf("unsupported notice type: %s", rootElement)
return nil, "", DocumentType(""), fmt.Errorf("unsupported document type: %s", parsedDoc.Type)
}
}
+73 -3
View File
@@ -275,6 +275,18 @@ func (s *TEDScraper) tarGzFileProcessor(ctx context.Context, reader io.Reader, o
return result, nil
}
func (s *TEDScraper) findTenderByContractNoticeID(ctx context.Context, contractNoticeID string) (*tender.Tender, error) {
existingTender, err := s.tenderRepo.GetByContractNoticeID(ctx, contractNoticeID)
if err != nil {
if err.Error() == "document not found" {
return nil, nil
}
return nil, err
}
return existingTender, nil
}
func (s *TEDScraper) processXMLContent(ctx context.Context, content []byte, fileName string, result *FileProcessingResult) error {
s.logger.Debug("Processing XML content", map[string]interface{}{
"file_name": fileName,
@@ -283,7 +295,7 @@ func (s *TEDScraper) processXMLContent(ctx context.Context, content []byte, file
})
// Parse XML using the flexible TED parser that detects notice type
notice, id, noticeType, err := s.xmlParser.Parse(content)
notice, id, noticeType, err := s.xmlParser.ParseXML(content)
if err != nil {
s.logger.Error("XML parsing failed", map[string]interface{}{
"file_name": fileName,
@@ -293,13 +305,71 @@ func (s *TEDScraper) processXMLContent(ctx context.Context, content []byte, file
return fmt.Errorf("failed to parse XML: %w", err)
}
_ = notice
// Map to tender entity
t := s.mapToTenderFromParsedDoc(notice, "", fileName)
existingTender, err := s.findTenderByContractNoticeID(ctx, t.ContractNoticeID)
if err != nil {
s.logger.Warn("Error checking for existing tender", map[string]interface{}{
"contract_notice_id": t.ContractNoticeID,
"error": err.Error(),
})
}
if err == nil && existingTender != nil {
// Update existing tender
s.logger.Info("Found existing tender, updating", map[string]interface{}{
"existing_tender_id": existingTender.ID.Hex(),
"contract_notice_id": t.ContractNoticeID,
})
t.ID = existingTender.ID
t.UpdatedAt = time.Now().Unix()
err = s.tenderRepo.Update(ctx, t)
if err != nil {
s.logger.Error("Failed to update existing tender", map[string]interface{}{
"tender_id": t.ID.Hex(),
"error": err.Error(),
})
return fmt.Errorf("failed to update existing tender: %w", err)
}
s.logger.Info("Successfully updated tender", map[string]interface{}{
"tender_id": t.ID.Hex(),
})
} else {
// Create new tender
s.logger.Info("Creating new tender", map[string]interface{}{
"contract_notice_id": t.ContractNoticeID,
"tender_id": t.TenderID,
})
t.CreatedAt = time.Now().Unix()
t.UpdatedAt = t.CreatedAt
err = s.tenderRepo.Create(ctx, t)
if err != nil {
s.logger.Error("Failed to create new tender", map[string]interface{}{
"contract_notice_id": t.ContractNoticeID,
"tender_id": t.TenderID,
"error": err.Error(),
})
return fmt.Errorf("failed to create tender: %w", err)
}
s.logger.Info("Successfully created new tender", map[string]interface{}{
"tender_id": t.TenderID,
"contract_notice_id": t.ContractNoticeID,
})
}
// Log detailed parsed information based on notice type
s.logger.Info("Successfully parsed notice", map[string]interface{}{
"notice_id": id,
"notice_type": noticeType,
"file_name": fileName,
"tender_id": t.TenderID,
"tender_url": t.TenderURL,
})
result.SuccessCount++
@@ -318,7 +388,7 @@ func (s *TEDScraper) Run(ctx context.Context) error {
s.logger.Error("Failed to get OJS", map[string]interface{}{
"error": err.Error(),
})
return err
return nil
}
// download daily package
+19 -40
View File
@@ -6,31 +6,10 @@ import (
"strings"
"time"
"tm/internal/tender"
"tm/ted/eform/business_registration_information"
"tm/ted/eform/contract"
"tm/ted/eform/contract_award"
"tm/ted/eform/prior_information"
)
func EFormMapper(notice interface{}) *tender.Tender {
t := new(tender.Tender)
switch notice := notice.(type) {
case contract.ContractNotice:
return notice.MapToTender()
case contract_award.ContractAwardNotice:
return notice.MapToTender()
case prior_information.PriorInformationNotice:
return notice.MapToTender()
case business_registration_information.BusinessRegistrationInformationNotice:
return notice.MapToTender()
default:
return t
}
}
// parseDate parses various date formats used in TED XML
func parseDate(dateStr string) (time.Time, error) {
func ParseDate(dateStr string) (time.Time, error) {
if dateStr == "" {
return time.Time{}, fmt.Errorf("empty date string")
}
@@ -58,20 +37,20 @@ func parseDate(dateStr string) (time.Time, error) {
}
// parseDateToUnixMilli converts string date to Unix milliseconds (int64)
func parseDateToUnixMilli(dateStr string) int64 {
func ParseDateToUnix(dateStr string) int64 {
if dateStr == "" {
return 0
}
if parsedTime, err := parseDate(dateStr); err == nil {
return parsedTime.UnixMilli()
if parsedTime, err := ParseDate(dateStr); err == nil {
return parsedTime.Unix()
}
return 0
}
// parseTimeToUnixMilli converts string time to Unix milliseconds (int64)
func parseTimeToUnixMilli(timeStr string) int64 {
// parseTimeToUnix converts string time to Unix milliseconds (int64)
func ParseTimeToUnix(timeStr string) int64 {
if timeStr == "" {
return 0
}
@@ -83,23 +62,23 @@ func parseTimeToUnixMilli(timeStr string) int64 {
timeStr = fmt.Sprintf("%sT%s", today, timeStr)
}
if parsedTime, err := parseDate(timeStr); err == nil {
return parsedTime.UnixMilli()
if parsedTime, err := ParseDate(timeStr); err == nil {
return parsedTime.Unix()
}
return 0
}
// unixMilliToDateString converts Unix milliseconds to date string for TenderID generation
func unixMilliToDateString(unixMilli int64) string {
if unixMilli == 0 {
// unixToDateString converts Unix to date string for TenderID generation
func UnixToDateString(unix int64) string {
if unix == 0 {
return "00000000"
}
return time.UnixMilli(unixMilli).Format("20060102")
return time.Unix(unix, 0).Format("20060102")
}
func calculateSubmissionDeadline(publicationDate time.Time) time.Time {
func CalculateSubmissionDeadline(publicationDate time.Time) time.Time {
deadline := publicationDate
workingHoursAdded := 0
@@ -114,7 +93,7 @@ func calculateSubmissionDeadline(publicationDate time.Time) time.Time {
return deadline
}
func calculateApplicationDeadline(tenderDeadline time.Time) time.Time {
func CalculateApplicationDeadline(tenderDeadline time.Time) time.Time {
applicationDeadline := tenderDeadline
workingDaysSubtracted := 0
@@ -130,7 +109,7 @@ func calculateApplicationDeadline(tenderDeadline time.Time) time.Time {
}
// padOrTruncate pads a string with zeros or truncates it to the specified length
func padOrTruncate(str string, length int) string {
func 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, " ", "")
@@ -144,7 +123,7 @@ func padOrTruncate(str string, length int) string {
}
// generateTenderURL generates the TED tender URL from NoticePublicationID
func generateTenderURL(noticePublicationID string) string {
func GenerateTenderURL(noticePublicationID string) string {
if noticePublicationID == "" {
return ""
}
@@ -167,7 +146,7 @@ func generateTenderURL(noticePublicationID string) string {
// generateTenderID generates a unique tender ID using the format:
// {Source}{BUYER_CODE}{TED_CODE}{DATE}{LOCATION_CODE}
func generateTenderID(t *tender.Tender) string {
func GenerateTenderID(t *tender.Tender) string {
var parts []string
// Source (TED)
@@ -176,7 +155,7 @@ func generateTenderID(t *tender.Tender) string {
// TED code (Notice Publication ID, first 8 chars)
tedCode := "00000000"
if t.NoticePublicationID != "" {
tedCode = padOrTruncate(t.NoticePublicationID, 8)
tedCode = PadOrTruncate(t.NoticePublicationID, 8)
}
parts = append(parts, tedCode)
@@ -188,7 +167,7 @@ func generateTenderID(t *tender.Tender) string {
// parts = append(parts, buyerCode)
// Date (YYYYMMDD format from issue date)
parts = append(parts, padOrTruncate(unixMilliToDateString(t.IssueDate), 4))
parts = append(parts, PadOrTruncate(UnixToDateString(t.IssueDate), 4))
// Location code (country code + region code, padded to 6 chars)
locationCode := "EU"