Refactor Tender Management System by Removing Scraping Functionality and Enhancing Tender API
- Removed all scraping-related functionality, including routes, handlers, services, and repository methods, to streamline the tender management system. - Updated the tender API to focus solely on tender management, enhancing endpoints for listing, retrieving, and updating tenders. - Improved Swagger documentation to reflect the removal of scraping endpoints and the addition of new tender-related features. - Ensured adherence to Clean Architecture principles throughout the refactoring process, maintaining a clear separation of concerns.
This commit is contained in:
+25
-332
@@ -3,33 +3,19 @@ package tender
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
"tm/internal/company"
|
||||
"tm/pkg/logger"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"tm/pkg/response"
|
||||
)
|
||||
|
||||
// TenderService interface defines tender business logic operations
|
||||
type TenderService interface {
|
||||
// Tender operations
|
||||
CreateTender(ctx context.Context, req CreateTenderRequest) (*Tender, error)
|
||||
GetTenderByID(ctx context.Context, id string) (*Tender, error)
|
||||
GetTenderByID(ctx context.Context, id string) (*TenderResponse, error)
|
||||
UpdateTender(ctx context.Context, req UpdateTenderRequest) (*Tender, error)
|
||||
DeleteTender(ctx context.Context, id string) error
|
||||
ListTenders(ctx context.Context, req ListTendersRequest) (*ListTendersResponse, error)
|
||||
SearchTenders(ctx context.Context, req SearchTendersRequest) (*SearchTendersResponse, error)
|
||||
|
||||
// Statistics and reporting
|
||||
GetTenderStatistics(ctx context.Context) (*TenderStatistics, error)
|
||||
GetDashboardData(ctx context.Context) (*DashboardData, error)
|
||||
|
||||
// Scraping management
|
||||
StartScrapingJob(ctx context.Context, req StartScrapingJobRequest) (*ScrapingJob, error)
|
||||
GetScrapingJobStatus(ctx context.Context, jobID string) (*ScrapingJob, error)
|
||||
ListScrapingJobs(ctx context.Context, req ListScrapingJobsRequest) (*ListScrapingJobsResponse, error)
|
||||
CancelScrapingJob(ctx context.Context, jobID string) error
|
||||
|
||||
// Maintenance operations
|
||||
UpdateExpiredTenders(ctx context.Context) error
|
||||
@@ -52,92 +38,8 @@ func NewTenderService(repository TenderRepository, companyService company.Servic
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTender creates a new tender
|
||||
func (s *tenderService) CreateTender(ctx context.Context, req CreateTenderRequest) (*Tender, error) {
|
||||
s.logger.Info("Creating new tender", map[string]interface{}{
|
||||
"notice_publication_id": req.NoticePublicationID,
|
||||
"title": req.Title,
|
||||
})
|
||||
|
||||
// Check if tender with same notice publication ID already exists
|
||||
if req.NoticePublicationID != "" {
|
||||
existing, err := s.repository.GetByNoticePublicationID(ctx, req.NoticePublicationID)
|
||||
if err == nil && existing != nil {
|
||||
s.logger.Warn("Tender with same notice publication ID already exists", map[string]interface{}{
|
||||
"notice_publication_id": req.NoticePublicationID,
|
||||
"existing_id": existing.ID,
|
||||
})
|
||||
return nil, fmt.Errorf("tender with notice publication ID %s already exists", req.NoticePublicationID)
|
||||
}
|
||||
}
|
||||
|
||||
tender := &Tender{
|
||||
ContractNoticeID: req.ContractNoticeID,
|
||||
NoticePublicationID: req.NoticePublicationID,
|
||||
ContractFolderID: req.ContractFolderID,
|
||||
NoticeTypeCode: req.NoticeTypeCode,
|
||||
NoticeSubTypeCode: req.NoticeSubTypeCode,
|
||||
NoticeLanguageCode: req.NoticeLanguageCode,
|
||||
IssueDate: req.IssueDate,
|
||||
IssueTime: req.IssueTime,
|
||||
PublicationDate: req.PublicationDate,
|
||||
GazetteID: req.GazetteID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
ProcurementTypeCode: req.ProcurementTypeCode,
|
||||
ProcedureCode: req.ProcedureCode,
|
||||
MainClassification: req.MainClassification,
|
||||
AdditionalClassifications: req.AdditionalClassifications,
|
||||
EstimatedValue: req.EstimatedValue,
|
||||
Currency: req.Currency,
|
||||
Duration: req.Duration,
|
||||
DurationUnit: req.DurationUnit,
|
||||
TenderDeadline: req.TenderDeadline,
|
||||
PlaceOfPerformance: req.PlaceOfPerformance,
|
||||
CountryCode: req.CountryCode,
|
||||
RegionCode: req.RegionCode,
|
||||
CityName: req.CityName,
|
||||
PostalCode: req.PostalCode,
|
||||
DocumentURI: req.DocumentURI,
|
||||
TenderURL: req.TenderURL,
|
||||
BuyerOrganization: req.BuyerOrganization,
|
||||
ReviewOrganization: req.ReviewOrganization,
|
||||
Organizations: req.Organizations,
|
||||
SelectionCriteria: req.SelectionCriteria,
|
||||
OfficialLanguages: req.OfficialLanguages,
|
||||
Status: TenderStatusActive,
|
||||
Source: req.Source,
|
||||
SourceFileURL: req.SourceFileURL,
|
||||
SourceFileName: req.SourceFileName,
|
||||
ProcessingMetadata: ProcessingMetadata{
|
||||
ProcessedAt: time.Now().Unix(),
|
||||
ProcessingVersion: "1.0",
|
||||
},
|
||||
}
|
||||
|
||||
// Generate tender URL if not provided
|
||||
if tender.TenderURL == "" && tender.NoticePublicationID != "" {
|
||||
tender.TenderURL = generateTenderURL(tender.NoticePublicationID)
|
||||
}
|
||||
|
||||
if err := s.repository.Create(ctx, tender); err != nil {
|
||||
s.logger.Error("Failed to create tender", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"notice_publication_id": req.NoticePublicationID,
|
||||
})
|
||||
return nil, fmt.Errorf("failed to create tender: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Tender created successfully", map[string]interface{}{
|
||||
"tender_id": tender.ID,
|
||||
"notice_publication_id": tender.NoticePublicationID,
|
||||
})
|
||||
|
||||
return tender, nil
|
||||
}
|
||||
|
||||
// GetTenderByID retrieves a tender by ID
|
||||
func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*Tender, error) {
|
||||
func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*TenderResponse, error) {
|
||||
s.logger.Info("Retrieving tender by ID", map[string]interface{}{
|
||||
"tender_id": id,
|
||||
})
|
||||
@@ -151,7 +53,7 @@ func (s *tenderService) GetTenderByID(ctx context.Context, id string) (*Tender,
|
||||
return nil, fmt.Errorf("failed to retrieve tender: %w", err)
|
||||
}
|
||||
|
||||
return tender, nil
|
||||
return tender.ToTenderResponse(), nil
|
||||
}
|
||||
|
||||
// UpdateTender updates an existing tender
|
||||
@@ -265,9 +167,11 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest)
|
||||
|
||||
return &ListTendersResponse{
|
||||
Tenders: tenderResponses,
|
||||
Total: total,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
Metadata: &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -286,9 +190,11 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest)
|
||||
|
||||
return &ListTendersResponse{
|
||||
Tenders: tenderResponses,
|
||||
Total: total,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
Metadata: &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -297,79 +203,24 @@ func (s *tenderService) ListTenders(ctx context.Context, req ListTendersRequest)
|
||||
companyCPVCodes = comp.Tags.CPVCodes
|
||||
}
|
||||
|
||||
// Calculate match percentages and prepare for sorting
|
||||
tenderMatches := make([]TenderWithMatch, 0, len(tenders))
|
||||
for _, tender := range tenders {
|
||||
tenderMatch := TenderWithMatch{
|
||||
Tender: &tender,
|
||||
MatchPercentage: tender.CalculateMatchPercentage(companyCPVCodes),
|
||||
DaysUntilDeadline: tender.GetDaysUntilDeadline(),
|
||||
}
|
||||
tenderMatches = append(tenderMatches, tenderMatch)
|
||||
}
|
||||
|
||||
// Sort by match percentage (descending) and then by deadline preference
|
||||
sort.Slice(tenderMatches, func(i, j int) bool {
|
||||
matchI := tenderMatches[i].MatchPercentage
|
||||
matchJ := tenderMatches[j].MatchPercentage
|
||||
|
||||
// First, sort by match percentage (descending)
|
||||
if matchI != matchJ {
|
||||
return matchI > matchJ
|
||||
}
|
||||
|
||||
// For equal match percentages, prioritize tenders with >30 days deadline
|
||||
daysI := tenderMatches[i].DaysUntilDeadline
|
||||
daysJ := tenderMatches[j].DaysUntilDeadline
|
||||
|
||||
// Both have >30 days or both have <=30 days, sort by deadline descending (more time is better)
|
||||
if (daysI > 30 && daysJ > 30) || (daysI <= 30 && daysJ <= 30) {
|
||||
return daysI > daysJ
|
||||
}
|
||||
|
||||
// One has >30 days, one has <=30 days - prioritize the one with >30 days
|
||||
return daysI > 30
|
||||
})
|
||||
|
||||
// Convert to TenderResponse objects with match percentages
|
||||
tenderResponses := make([]TenderResponse, 0, len(tenderMatches))
|
||||
for _, tenderMatch := range tenderMatches {
|
||||
tenderResponses = append(tenderResponses, *tenderMatch.Tender.ToTenderResponseWithMatch(companyCPVCodes))
|
||||
}
|
||||
|
||||
s.logger.Info("Tenders sorted by match percentage", map[string]interface{}{
|
||||
"company_id": *req.CompanyID,
|
||||
"company_cpv_codes": len(companyCPVCodes),
|
||||
"total_tenders": len(tenderResponses),
|
||||
"total_tenders": len(tenders),
|
||||
})
|
||||
|
||||
tenderResponses := make([]TenderResponse, 0, len(tenders))
|
||||
for _, tender := range tenders {
|
||||
tenderResponses = append(tenderResponses, *tender.ToTenderResponse())
|
||||
}
|
||||
|
||||
return &ListTendersResponse{
|
||||
Tenders: tenderResponses,
|
||||
Total: total,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchTenders searches tenders based on criteria
|
||||
func (s *tenderService) SearchTenders(ctx context.Context, req SearchTendersRequest) (*SearchTendersResponse, error) {
|
||||
s.logger.Info("Searching tenders", map[string]interface{}{
|
||||
"query": req.Query,
|
||||
})
|
||||
|
||||
tenders, err := s.repository.Search(ctx, req.Criteria)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to search tenders", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"query": req.Query,
|
||||
})
|
||||
return nil, fmt.Errorf("failed to search tenders: %w", err)
|
||||
}
|
||||
|
||||
return &SearchTendersResponse{
|
||||
Tenders: tenders,
|
||||
Query: req.Query,
|
||||
Total: len(tenders),
|
||||
Metadata: &response.Meta{
|
||||
Total: int(total),
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -388,164 +239,6 @@ func (s *tenderService) GetTenderStatistics(ctx context.Context) (*TenderStatist
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetDashboardData retrieves dashboard data
|
||||
func (s *tenderService) GetDashboardData(ctx context.Context) (*DashboardData, error) {
|
||||
s.logger.Info("Retrieving dashboard data", map[string]interface{}{})
|
||||
|
||||
stats, err := s.GetTenderStatistics(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get recent tenders
|
||||
recentTenders, _, err := s.repository.List(ctx, TenderSearchCriteria{}, 10, 0)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to retrieve recent tenders", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to retrieve recent tenders: %w", err)
|
||||
}
|
||||
|
||||
// Get active scraping jobs
|
||||
activeJobs, err := s.repository.GetActiveScrapingJobs(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to retrieve active scraping jobs", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to retrieve active jobs: %w", err)
|
||||
}
|
||||
|
||||
return &DashboardData{
|
||||
Statistics: stats,
|
||||
RecentTenders: recentTenders,
|
||||
ActiveJobs: activeJobs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StartScrapingJob starts a new scraping job
|
||||
func (s *tenderService) StartScrapingJob(ctx context.Context, req StartScrapingJobRequest) (*ScrapingJob, error) {
|
||||
s.logger.Info("Starting scraping job", map[string]interface{}{
|
||||
"type": req.Type,
|
||||
})
|
||||
|
||||
job := &ScrapingJob{
|
||||
ID: primitive.NewObjectID().Hex(),
|
||||
Type: req.Type,
|
||||
Status: ScrapingJobStatusPending,
|
||||
StartedAt: time.Now().Unix(),
|
||||
Progress: JobProgress{},
|
||||
Results: JobResults{},
|
||||
Config: req.Config,
|
||||
}
|
||||
|
||||
if err := s.repository.CreateScrapingJob(ctx, job); err != nil {
|
||||
s.logger.Error("Failed to create scraping job", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"type": req.Type,
|
||||
})
|
||||
return nil, fmt.Errorf("failed to create scraping job: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Scraping job created successfully", map[string]interface{}{
|
||||
"job_id": job.ID,
|
||||
"type": job.Type,
|
||||
})
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// GetScrapingJobStatus retrieves scraping job status
|
||||
func (s *tenderService) GetScrapingJobStatus(ctx context.Context, jobID string) (*ScrapingJob, error) {
|
||||
s.logger.Info("Retrieving scraping job status", map[string]interface{}{
|
||||
"job_id": jobID,
|
||||
})
|
||||
|
||||
job, err := s.repository.GetScrapingJob(ctx, jobID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to retrieve scraping job", map[string]interface{}{
|
||||
"job_id": jobID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to retrieve scraping job: %w", err)
|
||||
}
|
||||
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// ListScrapingJobs lists scraping jobs with pagination
|
||||
func (s *tenderService) ListScrapingJobs(ctx context.Context, req ListScrapingJobsRequest) (*ListScrapingJobsResponse, error) {
|
||||
s.logger.Info("Listing scraping jobs", map[string]interface{}{
|
||||
"limit": req.Limit,
|
||||
"offset": req.Offset,
|
||||
})
|
||||
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 20
|
||||
}
|
||||
if req.Limit > 100 {
|
||||
req.Limit = 100
|
||||
}
|
||||
|
||||
jobs, total, err := s.repository.ListScrapingJobs(ctx, req.Limit, req.Offset)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to list scraping jobs", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("failed to list scraping jobs: %w", err)
|
||||
}
|
||||
|
||||
return &ListScrapingJobsResponse{
|
||||
Jobs: jobs,
|
||||
Total: total,
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CancelScrapingJob cancels a running scraping job
|
||||
func (s *tenderService) CancelScrapingJob(ctx context.Context, jobID string) error {
|
||||
s.logger.Info("Cancelling scraping job", map[string]interface{}{
|
||||
"job_id": jobID,
|
||||
})
|
||||
|
||||
job, err := s.repository.GetScrapingJob(ctx, jobID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to find scraping job for cancellation", map[string]interface{}{
|
||||
"job_id": jobID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to find scraping job: %w", err)
|
||||
}
|
||||
|
||||
if job.Status != ScrapingJobStatusRunning && job.Status != ScrapingJobStatusPending {
|
||||
s.logger.Warn("Cannot cancel scraping job in current status", map[string]interface{}{
|
||||
"job_id": jobID,
|
||||
"status": job.Status,
|
||||
})
|
||||
return fmt.Errorf("cannot cancel job in status: %s", job.Status)
|
||||
}
|
||||
|
||||
job.Status = ScrapingJobStatusCancelled
|
||||
completedAt := time.Now().Unix()
|
||||
job.CompletedAt = &completedAt
|
||||
duration := completedAt - job.StartedAt
|
||||
job.Duration = &duration
|
||||
|
||||
if err := s.repository.UpdateScrapingJob(ctx, job); err != nil {
|
||||
s.logger.Error("Failed to update cancelled scraping job", map[string]interface{}{
|
||||
"job_id": jobID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return fmt.Errorf("failed to update scraping job: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Scraping job cancelled successfully", map[string]interface{}{
|
||||
"job_id": jobID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateExpiredTenders updates status of expired tenders
|
||||
func (s *tenderService) UpdateExpiredTenders(ctx context.Context) error {
|
||||
s.logger.Info("Updating expired tenders", map[string]interface{}{})
|
||||
|
||||
Reference in New Issue
Block a user