From 51defd9438db90a1bd342c2f6592aa6bd6493324 Mon Sep 17 00:00:00 2001 From: Mazyar Date: Mon, 25 May 2026 11:27:55 +0330 Subject: [PATCH] company feedback performance update --- internal/company/service.go | 25 +++++++++++ internal/feedback/service.go | 84 ++++++++++++++++++++++++++++++++++- internal/tender/repository.go | 28 ++++++++++++ internal/tender/service.go | 31 +++++++++++-- 4 files changed, 163 insertions(+), 5 deletions(-) diff --git a/internal/company/service.go b/internal/company/service.go index fb85f1d..7428de9 100644 --- a/internal/company/service.go +++ b/internal/company/service.go @@ -16,6 +16,7 @@ type Service interface { Update(ctx context.Context, id string, form *CompanyForm) (*CompanyResponse, error) GetByID(ctx context.Context, id string) (*CompanyResponse, error) GetByIDs(ctx context.Context, ids []string) ([]*CompanyResponse, error) + GetNamesByIDs(ctx context.Context, ids []string) (map[string]*CompanyResponse, error) Delete(ctx context.Context, id string) error // Company listing and search @@ -389,6 +390,30 @@ func (s *companyService) GetByIDs(ctx context.Context, ids []string) ([]*Company return companyResponses, nil } +// GetNamesByIDs returns company id and name keyed by company ID without loading categories. +func (s *companyService) GetNamesByIDs(ctx context.Context, ids []string) (map[string]*CompanyResponse, error) { + if len(ids) == 0 { + return map[string]*CompanyResponse{}, nil + } + + companies, err := s.repository.GetByIDs(ctx, ids) + if err != nil { + s.logger.Error("Failed to get companies by IDs for names", map[string]interface{}{ + "count": len(ids), + "error": err.Error(), + }) + return nil, errors.New("failed to get companies by IDs") + } + + out := make(map[string]*CompanyResponse, len(companies)) + for _, c := range companies { + id := c.ID.Hex() + out[id] = &CompanyResponse{ID: id, Name: c.Name} + } + + return out, nil +} + // UpdateCompanyStatus updates company status func (s *companyService) UpdateStatus(ctx context.Context, id string, form *StatusForm) error { // Get company to check if exists diff --git a/internal/feedback/service.go b/internal/feedback/service.go index 691734e..d17a6c5 100644 --- a/internal/feedback/service.go +++ b/internal/feedback/service.go @@ -137,10 +137,19 @@ func (s *feedbackService) Search(ctx context.Context, criteria SearchForm, pagin meta := pagination.ListMeta(page.TotalCount, page.NextCursor, page.HasMore, page.PageOffset) + tenderByID, companyByID := s.loadListDetails(ctx, page.Items) + result := make([]*FeedbackResponse, len(page.Items)) for i, feedback := range page.Items { - tender, company := s.getDetails(ctx, feedback.TenderID, feedbackCompanyID(feedback)) - result[i] = feedback.ToResponse(tender, company) + var tenderResp *tenderResponse + if t, ok := tenderByID[feedback.TenderID]; ok { + tenderResp = t + } + var companyResp *companyResponse + if cid := feedbackCompanyID(feedback); cid != "" { + companyResp = companyByID[cid] + } + result[i] = feedback.ToResponse(tenderResp, companyResp) } return &FeedbackListResponse{ @@ -175,6 +184,77 @@ func feedbackCompanyID(f Feedback) string { return *f.CompanyID } +func uniqueNonEmptyStrings(values []string) []string { + seen := make(map[string]struct{}, len(values)) + out := make([]string, 0, len(values)) + for _, v := range values { + if v == "" { + continue + } + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +} + +func collectFeedbackListIDs(items []Feedback) (tenderIDs, companyIDs []string) { + tenderIDs = make([]string, 0, len(items)) + companyIDs = make([]string, 0, len(items)) + for _, item := range items { + tenderIDs = append(tenderIDs, item.TenderID) + companyIDs = append(companyIDs, feedbackCompanyID(item)) + } + return uniqueNonEmptyStrings(tenderIDs), uniqueNonEmptyStrings(companyIDs) +} + +// loadListDetails batch-loads tender and company data for feedback list responses. +func (s *feedbackService) loadListDetails(ctx context.Context, items []Feedback) ( + map[string]*tenderResponse, + map[string]*companyResponse, +) { + tenderByID := make(map[string]*tenderResponse) + companyByID := make(map[string]*companyResponse) + + if len(items) == 0 { + return tenderByID, companyByID + } + + tenderIDs, companyIDs := collectFeedbackListIDs(items) + + if len(tenderIDs) > 0 { + tenders, err := s.tenderService.GetByIDs(ctx, tenderIDs) + if err != nil { + s.logger.Error("Failed to batch load tenders for feedback list", map[string]interface{}{ + "count": len(tenderIDs), + "error": err.Error(), + }) + } else { + for id, t := range tenders { + tenderByID[id] = tenderToFeedbackResponse(t) + } + } + } + + if len(companyIDs) > 0 { + companies, err := s.companyService.GetNamesByIDs(ctx, companyIDs) + if err != nil { + s.logger.Error("Failed to batch load companies for feedback list", map[string]interface{}{ + "count": len(companyIDs), + "error": err.Error(), + }) + } else { + for id, c := range companies { + companyByID[id] = companyToFeedbackResponse(c) + } + } + } + + return tenderByID, companyByID +} + func tenderToFeedbackResponse(t *tender.TenderResponse) *tenderResponse { if t == nil { return nil diff --git a/internal/tender/repository.go b/internal/tender/repository.go index 0c6d73d..1bcad44 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -18,6 +18,7 @@ type TenderRepository interface { // Tender CRUD operations Create(ctx context.Context, tender *Tender) error GetByID(ctx context.Context, id string) (*Tender, error) + GetByIDs(ctx context.Context, ids []string) ([]Tender, error) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) GetByNoticePublicationID(ctx context.Context, noticePublicationID string) (*Tender, error) GetByContractFolderID(ctx context.Context, contractFolderID string) (*Tender, error) @@ -214,6 +215,33 @@ func (r *tenderRepository) GetByID(ctx context.Context, id string) (*Tender, err return tender, nil } +// GetByIDs retrieves tenders by IDs in a single query. +func (r *tenderRepository) GetByIDs(ctx context.Context, ids []string) ([]Tender, error) { + if len(ids) == 0 { + return nil, nil + } + + objectIDs := make([]bson.ObjectID, 0, len(ids)) + for _, id := range ids { + objectID, err := bson.ObjectIDFromHex(id) + if err != nil { + return nil, err + } + objectIDs = append(objectIDs, objectID) + } + + result, err := r.ormRepo.FindAll(ctx, bson.M{"_id": bson.M{"$in": objectIDs}}, orm.NewPaginationBuilder().Build()) + if err != nil { + r.logger.Error("Failed to get tenders by IDs", map[string]interface{}{ + "count": len(ids), + "error": err.Error(), + }) + return nil, err + } + + return result.Items, nil +} + // GetByContractNoticeID retrieves a tender by contract notice ID func (r *tenderRepository) GetByContractNoticeID(ctx context.Context, contractNoticeID string) (*Tender, error) { filter := bson.M{"contract_notice_id": contractNoticeID} diff --git a/internal/tender/service.go b/internal/tender/service.go index eb2cc78..730f5cd 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -47,6 +47,7 @@ type TenderDocumentDownload struct { type Service interface { // Tender operations GetByID(ctx context.Context, id string, language string) (*TenderResponse, error) + GetByIDs(ctx context.Context, ids []string) (map[string]*TenderResponse, error) Update(ctx context.Context, req UpdateTenderRequest) (*TenderResponse, error) Delete(ctx context.Context, id string) error Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error) @@ -82,7 +83,7 @@ type tenderService struct { } const ( - aiSummaryEnrichmentTimeout = 2 * time.Second + aiSummaryEnrichmentTimeout = 2 * time.Second aiTranslationEnrichmentTimeout = 2 * time.Second ) @@ -128,6 +129,30 @@ func (s *tenderService) GetByID(ctx context.Context, id string, language string) return s.buildDetailResponse(ctx, tender, language), nil } +// GetByIDs loads multiple tenders for list endpoints without MinIO or AI enrichment. +func (s *tenderService) GetByIDs(ctx context.Context, ids []string) (map[string]*TenderResponse, error) { + if len(ids) == 0 { + return map[string]*TenderResponse{}, nil + } + + tenders, err := s.repository.GetByIDs(ctx, ids) + if err != nil { + s.logger.Error("Failed to retrieve tenders by IDs", map[string]interface{}{ + "count": len(ids), + "error": err.Error(), + }) + return nil, fmt.Errorf("failed to retrieve tenders: %w", err) + } + + out := make(map[string]*TenderResponse, len(tenders)) + for i := range tenders { + resp := tenders[i].ToResponseWithLanguage("") + out[resp.ID] = resp + } + + return out, nil +} + // buildDetailResponse maps a tender to API form and enriches translation (MinIO) and summary. func (s *tenderService) buildDetailResponse(ctx context.Context, tender *Tender, language string) *TenderResponse { resp := tender.ToResponseWithLanguage("") @@ -1271,8 +1296,8 @@ func (s *tenderService) ListTendersWithScrapedDocuments(ctx context.Context, pag } s.logger.Info("Listing tenders with scraped documents from MinIO", map[string]interface{}{ - "limit": pagination.Limit, - "offset": pagination.Offset, + "limit": pagination.Limit, + "offset": pagination.Offset, "sync_mongo_metadata": syncMongoMetadata, })