package tender import ( "context" "fmt" "strings" "sync" "go.mongodb.org/mongo-driver/v2/bson" "golang.org/x/sync/errgroup" "tm/pkg/ai_summarizer" ) type rankedRecommendedTender struct { tender Tender rank int analysis string tenderRef string } type recommendationTranslationCache struct { mu sync.Mutex items map[string]ai_summarizer.StoredTranslation } func newRecommendationTranslationCache() *recommendationTranslationCache { return &recommendationTranslationCache{ items: make(map[string]ai_summarizer.StoredTranslation), } } func recommendationTranslationCacheKey(tenderID, language string) string { return strings.TrimSpace(tenderID) + "|" + strings.TrimSpace(language) } func (c *recommendationTranslationCache) get(tenderID, language string) (ai_summarizer.StoredTranslation, bool) { if c == nil { return ai_summarizer.StoredTranslation{}, false } c.mu.Lock() defer c.mu.Unlock() stored, ok := c.items[recommendationTranslationCacheKey(tenderID, language)] return stored, ok } func (c *recommendationTranslationCache) set(tenderID, language string, stored ai_summarizer.StoredTranslation) { if c == nil { return } c.mu.Lock() defer c.mu.Unlock() c.items[recommendationTranslationCacheKey(tenderID, language)] = stored } // buildRecommendedTenderListResponses builds list responses without MinIO translation lookups. // Recommendation pages use MongoDB text; tender detail endpoints still enrich from storage. func (s *tenderService) buildRecommendedTenderListResponses(items []rankedRecommendedTender, language string) []TenderResponse { if len(items) == 0 { return nil } out := make([]TenderResponse, len(items)) for i, item := range items { resp := item.tender.ToResponseWithLanguage(language) resp.Rank = item.rank resp.Analysis = item.analysis resp.ProcedureRef = item.tenderRef out[i] = *resp } return out } func (s *tenderService) enrichRecommendedTenderResponsesParallel(ctx context.Context, responses []TenderResponse, language string) []TenderResponse { if len(responses) == 0 || s.aiSummarizerStorage == nil { return responses } out := make([]TenderResponse, len(responses)) if len(responses) == 1 { out[0] = s.enrichRecommendedTenderResponse(ctx, responses[0], language, newRecommendationTranslationCache()) return out } translationCache := newRecommendationTranslationCache() group, groupCtx := errgroup.WithContext(ctx) group.SetLimit(recommendationPageCacheConcurrency) for i := range responses { i := i group.Go(func() error { out[i] = s.enrichRecommendedTenderResponse(groupCtx, responses[i], language, translationCache) return nil }) } if err := group.Wait(); err != nil { for i, resp := range responses { out[i] = s.enrichRecommendedTenderResponse(ctx, resp, language, translationCache) } } return out } func (s *tenderService) enrichRecommendedTenderResponse( ctx context.Context, resp TenderResponse, language string, cache *recommendationTranslationCache, ) TenderResponse { enriched := resp tender := tenderFromRecommendationResponse(resp) s.enrichWithTranslationTimeout( ctx, &tender, &enriched, language, cache, recommendTranslationEnrichmentTimeout, ) return enriched } func tenderFromRecommendationResponse(item TenderResponse) Tender { var tender Tender if id, err := bson.ObjectIDFromHex(strings.TrimSpace(item.ID)); err == nil { tender.ID = id } tender.ContractFolderID = item.ContractFolderID tender.NoticePublicationID = item.NoticePublicationID tender.RelatedNoticePublicationIDs = append([]string(nil), item.RelatedNoticePublicationIDs...) tender.Status = item.Status tender.PublicationDate = item.PublicationDate tender.SubmissionDeadline = item.SubmissionDeadline tender.TenderDeadline = item.TenderDeadline return tender } func (s *tenderService) loadRecommendationExclusions( ctx context.Context, form *SearchForm, companyID string, companyIDs []string, ) (map[string]struct{}, map[string]map[string]struct{}, error) { if !form.ExcludeRejectedTenders { return nil, nil, nil } if len(companyIDs) > 0 { excludedByCompany, err := s.buildRejectedTenderIDSetsForCompanies(ctx, companyIDs) if err != nil { return nil, nil, fmt.Errorf("failed to build recommendation exclusions: %w", err) } return nil, excludedByCompany, nil } excluded, err := s.buildRejectedTenderIDSet(ctx, companyID) if err != nil { return nil, nil, fmt.Errorf("failed to build recommendation exclusions: %w", err) } return excluded, nil, nil }