Add AI procedure reference formatting and parsing functions with unit tests
continuous-integration/drone/push Build is passing

- Introduced `FormatAIProcedureRef` and `ParseAIProcedureRef` functions in the `tender` domain for handling AI service tender references.
- Added unit tests for these functions in `ai_reference_test.go` to ensure correct parsing and formatting behavior.
- Updated the `TenderResponse` struct to include a new `ProcedureRef` field for improved data representation.
- Enhanced the `GetByProcedureReference` method in the repository to retrieve tenders based on the new procedure reference format.
- Modified the `Recommend` method in the service layer to utilize the new procedure reference handling, improving the recommendation process.

This update enhances the handling of AI procedure references, ensuring better data integrity and usability in the tender management system.
This commit is contained in:
Mazyar
2026-06-21 15:36:36 +03:30
parent 2b6e25f979
commit fa258020f1
6 changed files with 205 additions and 26 deletions
+60 -23
View File
@@ -941,36 +941,17 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
return s.emptySearchResponse(pagination), nil
}
tenderIDs := make([]string, 0, len(recommendations))
for _, rec := range recommendations {
if id := strings.TrimSpace(rec.TenderID); id != "" {
tenderIDs = append(tenderIDs, id)
}
}
if len(tenderIDs) == 0 {
return s.emptySearchResponse(pagination), nil
}
tenders, err := s.repository.GetByIDs(ctx, tenderIDs)
tenderByRef, err := s.resolveRecommendedTenders(ctx, recommendations)
if err != nil {
s.logger.Error("Failed to load recommended tenders", map[string]interface{}{
"company_id": companyID,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to load recommended tenders: %w", err)
}
tenderByID := make(map[string]Tender, len(tenders))
for i := range tenders {
tenderByID[tenders[i].GetID()] = tenders[i]
return nil, err
}
lang := s.pickResponseLanguage(form.Language)
now := time.Now().Unix()
ordered := make([]TenderResponse, 0, len(recommendations))
for _, rec := range recommendations {
tenderID := strings.TrimSpace(rec.TenderID)
tender, ok := tenderByID[tenderID]
tenderRef := strings.TrimSpace(rec.TenderID)
tender, ok := tenderByRef[tenderRef]
if !ok {
continue
}
@@ -984,6 +965,7 @@ func (s *tenderService) Recommend(ctx context.Context, form *SearchForm, paginat
resp := tender.ToResponseWithLanguage(lang)
resp.Rank = rec.Rank
resp.Analysis = rec.Analysis
resp.ProcedureRef = tenderRef
ordered = append(ordered, *resp)
}
@@ -1688,6 +1670,61 @@ func (s *tenderService) enrichDocumentsScrapedFilter(ctx context.Context, form *
return nil
}
func (s *tenderService) resolveRecommendedTenders(ctx context.Context, recommendations []company.RecommendedTenderResponse) (map[string]Tender, error) {
out := make(map[string]Tender)
mongoIDs := make([]string, 0)
for _, rec := range recommendations {
ref := strings.TrimSpace(rec.TenderID)
if ref == "" {
continue
}
if folder, notice, ok := ParseAIProcedureRef(ref); ok {
tender, err := s.repository.GetByProcedureReference(ctx, folder, notice)
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
s.logger.Warn("Recommended tender not found by procedure reference", map[string]interface{}{
"tender_ref": ref,
"contract_folder_id": folder,
"notice_publication_id": notice,
})
continue
}
return nil, fmt.Errorf("failed to load recommended tender %s: %w", ref, err)
}
out[ref] = *tender
continue
}
if IsMongoObjectIDRef(ref) {
mongoIDs = append(mongoIDs, ref)
continue
}
s.logger.Warn("Skipping unrecognized recommended tender reference", map[string]interface{}{
"tender_ref": ref,
})
}
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]
}
return out, nil
}
func (s *tenderService) emptySearchResponse(pagination *response.Pagination) *SearchResponse {
return &SearchResponse{
Tenders: []TenderResponse{},