Refactor Tender Management API Endpoints and Update Documentation
- Renamed and restructured tender-related API endpoints for improved clarity and consistency, including changing the route from `/admin/tenders` to `/admin/v1/companies` and updating the associated methods. - Introduced a new SearchForm structure for listing tenders with advanced filtering capabilities, enhancing the API's search functionality. - Updated the service and repository layers to align with the new endpoint structure, ensuring proper handling of tender data. - Enhanced Swagger and YAML documentation to reflect the changes in endpoint structure, including detailed descriptions and examples for the new search functionality. - Improved error handling and response structures to provide clearer feedback to API consumers, ensuring a more robust and user-friendly experience.
This commit is contained in:
+66
-195
@@ -1,8 +1,6 @@
|
||||
package tender
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"tm/pkg/logger"
|
||||
"tm/pkg/response"
|
||||
|
||||
@@ -12,18 +10,22 @@ import (
|
||||
|
||||
// TenderHandler handles tender-related HTTP requests
|
||||
type TenderHandler struct {
|
||||
service TenderService
|
||||
service Service
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewTenderHandler creates a new tender handler
|
||||
func NewTenderHandler(service TenderService, logger logger.Logger) *TenderHandler {
|
||||
func NewTenderHandler(service Service, logger logger.Logger) *TenderHandler {
|
||||
return &TenderHandler{
|
||||
service: service,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************
|
||||
// * ADMIN HANDLERS *
|
||||
// *****************************************************
|
||||
|
||||
// GetTender retrieves a tender by ID
|
||||
// @Summary Get tender by ID
|
||||
// @Description Retrieve a specific tender by its ID
|
||||
@@ -33,15 +35,15 @@ func NewTenderHandler(service TenderService, logger logger.Logger) *TenderHandle
|
||||
// @Success 200 {object} response.APIResponse{data=TenderResponse}
|
||||
// @Failure 404 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/tenders/{id} [get]
|
||||
// @Router /admin/v1/tenders/{id} [get]
|
||||
// @Security BearerAuth
|
||||
func (h *TenderHandler) GetTender(c echo.Context) error {
|
||||
func (h *TenderHandler) Get(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)
|
||||
tender, err := h.service.GetByID(c.Request().Context(), id)
|
||||
if err != nil {
|
||||
return response.NotFound(c, "Tender not found")
|
||||
}
|
||||
@@ -61,9 +63,9 @@ func (h *TenderHandler) GetTender(c echo.Context) error {
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 404 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/tenders/{id} [put]
|
||||
// @Router /admin/v1/tenders/{id} [put]
|
||||
// @Security BearerAuth
|
||||
func (h *TenderHandler) UpdateTender(c echo.Context) error {
|
||||
func (h *TenderHandler) Update(c echo.Context) error {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
||||
@@ -80,7 +82,7 @@ func (h *TenderHandler) UpdateTender(c echo.Context) error {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
tender, err := h.service.UpdateTender(c.Request().Context(), req)
|
||||
tender, err := h.service.Update(c.Request().Context(), req)
|
||||
if err != nil {
|
||||
if err.Error() == "tender not found" {
|
||||
return response.NotFound(c, "Tender not found")
|
||||
@@ -88,7 +90,7 @@ func (h *TenderHandler) UpdateTender(c echo.Context) error {
|
||||
return response.InternalServerError(c, "Failed to update tender")
|
||||
}
|
||||
|
||||
return response.Success(c, tender.ToTenderResponse(), "Tender updated successfully")
|
||||
return response.Success(c, tender, "Tender updated successfully")
|
||||
}
|
||||
|
||||
// DeleteTender deletes a tender
|
||||
@@ -99,15 +101,15 @@ func (h *TenderHandler) UpdateTender(c echo.Context) error {
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 404 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/tenders/{id} [delete]
|
||||
// @Router /admin/v1/tenders/{id} [delete]
|
||||
// @Security BearerAuth
|
||||
func (h *TenderHandler) DeleteTender(c echo.Context) error {
|
||||
func (h *TenderHandler) Delete(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)
|
||||
err := h.service.Delete(c.Request().Context(), id)
|
||||
if err != nil {
|
||||
if err.Error() == "tender not found" {
|
||||
return response.NotFound(c, "Tender not found")
|
||||
@@ -123,10 +125,8 @@ func (h *TenderHandler) DeleteTender(c echo.Context) error {
|
||||
// @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 q 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)"
|
||||
@@ -135,90 +135,45 @@ func (h *TenderHandler) DeleteTender(c echo.Context) error {
|
||||
// @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"
|
||||
// @Param deadline_from query number false "Deadline from"
|
||||
// @Param deadline_to query number false "Deadline to"
|
||||
// @Param publication_date_from query number false "Publication date from"
|
||||
// @Param publication_date_to query number false "Publication date to"
|
||||
// @Param languages query []string false "Filter by languages (comma-separated)"
|
||||
// @Param buyer_organization_id query string false "Filter by buyer organization ID"
|
||||
// @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 sort_by query string false "Sort by field"
|
||||
// @Param sort_order query string false "Sort order (asc or desc)"
|
||||
// @Success 200 {object} response.APIResponse{data=map[string]interface{}}
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/tenders [get]
|
||||
// @Router /admin/v1/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)
|
||||
func (h *TenderHandler) Search(c echo.Context) error {
|
||||
form, err := response.Parse[SearchForm](c)
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve tenders")
|
||||
return response.ValidationError(c, "Invalid request data", err.Error())
|
||||
}
|
||||
|
||||
return response.Success(c, result, "Tenders retrieved successfully")
|
||||
// Validate form
|
||||
if _, err := govalidator.ValidateStruct(form); err != nil {
|
||||
return response.ValidationError(c, "Validation failed", err.Error())
|
||||
}
|
||||
|
||||
// Call service
|
||||
tenders, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to list tenders")
|
||||
}
|
||||
|
||||
return response.SuccessWithMeta(c, tenders, tenders.Metadata, "Tenders retrieved successfully")
|
||||
}
|
||||
|
||||
// *****************************************************
|
||||
// * PUBLIC HANDLERS *
|
||||
// *****************************************************
|
||||
|
||||
// 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
|
||||
@@ -237,64 +192,26 @@ func (h *TenderHandler) ListTenders(c echo.Context) error {
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/tenders [get]
|
||||
func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
|
||||
// Parse pagination parameters (more restrictive for public API)
|
||||
limit := 20
|
||||
if parsed, err := strconv.Atoi(c.QueryParam("limit")); err == nil && parsed > 0 && parsed <= 50 {
|
||||
limit = parsed
|
||||
|
||||
form, err := response.Parse[SearchForm](c)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", "")
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if parsed, err := strconv.Atoi(c.QueryParam("offset")); err == nil && parsed >= 0 {
|
||||
offset = parsed
|
||||
if _, err := govalidator.ValidateStruct(form); err != nil {
|
||||
return response.ValidationError(c, err.Error(), "")
|
||||
}
|
||||
|
||||
// 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 parsed, err := strconv.ParseFloat(c.QueryParam("min_estimated_value"), 64); err == nil {
|
||||
criteria.MinEstimatedValue = &parsed
|
||||
}
|
||||
|
||||
if parsed, err := strconv.ParseFloat(c.QueryParam("max_estimated_value"), 64); err == nil {
|
||||
criteria.MaxEstimatedValue = &parsed
|
||||
}
|
||||
form.Status = []string{string(TenderStatusActive)}
|
||||
|
||||
// Get company ID from customer context for automatic matching
|
||||
var companyID *string
|
||||
if customerCompanyID, ok := c.Get("company_id").(string); ok {
|
||||
companyID = &customerCompanyID
|
||||
}
|
||||
form.CompanyID = companyID
|
||||
|
||||
var from *int64
|
||||
if fromParam := c.QueryParam("publication_from"); fromParam != "" {
|
||||
if parsed, err := strconv.ParseInt(fromParam, 10, 64); err == nil {
|
||||
from = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
var to *int64
|
||||
if toParam := c.QueryParam("publication_to"); toParam != "" {
|
||||
if parsed, err := strconv.ParseInt(toParam, 10, 64); err == nil {
|
||||
to = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
req := ListTendersRequest{
|
||||
Criteria: criteria,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
CompanyID: companyID,
|
||||
From: from,
|
||||
To: to,
|
||||
}
|
||||
result, err := h.service.ListTenders(c.Request().Context(), req)
|
||||
result, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve tenders")
|
||||
}
|
||||
@@ -320,56 +237,26 @@ func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/tenders/recommend [get]
|
||||
func (h *TenderHandler) RecommendTenders(c echo.Context) error {
|
||||
// Parse pagination parameters (more restrictive for public API)
|
||||
limit := 20
|
||||
if parsed, err := strconv.Atoi(c.QueryParam("limit")); err == nil && parsed > 0 && parsed <= 50 {
|
||||
limit = parsed
|
||||
|
||||
form, err := response.Parse[SearchForm](c)
|
||||
if err != nil {
|
||||
return response.BadRequest(c, "Invalid request format", "")
|
||||
}
|
||||
|
||||
offset := 0
|
||||
if parsed, err := strconv.Atoi(c.QueryParam("offset")); err == nil && parsed >= 0 {
|
||||
offset = parsed
|
||||
if _, err := govalidator.ValidateStruct(form); err != nil {
|
||||
return response.ValidationError(c, err.Error(), "")
|
||||
}
|
||||
|
||||
// Build search criteria for public API (only active tenders)
|
||||
criteria := TenderSearchCriteria{
|
||||
Status: []TenderStatus{TenderStatusActive},
|
||||
}
|
||||
form.Status = []string{string(TenderStatusActive)}
|
||||
|
||||
if countryCode := c.QueryParam("country_code"); countryCode != "" {
|
||||
criteria.CountryCodes = []string{countryCode}
|
||||
}
|
||||
|
||||
// Parse numeric parameters
|
||||
if parsed, err := strconv.ParseFloat(c.QueryParam("min_estimated_value"), 64); err == nil {
|
||||
criteria.MinEstimatedValue = &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 {
|
||||
companyID = &customerCompanyID
|
||||
}
|
||||
|
||||
var from *int64
|
||||
if fromParam := c.QueryParam("publication_from"); fromParam != "" {
|
||||
if parsed, err := strconv.ParseInt(fromParam, 10, 64); err == nil {
|
||||
from = &parsed
|
||||
}
|
||||
}
|
||||
form.CompanyID = companyID
|
||||
|
||||
var to *int64
|
||||
if toParam := c.QueryParam("publication_to"); toParam != "" {
|
||||
if parsed, err := strconv.ParseInt(toParam, 10, 64); err == nil {
|
||||
to = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.service.RecommendTenders(c.Request().Context(), companyID, from, to, limit, offset)
|
||||
result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c))
|
||||
if err != nil {
|
||||
return response.InternalServerError(c, "Failed to retrieve tenders")
|
||||
}
|
||||
@@ -394,26 +281,10 @@ func (h *TenderHandler) GetPublicTenderDetails(c echo.Context) error {
|
||||
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
|
||||
}
|
||||
|
||||
tender, err := h.service.GetTenderByID(c.Request().Context(), id)
|
||||
tender, err := h.service.GetByID(c.Request().Context(), id)
|
||||
if err != nil {
|
||||
return response.NotFound(c, "Tender not found")
|
||||
}
|
||||
|
||||
return response.Success(c, tender, "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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user