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") }