0a23ff985a
- Updated the tender service to include company-based matching for tenders, allowing for more relevant results based on company CPV codes. - Modified the ListTenders endpoint to accept a company ID parameter for calculating match percentages and sorting tenders accordingly. - Refactored the tender response structure to include match percentage and days until deadline for better client-side handling. - Updated API documentation to reflect changes in the tender listing functionality and added new endpoints for public tender access. - Removed deprecated customer-related methods and streamlined customer listing functionality for improved clarity and performance.
677 lines
23 KiB
Go
677 lines
23 KiB
Go
package tender
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/response"
|
|
|
|
"github.com/asaskevich/govalidator"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// TenderHandler handles tender-related HTTP requests
|
|
type TenderHandler struct {
|
|
service TenderService
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewTenderHandler creates a new tender handler
|
|
func NewTenderHandler(service TenderService, logger logger.Logger) *TenderHandler {
|
|
return &TenderHandler{
|
|
service: service,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// @Tags Admin-Tenders
|
|
// @Produce json
|
|
// @Param id path string true "Tender ID"
|
|
// @Success 200 {object} response.APIResponse{data=TenderResponse}
|
|
// @Failure 404 {object} response.APIResponse
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/tenders/{id} [get]
|
|
// @Security BearerAuth
|
|
func (h *TenderHandler) GetTender(c echo.Context) error {
|
|
id := c.Param("id")
|
|
if id == "" {
|
|
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
|
}
|
|
|
|
tender, err := h.service.GetTenderByID(c.Request().Context(), id)
|
|
if err != nil {
|
|
return response.NotFound(c, "Tender not found")
|
|
}
|
|
|
|
return response.Success(c, tender.ToTenderResponse(), "Tender retrieved successfully")
|
|
}
|
|
|
|
// UpdateTender updates an existing tender
|
|
// @Summary Update tender
|
|
// @Description Update an existing tender with the provided information
|
|
// @Tags Admin-Tenders
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path string true "Tender ID"
|
|
// @Param tender body UpdateTenderRequest true "Updated tender information"
|
|
// @Success 200 {object} response.APIResponse{data=TenderResponse}
|
|
// @Failure 400 {object} response.APIResponse
|
|
// @Failure 404 {object} response.APIResponse
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/tenders/{id} [put]
|
|
// @Security BearerAuth
|
|
func (h *TenderHandler) UpdateTender(c echo.Context) error {
|
|
id := c.Param("id")
|
|
if id == "" {
|
|
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
|
}
|
|
|
|
var req UpdateTenderRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return response.ValidationError(c, "Invalid request format", err.Error())
|
|
}
|
|
|
|
req.ID = id
|
|
|
|
if _, err := govalidator.ValidateStruct(req); err != nil {
|
|
return response.ValidationError(c, "Validation failed", err.Error())
|
|
}
|
|
|
|
tender, err := h.service.UpdateTender(c.Request().Context(), req)
|
|
if err != nil {
|
|
if err.Error() == "tender not found" {
|
|
return response.NotFound(c, "Tender not found")
|
|
}
|
|
return response.InternalServerError(c, "Failed to update tender")
|
|
}
|
|
|
|
return response.Success(c, tender.ToTenderResponse(), "Tender updated successfully")
|
|
}
|
|
|
|
// DeleteTender deletes a tender
|
|
// @Summary Delete tender
|
|
// @Description Delete a tender by its ID
|
|
// @Tags Admin-Tenders
|
|
// @Param id path string true "Tender ID"
|
|
// @Success 200 {object} response.APIResponse
|
|
// @Failure 404 {object} response.APIResponse
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/tenders/{id} [delete]
|
|
// @Security BearerAuth
|
|
func (h *TenderHandler) DeleteTender(c echo.Context) error {
|
|
id := c.Param("id")
|
|
if id == "" {
|
|
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
|
}
|
|
|
|
err := h.service.DeleteTender(c.Request().Context(), id)
|
|
if err != nil {
|
|
if err.Error() == "tender not found" {
|
|
return response.NotFound(c, "Tender not found")
|
|
}
|
|
return response.InternalServerError(c, "Failed to delete tender")
|
|
}
|
|
|
|
return response.Success(c, map[string]interface{}{"deleted": true}, "Tender deleted successfully")
|
|
}
|
|
|
|
// ListTenders retrieves tenders with pagination and filtering
|
|
// @Summary List tenders
|
|
// @Description Retrieve tenders with pagination, filtering, and search capabilities with optional company-based matching
|
|
// @Tags Admin-Tenders
|
|
// @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 company_id query string false "Company ID for match percentage calculation and sorting"
|
|
// @Param query query string false "Search query for title and description"
|
|
// @Param notice_type query string false "Filter by notice type code"
|
|
// @Param procurement_type query string false "Filter by procurement type code"
|
|
// @Param country_codes query []string false "Filter by country codes (comma-separated)"
|
|
// @Param status query []string false "Filter by status (comma-separated)"
|
|
// @Param source query []string false "Filter by source (comma-separated)"
|
|
// @Param min_estimated_value query number false "Minimum estimated value"
|
|
// @Param max_estimated_value query number false "Maximum estimated value"
|
|
// @Param currency query string false "Filter by currency"
|
|
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
|
// @Failure 400 {object} response.APIResponse
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /admin/tenders [get]
|
|
// @Security BearerAuth
|
|
func (h *TenderHandler) ListTenders(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
|
|
}
|
|
}
|
|
|
|
// Build search criteria
|
|
criteria := TenderSearchCriteria{
|
|
Query: c.QueryParam("query"),
|
|
NoticeType: c.QueryParam("notice_type"),
|
|
ProcurementType: c.QueryParam("procurement_type"),
|
|
Currency: c.QueryParam("currency"),
|
|
}
|
|
|
|
// Parse array parameters
|
|
if countryCodes := c.QueryParam("country_codes"); countryCodes != "" {
|
|
criteria.CountryCodes = parseStringArray(countryCodes)
|
|
}
|
|
|
|
if statuses := c.QueryParam("status"); statuses != "" {
|
|
statusStrings := parseStringArray(statuses)
|
|
criteria.Status = make([]TenderStatus, len(statusStrings))
|
|
for i, s := range statusStrings {
|
|
criteria.Status[i] = TenderStatus(s)
|
|
}
|
|
}
|
|
|
|
if sources := c.QueryParam("source"); sources != "" {
|
|
sourceStrings := parseStringArray(sources)
|
|
criteria.Source = make([]TenderSource, len(sourceStrings))
|
|
for i, s := range sourceStrings {
|
|
criteria.Source[i] = TenderSource(s)
|
|
}
|
|
}
|
|
|
|
// Parse numeric parameters
|
|
if minValue := c.QueryParam("min_estimated_value"); minValue != "" {
|
|
if parsed, err := strconv.ParseFloat(minValue, 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
|
|
}
|
|
}
|
|
|
|
// Parse company_id parameter for matching
|
|
var companyID *string
|
|
if companyIDParam := c.QueryParam("company_id"); companyIDParam != "" {
|
|
companyID = &companyIDParam
|
|
}
|
|
|
|
req := ListTendersRequest{
|
|
Criteria: criteria,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
CompanyID: companyID,
|
|
}
|
|
|
|
result, err := h.service.ListTenders(c.Request().Context(), req)
|
|
if err != nil {
|
|
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
|
|
// @Router /admin/scraping/jobs [get]
|
|
// @Security BearerAuth
|
|
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")
|
|
}
|
|
|
|
// GetPublicTenders retrieves public tenders for mobile app
|
|
// @Summary Get public tenders
|
|
// @Description Retrieve active tenders for mobile application users with automatic company-based matching from customer profile
|
|
// @Tags Tenders
|
|
// @Produce json
|
|
// @Param limit query int false "Number of items per page (default: 20, max: 50)"
|
|
// @Param offset query int false "Number of items to skip (default: 0)"
|
|
// @Param country_code query string false "Filter by country code"
|
|
// @Param min_estimated_value query number false "Minimum estimated value"
|
|
// @Param max_estimated_value query number false "Maximum estimated value"
|
|
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
|
// @Failure 400 {object} response.APIResponse
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /api/v1/tenders [get]
|
|
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
|
|
}
|
|
}
|
|
|
|
offset := 0
|
|
if o := c.QueryParam("offset"); o != "" {
|
|
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
|
|
offset = parsed
|
|
}
|
|
}
|
|
|
|
// Build search criteria for public API (only active tenders)
|
|
criteria := TenderSearchCriteria{
|
|
Status: []TenderStatus{TenderStatusActive},
|
|
}
|
|
|
|
if countryCode := c.QueryParam("country_code"); countryCode != "" {
|
|
criteria.CountryCodes = []string{countryCode}
|
|
}
|
|
|
|
// Parse numeric parameters
|
|
if minValue := c.QueryParam("min_estimated_value"); minValue != "" {
|
|
if parsed, err := strconv.ParseFloat(minValue, 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
|
|
}
|
|
}
|
|
|
|
// Get company ID from customer context for automatic matching
|
|
var companyID *string
|
|
if customerCompanyID, ok := c.Get("company_id").(string); ok && customerCompanyID != "" {
|
|
companyID = &customerCompanyID
|
|
}
|
|
|
|
req := ListTendersRequest{
|
|
Criteria: criteria,
|
|
Limit: limit,
|
|
Offset: offset,
|
|
CompanyID: companyID,
|
|
}
|
|
|
|
result, err := h.service.ListTenders(c.Request().Context(), req)
|
|
if err != nil {
|
|
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")
|
|
}
|
|
|
|
// GetPublicTenderDetails retrieves public tender details for mobile app
|
|
// @Summary Get public tender details
|
|
// @Description Retrieve detailed information about a specific tender for mobile application users
|
|
// @Tags Tenders
|
|
// @Produce json
|
|
// @Param id path string true "Tender ID"
|
|
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
|
// @Failure 404 {object} response.APIResponse
|
|
// @Failure 500 {object} response.APIResponse
|
|
// @Router /api/v1/tenders/{id} [get]
|
|
func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error {
|
|
id := c.Param("id")
|
|
if id == "" {
|
|
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
|
}
|
|
|
|
tender, err := h.service.GetTenderByID(c.Request().Context(), id)
|
|
if err != nil {
|
|
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")
|
|
}
|
|
|
|
// parseStringArray parses a comma-separated string into a string array
|
|
func parseStringArray(s string) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
|
|
result := make([]string, 0)
|
|
for _, item := range strings.Split(s, ",") {
|
|
if trimmed := strings.TrimSpace(item); trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|