package document_scraper import ( "context" "fmt" "time" "tm/internal/tender" "tm/pkg/logger" "tm/pkg/response" "tm/ted" ) // Service defines business logic for the document scraper API type Service interface { // ListPendingTenders retrieves tenders that need document scraping ListPendingTenders(ctx context.Context, limit, offset int) (*DocumentScraperListResponse, *response.Meta, error) // GetTenderByNoticeID retrieves a specific tender by its notice publication ID GetTenderByNoticeID(ctx context.Context, noticeID string) (*DocumentScraperTenderResponse, error) } type service struct { tenderRepo tender.TenderRepository tedParser *ted.TEDParser logger logger.Logger } // NewService creates a new document scraper service func NewService(tenderRepo tender.TenderRepository, logger logger.Logger) Service { return &service{ tenderRepo: tenderRepo, tedParser: ted.NewTEDParser(), logger: logger, } } // buildTenderResponse creates a DocumentScraperTenderResponse from a tender func (s *service) buildTenderResponse(_ context.Context, t *tender.Tender) *DocumentScraperTenderResponse { documentURL := t.DocumentURI if documentURL == "" { documentURL = t.TenderURL } return &DocumentScraperTenderResponse{ ContractFolderID: t.ContractFolderID, NoticePublicationID: t.NoticePublicationID, DocumentURL: documentURL, Title: t.Title, Description: t.Description, } } // ListPendingTenders retrieves tenders that have not yet been scraped for documents. // Only returns tenders from Sweden with active deadlines (deadline not yet reached). func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*DocumentScraperListResponse, *response.Meta, error) { s.logger.Info("Listing pending tenders for document scraper", map[string]interface{}{ "limit": limit, "offset": offset, }) now := time.Now().Unix() // Fetch a larger batch to account for filtering, then apply Sweden + active deadline filters // We need to over-fetch since the repository doesn't filter by country/deadline fetchLimit := limit * 5 if fetchLimit > 500 { fetchLimit = 500 } var filteredTenders []tender.Tender currentOffset := offset hasMore := true for hasMore { tenders, _, err := s.tenderRepo.GetUnScrapedTenders(ctx, fetchLimit, currentOffset) if err != nil { s.logger.Error("Failed to list pending tenders", map[string]interface{}{ "error": err.Error(), }) return nil, nil, fmt.Errorf("failed to list pending tenders: %w", err) } if len(tenders) == 0 { hasMore = false break } for i := range tenders { if tenders[i].CountryCode == "SWE" && tenders[i].TenderDeadline > now { filteredTenders = append(filteredTenders, tenders[i]) } if len(filteredTenders) >= limit { break } } if len(filteredTenders) >= limit { filteredTenders = filteredTenders[:limit] hasMore = false } else { currentOffset += fetchLimit } } // Convert to response result := &DocumentScraperListResponse{ Tenders: make([]DocumentScraperTenderResponse, len(filteredTenders)), } for i := range filteredTenders { result.Tenders[i] = *s.buildTenderResponse(ctx, &filteredTenders[i]) } // Build pagination metadata meta := &response.Meta{ Total: len(filteredTenders), Limit: limit, Offset: offset, } if meta.Limit > 0 { meta.Page = (offset / meta.Limit) + 1 meta.Pages = 1 } s.logger.Info("Pending tenders retrieved successfully", map[string]interface{}{ "count": len(filteredTenders), }) return result, meta, nil } // GetTenderByNoticeID retrieves a specific tender by its notice publication ID. // Only returns tenders from Sweden with active deadlines (deadline not yet reached). func (s *service) GetTenderByNoticeID(ctx context.Context, noticeID string) (*DocumentScraperTenderResponse, error) { s.logger.Info("Getting tender by notice ID for document scraper", map[string]interface{}{ "notice_id": noticeID, }) t, err := s.tenderRepo.GetByNoticePublicationID(ctx, noticeID) if err != nil { s.logger.Error("Failed to get tender by notice ID", map[string]interface{}{ "notice_id": noticeID, "error": err.Error(), }) return nil, fmt.Errorf("failed to get tender: %w", err) } if t == nil { return nil, fmt.Errorf("tender not found") } // Only return tenders from Sweden with active deadlines now := time.Now().Unix() if t.CountryCode != "SWE" || t.TenderDeadline <= now { s.logger.Info("Tender filtered out: not Sweden or deadline passed", map[string]interface{}{ "notice_id": noticeID, "country_code": t.CountryCode, "tender_deadline": t.TenderDeadline, "now": now, }) return nil, fmt.Errorf("tender not found") } result := s.buildTenderResponse(ctx, t) s.logger.Info("Tender retrieved successfully for document scraper", map[string]interface{}{ "notice_id": noticeID, "tender_id": t.ID.Hex(), }) return result, nil }