package tender import ( "strings" "time" "tm/pkg/response" ) // UpdateTenderRequest represents a request to update an existing tender type UpdateTenderRequest struct { ID string `json:"id" valid:"required~Tender ID is required"` Title *string `json:"title,omitempty" valid:"stringlength(1|500)~Title must be between 1 and 500 characters"` Description *string `json:"description,omitempty" valid:"stringlength(0|5000)~Description must not exceed 5000 characters"` Status *TenderStatus `json:"status,omitempty"` EstimatedValue *float64 `json:"estimated_value,omitempty" valid:"range(0|999999999999)~Estimated value must be between 0 and 999999999999"` Currency *string `json:"currency,omitempty" valid:"stringlength(3|3)~Currency must be 3 characters"` TenderDeadline *int64 `json:"tender_deadline,omitempty"` // Unix milliseconds } // SearchForm represents a request to list tenders with pagination and filtering type SearchForm struct { Search *string `query:"q" valid:"optional"` Title *string `query:"title" valid:"optional"` Description *string `query:"description" valid:"optional"` NoticeType *string `query:"notice_type" valid:"optional"` NoticeTypes []string `query:"notice_types" valid:"optional"` FormTypes []string `query:"form_types" valid:"optional"` ProcurementType *string `query:"procurement_type" valid:"optional"` Country *string `query:"country" valid:"optional"` CountryCode *string `query:"country_code" valid:"optional"` CountryCodes []string `query:"country_codes" valid:"optional"` RegionCodes []string `query:"region_codes" valid:"optional"` CompanyID *string `query:"company_id" valid:"optional"` CompanyIDs []string `query:"-" valid:"optional"` CustomerID *string `query:"customer_id" valid:"optional"` Status []string `query:"status" valid:"optional"` MainClassification *string `query:"main_classification" valid:"optional"` Classifications []string `query:"classifications" valid:"optional"` CpvCodes []string `query:"cpv_codes" valid:"optional"` MinEstimatedValue *float64 `query:"min_estimated_value" valid:"optional"` MaxEstimatedValue *float64 `query:"max_estimated_value" valid:"optional"` Currency string `query:"currency" valid:"optional"` CreatedAt *int64 `query:"created_at" valid:"optional"` CreatedAtFrom *int64 `query:"created_at_from" valid:"optional"` CreatedAtTo *int64 `query:"created_at_to" valid:"optional"` TenderDeadline *int64 `query:"tender_deadline" valid:"optional"` TenderDeadlineFrom *int64 `query:"tender_deadline_from" valid:"optional"` TenderDeadlineTo *int64 `query:"tender_deadline_to" valid:"optional"` DeadlineFrom *int64 `query:"deadline_from" valid:"optional"` DeadlineTo *int64 `query:"deadline_to" valid:"optional"` PublicationDate *int64 `query:"publication_date" valid:"optional"` PublicationDateFrom *int64 `query:"publication_date_from" valid:"optional"` PublicationDateTo *int64 `query:"publication_date_to" valid:"optional"` SubmissionDeadline *int64 `query:"submission_deadline" valid:"optional"` SubmissionDateAliasFrom *int64 `query:"submission_deadline_from" valid:"optional"` SubmissionDateAliasTo *int64 `query:"submission_deadline_to" valid:"optional"` SubmissionDateFrom *int64 `query:"submission_date_from" valid:"optional"` SubmissionDateTo *int64 `query:"submission_date_to" valid:"optional"` Source []string `query:"source" valid:"optional"` BuyerOrganizationID *string `query:"buyer_organization_id" valid:"optional"` NoticePublicationID *string `query:"notice_publication_id" valid:"optional"` Languages []string `query:"languages" valid:"optional"` OnlyActiveDeadlines bool `query:"only_active_deadlines" valid:"optional"` Language *string `query:"lang" valid:"optional"` DocumentsScraped bool `query:"documents_scraped" valid:"optional"` // ContractFolderIDsWithDocuments is populated by the service from MinIO when DocumentsScraped is true. ContractFolderIDsWithDocuments []string `query:"-" valid:"optional"` // ExcludeRejectedTenders hides company-rejected tenders from recommendation results. ExcludeRejectedTenders bool `query:"-" valid:"optional"` } // HasListFilters reports whether the form applies any MongoDB list filters. func (f *SearchForm) HasListFilters() bool { if f == nil { return false } if f.DocumentsScraped || f.OnlyActiveDeadlines || f.ExcludeRejectedTenders { return true } if f.Search != nil && strings.TrimSpace(*f.Search) != "" { return true } if f.Title != nil && strings.TrimSpace(*f.Title) != "" { return true } if f.Description != nil && strings.TrimSpace(*f.Description) != "" { return true } if f.NoticeType != nil && strings.TrimSpace(*f.NoticeType) != "" { return true } if len(f.NoticeTypes) > 0 || len(f.FormTypes) > 0 { return true } if f.ProcurementType != nil && strings.TrimSpace(*f.ProcurementType) != "" { return true } if f.Country != nil && strings.TrimSpace(*f.Country) != "" { return true } if f.CountryCode != nil && strings.TrimSpace(*f.CountryCode) != "" { return true } if len(f.CountryCodes) > 0 || len(f.RegionCodes) > 0 { return true } if f.MainClassification != nil && strings.TrimSpace(*f.MainClassification) != "" { return true } if len(f.Classifications) > 0 || len(f.CpvCodes) > 0 { return true } if f.MinEstimatedValue != nil || f.MaxEstimatedValue != nil || f.Currency != "" { return true } if f.CreatedAt != nil || f.CreatedAtFrom != nil || f.CreatedAtTo != nil { return true } if f.TenderDeadline != nil || f.TenderDeadlineFrom != nil || f.TenderDeadlineTo != nil || f.DeadlineFrom != nil || f.DeadlineTo != nil { return true } if f.PublicationDate != nil || f.PublicationDateFrom != nil || f.PublicationDateTo != nil { return true } if f.SubmissionDeadline != nil || f.SubmissionDateAliasFrom != nil || f.SubmissionDateAliasTo != nil || f.SubmissionDateFrom != nil || f.SubmissionDateTo != nil { return true } if len(f.Status) > 0 || len(f.Source) > 0 || len(f.Languages) > 0 { return true } if f.BuyerOrganizationID != nil && strings.TrimSpace(*f.BuyerOrganizationID) != "" { return true } if f.NoticePublicationID != nil && strings.TrimSpace(*f.NoticePublicationID) != "" { return true } if len(f.ContractFolderIDsWithDocuments) > 0 { return true } return false } // SearchResponse represents the response for listing tenders type SearchResponse struct { Tenders []TenderResponse `json:"tenders"` Metadata *response.Meta `json:"-"` } // TenderScrapedDocumentsItem is a tender that has scraped documents stored in the database. type TenderScrapedDocumentsItem struct { TenderID string `json:"tender_id"` NoticePublicationID string `json:"notice_publication_id"` ContractFolderID string `json:"contract_folder_id"` Title string `json:"title"` DocumentsScrapedAt int64 `json:"documents_scraped_at,omitempty"` // Unix timestamp (seconds) DocumentCount int `json:"document_count"` } // TendersWithScrapedDocumentsResponse is the paginated list of tenders with scraped documents. type TendersWithScrapedDocumentsResponse struct { Tenders []TenderScrapedDocumentsItem `json:"tenders"` Metadata *response.Meta `json:"-"` } // ToTenderScrapedDocumentsItem maps a tender entity to a list item. func (t *Tender) ToTenderScrapedDocumentsItem() TenderScrapedDocumentsItem { if t == nil { return TenderScrapedDocumentsItem{} } return TenderScrapedDocumentsItem{ TenderID: t.ID.Hex(), NoticePublicationID: strings.TrimSpace(t.NoticePublicationID), ContractFolderID: strings.TrimSpace(t.ContractFolderID), Title: strings.TrimSpace(t.Title), DocumentsScrapedAt: t.ProcessingMetadata.DocumentsScrapedAt, DocumentCount: len(t.ScrapedDocuments), } } // AddressResponse is postal / NUTS address data for API consumers. type AddressResponse struct { StreetName string `json:"street_name,omitempty"` CityName string `json:"city_name,omitempty"` PostalZone string `json:"postal_zone,omitempty"` CountrySubentityCode string `json:"country_subentity_code,omitempty"` Department string `json:"department,omitempty"` Region string `json:"region,omitempty"` CountryCode string `json:"country_code"` } // OrganizationResponse is an organization (buyer, winner, etc.) in tender API payloads. type OrganizationResponse struct { ID string `json:"id,omitempty"` Name string `json:"name"` CompanyID string `json:"company_id,omitempty"` WebsiteURI string `json:"website_uri,omitempty"` ContactName string `json:"contact_name,omitempty"` ContactTelephone string `json:"contact_telephone,omitempty"` ContactEmail string `json:"contact_email,omitempty"` Address AddressResponse `json:"address,omitempty"` } // TenderLotResponse is one procurement lot for list/detail tender APIs. type TenderLotResponse struct { LotID string `json:"lot_id,omitempty"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` MainNatureOfContract string `json:"main_nature_of_contract,omitempty"` MainClassification string `json:"main_classification,omitempty"` MainClassificationDescription string `json:"main_classification_description,omitempty"` MainClassificationDisplay string `json:"main_classification_display,omitempty"` AdditionalClassifications []string `json:"additional_classifications,omitempty"` Duration string `json:"duration,omitempty"` EstimatedValue float64 `json:"estimated_value,omitempty"` Currency string `json:"currency,omitempty"` } // TenderWinnerResponse is a contract winner for awarded tenders. type TenderWinnerResponse struct { OfficialName string `json:"official_name"` RegistrationNumber string `json:"registration_number,omitempty"` PostalAddress string `json:"postal_address,omitempty"` Town string `json:"town,omitempty"` Postcode string `json:"postcode,omitempty"` CountrySubdivisionNuts string `json:"country_subdivision_nuts,omitempty"` Country string `json:"country,omitempty"` ContactPoint string `json:"contact_point,omitempty"` Email string `json:"email,omitempty"` Telephone string `json:"telephone,omitempty"` LotID string `json:"lot_id,omitempty"` Amount float64 `json:"amount,omitempty"` Currency string `json:"currency,omitempty"` } // TenderResponse represents a tender in API responses (search, list, and detail). type TenderResponse struct { ID string `json:"id"` NoticePublicationID string `json:"notice_publication_id"` RelatedNoticePublicationIDs []string `json:"related_notice_publication_ids,omitempty"` ContractFolderID string `json:"contract_folder_id,omitempty"` ProcurementProjectID string `json:"procurement_project_id,omitempty"` ContractNoticeID string `json:"contract_notice_id,omitempty"` NoticeTypeCode string `json:"notice_type_code"` FormType string `json:"form_type,omitempty"` NoticeSubTypeCode string `json:"notice_sub_type_code,omitempty"` NoticeIdentifier string `json:"notice_identifier,omitempty"` NoticeVersion string `json:"notice_version,omitempty"` NoticeLanguageCode string `json:"notice_language_code,omitempty"` GazetteID string `json:"gazette_id,omitempty"` NoticeDispatchAt int64 `json:"notice_dispatch_at,omitempty"` NoticeDispatchDateWithTimezone string `json:"notice_dispatch_date_with_timezone,omitempty"` ESenderDispatchAt int64 `json:"esender_dispatch_at,omitempty"` IssueDate int64 `json:"issue_date,omitempty"` IssueTime int64 `json:"issue_time,omitempty"` Title string `json:"title"` Description string `json:"description"` ProcurementTypeCode string `json:"procurement_type_code"` ProcedureCode string `json:"procedure_code"` MainClassification string `json:"main_classification"` MainClassificationDescription string `json:"main_classification_description,omitempty"` MainClassificationDisplay string `json:"main_classification_display,omitempty"` AdditionalClassifications []string `json:"additional_classifications,omitempty"` EstimatedValue float64 `json:"estimated_value"` Currency string `json:"currency"` EstimatedValueVATBasis string `json:"estimated_value_vat_basis,omitempty"` 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"` PlaceOfPerformance string `json:"place_of_performance,omitempty"` CountryCode string `json:"country_code"` RegionCode string `json:"region_code,omitempty"` CityName string `json:"city_name,omitempty"` PostalCode string `json:"postal_code,omitempty"` DocumentURI string `json:"document_uri,omitempty"` DocumentURL string `json:"document_url,omitempty"` TenderURL string `json:"tender_url,omitempty"` BuyerProfileURL string `json:"buyer_profile_url,omitempty"` BuyerOrganization *OrganizationResponse `json:"buyer_organization,omitempty"` WinningTenderer *OrganizationResponse `json:"winning_tenderer,omitempty"` Lots []TenderLotResponse `json:"lots,omitempty"` Winners []TenderWinnerResponse `json:"winners,omitempty"` OfficialLanguages []string `json:"official_languages,omitempty"` AwardDate int64 `json:"award_date,omitempty"` AwardedValue float64 `json:"awarded_value,omitempty"` ContractNumber string `json:"contract_number,omitempty"` Status TenderStatus `json:"status"` Source TenderSource `json:"source,omitempty"` TenderID string `json:"tender_id"` ProjectName string `json:"project_name,omitempty"` CreatedAt int64 `json:"created_at"` OverallSummary string `json:"overall_summary,omitempty"` Language string `json:"language,omitempty"` Rank int `json:"rank,omitempty"` Analysis string `json:"analysis,omitempty"` ProcedureRef string `json:"procedure_ref,omitempty"` } // TenderDocumentResponse represents a scraped tender document available for download. type TenderDocumentResponse struct { Filename string `json:"filename"` Size int64 `json:"size"` LastModified int64 `json:"last_modified"` DocumentType string `json:"document_type,omitempty"` } // TenderDocumentsResponse represents scraped documents for a tender. type TenderDocumentsResponse struct { TenderID string `json:"tender_id"` Documents []TenderDocumentResponse `json:"documents"` } const ( enrichmentMainCPVDescriptionKey = "main_classification_description" vatBasisUnspecified = "unspecified" ) // ToTenderResponse converts a Tender entity to TenderResponse func (t *Tender) ToResponse() *TenderResponse { return t.ToResponseWithLanguage("") } // ToResponseWithLanguage converts a Tender entity to TenderResponse using Mongo fields. // Detail endpoints overlay MinIO translations in the service layer. func (t *Tender) ToResponseWithLanguage(language string) *TenderResponse { title := t.Title description := t.Description _ = language mainDesc := firstNonEmpty( strings.TrimSpace(t.ProcessingMetadata.EnrichmentData[enrichmentMainCPVDescriptionKey]), strings.TrimSpace(t.MainClassificationDescription), ) mainCode := strings.TrimSpace(t.MainClassification) documentURL := strings.TrimSpace(t.DocumentURI) if documentURL == "" { documentURL = strings.TrimSpace(t.TenderURL) } estimatedValue, currency := t.ResolvedEstimatedValueAndCurrency() response := &TenderResponse{ ID: t.ID.Hex(), NoticePublicationID: t.NoticePublicationID, RelatedNoticePublicationIDs: append([]string(nil), t.RelatedNoticePublicationIDs...), ContractFolderID: strings.TrimSpace(t.ContractFolderID), ProcurementProjectID: strings.TrimSpace(t.ProcurementProjectID), ContractNoticeID: strings.TrimSpace(t.ContractNoticeID), NoticeTypeCode: strings.TrimSpace(t.NoticeTypeCode), FormType: strings.TrimSpace(t.FormType), NoticeSubTypeCode: strings.TrimSpace(t.NoticeSubTypeCode), NoticeIdentifier: strings.TrimSpace(t.NoticeIdentifier), NoticeVersion: strings.TrimSpace(t.NoticeVersion), NoticeLanguageCode: strings.TrimSpace(t.NoticeLanguageCode), GazetteID: strings.TrimSpace(t.GazetteID), NoticeDispatchAt: t.NoticeDispatchAt, NoticeDispatchDateWithTimezone: formatNoticeDispatchRFC3339(t), ESenderDispatchAt: t.ESenderDispatchAt, IssueDate: t.IssueDate, IssueTime: t.IssueTime, Title: title, Description: description, ProcurementTypeCode: strings.TrimSpace(t.ProcurementTypeCode), ProcedureCode: strings.TrimSpace(t.ProcedureCode), MainClassification: mainCode, MainClassificationDescription: mainDesc, MainClassificationDisplay: classificationDisplay(mainCode, mainDesc), AdditionalClassifications: append([]string(nil), t.AdditionalClassifications...), EstimatedValue: estimatedValue, Currency: currency, EstimatedValueVATBasis: vatBasisUnspecified, Duration: t.Duration, DurationUnit: t.DurationUnit, PublicationDate: t.PublicationDate, TenderDeadline: t.TenderDeadline, SubmissionDeadline: t.SubmissionDeadline, ApplicationDeadline: t.ApplicationDeadline, SubmissionURL: strings.TrimSpace(t.SubmissionURL), PlaceOfPerformance: strings.TrimSpace(t.PlaceOfPerformance), CountryCode: strings.TrimSpace(t.CountryCode), RegionCode: strings.TrimSpace(t.RegionCode), CityName: strings.TrimSpace(t.CityName), PostalCode: strings.TrimSpace(t.PostalCode), DocumentURI: strings.TrimSpace(t.DocumentURI), DocumentURL: documentURL, TenderURL: strings.TrimSpace(t.TenderURL), BuyerProfileURL: strings.TrimSpace(t.BuyerProfileURL), BuyerOrganization: organizationToResponse(t.BuyerOrganization), WinningTenderer: organizationToResponse(t.WinningTenderer), Lots: buildTenderLotsResponse(t, mainDesc), Winners: buildTenderWinnersResponse(t), OfficialLanguages: append([]string(nil), t.OfficialLanguages...), AwardDate: t.AwardDate, AwardedValue: t.AwardedValue, ContractNumber: strings.TrimSpace(t.ContractNumber), Status: t.Status, Source: t.Source, TenderID: t.TenderID, ProjectName: strings.TrimSpace(t.ProjectName), CreatedAt: t.CreatedAt, OverallSummary: strings.TrimSpace(t.AIOverallSummary), } return response } func organizationToResponse(o *Organization) *OrganizationResponse { if o == nil { return nil } return &OrganizationResponse{ ID: strings.TrimSpace(o.ID), Name: strings.TrimSpace(o.Name), CompanyID: strings.TrimSpace(o.CompanyID), WebsiteURI: strings.TrimSpace(o.WebsiteURI), ContactName: strings.TrimSpace(o.ContactName), ContactTelephone: strings.TrimSpace(o.ContactTelephone), ContactEmail: strings.TrimSpace(o.ContactEmail), Address: AddressResponse{ StreetName: strings.TrimSpace(o.Address.StreetName), CityName: strings.TrimSpace(o.Address.CityName), PostalZone: strings.TrimSpace(o.Address.PostalZone), CountrySubentityCode: strings.TrimSpace(o.Address.CountrySubentityCode), Department: strings.TrimSpace(o.Address.Department), Region: strings.TrimSpace(o.Address.Region), CountryCode: strings.TrimSpace(o.Address.CountryCode), }, } } func formatNoticeDispatchRFC3339(t *Tender) string { if t == nil { return "" } sec := t.NoticeDispatchAt if sec == 0 { sec = t.ESenderDispatchAt } if sec == 0 { return "" } return time.Unix(sec, 0).UTC().Format(time.RFC3339) } func firstNonEmpty(values ...string) string { for _, v := range values { if s := strings.TrimSpace(v); s != "" { return s } } return "" } func classificationDisplay(code, description string) string { code = strings.TrimSpace(code) description = strings.TrimSpace(description) if code != "" && description != "" { return code + " - " + description } if code != "" { return code } return description } func buildTenderLotsResponse(t *Tender, globalMainDesc string) []TenderLotResponse { if t == nil || len(t.ProcurementLots) == 0 { return nil } out := make([]TenderLotResponse, 0, len(t.ProcurementLots)) mainCode := strings.TrimSpace(t.MainClassification) for i := range t.ProcurementLots { lot := t.ProcurementLots[i] lotCode := strings.TrimSpace(lot.MainClassification) perLotDesc := strings.TrimSpace(lot.MainClassificationDescription) if perLotDesc == "" && lotCode != "" && lotCode == mainCode { perLotDesc = globalMainDesc } out = append(out, TenderLotResponse{ LotID: strings.TrimSpace(lot.LotID), Title: strings.TrimSpace(lot.Title), Description: strings.TrimSpace(lot.Description), MainNatureOfContract: strings.TrimSpace(lot.MainNatureOfContract), MainClassification: lotCode, MainClassificationDescription: perLotDesc, MainClassificationDisplay: classificationDisplay(lotCode, perLotDesc), AdditionalClassifications: append([]string(nil), lot.AdditionalClassifications...), Duration: strings.TrimSpace(lot.Duration), EstimatedValue: lot.EstimatedValue, Currency: strings.TrimSpace(lot.Currency), }) } return out } func buildTenderWinnersResponse(t *Tender) []TenderWinnerResponse { if t == nil { return nil } if len(t.AwardedEntities) > 0 { out := make([]TenderWinnerResponse, 0, len(t.AwardedEntities)) for _, a := range t.AwardedEntities { w := TenderWinnerResponse{ OfficialName: strings.TrimSpace(a.Name), RegistrationNumber: strings.TrimSpace(a.CompanyID), PostalAddress: strings.TrimSpace(a.Address), Country: strings.TrimSpace(a.Country), LotID: strings.TrimSpace(a.LotID), Amount: a.Amount, Currency: strings.TrimSpace(a.Currency), Town: "", Postcode: "", CountrySubdivisionNuts: "", ContactPoint: "", Email: "", Telephone: "", } if org := organizationByID(t, a.OrganizationID); org != nil { mergeWinnerFromOrganization(&w, org) } out = append(out, w) } return out } if t.WinningTenderer == nil || strings.TrimSpace(t.WinningTenderer.Name) == "" { return nil } w := t.WinningTenderer addr := w.Address return []TenderWinnerResponse{{ OfficialName: strings.TrimSpace(w.Name), RegistrationNumber: strings.TrimSpace(w.CompanyID), PostalAddress: strings.TrimSpace(addr.StreetName), Town: strings.TrimSpace(addr.CityName), Postcode: strings.TrimSpace(addr.PostalZone), CountrySubdivisionNuts: strings.TrimSpace(addr.CountrySubentityCode), Country: strings.TrimSpace(addr.CountryCode), ContactPoint: strings.TrimSpace(w.ContactName), Email: strings.TrimSpace(w.ContactEmail), Telephone: strings.TrimSpace(w.ContactTelephone), }} } func organizationByID(t *Tender, id string) *Organization { id = strings.TrimSpace(id) if id == "" || t == nil { return nil } for i := range t.Organizations { if strings.TrimSpace(t.Organizations[i].ID) == id { return &t.Organizations[i] } } return nil } func mergeWinnerFromOrganization(w *TenderWinnerResponse, org *Organization) { if w == nil || org == nil { return } if w.OfficialName == "" { w.OfficialName = strings.TrimSpace(org.Name) } if w.RegistrationNumber == "" { w.RegistrationNumber = strings.TrimSpace(org.CompanyID) } if w.PostalAddress == "" { w.PostalAddress = strings.TrimSpace(org.Address.StreetName) } if w.Town == "" { w.Town = strings.TrimSpace(org.Address.CityName) } if w.Postcode == "" { w.Postcode = strings.TrimSpace(org.Address.PostalZone) } if w.CountrySubdivisionNuts == "" { w.CountrySubdivisionNuts = strings.TrimSpace(org.Address.CountrySubentityCode) } if w.Country == "" { w.Country = strings.TrimSpace(org.Address.CountryCode) } if w.ContactPoint == "" { w.ContactPoint = strings.TrimSpace(org.ContactName) } if w.Email == "" { w.Email = strings.TrimSpace(org.ContactEmail) } if w.Telephone == "" { w.Telephone = strings.TrimSpace(org.ContactTelephone) } } // AITranslateRequest represents a request for on-demand AI translation. type AITranslateRequest struct { Language string `json:"language" valid:"required,stringlength(2|5)~Language must be between 2 and 5 characters"` } // AITranslateResponse represents the response for AI translation trigger. type AITranslateResponse struct { ContractFolderID string `json:"contract_folder_id"` NoticePublicationID string `json:"notice_publication_id"` Language string `json:"language"` TranslatedTitle string `json:"translated_title"` TranslatedDescription string `json:"translated_description"` } // AISummaryResponse represents the response for getting an AI-generated summary type AISummaryResponse struct { NoticeID string `json:"notice_id"` OverallSummary string `json:"overall_summary"` Source string `json:"source"` // storage | pending | unavailable } // AISummarizeResponse represents the response for triggering on-demand AI summarization type AISummarizeResponse struct { ContractFolderID string `json:"contract_folder_id"` NoticePublicationID string `json:"notice_publication_id"` Summaries []AIDocumentSummary `json:"summaries"` OverallSummary *string `json:"overall_summary"` } // AIDocumentSummary represents a single document summary from the AI service type AIDocumentSummary struct { DocumentName string `json:"document_name"` DocumentType string `json:"document_type"` Summary string `json:"summary"` SummaryModel string `json:"summary_model"` Error string `json:"error,omitempty"` } // AIAnalyzeResponse represents the response for on-demand agentic analysis. type AIAnalyzeResponse struct { ContractFolderID string `json:"contract_folder_id"` NoticePublicationID string `json:"notice_publication_id"` Summary string `json:"summary"` Documents []AIAnalyzeDocument `json:"documents"` ToolCallsLog []string `json:"tool_calls_log"` Iterations int `json:"iterations"` Model string `json:"model"` } // AIAnalyzeDocument describes one document referenced during analysis. type AIAnalyzeDocument struct { Filename string `json:"filename"` SizeBytes int64 `json:"size_bytes"` TimesRead int `json:"times_read"` TimesQueried int `json:"times_queried"` Description string `json:"description"` }