Add GetScrapePortals endpoint and related service functionality
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Introduced the GetScrapePortals method in the AI pipeline handler to list document scraping portals supported by the Opplens AI service. - Updated the service layer to include GetScrapePortals, which retrieves the portals from the client and handles errors appropriately. - Enhanced the routes to register the new endpoint for retrieving scrape portals. - Added a new error type for invalid date ranges in the document scraper, improving validation and error handling. This update expands the AI pipeline capabilities, allowing for better management of document scraping portals within the tender management system.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
package document_scraper
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrInvalidDateRange is returned when created_at_from is after created_at_to.
|
||||
ErrInvalidDateRange = errors.New("invalid date range: created_at_from must be before or equal to created_at_to")
|
||||
)
|
||||
@@ -2,8 +2,24 @@ package document_scraper
|
||||
|
||||
// DocumentScraperListRequest represents the request to list tenders pending document scraping
|
||||
type DocumentScraperListRequest struct {
|
||||
Limit int `query:"limit" valid:"optional,range(1|100)" default:"20"`
|
||||
Offset int `query:"offset" valid:"optional,range(0|1000000)" default:"0"`
|
||||
Limit int `query:"limit" valid:"optional,range(1|100)"`
|
||||
Offset int `query:"offset" valid:"optional,range(0|1000000)"`
|
||||
CreatedAtFrom *int64 `query:"created_at_from" valid:"optional"`
|
||||
CreatedAtTo *int64 `query:"created_at_to" valid:"optional"`
|
||||
IncludeExpired bool `query:"include_expired" valid:"optional"`
|
||||
}
|
||||
|
||||
// Normalize applies default pagination bounds.
|
||||
func (r *DocumentScraperListRequest) Normalize() {
|
||||
if r.Limit <= 0 {
|
||||
r.Limit = 20
|
||||
}
|
||||
if r.Limit > 100 {
|
||||
r.Limit = 100
|
||||
}
|
||||
if r.Offset < 0 {
|
||||
r.Offset = 0
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentScraperGetRequest represents the request to get a specific tender by notice publication ID
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package document_scraper
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"errors"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
|
||||
@@ -24,36 +24,31 @@ func NewHandler(service Service, logger logger.Logger) *Handler {
|
||||
|
||||
// ListPendingTenders retrieves tenders that need document scraping
|
||||
// @Summary List pending tenders for document scraping
|
||||
// @Description Retrieve tenders that have not yet been scraped for documents. Each item includes contract_folder_id, notice_publication_id, document_url, title, and description.
|
||||
// @Description Retrieve tenders that have not yet been scraped for documents. Each item includes contract_folder_id, notice_publication_id, document_url, title, and description. Optionally filter by created_at range (Unix timestamps in seconds). By default only tenders with active deadlines are returned; set include_expired=true to include expired tenders.
|
||||
// @Tags Document-Scraper
|
||||
// @Produce json
|
||||
// @Param limit query int false "Number of items per page (default: 20, max: 100)"
|
||||
// @Param offset query int false "Number of items to skip (default: 0)"
|
||||
// @Param created_at_from query int64 false "Filter by created_at lower bound (Unix timestamp, seconds)"
|
||||
// @Param created_at_to query int64 false "Filter by created_at upper bound (Unix timestamp, seconds)"
|
||||
// @Param include_expired query bool false "When true, include tenders whose deadline has passed (default: false)"
|
||||
// @Success 200 {object} response.APIResponse{data=DocumentScraperListResponse} "List of pending tenders"
|
||||
// @Failure 400 {object} response.APIResponse "Invalid query parameters"
|
||||
// @Failure 401 {object} response.APIResponse "Invalid or missing API key"
|
||||
// @Failure 500 {object} response.APIResponse "Internal server error"
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v1/scraper/tenders [get]
|
||||
func (h *Handler) ListPendingTenders(c echo.Context) error {
|
||||
// Parse pagination parameters
|
||||
limit, err := strconv.Atoi(c.QueryParam("limit"))
|
||||
if err != nil || limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
offset, err := strconv.Atoi(c.QueryParam("offset"))
|
||||
if err != nil || offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
tenders, meta, err := h.service.ListPendingTenders(c.Request().Context(), limit, offset)
|
||||
form, err := response.Parse[DocumentScraperListRequest](c)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to list pending tenders", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
tenders, meta, err := h.service.ListPendingTenders(c.Request().Context(), *form)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrInvalidDateRange) {
|
||||
return response.BadRequest(c, "Invalid date range", err.Error())
|
||||
}
|
||||
return response.InternalServerError(c, "Failed to list pending tenders")
|
||||
}
|
||||
|
||||
@@ -84,10 +79,6 @@ func (h *Handler) GetTenderByNoticeID(c echo.Context) error {
|
||||
if err.Error() == "tender not found" {
|
||||
return response.NotFound(c, "Tender not found")
|
||||
}
|
||||
h.logger.Error("Failed to get tender by notice ID", map[string]interface{}{
|
||||
"notice_id": noticeID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return response.InternalServerError(c, "Failed to get tender")
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func isDocumentScraperCountry(countryCode string) bool {
|
||||
// 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)
|
||||
ListPendingTenders(ctx context.Context, req DocumentScraperListRequest) (*DocumentScraperListResponse, *response.Meta, error)
|
||||
// GetTenderByNoticeID retrieves a specific tender by its notice publication ID
|
||||
GetTenderByNoticeID(ctx context.Context, noticeID string) (*DocumentScraperTenderResponse, error)
|
||||
}
|
||||
@@ -42,16 +42,36 @@ func NewService(tenderRepo tender.TenderRepository, logger logger.Logger) Servic
|
||||
}
|
||||
|
||||
// ListPendingTenders retrieves tenders that have not yet been scraped for documents.
|
||||
// Only returns tenders from Sweden, Finland, and Norway with active deadlines (deadline not yet reached).
|
||||
func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*DocumentScraperListResponse, *response.Meta, error) {
|
||||
// Only returns tenders from Sweden, Finland, and Norway. By default only tenders with active
|
||||
// deadlines are included; set IncludeExpired to include tenders whose deadline has passed.
|
||||
func (s *service) ListPendingTenders(ctx context.Context, req DocumentScraperListRequest) (*DocumentScraperListResponse, *response.Meta, error) {
|
||||
req.Normalize()
|
||||
|
||||
if req.CreatedAtFrom != nil && req.CreatedAtTo != nil && *req.CreatedAtFrom > *req.CreatedAtTo {
|
||||
return nil, nil, ErrInvalidDateRange
|
||||
}
|
||||
|
||||
s.logger.Info("Listing pending tenders for document scraper", map[string]interface{}{
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"limit": req.Limit,
|
||||
"offset": req.Offset,
|
||||
"created_at_from": req.CreatedAtFrom,
|
||||
"created_at_to": req.CreatedAtTo,
|
||||
"include_expired": req.IncludeExpired,
|
||||
})
|
||||
|
||||
now := time.Now().Unix()
|
||||
filter := tender.PendingDocumentScrapeFilter{
|
||||
CountryCodes: documentScraperCountryCodes,
|
||||
CreatedAtFrom: req.CreatedAtFrom,
|
||||
CreatedAtTo: req.CreatedAtTo,
|
||||
Limit: req.Limit,
|
||||
Skip: req.Offset,
|
||||
}
|
||||
if !req.IncludeExpired {
|
||||
now := time.Now().Unix()
|
||||
filter.MinDeadline = &now
|
||||
}
|
||||
|
||||
tenders, hasMore, err := s.tenderRepo.GetPendingDocumentScrapeTenders(ctx, documentScraperCountryCodes, now, limit, offset)
|
||||
tenders, hasMore, err := s.tenderRepo.GetPendingDocumentScrapeTenders(ctx, filter)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to list pending tenders", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -69,12 +89,12 @@ func (s *service) ListPendingTenders(ctx context.Context, limit, offset int) (*D
|
||||
|
||||
meta := &response.Meta{
|
||||
Total: len(tenders),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
HasMore: hasMore,
|
||||
}
|
||||
if meta.Limit > 0 {
|
||||
meta.Page = (offset / meta.Limit) + 1
|
||||
meta.Page = (req.Offset / meta.Limit) + 1
|
||||
}
|
||||
|
||||
s.logger.Info("Pending tenders retrieved successfully", map[string]interface{}{
|
||||
|
||||
Reference in New Issue
Block a user