Add unit test for mapping procedure references to tenders
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Introduced a new test function `TestMapProcedureReferencesToTenders` to validate the mapping of procedure references to corresponding tenders. - Enhanced the `GetByProcedureReferences` method in the tender repository to utilize a more efficient filtering mechanism based on unique contract folder IDs. - Updated pagination logic to dynamically adjust limits based on the number of unique folders and references, ensuring optimal data retrieval. This update improves the testing coverage for tender mapping functionality and optimizes the repository's data fetching strategy, enhancing overall performance and reliability.
This commit is contained in:
@@ -47,3 +47,40 @@ func TestFormatAIProcedureRef(t *testing.T) {
|
|||||||
t.Fatalf("FormatAIProcedureRef() = %q want %q", got, want)
|
t.Fatalf("FormatAIProcedureRef() = %q want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMapProcedureReferencesToTenders(t *testing.T) {
|
||||||
|
candidates := []Tender{
|
||||||
|
{
|
||||||
|
ContractFolderID: "folder-1",
|
||||||
|
NoticePublicationID: "notice-new",
|
||||||
|
RelatedNoticePublicationIDs: []string{"notice-old"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContractFolderID: "folder-1",
|
||||||
|
NoticePublicationID: "notice-legacy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ContractFolderID: "folder-2",
|
||||||
|
NoticePublicationID: "notice-2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
refs := []ProcedureReference{
|
||||||
|
{Ref: "PROC_folder-1/notice-old", ContractFolderID: "folder-1", NoticePublicationID: "notice-old"},
|
||||||
|
{Ref: "PROC_folder-1/notice-legacy", ContractFolderID: "folder-1", NoticePublicationID: "notice-legacy"},
|
||||||
|
{Ref: "PROC_folder-2/notice-2", ContractFolderID: "folder-2", NoticePublicationID: "notice-2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
got := mapProcedureReferencesToTenders(candidates, refs)
|
||||||
|
if len(got) != 3 {
|
||||||
|
t.Fatalf("mapProcedureReferencesToTenders() len = %d, want 3", len(got))
|
||||||
|
}
|
||||||
|
if got["PROC_folder-1/notice-old"].NoticePublicationID != "notice-new" {
|
||||||
|
t.Fatalf("expected notice-old ref to resolve to latest merged tender, got %+v", got["PROC_folder-1/notice-old"])
|
||||||
|
}
|
||||||
|
if got["PROC_folder-1/notice-legacy"].NoticePublicationID != "notice-legacy" {
|
||||||
|
t.Fatalf("expected legacy ref to resolve to legacy tender, got %+v", got["PROC_folder-1/notice-legacy"])
|
||||||
|
}
|
||||||
|
if got["PROC_folder-2/notice-2"].NoticePublicationID != "notice-2" {
|
||||||
|
t.Fatalf("expected folder-2 ref to resolve, got %+v", got["PROC_folder-2/notice-2"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ func tenderSearchListProjection() bson.M {
|
|||||||
"source_file_url": 0,
|
"source_file_url": 0,
|
||||||
"source_file_name": 0,
|
"source_file_name": 0,
|
||||||
// Not used by list response mapping; can be large per tender.
|
// Not used by list response mapping; can be large per tender.
|
||||||
"translations": 0,
|
"translations": 0,
|
||||||
"organizations": 0,
|
"organizations": 0,
|
||||||
"review_organization": 0,
|
"review_organization": 0,
|
||||||
"processing_metadata.translated_data": 0,
|
"processing_metadata.translated_data": 0,
|
||||||
"processing_metadata.parsing_errors": 0,
|
"processing_metadata.parsing_errors": 0,
|
||||||
"processing_metadata.validation_errors": 0,
|
"processing_metadata.validation_errors": 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -350,28 +350,31 @@ func (r *tenderRepository) GetByProcedureReferences(ctx context.Context, refs []
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
orClauses := make([]bson.M, 0, len(refs))
|
contractFolderIDs := make([]string, 0, len(refs))
|
||||||
for _, ref := range refs {
|
for _, ref := range refs {
|
||||||
folder := strings.TrimSpace(ref.ContractFolderID)
|
folder := strings.TrimSpace(ref.ContractFolderID)
|
||||||
notice := strings.TrimSpace(ref.NoticePublicationID)
|
notice := strings.TrimSpace(ref.NoticePublicationID)
|
||||||
if folder == "" || notice == "" {
|
if folder == "" || notice == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
orClauses = append(orClauses, bson.M{
|
contractFolderIDs = append(contractFolderIDs, folder)
|
||||||
"contract_folder_id": folder,
|
|
||||||
"$or": []bson.M{
|
|
||||||
{"notice_publication_id": notice},
|
|
||||||
{"related_notice_publication_ids": notice},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if len(orClauses) == 0 {
|
uniqueFolders := uniqueNonEmptyTrimmedStrings(contractFolderIDs)
|
||||||
|
if len(uniqueFolders) == 0 {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := bson.M{"$or": orClauses}
|
findLimit := len(uniqueFolders) * 5
|
||||||
|
if minLimit := len(refs) + len(uniqueFolders); findLimit < minLimit {
|
||||||
|
findLimit = minLimit
|
||||||
|
}
|
||||||
|
if findLimit > 100000 {
|
||||||
|
findLimit = 100000
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := bson.M{"contract_folder_id": bson.M{"$in": uniqueFolders}}
|
||||||
pagination := orm.Pagination{
|
pagination := orm.Pagination{
|
||||||
Limit: len(orClauses) * 2,
|
Limit: findLimit,
|
||||||
SortField: "updated_at",
|
SortField: "updated_at",
|
||||||
SortOrder: -1,
|
SortOrder: -1,
|
||||||
}
|
}
|
||||||
@@ -379,22 +382,26 @@ func (r *tenderRepository) GetByProcedureReferences(ctx context.Context, refs []
|
|||||||
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
result, err := r.ormRepo.FindAll(ctx, filter, pagination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("Failed to get tenders by procedure references", map[string]interface{}{
|
r.logger.Error("Failed to get tenders by procedure references", map[string]interface{}{
|
||||||
"count": len(orClauses),
|
"count": len(uniqueFolders),
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return mapProcedureReferencesToTenders(result.Items, refs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapProcedureReferencesToTenders(candidates []Tender, refs []ProcedureReference) map[string]Tender {
|
||||||
|
out := make(map[string]Tender)
|
||||||
for _, ref := range refs {
|
for _, ref := range refs {
|
||||||
for i := range result.Items {
|
for i := range candidates {
|
||||||
if tenderMatchesProcedureRef(result.Items[i], ref.ContractFolderID, ref.NoticePublicationID) {
|
if tenderMatchesProcedureRef(candidates[i], ref.ContractFolderID, ref.NoticePublicationID) {
|
||||||
out[ref.Ref] = result.Items[i]
|
out[ref.Ref] = candidates[i]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return out
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByContractFolderID retrieves a tender by its procedure-level identifier (ContractFolderID).
|
// GetByContractFolderID retrieves a tender by its procedure-level identifier (ContractFolderID).
|
||||||
|
|||||||
+43
-20
@@ -17,6 +17,8 @@ import (
|
|||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
orm "tm/pkg/mongo"
|
orm "tm/pkg/mongo"
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AISummarizerClient defines the interface for on-demand AI operations.
|
// AISummarizerClient defines the interface for on-demand AI operations.
|
||||||
@@ -1754,29 +1756,50 @@ func (s *tenderService) resolveRecommendedTenders(ctx context.Context, recommend
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
byProcedure = make(map[string]Tender)
|
||||||
|
byMongoID = make(map[string]Tender)
|
||||||
|
)
|
||||||
|
|
||||||
|
group, groupCtx := errgroup.WithContext(ctx)
|
||||||
if len(procRefs) > 0 {
|
if len(procRefs) > 0 {
|
||||||
byProcedure, err := s.repository.GetByProcedureReferences(ctx, procRefs)
|
group.Go(func() error {
|
||||||
if err != nil {
|
byProcedureResult, err := s.repository.GetByProcedureReferences(groupCtx, procRefs)
|
||||||
return nil, fmt.Errorf("failed to load recommended tenders by procedure reference: %w", err)
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("failed to load recommended tenders by procedure reference: %w", err)
|
||||||
for ref, tender := range byProcedure {
|
}
|
||||||
out[ref] = tender
|
for ref, tender := range byProcedureResult {
|
||||||
}
|
byProcedure[ref] = tender
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
if len(mongoIDs) == 0 {
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tenders, err := s.repository.GetByIDs(ctx, mongoIDs)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("Failed to load recommended tenders by Mongo ID", map[string]interface{}{
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
})
|
||||||
return nil, fmt.Errorf("failed to load recommended tenders: %w", err)
|
|
||||||
}
|
}
|
||||||
for i := range tenders {
|
|
||||||
out[tenders[i].GetID()] = tenders[i]
|
if len(mongoIDs) > 0 {
|
||||||
|
group.Go(func() error {
|
||||||
|
tenders, err := s.repository.GetByIDs(groupCtx, mongoIDs)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to load recommended tenders by Mongo ID", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return fmt.Errorf("failed to load recommended tenders: %w", err)
|
||||||
|
}
|
||||||
|
for i := range tenders {
|
||||||
|
byMongoID[tenders[i].GetID()] = tenders[i]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := group.Wait(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for ref, tender := range byProcedure {
|
||||||
|
out[ref] = tender
|
||||||
|
}
|
||||||
|
for ref, tender := range byMongoID {
|
||||||
|
out[ref] = tender
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, nil
|
return out, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user