From 2980a3beb40aec8dc719ebe42150098332004ba4 Mon Sep 17 00:00:00 2001 From: Mazyar Date: Wed, 8 Jul 2026 01:23:21 +0330 Subject: [PATCH] Refactor tender recommendation response handling and improve test coverage - Renamed the test function to `TestBuildRecommendedTenderListResponsesPreservesOrderWithoutStorage` for clarity and updated its logic to reflect changes in the recommendation response building process. - Removed unnecessary sleep in the test to enhance performance. - Refactored the `buildRecommendedTenderResponsesParallel` method to `buildRecommendedTenderListResponses`, simplifying the response building without relying on MinIO translation lookups. - Updated the repository's `mapProcedureReferencesToTenders` function to improve efficiency by grouping candidates by their contract folder IDs, enhancing the mapping process. This update streamlines the recommendation response handling and enhances the test suite for better maintainability and performance. --- internal/tender/recommendation_response.go | 53 ++++--------------- .../tender/recommendation_response_test.go | 14 +++-- internal/tender/repository.go | 18 +++++-- internal/tender/service.go | 7 ++- 4 files changed, 35 insertions(+), 57 deletions(-) diff --git a/internal/tender/recommendation_response.go b/internal/tender/recommendation_response.go index 35e080a..688ea86 100644 --- a/internal/tender/recommendation_response.go +++ b/internal/tender/recommendation_response.go @@ -6,13 +6,9 @@ import ( "strings" "sync" - "golang.org/x/sync/errgroup" - "tm/pkg/ai_summarizer" ) -const recommendationTranslationConcurrency = 8 - type rankedRecommendedTender struct { tender Tender rank int @@ -54,52 +50,21 @@ func (c *recommendationTranslationCache) set(tenderID, language string, stored a c.items[recommendationTranslationCacheKey(tenderID, language)] = stored } -func (s *tenderService) buildRecommendedTenderResponsesParallel( - ctx context.Context, - items []rankedRecommendedTender, - language string, -) []TenderResponse { +// 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)) - if len(items) == 1 { - out[0] = s.buildRecommendedTenderResponse(ctx, items[0].tender, items[0].rank, items[0].analysis, items[0].tenderRef, language, newRecommendationTranslationCache()) - return out + 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 } - - translationCache := newRecommendationTranslationCache() - group, groupCtx := errgroup.WithContext(ctx) - group.SetLimit(recommendationTranslationConcurrency) - - for i := range items { - i := i - item := items[i] - group.Go(func() error { - out[i] = s.buildRecommendedTenderResponse( - groupCtx, - item.tender, - item.rank, - item.analysis, - item.tenderRef, - language, - translationCache, - ) - return nil - }) - } - - if err := group.Wait(); err != nil { - s.logger.Warn("Parallel recommendation response build degraded to sequential", map[string]interface{}{ - "error": err.Error(), - "count": len(items), - }) - for i, item := range items { - out[i] = s.buildRecommendedTenderResponse(ctx, item.tender, item.rank, item.analysis, item.tenderRef, language, translationCache) - } - } - return out } diff --git a/internal/tender/recommendation_response_test.go b/internal/tender/recommendation_response_test.go index 9545dc5..46c0af0 100644 --- a/internal/tender/recommendation_response_test.go +++ b/internal/tender/recommendation_response_test.go @@ -5,7 +5,6 @@ import ( "io" "sync/atomic" "testing" - "time" "go.mongodb.org/mongo-driver/v2/bson" @@ -26,7 +25,6 @@ func (s *countingTranslationStorage) GetDocumentSummariesFromStorage(context.Con func (s *countingTranslationStorage) GetTranslationFromStorage(context.Context, string, string, string) (ai_summarizer.StoredTranslation, error) { s.calls.Add(1) - time.Sleep(20 * time.Millisecond) return ai_summarizer.StoredTranslation{ Title: "Translated title", Description: "Translated description", @@ -41,7 +39,7 @@ func (s *countingTranslationStorage) DownloadDocument(context.Context, string) ( return nil, ai_summarizer.ErrObjectNotFound } -func TestBuildRecommendedTenderResponsesParallelPreservesOrder(t *testing.T) { +func TestBuildRecommendedTenderListResponsesPreservesOrderWithoutStorage(t *testing.T) { storage := &countingTranslationStorage{} service := &tenderService{ aiSummarizerStorage: storage, @@ -71,7 +69,7 @@ func TestBuildRecommendedTenderResponsesParallelPreservesOrder(t *testing.T) { }) } - got := service.buildRecommendedTenderResponsesParallel(context.Background(), items, "fr") + got := service.buildRecommendedTenderListResponses(items, "fr") if len(got) != len(items) { t.Fatalf("len = %d, want %d", len(got), len(items)) } @@ -80,11 +78,11 @@ func TestBuildRecommendedTenderResponsesParallelPreservesOrder(t *testing.T) { if resp.Rank != wantRank { t.Fatalf("got[%d].Rank = %d, want %d", i, resp.Rank, wantRank) } - if resp.Title != "Translated title" { - t.Fatalf("got[%d].Title = %q, want translated title", i, resp.Title) + if resp.Title != "Original" { + t.Fatalf("got[%d].Title = %q, want MongoDB title without MinIO lookup", i, resp.Title) } } - if storage.calls.Load() != int32(len(items)) { - t.Fatalf("translation lookups = %d, want %d", storage.calls.Load(), len(items)) + if storage.calls.Load() != 0 { + t.Fatalf("translation lookups = %d, want 0", storage.calls.Load()) } } diff --git a/internal/tender/repository.go b/internal/tender/repository.go index d3416d5..6176e42 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -392,11 +392,21 @@ func (r *tenderRepository) GetByProcedureReferences(ctx context.Context, refs [] } func mapProcedureReferencesToTenders(candidates []Tender, refs []ProcedureReference) map[string]Tender { - out := make(map[string]Tender) + byFolder := make(map[string][]Tender) + for i := range candidates { + folder := strings.TrimSpace(candidates[i].ContractFolderID) + if folder == "" { + continue + } + byFolder[folder] = append(byFolder[folder], candidates[i]) + } + + out := make(map[string]Tender, len(refs)) for _, ref := range refs { - for i := range candidates { - if tenderMatchesProcedureRef(candidates[i], ref.ContractFolderID, ref.NoticePublicationID) { - out[ref.Ref] = candidates[i] + folder := strings.TrimSpace(ref.ContractFolderID) + for _, candidate := range byFolder[folder] { + if tenderMatchesProcedureRef(candidate, ref.ContractFolderID, ref.NoticePublicationID) { + out[ref.Ref] = candidate break } } diff --git a/internal/tender/service.go b/internal/tender/service.go index 4cae58d..5b362e0 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -947,8 +947,13 @@ func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination nil } +const recommendRequestTimeout = 15 * time.Second + // Recommend retrieves AI-ranked tenders for the authenticated customer's company. func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) { + ctx, cancel := context.WithTimeout(ctx, recommendRequestTimeout) + defer cancel() + companyIDs := normalizeRecommendationCompanyScope(form.CompanyIDs) companyID := "" if form.CompanyID != nil { @@ -1055,7 +1060,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat end = len(ordered) } - paged := s.buildRecommendedTenderResponsesParallel(ctx, ordered[start:end], lang) + paged := s.buildRecommendedTenderListResponses(ordered[start:end], lang) return &SearchResponse{ Tenders: paged,