diff --git a/internal/tender/form.go b/internal/tender/form.go index f3814cf..ae75722 100644 --- a/internal/tender/form.go +++ b/internal/tender/form.go @@ -66,30 +66,31 @@ type SearchResponse struct { // TenderResponse represents a tender in API responses type TenderResponse struct { - ID string `json:"id"` - NoticePublicationID string `json:"notice_publication_id"` - NoticeTypeCode string `json:"notice_type_code"` - Title string `json:"title"` - Description string `json:"description"` - ProcurementTypeCode string `json:"procurement_type_code"` - ProcedureCode string `json:"procedure_code"` - MainClassification string `json:"main_classification"` - EstimatedValue float64 `json:"estimated_value"` - Currency string `json:"currency"` - Duration string `json:"duration"` - DurationUnit string `json:"duration_unit"` - PublicationDate int64 `json:"publication_date"` - TenderDeadline int64 `json:"tender_deadline"` - SubmissionDeadline int64 `json:"submission_deadline"` - ApplicationDeadline int64 `json:"application_deadline"` - SubmissionURL string `json:"submission_url"` - CountryCode string `json:"country_code"` - BuyerOrganization *OrganizationResponse `json:"buyer_organization"` - Status TenderStatus `json:"status"` - TenderID string `json:"tender_id"` - CreatedAt int64 `json:"created_at"` - OverallSummary string `json:"overall_summary,omitempty"` // AI-generated summary from the pipeline - Language string `json:"language,omitempty"` + ID string `json:"id"` + NoticePublicationID string `json:"notice_publication_id"` + RelatedNoticePublicationIDs []string `json:"related_notice_publication_ids,omitempty"` + NoticeTypeCode string `json:"notice_type_code"` + Title string `json:"title"` + Description string `json:"description"` + ProcurementTypeCode string `json:"procurement_type_code"` + ProcedureCode string `json:"procedure_code"` + MainClassification string `json:"main_classification"` + EstimatedValue float64 `json:"estimated_value"` + Currency string `json:"currency"` + Duration string `json:"duration"` + DurationUnit string `json:"duration_unit"` + PublicationDate int64 `json:"publication_date"` + TenderDeadline int64 `json:"tender_deadline"` + SubmissionDeadline int64 `json:"submission_deadline"` + ApplicationDeadline int64 `json:"application_deadline"` + SubmissionURL string `json:"submission_url"` + CountryCode string `json:"country_code"` + BuyerOrganization *OrganizationResponse `json:"buyer_organization"` + Status TenderStatus `json:"status"` + TenderID string `json:"tender_id"` + CreatedAt int64 `json:"created_at"` + OverallSummary string `json:"overall_summary,omitempty"` // AI-generated summary from the pipeline + Language string `json:"language,omitempty"` } // TenderDocumentResponse represents a scraped tender document available for download. @@ -135,29 +136,30 @@ func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse { } response := &TenderResponse{ - ID: t.ID.Hex(), - NoticePublicationID: t.NoticePublicationID, - NoticeTypeCode: t.NoticeTypeCode, - Title: title, - Description: description, - ProcurementTypeCode: t.ProcurementTypeCode, - ProcedureCode: t.ProcedureCode, - MainClassification: t.MainClassification, - EstimatedValue: t.EstimatedValue, - Currency: t.Currency, - Duration: t.Duration, - DurationUnit: t.DurationUnit, - PublicationDate: t.PublicationDate, - TenderDeadline: t.TenderDeadline, - SubmissionDeadline: t.SubmissionDeadline, - ApplicationDeadline: t.ApplicationDeadline, - CountryCode: t.CountryCode, - SubmissionURL: t.SubmissionURL, - BuyerOrganization: org, - Status: t.Status, - TenderID: t.TenderID, - CreatedAt: t.CreatedAt, - Language: usedLanguage, + ID: t.ID.Hex(), + NoticePublicationID: t.NoticePublicationID, + RelatedNoticePublicationIDs: t.RelatedNoticePublicationIDs, + NoticeTypeCode: t.NoticeTypeCode, + Title: title, + Description: description, + ProcurementTypeCode: t.ProcurementTypeCode, + ProcedureCode: t.ProcedureCode, + MainClassification: t.MainClassification, + EstimatedValue: t.EstimatedValue, + Currency: t.Currency, + Duration: t.Duration, + DurationUnit: t.DurationUnit, + PublicationDate: t.PublicationDate, + TenderDeadline: t.TenderDeadline, + SubmissionDeadline: t.SubmissionDeadline, + ApplicationDeadline: t.ApplicationDeadline, + CountryCode: t.CountryCode, + SubmissionURL: t.SubmissionURL, + BuyerOrganization: org, + Status: t.Status, + TenderID: t.TenderID, + CreatedAt: t.CreatedAt, + Language: usedLanguage, } return response @@ -180,7 +182,7 @@ type AITranslateResponse struct { type AISummaryResponse struct { NoticeID string `json:"notice_id"` OverallSummary string `json:"overall_summary"` - Source string `json:"source"` // "storage" or "on_demand" + Source string `json:"source"` // storage | pending | unavailable } // AISummarizeResponse represents the response for triggering on-demand AI summarization diff --git a/internal/tender/handler.go b/internal/tender/handler.go index 717e71f..cf72e6e 100644 --- a/internal/tender/handler.go +++ b/internal/tender/handler.go @@ -7,6 +7,7 @@ import ( "strconv" "time" "tm/pkg/logger" + orm "tm/pkg/mongo" "tm/pkg/response" "github.com/asaskevich/govalidator" @@ -477,11 +478,14 @@ func (h *TenderHandler) GetAISummary(c echo.Context) error { summary, err := h.service.GetAISummary(c.Request().Context(), id) if err != nil { + if errors.Is(err, orm.ErrDocumentNotFound) { + return response.NotFound(c, "Tender not found") + } h.logger.Error("Failed to get AI summary", map[string]interface{}{ "tender_id": id, "error": err.Error(), }) - return response.NotFound(c, err.Error()) + return response.InternalServerError(c, "Failed to retrieve AI summary") } return response.Success(c, summary, "AI summary retrieved successfully") @@ -574,11 +578,14 @@ func (h *TenderHandler) GetPublicAISummary(c echo.Context) error { summary, err := h.service.GetAISummary(c.Request().Context(), id) if err != nil { + if errors.Is(err, orm.ErrDocumentNotFound) { + return response.NotFound(c, "Tender not found") + } h.logger.Error("Failed to get AI summary", map[string]interface{}{ "tender_id": id, "error": err.Error(), }) - return response.NotFound(c, err.Error()) + return response.InternalServerError(c, "Failed to retrieve AI summary") } return response.Success(c, summary, "AI summary retrieved successfully") diff --git a/internal/tender/service.go b/internal/tender/service.go index 0fac473..23e9ada 100644 --- a/internal/tender/service.go +++ b/internal/tender/service.go @@ -827,14 +827,14 @@ func (s *tenderService) CleanupOldData(ctx context.Context, olderThan time.Durat } // GetAISummary retrieves the AI-generated summary for a tender. -// It first tries to fetch from MinIO storage (async pipeline result). -// If not found or not ready, it returns an appropriate error. +// It loads from MinIO storage (async pipeline result). When no summary exists yet, +// the pipeline is still running, or storage is unavailable, it returns 200-compatible +// data with an empty overall_summary and a Source hint instead of an error. func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummaryResponse, error) { if id == "" { return nil, fmt.Errorf("tender ID cannot be empty") } - // Get tender to find the notice_publication_id t, err := s.repository.GetByID(ctx, id) if err != nil { s.logger.Error("Failed to get tender for AI summary", map[string]interface{}{ @@ -844,20 +844,24 @@ func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummary return nil, fmt.Errorf("failed to get tender: %w", err) } - if t == nil { - return nil, fmt.Errorf("tender not found") + noticeID := strings.TrimSpace(t.NoticePublicationID) + emptyResp := func(source string) *AISummaryResponse { + return &AISummaryResponse{ + NoticeID: noticeID, + OverallSummary: "", + Source: source, + } } - noticeID := t.NoticePublicationID - if noticeID == "" { - return nil, fmt.Errorf("tender has no notice publication ID") - } - if t.ContractFolderID == "" { - return nil, fmt.Errorf("tender has no contract folder id") + if noticeID == "" || strings.TrimSpace(t.ContractFolderID) == "" { + return emptyResp("unavailable"), nil } if s.aiSummarizerStorage == nil { - return nil, fmt.Errorf("AI summarizer storage is not configured") + s.logger.Debug("AI summarizer storage not configured; returning empty AI summary", map[string]interface{}{ + "tender_id": id, + }) + return emptyResp("unavailable"), nil } summary, usedNotice, err := s.lookupSummaryForTender(ctx, t) @@ -877,10 +881,10 @@ func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummary }) if errors.Is(err, ai_summarizer.ErrSummaryNotReady) { - return nil, fmt.Errorf("AI summary is not ready yet, the pipeline is still processing") + return emptyResp("pending"), nil } if errors.Is(err, ai_summarizer.ErrObjectNotFound) || errors.Is(err, ai_summarizer.ErrNoOverallSummary) { - return nil, fmt.Errorf("AI summary not found — this tender has not been processed by the AI pipeline yet") + return emptyResp("unavailable"), nil } return nil, fmt.Errorf("failed to get AI summary: %w", err) }