added missing tender data to response
This commit is contained in:
+60
-58
@@ -37,56 +37,57 @@ type Tender struct {
|
||||
NoticePublicationID string `bson:"notice_publication_id" json:"notice_publication_id"`
|
||||
// RelatedNoticePublicationIDs lists every TED notice publication id that has contributed to this tender
|
||||
// (same procedure / contract_folder_id). Ordered oldest-first; the latest is also reflected in NoticePublicationID.
|
||||
RelatedNoticePublicationIDs []string `bson:"related_notice_publication_ids,omitempty" json:"related_notice_publication_ids,omitempty"`
|
||||
ContractFolderID string `bson:"contract_folder_id" json:"contract_folder_id"`
|
||||
RelatedNoticePublicationIDs []string `bson:"related_notice_publication_ids,omitempty" json:"related_notice_publication_ids,omitempty"`
|
||||
ContractFolderID string `bson:"contract_folder_id" json:"contract_folder_id"`
|
||||
// ProcurementProjectID is UBL ProcurementProject/ID — merges per-lot CAN/CN notices into one tender.
|
||||
ProcurementProjectID string `bson:"procurement_project_id,omitempty" json:"procurement_project_id,omitempty"`
|
||||
NoticeTypeCode string `bson:"notice_type_code" json:"notice_type_code"`
|
||||
FormType string `bson:"form_type,omitempty" json:"form_type,omitempty"`
|
||||
NoticeSubTypeCode string `bson:"notice_sub_type_code" json:"notice_sub_type_code"`
|
||||
NoticeIdentifier string `bson:"notice_identifier,omitempty" json:"notice_identifier,omitempty"`
|
||||
NoticeVersion string `bson:"notice_version,omitempty" json:"notice_version,omitempty"`
|
||||
NoticeDispatchAt int64 `bson:"notice_dispatch_at,omitempty" json:"notice_dispatch_at,omitempty"`
|
||||
ESenderDispatchAt int64 `bson:"esender_dispatch_at,omitempty" json:"esender_dispatch_at,omitempty"`
|
||||
NoticeLanguageCode string `bson:"notice_language_code" json:"notice_language_code"`
|
||||
IssueDate int64 `bson:"issue_date" json:"issue_date"`
|
||||
IssueTime int64 `bson:"issue_time" json:"issue_time"`
|
||||
TenderDeadline int64 `bson:"tender_deadline" json:"tender_deadline"`
|
||||
PublicationDate int64 `bson:"publication_date" json:"publication_date"`
|
||||
SubmissionDeadline int64 `bson:"submission_deadline" json:"submission_deadline"`
|
||||
ApplicationDeadline int64 `bson:"application_deadline" json:"application_deadline"`
|
||||
GazetteID string `bson:"gazette_id" json:"gazette_id"`
|
||||
Title string `bson:"title" json:"title"`
|
||||
Description string `bson:"description" json:"description"`
|
||||
ProcurementTypeCode string `bson:"procurement_type_code" json:"procurement_type_code"`
|
||||
ProcedureCode string `bson:"procedure_code" json:"procedure_code"`
|
||||
MainClassification string `bson:"main_classification" json:"main_classification"`
|
||||
AdditionalClassifications []string `bson:"additional_classifications" json:"additional_classifications"`
|
||||
EstimatedValue float64 `bson:"estimated_value" json:"estimated_value"`
|
||||
Currency string `bson:"currency" json:"currency"`
|
||||
Duration string `bson:"duration" json:"duration"`
|
||||
DurationUnit string `bson:"duration_unit" json:"duration_unit"`
|
||||
PlaceOfPerformance string `bson:"place_of_performance" json:"place_of_performance"`
|
||||
CountryCode string `bson:"country_code" json:"country_code"`
|
||||
RegionCode string `bson:"region_code" json:"region_code"`
|
||||
CityName string `bson:"city_name" json:"city_name"`
|
||||
PostalCode string `bson:"postal_code" json:"postal_code"`
|
||||
DocumentURI string `bson:"document_uri" json:"document_uri"`
|
||||
BuyerProfileURL string `bson:"buyer_profile_url,omitempty" json:"buyer_profile_url,omitempty"`
|
||||
ProcurementLots []ProcurementLot `bson:"procurement_lots,omitempty" json:"procurement_lots,omitempty"`
|
||||
TenderURL string `bson:"tender_url" json:"tender_url"`
|
||||
SubmissionURL string `bson:"submission_url" json:"submission_url"`
|
||||
BuyerOrganization *Organization `bson:"buyer_organization" json:"buyer_organization"`
|
||||
ReviewOrganization *Organization `bson:"review_organization,omitempty" json:"review_organization,omitempty"`
|
||||
Organizations []Organization `bson:"organizations" json:"organizations"`
|
||||
SelectionCriteria []SelectionCriterion `bson:"selection_criteria" json:"selection_criteria"`
|
||||
OfficialLanguages []string `bson:"official_languages" json:"official_languages"`
|
||||
Status TenderStatus `bson:"status" json:"status"`
|
||||
Source TenderSource `bson:"source" json:"source"`
|
||||
SourceFileURL string `bson:"source_file_url" json:"source_file_url"`
|
||||
SourceFileName string `bson:"source_file_name" json:"source_file_name"`
|
||||
ContentXML string `bson:"content_xml,omitempty" json:"content_xml,omitempty"`
|
||||
ProcessingMetadata ProcessingMetadata `bson:"processing_metadata" json:"processing_metadata"`
|
||||
ProcurementProjectID string `bson:"procurement_project_id,omitempty" json:"procurement_project_id,omitempty"`
|
||||
NoticeTypeCode string `bson:"notice_type_code" json:"notice_type_code"`
|
||||
FormType string `bson:"form_type,omitempty" json:"form_type,omitempty"`
|
||||
NoticeSubTypeCode string `bson:"notice_sub_type_code" json:"notice_sub_type_code"`
|
||||
NoticeIdentifier string `bson:"notice_identifier,omitempty" json:"notice_identifier,omitempty"`
|
||||
NoticeVersion string `bson:"notice_version,omitempty" json:"notice_version,omitempty"`
|
||||
NoticeDispatchAt int64 `bson:"notice_dispatch_at,omitempty" json:"notice_dispatch_at,omitempty"`
|
||||
ESenderDispatchAt int64 `bson:"esender_dispatch_at,omitempty" json:"esender_dispatch_at,omitempty"`
|
||||
NoticeLanguageCode string `bson:"notice_language_code" json:"notice_language_code"`
|
||||
IssueDate int64 `bson:"issue_date" json:"issue_date"`
|
||||
IssueTime int64 `bson:"issue_time" json:"issue_time"`
|
||||
TenderDeadline int64 `bson:"tender_deadline" json:"tender_deadline"`
|
||||
PublicationDate int64 `bson:"publication_date" json:"publication_date"`
|
||||
SubmissionDeadline int64 `bson:"submission_deadline" json:"submission_deadline"`
|
||||
ApplicationDeadline int64 `bson:"application_deadline" json:"application_deadline"`
|
||||
GazetteID string `bson:"gazette_id" json:"gazette_id"`
|
||||
Title string `bson:"title" json:"title"`
|
||||
Description string `bson:"description" json:"description"`
|
||||
ProcurementTypeCode string `bson:"procurement_type_code" json:"procurement_type_code"`
|
||||
ProcedureCode string `bson:"procedure_code" json:"procedure_code"`
|
||||
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"`
|
||||
EstimatedValue float64 `bson:"estimated_value" json:"estimated_value"`
|
||||
Currency string `bson:"currency" json:"currency"`
|
||||
Duration string `bson:"duration" json:"duration"`
|
||||
DurationUnit string `bson:"duration_unit" json:"duration_unit"`
|
||||
PlaceOfPerformance string `bson:"place_of_performance" json:"place_of_performance"`
|
||||
CountryCode string `bson:"country_code" json:"country_code"`
|
||||
RegionCode string `bson:"region_code" json:"region_code"`
|
||||
CityName string `bson:"city_name" json:"city_name"`
|
||||
PostalCode string `bson:"postal_code" json:"postal_code"`
|
||||
DocumentURI string `bson:"document_uri" json:"document_uri"`
|
||||
BuyerProfileURL string `bson:"buyer_profile_url,omitempty" json:"buyer_profile_url,omitempty"`
|
||||
ProcurementLots []ProcurementLot `bson:"procurement_lots,omitempty" json:"procurement_lots,omitempty"`
|
||||
TenderURL string `bson:"tender_url" json:"tender_url"`
|
||||
SubmissionURL string `bson:"submission_url" json:"submission_url"`
|
||||
BuyerOrganization *Organization `bson:"buyer_organization" json:"buyer_organization"`
|
||||
ReviewOrganization *Organization `bson:"review_organization,omitempty" json:"review_organization,omitempty"`
|
||||
Organizations []Organization `bson:"organizations" json:"organizations"`
|
||||
SelectionCriteria []SelectionCriterion `bson:"selection_criteria" json:"selection_criteria"`
|
||||
OfficialLanguages []string `bson:"official_languages" json:"official_languages"`
|
||||
Status TenderStatus `bson:"status" json:"status"`
|
||||
Source TenderSource `bson:"source" json:"source"`
|
||||
SourceFileURL string `bson:"source_file_url" json:"source_file_url"`
|
||||
SourceFileName string `bson:"source_file_name" json:"source_file_name"`
|
||||
ContentXML string `bson:"content_xml,omitempty" json:"content_xml,omitempty"`
|
||||
ProcessingMetadata ProcessingMetadata `bson:"processing_metadata" json:"processing_metadata"`
|
||||
// AIOverallSummary optional denormalized overall summary for search/list (e.g. synced from AI pipeline storage).
|
||||
AIOverallSummary string `bson:"ai_overall_summary,omitempty" json:"ai_overall_summary,omitempty"`
|
||||
TenderID string `bson:"tender_id" json:"tender_id"` // PBL Project Number (SCDYYNNN format)
|
||||
@@ -150,15 +151,16 @@ type SelectionCriterion struct {
|
||||
|
||||
// ProcurementLot captures per-lot procurement fields from TED notices.
|
||||
type ProcurementLot struct {
|
||||
LotID string `bson:"lot_id,omitempty" json:"lot_id,omitempty"`
|
||||
Title string `bson:"title,omitempty" json:"title,omitempty"`
|
||||
Description string `bson:"description,omitempty" json:"description,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"`
|
||||
AdditionalClassifications []string `bson:"additional_classifications,omitempty" json:"additional_classifications,omitempty"`
|
||||
Duration string `bson:"duration,omitempty" json:"duration,omitempty"`
|
||||
EstimatedValue float64 `bson:"estimated_value,omitempty" json:"estimated_value,omitempty"`
|
||||
Currency string `bson:"currency,omitempty" json:"currency,omitempty"`
|
||||
LotID string `bson:"lot_id,omitempty" json:"lot_id,omitempty"`
|
||||
Title string `bson:"title,omitempty" json:"title,omitempty"`
|
||||
Description string `bson:"description,omitempty" json:"description,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"`
|
||||
MainClassificationDescription string `bson:"main_classification_description,omitempty" json:"main_classification_description,omitempty"`
|
||||
AdditionalClassifications []string `bson:"additional_classifications,omitempty" json:"additional_classifications,omitempty"`
|
||||
Duration string `bson:"duration,omitempty" json:"duration,omitempty"`
|
||||
EstimatedValue float64 `bson:"estimated_value,omitempty" json:"estimated_value,omitempty"`
|
||||
Currency string `bson:"currency,omitempty" json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
// ProcessingMetadata contains metadata about how the tender was processed
|
||||
|
||||
+402
-57
@@ -1,6 +1,11 @@
|
||||
package tender
|
||||
|
||||
import "tm/pkg/response"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// UpdateTenderRequest represents a request to update an existing tender
|
||||
type UpdateTenderRequest struct {
|
||||
@@ -64,33 +69,135 @@ type SearchResponse struct {
|
||||
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 {
|
||||
ID string `json:"id"`
|
||||
NoticePublicationID string `json:"notice_publication_id"`
|
||||
RelatedNoticePublicationIDs []string `json:"related_notice_publication_ids,omitempty"`
|
||||
NoticeTypeCode string `json:"notice_type_code"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
ProcurementTypeCode string `json:"procurement_type_code"`
|
||||
ProcedureCode string `json:"procedure_code"`
|
||||
MainClassification string `json:"main_classification"`
|
||||
EstimatedValue float64 `json:"estimated_value"`
|
||||
Currency string `json:"currency"`
|
||||
Duration string `json:"duration"`
|
||||
DurationUnit string `json:"duration_unit"`
|
||||
PublicationDate int64 `json:"publication_date"`
|
||||
TenderDeadline int64 `json:"tender_deadline"`
|
||||
SubmissionDeadline int64 `json:"submission_deadline"`
|
||||
ApplicationDeadline int64 `json:"application_deadline"`
|
||||
SubmissionURL string `json:"submission_url"`
|
||||
CountryCode string `json:"country_code"`
|
||||
BuyerOrganization *OrganizationResponse `json:"buyer_organization"`
|
||||
Status TenderStatus `json:"status"`
|
||||
TenderID string `json:"tender_id"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
OverallSummary string `json:"overall_summary,omitempty"` // AI-generated summary from the pipeline
|
||||
Language string `json:"language,omitempty"`
|
||||
ID string `json:"id"`
|
||||
NoticePublicationID string `json:"notice_publication_id"`
|
||||
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"`
|
||||
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"`
|
||||
Description string `json:"description"`
|
||||
|
||||
ProcurementTypeCode string `json:"procurement_type_code"`
|
||||
ProcedureCode string `json:"procedure_code"`
|
||||
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"`
|
||||
Currency string `json:"currency"`
|
||||
EstimatedValueVATBasis string `json:"estimated_value_vat_basis,omitempty"`
|
||||
|
||||
Duration string `json:"duration"`
|
||||
DurationUnit string `json:"duration_unit"`
|
||||
|
||||
PublicationDate int64 `json:"publication_date"`
|
||||
TenderDeadline int64 `json:"tender_deadline"`
|
||||
SubmissionDeadline int64 `json:"submission_deadline"`
|
||||
ApplicationDeadline int64 `json:"application_deadline"`
|
||||
SubmissionURL string `json:"submission_url"`
|
||||
|
||||
PlaceOfPerformance string `json:"place_of_performance,omitempty"`
|
||||
CountryCode string `json:"country_code"`
|
||||
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"`
|
||||
Source TenderSource `json:"source,omitempty"`
|
||||
TenderID string `json:"tender_id"`
|
||||
ProjectName string `json:"project_name,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
|
||||
OverallSummary string `json:"overall_summary,omitempty"`
|
||||
Language string `json:"language,omitempty"`
|
||||
}
|
||||
|
||||
// TenderDocumentResponse represents a scraped tender document available for download.
|
||||
@@ -107,9 +214,10 @@ type TenderDocumentsResponse struct {
|
||||
Documents []TenderDocumentResponse `json:"documents"`
|
||||
}
|
||||
|
||||
type OrganizationResponse struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
const (
|
||||
enrichmentMainCPVDescriptionKey = "main_classification_description"
|
||||
vatBasisUnspecified = "unspecified"
|
||||
)
|
||||
|
||||
// ToTenderResponse converts a Tender entity to 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.
|
||||
// When language is provided and translation exists, translated title/description are returned.
|
||||
func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
|
||||
|
||||
org := &OrganizationResponse{}
|
||||
if t.BuyerOrganization != nil {
|
||||
org.Name = t.BuyerOrganization.Name
|
||||
}
|
||||
title := t.Title
|
||||
description := t.Description
|
||||
usedLanguage := ""
|
||||
@@ -135,36 +238,278 @@ 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{
|
||||
ID: t.ID.Hex(),
|
||||
NoticePublicationID: t.NoticePublicationID,
|
||||
RelatedNoticePublicationIDs: t.RelatedNoticePublicationIDs,
|
||||
NoticeTypeCode: t.NoticeTypeCode,
|
||||
Title: title,
|
||||
Description: description,
|
||||
ProcurementTypeCode: t.ProcurementTypeCode,
|
||||
ProcedureCode: t.ProcedureCode,
|
||||
MainClassification: t.MainClassification,
|
||||
EstimatedValue: t.EstimatedValue,
|
||||
Currency: t.Currency,
|
||||
Duration: t.Duration,
|
||||
DurationUnit: t.DurationUnit,
|
||||
PublicationDate: t.PublicationDate,
|
||||
TenderDeadline: t.TenderDeadline,
|
||||
SubmissionDeadline: t.SubmissionDeadline,
|
||||
ApplicationDeadline: t.ApplicationDeadline,
|
||||
CountryCode: t.CountryCode,
|
||||
SubmissionURL: t.SubmissionURL,
|
||||
BuyerOrganization: org,
|
||||
Status: t.Status,
|
||||
TenderID: t.TenderID,
|
||||
CreatedAt: t.CreatedAt,
|
||||
Language: usedLanguage,
|
||||
RelatedNoticePublicationIDs: append([]string(nil), t.RelatedNoticePublicationIDs...),
|
||||
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,
|
||||
Description: description,
|
||||
ProcurementTypeCode: strings.TrimSpace(t.ProcurementTypeCode),
|
||||
ProcedureCode: strings.TrimSpace(t.ProcedureCode),
|
||||
MainClassification: mainCode,
|
||||
MainClassificationDescription: mainDesc,
|
||||
MainClassificationDisplay: classificationDisplay(mainCode, mainDesc),
|
||||
AdditionalClassifications: append([]string(nil), t.AdditionalClassifications...),
|
||||
EstimatedValue: t.EstimatedValue,
|
||||
Currency: strings.TrimSpace(t.Currency),
|
||||
EstimatedValueVATBasis: vatBasisUnspecified,
|
||||
|
||||
Duration: t.Duration,
|
||||
DurationUnit: t.DurationUnit,
|
||||
|
||||
PublicationDate: t.PublicationDate,
|
||||
TenderDeadline: t.TenderDeadline,
|
||||
SubmissionDeadline: t.SubmissionDeadline,
|
||||
ApplicationDeadline: t.ApplicationDeadline,
|
||||
SubmissionURL: strings.TrimSpace(t.SubmissionURL),
|
||||
|
||||
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,
|
||||
Source: t.Source,
|
||||
TenderID: t.TenderID,
|
||||
ProjectName: strings.TrimSpace(t.ProjectName),
|
||||
CreatedAt: t.CreatedAt,
|
||||
Language: usedLanguage,
|
||||
}
|
||||
|
||||
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.
|
||||
type AITranslateRequest struct {
|
||||
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)
|
||||
}
|
||||
|
||||
resp := tender.ToResponse()
|
||||
if tr, ok := tender.Translations[s.defaultLanguage]; ok {
|
||||
resp.Title = tr.Title
|
||||
resp.Description = tr.Description
|
||||
resp.Language = s.defaultLanguage
|
||||
}
|
||||
resp := tender.ToResponseWithLanguage(s.defaultLanguage)
|
||||
|
||||
// Enrich with AI summary from MinIO storage (best-effort, non-blocking)
|
||||
s.enrichWithAISummary(ctx, tender, resp)
|
||||
@@ -284,7 +279,7 @@ func (s *tenderService) Update(ctx context.Context, req UpdateTenderRequest) (*T
|
||||
"tender_id": tender.ID,
|
||||
})
|
||||
|
||||
return tender.ToResponse(), nil
|
||||
return tender.ToResponseWithLanguage(s.defaultLanguage), nil
|
||||
}
|
||||
|
||||
// Delete deletes a tender
|
||||
|
||||
Reference in New Issue
Block a user