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"` 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"` Languages []string `query:"languages" valid:"optional"` OnlyActiveDeadlines bool `query:"-" valid:"optional"` Language *string `query:"lang" valid:"optional"` } // SearchResponse represents the response for listing tenders type SearchResponse struct { Tenders []TenderResponse `json:"tenders"` Metadata *response.Meta `json:"-"` } // 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"` } // 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 a preferred language. // When language is provided and translation exists, translated title/description are returned. func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse { title := t.Title description := t.Description usedLanguage := "" if language != "" { if entry, ok := t.Translations[language]; ok { title = entry.Title description = entry.Description usedLanguage = 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) } 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: 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"` } // 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 { NoticeID string `json:"notice_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"` }