List and download tender documents - worker auto translation to english job

This commit is contained in:
Mazyar
2026-05-10 02:14:53 +03:30
parent 567e20b19c
commit e136f0eaa7
11 changed files with 652 additions and 2 deletions
+193
View File
@@ -1,9 +1,13 @@
package tender
import (
"archive/zip"
"context"
"errors"
"fmt"
"io"
"path"
"path/filepath"
"strings"
"time"
"tm/internal/company"
@@ -22,6 +26,16 @@ type AISummarizerClient interface {
// AISummarizerStorage defines the interface for fetching summaries from MinIO storage
type AISummarizerStorage interface {
GetSummaryFromStorage(ctx context.Context, noticeID string) (string, error)
ListDocuments(ctx context.Context, noticeID string) ([]ai_summarizer.StoredDocument, error)
DownloadDocument(ctx context.Context, objectName string) (io.ReadCloser, error)
}
// TenderDocumentDownload contains an authorized document stream.
type TenderDocumentDownload struct {
Reader io.ReadCloser
Filename string
ContentType string
Size int64
}
// TenderService interface defines tender business logic operations
@@ -33,6 +47,8 @@ type Service interface {
Search(ctx context.Context, req *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
Recommend(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error)
GetDocumentSummaries(ctx context.Context, id string) ([]DocumentSummary, error)
GetDocuments(ctx context.Context, id string) (*TenderDocumentsResponse, error)
DownloadDocuments(ctx context.Context, id string) (*TenderDocumentDownload, error)
// AI summarization operations
GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error)
@@ -58,6 +74,11 @@ type tenderService struct {
const aiSummaryEnrichmentTimeout = 2 * time.Second
var (
ErrTenderDocumentStorageUnavailable = errors.New("tender document storage is unavailable")
ErrTenderDocumentNotFound = errors.New("tender document not found")
)
// NewService creates a new tender service
func NewService(repository TenderRepository, companyService company.Service, customerService customer.Service, logger logger.Logger, aiClient AISummarizerClient, aiStorage AISummarizerStorage, defaultLanguage string) Service {
if strings.TrimSpace(defaultLanguage) == "" {
@@ -241,6 +262,178 @@ func (s *tenderService) GetDocumentSummaries(ctx context.Context, id string) ([]
return tender.DocumentSummaries, nil
}
// GetDocuments retrieves scraped document metadata for a tender.
func (s *tenderService) GetDocuments(ctx context.Context, id string) (*TenderDocumentsResponse, error) {
if id == "" {
return nil, fmt.Errorf("tender ID cannot be empty")
}
if s.aiSummarizerStorage == nil {
return nil, ErrTenderDocumentStorageUnavailable
}
tender, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get tender for documents", map[string]interface{}{
"tender_id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get tender: %w", err)
}
if tender == nil {
return nil, fmt.Errorf("tender not found")
}
if strings.TrimSpace(tender.NoticePublicationID) == "" {
return nil, fmt.Errorf("tender notice publication ID is empty")
}
storedDocuments, err := s.aiSummarizerStorage.ListDocuments(ctx, tender.NoticePublicationID)
if err != nil {
s.logger.Error("Failed to list tender documents from storage", map[string]interface{}{
"tender_id": id,
"notice_id": tender.NoticePublicationID,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to list tender documents: %w", err)
}
documents := make([]TenderDocumentResponse, 0, len(storedDocuments))
for _, doc := range storedDocuments {
documents = append(documents, TenderDocumentResponse{
Filename: doc.DocumentName,
Size: doc.Size,
LastModified: doc.LastModified,
DocumentType: doc.DocumentType,
})
}
s.logger.Info("Retrieved scraped documents for tender", map[string]interface{}{
"tender_id": id,
"notice_id": tender.NoticePublicationID,
"documents_count": len(documents),
})
return &TenderDocumentsResponse{
TenderID: id,
Documents: documents,
}, nil
}
// DownloadDocuments returns a ZIP stream containing all documents for a tender.
func (s *tenderService) DownloadDocuments(ctx context.Context, id string) (*TenderDocumentDownload, error) {
if id == "" {
return nil, fmt.Errorf("tender ID cannot be empty")
}
if s.aiSummarizerStorage == nil {
return nil, ErrTenderDocumentStorageUnavailable
}
tender, err := s.repository.GetByID(ctx, id)
if err != nil {
s.logger.Error("Failed to get tender for document download", map[string]interface{}{
"tender_id": id,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to get tender: %w", err)
}
if tender == nil {
return nil, fmt.Errorf("tender not found")
}
if strings.TrimSpace(tender.NoticePublicationID) == "" {
return nil, ErrTenderDocumentNotFound
}
documents, err := s.aiSummarizerStorage.ListDocuments(ctx, tender.NoticePublicationID)
if err != nil {
s.logger.Error("Failed to list tender documents for archive", map[string]interface{}{
"tender_id": id,
"notice_id": tender.NoticePublicationID,
"error": err.Error(),
})
return nil, fmt.Errorf("failed to list tender documents: %w", err)
}
if len(documents) == 0 {
return nil, ErrTenderDocumentNotFound
}
reader, writer := io.Pipe()
go s.writeDocumentsArchive(ctx, writer, documents)
filename := fmt.Sprintf("%s-documents.zip", tender.NoticePublicationID)
s.logger.Info("Tender documents archive download prepared", map[string]interface{}{
"tender_id": id,
"notice_id": tender.NoticePublicationID,
"documents_count": len(documents),
})
return &TenderDocumentDownload{
Reader: reader,
Filename: filename,
ContentType: "application/zip",
}, nil
}
func (s *tenderService) writeDocumentsArchive(ctx context.Context, writer *io.PipeWriter, documents []ai_summarizer.StoredDocument) {
zipWriter := zip.NewWriter(writer)
usedNames := make(map[string]int, len(documents))
for _, doc := range documents {
select {
case <-ctx.Done():
zipWriter.Close()
writer.CloseWithError(ctx.Err())
return
default:
}
if err := s.addDocumentToArchive(ctx, zipWriter, usedNames, doc); err != nil {
zipWriter.Close()
writer.CloseWithError(err)
return
}
}
if err := zipWriter.Close(); err != nil {
writer.CloseWithError(err)
return
}
writer.Close()
}
func (s *tenderService) addDocumentToArchive(ctx context.Context, zipWriter *zip.Writer, usedNames map[string]int, doc ai_summarizer.StoredDocument) error {
reader, err := s.aiSummarizerStorage.DownloadDocument(ctx, doc.ObjectName)
if err != nil {
return fmt.Errorf("failed to open document %s: %w", doc.DocumentName, err)
}
defer reader.Close()
entry, err := zipWriter.Create(uniqueArchiveName(usedNames, doc.DocumentName))
if err != nil {
return fmt.Errorf("failed to create archive entry for %s: %w", doc.DocumentName, err)
}
if _, err := io.Copy(entry, reader); err != nil {
return fmt.Errorf("failed to write document %s to archive: %w", doc.DocumentName, err)
}
return nil
}
func uniqueArchiveName(usedNames map[string]int, filename string) string {
cleanName := path.Base(filename)
if cleanName == "." || cleanName == "/" || cleanName == "" {
cleanName = "document"
}
count := usedNames[cleanName]
usedNames[cleanName] = count + 1
if count == 0 {
return cleanName
}
extension := filepath.Ext(cleanName)
base := strings.TrimSuffix(cleanName, extension)
return fmt.Sprintf("%s-%d%s", base, count+1, extension)
}
// ListTenders retrieves tenders with pagination and filtering
func (s *tenderService) Search(ctx context.Context, form *SearchForm, pagination *response.Pagination) (*SearchResponse, error) {
s.logger.Info("Listing tenders", map[string]interface{}{