Files
tm_back/internal/tender/handler.go
T
2026-05-05 17:47:31 +03:30

474 lines
18 KiB
Go

package tender
import (
"time"
"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 Service
logger logger.Logger
}
// NewHandler creates a new tender handler
func NewHandler(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
// @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/v1/tenders/{id} [get]
// @Security BearerAuth
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.GetByID(c.Request().Context(), id)
if err != nil {
return response.NotFound(c, "Tender not found")
}
return response.Success(c, tender, "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/v1/tenders/{id} [put]
// @Security BearerAuth
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")
}
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.Update(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, "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/v1/tenders/{id} [delete]
// @Security BearerAuth
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.Delete(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 company_id query string false "Company ID for match percentage calculation and sorting"
// @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)"
// @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"
// @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/v1/tenders [get]
// @Security BearerAuth
func (h *TenderHandler) Search(c echo.Context) error {
form, err := response.Parse[SearchForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
// 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
// @Tags Tenders
// @Produce json
// @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)"
// @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"
// @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=SearchResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /api/v1/tenders [get]
func (h *TenderHandler) GetPublicTenders(c echo.Context) error {
form, err := response.Parse[SearchForm](c)
if err != nil {
return response.BadRequest(c, "Invalid request format", "")
}
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, err.Error(), "")
}
form.Status = []string{string(TenderStatusActive)}
form.OnlyActiveDeadlines = true
// Get company ID and customer 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 customerID *string
if customerIDStr, ok := c.Get("customer_id").(string); ok {
customerID = &customerIDStr
}
form.CustomerID = customerID
result, err := h.service.Search(c.Request().Context(), form, response.NewPagination(c))
if err != nil {
return response.InternalServerError(c, "Failed to retrieve tenders")
}
return response.SuccessWithMeta(c, result, result.Metadata, "Tenders retrieved 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 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)"
// @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"
// @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=SearchResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Security BearerAuth
// @Router /api/v1/tenders/recommend [get]
func (h *TenderHandler) RecommendTenders(c echo.Context) error {
form, err := response.Parse[SearchForm](c)
if err != nil {
return response.BadRequest(c, "Invalid request format", "")
}
if _, err := govalidator.ValidateStruct(form); err != nil {
return response.ValidationError(c, err.Error(), "")
}
form.Status = []string{string(TenderStatusActive)}
form.OnlyActiveDeadlines = true
var companyID *string
if customerCompanyID, ok := c.Get("company_id").(string); ok {
companyID = &customerCompanyID
}
form.CompanyID = companyID
var customerID *string
if customerIDStr, ok := c.Get("customer_id").(string); ok {
customerID = &customerIDStr
}
form.CustomerID = customerID
result, err := h.service.Recommend(c.Request().Context(), form, response.NewPagination(c))
if err != nil {
return response.InternalServerError(c, "Failed to retrieve tenders")
}
return response.SuccessWithMeta(c, result, result.Metadata, "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
// @Security BearerAuth
// @Router /api/v1/tenders/details/{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.GetByID(c.Request().Context(), id)
if err != nil {
return response.NotFound(c, "Tender not found")
}
// Only show tenders whose deadlines have not been reached
if tender.TenderDeadline > 0 && tender.TenderDeadline <= time.Now().Unix() {
return response.NotFound(c, "Tender not found")
}
return response.Success(c, tender, "Tender details retrieved successfully")
}
// GetDocumentSummaries retrieves document summaries for a tender
// @Summary Get document summaries for a tender
// @Description Retrieve AI-generated summaries of documents for a specific tender
// @Tags Tenders
// @Produce json
// @Param id path string true "Tender ID"
// @Success 200 {object} response.APIResponse{data=[]DocumentSummary}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/tenders/{id}/document-summaries [get]
func (h *TenderHandler) GetDocumentSummaries(c echo.Context) error {
id := c.Param("id")
if id == "" {
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
}
summaries, err := h.service.GetDocumentSummaries(c.Request().Context(), id)
if err != nil {
h.logger.Error("Failed to get document summaries", map[string]interface{}{
"tender_id": id,
"error": err.Error(),
})
return response.NotFound(c, "Document summaries not found")
}
return response.Success(c, summaries, "Document summaries retrieved successfully")
}
// GetAISummary retrieves the AI-generated overall summary for a tender
// @Summary Get AI-generated tender summary
// @Description Retrieve the AI-generated overall summary for a tender from the AI pipeline storage
// @Tags Admin-Tenders
// @Produce json
// @Param id path string true "Tender ID"
// @Success 200 {object} response.APIResponse{data=AISummaryResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/tenders/{id}/ai-summary [get]
// @Security BearerAuth
func (h *TenderHandler) GetAISummary(c echo.Context) error {
id := c.Param("id")
if id == "" {
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
}
summary, err := h.service.GetAISummary(c.Request().Context(), id)
if err != nil {
h.logger.Error("Failed to get AI summary", map[string]interface{}{
"tender_id": id,
"error": err.Error(),
})
return response.NotFound(c, err.Error())
}
return response.Success(c, summary, "AI summary retrieved successfully")
}
// TriggerAISummarize triggers on-demand AI summarization for a tender
// @Summary Trigger AI summarization
// @Description Trigger on-demand AI summarization for a tender by calling the external AI service
// @Tags Admin-Tenders
// @Produce json
// @Param id path string true "Tender ID"
// @Success 200 {object} response.APIResponse{data=AISummarizeResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/tenders/{id}/ai-summarize [post]
// @Security BearerAuth
func (h *TenderHandler) TriggerAISummarize(c echo.Context) error {
id := c.Param("id")
if id == "" {
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
}
result, err := h.service.TriggerAISummarize(c.Request().Context(), id)
if err != nil {
h.logger.Error("Failed to trigger AI summarization", map[string]interface{}{
"tender_id": id,
"error": err.Error(),
})
return response.InternalServerError(c, err.Error())
}
return response.Success(c, result, "AI summarization completed successfully")
}
// GetPublicAISummary retrieves the AI-generated overall summary for a tender (public endpoint)
// @Summary Get AI-generated tender summary (public)
// @Description Retrieve the AI-generated overall summary for a tender from the AI pipeline storage
// @Tags Tenders
// @Produce json
// @Param id path string true "Tender ID"
// @Success 200 {object} response.APIResponse{data=AISummaryResponse}
// @Failure 400 {object} response.APIResponse
// @Failure 404 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/tenders/{id}/ai-summary [get]
// @Security BearerAuth
func (h *TenderHandler) GetPublicAISummary(c echo.Context) error {
id := c.Param("id")
if id == "" {
return response.BadRequest(c, "Tender ID is required", "ID parameter cannot be empty")
}
summary, err := h.service.GetAISummary(c.Request().Context(), id)
if err != nil {
h.logger.Error("Failed to get AI summary", map[string]interface{}{
"tender_id": id,
"error": err.Error(),
})
return response.NotFound(c, err.Error())
}
return response.Success(c, summary, "AI summary retrieved successfully")
}
// GetAllAISummaries retrieves all AI-generated summaries from storage
// @Summary Get all AI-generated tender summaries
// @Description Retrieve all AI-generated summaries from the AI pipeline storage, including pending and completed ones
// @Tags Admin-Tenders
// @Produce json
// @Success 200 {object} response.APIResponse{data=[]ai_summarizer.TenderSummaryItem}
// @Failure 500 {object} response.APIResponse
// @Router /admin/v1/tenders/ai-summaries [get]
// @Security BearerAuth
func (h *TenderHandler) GetAllAISummaries(c echo.Context) error {
summaries, err := h.service.GetAllAISummaries(c.Request().Context())
if err != nil {
h.logger.Error("Failed to get all AI summaries", map[string]interface{}{
"error": err.Error(),
})
return response.InternalServerError(c, err.Error())
}
return response.Success(c, summaries, "AI summaries retrieved successfully")
}