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:
@@ -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}}),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+39
-40
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user