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 {
|
func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) TenderRepository {
|
||||||
// Create indexes for tenders collection
|
// Create indexes for tenders collection
|
||||||
tenderIndexes := []orm.Index{
|
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("contract_notice_id_idx", bson.D{{Key: "contract_notice_id", Value: 1}}),
|
||||||
*orm.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
*orm.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}),
|
||||||
*orm.NewIndex("source_idx", bson.D{{Key: "source", 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
+42
-43
@@ -5,26 +5,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"tm/pkg/xmlparser"
|
"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 {
|
type TEDParser struct {
|
||||||
ContractNotice xmlparser.Parser[contract.ContractNotice]
|
ContractNotice xmlparser.Parser[ContractNotice]
|
||||||
ContractAwardNotice xmlparser.Parser[contract_award.ContractAwardNotice]
|
ContractAwardNotice xmlparser.Parser[ContractAwardNotice]
|
||||||
PriorInformationNotice xmlparser.Parser[prior_information.PriorInformationNotice]
|
PriorInformationNotice xmlparser.Parser[PriorInformationNotice]
|
||||||
BusinessRegistrationInformationNotice xmlparser.Parser[business_registration_information.BusinessRegistrationInformationNotice]
|
// BusinessRegistrationInformationNotice xmlparser.Parser[BusinessRegistrationInformationNotice]
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTEDParser creates a new TED parser
|
// 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.MaxSize = 50 * 1024 * 1024 // 50MB limit for TED XML files
|
||||||
options.IgnoreUnknownElements = true // TED XML has many optional fields
|
options.IgnoreUnknownElements = true // TED XML has many optional fields
|
||||||
|
|
||||||
contractNoticeParser := xmlparser.NewParser[contract.ContractNotice](options)
|
contractNoticeParser := xmlparser.NewParser[ContractNotice](options)
|
||||||
contractAwardNoticeParser := xmlparser.NewParser[contract_award.ContractAwardNotice](options)
|
contractAwardNoticeParser := xmlparser.NewParser[ContractAwardNotice](options)
|
||||||
priorInformationNoticeParser := xmlparser.NewParser[prior_information.PriorInformationNotice](options)
|
priorInformationNoticeParser := xmlparser.NewParser[PriorInformationNotice](options)
|
||||||
businessRegistrationInformationNoticeParser := xmlparser.NewParser[business_registration_information.BusinessRegistrationInformationNotice](options)
|
// businessRegistrationInformationNoticeParser := xmlparser.NewParser[BusinessRegistrationInformationNotice](options)
|
||||||
|
|
||||||
return &TEDParser{
|
return &TEDParser{
|
||||||
ContractNotice: contractNoticeParser,
|
ContractNotice: contractNoticeParser,
|
||||||
ContractAwardNotice: contractAwardNoticeParser,
|
ContractAwardNotice: contractAwardNoticeParser,
|
||||||
PriorInformationNotice: priorInformationNoticeParser,
|
PriorInformationNotice: priorInformationNoticeParser,
|
||||||
BusinessRegistrationInformationNotice: businessRegistrationInformationNoticeParser,
|
// BusinessRegistrationInformationNotice: businessRegistrationInformationNoticeParser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseAnyNotice parses XML content and detects the notice type automatically
|
// ParseXML automatically detects and parses any supported TED XML document type
|
||||||
func (tp *TEDParser) Parse(content []byte) (interface{}, string, ParserType, error) {
|
func (p *TEDParser) ParseXML(content []byte) (*ParsedDocument, string, DocumentType, error) {
|
||||||
// First, detect the root element type
|
// First, detect the root element type
|
||||||
rootElement, err := detectRootElement(content)
|
rootElement, err := detectRootElement(content)
|
||||||
if err != nil {
|
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
|
parsedDoc := &ParsedDocument{Type: DocumentType(rootElement)}
|
||||||
switch ParserType(rootElement) {
|
|
||||||
case ParserTypeContractNotice:
|
switch parsedDoc.Type {
|
||||||
notice, err := tp.ContractNotice.ParseBytes(content)
|
case DocumentTypeContractNotice:
|
||||||
return notice, notice.GetID(), ParserTypeContractNotice, err
|
notice, err := p.ContractNotice.ParseBytes(content)
|
||||||
case ParserTypeContractAwardNotice:
|
if err != nil {
|
||||||
notice, err := tp.ContractAwardNotice.ParseBytes(content)
|
return nil, "", DocumentType(""), fmt.Errorf("failed to parse contract notice: %w", err)
|
||||||
return notice, notice.GetID(), ParserTypeContractAwardNotice, err
|
}
|
||||||
case ParserTypePriorInformationNotice:
|
parsedDoc.ContractNotice = notice
|
||||||
notice, err := tp.PriorInformationNotice.ParseBytes(content)
|
return parsedDoc, notice.GetID(), parsedDoc.Type, nil
|
||||||
return notice, notice.GetID(), ParserTypePriorInformationNotice, err
|
|
||||||
case ParserTypeBusinessRegistrationInformationNotice:
|
case DocumentTypeContractAwardNotice:
|
||||||
notice, err := tp.BusinessRegistrationInformationNotice.ParseBytes(content)
|
notice, err := p.ContractAwardNotice.ParseBytes(content)
|
||||||
return notice, notice.GetID(), ParserTypeBusinessRegistrationInformationNotice, err
|
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:
|
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
|
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 {
|
func (s *TEDScraper) processXMLContent(ctx context.Context, content []byte, fileName string, result *FileProcessingResult) error {
|
||||||
s.logger.Debug("Processing XML content", map[string]interface{}{
|
s.logger.Debug("Processing XML content", map[string]interface{}{
|
||||||
"file_name": fileName,
|
"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
|
// 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 {
|
if err != nil {
|
||||||
s.logger.Error("XML parsing failed", map[string]interface{}{
|
s.logger.Error("XML parsing failed", map[string]interface{}{
|
||||||
"file_name": fileName,
|
"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)
|
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
|
// Log detailed parsed information based on notice type
|
||||||
s.logger.Info("Successfully parsed notice", map[string]interface{}{
|
s.logger.Info("Successfully parsed notice", map[string]interface{}{
|
||||||
"notice_id": id,
|
"notice_id": id,
|
||||||
"notice_type": noticeType,
|
"notice_type": noticeType,
|
||||||
"file_name": fileName,
|
"file_name": fileName,
|
||||||
|
"tender_id": t.TenderID,
|
||||||
|
"tender_url": t.TenderURL,
|
||||||
})
|
})
|
||||||
|
|
||||||
result.SuccessCount++
|
result.SuccessCount++
|
||||||
@@ -318,7 +388,7 @@ func (s *TEDScraper) Run(ctx context.Context) error {
|
|||||||
s.logger.Error("Failed to get OJS", map[string]interface{}{
|
s.logger.Error("Failed to get OJS", map[string]interface{}{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// download daily package
|
// download daily package
|
||||||
|
|||||||
+19
-40
@@ -6,31 +6,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"tm/internal/tender"
|
"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
|
// 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 == "" {
|
if dateStr == "" {
|
||||||
return time.Time{}, fmt.Errorf("empty date string")
|
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)
|
// parseDateToUnixMilli converts string date to Unix milliseconds (int64)
|
||||||
func parseDateToUnixMilli(dateStr string) int64 {
|
func ParseDateToUnix(dateStr string) int64 {
|
||||||
if dateStr == "" {
|
if dateStr == "" {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedTime, err := parseDate(dateStr); err == nil {
|
if parsedTime, err := ParseDate(dateStr); err == nil {
|
||||||
return parsedTime.UnixMilli()
|
return parsedTime.Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTimeToUnixMilli converts string time to Unix milliseconds (int64)
|
// parseTimeToUnix converts string time to Unix milliseconds (int64)
|
||||||
func parseTimeToUnixMilli(timeStr string) int64 {
|
func ParseTimeToUnix(timeStr string) int64 {
|
||||||
if timeStr == "" {
|
if timeStr == "" {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -83,23 +62,23 @@ func parseTimeToUnixMilli(timeStr string) int64 {
|
|||||||
timeStr = fmt.Sprintf("%sT%s", today, timeStr)
|
timeStr = fmt.Sprintf("%sT%s", today, timeStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedTime, err := parseDate(timeStr); err == nil {
|
if parsedTime, err := ParseDate(timeStr); err == nil {
|
||||||
return parsedTime.UnixMilli()
|
return parsedTime.Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// unixMilliToDateString converts Unix milliseconds to date string for TenderID generation
|
// unixToDateString converts Unix to date string for TenderID generation
|
||||||
func unixMilliToDateString(unixMilli int64) string {
|
func UnixToDateString(unix int64) string {
|
||||||
if unixMilli == 0 {
|
if unix == 0 {
|
||||||
return "00000000"
|
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
|
deadline := publicationDate
|
||||||
workingHoursAdded := 0
|
workingHoursAdded := 0
|
||||||
|
|
||||||
@@ -114,7 +93,7 @@ func calculateSubmissionDeadline(publicationDate time.Time) time.Time {
|
|||||||
return deadline
|
return deadline
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateApplicationDeadline(tenderDeadline time.Time) time.Time {
|
func CalculateApplicationDeadline(tenderDeadline time.Time) time.Time {
|
||||||
applicationDeadline := tenderDeadline
|
applicationDeadline := tenderDeadline
|
||||||
workingDaysSubtracted := 0
|
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
|
// 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
|
// Remove any non-alphanumeric characters and convert to uppercase
|
||||||
clean := strings.ToUpper(strings.ReplaceAll(str, "-", ""))
|
clean := strings.ToUpper(strings.ReplaceAll(str, "-", ""))
|
||||||
clean = strings.ReplaceAll(clean, " ", "")
|
clean = strings.ReplaceAll(clean, " ", "")
|
||||||
@@ -144,7 +123,7 @@ func padOrTruncate(str string, length int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generateTenderURL generates the TED tender URL from NoticePublicationID
|
// generateTenderURL generates the TED tender URL from NoticePublicationID
|
||||||
func generateTenderURL(noticePublicationID string) string {
|
func GenerateTenderURL(noticePublicationID string) string {
|
||||||
if noticePublicationID == "" {
|
if noticePublicationID == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -167,7 +146,7 @@ func generateTenderURL(noticePublicationID string) string {
|
|||||||
|
|
||||||
// generateTenderID generates a unique tender ID using the format:
|
// generateTenderID generates a unique tender ID using the format:
|
||||||
// {Source}{BUYER_CODE}{TED_CODE}{DATE}{LOCATION_CODE}
|
// {Source}{BUYER_CODE}{TED_CODE}{DATE}{LOCATION_CODE}
|
||||||
func generateTenderID(t *tender.Tender) string {
|
func GenerateTenderID(t *tender.Tender) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
|
|
||||||
// Source (TED)
|
// Source (TED)
|
||||||
@@ -176,7 +155,7 @@ func generateTenderID(t *tender.Tender) string {
|
|||||||
// TED code (Notice Publication ID, first 8 chars)
|
// TED code (Notice Publication ID, first 8 chars)
|
||||||
tedCode := "00000000"
|
tedCode := "00000000"
|
||||||
if t.NoticePublicationID != "" {
|
if t.NoticePublicationID != "" {
|
||||||
tedCode = padOrTruncate(t.NoticePublicationID, 8)
|
tedCode = PadOrTruncate(t.NoticePublicationID, 8)
|
||||||
}
|
}
|
||||||
parts = append(parts, tedCode)
|
parts = append(parts, tedCode)
|
||||||
|
|
||||||
@@ -188,7 +167,7 @@ func generateTenderID(t *tender.Tender) string {
|
|||||||
// parts = append(parts, buyerCode)
|
// parts = append(parts, buyerCode)
|
||||||
|
|
||||||
// Date (YYYYMMDD format from issue date)
|
// 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)
|
// Location code (country code + region code, padded to 6 chars)
|
||||||
locationCode := "EU"
|
locationCode := "EU"
|
||||||
|
|||||||
Reference in New Issue
Block a user