package tender import ( "fmt" "strconv" "strings" "time" "tm/pkg/mongo" ) // 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"` ContractFolderID string `bson:"contract_folder_id" json:"contract_folder_id"` NoticeTypeCode string `bson:"notice_type_code" json:"notice_type_code"` NoticeSubTypeCode string `bson:"notice_sub_type_code" json:"notice_sub_type_code"` NoticeLanguageCode string `bson:"notice_language_code" json:"notice_language_code"` IssueDate int64 `bson:"issue_date" json:"issue_date"` // Unix milliseconds IssueTime int64 `bson:"issue_time" json:"issue_time"` // Unix milliseconds TenderDeadline int64 `bson:"tender_deadline" json:"tender_deadline"` // Unix milliseconds PublicationDate int64 `bson:"publication_date" json:"publication_date"` // Unix milliseconds SubmissionDeadline int64 `bson:"submission_deadline" json:"submission_deadline"` // Unix milliseconds ApplicationDeadline int64 `bson:"application_deadline" json:"application_deadline"` // Unix milliseconds 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"` 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"` ProcessingMetadata ProcessingMetadata `bson:"processing_metadata" json:"processing_metadata"` TenderID string `bson:"tender_id" json:"tender_id"` // Implement logic to generate a unique tender_id using the following format: {Source}{BUYER_CODE}{TED_CODE}{DATE}{LOCATION_CODE} } // 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"` } // 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"` } // 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" ) // 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" ) // TenderSearchCriteria represents search criteria for tenders type TenderSearchCriteria struct { Query string `json:"query,omitempty"` NoticeType string `json:"notice_type,omitempty"` ProcurementType string `json:"procurement_type,omitempty"` CountryCodes []string `json:"country_codes,omitempty"` RegionCodes []string `json:"region_codes,omitempty"` Classifications []string `json:"classifications,omitempty"` MinEstimatedValue *float64 `json:"min_estimated_value,omitempty"` MaxEstimatedValue *float64 `json:"max_estimated_value,omitempty"` Currency string `json:"currency,omitempty"` DeadlineFrom *int64 `json:"deadline_from,omitempty"` DeadlineTo *int64 `json:"deadline_to,omitempty"` PublicationDateFrom *int64 `json:"publication_date_from,omitempty"` PublicationDateTo *int64 `json:"publication_date_to,omitempty"` Status []TenderStatus `json:"status,omitempty"` Source []TenderSource `json:"source,omitempty"` BuyerOrganizationID string `json:"buyer_organization_id,omitempty"` Languages []string `json:"languages,omitempty"` } // 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().UnixMilli() 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().UnixMilli() } // GetDaysUntilDeadline returns the number of days until the tender deadline func (t *Tender) GetDaysUntilDeadline() int { if t.TenderDeadline == 0 { return 0 } now := time.Now().UnixMilli() if t.TenderDeadline <= now { return 0 // Already expired } diffMillis := t.TenderDeadline - now diffDays := diffMillis / (24 * 60 * 60 * 1000) return int(diffDays) }