added missing tender data to response
This commit is contained in:
@@ -450,6 +450,7 @@ func (w *NoticeWorker) ToTender(n *notice.Notice) (*tender.Tender, error) {
|
|||||||
Description: l.Description,
|
Description: l.Description,
|
||||||
MainNatureOfContract: l.MainNatureOfContract,
|
MainNatureOfContract: l.MainNatureOfContract,
|
||||||
MainClassification: l.MainClassification,
|
MainClassification: l.MainClassification,
|
||||||
|
MainClassificationDescription: l.MainClassificationDescription,
|
||||||
AdditionalClassifications: append([]string(nil), l.AdditionalClassifications...),
|
AdditionalClassifications: append([]string(nil), l.AdditionalClassifications...),
|
||||||
Duration: l.Duration,
|
Duration: l.Duration,
|
||||||
EstimatedValue: l.EstimatedValue,
|
EstimatedValue: l.EstimatedValue,
|
||||||
@@ -528,6 +529,7 @@ func (w *NoticeWorker) ToTender(n *notice.Notice) (*tender.Tender, error) {
|
|||||||
t.BuyerProfileURL = n.BuyerProfileURL
|
t.BuyerProfileURL = n.BuyerProfileURL
|
||||||
t.TenderURL = n.TenderURL
|
t.TenderURL = n.TenderURL
|
||||||
t.MainClassification = n.MainClassification
|
t.MainClassification = n.MainClassification
|
||||||
|
t.MainClassificationDescription = n.MainClassificationDescription
|
||||||
t.AdditionalClassifications = n.AdditionalClassifications
|
t.AdditionalClassifications = n.AdditionalClassifications
|
||||||
t.OfficialLanguages = n.OfficialLanguages
|
t.OfficialLanguages = n.OfficialLanguages
|
||||||
t.Status = chooseTenderStatus(previousStatus, tender.TenderStatus(n.Status))
|
t.Status = chooseTenderStatus(previousStatus, tender.TenderStatus(n.Status))
|
||||||
|
|||||||
@@ -11,6 +11,46 @@ type DocumentScraperGetRequest struct {
|
|||||||
NoticeID string `param:"notice_id" valid:"required~Notice ID is required"`
|
NoticeID string `param:"notice_id" valid:"required~Notice ID is required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DocumentScraperBuyerResponse is buyer/contact and location data exposed to the document scraper.
|
||||||
|
type DocumentScraperBuyerResponse struct {
|
||||||
|
BuyerName string `json:"buyer_name"`
|
||||||
|
BuyerEmail string `json:"buyer_email"`
|
||||||
|
BuyerCountry string `json:"buyer_country"`
|
||||||
|
BuyerRegionNuts string `json:"buyer_region_nuts"`
|
||||||
|
BuyerCity string `json:"buyer_city"`
|
||||||
|
BuyerProfileURL string `json:"buyer_profile_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentScraperLotResponse is one procurement lot for the scraper task payload.
|
||||||
|
type DocumentScraperLotResponse struct {
|
||||||
|
LotID string `json:"lot_id,omitempty"`
|
||||||
|
LotTitle string `json:"lot_title"`
|
||||||
|
LotDescription string `json:"lot_description"`
|
||||||
|
LotMainNature string `json:"lot_main_nature"`
|
||||||
|
LotMainClassificationCode string `json:"lot_main_classification_code"`
|
||||||
|
LotMainClassificationDescription string `json:"lot_main_classification_description"`
|
||||||
|
LotMainClassificationDisplay string `json:"lot_main_classification_display,omitempty"`
|
||||||
|
AdditionalClassifications []string `json:"additional_classifications,omitempty"`
|
||||||
|
LotDuration string `json:"lot_duration"`
|
||||||
|
LotEstimatedValueExclVAT float64 `json:"lot_estimated_value_excl_vat"`
|
||||||
|
LotEstimatedValueCurrency string `json:"lot_estimated_value_currency,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DocumentScraperWinnerResponse is a contract winner / winning tenderer for result notices.
|
||||||
|
type DocumentScraperWinnerResponse struct {
|
||||||
|
OfficialName string `json:"official_name"`
|
||||||
|
RegistrationNumber string `json:"registration_number"`
|
||||||
|
PostalAddress string `json:"postal_address"`
|
||||||
|
Town string `json:"town"`
|
||||||
|
Postcode string `json:"postcode"`
|
||||||
|
CountrySubdivisionNuts string `json:"country_subdivision_nuts"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
ContactPoint string `json:"contact_point"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Telephone string `json:"telephone"`
|
||||||
|
LotID string `json:"lot_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// DocumentScraperTenderResponse represents the response for a tender returned to the document scraper service.
|
// DocumentScraperTenderResponse represents the response for a tender returned to the document scraper service.
|
||||||
//
|
//
|
||||||
// ContractFolderID is the TED procedure identifier (BT-04 / ContractFolderID),
|
// ContractFolderID is the TED procedure identifier (BT-04 / ContractFolderID),
|
||||||
@@ -23,6 +63,25 @@ type DocumentScraperTenderResponse struct {
|
|||||||
DocumentURL string `json:"document_url"`
|
DocumentURL string `json:"document_url"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
NoticeIdentifier string `json:"notice_identifier"`
|
||||||
|
NoticeVersion string `json:"notice_version"`
|
||||||
|
FormType string `json:"form_type"`
|
||||||
|
NoticeType string `json:"notice_type"`
|
||||||
|
NoticeSubtype string `json:"notice_subtype"`
|
||||||
|
PublicationDate int64 `json:"publication_date"`
|
||||||
|
NoticeDispatchDateWithTimezone string `json:"notice_dispatch_date_with_timezone"`
|
||||||
|
ProcurementTypeCode string `json:"procurement_type_code"`
|
||||||
|
MainClassification string `json:"main_classification"`
|
||||||
|
MainClassificationDescription string `json:"main_classification_description"`
|
||||||
|
MainClassificationDisplay string `json:"main_classification_display,omitempty"`
|
||||||
|
AdditionalClassifications []string `json:"additional_classifications,omitempty"`
|
||||||
|
EstimatedValue float64 `json:"estimated_value"`
|
||||||
|
EstimatedValueCurrency string `json:"estimated_value_currency,omitempty"`
|
||||||
|
EstimatedValueVATBasis string `json:"estimated_value_vat_basis"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Buyer DocumentScraperBuyerResponse `json:"buyer"`
|
||||||
|
Lots []DocumentScraperLotResponse `json:"lots"`
|
||||||
|
Winners []DocumentScraperWinnerResponse `json:"winners"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DocumentScraperListResponse represents the paginated list response
|
// DocumentScraperListResponse represents the paginated list response
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func NewHandler(service Service, logger logger.Logger) *Handler {
|
|||||||
|
|
||||||
// ListPendingTenders retrieves tenders that need document scraping
|
// ListPendingTenders retrieves tenders that need document scraping
|
||||||
// @Summary List pending tenders for document scraping
|
// @Summary List pending tenders for document scraping
|
||||||
// @Description Retrieve tenders that have not yet been scraped for documents, including notice ID, title, description
|
// @Description Retrieve tenders that have not yet been scraped for documents. Each item includes notice identifiers, dispatch time (RFC3339), procurement lots, buyer, winners (when present), CPV fields, and value metadata.
|
||||||
// @Tags Document-Scraper
|
// @Tags Document-Scraper
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param limit query int false "Number of items per page (default: 20, max: 100)"
|
// @Param limit query int false "Number of items per page (default: 20, max: 100)"
|
||||||
@@ -62,7 +62,7 @@ func (h *Handler) ListPendingTenders(c echo.Context) error {
|
|||||||
|
|
||||||
// GetTenderByNoticeID retrieves a specific tender by its notice publication ID
|
// GetTenderByNoticeID retrieves a specific tender by its notice publication ID
|
||||||
// @Summary Get tender by notice ID for document scraping
|
// @Summary Get tender by notice ID for document scraping
|
||||||
// @Description Retrieve a specific tender by its notice publication ID, including notice ID, title, description
|
// @Description Retrieve a specific tender by its notice publication ID with full notice, lot, buyer, and winner fields for the document scraper pipeline.
|
||||||
// @Tags Document-Scraper
|
// @Tags Document-Scraper
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param notice_id path string true "Notice Publication ID"
|
// @Param notice_id path string true "Notice Publication ID"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"tm/internal/tender"
|
"tm/internal/tender"
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
"tm/ted"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Service defines business logic for the document scraper API
|
// Service defines business logic for the document scraper API
|
||||||
@@ -20,7 +19,6 @@ type Service interface {
|
|||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
tenderRepo tender.TenderRepository
|
tenderRepo tender.TenderRepository
|
||||||
tedParser *ted.TEDParser
|
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,27 +26,10 @@ type service struct {
|
|||||||
func NewService(tenderRepo tender.TenderRepository, logger logger.Logger) Service {
|
func NewService(tenderRepo tender.TenderRepository, logger logger.Logger) Service {
|
||||||
return &service{
|
return &service{
|
||||||
tenderRepo: tenderRepo,
|
tenderRepo: tenderRepo,
|
||||||
tedParser: ted.NewTEDParser(),
|
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildTenderResponse creates a DocumentScraperTenderResponse from a tender
|
|
||||||
func (s *service) buildTenderResponse(_ context.Context, t *tender.Tender) *DocumentScraperTenderResponse {
|
|
||||||
documentURL := t.DocumentURI
|
|
||||||
if documentURL == "" {
|
|
||||||
documentURL = t.TenderURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return &DocumentScraperTenderResponse{
|
|
||||||
ContractFolderID: t.ContractFolderID,
|
|
||||||
NoticePublicationID: t.NoticePublicationID,
|
|
||||||
DocumentURL: documentURL,
|
|
||||||
Title: t.Title,
|
|
||||||
Description: t.Description,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPendingTenders retrieves tenders that have not yet been scraped for documents.
|
// ListPendingTenders retrieves tenders that have not yet been scraped for documents.
|
||||||
// Only returns tenders from Sweden with active deadlines (deadline not yet reached).
|
// Only returns tenders from Sweden with active deadlines (deadline not yet reached).
|
||||||
func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*DocumentScraperListResponse, *response.Meta, error) {
|
func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*DocumentScraperListResponse, *response.Meta, error) {
|
||||||
@@ -107,7 +88,7 @@ func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*D
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range filteredTenders {
|
for i := range filteredTenders {
|
||||||
result.Tenders[i] = *s.buildTenderResponse(ctx, &filteredTenders[i])
|
result.Tenders[i] = *BuildDocumentScraperTenderResponse(&filteredTenders[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build pagination metadata
|
// Build pagination metadata
|
||||||
@@ -160,7 +141,7 @@ func (s *service) GetTenderByNoticeID(ctx context.Context, noticeID string) (*Do
|
|||||||
return nil, fmt.Errorf("tender not found")
|
return nil, fmt.Errorf("tender not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
result := s.buildTenderResponse(ctx, t)
|
result := BuildDocumentScraperTenderResponse(t)
|
||||||
|
|
||||||
s.logger.Info("Tender retrieved successfully for document scraper", map[string]interface{}{
|
s.logger.Info("Tender retrieved successfully for document scraper", map[string]interface{}{
|
||||||
"notice_id": noticeID,
|
"notice_id": noticeID,
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package document_scraper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tm/internal/tender"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
enrichmentMainCPVDescription = "main_classification_description"
|
||||||
|
vatBasisUnspecified = "unspecified"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildDocumentScraperTenderResponse maps a stored tender to the scraper API contract.
|
||||||
|
func BuildDocumentScraperTenderResponse(t *tender.Tender) *DocumentScraperTenderResponse {
|
||||||
|
if t == nil {
|
||||||
|
return &DocumentScraperTenderResponse{
|
||||||
|
Lots: []DocumentScraperLotResponse{},
|
||||||
|
Winners: []DocumentScraperWinnerResponse{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
documentURL := strings.TrimSpace(t.DocumentURI)
|
||||||
|
if documentURL == "" {
|
||||||
|
documentURL = strings.TrimSpace(t.TenderURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
mainDesc := firstNonEmpty(
|
||||||
|
strings.TrimSpace(t.ProcessingMetadata.EnrichmentData[enrichmentMainCPVDescription]),
|
||||||
|
strings.TrimSpace(t.MainClassificationDescription),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp := &DocumentScraperTenderResponse{
|
||||||
|
ContractFolderID: t.ContractFolderID,
|
||||||
|
NoticePublicationID: t.NoticePublicationID,
|
||||||
|
DocumentURL: documentURL,
|
||||||
|
Title: t.Title,
|
||||||
|
Description: t.Description,
|
||||||
|
NoticeIdentifier: strings.TrimSpace(t.NoticeIdentifier),
|
||||||
|
NoticeVersion: strings.TrimSpace(t.NoticeVersion),
|
||||||
|
FormType: strings.TrimSpace(t.FormType),
|
||||||
|
NoticeType: strings.TrimSpace(t.NoticeTypeCode),
|
||||||
|
NoticeSubtype: strings.TrimSpace(t.NoticeSubTypeCode),
|
||||||
|
PublicationDate: t.PublicationDate,
|
||||||
|
NoticeDispatchDateWithTimezone: formatNoticeDispatchRFC3339(t),
|
||||||
|
ProcurementTypeCode: strings.TrimSpace(t.ProcurementTypeCode),
|
||||||
|
MainClassification: strings.TrimSpace(t.MainClassification),
|
||||||
|
MainClassificationDescription: mainDesc,
|
||||||
|
MainClassificationDisplay: classificationDisplay(strings.TrimSpace(t.MainClassification), mainDesc),
|
||||||
|
AdditionalClassifications: append([]string(nil), t.AdditionalClassifications...),
|
||||||
|
EstimatedValue: t.EstimatedValue,
|
||||||
|
EstimatedValueCurrency: strings.TrimSpace(t.Currency),
|
||||||
|
EstimatedValueVATBasis: vatBasisUnspecified,
|
||||||
|
Status: string(t.Status),
|
||||||
|
Buyer: buildBuyer(t),
|
||||||
|
Lots: buildLots(t, mainDesc),
|
||||||
|
Winners: buildWinners(t),
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatNoticeDispatchRFC3339(t *tender.Tender) string {
|
||||||
|
sec := t.NoticeDispatchAt
|
||||||
|
if sec == 0 {
|
||||||
|
sec = t.ESenderDispatchAt
|
||||||
|
}
|
||||||
|
if sec == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return time.Unix(sec, 0).UTC().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBuyer(t *tender.Tender) DocumentScraperBuyerResponse {
|
||||||
|
if t.BuyerOrganization == nil {
|
||||||
|
return DocumentScraperBuyerResponse{}
|
||||||
|
}
|
||||||
|
b := t.BuyerOrganization
|
||||||
|
return DocumentScraperBuyerResponse{
|
||||||
|
BuyerName: strings.TrimSpace(b.Name),
|
||||||
|
BuyerEmail: strings.TrimSpace(b.ContactEmail),
|
||||||
|
BuyerCountry: strings.TrimSpace(b.Address.CountryCode),
|
||||||
|
BuyerRegionNuts: strings.TrimSpace(b.Address.CountrySubentityCode),
|
||||||
|
BuyerCity: strings.TrimSpace(b.Address.CityName),
|
||||||
|
BuyerProfileURL: strings.TrimSpace(firstNonEmpty(t.BuyerProfileURL, b.WebsiteURI)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonEmpty(values ...string) string {
|
||||||
|
for _, v := range values {
|
||||||
|
if s := strings.TrimSpace(v); s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func classificationDisplay(code, description string) string {
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
description = strings.TrimSpace(description)
|
||||||
|
if code != "" && description != "" {
|
||||||
|
return code + " - " + description
|
||||||
|
}
|
||||||
|
if code != "" {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLots(t *tender.Tender, globalMainDesc string) []DocumentScraperLotResponse {
|
||||||
|
if len(t.ProcurementLots) == 0 {
|
||||||
|
return []DocumentScraperLotResponse{}
|
||||||
|
}
|
||||||
|
out := make([]DocumentScraperLotResponse, 0, len(t.ProcurementLots))
|
||||||
|
for _, lot := range t.ProcurementLots {
|
||||||
|
lotCode := strings.TrimSpace(lot.MainClassification)
|
||||||
|
perLotDesc := strings.TrimSpace(lot.MainClassificationDescription)
|
||||||
|
if perLotDesc == "" && lotCode != "" && lotCode == strings.TrimSpace(t.MainClassification) {
|
||||||
|
perLotDesc = globalMainDesc
|
||||||
|
}
|
||||||
|
out = append(out, DocumentScraperLotResponse{
|
||||||
|
LotID: strings.TrimSpace(lot.LotID),
|
||||||
|
LotTitle: strings.TrimSpace(lot.Title),
|
||||||
|
LotDescription: strings.TrimSpace(lot.Description),
|
||||||
|
LotMainNature: strings.TrimSpace(lot.MainNatureOfContract),
|
||||||
|
LotMainClassificationCode: lotCode,
|
||||||
|
LotMainClassificationDescription: perLotDesc,
|
||||||
|
LotMainClassificationDisplay: classificationDisplay(lotCode, perLotDesc),
|
||||||
|
AdditionalClassifications: append([]string(nil), lot.AdditionalClassifications...),
|
||||||
|
LotDuration: strings.TrimSpace(lot.Duration),
|
||||||
|
LotEstimatedValueExclVAT: lot.EstimatedValue,
|
||||||
|
LotEstimatedValueCurrency: strings.TrimSpace(lot.Currency),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWinners(t *tender.Tender) []DocumentScraperWinnerResponse {
|
||||||
|
if len(t.AwardedEntities) > 0 {
|
||||||
|
out := make([]DocumentScraperWinnerResponse, 0, len(t.AwardedEntities))
|
||||||
|
for _, a := range t.AwardedEntities {
|
||||||
|
w := DocumentScraperWinnerResponse{
|
||||||
|
OfficialName: strings.TrimSpace(a.Name),
|
||||||
|
RegistrationNumber: strings.TrimSpace(a.CompanyID),
|
||||||
|
PostalAddress: strings.TrimSpace(a.Address),
|
||||||
|
Country: strings.TrimSpace(a.Country),
|
||||||
|
LotID: strings.TrimSpace(a.LotID),
|
||||||
|
Town: "",
|
||||||
|
Postcode: "",
|
||||||
|
CountrySubdivisionNuts: "",
|
||||||
|
ContactPoint: "",
|
||||||
|
Email: "",
|
||||||
|
Telephone: "",
|
||||||
|
}
|
||||||
|
if org := orgByID(t, a.OrganizationID); org != nil {
|
||||||
|
mergeWinnerFromOrganization(&w, org)
|
||||||
|
}
|
||||||
|
out = append(out, w)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.WinningTenderer == nil {
|
||||||
|
return []DocumentScraperWinnerResponse{}
|
||||||
|
}
|
||||||
|
w := t.WinningTenderer
|
||||||
|
if strings.TrimSpace(w.Name) == "" {
|
||||||
|
return []DocumentScraperWinnerResponse{}
|
||||||
|
}
|
||||||
|
addr := w.Address
|
||||||
|
return []DocumentScraperWinnerResponse{{
|
||||||
|
OfficialName: strings.TrimSpace(w.Name),
|
||||||
|
RegistrationNumber: strings.TrimSpace(w.CompanyID),
|
||||||
|
PostalAddress: strings.TrimSpace(addr.StreetName),
|
||||||
|
Town: strings.TrimSpace(addr.CityName),
|
||||||
|
Postcode: strings.TrimSpace(addr.PostalZone),
|
||||||
|
CountrySubdivisionNuts: strings.TrimSpace(addr.CountrySubentityCode),
|
||||||
|
Country: strings.TrimSpace(addr.CountryCode),
|
||||||
|
ContactPoint: strings.TrimSpace(w.ContactName),
|
||||||
|
Email: strings.TrimSpace(w.ContactEmail),
|
||||||
|
Telephone: strings.TrimSpace(w.ContactTelephone),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func orgByID(t *tender.Tender, id string) *tender.Organization {
|
||||||
|
id = strings.TrimSpace(id)
|
||||||
|
if id == "" || t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range t.Organizations {
|
||||||
|
if strings.TrimSpace(t.Organizations[i].ID) == id {
|
||||||
|
return &t.Organizations[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeWinnerFromOrganization(w *DocumentScraperWinnerResponse, org *tender.Organization) {
|
||||||
|
if w == nil || org == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if w.OfficialName == "" {
|
||||||
|
w.OfficialName = strings.TrimSpace(org.Name)
|
||||||
|
}
|
||||||
|
if w.RegistrationNumber == "" {
|
||||||
|
w.RegistrationNumber = strings.TrimSpace(org.CompanyID)
|
||||||
|
}
|
||||||
|
if w.PostalAddress == "" {
|
||||||
|
w.PostalAddress = strings.TrimSpace(org.Address.StreetName)
|
||||||
|
}
|
||||||
|
if w.Town == "" {
|
||||||
|
w.Town = strings.TrimSpace(org.Address.CityName)
|
||||||
|
}
|
||||||
|
if w.Postcode == "" {
|
||||||
|
w.Postcode = strings.TrimSpace(org.Address.PostalZone)
|
||||||
|
}
|
||||||
|
if w.CountrySubdivisionNuts == "" {
|
||||||
|
w.CountrySubdivisionNuts = strings.TrimSpace(org.Address.CountrySubentityCode)
|
||||||
|
}
|
||||||
|
if w.Country == "" {
|
||||||
|
w.Country = strings.TrimSpace(org.Address.CountryCode)
|
||||||
|
}
|
||||||
|
if w.ContactPoint == "" {
|
||||||
|
w.ContactPoint = strings.TrimSpace(org.ContactName)
|
||||||
|
}
|
||||||
|
if w.Email == "" {
|
||||||
|
w.Email = strings.TrimSpace(org.ContactEmail)
|
||||||
|
}
|
||||||
|
if w.Telephone == "" {
|
||||||
|
w.Telephone = strings.TrimSpace(org.ContactTelephone)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ type Notice struct {
|
|||||||
ProcurementTypeCode string `bson:"procurement_type_code" json:"procurement_type_code"`
|
ProcurementTypeCode string `bson:"procurement_type_code" json:"procurement_type_code"`
|
||||||
ProcedureCode string `bson:"procedure_code" json:"procedure_code"`
|
ProcedureCode string `bson:"procedure_code" json:"procedure_code"`
|
||||||
MainClassification string `bson:"main_classification" json:"main_classification"`
|
MainClassification string `bson:"main_classification" json:"main_classification"`
|
||||||
|
MainClassificationDescription string `bson:"main_classification_description,omitempty" json:"main_classification_description,omitempty"`
|
||||||
AdditionalClassifications []string `bson:"additional_classifications" json:"additional_classifications"`
|
AdditionalClassifications []string `bson:"additional_classifications" json:"additional_classifications"`
|
||||||
EstimatedValue float64 `bson:"estimated_value" json:"estimated_value"`
|
EstimatedValue float64 `bson:"estimated_value" json:"estimated_value"`
|
||||||
Currency string `bson:"currency" json:"currency"`
|
Currency string `bson:"currency" json:"currency"`
|
||||||
@@ -115,6 +116,7 @@ type ProcurementLot struct {
|
|||||||
Description string `bson:"description,omitempty" json:"description,omitempty"`
|
Description string `bson:"description,omitempty" json:"description,omitempty"`
|
||||||
MainNatureOfContract string `bson:"main_nature_of_contract,omitempty" json:"main_nature_of_contract,omitempty"`
|
MainNatureOfContract string `bson:"main_nature_of_contract,omitempty" json:"main_nature_of_contract,omitempty"`
|
||||||
MainClassification string `bson:"main_classification,omitempty" json:"main_classification,omitempty"`
|
MainClassification string `bson:"main_classification,omitempty" json:"main_classification,omitempty"`
|
||||||
|
MainClassificationDescription string `bson:"main_classification_description,omitempty" json:"main_classification_description,omitempty"`
|
||||||
AdditionalClassifications []string `bson:"additional_classifications,omitempty" json:"additional_classifications,omitempty"`
|
AdditionalClassifications []string `bson:"additional_classifications,omitempty" json:"additional_classifications,omitempty"`
|
||||||
Duration string `bson:"duration,omitempty" json:"duration,omitempty"`
|
Duration string `bson:"duration,omitempty" json:"duration,omitempty"`
|
||||||
EstimatedValue float64 `bson:"estimated_value,omitempty" json:"estimated_value,omitempty"`
|
EstimatedValue float64 `bson:"estimated_value,omitempty" json:"estimated_value,omitempty"`
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ type Tender struct {
|
|||||||
ProcurementTypeCode string `bson:"procurement_type_code" json:"procurement_type_code"`
|
ProcurementTypeCode string `bson:"procurement_type_code" json:"procurement_type_code"`
|
||||||
ProcedureCode string `bson:"procedure_code" json:"procedure_code"`
|
ProcedureCode string `bson:"procedure_code" json:"procedure_code"`
|
||||||
MainClassification string `bson:"main_classification" json:"main_classification"`
|
MainClassification string `bson:"main_classification" json:"main_classification"`
|
||||||
|
MainClassificationDescription string `bson:"main_classification_description,omitempty" json:"main_classification_description,omitempty"`
|
||||||
AdditionalClassifications []string `bson:"additional_classifications" json:"additional_classifications"`
|
AdditionalClassifications []string `bson:"additional_classifications" json:"additional_classifications"`
|
||||||
EstimatedValue float64 `bson:"estimated_value" json:"estimated_value"`
|
EstimatedValue float64 `bson:"estimated_value" json:"estimated_value"`
|
||||||
Currency string `bson:"currency" json:"currency"`
|
Currency string `bson:"currency" json:"currency"`
|
||||||
@@ -155,6 +156,7 @@ type ProcurementLot struct {
|
|||||||
Description string `bson:"description,omitempty" json:"description,omitempty"`
|
Description string `bson:"description,omitempty" json:"description,omitempty"`
|
||||||
MainNatureOfContract string `bson:"main_nature_of_contract,omitempty" json:"main_nature_of_contract,omitempty"`
|
MainNatureOfContract string `bson:"main_nature_of_contract,omitempty" json:"main_nature_of_contract,omitempty"`
|
||||||
MainClassification string `bson:"main_classification,omitempty" json:"main_classification,omitempty"`
|
MainClassification string `bson:"main_classification,omitempty" json:"main_classification,omitempty"`
|
||||||
|
MainClassificationDescription string `bson:"main_classification_description,omitempty" json:"main_classification_description,omitempty"`
|
||||||
AdditionalClassifications []string `bson:"additional_classifications,omitempty" json:"additional_classifications,omitempty"`
|
AdditionalClassifications []string `bson:"additional_classifications,omitempty" json:"additional_classifications,omitempty"`
|
||||||
Duration string `bson:"duration,omitempty" json:"duration,omitempty"`
|
Duration string `bson:"duration,omitempty" json:"duration,omitempty"`
|
||||||
EstimatedValue float64 `bson:"estimated_value,omitempty" json:"estimated_value,omitempty"`
|
EstimatedValue float64 `bson:"estimated_value,omitempty" json:"estimated_value,omitempty"`
|
||||||
|
|||||||
+366
-21
@@ -1,6 +1,11 @@
|
|||||||
package tender
|
package tender
|
||||||
|
|
||||||
import "tm/pkg/response"
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tm/pkg/response"
|
||||||
|
)
|
||||||
|
|
||||||
// UpdateTenderRequest represents a request to update an existing tender
|
// UpdateTenderRequest represents a request to update an existing tender
|
||||||
type UpdateTenderRequest struct {
|
type UpdateTenderRequest struct {
|
||||||
@@ -64,32 +69,134 @@ type SearchResponse struct {
|
|||||||
Metadata *response.Meta `json:"-"`
|
Metadata *response.Meta `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TenderResponse represents a tender in API responses
|
// AddressResponse is postal / NUTS address data for API consumers.
|
||||||
|
type AddressResponse struct {
|
||||||
|
StreetName string `json:"street_name,omitempty"`
|
||||||
|
CityName string `json:"city_name,omitempty"`
|
||||||
|
PostalZone string `json:"postal_zone,omitempty"`
|
||||||
|
CountrySubentityCode string `json:"country_subentity_code,omitempty"`
|
||||||
|
Department string `json:"department,omitempty"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrganizationResponse is an organization (buyer, winner, etc.) in tender API payloads.
|
||||||
|
type OrganizationResponse struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CompanyID string `json:"company_id,omitempty"`
|
||||||
|
WebsiteURI string `json:"website_uri,omitempty"`
|
||||||
|
ContactName string `json:"contact_name,omitempty"`
|
||||||
|
ContactTelephone string `json:"contact_telephone,omitempty"`
|
||||||
|
ContactEmail string `json:"contact_email,omitempty"`
|
||||||
|
Address AddressResponse `json:"address,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TenderLotResponse is one procurement lot for list/detail tender APIs.
|
||||||
|
type TenderLotResponse struct {
|
||||||
|
LotID string `json:"lot_id,omitempty"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
MainNatureOfContract string `json:"main_nature_of_contract,omitempty"`
|
||||||
|
MainClassification string `json:"main_classification,omitempty"`
|
||||||
|
MainClassificationDescription string `json:"main_classification_description,omitempty"`
|
||||||
|
MainClassificationDisplay string `json:"main_classification_display,omitempty"`
|
||||||
|
AdditionalClassifications []string `json:"additional_classifications,omitempty"`
|
||||||
|
Duration string `json:"duration,omitempty"`
|
||||||
|
EstimatedValue float64 `json:"estimated_value,omitempty"`
|
||||||
|
Currency string `json:"currency,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TenderWinnerResponse is a contract winner for awarded tenders.
|
||||||
|
type TenderWinnerResponse struct {
|
||||||
|
OfficialName string `json:"official_name"`
|
||||||
|
RegistrationNumber string `json:"registration_number,omitempty"`
|
||||||
|
PostalAddress string `json:"postal_address,omitempty"`
|
||||||
|
Town string `json:"town,omitempty"`
|
||||||
|
Postcode string `json:"postcode,omitempty"`
|
||||||
|
CountrySubdivisionNuts string `json:"country_subdivision_nuts,omitempty"`
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
ContactPoint string `json:"contact_point,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Telephone string `json:"telephone,omitempty"`
|
||||||
|
LotID string `json:"lot_id,omitempty"`
|
||||||
|
Amount float64 `json:"amount,omitempty"`
|
||||||
|
Currency string `json:"currency,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TenderResponse represents a tender in API responses (search, list, and detail).
|
||||||
type TenderResponse struct {
|
type TenderResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
NoticePublicationID string `json:"notice_publication_id"`
|
NoticePublicationID string `json:"notice_publication_id"`
|
||||||
RelatedNoticePublicationIDs []string `json:"related_notice_publication_ids,omitempty"`
|
RelatedNoticePublicationIDs []string `json:"related_notice_publication_ids,omitempty"`
|
||||||
|
ContractFolderID string `json:"contract_folder_id,omitempty"`
|
||||||
|
ProcurementProjectID string `json:"procurement_project_id,omitempty"`
|
||||||
|
ContractNoticeID string `json:"contract_notice_id,omitempty"`
|
||||||
|
|
||||||
NoticeTypeCode string `json:"notice_type_code"`
|
NoticeTypeCode string `json:"notice_type_code"`
|
||||||
|
FormType string `json:"form_type,omitempty"`
|
||||||
|
NoticeSubTypeCode string `json:"notice_sub_type_code,omitempty"`
|
||||||
|
NoticeIdentifier string `json:"notice_identifier,omitempty"`
|
||||||
|
NoticeVersion string `json:"notice_version,omitempty"`
|
||||||
|
NoticeLanguageCode string `json:"notice_language_code,omitempty"`
|
||||||
|
GazetteID string `json:"gazette_id,omitempty"`
|
||||||
|
|
||||||
|
NoticeDispatchAt int64 `json:"notice_dispatch_at,omitempty"`
|
||||||
|
NoticeDispatchDateWithTimezone string `json:"notice_dispatch_date_with_timezone,omitempty"`
|
||||||
|
ESenderDispatchAt int64 `json:"esender_dispatch_at,omitempty"`
|
||||||
|
IssueDate int64 `json:"issue_date,omitempty"`
|
||||||
|
IssueTime int64 `json:"issue_time,omitempty"`
|
||||||
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
|
||||||
ProcurementTypeCode string `json:"procurement_type_code"`
|
ProcurementTypeCode string `json:"procurement_type_code"`
|
||||||
ProcedureCode string `json:"procedure_code"`
|
ProcedureCode string `json:"procedure_code"`
|
||||||
MainClassification string `json:"main_classification"`
|
MainClassification string `json:"main_classification"`
|
||||||
|
MainClassificationDescription string `json:"main_classification_description,omitempty"`
|
||||||
|
MainClassificationDisplay string `json:"main_classification_display,omitempty"`
|
||||||
|
AdditionalClassifications []string `json:"additional_classifications,omitempty"`
|
||||||
EstimatedValue float64 `json:"estimated_value"`
|
EstimatedValue float64 `json:"estimated_value"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
|
EstimatedValueVATBasis string `json:"estimated_value_vat_basis,omitempty"`
|
||||||
|
|
||||||
Duration string `json:"duration"`
|
Duration string `json:"duration"`
|
||||||
DurationUnit string `json:"duration_unit"`
|
DurationUnit string `json:"duration_unit"`
|
||||||
|
|
||||||
PublicationDate int64 `json:"publication_date"`
|
PublicationDate int64 `json:"publication_date"`
|
||||||
TenderDeadline int64 `json:"tender_deadline"`
|
TenderDeadline int64 `json:"tender_deadline"`
|
||||||
SubmissionDeadline int64 `json:"submission_deadline"`
|
SubmissionDeadline int64 `json:"submission_deadline"`
|
||||||
ApplicationDeadline int64 `json:"application_deadline"`
|
ApplicationDeadline int64 `json:"application_deadline"`
|
||||||
SubmissionURL string `json:"submission_url"`
|
SubmissionURL string `json:"submission_url"`
|
||||||
|
|
||||||
|
PlaceOfPerformance string `json:"place_of_performance,omitempty"`
|
||||||
CountryCode string `json:"country_code"`
|
CountryCode string `json:"country_code"`
|
||||||
BuyerOrganization *OrganizationResponse `json:"buyer_organization"`
|
RegionCode string `json:"region_code,omitempty"`
|
||||||
|
CityName string `json:"city_name,omitempty"`
|
||||||
|
PostalCode string `json:"postal_code,omitempty"`
|
||||||
|
|
||||||
|
DocumentURI string `json:"document_uri,omitempty"`
|
||||||
|
DocumentURL string `json:"document_url,omitempty"`
|
||||||
|
TenderURL string `json:"tender_url,omitempty"`
|
||||||
|
BuyerProfileURL string `json:"buyer_profile_url,omitempty"`
|
||||||
|
|
||||||
|
BuyerOrganization *OrganizationResponse `json:"buyer_organization,omitempty"`
|
||||||
|
WinningTenderer *OrganizationResponse `json:"winning_tenderer,omitempty"`
|
||||||
|
Lots []TenderLotResponse `json:"lots,omitempty"`
|
||||||
|
Winners []TenderWinnerResponse `json:"winners,omitempty"`
|
||||||
|
OfficialLanguages []string `json:"official_languages,omitempty"`
|
||||||
|
|
||||||
|
AwardDate int64 `json:"award_date,omitempty"`
|
||||||
|
AwardedValue float64 `json:"awarded_value,omitempty"`
|
||||||
|
ContractNumber string `json:"contract_number,omitempty"`
|
||||||
|
|
||||||
Status TenderStatus `json:"status"`
|
Status TenderStatus `json:"status"`
|
||||||
|
Source TenderSource `json:"source,omitempty"`
|
||||||
TenderID string `json:"tender_id"`
|
TenderID string `json:"tender_id"`
|
||||||
|
ProjectName string `json:"project_name,omitempty"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
OverallSummary string `json:"overall_summary,omitempty"` // AI-generated summary from the pipeline
|
|
||||||
|
OverallSummary string `json:"overall_summary,omitempty"`
|
||||||
Language string `json:"language,omitempty"`
|
Language string `json:"language,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,9 +214,10 @@ type TenderDocumentsResponse struct {
|
|||||||
Documents []TenderDocumentResponse `json:"documents"`
|
Documents []TenderDocumentResponse `json:"documents"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrganizationResponse struct {
|
const (
|
||||||
Name string `json:"name"`
|
enrichmentMainCPVDescriptionKey = "main_classification_description"
|
||||||
}
|
vatBasisUnspecified = "unspecified"
|
||||||
|
)
|
||||||
|
|
||||||
// ToTenderResponse converts a Tender entity to TenderResponse
|
// ToTenderResponse converts a Tender entity to TenderResponse
|
||||||
func (t *Tender) ToResponse() *TenderResponse {
|
func (t *Tender) ToResponse() *TenderResponse {
|
||||||
@@ -119,11 +227,6 @@ func (t *Tender) ToResponse() *TenderResponse {
|
|||||||
// ToResponseWithLanguage converts a Tender entity to TenderResponse using a preferred language.
|
// ToResponseWithLanguage converts a Tender entity to TenderResponse using a preferred language.
|
||||||
// When language is provided and translation exists, translated title/description are returned.
|
// When language is provided and translation exists, translated title/description are returned.
|
||||||
func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
|
func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
|
||||||
|
|
||||||
org := &OrganizationResponse{}
|
|
||||||
if t.BuyerOrganization != nil {
|
|
||||||
org.Name = t.BuyerOrganization.Name
|
|
||||||
}
|
|
||||||
title := t.Title
|
title := t.Title
|
||||||
description := t.Description
|
description := t.Description
|
||||||
usedLanguage := ""
|
usedLanguage := ""
|
||||||
@@ -135,29 +238,85 @@ func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainDesc := firstNonEmpty(
|
||||||
|
strings.TrimSpace(t.ProcessingMetadata.EnrichmentData[enrichmentMainCPVDescriptionKey]),
|
||||||
|
strings.TrimSpace(t.MainClassificationDescription),
|
||||||
|
)
|
||||||
|
mainCode := strings.TrimSpace(t.MainClassification)
|
||||||
|
|
||||||
|
documentURL := strings.TrimSpace(t.DocumentURI)
|
||||||
|
if documentURL == "" {
|
||||||
|
documentURL = strings.TrimSpace(t.TenderURL)
|
||||||
|
}
|
||||||
|
|
||||||
response := &TenderResponse{
|
response := &TenderResponse{
|
||||||
ID: t.ID.Hex(),
|
ID: t.ID.Hex(),
|
||||||
NoticePublicationID: t.NoticePublicationID,
|
NoticePublicationID: t.NoticePublicationID,
|
||||||
RelatedNoticePublicationIDs: t.RelatedNoticePublicationIDs,
|
RelatedNoticePublicationIDs: append([]string(nil), t.RelatedNoticePublicationIDs...),
|
||||||
NoticeTypeCode: t.NoticeTypeCode,
|
ContractFolderID: strings.TrimSpace(t.ContractFolderID),
|
||||||
|
ProcurementProjectID: strings.TrimSpace(t.ProcurementProjectID),
|
||||||
|
ContractNoticeID: strings.TrimSpace(t.ContractNoticeID),
|
||||||
|
|
||||||
|
NoticeTypeCode: strings.TrimSpace(t.NoticeTypeCode),
|
||||||
|
FormType: strings.TrimSpace(t.FormType),
|
||||||
|
NoticeSubTypeCode: strings.TrimSpace(t.NoticeSubTypeCode),
|
||||||
|
NoticeIdentifier: strings.TrimSpace(t.NoticeIdentifier),
|
||||||
|
NoticeVersion: strings.TrimSpace(t.NoticeVersion),
|
||||||
|
NoticeLanguageCode: strings.TrimSpace(t.NoticeLanguageCode),
|
||||||
|
GazetteID: strings.TrimSpace(t.GazetteID),
|
||||||
|
|
||||||
|
NoticeDispatchAt: t.NoticeDispatchAt,
|
||||||
|
NoticeDispatchDateWithTimezone: formatNoticeDispatchRFC3339(t),
|
||||||
|
ESenderDispatchAt: t.ESenderDispatchAt,
|
||||||
|
IssueDate: t.IssueDate,
|
||||||
|
IssueTime: t.IssueTime,
|
||||||
|
|
||||||
Title: title,
|
Title: title,
|
||||||
Description: description,
|
Description: description,
|
||||||
ProcurementTypeCode: t.ProcurementTypeCode,
|
ProcurementTypeCode: strings.TrimSpace(t.ProcurementTypeCode),
|
||||||
ProcedureCode: t.ProcedureCode,
|
ProcedureCode: strings.TrimSpace(t.ProcedureCode),
|
||||||
MainClassification: t.MainClassification,
|
MainClassification: mainCode,
|
||||||
|
MainClassificationDescription: mainDesc,
|
||||||
|
MainClassificationDisplay: classificationDisplay(mainCode, mainDesc),
|
||||||
|
AdditionalClassifications: append([]string(nil), t.AdditionalClassifications...),
|
||||||
EstimatedValue: t.EstimatedValue,
|
EstimatedValue: t.EstimatedValue,
|
||||||
Currency: t.Currency,
|
Currency: strings.TrimSpace(t.Currency),
|
||||||
|
EstimatedValueVATBasis: vatBasisUnspecified,
|
||||||
|
|
||||||
Duration: t.Duration,
|
Duration: t.Duration,
|
||||||
DurationUnit: t.DurationUnit,
|
DurationUnit: t.DurationUnit,
|
||||||
|
|
||||||
PublicationDate: t.PublicationDate,
|
PublicationDate: t.PublicationDate,
|
||||||
TenderDeadline: t.TenderDeadline,
|
TenderDeadline: t.TenderDeadline,
|
||||||
SubmissionDeadline: t.SubmissionDeadline,
|
SubmissionDeadline: t.SubmissionDeadline,
|
||||||
ApplicationDeadline: t.ApplicationDeadline,
|
ApplicationDeadline: t.ApplicationDeadline,
|
||||||
CountryCode: t.CountryCode,
|
SubmissionURL: strings.TrimSpace(t.SubmissionURL),
|
||||||
SubmissionURL: t.SubmissionURL,
|
|
||||||
BuyerOrganization: org,
|
PlaceOfPerformance: strings.TrimSpace(t.PlaceOfPerformance),
|
||||||
|
CountryCode: strings.TrimSpace(t.CountryCode),
|
||||||
|
RegionCode: strings.TrimSpace(t.RegionCode),
|
||||||
|
CityName: strings.TrimSpace(t.CityName),
|
||||||
|
PostalCode: strings.TrimSpace(t.PostalCode),
|
||||||
|
|
||||||
|
DocumentURI: strings.TrimSpace(t.DocumentURI),
|
||||||
|
DocumentURL: documentURL,
|
||||||
|
TenderURL: strings.TrimSpace(t.TenderURL),
|
||||||
|
BuyerProfileURL: strings.TrimSpace(t.BuyerProfileURL),
|
||||||
|
|
||||||
|
BuyerOrganization: organizationToResponse(t.BuyerOrganization),
|
||||||
|
WinningTenderer: organizationToResponse(t.WinningTenderer),
|
||||||
|
Lots: buildTenderLotsResponse(t, mainDesc),
|
||||||
|
Winners: buildTenderWinnersResponse(t),
|
||||||
|
OfficialLanguages: append([]string(nil), t.OfficialLanguages...),
|
||||||
|
|
||||||
|
AwardDate: t.AwardDate,
|
||||||
|
AwardedValue: t.AwardedValue,
|
||||||
|
ContractNumber: strings.TrimSpace(t.ContractNumber),
|
||||||
|
|
||||||
Status: t.Status,
|
Status: t.Status,
|
||||||
|
Source: t.Source,
|
||||||
TenderID: t.TenderID,
|
TenderID: t.TenderID,
|
||||||
|
ProjectName: strings.TrimSpace(t.ProjectName),
|
||||||
CreatedAt: t.CreatedAt,
|
CreatedAt: t.CreatedAt,
|
||||||
Language: usedLanguage,
|
Language: usedLanguage,
|
||||||
}
|
}
|
||||||
@@ -165,6 +324,192 @@ func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
|
|||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func organizationToResponse(o *Organization) *OrganizationResponse {
|
||||||
|
if o == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &OrganizationResponse{
|
||||||
|
ID: strings.TrimSpace(o.ID),
|
||||||
|
Name: strings.TrimSpace(o.Name),
|
||||||
|
CompanyID: strings.TrimSpace(o.CompanyID),
|
||||||
|
WebsiteURI: strings.TrimSpace(o.WebsiteURI),
|
||||||
|
ContactName: strings.TrimSpace(o.ContactName),
|
||||||
|
ContactTelephone: strings.TrimSpace(o.ContactTelephone),
|
||||||
|
ContactEmail: strings.TrimSpace(o.ContactEmail),
|
||||||
|
Address: AddressResponse{
|
||||||
|
StreetName: strings.TrimSpace(o.Address.StreetName),
|
||||||
|
CityName: strings.TrimSpace(o.Address.CityName),
|
||||||
|
PostalZone: strings.TrimSpace(o.Address.PostalZone),
|
||||||
|
CountrySubentityCode: strings.TrimSpace(o.Address.CountrySubentityCode),
|
||||||
|
Department: strings.TrimSpace(o.Address.Department),
|
||||||
|
Region: strings.TrimSpace(o.Address.Region),
|
||||||
|
CountryCode: strings.TrimSpace(o.Address.CountryCode),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatNoticeDispatchRFC3339(t *Tender) string {
|
||||||
|
if t == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sec := t.NoticeDispatchAt
|
||||||
|
if sec == 0 {
|
||||||
|
sec = t.ESenderDispatchAt
|
||||||
|
}
|
||||||
|
if sec == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return time.Unix(sec, 0).UTC().Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonEmpty(values ...string) string {
|
||||||
|
for _, v := range values {
|
||||||
|
if s := strings.TrimSpace(v); s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func classificationDisplay(code, description string) string {
|
||||||
|
code = strings.TrimSpace(code)
|
||||||
|
description = strings.TrimSpace(description)
|
||||||
|
if code != "" && description != "" {
|
||||||
|
return code + " - " + description
|
||||||
|
}
|
||||||
|
if code != "" {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTenderLotsResponse(t *Tender, globalMainDesc string) []TenderLotResponse {
|
||||||
|
if t == nil || len(t.ProcurementLots) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]TenderLotResponse, 0, len(t.ProcurementLots))
|
||||||
|
mainCode := strings.TrimSpace(t.MainClassification)
|
||||||
|
for i := range t.ProcurementLots {
|
||||||
|
lot := t.ProcurementLots[i]
|
||||||
|
lotCode := strings.TrimSpace(lot.MainClassification)
|
||||||
|
perLotDesc := strings.TrimSpace(lot.MainClassificationDescription)
|
||||||
|
if perLotDesc == "" && lotCode != "" && lotCode == mainCode {
|
||||||
|
perLotDesc = globalMainDesc
|
||||||
|
}
|
||||||
|
out = append(out, TenderLotResponse{
|
||||||
|
LotID: strings.TrimSpace(lot.LotID),
|
||||||
|
Title: strings.TrimSpace(lot.Title),
|
||||||
|
Description: strings.TrimSpace(lot.Description),
|
||||||
|
MainNatureOfContract: strings.TrimSpace(lot.MainNatureOfContract),
|
||||||
|
MainClassification: lotCode,
|
||||||
|
MainClassificationDescription: perLotDesc,
|
||||||
|
MainClassificationDisplay: classificationDisplay(lotCode, perLotDesc),
|
||||||
|
AdditionalClassifications: append([]string(nil), lot.AdditionalClassifications...),
|
||||||
|
Duration: strings.TrimSpace(lot.Duration),
|
||||||
|
EstimatedValue: lot.EstimatedValue,
|
||||||
|
Currency: strings.TrimSpace(lot.Currency),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTenderWinnersResponse(t *Tender) []TenderWinnerResponse {
|
||||||
|
if t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(t.AwardedEntities) > 0 {
|
||||||
|
out := make([]TenderWinnerResponse, 0, len(t.AwardedEntities))
|
||||||
|
for _, a := range t.AwardedEntities {
|
||||||
|
w := TenderWinnerResponse{
|
||||||
|
OfficialName: strings.TrimSpace(a.Name),
|
||||||
|
RegistrationNumber: strings.TrimSpace(a.CompanyID),
|
||||||
|
PostalAddress: strings.TrimSpace(a.Address),
|
||||||
|
Country: strings.TrimSpace(a.Country),
|
||||||
|
LotID: strings.TrimSpace(a.LotID),
|
||||||
|
Amount: a.Amount,
|
||||||
|
Currency: strings.TrimSpace(a.Currency),
|
||||||
|
Town: "",
|
||||||
|
Postcode: "",
|
||||||
|
CountrySubdivisionNuts: "",
|
||||||
|
ContactPoint: "",
|
||||||
|
Email: "",
|
||||||
|
Telephone: "",
|
||||||
|
}
|
||||||
|
if org := organizationByID(t, a.OrganizationID); org != nil {
|
||||||
|
mergeWinnerFromOrganization(&w, org)
|
||||||
|
}
|
||||||
|
out = append(out, w)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
if t.WinningTenderer == nil || strings.TrimSpace(t.WinningTenderer.Name) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w := t.WinningTenderer
|
||||||
|
addr := w.Address
|
||||||
|
return []TenderWinnerResponse{{
|
||||||
|
OfficialName: strings.TrimSpace(w.Name),
|
||||||
|
RegistrationNumber: strings.TrimSpace(w.CompanyID),
|
||||||
|
PostalAddress: strings.TrimSpace(addr.StreetName),
|
||||||
|
Town: strings.TrimSpace(addr.CityName),
|
||||||
|
Postcode: strings.TrimSpace(addr.PostalZone),
|
||||||
|
CountrySubdivisionNuts: strings.TrimSpace(addr.CountrySubentityCode),
|
||||||
|
Country: strings.TrimSpace(addr.CountryCode),
|
||||||
|
ContactPoint: strings.TrimSpace(w.ContactName),
|
||||||
|
Email: strings.TrimSpace(w.ContactEmail),
|
||||||
|
Telephone: strings.TrimSpace(w.ContactTelephone),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func organizationByID(t *Tender, id string) *Organization {
|
||||||
|
id = strings.TrimSpace(id)
|
||||||
|
if id == "" || t == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range t.Organizations {
|
||||||
|
if strings.TrimSpace(t.Organizations[i].ID) == id {
|
||||||
|
return &t.Organizations[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeWinnerFromOrganization(w *TenderWinnerResponse, org *Organization) {
|
||||||
|
if w == nil || org == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if w.OfficialName == "" {
|
||||||
|
w.OfficialName = strings.TrimSpace(org.Name)
|
||||||
|
}
|
||||||
|
if w.RegistrationNumber == "" {
|
||||||
|
w.RegistrationNumber = strings.TrimSpace(org.CompanyID)
|
||||||
|
}
|
||||||
|
if w.PostalAddress == "" {
|
||||||
|
w.PostalAddress = strings.TrimSpace(org.Address.StreetName)
|
||||||
|
}
|
||||||
|
if w.Town == "" {
|
||||||
|
w.Town = strings.TrimSpace(org.Address.CityName)
|
||||||
|
}
|
||||||
|
if w.Postcode == "" {
|
||||||
|
w.Postcode = strings.TrimSpace(org.Address.PostalZone)
|
||||||
|
}
|
||||||
|
if w.CountrySubdivisionNuts == "" {
|
||||||
|
w.CountrySubdivisionNuts = strings.TrimSpace(org.Address.CountrySubentityCode)
|
||||||
|
}
|
||||||
|
if w.Country == "" {
|
||||||
|
w.Country = strings.TrimSpace(org.Address.CountryCode)
|
||||||
|
}
|
||||||
|
if w.ContactPoint == "" {
|
||||||
|
w.ContactPoint = strings.TrimSpace(org.ContactName)
|
||||||
|
}
|
||||||
|
if w.Email == "" {
|
||||||
|
w.Email = strings.TrimSpace(org.ContactEmail)
|
||||||
|
}
|
||||||
|
if w.Telephone == "" {
|
||||||
|
w.Telephone = strings.TrimSpace(org.ContactTelephone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AITranslateRequest represents a request for on-demand AI translation.
|
// AITranslateRequest represents a request for on-demand AI translation.
|
||||||
type AITranslateRequest struct {
|
type AITranslateRequest struct {
|
||||||
Language string `json:"language" valid:"required,stringlength(2|5)~Language must be between 2 and 5 characters"`
|
Language string `json:"language" valid:"required,stringlength(2|5)~Language must be between 2 and 5 characters"`
|
||||||
|
|||||||
@@ -113,12 +113,7 @@ func (s *tenderService) GetByID(ctx context.Context, id string) (*TenderResponse
|
|||||||
return nil, fmt.Errorf("failed to retrieve tender: %w", err)
|
return nil, fmt.Errorf("failed to retrieve tender: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := tender.ToResponse()
|
resp := tender.ToResponseWithLanguage(s.defaultLanguage)
|
||||||
if tr, ok := tender.Translations[s.defaultLanguage]; ok {
|
|
||||||
resp.Title = tr.Title
|
|
||||||
resp.Description = tr.Description
|
|
||||||
resp.Language = s.defaultLanguage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enrich with AI summary from MinIO storage (best-effort, non-blocking)
|
// Enrich with AI summary from MinIO storage (best-effort, non-blocking)
|
||||||
s.enrichWithAISummary(ctx, tender, resp)
|
s.enrichWithAISummary(ctx, tender, resp)
|
||||||
@@ -284,7 +279,7 @@ func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*T
|
|||||||
"tender_id": tender.ID,
|
"tender_id": tender.ID,
|
||||||
})
|
})
|
||||||
|
|
||||||
return tender.ToResponse(), nil
|
return tender.ToResponseWithLanguage(s.defaultLanguage), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes a tender
|
// Delete deletes a tender
|
||||||
|
|||||||
+172
-21
@@ -19,7 +19,9 @@ func mapProcurementLotFromTED(lot *ProcurementProjectLot) notice.ProcurementLot
|
|||||||
out.Description = pp.Description
|
out.Description = pp.Description
|
||||||
out.MainNatureOfContract = pp.ProcurementTypeCode
|
out.MainNatureOfContract = pp.ProcurementTypeCode
|
||||||
if pp.MainCommodityClassification != nil {
|
if pp.MainCommodityClassification != nil {
|
||||||
out.MainClassification = strings.TrimSpace(pp.MainCommodityClassification.ItemClassificationCode)
|
mc := pp.MainCommodityClassification
|
||||||
|
out.MainClassification = strings.TrimSpace(mc.ItemClassificationCode)
|
||||||
|
out.MainClassificationDescription = mainCommodityClassificationLabel(mc)
|
||||||
}
|
}
|
||||||
for _, ac := range pp.AdditionalCommodityClassification {
|
for _, ac := range pp.AdditionalCommodityClassification {
|
||||||
c := strings.TrimSpace(ac.ItemClassificationCode)
|
c := strings.TrimSpace(ac.ItemClassificationCode)
|
||||||
@@ -86,7 +88,7 @@ func (s *TEDScraper) mapToTenderFromParsedDoc(parsedDoc *ParsedDocument, sourceF
|
|||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return s.mapGenericNoticeToTender(parsedDoc.PriorInformationNotice, sourceFileURL, sourceFileName)
|
return s.mapPriorInformationNoticeToTender(parsedDoc.PriorInformationNotice, sourceFileURL, sourceFileName)
|
||||||
default:
|
default:
|
||||||
s.logger.Error("Unknown document type for mapping", map[string]interface{}{
|
s.logger.Error("Unknown document type for mapping", map[string]interface{}{
|
||||||
"document_type": string(parsedDoc.Type),
|
"document_type": string(parsedDoc.Type),
|
||||||
@@ -95,30 +97,37 @@ func (s *TEDScraper) mapToTenderFromParsedDoc(parsedDoc *ParsedDocument, sourceF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mapGenericNoticeToTender maps any TED document type to tender entity using the common interface
|
// mapPriorInformationNoticeToTender maps a TED PriorInformationNotice to the same notice shape as a contract notice,
|
||||||
func (s *TEDScraper) mapGenericNoticeToTender(doc TEDDocument, sourceFileURL, sourceFileName string) *notice.Notice {
|
// so procedure keys (ContractFolderID, NoticePublicationID, ProcurementProjectID) merge into one tender.
|
||||||
if doc == nil {
|
func (s *TEDScraper) mapPriorInformationNoticeToTender(pin *PriorInformationNotice, sourceFileURL, sourceFileName string) *notice.Notice {
|
||||||
s.logger.Error("Document is nil in mapGenericNoticeToTender", map[string]interface{}{})
|
if pin == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
t := ¬ice.Notice{
|
t := ¬ice.Notice{
|
||||||
ContractNoticeID: doc.GetID(),
|
ContractNoticeID: pin.ID,
|
||||||
ContractFolderID: "", // Not available in generic interface
|
ContractFolderID: pin.ContractFolderID,
|
||||||
NoticeTypeCode: doc.GetNoticeType(),
|
ProcurementProjectID: procurementProjectIDFromPriorInformationNotice(pin),
|
||||||
NoticeSubTypeCode: "", // Not available in generic interface
|
NoticeTypeCode: strings.TrimSpace(pin.GetNoticeType()),
|
||||||
NoticeLanguageCode: doc.GetLanguage(),
|
FormType: pin.GetNoticeFormType(),
|
||||||
IssueDate: now, // TODO: Parse actual date from document
|
NoticeSubTypeCode: pin.GetNoticeSubType(),
|
||||||
IssueTime: now, // TODO: Parse actual time from document
|
NoticeIdentifier: pin.ID,
|
||||||
GazetteID: "", // Not available in generic interface
|
NoticeVersion: pin.VersionID,
|
||||||
NoticePublicationID: "", // Not available in generic interface
|
NoticeDispatchAt: ParseIssueDateTimeToUnix(pin.IssueDate, pin.IssueTime),
|
||||||
PublicationDate: now, // TODO: Parse actual publication date
|
ESenderDispatchAt: TransmissionDispatchUnix(pin.Extensions),
|
||||||
|
NoticeLanguageCode: pin.NoticeLanguageCode,
|
||||||
// Basic tender information
|
IssueDate: ParseDateToUnix(pin.IssueDate),
|
||||||
Title: "Generic Notice " + doc.GetNoticeType(),
|
IssueTime: ParseTimeToUnix(pin.IssueTime),
|
||||||
Description: "Processed from " + doc.GetNoticeType() + " document",
|
GazetteID: pin.GetOJSID(),
|
||||||
|
Title: pin.GetProcurementTitle(),
|
||||||
|
Description: pin.GetProcurementDescription(),
|
||||||
|
ProcurementTypeCode: pin.GetContractNature(),
|
||||||
|
ProcedureCode: pin.GetProcedureCode(),
|
||||||
|
MainClassification: pin.GetMainClassification(),
|
||||||
|
MainClassificationDescription: pin.GetMainClassificationDescription(),
|
||||||
|
PlaceOfPerformance: pin.GetPlaceOfPerformance(),
|
||||||
|
DocumentURI: pin.GetDocumentURI(),
|
||||||
Status: notice.TenderStatusActive,
|
Status: notice.TenderStatusActive,
|
||||||
Source: notice.TenderSourceTEDScraper,
|
Source: notice.TenderSourceTEDScraper,
|
||||||
SourceFileURL: sourceFileURL,
|
SourceFileURL: sourceFileURL,
|
||||||
@@ -130,9 +139,149 @@ func (s *TEDScraper) mapGenericNoticeToTender(doc TEDDocument, sourceFileURL, so
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if pubInfo := pin.GetPublicationInfo(); pubInfo != nil {
|
||||||
|
t.NoticePublicationID = pubInfo.NoticePublicationID
|
||||||
|
t.PublicationDate = ParseDateToUnix(pubInfo.PublicationDate)
|
||||||
|
t.TenderURL = GenerateTenderURL(pubInfo.NoticePublicationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pin.ProcurementProject != nil && pin.ProcurementProject.AdditionalCommodityClassification != nil {
|
||||||
|
for _, additionalClass := range pin.ProcurementProject.AdditionalCommodityClassification {
|
||||||
|
if additionalClass.ItemClassificationCode != "" {
|
||||||
|
t.AdditionalClassifications = append(t.AdditionalClassifications, additionalClass.ItemClassificationCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if amount, currency := pin.GetBudget(); amount != "" {
|
||||||
|
if value, ok := ParseEuropeanAmount(amount); ok {
|
||||||
|
t.EstimatedValue = value
|
||||||
|
t.Currency = currency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pin.ContractingParty != nil {
|
||||||
|
t.BuyerProfileURL = pin.ContractingParty.BuyerProfileURI
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range pin.ProcurementProjectLot {
|
||||||
|
t.ProcurementLots = append(t.ProcurementLots, mapProcurementLotFromTED(&pin.ProcurementProjectLot[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if duration, unit := pin.GetDuration(); duration != "" {
|
||||||
|
t.Duration = duration
|
||||||
|
t.DurationUnit = unit
|
||||||
|
}
|
||||||
|
|
||||||
|
if deadline := pin.GetTenderDeadline(); deadline != "" {
|
||||||
|
t.TenderDeadline = ParseDateToUnix(deadline)
|
||||||
|
if deadlineTime, err := ParseDate(deadline); err == nil {
|
||||||
|
applicationDeadline := CalculateApplicationDeadline(deadlineTime)
|
||||||
|
t.ApplicationDeadline = applicationDeadline.Unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubDate, err := ParseDate(pin.GetPublicationDate()); err == nil {
|
||||||
|
submissionDeadline := CalculateSubmissionDeadline(pubDate)
|
||||||
|
t.SubmissionDeadline = submissionDeadline.Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
if lot := pin.firstLot(); lot != nil && lot.TenderingTerms != nil {
|
||||||
|
if tenderingTerms := lot.TenderingTerms; tenderingTerms.TenderRecipientParty != nil {
|
||||||
|
if endpointID := tenderingTerms.TenderRecipientParty.EndpointID; endpointID != "" {
|
||||||
|
if strings.HasPrefix(endpointID, "http") {
|
||||||
|
t.SubmissionURL = endpointID
|
||||||
|
} else {
|
||||||
|
t.SubmissionURL = "https://" + endpointID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pin.ProcurementProject != nil && pin.ProcurementProject.RealizedLocation != nil {
|
||||||
|
if addr := pin.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if lot := pin.firstLot(); lot != nil && lot.ProcurementProject != nil &&
|
||||||
|
lot.ProcurementProject.RealizedLocation != nil {
|
||||||
|
if addr := lot.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buyerID := pin.GetBuyerID()
|
||||||
|
if buyerID != "" {
|
||||||
|
if buyerOrg := pin.GetOrganizationByID(buyerID); buyerOrg != nil {
|
||||||
|
t.BuyerOrganization = s.mapOrganization(buyerOrg, "buyer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reviewID := pin.GetReviewOrganizationID()
|
||||||
|
if reviewID != "" {
|
||||||
|
if reviewOrg := pin.GetOrganizationByID(reviewID); reviewOrg != nil {
|
||||||
|
t.ReviewOrganization = s.mapOrganization(reviewOrg, "review")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if orgs := pin.GetOrganizations(); orgs != nil {
|
||||||
|
for _, org := range orgs.Organization {
|
||||||
|
mappedOrg := s.mapOrganization(&org, "")
|
||||||
|
if mappedOrg != nil {
|
||||||
|
t.Organizations = append(t.Organizations, *mappedOrg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if criteria := pin.GetSelectionCriteria(); criteria != nil {
|
||||||
|
for _, criterion := range criteria {
|
||||||
|
t.SelectionCriteria = append(t.SelectionCriteria, notice.SelectionCriterion{
|
||||||
|
TypeCode: criterion.CriterionTypeCode,
|
||||||
|
Description: criterion.Description,
|
||||||
|
LanguageID: criterion.LanguageID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if languages := pin.GetOfficialLanguages(); languages != nil {
|
||||||
|
t.OfficialLanguages = languages
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(t.Title) == "" {
|
||||||
|
if np := strings.TrimSpace(t.NoticePublicationID); np != "" {
|
||||||
|
t.Title = "Prior information notice " + np
|
||||||
|
} else if cf := strings.TrimSpace(t.ContractFolderID); cf != "" {
|
||||||
|
t.Title = "Prior information notice — procedure " + cf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func procurementProjectIDFromPriorInformationNotice(pin *PriorInformationNotice) string {
|
||||||
|
if pin == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if pin.ProcurementProject != nil {
|
||||||
|
if id := strings.TrimSpace(pin.ProcurementProject.ID); id != "" {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lot := pin.firstLot(); lot != nil && lot.ProcurementProject != nil {
|
||||||
|
return strings.TrimSpace(lot.ProcurementProject.ID)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// mapToTender maps a TED ContractNotice to tender entity
|
// mapToTender maps a TED ContractNotice to tender entity
|
||||||
func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileName string) *notice.Notice {
|
func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileName string) *notice.Notice {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
@@ -157,6 +306,7 @@ func (s *TEDScraper) mapToTender(cn *ContractNotice, sourceFileURL, sourceFileNa
|
|||||||
ProcurementTypeCode: cn.GetContractNature(),
|
ProcurementTypeCode: cn.GetContractNature(),
|
||||||
ProcedureCode: cn.GetProcedureCode(),
|
ProcedureCode: cn.GetProcedureCode(),
|
||||||
MainClassification: cn.GetMainClassification(),
|
MainClassification: cn.GetMainClassification(),
|
||||||
|
MainClassificationDescription: cn.GetMainClassificationDescription(),
|
||||||
PlaceOfPerformance: cn.GetPlaceOfPerformance(),
|
PlaceOfPerformance: cn.GetPlaceOfPerformance(),
|
||||||
DocumentURI: cn.GetDocumentURI(),
|
DocumentURI: cn.GetDocumentURI(),
|
||||||
Status: notice.TenderStatusActive,
|
Status: notice.TenderStatusActive,
|
||||||
@@ -317,6 +467,7 @@ func (s *TEDScraper) mapContractAwardNoticeToTender(can *ContractAwardNotice, so
|
|||||||
ProcurementTypeCode: can.GetContractNature(),
|
ProcurementTypeCode: can.GetContractNature(),
|
||||||
ProcedureCode: can.GetProcedureCode(),
|
ProcedureCode: can.GetProcedureCode(),
|
||||||
MainClassification: can.GetMainClassification(),
|
MainClassification: can.GetMainClassification(),
|
||||||
|
MainClassificationDescription: can.GetMainClassificationDescription(),
|
||||||
Status: notice.TenderStatusAwarded, // Award notices are for completed tenders
|
Status: notice.TenderStatusAwarded, // Award notices are for completed tenders
|
||||||
Source: notice.TenderSourceTEDScraper,
|
Source: notice.TenderSourceTEDScraper,
|
||||||
SourceFileURL: sourceFileURL,
|
SourceFileURL: sourceFileURL,
|
||||||
|
|||||||
+321
-23
@@ -55,6 +55,14 @@ type TEDDocument interface {
|
|||||||
Validate() error
|
Validate() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ublEformsExtension returns embedded eForms extension data from UBL extensions (shared across notice root types).
|
||||||
|
func ublEformsExtension(ext *UBLExtensions) *EformsExtension {
|
||||||
|
if ext == nil || ext.UBLExtension == nil || ext.UBLExtension.ExtensionContent == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return ext.UBLExtension.ExtensionContent.EformsExtension
|
||||||
|
}
|
||||||
|
|
||||||
// PriorInformationNotice represents a prior information notice
|
// PriorInformationNotice represents a prior information notice
|
||||||
type PriorInformationNotice struct {
|
type PriorInformationNotice struct {
|
||||||
XMLName xml.Name `xml:"PriorInformationNotice"`
|
XMLName xml.Name `xml:"PriorInformationNotice"`
|
||||||
@@ -73,7 +81,7 @@ type PriorInformationNotice struct {
|
|||||||
IssueTime string `xml:"IssueTime"`
|
IssueTime string `xml:"IssueTime"`
|
||||||
VersionID string `xml:"VersionID"`
|
VersionID string `xml:"VersionID"`
|
||||||
RegulatoryDomain string `xml:"RegulatoryDomain"`
|
RegulatoryDomain string `xml:"RegulatoryDomain"`
|
||||||
NoticeTypeCode string `xml:"NoticeTypeCode"`
|
NoticeTypeCode NoticeTypeCodeField `xml:"NoticeTypeCode,omitempty"`
|
||||||
NoticeLanguageCode string `xml:"NoticeLanguageCode"`
|
NoticeLanguageCode string `xml:"NoticeLanguageCode"`
|
||||||
Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"`
|
Extensions *UBLExtensions `xml:"UBLExtensions,omitempty"`
|
||||||
ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"`
|
ContractingParty *ContractingParty `xml:"ContractingParty,omitempty"`
|
||||||
@@ -90,7 +98,274 @@ func (pin PriorInformationNotice) GetID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pin PriorInformationNotice) GetNoticeType() string {
|
func (pin PriorInformationNotice) GetNoticeType() string {
|
||||||
return pin.NoticeTypeCode
|
return strings.TrimSpace(pin.NoticeTypeCode.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNoticeFormType returns the form type from listName on cbc:NoticeTypeCode (BT-03), e.g. "planning".
|
||||||
|
func (pin PriorInformationNotice) GetNoticeFormType() string {
|
||||||
|
return strings.TrimSpace(pin.NoticeTypeCode.ListName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pin PriorInformationNotice) firstLot() *ProcurementProjectLot {
|
||||||
|
if len(pin.ProcurementProjectLot) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &pin.ProcurementProjectLot[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPublicationInfo returns publication information if available.
|
||||||
|
func (pin PriorInformationNotice) GetPublicationInfo() *Publication {
|
||||||
|
if ef := ublEformsExtension(pin.Extensions); ef != nil {
|
||||||
|
return ef.Publication
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrganizations returns organizations from extensions.
|
||||||
|
func (pin PriorInformationNotice) GetOrganizations() *Organizations {
|
||||||
|
if ef := ublEformsExtension(pin.Extensions); ef != nil {
|
||||||
|
return ef.Organizations
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNoticeSubType returns the notice subtype code from extensions.
|
||||||
|
func (pin PriorInformationNotice) GetNoticeSubType() string {
|
||||||
|
if ef := ublEformsExtension(pin.Extensions); ef != nil && ef.NoticeSubType != nil {
|
||||||
|
return ef.NoticeSubType.SubTypeCode
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNoticePublicationID returns the notice publication ID from extensions.
|
||||||
|
func (pin PriorInformationNotice) GetNoticePublicationID() string {
|
||||||
|
pub := pin.GetPublicationInfo()
|
||||||
|
if pub != nil {
|
||||||
|
return pub.NoticePublicationID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOJSID returns the OJS gazette ID from extensions.
|
||||||
|
func (pin PriorInformationNotice) GetOJSID() string {
|
||||||
|
pub := pin.GetPublicationInfo()
|
||||||
|
if pub != nil {
|
||||||
|
return pub.GazetteID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPublicationDate returns the publication date from extensions.
|
||||||
|
func (pin PriorInformationNotice) GetPublicationDate() string {
|
||||||
|
pub := pin.GetPublicationInfo()
|
||||||
|
if pub != nil {
|
||||||
|
return pub.PublicationDate
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrganizationByID returns an organization by its ID.
|
||||||
|
func (pin PriorInformationNotice) GetOrganizationByID(orgID string) *Organization {
|
||||||
|
orgs := pin.GetOrganizations()
|
||||||
|
if orgs == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, org := range orgs.Organization {
|
||||||
|
if org.Company != nil &&
|
||||||
|
org.Company.PartyIdentification != nil &&
|
||||||
|
org.Company.PartyIdentification.ID == orgID {
|
||||||
|
return &org
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBuyerID returns the buyer organization ID from contracting party.
|
||||||
|
func (pin PriorInformationNotice) GetBuyerID() string {
|
||||||
|
if pin.ContractingParty != nil &&
|
||||||
|
pin.ContractingParty.Party != nil &&
|
||||||
|
pin.ContractingParty.Party.PartyIdentification != nil {
|
||||||
|
return pin.ContractingParty.Party.PartyIdentification.ID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReviewOrganizationID returns the review organization ID from appeal terms.
|
||||||
|
func (pin PriorInformationNotice) GetReviewOrganizationID() string {
|
||||||
|
if pin.firstLot() != nil &&
|
||||||
|
pin.firstLot().TenderingTerms != nil &&
|
||||||
|
pin.firstLot().TenderingTerms.AppealTerms != nil &&
|
||||||
|
pin.firstLot().TenderingTerms.AppealTerms.AppealReceiverParty != nil &&
|
||||||
|
pin.firstLot().TenderingTerms.AppealTerms.AppealReceiverParty.PartyIdentification != nil {
|
||||||
|
return pin.firstLot().TenderingTerms.AppealTerms.AppealReceiverParty.PartyIdentification.ID
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProcedureCode returns the type of procedure.
|
||||||
|
func (pin PriorInformationNotice) GetProcedureCode() string {
|
||||||
|
if pin.TenderingProcess != nil {
|
||||||
|
return pin.TenderingProcess.ProcedureCode
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProcurementTitle returns the procurement project title.
|
||||||
|
func (pin PriorInformationNotice) GetProcurementTitle() string {
|
||||||
|
if pin.ProcurementProject != nil && strings.TrimSpace(pin.ProcurementProject.Name) != "" {
|
||||||
|
return pin.ProcurementProject.Name
|
||||||
|
}
|
||||||
|
if pin.firstLot() != nil && pin.firstLot().ProcurementProject != nil {
|
||||||
|
return pin.firstLot().ProcurementProject.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProcurementDescription returns the procurement project description.
|
||||||
|
func (pin PriorInformationNotice) GetProcurementDescription() string {
|
||||||
|
if pin.ProcurementProject != nil && strings.TrimSpace(pin.ProcurementProject.Description) != "" {
|
||||||
|
return pin.ProcurementProject.Description
|
||||||
|
}
|
||||||
|
if pin.firstLot() != nil && pin.firstLot().ProcurementProject != nil {
|
||||||
|
return pin.firstLot().ProcurementProject.Description
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContractNature returns the main nature of the contract.
|
||||||
|
func (pin PriorInformationNotice) GetContractNature() string {
|
||||||
|
if pin.ProcurementProject != nil && strings.TrimSpace(pin.ProcurementProject.ProcurementTypeCode) != "" {
|
||||||
|
return pin.ProcurementProject.ProcurementTypeCode
|
||||||
|
}
|
||||||
|
if pin.firstLot() != nil && pin.firstLot().ProcurementProject != nil {
|
||||||
|
return pin.firstLot().ProcurementProject.ProcurementTypeCode
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMainClassification returns the main CPV classification code.
|
||||||
|
func (pin PriorInformationNotice) GetMainClassification() string {
|
||||||
|
if pin.ProcurementProject != nil &&
|
||||||
|
pin.ProcurementProject.MainCommodityClassification != nil {
|
||||||
|
return pin.ProcurementProject.MainCommodityClassification.ItemClassificationCode
|
||||||
|
}
|
||||||
|
if lot := pin.firstLot(); lot != nil && lot.ProcurementProject != nil &&
|
||||||
|
lot.ProcurementProject.MainCommodityClassification != nil {
|
||||||
|
return lot.ProcurementProject.MainCommodityClassification.ItemClassificationCode
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMainClassificationDescription returns the CPV / item classification description when present in the notice XML.
|
||||||
|
func (pin PriorInformationNotice) GetMainClassificationDescription() string {
|
||||||
|
if pin.ProcurementProject != nil {
|
||||||
|
if s := mainCommodityClassificationLabel(pin.ProcurementProject.MainCommodityClassification); s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lot := pin.firstLot(); lot != nil && lot.ProcurementProject != nil {
|
||||||
|
return mainCommodityClassificationLabel(lot.ProcurementProject.MainCommodityClassification)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlaceOfPerformance returns the place of performance.
|
||||||
|
func (pin PriorInformationNotice) GetPlaceOfPerformance() string {
|
||||||
|
if pin.ProcurementProject != nil &&
|
||||||
|
pin.ProcurementProject.RealizedLocation != nil &&
|
||||||
|
pin.ProcurementProject.RealizedLocation.Address != nil {
|
||||||
|
return pin.ProcurementProject.RealizedLocation.Address.Region
|
||||||
|
}
|
||||||
|
if lot := pin.firstLot(); lot != nil && lot.ProcurementProject != nil &&
|
||||||
|
lot.ProcurementProject.RealizedLocation != nil &&
|
||||||
|
lot.ProcurementProject.RealizedLocation.Address != nil {
|
||||||
|
return lot.ProcurementProject.RealizedLocation.Address.Region
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSelectionCriteria returns selection criteria from extensions.
|
||||||
|
func (pin PriorInformationNotice) GetSelectionCriteria() []SelectionCriteria {
|
||||||
|
if ef := ublEformsExtension(pin.Extensions); ef != nil {
|
||||||
|
return ef.SelectionCriteria
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOfficialLanguages returns official languages from tender terms extensions.
|
||||||
|
func (pin PriorInformationNotice) GetOfficialLanguages() []string {
|
||||||
|
if pin.firstLot() != nil &&
|
||||||
|
pin.firstLot().TenderingTerms != nil &&
|
||||||
|
len(pin.firstLot().TenderingTerms.CallForTendersDocumentReference) > 0 &&
|
||||||
|
pin.firstLot().TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions != nil &&
|
||||||
|
pin.firstLot().TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension != nil &&
|
||||||
|
pin.firstLot().TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension.ExtensionContent != nil &&
|
||||||
|
pin.firstLot().TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension.ExtensionContent.EformsExtension != nil &&
|
||||||
|
pin.firstLot().TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension.ExtensionContent.EformsExtension.OfficialLanguages != nil {
|
||||||
|
|
||||||
|
langs := pin.firstLot().TenderingTerms.CallForTendersDocumentReference[0].UBLExtensions.UBLExtension.ExtensionContent.EformsExtension.OfficialLanguages.Language
|
||||||
|
languages := make([]string, len(langs))
|
||||||
|
for i, lang := range langs {
|
||||||
|
languages[i] = lang.ID
|
||||||
|
}
|
||||||
|
return languages
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDocumentURI returns the document URI from call for tenders document reference.
|
||||||
|
func (pin PriorInformationNotice) GetDocumentURI() string {
|
||||||
|
if pin.firstLot() != nil &&
|
||||||
|
pin.firstLot().TenderingTerms != nil &&
|
||||||
|
len(pin.firstLot().TenderingTerms.CallForTendersDocumentReference) > 0 &&
|
||||||
|
pin.firstLot().TenderingTerms.CallForTendersDocumentReference[0].Attachment != nil &&
|
||||||
|
pin.firstLot().TenderingTerms.CallForTendersDocumentReference[0].Attachment.ExternalReference != nil {
|
||||||
|
return pin.firstLot().TenderingTerms.CallForTendersDocumentReference[0].Attachment.ExternalReference.URI
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTenderDeadline returns the tender submission deadline.
|
||||||
|
func (pin PriorInformationNotice) GetTenderDeadline() string {
|
||||||
|
if pin.firstLot() != nil &&
|
||||||
|
pin.firstLot().TenderingProcess != nil &&
|
||||||
|
pin.firstLot().TenderingProcess.TenderSubmissionDeadlinePeriod != nil {
|
||||||
|
return pin.firstLot().TenderingProcess.TenderSubmissionDeadlinePeriod.EndDate
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBudget returns the estimated contract amount and currency.
|
||||||
|
func (pin PriorInformationNotice) GetBudget() (amount string, currency string) {
|
||||||
|
if pin.firstLot() != nil &&
|
||||||
|
pin.firstLot().ProcurementProject != nil &&
|
||||||
|
pin.firstLot().ProcurementProject.RequestedTenderTotal != nil {
|
||||||
|
contractAmount := pin.firstLot().ProcurementProject.RequestedTenderTotal.EstimatedOverallContractAmount
|
||||||
|
return contractAmount.Text, contractAmount.CurrencyID
|
||||||
|
}
|
||||||
|
if pin.ProcurementProject != nil && pin.ProcurementProject.RequestedTenderTotal != nil {
|
||||||
|
a := pin.ProcurementProject.RequestedTenderTotal.EstimatedOverallContractAmount
|
||||||
|
return a.Text, a.CurrencyID
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDuration returns the contract duration and unit.
|
||||||
|
func (pin PriorInformationNotice) GetDuration() (duration string, unit string) {
|
||||||
|
if pin.firstLot() != nil &&
|
||||||
|
pin.firstLot().ProcurementProject != nil &&
|
||||||
|
pin.firstLot().ProcurementProject.PlannedPeriod != nil &&
|
||||||
|
pin.firstLot().ProcurementProject.PlannedPeriod.DurationMeasure != nil {
|
||||||
|
durationMeasure := pin.firstLot().ProcurementProject.PlannedPeriod.DurationMeasure
|
||||||
|
return durationMeasure.Text, durationMeasure.UnitCode
|
||||||
|
}
|
||||||
|
if pin.ProcurementProject != nil &&
|
||||||
|
pin.ProcurementProject.PlannedPeriod != nil &&
|
||||||
|
pin.ProcurementProject.PlannedPeriod.DurationMeasure != nil {
|
||||||
|
dm := pin.ProcurementProject.PlannedPeriod.DurationMeasure
|
||||||
|
return dm.Text, dm.UnitCode
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pin PriorInformationNotice) GetLanguage() string {
|
func (pin PriorInformationNotice) GetLanguage() string {
|
||||||
@@ -1288,6 +1563,9 @@ type CurrencyText struct {
|
|||||||
type MainCommodityClassification struct {
|
type MainCommodityClassification struct {
|
||||||
XMLName xml.Name `xml:"MainCommodityClassification"`
|
XMLName xml.Name `xml:"MainCommodityClassification"`
|
||||||
ItemClassificationCode string `xml:"ItemClassificationCode"`
|
ItemClassificationCode string `xml:"ItemClassificationCode"`
|
||||||
|
ItemClassificationName string `xml:"ItemClassificationName,omitempty"`
|
||||||
|
ItemClassificationDesc string `xml:"ItemClassificationDescription,omitempty"`
|
||||||
|
Name string `xml:"Name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdditionalCommodityClassification contains additional commodity classification
|
// AdditionalCommodityClassification contains additional commodity classification
|
||||||
@@ -1451,34 +1729,24 @@ func (cn ContractNotice) GetLanguage() string {
|
|||||||
|
|
||||||
// GetPublicationInfo returns publication information if available
|
// GetPublicationInfo returns publication information if available
|
||||||
func (cn ContractNotice) GetPublicationInfo() *Publication {
|
func (cn ContractNotice) GetPublicationInfo() *Publication {
|
||||||
if cn.Extensions != nil &&
|
if ef := ublEformsExtension(cn.Extensions); ef != nil {
|
||||||
cn.Extensions.UBLExtension != nil &&
|
return ef.Publication
|
||||||
cn.Extensions.UBLExtension.ExtensionContent != nil &&
|
|
||||||
cn.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil {
|
|
||||||
return cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.Publication
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOrganizations returns organizations from extensions
|
// GetOrganizations returns organizations from extensions
|
||||||
func (cn ContractNotice) GetOrganizations() *Organizations {
|
func (cn ContractNotice) GetOrganizations() *Organizations {
|
||||||
if cn.Extensions != nil &&
|
if ef := ublEformsExtension(cn.Extensions); ef != nil {
|
||||||
cn.Extensions.UBLExtension != nil &&
|
return ef.Organizations
|
||||||
cn.Extensions.UBLExtension.ExtensionContent != nil &&
|
|
||||||
cn.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil {
|
|
||||||
return cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.Organizations
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNoticeSubType returns the notice subtype code from extensions
|
// GetNoticeSubType returns the notice subtype code from extensions
|
||||||
func (cn ContractNotice) GetNoticeSubType() string {
|
func (cn ContractNotice) GetNoticeSubType() string {
|
||||||
if cn.Extensions != nil &&
|
if ef := ublEformsExtension(cn.Extensions); ef != nil && ef.NoticeSubType != nil {
|
||||||
cn.Extensions.UBLExtension != nil &&
|
return ef.NoticeSubType.SubTypeCode
|
||||||
cn.Extensions.UBLExtension.ExtensionContent != nil &&
|
|
||||||
cn.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil &&
|
|
||||||
cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.NoticeSubType != nil {
|
|
||||||
return cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.NoticeSubType.SubTypeCode
|
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -1592,6 +1860,26 @@ func (cn ContractNotice) GetMainClassification() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mainCommodityClassificationLabel(m *MainCommodityClassification) string {
|
||||||
|
if m == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, s := range []string{m.ItemClassificationName, m.ItemClassificationDesc, m.Name} {
|
||||||
|
if v := strings.TrimSpace(s); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMainClassificationDescription returns the CPV / item classification description when present in the notice XML.
|
||||||
|
func (cn ContractNotice) GetMainClassificationDescription() string {
|
||||||
|
if cn.ProcurementProject == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return mainCommodityClassificationLabel(cn.ProcurementProject.MainCommodityClassification)
|
||||||
|
}
|
||||||
|
|
||||||
// GetPlaceOfPerformance returns the place of performance
|
// GetPlaceOfPerformance returns the place of performance
|
||||||
func (cn ContractNotice) GetPlaceOfPerformance() string {
|
func (cn ContractNotice) GetPlaceOfPerformance() string {
|
||||||
if cn.ProcurementProject != nil &&
|
if cn.ProcurementProject != nil &&
|
||||||
@@ -1612,11 +1900,8 @@ func (cn ContractNotice) GetLotID() string {
|
|||||||
|
|
||||||
// GetSelectionCriteria returns selection criteria from extensions
|
// GetSelectionCriteria returns selection criteria from extensions
|
||||||
func (cn ContractNotice) GetSelectionCriteria() []SelectionCriteria {
|
func (cn ContractNotice) GetSelectionCriteria() []SelectionCriteria {
|
||||||
if cn.Extensions != nil &&
|
if ef := ublEformsExtension(cn.Extensions); ef != nil {
|
||||||
cn.Extensions.UBLExtension != nil &&
|
return ef.SelectionCriteria
|
||||||
cn.Extensions.UBLExtension.ExtensionContent != nil &&
|
|
||||||
cn.Extensions.UBLExtension.ExtensionContent.EformsExtension != nil {
|
|
||||||
return cn.Extensions.UBLExtension.ExtensionContent.EformsExtension.SelectionCriteria
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1913,6 +2198,19 @@ func (can ContractAwardNotice) GetMainClassification() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMainClassificationDescription returns the CPV / item classification description when present in the notice XML.
|
||||||
|
func (can ContractAwardNotice) GetMainClassificationDescription() string {
|
||||||
|
if can.ProcurementProject != nil {
|
||||||
|
if s := mainCommodityClassificationLabel(can.ProcurementProject.MainCommodityClassification); s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lot := can.firstAwardLot(); lot != nil && lot.ProcurementProject != nil {
|
||||||
|
return mainCommodityClassificationLabel(lot.ProcurementProject.MainCommodityClassification)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// GetTotalAwardValue returns the total award amount and currency
|
// GetTotalAwardValue returns the total award amount and currency
|
||||||
func (can ContractAwardNotice) GetTotalAwardValue() (amount string, currency string) {
|
func (can ContractAwardNotice) GetTotalAwardValue() (amount string, currency string) {
|
||||||
noticeResult := can.GetNoticeResult()
|
noticeResult := can.GetNoticeResult()
|
||||||
|
|||||||
Reference in New Issue
Block a user