package workers import ( "context" "strings" "time" "tm/internal/notice" "tm/internal/tender" "tm/pkg/glm" "tm/pkg/logger" "tm/pkg/mongo" "tm/pkg/notification" ) type NoticeWorker struct { Mongo *mongo.ConnectionManager Logger logger.Logger Notify *notification.SDK NoticeRepo notice.Repository TenderRepo tender.TenderRepository GLM *glm.SDK } func NewNoticeWorker(mongo *mongo.ConnectionManager, logger logger.Logger, notify *notification.SDK, noticeRepo notice.Repository, tenderRepo tender.TenderRepository, glmService *glm.SDK) *NoticeWorker { return &NoticeWorker{ Mongo: mongo, Logger: logger, Notify: notify, NoticeRepo: noticeRepo, TenderRepo: tenderRepo, GLM: glmService, } } func (w *NoticeWorker) Run() { w.Logger.Info("Notice worker started", map[string]interface{}{}) limit := 10 skip := 0 for { notices, _, err := w.NoticeRepo.GetUnProcessedNotices(context.Background(), limit, skip) if err != nil { w.Logger.Error("Failed to get notices", map[string]interface{}{ "error": err.Error(), }) continue } if len(notices) == 0 { break } for _, n := range notices { w.Logger.Info("Notice", map[string]interface{}{ "notice": n.ID.Hex(), }) t, err := w.ToTender(&n) if err != nil { w.Logger.Error("Failed to create tender", map[string]interface{}{ "error": err.Error(), }) continue } if t.ID.IsZero() { err = w.TenderRepo.Create(context.Background(), t) if err != nil { w.Logger.Error("Failed to create tender", map[string]interface{}{ "error": err.Error(), }) } } else { err = w.TenderRepo.Update(context.Background(), t) if err != nil { w.Logger.Error("Failed to update tender", map[string]interface{}{ "error": err.Error(), }) } } n.ProcessingMetadata.Processed = true n.ProcessingMetadata.ProcessedAt = time.Now().Unix() err = w.NoticeRepo.Update(context.Background(), &n) if err != nil { w.Logger.Error("Failed to update notice", map[string]interface{}{ "error": err.Error(), }) } w.Logger.Info("Notice processed", map[string]interface{}{ "notice": n.ID.Hex(), }) } skip += limit } } func (w *NoticeWorker) ToTender(n *notice.Notice) (*tender.Tender, error) { t, err := w.TenderRepo.GetByNoticePublicationID(context.Background(), n.NoticePublicationID) if err != nil { w.Logger.Info("New tender raised", map[string]interface{}{ "notice_publication_id": n.NoticePublicationID, "country_code": n.CountryCode, }) t = new(tender.Tender) } // Buyer Organization buyerOrg := &tender.Organization{} if n.BuyerOrganization != nil { buyerOrg = &tender.Organization{ Name: n.BuyerOrganization.Name, CompanyID: n.BuyerOrganization.CompanyID, WebsiteURI: n.BuyerOrganization.WebsiteURI, ContactName: n.BuyerOrganization.ContactName, ContactTelephone: n.BuyerOrganization.ContactTelephone, ContactEmail: n.BuyerOrganization.ContactEmail, ContactFax: n.BuyerOrganization.ContactFax, Role: n.BuyerOrganization.Role, Address: tender.Address{ StreetName: n.BuyerOrganization.Address.StreetName, CityName: n.BuyerOrganization.Address.CityName, PostalZone: n.BuyerOrganization.Address.PostalZone, CountrySubentityCode: n.BuyerOrganization.Address.CountrySubentityCode, Department: n.BuyerOrganization.Address.Department, Region: n.BuyerOrganization.Address.Region, CountryCode: n.BuyerOrganization.Address.CountryCode, }, } } // Review Organization reviewOrg := &tender.Organization{} if n.ReviewOrganization != nil { reviewOrg = &tender.Organization{ Name: n.ReviewOrganization.Name, CompanyID: n.ReviewOrganization.CompanyID, WebsiteURI: n.ReviewOrganization.WebsiteURI, ContactName: n.ReviewOrganization.ContactName, ContactTelephone: n.ReviewOrganization.ContactTelephone, ContactEmail: n.ReviewOrganization.ContactEmail, ContactFax: n.ReviewOrganization.ContactFax, Role: n.ReviewOrganization.Role, Address: tender.Address{ StreetName: n.ReviewOrganization.Address.StreetName, CityName: n.ReviewOrganization.Address.CityName, PostalZone: n.ReviewOrganization.Address.PostalZone, CountrySubentityCode: n.ReviewOrganization.Address.CountrySubentityCode, Department: n.ReviewOrganization.Address.Department, Region: n.ReviewOrganization.Address.Region, CountryCode: n.ReviewOrganization.Address.CountryCode, }, } } // Organizations organizations := make([]tender.Organization, len(n.Organizations)) for i, org := range n.Organizations { organizations[i] = tender.Organization{ Name: org.Name, CompanyID: org.CompanyID, WebsiteURI: org.WebsiteURI, ContactName: org.ContactName, ContactTelephone: org.ContactTelephone, ContactEmail: org.ContactEmail, ContactFax: org.ContactFax, Role: org.Role, Address: tender.Address{ StreetName: org.Address.StreetName, CityName: org.Address.CityName, PostalZone: org.Address.PostalZone, CountrySubentityCode: org.Address.CountrySubentityCode, Department: org.Address.Department, Region: org.Address.Region, CountryCode: org.Address.CountryCode, }, } } // Selection Criteria selectionCriteria := make([]tender.SelectionCriterion, len(n.SelectionCriteria)) for i, criterion := range n.SelectionCriteria { selectionCriteria[i] = tender.SelectionCriterion{ TypeCode: criterion.TypeCode, Description: criterion.Description, LanguageID: criterion.LanguageID, } } // Winning Tenderer winningTenderer := &tender.Organization{} if n.WinningTenderer != nil { winningTenderer = &tender.Organization{ Name: n.WinningTenderer.Name, CompanyID: n.WinningTenderer.CompanyID, WebsiteURI: n.WinningTenderer.WebsiteURI, ContactName: n.WinningTenderer.ContactName, ContactTelephone: n.WinningTenderer.ContactTelephone, ContactEmail: n.WinningTenderer.ContactEmail, ContactFax: n.WinningTenderer.ContactFax, Role: n.WinningTenderer.Role, Address: tender.Address{ StreetName: n.WinningTenderer.Address.StreetName, CityName: n.WinningTenderer.Address.CityName, PostalZone: n.WinningTenderer.Address.PostalZone, CountrySubentityCode: n.WinningTenderer.Address.CountrySubentityCode, Department: n.WinningTenderer.Address.Department, Region: n.WinningTenderer.Address.Region, CountryCode: n.WinningTenderer.Address.CountryCode, }, } } // Modifications modifications := make([]tender.TenderModification, len(n.Modifications)) for i, modification := range n.Modifications { modifications[i] = tender.TenderModification{ ModificationDate: modification.ModificationDate, ModificationReason: modification.ModificationReason, Description: modification.Description, LanguageID: modification.LanguageID, } } // Awarded Entities awardedEntities := make([]tender.Awarded, len(n.AwardedEntities)) for i, awarded := range n.AwardedEntities { awardedEntities[i] = tender.Awarded{ Name: awarded.Name, Address: awarded.Address, Country: awarded.Country, Amount: awarded.Amount, Currency: awarded.Currency, Share: awarded.Share, AwardDate: awarded.AwardDate, ContractID: awarded.ContractID, TenderID: awarded.TenderID, LotID: awarded.LotID, CompanyID: awarded.CompanyID, OrganizationID: awarded.OrganizationID, } } title, err := w.GLM.Translate(context.Background(), n.Title, n.NoticeLanguageCode, "en") if err != nil { w.Logger.Error("Failed to translate title", map[string]interface{}{ "notice_publication_id": n.NoticePublicationID, "country_code": n.CountryCode, "content": n.Title, "error": err.Error(), }) title = n.Title // return err } description, err := w.GLM.Translate(context.Background(), n.Description, n.NoticeLanguageCode, "en") if err != nil { w.Logger.Error("Failed to translate description", map[string]interface{}{ "notice_publication_id": n.NoticePublicationID, "country_code": n.CountryCode, "content": n.Description, "error": err.Error(), }) description = n.Description // return err } t.Title = title t.Description = description t.NoticePublicationID = n.NoticePublicationID t.ProcurementTypeCode = n.ProcurementTypeCode t.ProcedureCode = n.ProcedureCode t.EstimatedValue = n.EstimatedValue t.Currency = n.Currency t.TenderDeadline = n.TenderDeadline t.SubmissionURL = n.SubmissionURL t.BuyerOrganization = buyerOrg t.ReviewOrganization = reviewOrg t.Organizations = organizations t.SelectionCriteria = selectionCriteria t.WinningTenderer = winningTenderer t.Modifications = modifications t.AwardedEntities = awardedEntities t.NoticeTypeCode = n.NoticeTypeCode t.NoticeSubTypeCode = n.NoticeSubTypeCode t.NoticeLanguageCode = n.NoticeLanguageCode t.IssueDate = n.IssueDate t.IssueTime = n.IssueTime t.PublicationDate = n.PublicationDate t.SubmissionDeadline = n.SubmissionDeadline t.ApplicationDeadline = n.ApplicationDeadline t.GazetteID = n.GazetteID t.PlaceOfPerformance = n.PlaceOfPerformance t.CountryCode = n.CountryCode t.RegionCode = n.RegionCode t.CityName = n.CityName t.PostalCode = n.PostalCode t.DocumentURI = n.DocumentURI t.TenderURL = n.TenderURL t.MainClassification = n.MainClassification t.AdditionalClassifications = n.AdditionalClassifications t.OfficialLanguages = n.OfficialLanguages t.CancellationReason = n.CancellationReason t.CancellationDate = n.CancellationDate t.AwardDate = n.AwardDate t.AwardedValue = n.AwardedValue t.ContractNumber = n.ContractNumber t.SuspensionReason = n.SuspensionReason t.SuspensionDate = n.SuspensionDate t.WinningTenderer = winningTenderer t.Modifications = modifications t.AwardedEntities = awardedEntities t.TenderID = GenerateTenderID(n) return t, nil } // generateTenderID generates a unique tender ID using the format: // {Source}{BUYER_CODE}{TED_CODE}{DATE}{LOCATION_CODE} func GenerateTenderID(t *notice.Notice) string { var parts []string // Source (TED) parts = append(parts, "1") // TED code (Notice Publication ID, first 8 chars) tedCode := "00000000" if t.NoticePublicationID != "" { tedCode = PadOrTruncate(t.NoticePublicationID, 8) } parts = append(parts, tedCode) // Buyer code (first 8 chars of buyer organization ID, or 8 zeros if not available) // buyerCode := "00000000" // if t.BuyerOrganization != nil && t.BuyerOrganization.ID != "" { // buyerCode = padOrTruncate(t.BuyerOrganization.ID, 8) // } // parts = append(parts, buyerCode) // Date (YYYYMMDD format from issue date) parts = append(parts, PadOrTruncate(UnixToDateString(t.IssueDate), 4)) // Location code (country code + region code, padded to 6 chars) locationCode := "EU" if t.CountryCode != "" { locationCode = t.CountryCode } parts = append(parts, locationCode) return strings.Join(parts, "") } // padOrTruncate pads a string with zeros or truncates it to the specified length func PadOrTruncate(str string, length int) string { // Remove any non-alphanumeric characters and convert to uppercase clean := strings.ToUpper(strings.ReplaceAll(str, "-", "")) clean = strings.ReplaceAll(clean, " ", "") if len(clean) >= length { return clean[:length] } // Pad with zeros return clean + strings.Repeat("0", length-len(clean)) } // unixToDateString converts Unix to date string for TenderID generation func UnixToDateString(unix int64) string { if unix == 0 { return "00000000" } return time.Unix(unix, 0).Format("20060102") }