diff --git a/internal/tender/recommendation_translation_test.go b/internal/tender/recommendation_translation_test.go new file mode 100644 index 0000000..00458dc --- /dev/null +++ b/internal/tender/recommendation_translation_test.go @@ -0,0 +1,134 @@ +package tender + +import ( + "context" + "io" + "testing" + + "tm/pkg/ai_summarizer" + "tm/pkg/logger" +) + +type noopTenderTestLogger struct{} + +func (noopTenderTestLogger) Debug(string, map[string]interface{}) {} +func (noopTenderTestLogger) Info(string, map[string]interface{}) {} +func (noopTenderTestLogger) Warn(string, map[string]interface{}) {} +func (noopTenderTestLogger) Error(string, map[string]interface{}) {} +func (noopTenderTestLogger) Fatal(string, map[string]interface{}) {} +func (l noopTenderTestLogger) WithFields(map[string]interface{}) logger.Logger { + return l +} +func (noopTenderTestLogger) Sync() error { return nil } + +type stubRecommendationTranslationStorage struct { + translations map[string]ai_summarizer.StoredTranslation +} + +func (s stubRecommendationTranslationStorage) GetSummaryFromStorage(context.Context, string, string) (string, error) { + return "", ai_summarizer.ErrSummaryNotReady +} + +func (s stubRecommendationTranslationStorage) GetDocumentSummariesFromStorage(context.Context, string, string) ([]ai_summarizer.DocumentSummary, error) { + return nil, nil +} + +func (s stubRecommendationTranslationStorage) GetTranslationFromStorage(_ context.Context, contractFolderID, noticePublicationID, language string) (ai_summarizer.StoredTranslation, error) { + key := contractFolderID + "|" + noticePublicationID + "|" + language + if translation, ok := s.translations[key]; ok { + return translation, nil + } + return ai_summarizer.StoredTranslation{}, ai_summarizer.ErrTranslationNotReady +} + +func (s stubRecommendationTranslationStorage) ListDocuments(context.Context, string, string) ([]ai_summarizer.StoredDocument, error) { + return nil, nil +} + +func (s stubRecommendationTranslationStorage) DownloadDocument(context.Context, string) (io.ReadCloser, error) { + return nil, ai_summarizer.ErrObjectNotFound +} + +func TestBuildRecommendedTenderResponseAppliesStoredTranslation(t *testing.T) { + service := &tenderService{ + aiSummarizerStorage: stubRecommendationTranslationStorage{ + translations: map[string]ai_summarizer.StoredTranslation{ + "folder-1|00123456-2026|fr": { + Title: "Titre traduit", + Description: "Description traduite", + }, + }, + }, + defaultLanguage: "en", + logger: noopTenderTestLogger{}, + } + + tender := Tender{ + ContractFolderID: "folder-1", + NoticePublicationID: "00123456-2026", + Title: "Original title", + Description: "Original description", + } + + resp := service.buildRecommendedTenderResponse( + context.Background(), + tender, + 3, + "Strong fit", + "PROC_folder-1/00123456-2026", + "fr", + ) + + if resp.Title != "Titre traduit" { + t.Fatalf("expected translated title, got %q", resp.Title) + } + if resp.Description != "Description traduite" { + t.Fatalf("expected translated description, got %q", resp.Description) + } + if resp.Language != "fr" { + t.Fatalf("expected response language fr, got %q", resp.Language) + } + if resp.Rank != 3 { + t.Fatalf("expected rank 3, got %d", resp.Rank) + } + if resp.Analysis != "Strong fit" { + t.Fatalf("expected analysis to be preserved, got %q", resp.Analysis) + } + if resp.ProcedureRef != "PROC_folder-1/00123456-2026" { + t.Fatalf("expected procedure ref to be preserved, got %q", resp.ProcedureRef) + } +} + +func TestBuildRecommendedTenderResponseKeepsOriginalTextWithoutStoredTranslation(t *testing.T) { + service := &tenderService{ + aiSummarizerStorage: stubRecommendationTranslationStorage{}, + defaultLanguage: "en", + logger: noopTenderTestLogger{}, + } + + tender := Tender{ + ContractFolderID: "folder-1", + NoticePublicationID: "00123456-2026", + Title: "Original title", + Description: "Original description", + } + + resp := service.buildRecommendedTenderResponse( + context.Background(), + tender, + 1, + "Fallback", + "PROC_folder-1/00123456-2026", + "fr", + ) + + if resp.Title != "Original title" { + t.Fatalf("expected original title, got %q", resp.Title) + } + if resp.Description != "Original description" { + t.Fatalf("expected original description, got %q", resp.Description) + } + if resp.Language != "" { + t.Fatalf("expected empty language when no translation exists, got %q", resp.Language) + } +} diff --git a/internal/tender/service.go b/internal/tender/service.go index d499dca..ecf874b 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -995,7 +995,13 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat lang := s.pickResponseLanguage(form.Language) now := time.Now().Unix() - ordered := make([]TenderResponse, 0, len(recommendations)) + type rankedRecommendedTender struct { + tender Tender + rank int + analysis string + tenderRef string + } + ordered := make([]rankedRecommendedTender, 0, len(recommendations)) for _, rec := range recommendations { tenderRef := strings.TrimSpace(rec.TenderID) tender, ok := tenderByRef[tenderRef] @@ -1016,11 +1022,12 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat continue } - resp := tender.ToResponseWithLanguage(lang) - resp.Rank = rec.Rank - resp.Analysis = rec.Analysis - resp.ProcedureRef = tenderRef - ordered = append(ordered, *resp) + ordered = append(ordered, rankedRecommendedTender{ + tender: tender, + rank: rec.Rank, + analysis: rec.Analysis, + tenderRef: tenderRef, + }) } total := int64(len(ordered)) @@ -1033,12 +1040,26 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat end = len(ordered) } + paged := make([]TenderResponse, 0, end-start) + for _, item := range ordered[start:end] { + paged = append(paged, s.buildRecommendedTenderResponse(ctx, item.tender, item.rank, item.analysis, item.tenderRef, lang)) + } + return &SearchResponse{ - Tenders: ordered[start:end], + Tenders: paged, Metadata: pagination.ListMeta(total, "", end < len(ordered), start), }, nil } +func (s *tenderService) buildRecommendedTenderResponse(ctx context.Context, tender Tender, rank int, analysis, tenderRef, language string) TenderResponse { + resp := tender.ToResponseWithLanguage(language) + s.enrichWithTranslation(ctx, &tender, resp, language) + resp.Rank = rank + resp.Analysis = analysis + resp.ProcedureRef = tenderRef + return *resp +} + func statusAllowed(status TenderStatus, allowed []string) bool { for _, s := range allowed { if string(status) == strings.TrimSpace(s) {