package tender import ( "fmt" "strconv" "strings" "time" "tm/pkg/mongo" ) // DocumentSummary represents a summary of a tender document type DocumentSummary struct { DocumentName string `bson:"document_name" json:"document_name"` ObjectName string `bson:"object_name" json:"object_name"` BucketName string `bson:"bucket_name" json:"bucket_name"` Summary string `bson:"summary" json:"summary"` SummaryLanguage string `bson:"summary_language" json:"summary_language"` DocumentType string `bson:"document_type" json:"document_type"` SummarizedAt int64 `bson:"summarized_at" json:"summarized_at"` SummaryModel string `bson:"summary_model" json:"summary_model"` Error string `bson:"error,omitempty" json:"error,omitempty"` } // ScrapedDocument represents information about a document that was scraped and uploaded to MinIO type ScrapedDocument struct { Filename string `bson:"filename" json:"filename"` ObjectName string `bson:"object_name" json:"object_name"` BucketName string `bson:"bucket_name" json:"bucket_name"` Size int64 `bson:"size" json:"size"` ScrapedAt int64 `bson:"scraped_at" json:"scraped_at"` } // Tender represents a tender/contract notice entity type Tender struct { mongo.Model `bson:",inline"` ContractNoticeID string `bson:"contract_notice_id" json:"contract_notice_id"` 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"` 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"` // 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) ProjectName string `bson:"project_name" json:"project_name"` // PBL Project Name (__) // Status-specific fields CancellationReason string `bson:"cancellation_reason,omitempty" json:"cancellation_reason,omitempty"` CancellationDate int64 `bson:"cancellation_date,omitempty" json:"cancellation_date,omitempty"` // Unix milliseconds AwardDate int64 `bson:"award_date,omitempty" json:"award_date,omitempty"` // Unix milliseconds AwardedValue float64 `bson:"awarded_value,omitempty" json:"awarded_value,omitempty"` WinningTenderer *Organization `bson:"winning_tenderer,omitempty" json:"winning_tenderer,omitempty"` ContractNumber string `bson:"contract_number,omitempty" json:"contract_number,omitempty"` SuspensionReason string `bson:"suspension_reason,omitempty" json:"suspension_reason,omitempty"` SuspensionDate int64 `bson:"suspension_date,omitempty" json:"suspension_date,omitempty"` // Unix milliseconds Modifications []TenderModification `bson:"modifications,omitempty" json:"modifications,omitempty"` AwardedEntities []Awarded `bson:"awarded,omitempty" json:"awarded,omitempty"` ScrapedDocuments []ScrapedDocument `bson:"scraped_documents,omitempty" json:"scraped_documents,omitempty"` DocumentSummaries []DocumentSummary `bson:"document_summaries,omitempty" json:"document_summaries,omitempty"` Translations map[string]TranslationEntry `bson:"translations,omitempty" json:"translations,omitempty"` } // TranslationEntry stores translated tender content for a target language. type TranslationEntry struct { Title string `bson:"title" json:"title"` Description string `bson:"description" json:"description"` Language string `bson:"language" json:"language"` TranslatedAt int64 `bson:"translated_at" json:"translated_at"` } // Organization represents organization information type Organization struct { ID string `bson:"id" json:"id"` Name string `bson:"name" json:"name"` CompanyID string `bson:"company_id,omitempty" json:"company_id,omitempty"` WebsiteURI string `bson:"website_uri,omitempty" json:"website_uri,omitempty"` ContactName string `bson:"contact_name,omitempty" json:"contact_name,omitempty"` ContactTelephone string `bson:"contact_telephone,omitempty" json:"contact_telephone,omitempty"` ContactEmail string `bson:"contact_email,omitempty" json:"contact_email,omitempty"` ContactFax string `bson:"contact_fax,omitempty" json:"contact_fax,omitempty"` Address Address `bson:"address" json:"address"` Role string `bson:"role,omitempty" json:"role,omitempty"` } // Address represents address information type Address struct { StreetName string `bson:"street_name,omitempty" json:"street_name,omitempty"` CityName string `bson:"city_name,omitempty" json:"city_name,omitempty"` PostalZone string `bson:"postal_zone,omitempty" json:"postal_zone,omitempty"` CountrySubentityCode string `bson:"country_subentity_code,omitempty" json:"country_subentity_code,omitempty"` Department string `bson:"department,omitempty" json:"department,omitempty"` Region string `bson:"region,omitempty" json:"region,omitempty"` CountryCode string `bson:"country_code" json:"country_code"` } // SelectionCriterion represents selection criteria type SelectionCriterion struct { TypeCode string `bson:"type_code" json:"type_code"` Description string `bson:"description,omitempty" json:"description,omitempty"` LanguageID string `bson:"language_id,omitempty" json:"language_id,omitempty"` } // 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"` } // ProcessingMetadata contains metadata about how the tender was processed type ProcessingMetadata struct { ScrapedAt int64 `bson:"scraped_at" json:"scraped_at"` // Unix milliseconds ProcessedAt int64 `bson:"processed_at" json:"processed_at"` // Unix milliseconds ProcessingVersion string `bson:"processing_version" json:"processing_version"` ParsingErrors []string `bson:"parsing_errors,omitempty" json:"parsing_errors,omitempty"` ValidationErrors []string `bson:"validation_errors,omitempty" json:"validation_errors,omitempty"` EnrichmentData map[string]string `bson:"enrichment_data,omitempty" json:"enrichment_data,omitempty"` TranslatedData map[string]string `bson:"translated_data,omitempty" json:"translated_data,omitempty"` TranslatedAt int64 `bson:"translated_at,omitempty" json:"translated_at,omitempty"` Processed bool `bson:"processed" json:"processed"` DocumentsScraped bool `bson:"documents_scraped" json:"documents_scraped"` // Whether documents have been scraped DocumentsScrapedAt int64 `bson:"documents_scraped_at" json:"documents_scraped_at"` // When documents were scraped DocumentsSummarized bool `bson:"documents_summarized" json:"documents_summarized"` // Whether documents have been summarized DocumentsSummarizedAt int64 `bson:"documents_summarized_at" json:"documents_summarized_at"` // When documents were summarized } // TenderModification represents a modification made to a tender type TenderModification struct { ModificationDate int64 `bson:"modification_date" json:"modification_date"` // Unix milliseconds ModificationReason string `bson:"modification_reason" json:"modification_reason"` Description string `bson:"description,omitempty" json:"description,omitempty"` LanguageID string `bson:"language_id,omitempty" json:"language_id,omitempty"` } // AwardedEntity represents an entity that has been awarded a contract type Awarded struct { Name string `bson:"name" json:"name"` Address string `bson:"address,omitempty" json:"address,omitempty"` Country string `bson:"country,omitempty" json:"country,omitempty"` Amount float64 `bson:"amount" json:"amount"` Currency string `bson:"currency,omitempty" json:"currency,omitempty"` Share float64 `bson:"share,omitempty" json:"share,omitempty"` // Percentage share for multiple winners AwardDate int64 `bson:"award_date" json:"award_date"` // Unix milliseconds ContractID string `bson:"contract_id,omitempty" json:"contract_id,omitempty"` // Contract reference ID TenderID string `bson:"tender_id,omitempty" json:"tender_id,omitempty"` // Tender reference ID LotID string `bson:"lot_id,omitempty" json:"lot_id,omitempty"` // Lot reference ID CompanyID string `bson:"company_id,omitempty" json:"company_id,omitempty"` // Company registration ID OrganizationID string `bson:"organization_id,omitempty" json:"organization_id,omitempty"` // Organization reference ID } // TenderStatus represents the status of a tender type TenderStatus string const ( TenderStatusActive TenderStatus = "active" TenderStatusExpired TenderStatus = "expired" TenderStatusCancelled TenderStatus = "cancelled" TenderStatusAwarded TenderStatus = "awarded" TenderStatusDraft TenderStatus = "draft" TenderStatusClosed TenderStatus = "closed" TenderStatusModified TenderStatus = "modified" TenderStatusSuspended TenderStatus = "suspended" TenderStatusPublished TenderStatus = "published" ) // TenderSource represents the source of tender data type TenderSource string const ( TenderSourceTEDScraper TenderSource = "ted_scraper" TenderSourceManual TenderSource = "manual" TenderSourceAPI TenderSource = "api" TenderSourceBulkImport TenderSource = "bulk_import" ) // TenderStatistics represents tender statistics type TenderStatistics struct { TotalTenders int64 `json:"total_tenders"` ActiveTenders int64 `json:"active_tenders"` ExpiredTenders int64 `json:"expired_tenders"` TendersByCountry map[string]int64 `json:"tenders_by_country"` TendersByType map[string]int64 `json:"tenders_by_type"` TendersByClassification map[string]int64 `json:"tenders_by_classification"` AverageEstimatedValue float64 `json:"average_estimated_value"` TotalEstimatedValue float64 `json:"total_estimated_value"` LastUpdated int64 `json:"last_updated"` // Unix milliseconds } // GetID returns the tender ID func (t *Tender) GetID() string { return t.ID.Hex() } // IsActive returns true if the tender is active func (t *Tender) IsActive() bool { return t.Status == TenderStatusActive } // IsExpired returns true if the tender deadline has passed func (t *Tender) IsExpired() bool { if t.Status == TenderStatusExpired { return true } if t.TenderDeadline == 0 { return false } // Check if deadline (in milliseconds) is in the past now := time.Now().Unix() return t.TenderDeadline < now } // GetTenderURL returns the TED tender URL func (t *Tender) GetTenderURL() string { if t.TenderURL != "" { return t.TenderURL } if t.NoticePublicationID != "" { return generateTenderURL(t.NoticePublicationID) } return "" } // generateTenderURL generates the TED tender URL from NoticePublicationID func generateTenderURL(noticePublicationID string) string { // Remove leading zeros and format for URL // Example: 00522942-2025 -> https://ted.europa.eu/en/notice/-/detail/522942-2025 parts := strings.Split(noticePublicationID, "-") if len(parts) != 2 { return "" } // Convert to int and back to string to remove leading zeros if num, err := strconv.Atoi(parts[0]); err == nil { cleanID := fmt.Sprintf("%d-%s", num, parts[1]) return fmt.Sprintf("https://ted.europa.eu/en/notice/-/detail/%s", cleanID) } return "" } // GetMainOrganization returns the buyer organization func (t *Tender) GetMainOrganization() *Organization { return t.BuyerOrganization } // HasEstimatedValue returns true if the tender has an estimated value func (t *Tender) HasEstimatedValue() bool { return t.EstimatedValue > 0 } // GetFormattedEstimatedValue returns formatted estimated value with currency func (t *Tender) GetFormattedEstimatedValue() string { if !t.HasEstimatedValue() { return "" } if t.Currency != "" { return fmt.Sprintf("%.2f %s", t.EstimatedValue, t.Currency) } return fmt.Sprintf("%.2f", t.EstimatedValue) } // UpdateStatus updates the tender status and updated timestamp func (t *Tender) UpdateStatus(status TenderStatus) { t.Status = status t.UpdatedAt = time.Now().Unix() } // GetDaysUntilDeadline returns the number of days until the tender deadline func (t *Tender) GetDaysUntilDeadline() int { if t.TenderDeadline == 0 { return 0 } now := time.Now().Unix() if t.TenderDeadline <= now { return 0 // Already expired } diffMillis := t.TenderDeadline - now diffDays := diffMillis / (24 * 60 * 60 * 1000) return int(diffDays) }