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
+13 -364
View File
@@ -24,40 +24,6 @@ func NewTenderHandler(service TenderService, logger logger.Logger) *TenderHandle
}
}
// CreateTender creates a new tender
// @Summary Create a new tender
// @Description Create a new tender with the provided information
// @Tags Admin-Tenders
// @Accept json
// @Produce json
// @Param tender body CreateTenderRequest true "Tender information"
// @Success 201 {object} response.APIResponse{data=TenderResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 409 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/tenders [post]
// @Security BearerAuth
func (h *TenderHandler) CreateTender(c echo.Context) error {
var req CreateTenderRequest
if err := c.Bind(&req); err != nil {
return response.ValidationError(c, "Invalid request format", err.Error())
}
if _, err := govalidator.ValidateStruct(req); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
tender, err := h.service.CreateTender(c.Request().Context(), req)
if err != nil {
if err.Error() == "tender with notice publication ID already exists" {
return response.Conflict(c, "Tender already exists")
}
return response.InternalServerError(c, "Failed to create tender")
}
return response.Created(c, tender.ToTenderResponse(), "Tender created successfully")
}
// GetTender retrieves a tender by ID
// @Summary Get tender by ID
// @Description Retrieve a specific tender by its ID
@@ -80,7 +46,7 @@ func (h *TenderHandler) GetTender(c echo.Context) error {
return response.NotFound(c, "Tender not found")
}
return response.Success(c, tender.ToTenderResponse(), "Tender retrieved successfully")
return response.Success(c, tender, "Tender retrieved successfully")
}
// UpdateTender updates an existing tender
@@ -250,247 +216,7 @@ func (h *TenderHandler) ListTenders(c echo.Context) error {
return response.InternalServerError(c, "Failed to retrieve tenders")
}
responseData := map[string]interface{}{
"tenders": result.Tenders,
"total": result.Total,
"limit": result.Limit,
"offset": result.Offset,
}
return response.Success(c, responseData, "Tenders retrieved successfully")
}
// SearchTenders searches tenders
// @Summary Search tenders
// @Description Search tenders using full-text search and filtering
// @Tags Admin-Tenders
// @Accept json
// @Produce json
// @Param search body SearchTendersRequest true "Search criteria"
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/tenders/search [post]
// @Security BearerAuth
func (h *TenderHandler) SearchTenders(c echo.Context) error {
var req SearchTendersRequest
if err := c.Bind(&req); err != nil {
return response.ValidationError(c, "Invalid request format", err.Error())
}
if _, err := govalidator.ValidateStruct(req); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
result, err := h.service.SearchTenders(c.Request().Context(), req)
if err != nil {
return response.InternalServerError(c, "Failed to search tenders")
}
// Convert tenders to response format
tenderResponses := make([]TenderResponse, len(result.Tenders))
for i, tender := range result.Tenders {
tenderResponses[i] = *tender.ToTenderResponse()
}
responseData := map[string]interface{}{
"tenders": tenderResponses,
"query": result.Query,
"total": result.Total,
}
return response.Success(c, responseData, "Tenders found successfully")
}
// GetTenderStatistics retrieves tender statistics
// @Summary Get tender statistics
// @Description Retrieve comprehensive statistics about tenders
// @Tags Admin-Tenders
// @Produce json
// @Success 200 {object} response.APIResponse{data=TenderStatistics}
// @Failure 500 {object} response.APIResponse
// @Router /admin/tenders/statistics [get]
// @Security BearerAuth
func (h *TenderHandler) GetTenderStatistics(c echo.Context) error {
stats, err := h.service.GetTenderStatistics(c.Request().Context())
if err != nil {
return response.InternalServerError(c, "Failed to retrieve statistics")
}
return response.Success(c, stats, "Statistics retrieved successfully")
}
// GetDashboard retrieves dashboard data
// @Summary Get dashboard data
// @Description Retrieve dashboard data including statistics, recent tenders, and active jobs
// @Tags Admin-Tenders
// @Produce json
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
// @Failure 500 {object} response.APIResponse
// @Router /admin/tenders/dashboard [get]
// @Security BearerAuth
func (h *TenderHandler) GetDashboard(c echo.Context) error {
dashboard, err := h.service.GetDashboardData(c.Request().Context())
if err != nil {
return response.InternalServerError(c, "Failed to retrieve dashboard data")
}
// Convert recent tenders to response format
recentTenderResponses := make([]TenderResponse, len(dashboard.RecentTenders))
for i, tender := range dashboard.RecentTenders {
recentTenderResponses[i] = *tender.ToTenderResponse()
}
// Convert active jobs to response format
activeJobResponses := make([]ScrapingJobResponse, len(dashboard.ActiveJobs))
for i, job := range dashboard.ActiveJobs {
activeJobResponses[i] = *job.ToScrapingJobResponse()
}
responseData := map[string]interface{}{
"statistics": dashboard.Statistics,
"recent_tenders": recentTenderResponses,
"active_jobs": activeJobResponses,
}
return response.Success(c, responseData, "Dashboard data retrieved successfully")
}
// StartScrapingJob starts a new scraping job
// @Summary Start scraping job
// @Description Start a new TED XML scraping job
// @Tags Admin-Scraping
// @Accept json
// @Produce json
// @Param job body StartScrapingJobRequest true "Scraping job configuration"
// @Success 201 {object} response.APIResponse{data=ScrapingJobResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/scraping/jobs [post]
// @Security BearerAuth
func (h *TenderHandler) StartScrapingJob(c echo.Context) error {
var req StartScrapingJobRequest
if err := c.Bind(&req); err != nil {
return response.ValidationError(c, "Invalid request format", err.Error())
}
if _, err := govalidator.ValidateStruct(req); err != nil {
return response.ValidationError(c, "Validation failed", err.Error())
}
job, err := h.service.StartScrapingJob(c.Request().Context(), req)
if err != nil {
return response.InternalServerError(c, "Failed to start scraping job")
}
return response.Created(c, job.ToScrapingJobResponse(), "Scraping job started successfully")
}
// GetScrapingJob retrieves a scraping job by ID
// @Summary Get scraping job
// @Description Retrieve a specific scraping job by its ID
// @Tags Admin-Scraping
// @Produce json
// @Param id path string true "Scraping job ID"
// @Success 200 {object} response.APIResponse{data=ScrapingJobResponse}
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/scraping/jobs/{id} [get]
// @Security BearerAuth
func (h *TenderHandler) GetScrapingJob(c echo.Context) error {
id := c.Param("id")
if id == "" {
return response.BadRequest(c, "Scraping job ID is required", "ID parameter cannot be empty")
}
job, err := h.service.GetScrapingJobStatus(c.Request().Context(), id)
if err != nil {
return response.NotFound(c, "Scraping job not found")
}
return response.Success(c, job.ToScrapingJobResponse(), "Scraping job retrieved successfully")
}
// ListScrapingJobs retrieves scraping jobs with pagination
// @Summary List scraping jobs
// @Description Retrieve scraping jobs with pagination
// @Tags Admin-Scraping
// @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)"
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /admin/scraping/jobs [get]
func (h *TenderHandler) ListScrapingJobs(c echo.Context) error {
// Parse pagination parameters
limit := 20
if l := c.QueryParam("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
limit = parsed
}
}
offset := 0
if o := c.QueryParam("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
req := ListScrapingJobsRequest{
Limit: limit,
Offset: offset,
}
result, err := h.service.ListScrapingJobs(c.Request().Context(), req)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve scraping jobs")
}
// Convert jobs to response format
jobResponses := make([]ScrapingJobResponse, len(result.Jobs))
for i, job := range result.Jobs {
jobResponses[i] = *job.ToScrapingJobResponse()
}
responseData := map[string]interface{}{
"jobs": jobResponses,
"total": result.Total,
"limit": result.Limit,
"offset": result.Offset,
}
return response.Success(c, responseData, "Scraping jobs retrieved successfully")
}
// CancelScrapingJob cancels a running scraping job
// @Summary Cancel scraping job
// @Description Cancel a running or pending scraping job
// @Tags Admin-Scraping
// @Param id path string true "Scraping job ID"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/scraping/jobs/{id}/cancel [post]
// @Security BearerAuth
func (h *TenderHandler) CancelScrapingJob(c echo.Context) error {
id := c.Param("id")
if id == "" {
return response.BadRequest(c, "Scraping job ID is required", "ID parameter cannot be empty")
}
err := h.service.CancelScrapingJob(c.Request().Context(), id)
if err != nil {
if err.Error() == "scraping job not found" {
return response.NotFound(c, "Scraping job not found")
}
return response.InternalServerError(c, "Failed to cancel scraping job")
}
return response.Success(c, map[string]interface{}{"cancelled": true}, "Scraping job cancelled successfully")
return response.Success(c, result, "Tenders retrieved successfully")
}
// GetPublicTenders retrieves public tenders for mobile app
@@ -511,17 +237,13 @@ func (h *TenderHandler) CancelScrapingJob(c echo.Context) error {
func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
// Parse pagination parameters (more restrictive for public API)
limit := 20
if l := c.QueryParam("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 50 {
limit = parsed
}
if parsed, err := strconv.Atoi(c.QueryParam("limit")); err == nil && parsed > 0 && parsed <= 50 {
limit = parsed
}
offset := 0
if o := c.QueryParam("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
if parsed, err := strconv.Atoi(c.QueryParam("offset")); err == nil && parsed >= 0 {
offset = parsed
}
// Build search criteria for public API (only active tenders)
@@ -534,21 +256,17 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
}
// Parse numeric parameters
if minValue := c.QueryParam("min_estimated_value"); minValue != "" {
if parsed, err := strconv.ParseFloat(minValue, 64); err == nil {
criteria.MinEstimatedValue = &parsed
}
if parsed, err := strconv.ParseFloat(c.QueryParam("min_estimated_value"), 64); err == nil {
criteria.MinEstimatedValue = &parsed
}
if maxValue := c.QueryParam("max_estimated_value"); maxValue != "" {
if parsed, err := strconv.ParseFloat(maxValue, 64); err == nil {
criteria.MaxEstimatedValue = &parsed
}
if parsed, err := strconv.ParseFloat(c.QueryParam("max_estimated_value"), 64); err == nil {
criteria.MaxEstimatedValue = &parsed
}
// Get company ID from customer context for automatic matching
var companyID *string
if customerCompanyID, ok := c.Get("company_id").(string); ok && customerCompanyID != "" {
if customerCompanyID, ok := c.Get("company_id").(string); ok {
companyID = &customerCompanyID
}
@@ -564,43 +282,7 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
return response.InternalServerError(c, "Failed to retrieve tenders")
}
// Convert tenders to public response format (limited fields)
publicTenders := make([]map[string]interface{}, len(result.Tenders))
for i, tender := range result.Tenders {
tenderData := map[string]interface{}{
"id": tender.ID,
"title": tender.Title,
"description": tender.Description,
"procurement_type_code": tender.ProcurementTypeCode,
"main_classification": tender.MainClassification,
"estimated_value": tender.EstimatedValue,
"currency": tender.Currency,
"formatted_estimated_value": tender.FormattedEstimatedValue,
"tender_deadline": tender.TenderDeadline,
"country_code": tender.CountryCode,
"city_name": tender.CityName,
"tender_url": tender.TenderURL,
"publication_date": tender.PublicationDate,
"buyer_organization": tender.BuyerOrganization,
"is_expired": tender.IsExpired,
}
// Add match percentage if available
if tender.MatchPercentage != nil {
tenderData["match_percentage"] = *tender.MatchPercentage
}
publicTenders[i] = tenderData
}
responseData := map[string]interface{}{
"tenders": publicTenders,
"total": result.Total,
"limit": result.Limit,
"offset": result.Offset,
}
return response.Success(c, responseData, "Tenders retrieved successfully")
return response.Success(c, result, "Tenders retrieved successfully")
}
// GetPublicTenderDetails retrieves public tender details for mobile app
@@ -625,40 +307,7 @@ func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error {
return response.NotFound(c, "Tender not found")
}
// Return public-safe tender details
publicTender := map[string]interface{}{
"id": tender.ID,
"notice_publication_id": tender.NoticePublicationID,
"title": tender.Title,
"description": tender.Description,
"procurement_type_code": tender.ProcurementTypeCode,
"procedure_code": tender.ProcedureCode,
"main_classification": tender.MainClassification,
"additional_classifications": tender.AdditionalClassifications,
"estimated_value": tender.EstimatedValue,
"currency": tender.Currency,
"formatted_estimated_value": tender.GetFormattedEstimatedValue(),
"duration": tender.Duration,
"duration_unit": tender.DurationUnit,
"tender_deadline": tender.TenderDeadline,
"place_of_performance": tender.PlaceOfPerformance,
"country_code": tender.CountryCode,
"region_code": tender.RegionCode,
"city_name": tender.CityName,
"postal_code": tender.PostalCode,
"document_uri": tender.DocumentURI,
"tender_url": tender.TenderURL,
"buyer_organization": tender.BuyerOrganization,
"organizations": tender.Organizations,
"selection_criteria": tender.SelectionCriteria,
"official_languages": tender.OfficialLanguages,
"publication_date": tender.PublicationDate,
"gazette_id": tender.GazetteID,
"is_active": tender.IsActive(),
"is_expired": tender.IsExpired(),
}
return response.Success(c, publicTender, "Tender details retrieved successfully")
return response.Success(c, tender, "Tender details retrieved successfully")
}
// parseStringArray parses a comma-separated string into a string array