1edf42187d
continuous-integration/drone/push Build is passing
- Added functionality to retrieve merged AI recommendations for multiple companies, improving the relevance of tender suggestions based on company-specific data. - Introduced normalization functions to clean and deduplicate company IDs, ensuring accurate processing of recommendations. - Enhanced the company context resolution in customer middleware to support multiple assigned companies, improving the handling of company-specific requests. - Updated the tender recommendation logic to utilize the new merged recommendations and handle exclusions for rejected tenders accordingly. - Added unit tests to verify the new recommendation merging logic and company ID normalization, ensuring robust functionality. This update significantly enhances the tender recommendation process by allowing for more comprehensive and relevant suggestions based on multiple company contexts, improving user experience and satisfaction.
608 lines
26 KiB
Go
608 lines
26 KiB
Go
package tender
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"tm/pkg/response"
|
|
)
|
|
|
|
// UpdateTenderRequest represents a request to update an existing tender
|
|
type UpdateTenderRequest struct {
|
|
ID string `json:"id" valid:"required~Tender ID is required"`
|
|
Title *string `json:"title,omitempty" valid:"stringlength(1|500)~Title must be between 1 and 500 characters"`
|
|
Description *string `json:"description,omitempty" valid:"stringlength(0|5000)~Description must not exceed 5000 characters"`
|
|
Status *TenderStatus `json:"status,omitempty"`
|
|
EstimatedValue *float64 `json:"estimated_value,omitempty" valid:"range(0|999999999999)~Estimated value must be between 0 and 999999999999"`
|
|
Currency *string `json:"currency,omitempty" valid:"stringlength(3|3)~Currency must be 3 characters"`
|
|
TenderDeadline *int64 `json:"tender_deadline,omitempty"` // Unix milliseconds
|
|
}
|
|
|
|
// SearchForm represents a request to list tenders with pagination and filtering
|
|
type SearchForm struct {
|
|
Search *string `query:"q" valid:"optional"`
|
|
Title *string `query:"title" valid:"optional"`
|
|
Description *string `query:"description" valid:"optional"`
|
|
NoticeType *string `query:"notice_type" valid:"optional"`
|
|
NoticeTypes []string `query:"notice_types" valid:"optional"`
|
|
FormTypes []string `query:"form_types" valid:"optional"`
|
|
ProcurementType *string `query:"procurement_type" valid:"optional"`
|
|
Country *string `query:"country" valid:"optional"`
|
|
CountryCode *string `query:"country_code" valid:"optional"`
|
|
CountryCodes []string `query:"country_codes" valid:"optional"`
|
|
RegionCodes []string `query:"region_codes" valid:"optional"`
|
|
CompanyID *string `query:"company_id" valid:"optional"`
|
|
CompanyIDs []string `query:"-" valid:"optional"`
|
|
CustomerID *string `query:"customer_id" valid:"optional"`
|
|
Status []string `query:"status" valid:"optional"`
|
|
MainClassification *string `query:"main_classification" valid:"optional"`
|
|
Classifications []string `query:"classifications" valid:"optional"`
|
|
CpvCodes []string `query:"cpv_codes" valid:"optional"`
|
|
MinEstimatedValue *float64 `query:"min_estimated_value" valid:"optional"`
|
|
MaxEstimatedValue *float64 `query:"max_estimated_value" valid:"optional"`
|
|
Currency string `query:"currency" valid:"optional"`
|
|
CreatedAt *int64 `query:"created_at" valid:"optional"`
|
|
CreatedAtFrom *int64 `query:"created_at_from" valid:"optional"`
|
|
CreatedAtTo *int64 `query:"created_at_to" valid:"optional"`
|
|
TenderDeadline *int64 `query:"tender_deadline" valid:"optional"`
|
|
TenderDeadlineFrom *int64 `query:"tender_deadline_from" valid:"optional"`
|
|
TenderDeadlineTo *int64 `query:"tender_deadline_to" valid:"optional"`
|
|
DeadlineFrom *int64 `query:"deadline_from" valid:"optional"`
|
|
DeadlineTo *int64 `query:"deadline_to" valid:"optional"`
|
|
PublicationDate *int64 `query:"publication_date" valid:"optional"`
|
|
PublicationDateFrom *int64 `query:"publication_date_from" valid:"optional"`
|
|
PublicationDateTo *int64 `query:"publication_date_to" valid:"optional"`
|
|
SubmissionDeadline *int64 `query:"submission_deadline" valid:"optional"`
|
|
SubmissionDateAliasFrom *int64 `query:"submission_deadline_from" valid:"optional"`
|
|
SubmissionDateAliasTo *int64 `query:"submission_deadline_to" valid:"optional"`
|
|
SubmissionDateFrom *int64 `query:"submission_date_from" valid:"optional"`
|
|
SubmissionDateTo *int64 `query:"submission_date_to" valid:"optional"`
|
|
Source []string `query:"source" valid:"optional"`
|
|
BuyerOrganizationID *string `query:"buyer_organization_id" valid:"optional"`
|
|
NoticePublicationID *string `query:"notice_publication_id" valid:"optional"`
|
|
Languages []string `query:"languages" valid:"optional"`
|
|
OnlyActiveDeadlines bool `query:"only_active_deadlines" valid:"optional"`
|
|
Language *string `query:"lang" valid:"optional"`
|
|
DocumentsScraped bool `query:"documents_scraped" valid:"optional"`
|
|
// ContractFolderIDsWithDocuments is populated by the service from MinIO when DocumentsScraped is true.
|
|
ContractFolderIDsWithDocuments []string `query:"-" valid:"optional"`
|
|
// ExcludeRejectedTenders hides company-rejected tenders from recommendation results.
|
|
ExcludeRejectedTenders bool `query:"-" valid:"optional"`
|
|
}
|
|
|
|
// SearchResponse represents the response for listing tenders
|
|
type SearchResponse struct {
|
|
Tenders []TenderResponse `json:"tenders"`
|
|
Metadata *response.Meta `json:"-"`
|
|
}
|
|
|
|
// TenderScrapedDocumentsItem is a tender that has scraped documents stored in the database.
|
|
type TenderScrapedDocumentsItem struct {
|
|
TenderID string `json:"tender_id"`
|
|
NoticePublicationID string `json:"notice_publication_id"`
|
|
ContractFolderID string `json:"contract_folder_id"`
|
|
Title string `json:"title"`
|
|
DocumentsScrapedAt int64 `json:"documents_scraped_at,omitempty"` // Unix timestamp (seconds)
|
|
DocumentCount int `json:"document_count"`
|
|
}
|
|
|
|
// TendersWithScrapedDocumentsResponse is the paginated list of tenders with scraped documents.
|
|
type TendersWithScrapedDocumentsResponse struct {
|
|
Tenders []TenderScrapedDocumentsItem `json:"tenders"`
|
|
Metadata *response.Meta `json:"-"`
|
|
}
|
|
|
|
// ToTenderScrapedDocumentsItem maps a tender entity to a list item.
|
|
func (t *Tender) ToTenderScrapedDocumentsItem() TenderScrapedDocumentsItem {
|
|
if t == nil {
|
|
return TenderScrapedDocumentsItem{}
|
|
}
|
|
return TenderScrapedDocumentsItem{
|
|
TenderID: t.ID.Hex(),
|
|
NoticePublicationID: strings.TrimSpace(t.NoticePublicationID),
|
|
ContractFolderID: strings.TrimSpace(t.ContractFolderID),
|
|
Title: strings.TrimSpace(t.Title),
|
|
DocumentsScrapedAt: t.ProcessingMetadata.DocumentsScrapedAt,
|
|
DocumentCount: len(t.ScrapedDocuments),
|
|
}
|
|
}
|
|
|
|
// 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"`
|
|
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"`
|
|
|
|
Rank int `json:"rank,omitempty"`
|
|
Analysis string `json:"analysis,omitempty"`
|
|
ProcedureRef string `json:"procedure_ref,omitempty"`
|
|
}
|
|
|
|
// TenderDocumentResponse represents a scraped tender document available for download.
|
|
type TenderDocumentResponse struct {
|
|
Filename string `json:"filename"`
|
|
Size int64 `json:"size"`
|
|
LastModified int64 `json:"last_modified"`
|
|
DocumentType string `json:"document_type,omitempty"`
|
|
}
|
|
|
|
// TenderDocumentsResponse represents scraped documents for a tender.
|
|
type TenderDocumentsResponse struct {
|
|
TenderID string `json:"tender_id"`
|
|
Documents []TenderDocumentResponse `json:"documents"`
|
|
}
|
|
|
|
const (
|
|
enrichmentMainCPVDescriptionKey = "main_classification_description"
|
|
vatBasisUnspecified = "unspecified"
|
|
)
|
|
|
|
// ToTenderResponse converts a Tender entity to TenderResponse
|
|
func (t *Tender) ToResponse() *TenderResponse {
|
|
return t.ToResponseWithLanguage("")
|
|
}
|
|
|
|
// ToResponseWithLanguage converts a Tender entity to TenderResponse using Mongo fields.
|
|
// Detail endpoints overlay MinIO translations in the service layer.
|
|
func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse {
|
|
title := t.Title
|
|
description := t.Description
|
|
_ = language
|
|
|
|
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)
|
|
}
|
|
|
|
estimatedValue, currency := t.ResolvedEstimatedValueAndCurrency()
|
|
|
|
response := &TenderResponse{
|
|
ID: t.ID.Hex(),
|
|
NoticePublicationID: t.NoticePublicationID,
|
|
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: estimatedValue,
|
|
Currency: 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,
|
|
|
|
OverallSummary: strings.TrimSpace(t.AIOverallSummary),
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
// AITranslateResponse represents the response for AI translation trigger.
|
|
type AITranslateResponse struct {
|
|
ContractFolderID string `json:"contract_folder_id"`
|
|
NoticePublicationID string `json:"notice_publication_id"`
|
|
Language string `json:"language"`
|
|
TranslatedTitle string `json:"translated_title"`
|
|
TranslatedDescription string `json:"translated_description"`
|
|
}
|
|
|
|
// AISummaryResponse represents the response for getting an AI-generated summary
|
|
type AISummaryResponse struct {
|
|
NoticeID string `json:"notice_id"`
|
|
OverallSummary string `json:"overall_summary"`
|
|
Source string `json:"source"` // storage | pending | unavailable
|
|
}
|
|
|
|
// AISummarizeResponse represents the response for triggering on-demand AI summarization
|
|
type AISummarizeResponse struct {
|
|
ContractFolderID string `json:"contract_folder_id"`
|
|
NoticePublicationID string `json:"notice_publication_id"`
|
|
Summaries []AIDocumentSummary `json:"summaries"`
|
|
OverallSummary *string `json:"overall_summary"`
|
|
}
|
|
|
|
// AIDocumentSummary represents a single document summary from the AI service
|
|
type AIDocumentSummary struct {
|
|
DocumentName string `json:"document_name"`
|
|
DocumentType string `json:"document_type"`
|
|
Summary string `json:"summary"`
|
|
SummaryModel string `json:"summary_model"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// AIAnalyzeResponse represents the response for on-demand agentic analysis.
|
|
type AIAnalyzeResponse struct {
|
|
ContractFolderID string `json:"contract_folder_id"`
|
|
NoticePublicationID string `json:"notice_publication_id"`
|
|
Summary string `json:"summary"`
|
|
Documents []AIAnalyzeDocument `json:"documents"`
|
|
ToolCallsLog []string `json:"tool_calls_log"`
|
|
Iterations int `json:"iterations"`
|
|
Model string `json:"model"`
|
|
}
|
|
|
|
// AIAnalyzeDocument describes one document referenced during analysis.
|
|
type AIAnalyzeDocument struct {
|
|
Filename string `json:"filename"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
TimesRead int `json:"times_read"`
|
|
TimesQueried int `json:"times_queried"`
|
|
Description string `json:"description"`
|
|
}
|