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:
n.nakhostin
2025-08-16 16:52:17 +03:30
parent 21b736b55b
commit e986ee4135
11 changed files with 239 additions and 4266 deletions
+25 -332
View File
@@ -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{}{})